@@ -8,21 +8,36 @@ module Development.IDE.Import.DependencyInformation
88 , NodeError(..)
99 , ModuleParseError(..)
1010 , TransitiveDependencies(..)
11+ , FilePathId(..)
12+
13+ , PathIdMap
14+ , emptyPathIdMap
15+ , getPathId
16+ , insertImport
17+ , pathToId
18+ , idToPath
19+ , reachableModules
20+
1121 , processDependencyInformation
1222 , transitiveDeps
1323 ) where
1424
1525import Control.DeepSeq
1626import Data.Bifunctor
27+import Data.Coerce
1728import Data.List
1829import Development.IDE.GHC.Orphans()
1930import Data.Either
2031import Data.Graph
2132import Data.List.NonEmpty (NonEmpty(..), nonEmpty)
2233import qualified Data.List.NonEmpty as NonEmpty
34+import Data.IntMap (IntMap)
35+import qualified Data.IntMap.Strict as IntMap
36+import qualified Data.IntMap.Lazy as IntMapLazy
37+import Data.IntSet (IntSet)
38+import qualified Data.IntSet as IntSet
2339import Data.Map (Map)
2440import qualified Data.Map.Strict as MS
25−import qualified Data.Map.Lazy as ML
2641import Data.Maybe
2742import Data.Set (Set)
2843import qualified Data.Set as Set
@@ -37,32 +52,78 @@ import Module
3752
3853-- | The imports for a given module.
3954data ModuleImports = ModuleImports
40− { moduleImports :: ![(Located ModuleName, Maybe NormalizedFilePath)]
55+ { moduleImports :: ![(Located ModuleName, Maybe FilePathId)]
4156 -- ^ Imports of a module in the current package and the file path of
4257 -- that module on disk (if we found it)
4358 , packageImports :: !(Set InstalledUnitId)
4459 -- ^ Transitive package dependencies unioned for all imports.
4560 }
4661
62+-- | For processing dependency information, we need lots of maps and sets
63+-- of filepaths. Comparing Strings is really slow, so we work with IntMap/IntSet
64+-- instead and only convert at the edges
65+-- and
66+newtype FilePathId = FilePathId { getFilePathId :: Int }
67+ deriving (Show, NFData, Eq, Ord)
68+
69+data PathIdMap = PathIdMap
70+ { idToPathMap :: !(IntMap NormalizedFilePath)
71+ , pathToIdMap :: !(Map NormalizedFilePath FilePathId)
72+ }
73+ deriving (Show, Generic)
74+
75+instance NFData PathIdMap
76+
77+emptyPathIdMap :: PathIdMap
78+emptyPathIdMap = PathIdMap IntMap.empty MS.empty
79+
80+getPathId :: NormalizedFilePath -> PathIdMap -> (FilePathId, PathIdMap)
81+getPathId path m@PathIdMap{..} =
82+ case MS.lookup path pathToIdMap of
83+ Nothing ->
84+ let !newId = FilePathId $ MS.size pathToIdMap
85+ in (newId, insertPathId path newId m)
86+ Just id -> (id, m)
87+
88+insertPathId :: NormalizedFilePath -> FilePathId -> PathIdMap -> PathIdMap
89+insertPathId path id PathIdMap{..} =
90+ PathIdMap (IntMap.insert (getFilePathId id) path idToPathMap) (MS.insert path id pathToIdMap)
91+
92+insertImport :: FilePathId -> Either ModuleParseError ModuleImports -> RawDependencyInformation -> RawDependencyInformation
93+insertImport (FilePathId k) v rawDepInfo = rawDepInfo { rawImports = IntMap.insert k v (rawImports rawDepInfo) }
94+
95+pathToId :: PathIdMap -> NormalizedFilePath -> FilePathId
96+pathToId PathIdMap{pathToIdMap} path = pathToIdMap MS.! path
97+
98+idToPath :: PathIdMap -> FilePathId -> NormalizedFilePath
99+idToPath PathIdMap{idToPathMap} (FilePathId id) = idToPathMap IntMap.! id
100+
47101-- | Unprocessed results that we find by following imports recursively.
48−newtype RawDependencyInformation = RawDependencyInformation
49− { getRawDeps :: Map NormalizedFilePath (Either ModuleParseError ModuleImports)
102+data RawDependencyInformation = RawDependencyInformation
103+ { rawImports :: !(IntMap (Either ModuleParseError ModuleImports))
104+ , rawPathIdMap :: !PathIdMap
50105 }
51106
52−pkgDependencies :: RawDependencyInformation -> Map NormalizedFilePath (Set InstalledUnitId)
53−pkgDependencies (RawDependencyInformation m) = MS.map (either (const Set.empty) packageImports) m
107+pkgDependencies :: RawDependencyInformation -> IntMap (Set InstalledUnitId)
108+pkgDependencies RawDependencyInformation{..} =
109+ IntMap.map (either (const Set.empty) packageImports) rawImports
54110
55111data DependencyInformation =
56112 DependencyInformation
57− { depErrorNodes :: Map NormalizedFilePath (NonEmpty NodeError)
113+ { depErrorNodes :: !(IntMap (NonEmpty NodeError))
58114 -- ^ Nodes that cannot be processed correctly.
59− , depModuleDeps :: Map NormalizedFilePath (Set NormalizedFilePath)
115+ , depModuleDeps :: !(IntMap IntSet)
60116 -- ^ For a non-error node, this contains the set of module immediate dependencies
61117 -- in the same package.
62− , depPkgDeps :: Map NormalizedFilePath (Set InstalledUnitId)
118+ , depPkgDeps :: !(IntMap (Set InstalledUnitId))
63119 -- ^ For a non-error node, this contains the set of immediate pkg deps.
120+ , depPathIdMap :: !PathIdMap
64121 } deriving (Show, Generic)
65122
123+reachableModules :: DependencyInformation -> [NormalizedFilePath]
124+reachableModules DependencyInformation{..} =
125+ map (idToPath depPathIdMap . FilePathId) $ IntMap.keys depErrorNodes <> IntMap.keys depModuleDeps
126+
66127instance NFData DependencyInformation
67128
68129-- | This does not contain the actual parse error as that is already reported by GetParsedModule.
@@ -79,7 +140,7 @@ instance NFData LocateError
79140
80141-- | An error attached to a node in the dependency graph.
81142data NodeError
82− = PartOfCycle (Located ModuleName) [NormalizedFilePath]
143+ = PartOfCycle (Located ModuleName) [FilePathId]
83144 -- ^ This module is part of an import cycle. The module name corresponds
84145 -- to the import that enters the cycle starting from this module.
85146 -- The list of filepaths represents the elements
@@ -104,12 +165,12 @@ instance NFData NodeError where
104165-- `ErrorNode`. Otherwise it is a `SuccessNode`.
105166data NodeResult
106167 = ErrorNode (NonEmpty NodeError)
107− | SuccessNode [(Located ModuleName, NormalizedFilePath)]
168+ | SuccessNode [(Located ModuleName, FilePathId)]
108169 deriving Show
109170
110171partitionNodeResults
111172 :: [(a, NodeResult)]
112− -> ([(a, NonEmpty NodeError)], [(a, [(Located ModuleName, NormalizedFilePath)])])
173+ -> ([(a, NonEmpty NodeError)], [(a, [(Located ModuleName, FilePathId)])])
113174partitionNodeResults = partitionEithers . map f
114175 where f (a, ErrorNode errs) = Left (a, errs)
115176 f (a, SuccessNode imps) = Right (a, imps)
@@ -121,40 +182,41 @@ instance Semigroup NodeResult where
121182 SuccessNode a <> SuccessNode _ = SuccessNode a
122183
123184processDependencyInformation :: RawDependencyInformation -> DependencyInformation
124−processDependencyInformation rawResults =
185+processDependencyInformation rawDepInfo@RawDependencyInformation{..} =
125186 DependencyInformation
126− { depErrorNodes = MS.fromList errorNodes
187+ { depErrorNodes = IntMap.fromList errorNodes
127188 , depModuleDeps = moduleDeps
128− , depPkgDeps = pkgDependencies rawResults
189+ , depPkgDeps = pkgDependencies rawDepInfo
190+ , depPathIdMap = rawPathIdMap
129191 }
130− where resultGraph = buildResultGraph rawResults
131− (errorNodes, successNodes) = partitionNodeResults $ MS.toList resultGraph
132− successEdges :: [(NormalizedFilePath, NormalizedFilePath, [NormalizedFilePath])]
192+ where resultGraph = buildResultGraph rawImports
193+ (errorNodes, successNodes) = partitionNodeResults $ IntMap.toList resultGraph
194+ successEdges :: [(FilePathId, FilePathId, [FilePathId])]
133195 successEdges =
134− map (\(file, imports) -> (file, file, map snd imports)) successNodes
196+ map (\(file, imports) -> (FilePathId file, FilePathId file, map snd imports)) successNodes
135197 moduleDeps =
136− MS.fromList $ map (\(_, v, vs) -> (v, Set.fromList vs)) successEdges
198+ IntMap.fromList $ map (\(_, FilePathId v, vs) -> (v, IntSet.fromList $ coerce vs)) successEdges
137199
138200-- | Given a dependency graph, buildResultGraph detects and propagates errors in that graph as follows:
139201-- 1. Mark each node that is part of an import cycle as an error node.
140202-- 2. Mark each node that has a parse error as an error node.
141203-- 3. Mark each node whose immediate children could not be located as an error.
142204-- 4. Recursively propagate errors to parents if they are not already error nodes.
143−buildResultGraph :: RawDependencyInformation -> Map NormalizedFilePath NodeResult
205+buildResultGraph :: IntMap (Either ModuleParseError ModuleImports) -> IntMap NodeResult
144206buildResultGraph g = propagatedErrors
145207 where
146208 sccs = stronglyConnComp (graphEdges g)
147209 (_, cycles) = partitionSCC sccs
148− cycleErrors :: Map NormalizedFilePath NodeResult
149− cycleErrors = MS.unionsWith (<>) $ map errorsForCycle cycles
150− errorsForCycle :: [NormalizedFilePath] -> Map NormalizedFilePath NodeResult
210+ cycleErrors :: IntMap NodeResult
211+ cycleErrors = IntMap.unionsWith (<>) $ map errorsForCycle cycles
212+ errorsForCycle :: [FilePathId] -> IntMap NodeResult
151213 errorsForCycle files =
152− MS.fromListWith (<>) (concatMap (cycleErrorsForFile files) files)
153− cycleErrorsForFile :: [NormalizedFilePath] -> NormalizedFilePath -> [(NormalizedFilePath,NodeResult)]
214+ IntMap.fromListWith (<>) $ coerce $ concatMap (cycleErrorsForFile files) files
215+ cycleErrorsForFile :: [FilePathId] -> FilePathId -> [(FilePathId,NodeResult)]
154216 cycleErrorsForFile cycle f =
155217 let entryPoints = mapMaybe (findImport f) cycle
156218 in map (\imp -> (f, ErrorNode (PartOfCycle imp cycle :| []))) entryPoints
157− otherErrors = MS.map otherErrorsForFile (getRawDeps g)
219+ otherErrors = IntMap.map otherErrorsForFile g
158220 otherErrorsForFile :: Either ModuleParseError ModuleImports -> NodeResult
159221 otherErrorsForFile (Left err) = ErrorNode (ParseError err :| [])
160222 otherErrorsForFile (Right ModuleImports{moduleImports}) =
@@ -165,32 +227,32 @@ buildResultGraph g = propagatedErrors
165227 Nothing -> SuccessNode imports'
166228 Just errs' -> ErrorNode (NonEmpty.map FailedToLocateImport errs')
167229
168− unpropagatedErrors = MS.unionWith (<>) cycleErrors otherErrors
230+ unpropagatedErrors = IntMap.unionWith (<>) cycleErrors otherErrors
169231 -- The recursion here is fine since we use a lazy map and
170232 -- we only recurse on SuccessNodes. In particular, we do not recurse
171233 -- on nodes that are part of a cycle as they are already marked as
172234 -- error nodes.
173235 propagatedErrors =
174− ML.map propagate unpropagatedErrors
236+ IntMapLazy.map propagate unpropagatedErrors
175237 propagate :: NodeResult -> NodeResult
176238 propagate n@(ErrorNode _) = n
177239 propagate n@(SuccessNode imps) =
178− let results = map (\(imp, dep) -> (imp, propagatedErrors MS.! dep)) imps
240+ let results = map (\(imp, FilePathId dep) -> (imp, propagatedErrors IntMap.! dep)) imps
179241 (errs, _) = partitionNodeResults results
180242 in case nonEmpty errs of
181243 Nothing -> n
182244 Just errs' -> ErrorNode (NonEmpty.map (ParentOfErrorNode . fst) errs')
183− findImport :: NormalizedFilePath -> NormalizedFilePath -> Maybe (Located ModuleName)
184− findImport file importedFile =
185− case getRawDeps g MS.! file of
245+ findImport :: FilePathId -> FilePathId -> Maybe (Located ModuleName)
246+ findImport (FilePathId file) importedFile =
247+ case g IntMap.! file of
186248 Left _ -> error "Tried to call findImport on a module with a parse error"
187249 Right ModuleImports{moduleImports} ->
188250 fmap fst $ find (\(_, resolvedImp) -> resolvedImp == Just importedFile) moduleImports
189251
190−graphEdges :: RawDependencyInformation -> [(NormalizedFilePath, NormalizedFilePath, [NormalizedFilePath])]
252+graphEdges :: IntMap (Either ModuleParseError ModuleImports) -> [(FilePathId, FilePathId, [FilePathId])]
191253graphEdges g =
192− map (\(k, v) -> (k, k, deps v)) $ MS.toList $ getRawDeps g
193− where deps :: Either e ModuleImports -> [NormalizedFilePath]
254+ map (\(k, v) -> (FilePathId k, FilePathId k, deps v)) $ IntMap.toList g
255+ where deps :: Either e ModuleImports -> [FilePathId]
194256 deps (Left _) = []
195257 deps (Right ModuleImports{moduleImports}) = mapMaybe snd moduleImports
196258
@@ -200,12 +262,20 @@ partitionSCC (AcyclicSCC x:rest) = first (x:) $ partitionSCC rest
200262partitionSCC [] = ([], [])
201263
202264transitiveDeps :: DependencyInformation -> NormalizedFilePath -> Maybe TransitiveDependencies
203−transitiveDeps DependencyInformation{..} f = do
204− reachableVs <- Set.delete f . Set.fromList . map (fst3 . fromVertex) . reachable g <$> toVertex f
205− let transitiveModuleDeps = filter (\v -> v `Set.member` reachableVs) $ map (fst3 . fromVertex) vs
206− let transitivePkgDeps = Set.toList $ Set.unions $ map (\f -> MS.findWithDefault Set.empty f depPkgDeps) (f : transitiveModuleDeps)
265+transitiveDeps DependencyInformation{..} file = do
266+ let !fileId = pathToId depPathIdMap file
267+ reachableVs <-
268+ IntSet.delete (getFilePathId fileId) .
269+ IntSet.fromList . map (fst3 . fromVertex) .
270+ reachable g <$> toVertex (getFilePathId fileId)
271+ let transitiveModuleDepIds = filter (\v -> v `IntSet.member` reachableVs) $ map (fst3 . fromVertex) vs
272+ let transitivePkgDeps =
273+ Set.toList $ Set.unions $
274+ map (\f -> IntMap.findWithDefault Set.empty f depPkgDeps) $
275+ getFilePathId fileId : transitiveModuleDepIds
276+ let transitiveModuleDeps = map (idToPath depPathIdMap . FilePathId) transitiveModuleDepIds
207277 pure TransitiveDependencies {..}
208− where (g, fromVertex, toVertex) = graphFromEdges (map (\(f, fs) -> (f, f, Set.toList fs)) $ MS.toList depModuleDeps)
278+ where (g, fromVertex, toVertex) = graphFromEdges (map (\(f, fs) -> (f, f, IntSet.toList fs)) $ IntMap.toList depModuleDeps)
209279 vs = topSort g
210280
211281data TransitiveDependencies = TransitiveDependencies