Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parallel type-checking in compilation (behind a feature flag) #14494

Merged
merged 88 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
0c5c1ff
Squashed commit of the following:
safesparrow Dec 19, 2022
7f69062
Code clean up after initial draft PR.
nojaf Dec 20, 2022
30a0081
Revert fscmain.fs
nojaf Dec 20, 2022
95d8024
Revert more unwanted changes.
nojaf Dec 20, 2022
c496359
Surface area for now.
nojaf Dec 20, 2022
c57acf2
Mark GraphChecking as internal.
nojaf Jan 3, 2023
9c6b5f1
Mark helper functions in unit tests as private.
nojaf Jan 3, 2023
5666202
Mark Scenarios as internal.
nojaf Jan 3, 2023
a4dfa8f
Make Scenario public and use TestFileWithAST.
nojaf Jan 3, 2023
16e5614
Use Graph when FSHARP_EXPERIMENTAL_FEATURES is set.
nojaf Jan 3, 2023
f223333
Merge branch 'main' into graph-tc-new
vzarytovskii Jan 3, 2023
244bbaa
Always depend on prim-types-prelude.fsi for F# Core.
nojaf Jan 3, 2023
81fb7d1
Add CI leg for GraphBasedCheckingOn.
nojaf Jan 3, 2023
cb1a22f
Merge branch 'graph-tc-new' of https://github.com/safesparrow/fsharp …
nojaf Jan 3, 2023
cc0aaba
Remove unused members of TcState.
nojaf Jan 4, 2023
0fabdd3
Trigger CI
nojaf Jan 4, 2023
992b9c0
Scope additional links in F# core to files between prim-types-prelude…
nojaf Jan 5, 2023
8bd8cd4
Merge branch 'main' into graph-tc-new
nojaf Jan 12, 2023
08e5e7e
Workaround for Set.empty.
nojaf Jan 12, 2023
af21780
Merge branch 'main' into graph-tc-new
nojaf Jan 12, 2023
2378cf9
Remove --test:ParallelCheckingWithSignatureFilesOn
nojaf Jan 12, 2023
5510383
Wrap lambda's in types.
nojaf Jan 12, 2023
60fe3d1
Update type name and add some more comments.
nojaf Jan 12, 2023
79f487c
Move comment to type definition.
nojaf Jan 12, 2023
649727b
Always depend on prim-types-prelude.fsi in FSharp.Core.
nojaf Jan 12, 2023
53265d0
Remove ParaChecking
nojaf Jan 12, 2023
c065a12
Merge branch 'main' into graph-tc-new
nojaf Jan 13, 2023
c09a49b
Update surface area
nojaf Jan 13, 2023
a74651f
Removed unused setting in VS
nojaf Jan 13, 2023
c39c5ac
Merge branch 'main' into graph-tc-new
nojaf Jan 16, 2023
b708e4e
Exclude graph mode for interactive compilation.
nojaf Jan 16, 2023
fcf8c70
Pipe collections into module functions.
nojaf Jan 16, 2023
fd46607
Update SurfaceArea in Release mode 🙃
nojaf Jan 16, 2023
72cdcfd
Add signature files for GraphChecking modules.
nojaf Jan 16, 2023
01a7887
Add warning when AutoOpenAttribute is being aliased.
nojaf Jan 16, 2023
8ad542a
Initial comparison benchmark.
nojaf Jan 17, 2023
d23922c
Check if CompiledRepresentation is Named.
nojaf Jan 18, 2023
3efaf23
Remove trailing space.
nojaf Jan 18, 2023
1a78368
Excluded erased types.
nojaf Jan 18, 2023
6900373
Dump graph as mermaid diagram.
nojaf Jan 18, 2023
95a0e9b
Missing files.
nojaf Jan 18, 2023
0692914
Make alias types internal.
nojaf Jan 18, 2023
3cf87c8
SurfaceArea!
nojaf Jan 18, 2023
a942941
Merge branch 'main' into graph-tc-new
nojaf Jan 19, 2023
1ddcbc1
Merge branch 'main' into graph-tc-new
vzarytovskii Jan 19, 2023
9f5195b
Merge upstream/main
safesparrow Jan 20, 2023
b960811
Fix build - remove unused symbols.
safesparrow Jan 20, 2023
b80df24
Auto-disable graph mode in deterministic build
safesparrow Jan 20, 2023
aa56edc
Merge branch 'main' into graph-tc-new
nojaf Jan 24, 2023
d6ded54
Merge branch 'main' into graph-tc-new
nojaf Jan 24, 2023
6f0aa50
Merge branch 'main' into graph-tc-new
nojaf Jan 24, 2023
479b66a
A few local refactorings + update comments
safesparrow Jan 25, 2023
ea16fd7
Use TcGlobals for AutoOpenAttribute reference. Assert exitCode of 0 i…
safesparrow Jan 25, 2023
362b25c
Merge branch 'graph-tc-new' of https://github.com/safesparrow/fsharp …
safesparrow Jan 25, 2023
9b0ed15
Do not match on `TType` directly
safesparrow Jan 26, 2023
72279dd
Merge branch 'main' into graph-tc-new
nojaf Jan 31, 2023
e520e2e
Add comment on why we warn when user aliases the AutoOpenAttribute.
nojaf Jan 31, 2023
88e5ee6
Capitalize type parameter names.
nojaf Jan 31, 2023
9abdd77
Prefer array comprehension expression.
nojaf Feb 1, 2023
d2e4354
Refactor cryptic abbreviations.
nojaf Feb 1, 2023
b103d2b
Refactor List.collect id to List.concat.
nojaf Feb 1, 2023
d8c0ee9
Show scrape.fsx in IDE.
nojaf Feb 1, 2023
8a6bb9f
Prefer array comprehension expression.
nojaf Feb 1, 2023
29ae5f2
Prefer list comprehension expression.
nojaf Feb 1, 2023
e94d0d0
Refactor cryptic function into a helper module.
nojaf Feb 1, 2023
52eee66
Refactor List.collect with lambda to list comprehension expression.
nojaf Feb 1, 2023
e4d69eb
Refactor List.collect id to List.concat.
nojaf Feb 1, 2023
8545b92
Remove with in type.
nojaf Feb 1, 2023
c66ea08
Remove unnecessary open.
nojaf Feb 1, 2023
aa8adc7
Use `||>`
nojaf Feb 1, 2023
a23588f
Don't use pipe-to-fun.
nojaf Feb 1, 2023
be2bc12
Remove unused CheckArgs.
nojaf Feb 1, 2023
006f562
Merge branch 'main' into graph-tc-new
nojaf Feb 2, 2023
154614c
Merge branch 'main' into graph-tc-new
nojaf Feb 2, 2023
a1a8eb1
Add some basic regression tests for the Graph module.
nojaf Feb 2, 2023
3c9fa6b
Use FileSystemUtils helper instead of direct Path.GetFileName access.
nojaf Feb 2, 2023
6272b5f
Merge branch 'main' into graph-tc-new
nojaf Feb 4, 2023
be5a612
Merge branch 'main' into graph-tc-new
nojaf Feb 6, 2023
742c7b0
Merge branch 'main' into graph-tc-new
nojaf Feb 6, 2023
6221c2a
Merge branch 'main' into graph-tc-new
nojaf Feb 6, 2023
20a4f9d
Merge branch 'main' into graph-tc-new
nojaf Feb 7, 2023
1e070b6
Merge branch 'main' into graph-tc-new
nojaf Feb 8, 2023
07c8cb6
Clean up after code review.
nojaf Feb 8, 2023
586b01a
Update algorithm info in Docs.md
nojaf Feb 8, 2023
5256c78
Update Docs.md
safesparrow Feb 8, 2023
e69f85b
Add concatenate to Continuation module.
nojaf Feb 9, 2023
27e6916
Use F# set instead of HashSet.
nojaf Feb 9, 2023
a03a66a
Merge branch 'graph-tc-new' of https://github.com/safesparrow/fsharp …
nojaf Feb 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/Compiler/Driver/CompilerConfig.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ let FSharpScriptFileSuffixes = [ ".fsscript"; ".fsx" ]
let FSharpIndentationAwareSyntaxFileSuffixes =
[ ".fs"; ".fsscript"; ".fsx"; ".fsi" ]

let FsharpExperimentalFeaturesEnabledAutomatically =
Environment.GetEnvironmentVariable("FSHARP_EXPERIMENTAL_FEATURES") |> isNotNull
safesparrow marked this conversation as resolved.
Show resolved Hide resolved

//--------------------------------------------------------------------------
// General file name resolver
//--------------------------------------------------------------------------
Expand Down Expand Up @@ -393,6 +396,15 @@ type ParallelReferenceResolution =
| On
| Off

[<RequireQualifiedAccess>]
type TypeCheckingMode =
safesparrow marked this conversation as resolved.
Show resolved Hide resolved
| Sequential
| ParallelCheckingOfBackedImplFiles
| Graph

[<RequireQualifiedAccess>]
type TypeCheckingConfig = { Mode: TypeCheckingMode }

[<NoEquality; NoComparison>]
type TcConfigBuilder =
{
Expand Down Expand Up @@ -507,7 +519,6 @@ type TcConfigBuilder =
mutable emitTailcalls: bool
mutable deterministic: bool
mutable concurrentBuild: bool
mutable parallelCheckingWithSignatureFiles: bool
mutable emitMetadataAssembly: MetadataAssemblyGeneration
mutable preferredUiLang: string option
mutable lcid: int option
Expand Down Expand Up @@ -588,6 +599,8 @@ type TcConfigBuilder =
mutable exiter: Exiter

mutable parallelReferenceResolution: ParallelReferenceResolution

mutable typeCheckingConfig: TypeCheckingConfig
}

// Directories to start probing in
Expand Down Expand Up @@ -734,7 +747,6 @@ type TcConfigBuilder =
emitTailcalls = true
deterministic = false
concurrentBuild = true
parallelCheckingWithSignatureFiles = false
emitMetadataAssembly = MetadataAssemblyGeneration.None
preferredUiLang = None
lcid = None
Expand Down Expand Up @@ -777,6 +789,14 @@ type TcConfigBuilder =
xmlDocInfoLoader = None
exiter = QuitProcessExiter
parallelReferenceResolution = ParallelReferenceResolution.Off
typeCheckingConfig =
{
TypeCheckingConfig.Mode =
if FsharpExperimentalFeaturesEnabledAutomatically then
TypeCheckingMode.Graph
else
TypeCheckingMode.Sequential
}
}

member tcConfigB.FxResolver =
Expand Down Expand Up @@ -1288,7 +1308,6 @@ type TcConfig private (data: TcConfigBuilder, validate: bool) =
member _.emitTailcalls = data.emitTailcalls
member _.deterministic = data.deterministic
member _.concurrentBuild = data.concurrentBuild
member _.parallelCheckingWithSignatureFiles = data.parallelCheckingWithSignatureFiles
member _.emitMetadataAssembly = data.emitMetadataAssembly
member _.pathMap = data.pathMap
member _.langVersion = data.langVersion
Expand Down Expand Up @@ -1322,6 +1341,7 @@ type TcConfig private (data: TcConfigBuilder, validate: bool) =
member _.xmlDocInfoLoader = data.xmlDocInfoLoader
member _.exiter = data.exiter
member _.parallelReferenceResolution = data.parallelReferenceResolution
member _.typeCheckingConfig = data.typeCheckingConfig

static member Create(builder, validate) =
use _ = UseBuildPhase BuildPhase.Parameter
Expand Down
20 changes: 16 additions & 4 deletions src/Compiler/Driver/CompilerConfig.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ type ParallelReferenceResolution =
| On
| Off

[<RequireQualifiedAccess>]
type TypeCheckingMode =
/// Default mode where all source files are processed sequentially in compilation order.
| Sequential
/// Signature files and implementation files without backing files are processed sequentially, then backed implementation files are processed in parallel.
| ParallelCheckingOfBackedImplFiles
/// Parallel type-checking that uses automated file-to-file dependency detection to construct a highly-parallelisable file graph.
| Graph

[<RequireQualifiedAccess>]
type TypeCheckingConfig = { Mode: TypeCheckingMode }

[<NoEquality; NoComparison>]
type TcConfigBuilder =
{
Expand Down Expand Up @@ -412,8 +424,6 @@ type TcConfigBuilder =

mutable concurrentBuild: bool

mutable parallelCheckingWithSignatureFiles: bool

mutable emitMetadataAssembly: MetadataAssemblyGeneration

mutable preferredUiLang: string option
Expand Down Expand Up @@ -491,6 +501,8 @@ type TcConfigBuilder =
mutable exiter: Exiter

mutable parallelReferenceResolution: ParallelReferenceResolution

mutable typeCheckingConfig: TypeCheckingConfig
}

static member CreateNew:
Expand Down Expand Up @@ -734,8 +746,6 @@ type TcConfig =

member concurrentBuild: bool

member parallelCheckingWithSignatureFiles: bool

member emitMetadataAssembly: MetadataAssemblyGeneration

member pathMap: PathMap
Expand Down Expand Up @@ -858,6 +868,8 @@ type TcConfig =

member parallelReferenceResolution: ParallelReferenceResolution

member typeCheckingConfig: TypeCheckingConfig

/// Represents a computation to return a TcConfig. Normally this is just a constant immutable TcConfig,
/// but for F# Interactive it may be based on an underlying mutable TcConfigBuilder.
[<Sealed>]
Expand Down
11 changes: 10 additions & 1 deletion src/Compiler/Driver/CompilerOptions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,16 @@ let testFlag tcConfigB =
| "ShowLoadedAssemblies" -> tcConfigB.showLoadedAssemblies <- true
| "ContinueAfterParseFailure" -> tcConfigB.continueAfterParseFailure <- true
| "ParallelOff" -> tcConfigB.concurrentBuild <- false
| "ParallelCheckingWithSignatureFilesOn" -> tcConfigB.parallelCheckingWithSignatureFiles <- true
| "ParallelCheckingWithSignatureFilesOn" ->
tcConfigB.typeCheckingConfig <-
{ tcConfigB.typeCheckingConfig with
Mode = TypeCheckingMode.ParallelCheckingOfBackedImplFiles
}
| "GraphBasedChecking" ->
tcConfigB.typeCheckingConfig <-
{ tcConfigB.typeCheckingConfig with
Mode = TypeCheckingMode.Graph
}
#if DEBUG
| "ShowParserStackOnParseError" -> showParserStackOnParseError <- true
#endif
Expand Down
7 changes: 7 additions & 0 deletions src/Compiler/Driver/GraphChecking/Continuation.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[<RequireQualifiedAccess>]
module internal Continuation

let rec sequence<'a, 'ret> (recursions: (('a -> 'ret) -> 'ret) list) (finalContinuation: 'a list -> 'ret) : 'ret =
safesparrow marked this conversation as resolved.
Show resolved Hide resolved
match recursions with
| [] -> [] |> finalContinuation
| recurse :: recurses -> recurse (fun ret -> sequence recurses (fun rets -> ret :: rets |> finalContinuation))
218 changes: 218 additions & 0 deletions src/Compiler/Driver/GraphChecking/DependencyResolution.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
module internal FSharp.Compiler.GraphChecking.DependencyResolution

open FSharp.Compiler.Syntax

// This code just looks for a path in the trie
// It could be cached and is easy to reason about.
let queryTrie (trie: TrieNode) (path: ModuleSegment list) : QueryTrieNodeResult =
let rec visit (currentNode: TrieNode) (path: ModuleSegment list) =
match path with
| [] -> failwith "path should not be empty"
| [ lastNodeFromPath ] ->
match currentNode.Children.TryGetValue(lastNodeFromPath) with
| false, _ -> QueryTrieNodeResult.NodeDoesNotExist
| true, childNode ->
if Set.isEmpty childNode.Files then
QueryTrieNodeResult.NodeDoesNotExposeData
else
QueryTrieNodeResult.NodeExposesData(childNode.Files)
| currentPath :: restPath ->
match currentNode.Children.TryGetValue(currentPath) with
| false, _ -> QueryTrieNodeResult.NodeDoesNotExist
| true, childNode -> visit childNode restPath

visit trie path

let queryTrieMemoized (trie: TrieNode) : QueryTrie =
Internal.Utilities.Library.Tables.memoize (queryTrie trie)

// Now how to detect the deps between files?
// Process the content of each file using some state

let processOwnNamespace (queryTrie: QueryTrie) (path: ModuleSegment list) (state: FileContentQueryState) : FileContentQueryState =
let queryResult = queryTrie path

match queryResult with
| QueryTrieNodeResult.NodeDoesNotExist -> state
| QueryTrieNodeResult.NodeDoesNotExposeData -> state.AddOwnNamespace path
| QueryTrieNodeResult.NodeExposesData files -> state.AddOwnNamespace(path, files)

// Helper function to process a open statement
// The statement could link to files and/or should be tracked as an open namespace
let processOpenPath (queryTrie: QueryTrie) (path: ModuleSegment list) (state: FileContentQueryState) : FileContentQueryState =
let queryResult = queryTrie path

match queryResult with
| QueryTrieNodeResult.NodeDoesNotExist -> state
| QueryTrieNodeResult.NodeDoesNotExposeData -> state.AddOpenNamespace path
| QueryTrieNodeResult.NodeExposesData files -> state.AddOpenNamespace(path, files)

// Helper function to process an identifier
let processIdentifier (queryTrie: QueryTrie) (path: ModuleSegment list) (state: FileContentQueryState) : FileContentQueryState =
let queryResult = queryTrie path

match queryResult with
| QueryTrieNodeResult.NodeDoesNotExist -> state
| QueryTrieNodeResult.NodeDoesNotExposeData ->
// This can occur when you have a file that uses a known namespace (for example namespace System).
// When any other code uses that System namespace it won't find anything in the user code.
state
| QueryTrieNodeResult.NodeExposesData files -> state.AddDependencies files

// Typically used to folder FileContentEntry items over a FileContentQueryState
let rec processStateEntry (queryTrie: QueryTrie) (state: FileContentQueryState) (entry: FileContentEntry) : FileContentQueryState =
match entry with
| FileContentEntry.TopLevelNamespace (topLevelPath, content) ->
let state =
match topLevelPath with
| [] -> state
| _ -> processOwnNamespace queryTrie topLevelPath state

List.fold (processStateEntry queryTrie) state content

| FileContentEntry.OpenStatement path ->
// An open statement can directly reference file or be a partial open statement
// Both cases need to be processed.
let stateAfterFullOpenPath = processOpenPath queryTrie path state

// Any existing open statement could be extended with the current path (if that node where to exists in the trie)
// The extended path could add a new link (in case of a module or namespace with types)
// It might also not add anything at all (in case it the extended path is still a partial one)
(stateAfterFullOpenPath, state.OpenNamespaces)
||> Set.fold (fun acc openNS -> processOpenPath queryTrie [ yield! openNS; yield! path ] acc)

| FileContentEntry.PrefixedIdentifier path ->
match path with
| [] ->
// should not be possible though
state
| _ ->
// path could consist out of multiple segments
safesparrow marked this conversation as resolved.
Show resolved Hide resolved
(state, [| 1 .. path.Length |])
||> Array.fold (fun state takeParts ->
let path = List.take takeParts path
// process the name was if it were a FQN
let stateAfterFullIdentifier = processIdentifier queryTrie path state

// Process the name in combination with the existing open namespaces
(stateAfterFullIdentifier, state.OpenNamespaces)
||> Set.fold (fun acc openNS -> processIdentifier queryTrie [ yield! openNS; yield! path ] acc))

| FileContentEntry.NestedModule (nestedContent = nestedContent) ->
// We don't want our current state to be affect by any open statements in the nested module
let nestedState = List.fold (processStateEntry queryTrie) state nestedContent
// Afterward we are only interested in the found dependencies in the nested module
let foundDependencies =
Set.union state.FoundDependencies nestedState.FoundDependencies

{ state with
FoundDependencies = foundDependencies
}

let getFileNameBefore (files: FileWithAST array) idx =
files[0 .. (idx - 1)] |> Array.map (fun f -> f.Idx) |> Set.ofArray

/// Returns files contain in any node of the given Trie
let indicesUnderNode (node: TrieNode) : Set<int> =
let rec collect (node: TrieNode) (continuation: int list -> int list) : int list =
let continuations: ((int list -> int list) -> int list) list =
[
for node in node.Children.Values do
yield collect node
]

let finalContinuation indexes =
continuation [ yield! node.Files; yield! List.collect id indexes ]

Continuation.sequence continuations finalContinuation

Set.ofList (collect node id)

/// <summary>
/// For a given file's content, find all missing ("ghost") file dependencies that are required to satisfy the type-checker.
/// </summary>
/// <remarks>
/// A "ghost" dependency is a link between files that actually should be avoided.
/// The user has a partial namespace or opens a namespace that does not produce anything.
/// In order to still be able to compile the current file, the given namespace should be known to the file.
/// We did not find it via the trie, because there are no files that contribute to this namespace.
/// </remarks>
let collectGhostDependencies (fileIndex: int) (trie: TrieNode) (queryTrie: QueryTrie) (result: FileContentQueryState) =
// Go over all open namespaces, and assert all those links eventually went anywhere
Set.toArray result.OpenedNamespaces
|> Array.collect (fun path ->
match queryTrie path with
| QueryTrieNodeResult.NodeExposesData _
| QueryTrieNodeResult.NodeDoesNotExist -> Array.empty
| QueryTrieNodeResult.NodeDoesNotExposeData ->
// At this point we are following up if an open namespace really lead nowhere.
let node =
let rec visit (node: TrieNode) (path: ModuleSegment list) =
match path with
| [] -> node
| head :: tail -> visit node.Children[head] tail

visit trie path

let children = indicesUnderNode node |> Set.filter (fun idx -> idx < fileIndex)
let intersection = Set.intersect result.FoundDependencies children

if Set.isEmpty intersection then
// The partial open did not lead to anything
// In order for it to exist in the current file we need to link it
// to some file that introduces the namespace in the trie.
if Set.isEmpty children then
// In this case not a single file is contributing to the opened namespace.
safesparrow marked this conversation as resolved.
Show resolved Hide resolved
// As a last resort we assume all files are dependent, in order to preserve valid code.
[| 0 .. (fileIndex - 1) |]
else
[| Seq.head children |]
else
// The partial open did eventually lead to a link in a file
Array.empty)

let mkGraph (filePairs: FilePairMap) (files: FileWithAST array) : Graph<int> =
// Implementation files backed by signatures should be excluded to construct the trie.
let trieInput =
Array.choose
(fun f ->
match f.AST with
| ParsedInput.SigFile _ -> Some f
| ParsedInput.ImplFile _ -> if filePairs.HasSignature f.Idx then None else Some f)
files

let trie = TrieMapping.mkTrie trieInput
let queryTrie: QueryTrie = queryTrieMemoized trie

let fileContents = Array.Parallel.map FileContentMapping.mkFileContent files

let findDependencies (file: FileWithAST) : int * int array =
let fileContent = fileContents[file.Idx]
let knownFiles = getFileNameBefore files file.Idx
let filesFromRoot = trie.Files |> Set.filter (fun rootIdx -> rootIdx < file.Idx)

// Process all entries of a file and query the trie when required to find the dependent files.
let result =
// Seq is faster than List in this case.
Seq.fold (processStateEntry queryTrie) (FileContentQueryState.Create file.Idx knownFiles filesFromRoot) fileContent

// after processing the file we should verify if any of the open statements are found in the trie but do not yield any file link.
let ghostDependencies = collectGhostDependencies file.Idx trie queryTrie result

// Automatically add a link from an implementation to its signature file (if present)
let signatureDependency =
match filePairs.TryGetSignatureIndex file.Idx with
| None -> Array.empty
| Some sigIdx -> Array.singleton sigIdx

let allDependencies =
[|
yield! result.FoundDependencies
yield! ghostDependencies
yield! signatureDependency
|]
|> Array.distinct

file.Idx, allDependencies

Array.Parallel.map findDependencies files |> readOnlyDict