From 6d06c0efca22db4a5df2f25f42e1b2b245d3d4fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:26:16 +0000 Subject: [PATCH 01/10] Initial plan From 9378a04f08418a9328b3bb9158f2ddad1d406064 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:35:07 +0000 Subject: [PATCH 02/10] Add Prime and Nu to regression test matrix Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- azure-pipelines-PR.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/azure-pipelines-PR.yml b/azure-pipelines-PR.yml index adac619969..b4238426bf 100644 --- a/azure-pipelines-PR.yml +++ b/azure-pipelines-PR.yml @@ -949,3 +949,11 @@ stages: commit: 60c20cca65a7df6e8335e8d6060d91b30909fbea buildScript: dotnet build tests/OpenTK.Tests/OpenTK.Tests.fsproj -c Release ;; dotnet build tests/OpenTK.Tests.Integration/OpenTK.Tests.Integration.fsproj -c Release displayName: OpenTK_FSharp_Build + - repo: bryanedds/Prime + commit: 8d55f4e8e1d76e42f5fb3c9ba69eb79fe695e9fa + buildScript: dotnet build Prime.sln --configuration Release + displayName: Prime_Build + - repo: bryanedds/Nu + commit: b35cbe02029e0e33d72a4846816cf22714eb3aac + buildScript: dotnet build Nu.sln --configuration Release + displayName: Nu_Build From 79413f4c47c4816af1d3d924a2a5e0c53e056715 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 14:45:40 +0100 Subject: [PATCH 03/10] Fix graph resolution: handle Module+Module merge in trie When two files define the same module name in the same namespace (e.g., module Assets in namespace Nu across Core/Assets.fs and World/WorldAssets.fs), mergeTrieNodes' fallback case kept only the first file's index, losing the second. This caused missing dependency edges in the graph-based parallel type checker, leading to FS0039 errors when files were typechecked before their dependencies. Fix: Add an explicit Module+Module case that promotes to a Namespace node preserving both file indices in filesThatExposeTypes, following the existing Module+Namespace merge pattern. Added three test scenarios covering sub-namespace access patterns and the duplicate module name case. --- .../Driver/GraphChecking/TrieMapping.fs | 4 + .../TypeChecks/Graph/Scenarios.fs | 97 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/Compiler/Driver/GraphChecking/TrieMapping.fs b/src/Compiler/Driver/GraphChecking/TrieMapping.fs index 215f8a2dae..e8b027d8d5 100644 --- a/src/Compiler/Driver/GraphChecking/TrieMapping.fs +++ b/src/Compiler/Driver/GraphChecking/TrieMapping.fs @@ -53,6 +53,10 @@ let rec mergeTrieNodes (accumulatorTrie: TrieNode) (currentTrie: TrieNode) : Tri // Replace the module in favour of the namespace (which can hold nested children). | TrieNodeInfo.Module(_name, file), TrieNodeInfo.Namespace(name, currentFilesThatExposeTypes, filesDefiningNamespaceWithoutTypes) -> TrieNodeInfo.Namespace(name, currentFilesThatExposeTypes.Add file, filesDefiningNamespaceWithoutTypes) + // Multiple files define the same module name in the same namespace. + // Promote to a Namespace node so both file indices are preserved as dependencies. + | TrieNodeInfo.Module(name, file1), TrieNodeInfo.Module(_, file2) -> + TrieNodeInfo.Namespace(name, ImmutableHashSet.singleton file1 |> _.Add(file2), ImmutableHashSet.empty ()) | _ -> accumulatorTrie.Current let nextChildren = diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs index 424fc6d471..c50068772a 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs @@ -1150,6 +1150,103 @@ let value = Script.ScriptModule.compute "hi" """ (set [| 2 |]) ] + scenario + "Sub-namespace opens parent namespace with types and modules" + [ + sourceFile + "Types.fs" + """ +namespace Nu + +type GameTime = + { Value: float } + static member ofSeconds (s: float) = { Value = s } +""" + Set.empty + sourceFile + "Assets.fs" + """ +namespace Nu + +module Assets = + module Default = + let Black = "black" +""" + (set [| 0 |]) + sourceFile + "Constants.fs" + """ +namespace Nu.Constants +open Nu + +module Dissolve = + let x = Assets.Default.Black +""" + (set [| 0; 1 |]) + ] + scenario + "Sub-namespace opens parent namespace with only modules" + [ + sourceFile + "Modules.fs" + """ +namespace Nu + +module Assets = + module Default = + let Black = "black" +""" + Set.empty + sourceFile + "Consumer.fs" + """ +namespace Nu.Sub +open Nu + +module M = + let x = Assets.Default.Black +""" + (set [| 0 |]) + ] + // When two files define the same module name in the same namespace, + // the trie must track both file indices so that dependencies on either are detected. + // ModuleSuffix avoids FS0248 while preserving the same source-level name. + scenario + "Same module name defined in multiple files of the same namespace" + [ + sourceFile + "Assets1.fs" + """ +namespace Nu + +[] +module Assets = + module Default = + let PackageName = "Default" +""" + Set.empty + sourceFile + "Assets2.fs" + """ +namespace Nu + +[] +module Assets = + module Default = + let Black = "black" +""" + Set.empty + sourceFile + "Constants.fs" + """ +namespace Nu.Constants +open Nu + +module Dissolve = + let x = Assets.Default.Black +""" + (set [| 0; 1 |]) + ] ] From f85592cdf1eee730c5f082bbd52ef4f7b2d07137 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 15:25:24 +0100 Subject: [PATCH 04/10] Fixup: improve graph resolution test coverage and quality - Add scenario for type constructor usage through open parent namespace - Add scenario for multiple namespace declarations in one file with AutoOpen - Add descriptive comments explaining test groups and Module+Module fix rationale - All 48 dependency resolution tests and 96 compilation tests pass --- .../TypeChecks/Graph/Scenarios.fs | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs index c50068772a..6daabb2419 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs @@ -1150,6 +1150,9 @@ let value = Script.ScriptModule.compute "hi" """ (set [| 2 |]) ] + // Regression tests for graph-based dependency resolution in projects with + // sub-namespace patterns (e.g. the Nu game engine). These verify that + // PrefixedIdentifier resolution correctly discovers modules through open namespaces. scenario "Sub-namespace opens parent namespace with types and modules" [ @@ -1184,6 +1187,30 @@ module Dissolve = """ (set [| 0; 1 |]) ] + scenario + "Sub-namespace opens parent namespace and uses type constructor" + [ + sourceFile + "Types.fs" + """ +namespace Nu + +type GameTime = + { Value: float } + static member ofSeconds (s: float) = { Value = s } +""" + Set.empty + sourceFile + "Constants.fs" + """ +namespace Nu.Constants +open Nu + +module Defaults = + let IncomingTime = GameTime.ofSeconds 0.5 +""" + (set [| 0 |]) + ] scenario "Sub-namespace opens parent namespace with only modules" [ @@ -1208,9 +1235,49 @@ module M = """ (set [| 0 |]) ] - // When two files define the same module name in the same namespace, - // the trie must track both file indices so that dependencies on either are detected. - // ModuleSuffix avoids FS0248 while preserving the same source-level name. + scenario + "Multiple namespace declarations in one file with AutoOpen" + [ + sourceFile + "Entity.fs" + """ +namespace Nu + +type Entity = { Name: string } +""" + Set.empty + sourceFile + "BlockMap.fs" + """ +namespace Nu.BlockMap + +module BlockMapCore = + let defaultSize = 32 + +namespace Nu + +[] +module BlockMapExtensions = + let getBlockMapSize () = 42 +""" + (set [| 0 |]) + sourceFile + "Consumer.fs" + """ +namespace Nu.Game +open Nu + +module GameLogic = + let size = getBlockMapSize () + let e : Entity = { Name = "test" } +""" + (set [| 0; 1 |]) + ] + // Two files define the same nested module name within the same namespace. + // Without the Module+Module merge case in TrieMapping.mergeTrieNodes, + // only the first file's index would be tracked, causing the second to be + // missed as a dependency. CompilationRepresentation(ModuleSuffix) on the + // second file avoids FS0248 (duplicate module name) at the CLR level. scenario "Same module name defined in multiple files of the same namespace" [ From 82351466c6c357cd3af06277477948a4db1921a8 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 15:37:57 +0100 Subject: [PATCH 05/10] Remove redundant test comments, keep only non-obvious CompilationRepresentation note --- .../TypeChecks/Graph/Scenarios.fs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs index 6daabb2419..2d73ff1313 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/Scenarios.fs @@ -1150,9 +1150,6 @@ let value = Script.ScriptModule.compute "hi" """ (set [| 2 |]) ] - // Regression tests for graph-based dependency resolution in projects with - // sub-namespace patterns (e.g. the Nu game engine). These verify that - // PrefixedIdentifier resolution correctly discovers modules through open namespaces. scenario "Sub-namespace opens parent namespace with types and modules" [ @@ -1273,11 +1270,7 @@ module GameLogic = """ (set [| 0; 1 |]) ] - // Two files define the same nested module name within the same namespace. - // Without the Module+Module merge case in TrieMapping.mergeTrieNodes, - // only the first file's index would be tracked, causing the second to be - // missed as a dependency. CompilationRepresentation(ModuleSuffix) on the - // second file avoids FS0248 (duplicate module name) at the CLR level. + // CompilationRepresentation(ModuleSuffix) on Assets2 avoids FS0248 at the CLR level. scenario "Same module name defined in multiple files of the same namespace" [ From 6bb26f91075a6f9b28c3eeed5c266a2b83a0b529 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 15:42:44 +0100 Subject: [PATCH 06/10] Add direct unit tests for Module+Module merge in TrieMapping Address TEST-COVERAGE gap: the existing 'Two modules with the same name' test only had 2 files, so mkTrie (which skips the last file) never exercised the Module+Module merge branch in mergeTrieNodes. Add three targeted tests: - Module+Module merge preserves both file indices (3 files) - Module+Module merge across three files (4 files, 3 sharing name) - Module+Module merge preserves children from both sides --- .../TypeChecks/Graph/TrieMappingTests.fs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs index 2a3d7b608e..9b81137a14 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs @@ -329,6 +329,157 @@ let _ = () let aNode = trie.Children.["A"] Assert.Equal>(set [| 0 |], aNode.Files) +[] +let ``Module+Module merge promotes to Namespace and preserves both file indices`` () = + // Three files: files 0 and 1 both declare "module N.M", file 2 is a dummy (skipped by mkTrie). + // This exercises the Module+Module merge branch in mergeTrieNodes. + let trie = + getLastTrie + [| + { + Idx = 0 + FileName = "M1.fs" + ParsedInput = + parseSourceCode ( + "M1.fs", + """ +module N.M + +type A = { X: int } +""" + ) + } + { + Idx = 1 + FileName = "M2.fs" + ParsedInput = + parseSourceCode ( + "M2.fs", + """ +module N.M + +let y = 42 +""" + ) + } + { + Idx = 2 + FileName = "Dummy.fs" + ParsedInput = Unchecked.defaultof + } + |] + + let nNode = trie.Children.["N"] + let mNode = nNode.Children.["M"] + Assert.Equal>(set [| 0; 1 |], mNode.Files) + +[] +let ``Module+Module merge across three files preserves all file indices`` () = + // Four files: files 0, 1, 2 all declare "module N.M", file 3 is a dummy. + let trie = + getLastTrie + [| + { + Idx = 0 + FileName = "M1.fs" + ParsedInput = + parseSourceCode ( + "M1.fs", + """ +module N.M + +type A = { X: int } +""" + ) + } + { + Idx = 1 + FileName = "M2.fs" + ParsedInput = + parseSourceCode ( + "M2.fs", + """ +module N.M + +let y = 42 +""" + ) + } + { + Idx = 2 + FileName = "M3.fs" + ParsedInput = + parseSourceCode ( + "M3.fs", + """ +module N.M + +let z = "hello" +""" + ) + } + { + Idx = 3 + FileName = "Dummy.fs" + ParsedInput = Unchecked.defaultof + } + |] + + let nNode = trie.Children.["N"] + let mNode = nNode.Children.["M"] + Assert.Equal>(set [| 0; 1; 2 |], mNode.Files) + +[] +let ``Module+Module merge preserves children from both sides`` () = + // Two files under namespace N both define nested module M with different children. + // After merge, children from both files should be present. + let trie = + getLastTrie + [| + { + Idx = 0 + FileName = "M1.fs" + ParsedInput = + parseSourceCode ( + "M1.fs", + """ +namespace N + +module M = + module Child1 = + let x = 1 +""" + ) + } + { + Idx = 1 + FileName = "M2.fs" + ParsedInput = + parseSourceCode ( + "M2.fs", + """ +namespace N + +module M = + module Child2 = + let y = 2 +""" + ) + } + { + Idx = 2 + FileName = "Dummy.fs" + ParsedInput = Unchecked.defaultof + } + |] + + let nNode = trie.Children.["N"] + let mNode = nNode.Children.["M"] + Assert.Equal>(set [| 0; 1 |], mNode.Files) + Assert.True(mNode.Children.ContainsKey("Child1"), "Child1 from file 0 should survive merge") + Assert.True(mNode.Children.ContainsKey("Child2"), "Child2 from file 1 should survive merge") + Assert.Equal(2, mNode.Children.Count) + [] let ``Two nested modules with the same name, in named namespace`` () = let trie = From bc5e951717d8d24a553311c74319f80b15207660 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 15:50:54 +0100 Subject: [PATCH 07/10] Remove redundant setup comments from Module+Module merge tests --- .../TypeChecks/Graph/TrieMappingTests.fs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs index 9b81137a14..9339ed9648 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs @@ -331,8 +331,6 @@ let _ = () [] let ``Module+Module merge promotes to Namespace and preserves both file indices`` () = - // Three files: files 0 and 1 both declare "module N.M", file 2 is a dummy (skipped by mkTrie). - // This exercises the Module+Module merge branch in mergeTrieNodes. let trie = getLastTrie [| @@ -375,7 +373,6 @@ let y = 42 [] let ``Module+Module merge across three files preserves all file indices`` () = - // Four files: files 0, 1, 2 all declare "module N.M", file 3 is a dummy. let trie = getLastTrie [| @@ -431,8 +428,6 @@ let z = "hello" [] let ``Module+Module merge preserves children from both sides`` () = - // Two files under namespace N both define nested module M with different children. - // After merge, children from both files should be present. let trie = getLastTrie [| From f167a7d0c8e9c8734e9e9af452417d51caee761a Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 16:26:04 +0100 Subject: [PATCH 08/10] Strengthen Module+Module merge test assertions and add integration test - TrieMappingTests: Assert merged node is Namespace with correct filesThatExposeTypes (not just Files property) - DependencyResolutionTests: Add focused test verifying Module+Module merge creates dependency edges to both defining files --- .../Graph/DependencyResolutionTests.fs | 66 +++++++++++++++++++ .../TypeChecks/Graph/TrieMappingTests.fs | 17 +++++ 2 files changed, 83 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs index 81adc54076..48266e377e 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs @@ -22,3 +22,69 @@ let ``Supported scenario`` (scenario: Scenario) = let expectedDeps = file.ExpectedDependencies let actualDeps = set graph.[file.Index] Assert.True((expectedDeps = actualDeps), $"Dependencies don't match for {System.IO.Path.GetFileName file.FileName}") + +/// Verify that Module+Module merge in the trie creates dependency edges to both files. +/// Without the merge fix, the second module's file index would be lost, and the consumer +/// would only depend on the first file. +[] +let ``Module+Module merge creates dependency edges to both defining files`` () = + let files = + [| + { + Idx = 0 + FileName = "M1.fs" + ParsedInput = + parseSourceCode ( + "M1.fs", + """ +namespace N + +module M = + let x = 1 +""" + ) + } + { + Idx = 1 + FileName = "M2.fs" + ParsedInput = + parseSourceCode ( + "M2.fs", + """ +namespace N + +module M = + let y = 2 +""" + ) + } + { + Idx = 2 + FileName = "Consumer.fs" + ParsedInput = + parseSourceCode ( + "Consumer.fs", + """ +namespace N.Sub +open N + +module C = + let z = M.x + M.y +""" + ) + } + |] + + let filePairs = FilePairMap(files) + let graph, _trie = DependencyResolution.mkGraph filePairs files + let consumerDeps = set graph.[2] + + Assert.True( + consumerDeps.Contains 0, + "Consumer should depend on first file defining module M" + ) + + Assert.True( + consumerDeps.Contains 1, + "Consumer should depend on second file defining module M (requires Module+Module merge)" + ) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs index 9339ed9648..1d8f3e2b60 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs @@ -371,6 +371,12 @@ let y = 42 let mNode = nNode.Children.["M"] Assert.Equal>(set [| 0; 1 |], mNode.Files) + match mNode.Current with + | TrieNodeInfo.Namespace(_, filesThatExposeTypes, filesDefiningNamespaceWithoutTypes) -> + Assert.Equal>(set [| 0; 1 |], set filesThatExposeTypes) + Assert.True(filesDefiningNamespaceWithoutTypes.Count = 0, "filesDefiningNamespaceWithoutTypes should be empty after Module+Module merge") + | other -> Assert.Fail($"Expected Namespace after Module+Module merge, got {other}") + [] let ``Module+Module merge across three files preserves all file indices`` () = let trie = @@ -426,6 +432,11 @@ let z = "hello" let mNode = nNode.Children.["M"] Assert.Equal>(set [| 0; 1; 2 |], mNode.Files) + match mNode.Current with + | TrieNodeInfo.Namespace(_, filesThatExposeTypes, _) -> + Assert.Equal>(set [| 0; 1; 2 |], set filesThatExposeTypes) + | other -> Assert.Fail($"Expected Namespace after triple Module merge, got {other}") + [] let ``Module+Module merge preserves children from both sides`` () = let trie = @@ -471,6 +482,12 @@ module M = let nNode = trie.Children.["N"] let mNode = nNode.Children.["M"] Assert.Equal>(set [| 0; 1 |], mNode.Files) + + match mNode.Current with + | TrieNodeInfo.Namespace(_, filesThatExposeTypes, _) -> + Assert.Equal>(set [| 0; 1 |], set filesThatExposeTypes) + | other -> Assert.Fail($"Expected Namespace after Module+Module merge, got {other}") + Assert.True(mNode.Children.ContainsKey("Child1"), "Child1 from file 0 should survive merge") Assert.True(mNode.Children.ContainsKey("Child2"), "Child2 from file 1 should survive merge") Assert.Equal(2, mNode.Children.Count) From fe26597d899e7aa6d04486f7ac173c49f491b4cb Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 16:47:26 +0100 Subject: [PATCH 09/10] Fixup #2: Remove duplicate test, parameterize Module+Module merge tests - Remove standalone Module+Module merge Fact test from DependencyResolutionTests.fs (duplicated scenario-based coverage, used Contains instead of set equality) - Extract assertModuleMergePromotesToNamespace helper in TrieMappingTests - Combine 2-file and 3-file Module+Module merge tests into a single parameterized Theory test --- .../Graph/DependencyResolutionTests.fs | 66 ---------- .../TypeChecks/Graph/TrieMappingTests.fs | 124 ++++-------------- 2 files changed, 24 insertions(+), 166 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs index 48266e377e..81adc54076 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/DependencyResolutionTests.fs @@ -22,69 +22,3 @@ let ``Supported scenario`` (scenario: Scenario) = let expectedDeps = file.ExpectedDependencies let actualDeps = set graph.[file.Index] Assert.True((expectedDeps = actualDeps), $"Dependencies don't match for {System.IO.Path.GetFileName file.FileName}") - -/// Verify that Module+Module merge in the trie creates dependency edges to both files. -/// Without the merge fix, the second module's file index would be lost, and the consumer -/// would only depend on the first file. -[] -let ``Module+Module merge creates dependency edges to both defining files`` () = - let files = - [| - { - Idx = 0 - FileName = "M1.fs" - ParsedInput = - parseSourceCode ( - "M1.fs", - """ -namespace N - -module M = - let x = 1 -""" - ) - } - { - Idx = 1 - FileName = "M2.fs" - ParsedInput = - parseSourceCode ( - "M2.fs", - """ -namespace N - -module M = - let y = 2 -""" - ) - } - { - Idx = 2 - FileName = "Consumer.fs" - ParsedInput = - parseSourceCode ( - "Consumer.fs", - """ -namespace N.Sub -open N - -module C = - let z = M.x + M.y -""" - ) - } - |] - - let filePairs = FilePairMap(files) - let graph, _trie = DependencyResolution.mkGraph filePairs files - let consumerDeps = set graph.[2] - - Assert.True( - consumerDeps.Contains 0, - "Consumer should depend on first file defining module M" - ) - - Assert.True( - consumerDeps.Contains 1, - "Consumer should depend on second file defining module M (requires Module+Module merge)" - ) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs index 1d8f3e2b60..02b8d998fe 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/TrieMappingTests.fs @@ -329,113 +329,37 @@ let _ = () let aNode = trie.Children.["A"] Assert.Equal>(set [| 0 |], aNode.Files) -[] -let ``Module+Module merge promotes to Namespace and preserves both file indices`` () = - let trie = - getLastTrie - [| - { - Idx = 0 - FileName = "M1.fs" - ParsedInput = - parseSourceCode ( - "M1.fs", - """ -module N.M - -type A = { X: int } -""" - ) - } - { - Idx = 1 - FileName = "M2.fs" - ParsedInput = - parseSourceCode ( - "M2.fs", - """ -module N.M - -let y = 42 -""" - ) - } - { - Idx = 2 - FileName = "Dummy.fs" - ParsedInput = Unchecked.defaultof - } - |] - +let private assertModuleMergePromotesToNamespace (fileCount: int) = + let sourceFiles = + [| for i in 0 .. fileCount - 1 do + { + Idx = i + FileName = $"M{i + 1}.fs" + ParsedInput = parseSourceCode ($"M{i + 1}.fs", $"module N.M\n\nlet v{i} = {i}") + } + { + Idx = fileCount + FileName = "Dummy.fs" + ParsedInput = Unchecked.defaultof + } |] + + let trie = getLastTrie sourceFiles let nNode = trie.Children.["N"] let mNode = nNode.Children.["M"] - Assert.Equal>(set [| 0; 1 |], mNode.Files) + let expectedIndices = set [| 0 .. fileCount - 1 |] + Assert.Equal>(expectedIndices, mNode.Files) match mNode.Current with | TrieNodeInfo.Namespace(_, filesThatExposeTypes, filesDefiningNamespaceWithoutTypes) -> - Assert.Equal>(set [| 0; 1 |], set filesThatExposeTypes) + Assert.Equal>(expectedIndices, set filesThatExposeTypes) Assert.True(filesDefiningNamespaceWithoutTypes.Count = 0, "filesDefiningNamespaceWithoutTypes should be empty after Module+Module merge") - | other -> Assert.Fail($"Expected Namespace after Module+Module merge, got {other}") + | other -> Assert.Fail($"Expected Namespace after Module+Module merge of {fileCount} files, got {other}") -[] -let ``Module+Module merge across three files preserves all file indices`` () = - let trie = - getLastTrie - [| - { - Idx = 0 - FileName = "M1.fs" - ParsedInput = - parseSourceCode ( - "M1.fs", - """ -module N.M - -type A = { X: int } -""" - ) - } - { - Idx = 1 - FileName = "M2.fs" - ParsedInput = - parseSourceCode ( - "M2.fs", - """ -module N.M - -let y = 42 -""" - ) - } - { - Idx = 2 - FileName = "M3.fs" - ParsedInput = - parseSourceCode ( - "M3.fs", - """ -module N.M - -let z = "hello" -""" - ) - } - { - Idx = 3 - FileName = "Dummy.fs" - ParsedInput = Unchecked.defaultof - } - |] - - let nNode = trie.Children.["N"] - let mNode = nNode.Children.["M"] - Assert.Equal>(set [| 0; 1; 2 |], mNode.Files) - - match mNode.Current with - | TrieNodeInfo.Namespace(_, filesThatExposeTypes, _) -> - Assert.Equal>(set [| 0; 1; 2 |], set filesThatExposeTypes) - | other -> Assert.Fail($"Expected Namespace after triple Module merge, got {other}") +[] +[] +[] +let ``Module+Module merge promotes to Namespace and preserves all file indices`` (fileCount: int) = + assertModuleMergePromotesToNamespace fileCount [] let ``Module+Module merge preserves children from both sides`` () = From 4f2029748a804bb4700f73b927885839642cd9df Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 12 Feb 2026 19:51:03 +0100 Subject: [PATCH 10/10] release notes added --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index 085f73b09d..9db28a547c 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -3,6 +3,7 @@ * Fix false FS1182 (unused variable) warning for query expression variables used in where, let, join, and select clauses. ([Issue #422](https://github.com/dotnet/fsharp/issues/422)) * Fix FS0229 B-stream misalignment when reading metadata from assemblies compiled with LangVersion < 9.0, introduced by [#17706](https://github.com/dotnet/fsharp/pull/17706). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) * Fix FS3356 false positive for instance extension members with same name on different types, introduced by [#18821](https://github.com/dotnet/fsharp/pull/18821). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) +* Fix graph-based type checking incorrectly resolving dependencies when the same module name is defined across multiple files in the same namespace. ([PR #19280](https://github.com/dotnet/fsharp/pull/19280)) ### Added