From 1252d71939b66d5b55e649057a6d1b378b2b1413 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 1/6] Build a project's TcGlobals with its own path map when the framework imports are cached FrameworkImportsCache keys the framework imports, and the TcGlobals built with them, by the framework set alone. A project reusing the entry got a fresh TcGlobals only when langVersion or realsig differed, and even then took pathMap from the cached instance. Since TypedTreePickle applies that map to every range it writes, the in-memory reference data of each project carried the --pathmap of whichever project filled the cache first, and a project without a map handed its consumers file names nothing on disk matches. pathMap now takes part in the decision like langVersion and realsig, and the new TcGlobals takes it from the project's own TcConfig, in the incremental builder and the transparent compiler alike. Fixes #20474 Co-Authored-By: Claude Fable 5.1 --- src/Compiler/Service/IncrementalBuild.fs | 11 ++++++----- src/Compiler/Service/TransparentCompiler.fs | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index 4f02bda91e7..f93bb9e7dba 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -557,12 +557,13 @@ type FrameworkImportsCache(size) = let node = this.GetNode(tcConfig, frameworkDLLs, nonFrameworkResolutions) let! tcGlobals, frameworkTcImports = node.GetOrComputeValue() - // If the tcGlobals was loaded from a different project, langVersion and realsig may be different - // for each cached project. So here we create a new tcGlobals, with the existing framework values - // and updated realsig and langversion + // If the tcGlobals was loaded from a different project, langVersion, realsig and pathMap may be + // different for each cached project. So here we create a new tcGlobals, with the existing framework + // values and the updated realsig, langversion and pathMap let tcGlobals = if tcGlobals.langVersion <> tcConfig.langVersion - || tcGlobals.realsig <> tcConfig.realsig then + || tcGlobals.realsig <> tcConfig.realsig + || tcGlobals.pathMap <> tcConfig.pathMap then TcGlobals( tcGlobals.compilingFSharpCore, tcGlobals.ilg, @@ -574,7 +575,7 @@ type FrameworkImportsCache(size) = tcGlobals.tryFindSysTypeCcuHelper, tcGlobals.emitDebugInfoInQuotations, tcGlobals.noDebugAttributes, - tcGlobals.pathMap, + tcConfig.pathMap, tcConfig.langVersion, tcConfig.realsig, tcConfig.compilationMode diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 691dde3e802..e7e83676b70 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -964,13 +964,14 @@ type internal TransparentCompiler // Prepare the frameworkTcImportsCache let! tcGlobals, frameworkTcImports = ComputeFrameworkImports tcConfig frameworkDLLs nonFrameworkResolutions - // If the tcGlobals was loaded from a different project, langVersion and realsig may be different - // for each cached project. So here we create a new tcGlobals, with the existing framework values - // and updated realsig and langversion + // If the tcGlobals was loaded from a different project, langVersion, realsig and pathMap may be + // different for each cached project. So here we create a new tcGlobals, with the existing + // framework values and the updated realsig, langversion and pathMap let tcGlobals = if tcGlobals.langVersion <> tcConfig.langVersion || tcGlobals.realsig <> tcConfig.realsig + || tcGlobals.pathMap <> tcConfig.pathMap then TcGlobals( tcGlobals.compilingFSharpCore, @@ -983,7 +984,7 @@ type internal TransparentCompiler tcGlobals.tryFindSysTypeCcuHelper, tcGlobals.emitDebugInfoInQuotations, tcGlobals.noDebugAttributes, - tcGlobals.pathMap, + tcConfig.pathMap, tcConfig.langVersion, tcConfig.realsig, tcConfig.compilationMode From c3c6a5f5de640ec6c436460d8fc516715be4b692 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 2/6] Test that a cached framework set does not lend its path map to a sibling project Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Compiler.ComponentTests.fsproj | 3 +- .../FSharpChecker/PathMap.fs | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index c50c9a03675..8755c2a44e2 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -1,4 +1,4 @@ - + @@ -540,6 +540,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs new file mode 100644 index 00000000000..18e99d829ce --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -0,0 +1,57 @@ +module FSharpChecker.PathMap + +open System.IO +open System.Threading.Tasks +open Xunit +open FSharp.Test.ProjectGeneration +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Text + +let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = + ProjectWorkflowBuilder(project, checker = checker).Yield() |> Async.Ignore + +/// The framework imports, and the TcGlobals with them, are cached per framework set; the path map of +/// the project that filled the cache must not reach the ranges a sibling exposes to its consumers. +[] +[] +[] +let ``a sibling's path map does not reach the ranges of a project without one`` (useTransparentCompiler: bool) : Task = + task { + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + + let mapped = + { SyntheticProject.Create("Mapped", sourceFile "Mapped" []) with + OtherOptions = [ $"--pathmap:{Path.GetDirectoryName library.ProjectDir}=.\\" ] } + + let app = + { SyntheticProject.Create("App", sourceFile "App" [ "Library" ]) with + DependsOn = [ library ] } + + do! checkWith checker mapped + do! checkWith checker app + + let appFile = app.GetFilePath "App" + + let! _, answer = + checker.ParseAndCheckFileInProject( + appFile, + 0, + SourceText.ofString (File.ReadAllText appFile), + app.GetProjectOptions checker + ) + + let checkResults = + match answer with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" + + let libraryFunction = + checkResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.tryFind (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f") + |> Option.defaultWith (fun () -> + failwith + $"""ModuleLibrary.f not used; symbols: %A{checkResults.GetAllUsesOfAllSymbolsInFile() |> Seq.map _.Symbol.FullName |> Seq.distinct |> List.ofSeq}""") + + Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) + } From 6a5abf74b53a66f1888b3046f1e69c6c4529113c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 12:32:15 +0200 Subject: [PATCH 3/6] Add the release note for PR #20476 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 27f2c0070d7..9053058d9a0 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -148,6 +148,7 @@ * Import: Don't walk non-F# assemblies when labelling trait constraint sources (PR [#20090](https://github.com/dotnet/fsharp/pull/20090)) * Avoid per-instance lock object in InterruptibleLazy and DelayInitArrayMap (PR [#20088](https://github.com/dotnet/fsharp/pull/20088)) * IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250)) +* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) ### Added From 6f536ecc40329f30826573a11b146b3e496f1a5b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 10 Sep 2026 20:14:19 +0200 Subject: [PATCH 4/6] Name a mapped declaration from its root once A build that maps its source paths - as DeterministicSourcePaths does - maps the directory it compiled in along with the file names of its ranges, so the two reach the same root and neither records what that root is. `fileNameOfItem` joined them anyway, on the assumption that the directory is absolute and the file name relative to it, and the declaration of a symbol imported from such an assembly came back naming the directory twice. Nothing matches a path like that, so Go To Definition reported the symbol as external and opened generated metadata instead of its source. Join only when the directory is rooted. When it is not, the file name already reaches the root on its own and is as complete as the metadata can make it. The IDE drops the path map from the options it builds, so this is reached through an assembly on disk - which is where a path map is normally applied, and which no change to those options can rewrite. Co-Authored-By: Claude Opus 5 (1M context) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Symbols/SymbolHelpers.fs | 10 +++- .../FSharpChecker/PathMap.fs | 57 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 9053058d9a0..663ac269a09 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,6 +1,7 @@ ### Fixed * Fix internal error FS0192 "Iterate2D" when a `[]` parameter auto-quotes an argument that captures a not-yet-generalized use of an inferred generically-recursive function. The auto-quoted (`Expr.WithValue`) copy now keeps a fresh link to the recursive-value use so it receives the same inferred type arguments as the executable expression at the letrec point. ([Issue #20379](https://github.com/dotnet/fsharp/issues/20379)) +* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) diff --git a/src/Compiler/Symbols/SymbolHelpers.fs b/src/Compiler/Symbols/SymbolHelpers.fs index 222edab7c13..1ee4c203a68 100644 --- a/src/Compiler/Symbols/SymbolHelpers.fs +++ b/src/Compiler/Symbols/SymbolHelpers.fs @@ -230,14 +230,20 @@ module internal SymbolHelpers = let fileNameOfItem (g: TcGlobals) qualProjectDir (m: range) h = let file = m.FileName if verbose then dprintf "file stored in metadata is '%s'\n" file + + // A build that maps its source paths maps the directory it compiled in as well, so that directory + // and the file name reach the same unrecorded root on their own: joining them repeats the directory. + let underDirectory (dir: string) = + if FileSystem.IsPathRootedShim dir then Path.Combine(dir, file) else file + if not (FileSystem.IsPathRootedShim file) then match ccuOfItem g h with | Some ccu -> - Path.Combine(ccu.SourceCodeDirectory, file) + underDirectory ccu.SourceCodeDirectory | None -> match qualProjectDir with | None -> file - | Some dir -> Path.Combine(dir, file) + | Some dir -> underDirectory dir else file let ParamNameAndTypesOfUnaryCustomOperation g minfo = diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 18e99d829ce..8bf70c250d5 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -5,6 +5,7 @@ open System.Threading.Tasks open Xunit open FSharp.Test.ProjectGeneration open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.EditorServices open FSharp.Compiler.Text let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = @@ -55,3 +56,59 @@ let ``a sibling's path map does not reach the ranges of a project without one`` Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) } + +/// A build that maps its source paths maps the directory it compiled in as well, so the file name of a +/// range and that directory both reach the same root on their own. Joining them names the directory twice. +[] +[] +[] +let ``a declaration in a project with a path map is named from the root once`` (useTransparentCompiler: bool) : Task = + task { + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + let library = SyntheticProject.Create("MappedLibrary", sourceFile "Library" []) + let root = Path.GetDirectoryName library.ProjectDir + + let library = + { library with + OtherOptions = [ $"--pathmap:{root}=.\\" ] } + + let app = + { SyntheticProject.Create("MappedApp", sourceFile "App" [ "Library" ]) with + DependsOn = [ library ] } + + do! checkWith checker library + do! checkWith checker app + + let appFile = app.GetFilePath "App" + let appLines = File.ReadAllLines appFile + + let! _, answer = + checker.ParseAndCheckFileInProject( + appFile, + 0, + SourceText.ofString (File.ReadAllText appFile), + app.GetProjectOptions checker + ) + + let checkResults = + match answer with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" + + let usage = + checkResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.find (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f") + + let declaration = + checkResults.GetDeclarationLocation( + usage.Range.EndLine, + usage.Range.EndColumn, + appLines[usage.Range.EndLine - 1], + [ "ModuleLibrary"; "f" ] + ) + + match declaration with + | FindDeclResult.DeclFound range -> + Assert.Equal(library.GetFilePath "Library", Path.GetFullPath(Path.Combine(root, range.FileName))) + | result -> failwith $"expected the declaration of ModuleLibrary.f, got %A{result}" + } From 3ed6bae6ec917474e278c626c877a469fc0102ed Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 04:26:12 +0200 Subject: [PATCH 5/6] Map the test's root with the platform's separator On Linux and macOS a backslash is part of a file name, so a map to .\ left the declaration's name unresolvable against the root there. Co-Authored-By: Claude Opus 5 --- tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 8bf70c250d5..e8e607db286 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -70,7 +70,7 @@ let ``a declaration in a project with a path map is named from the root once`` ( let library = { library with - OtherOptions = [ $"--pathmap:{root}=.\\" ] } + OtherOptions = [ $"--pathmap:{root}=.{Path.DirectorySeparatorChar}" ] } let app = { SyntheticProject.Create("MappedApp", sourceFile "App" [ "Library" ]) with From a4ae807e9b35a927be17076a39c5e4375cde0eb2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 04:31:04 +0200 Subject: [PATCH 6/6] Link the release note to PR #20518 Co-Authored-By: Claude Opus 5 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 663ac269a09..f1ddf2e0851 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -2,6 +2,7 @@ * Fix internal error FS0192 "Iterate2D" when a `[]` parameter auto-quotes an argument that captures a not-yet-generalized use of an inferred generically-recursive function. The auto-quoted (`Expr.WithValue`) copy now keeps a fresh link to the recursive-value use so it receives the same inferred type arguments as the executable expression at the letrec point. ([Issue #20379](https://github.com/dotnet/fsharp/issues/20379)) * `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. +* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. ([PR #20518](https://github.com/dotnet/fsharp/pull/20518)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203))