From af19aa0418150c71cc1f576fa4aa144993b86127 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 30 Aug 2026 22:28:25 +0200 Subject: [PATCH 001/133] Add Copilot rules reference for other agent engines --- .claude/rules/CcrOptout.md | 6 ++++++ .claude/rules/CodeGen.md | 6 ++++++ .claude/rules/ComponentTests.md | 6 ++++++ .claude/rules/DebugEmit.md | 6 ++++++ .claude/rules/EngVersioning.md | 9 +++++++++ .claude/rules/ExpertReview.md | 6 ++++++ .claude/rules/FSComp.md | 6 ++++++ .claude/rules/FSharpCore.md | 6 ++++++ .claude/rules/LSP.md | 6 ++++++ .claude/rules/NoBloat.md | 10 ++++++++++ .claude/rules/Optimizer.md | 6 ++++++ .claude/rules/SyntaxTree.md | 9 +++++++++ .claude/rules/TypedTreePickle.md | 8 ++++++++ AGENTS.md | 3 +++ CLAUDE.md | 1 + 15 files changed, 94 insertions(+) create mode 100644 .claude/rules/CcrOptout.md create mode 100644 .claude/rules/CodeGen.md create mode 100644 .claude/rules/ComponentTests.md create mode 100644 .claude/rules/DebugEmit.md create mode 100644 .claude/rules/EngVersioning.md create mode 100644 .claude/rules/ExpertReview.md create mode 100644 .claude/rules/FSComp.md create mode 100644 .claude/rules/FSharpCore.md create mode 100644 .claude/rules/LSP.md create mode 100644 .claude/rules/NoBloat.md create mode 100644 .claude/rules/Optimizer.md create mode 100644 .claude/rules/SyntaxTree.md create mode 100644 .claude/rules/TypedTreePickle.md create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.claude/rules/CcrOptout.md b/.claude/rules/CcrOptout.md new file mode 100644 index 00000000000..a4e911e2dca --- /dev/null +++ b/.claude/rules/CcrOptout.md @@ -0,0 +1,6 @@ +--- +paths: + - "**" +--- + +@../../.github/instructions/CcrOptout.instructions.md diff --git a/.claude/rules/CodeGen.md b/.claude/rules/CodeGen.md new file mode 100644 index 00000000000..53e32af521c --- /dev/null +++ b/.claude/rules/CodeGen.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/Compiler/CodeGen/**/*.{fs,fsi}" +--- + +@../../.github/instructions/CodeGen.instructions.md diff --git a/.claude/rules/ComponentTests.md b/.claude/rules/ComponentTests.md new file mode 100644 index 00000000000..a71b46bae53 --- /dev/null +++ b/.claude/rules/ComponentTests.md @@ -0,0 +1,6 @@ +--- +paths: + - "tests/FSharp.Compiler.ComponentTests/**/*.fs" +--- + +@../../.github/instructions/ComponentTests.instructions.md diff --git a/.claude/rules/DebugEmit.md b/.claude/rules/DebugEmit.md new file mode 100644 index 00000000000..a58978e5d3a --- /dev/null +++ b/.claude/rules/DebugEmit.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/Compiler/AbstractIL/ilwritepdb.{fs,fsi}" +--- + +@../../.github/instructions/DebugEmit.instructions.md diff --git a/.claude/rules/EngVersioning.md b/.claude/rules/EngVersioning.md new file mode 100644 index 00000000000..76483c8d6de --- /dev/null +++ b/.claude/rules/EngVersioning.md @@ -0,0 +1,9 @@ +--- +paths: + - "eng/Packages.props" + - "eng/Versions.props" + - "eng/Version.Details.xml" + - "eng/Version.Details.props" +--- + +@../../.github/instructions/EngVersioning.instructions.md diff --git a/.claude/rules/ExpertReview.md b/.claude/rules/ExpertReview.md new file mode 100644 index 00000000000..450e5c6e77f --- /dev/null +++ b/.claude/rules/ExpertReview.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/Compiler/**/*.{fs,fsi}" +--- + +@../../.github/instructions/ExpertReview.instructions.md diff --git a/.claude/rules/FSComp.md b/.claude/rules/FSComp.md new file mode 100644 index 00000000000..95379c7e34d --- /dev/null +++ b/.claude/rules/FSComp.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/Compiler/FSComp.txt" +--- + +@../../.github/instructions/FSComp.instructions.md diff --git a/.claude/rules/FSharpCore.md b/.claude/rules/FSharpCore.md new file mode 100644 index 00000000000..dc879081eac --- /dev/null +++ b/.claude/rules/FSharpCore.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/FSharp.Core/**/*.{fs,fsi}" +--- + +@../../.github/instructions/FSharpCore.instructions.md diff --git a/.claude/rules/LSP.md b/.claude/rules/LSP.md new file mode 100644 index 00000000000..04f1c6ae806 --- /dev/null +++ b/.claude/rules/LSP.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/FSharp.Compiler.LanguageServer/**/*.{fs,fsi}" +--- + +@../../.github/instructions/LSP.instructions.md diff --git a/.claude/rules/NoBloat.md b/.claude/rules/NoBloat.md new file mode 100644 index 00000000000..f2554b4c0ab --- /dev/null +++ b/.claude/rules/NoBloat.md @@ -0,0 +1,10 @@ +--- +paths: + - "src/Compiler/**/*.{fs,fsi}" + - "vsintegration/src/**/*.{fs,fsi}" + - "tests/FSharp.Compiler.ComponentTests/**/*.fs" + - "tests/FSharp.Compiler.Service.Tests/**/*.fs" + - "vsintegration/tests/**/*.fs" +--- + +@../../.github/instructions/NoBloat.instructions.md diff --git a/.claude/rules/Optimizer.md b/.claude/rules/Optimizer.md new file mode 100644 index 00000000000..41eb3aacec7 --- /dev/null +++ b/.claude/rules/Optimizer.md @@ -0,0 +1,6 @@ +--- +paths: + - "src/Compiler/Optimize/**/*.{fs,fsi}" +--- + +@../../.github/instructions/Optimizer.instructions.md diff --git a/.claude/rules/SyntaxTree.md b/.claude/rules/SyntaxTree.md new file mode 100644 index 00000000000..496da4a2cf4 --- /dev/null +++ b/.claude/rules/SyntaxTree.md @@ -0,0 +1,9 @@ +--- +paths: + - "src/Compiler/SyntaxTree/SyntaxTree.{fs,fsi}" + - "src/Compiler/SyntaxTree/SyntaxTreeOps.{fs,fsi}" + - "src/Compiler/SyntaxTree/ParseHelpers.{fs,fsi}" + - "src/Compiler/pars.fsy" +--- + +@../../.github/instructions/SyntaxTree.instructions.md diff --git a/.claude/rules/TypedTreePickle.md b/.claude/rules/TypedTreePickle.md new file mode 100644 index 00000000000..ec6a818ca9c --- /dev/null +++ b/.claude/rules/TypedTreePickle.md @@ -0,0 +1,8 @@ +--- +paths: + - "src/Compiler/TypedTree/TypedTreePickle.{fs,fsi}" + - "src/Compiler/TypedTree/TypedTree.{fs,fsi}" + - "src/Compiler/Driver/CompilerImports.{fs,fsi}" +--- + +@../../.github/instructions/TypedTreePickle.instructions.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..ac883d4f548 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# Agent Instructions + +See [`.github/copilot-instructions.md`](.github/copilot-instructions.md) for build, test, and coding rules for this repository. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..b21d16db837 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@.github/copilot-instructions.md From 8c720e2d2a02f961e2f05b40eb66c3d1625b08c4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 00:16:37 +0200 Subject: [PATCH 002/133] Prefer the Visual Studio MCP server over the shell Build, diagnostics, navigation, rename and formatting go through the IDE that already has the solution open, so the agent reads what the compiler and the symbol graph know rather than what the files say. Records the traps that cost a wrong edit otherwise: an unsaved VS buffer outranks the file on disk, build_* follows the IDE's active configuration rather than Debug, and project_add_file appends without a position so it cannot place a Compile item in an order-sensitive F# project. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b21d16db837..8b5dd8a7143 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,18 @@ @.github/copilot-instructions.md + +## Visual Studio first + +The `vs` MCP server reaches the Visual Studio instance holding this repo's solution. Where it and the shell both work, use it — it reports what the IDE's compiler and symbol graph know, not what the text files say. + +- Build with `build_solution` / `build_project` / `build_clean` rather than `dotnet build` or `msbuild`; the Build section above is the fallback for when no solution is loaded (`ide_get_workspace_folders` is empty). +- Read errors from `ide_get_diagnostics`, not by parsing build output. +- Navigate and rename with `nav_go_to_definition`, `nav_find_references`, `nav_search_workspace_symbols`, `nav_rename_symbol` — the symbol graph, not `grep` plus hand edits. +- Format what you touched with `document_format` / `document_organize_imports` before reaching for `dotnet fantomas`. + +Before editing a file on disk, `document_check_dirty` it. An unsaved VS buffer is the real content: read it with `document_read_buffer` and `document_save` first, or the edit lands on stale text and the user's next save reverts it. + +`build_*` compiles whichever configuration the IDE has active, and `solution_set_configuration` changes it for the user's next manual build too — read `solution_get_configuration` before assuming `Debug`. + +`project_add_file` appends without a position, so it cannot place a new `` correctly in an order-sensitive F# project: add those to the `.fsproj` by hand. + +Tests have no IDE path — the `vs` server exposes no Test Explorer tools (https://github.com/Corsinvest/cv4vs-agents/issues/192), so they run through `dotnet test` as described above. From 5899a3ca6c6c15195b557bf5ccf104f0d8acff66 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 30 Aug 2026 22:38:52 +0200 Subject: [PATCH 003/133] Add modern F# idiom instructions for Copilot and Claude Code LLMs are trained predominantly on pre-existing F# code, which skews toward older language versions and idioms (option over voption, sprintf over interpolation, tuples over struct tuples, isNull over match). Codify the constructs this codebase actually favors so agent-generated code reaches for the current idiom by default. --- .claude/rules/FSharp.md | 8 ++ .github/instructions/FSharp.instructions.md | 89 +++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 .claude/rules/FSharp.md create mode 100644 .github/instructions/FSharp.instructions.md diff --git a/.claude/rules/FSharp.md b/.claude/rules/FSharp.md new file mode 100644 index 00000000000..ab9f89607df --- /dev/null +++ b/.claude/rules/FSharp.md @@ -0,0 +1,8 @@ +--- +paths: + - "src/**/*.{fs,fsi,fsx}" + - "vsintegration/src/**/*.{fs,fsi}" + - "tests/**/*.{fs,fsi,fsx}" +--- + +@../../.github/instructions/FSharp.instructions.md diff --git a/.github/instructions/FSharp.instructions.md b/.github/instructions/FSharp.instructions.md new file mode 100644 index 00000000000..93ccaa8c9c0 --- /dev/null +++ b/.github/instructions/FSharp.instructions.md @@ -0,0 +1,89 @@ +--- +applyTo: + - "src/**/*.{fs,fsi,fsx}" + - "vsintegration/src/**/*.{fs,fsi}" + - "tests/**/*.{fs,fsi,fsx}" +--- + +# Writing F# + +Which constructs to reach for. Code shape and comments are NoBloat's; naming and the abbreviation glossary belong to `docs/coding-standards.md` and `DEVGUIDE.md`. + +These rules govern code you write or touch. Do not sweep the codebase to apply them – `CONTRIBUTING.md`: *"DO NOT submit large code formatting changes without discussing with the team first."* + +## Tools + +When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp.json`, FsLangMCP) for navigation, symbol discovery, diagnostics and cross-project usage search. Prefer its semantic tools over plain text search for F#-specific work. + +## Strings + +- Prefer interpolated strings over `sprintf` and `String.Format`. Use `$"""…"""` when the text itself contains quotes. +- Format specifiers are valid in interpolated strings and help type inference: `$"count %d{n}"`. +- `nameof` over a string literal that names a value, member or type. +- An explicit `StringComparison` on every `Equals`, `StartsWith`, `EndsWith`, `Contains`, `IndexOf` and `Compare`, and an explicit comparer on every `HashSet` and `Dictionary`. `Ordinal` by default, `OrdinalIgnoreCase` for identifiers and paths. Culture-sensitive comparison is a decision, never a default. + +## Values and types + +- `voption` – `ValueSome`/`ValueNone` – over `option` when the value does not escape; it is this compiler's option type. Exception: when an API hands you `'T option` (`Seq.tryHead`, `List.tryFind`), unwrap with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules. +- `struct ('T1 * 'T2)` tuples and `[]` types on allocation-sensitive paths. +- Anonymous struct records (`struct {| … |}`) over bare tuples for multi-value returns of internal helpers. Public FCS surface is governed by `.fsi` files and compatibility – do not change it for style. +- The compiler generates `IsCaseName` properties (`IsDefault`, `IsCustom`) for DU cases – use them for a specific-case check instead of a full `match`. +- Deconstruct `KeyValuePair` with the `KeyValue` active pattern: `for KeyValue(k, v) in map do …`. +- `[]` on `inline` higher-order helpers whose lambda argument must not become a closure. + +## Lambdas and collections + +- Prefer the `_.Property` shorthand in pipeline position: `tys |> List.map _.Type`. Complex expressions (`fun x -> x.Name = name`, `fun x -> x.A, x.B`) cannot use it. Never add a space – `_.MethodCall ()` breaks parsing. Unrelated to the `member _.Foo` self-identifier. +- Eta-reduce: `Seq.map (fun x -> someFunction x)` must become `Seq.map someFunction`. +- Several pipeline stages in a row over a `List`/`Array` allocate an intermediate collection each – route the chain through `Seq` and materialize once at the end. +- Concatenate with `[ yield! xs; yield! ys ]` / `seq { yield! xs; yield! ys }` rather than `@` or `Seq.append` – `@` forces both sides to lists and is O(n). +- Cast sequence items with `Seq.cast`, not `Seq.map (fun item -> item :> Target)`. + +## Async and exceptions + +- `src/Compiler` targets `netstandard2.0`: use `async { }` and the repo's `cancellable { }` (`src/Compiler/Utilities/Cancellable.fs`). `task { }` has no foothold there. +- `vsintegration` runs on the VS threading model where `task { }` is at home. When an override must return non-generic `Task`, annotate explicitly – `override _.M(…) : Task = task { … }` – never cast through pipelines. +- Thread cancellation through; see `ExpertReview.instructions.md`. +- `reraise ()` does not compile inside a `task`/`async` CE (FS0413). There, rethrow with `ExceptionDispatchInfo.Capture(ex).Throw()`; outside CEs plain `reraise ()` is correct. + +## Nullness + +Enabled in `src/Compiler`, `src/FSharp.Build`, `src/FSharp.Compiler.LanguageServer`. There: + +- Declare non-nullable; check for `null` at entry points. Trust C#/F# annotations – no null checks where the type system says a value cannot be null. +- Prefer `match x with | null -> … | x -> …` over `isNull` – the match narrows the type, `isNull` does not. +- Use `withNull` to hint nullability instead of boxing (`isNull (box f)`). +- Before suppressing a nullness warning (3261, 3262, …), escalate in order: `nonNull value` (runtime assert, fail fast) → `Unchecked.nonNull value` (null provably impossible upstream) → inline `#nowarn`/`#warnon` pair, centralised in one interop helper rather than scattered across call sites. + +## Warning suppression + +Inline `#nowarn "NN"` / `#warnon "NN"` pairs around the smallest possible scope – they are valid anywhere in an `.fs` file, not only at the top. File-level suppression is a last resort. + +## Classes (mostly `vsintegration`) + +- Initializer syntax over post-construction property assignment: `MyType(ctorArg, MutableProp1 = v1, MutableProp2 = (5 |> string))` – settable properties by name after positional arguments, computed values in parentheses. +- Extension members consumable from C#: `[]` module, `[]` on each member. +- Prefer a root module (`module Ns.FeatureExtensions`) over `namespace` + a static holder type for extension files; name it after the single target type plus `Extensions`, or after the feature when there are several targets. +- XML doc comments that use markup (``, ``) need their text wrapped in ``. + +## Opens + +Sort into blank-line-separated groups, alphabetically within each: `System.*` → `Microsoft.*` → `Internal.Utilities.*` → `FSharp.Compiler.*` (see the top of `src/Compiler/Service/TransparentCompiler.fs`). `open type` last within its group; type and module aliases at the very end. + +## Do not mistake for conventions + +This codebase implements every F# feature, so finding one here is no evidence that it is used here. These have no foothold – introducing them is a new pattern, not a continuation: + +- `while!` and `and!` – no uses at all; the matches are the parser and the checker implementing them. +- `[]` – a handful of deliberate assertions on specific recursive functions, not a habit. + +## The language version is not uniform + +`FSharp.Profiles.props` sets `LangVersion=preview`, but not under `Configuration=Proto` and not when `BUILDING_USING_DOTNET=true`. The compiler bootstraps, so a feature this repository is *adding* cannot be used in its own source until it ships in the SDK compiler named by `global.json` – otherwise the Proto stage fails. + +- `src/FSharp.Build` is pinned to `LangVersion 9`; it can load in Visual Studio against an older FSharp.Core. +- `src/FSharp.Core` leaves nullness off and is bound by `docs/fsharp-core-notes.md`. + +## Tests + +The ComponentTests DSL and its pipeline are covered by `ComponentTests.instructions.md`. Name tests with backticked spaces: ``let ``Issue 12345 - brief description`` () = …``. From aa35fa4a5a36c5a24929c63d935f72f72cdf8c83 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 02:32:38 +0200 Subject: [PATCH 004/133] Fix NRE on unlinked placeholder entities and add release notes --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/TypedTree/TypedTree.fs | 2684 +++++++++-------- src/Compiler/TypedTree/TypedTree.fsi | 4 +- .../TypedTree/TypedTreeOps.Remapping.fs | 46 +- .../TypedTree/TypedTreeOps.Remapping.fsi | 2 +- src/Compiler/Utilities/lib.fs | 8 +- 6 files changed, 1382 insertions(+), 1363 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 9441f8587e0..18ceff70d83 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -59,6 +59,7 @@ * Narrow overload-resolution error ranges to the method name only instead of covering the entire expression. ([Issue #14284](https://github.com/dotnet/fsharp/issues/14284), [PR #19505](https://github.com/dotnet/fsharp/pull/19505)) * Fix attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) +* Fix a `NullReferenceException` in typed-tree remapping and semantic classification when an unlinked placeholder entity is encountered while editing broken code. The compiler now treats these placeholder entities as absent instead of crashing the whole file's coloring/classification pipeline. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix abstract event accessors now have SpecialName flag. (Issue [#5834](https://github.com/dotnet/fsharp/issues/5834), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix warning 20 ("expression is implicitly ignored") pointing at the wrong range when the last expression in a sequential block (e.g. inside `for`, `while` loops) is non-unit. The squiggle now correctly highlights only the offending expression. ([Issue #5418](https://github.com/dotnet/fsharp/issues/5418), [PR #19504](https://github.com/dotnet/fsharp/pull/19504)) diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index 5030f59334d..b1a739752fe 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. - + /// Defines the typed abstract syntax intermediate representation used throughout the F# compiler. -module internal rec FSharp.Compiler.TypedTree +module internal rec FSharp.Compiler.TypedTree open System open System.Collections.Generic @@ -13,8 +13,8 @@ open Internal.Utilities.Library open Internal.Utilities.Library.Extras open Internal.Utilities.Rational -open FSharp.Compiler -open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler +open FSharp.Compiler.AbstractIL.IL open FSharp.Compiler.AbstractIL.ILX.Types open FSharp.Compiler.CompilerGlobalState open FSharp.Compiler.DiagnosticsLogger @@ -47,62 +47,62 @@ type ValInline = | Never | InlinedDefinition - member x.ShouldInline = - match x with - | ValInline.Always -> true + member x.ShouldInline = + match x with + | ValInline.Always -> true | ValInline.Optional | ValInline.Never | ValInline.InlinedDefinition -> false -/// A flag associated with values that indicates whether the recursive scope of the value is currently being processed, and +/// A flag associated with values that indicates whether the recursive scope of the value is currently being processed, and /// if the value has been generalized or not as yet. type ValRecursiveScopeInfo = - /// Set while the value is within its recursive scope. The flag indicates if the value has been eagerly generalized and accepts generic-recursive calls + /// Set while the value is within its recursive scope. The flag indicates if the value has been eagerly generalized and accepts generic-recursive calls | ValInRecScope of bool - /// The normal value for this flag when the value is not within its recursive scope + /// The normal value for this flag when the value is not within its recursive scope | ValNotInRecScope -type ValMutability = - | Immutable - | Mutable +type ValMutability = + | Immutable + | Mutable /// Indicates if a type parameter is needed at runtime and may not be eliminated [] -type TyparDynamicReq = +type TyparDynamicReq = /// Indicates the type parameter is not needed at runtime and may be eliminated - | No + | No /// Indicates the type parameter is needed at runtime and may not be eliminated | Yes -type ValBaseOrThisInfo = +type ValBaseOrThisInfo = - /// Indicates a ref-cell holding 'this' or the implicit 'this' used throughout an + /// Indicates a ref-cell holding 'this' or the implicit 'this' used throughout an /// implicit constructor to access and set values - | CtorThisVal + | CtorThisVal /// Indicates the value called 'base' available for calling base class members - | BaseVal + | BaseVal /// Indicates a normal value - | NormalVal + | NormalVal /// Indicates the 'this' value specified in a member e.g. 'x' in 'member x.M() = 1' - | MemberThisVal + | MemberThisVal /// Flags on values [] -type ValFlags(flags: int64) = +type ValFlags(flags: int64) = new (recValInfo, baseOrThis, isCompGen, inlineInfo, isMutable, isModuleOrMemberBinding, isExtensionMember, isIncrClassSpecialMember, isTyFunc, allowTypeInst, isGeneratedEventVal) = - let flags = + let flags = (match baseOrThis with | BaseVal -> 0b00000000000000000000L | CtorThisVal -> 0b00000000000000000010L | NormalVal -> 0b00000000000000000100L | MemberThisVal -> 0b00000000000000000110L) ||| - (if isCompGen then 0b00000000000000001000L + (if isCompGen then 0b00000000000000001000L else 0b000000000000000000000L) ||| (match inlineInfo with | ValInline.InlinedDefinition -> 0b00000000000000000000L @@ -137,12 +137,12 @@ type ValFlags(flags: int64) = (match isGeneratedEventVal with | false -> 0b00000000000000000000L - | true -> 0b00100000000000000000L) + | true -> 0b00100000000000000000L) ValFlags flags - member x.BaseOrThisInfo = - match (flags &&& 0b00000000000000000110L) with + member x.BaseOrThisInfo = + match (flags &&& 0b00000000000000000110L) with | 0b00000000000000000000L -> BaseVal | 0b00000000000000000010L -> CtorThisVal | 0b00000000000000000100L -> NormalVal @@ -153,15 +153,15 @@ type ValFlags(flags: int64) = member x.IsCompilerGenerated = (flags &&& 0b00000000000000001000L) <> 0x0L - member x.WithIsCompilerGenerated isCompGen = + member x.WithIsCompilerGenerated isCompGen = let flags = (flags &&& ~~~0b00000000000000001000L) ||| (match isCompGen with | false -> 0b00000000000000000000L | true -> 0b00000000000000001000L) ValFlags flags - member x.InlineInfo = - match (flags &&& 0b00000000000000110000L) with + member x.InlineInfo = + match (flags &&& 0b00000000000000110000L) with | 0b00000000000000000000L -> ValInline.InlinedDefinition | 0b00000000000000010000L -> ValInline.Always | 0b00000000000000100000L -> ValInline.Optional @@ -178,15 +178,15 @@ type ValFlags(flags: int64) = | ValInline.Never -> 0b00000000000000110000L) ValFlags flags - member x.MutabilityInfo = - match (flags &&& 0b00000000000001000000L) with + member x.MutabilityInfo = + match (flags &&& 0b00000000000001000000L) with | 0b00000000000000000000L -> Immutable | 0b00000000000001000000L -> Mutable | _ -> failwith "unreachable" - member x.IsMemberOrModuleBinding = - match (flags &&& 0b00000000000010000000L) with + member x.IsMemberOrModuleBinding = + match (flags &&& 0b00000000000010000000L) with | 0b00000000000000000000L -> false | 0b00000000000010000000L -> true | _ -> failwith "unreachable" @@ -201,19 +201,19 @@ type ValFlags(flags: int64) = member x.IsTypeFunction = (flags &&& 0b00000000010000000000L) <> 0L - member x.RecursiveValInfo = match (flags &&& 0b00000001100000000000L) with + member x.RecursiveValInfo = match (flags &&& 0b00000001100000000000L) with | 0b00000000000000000000L -> ValNotInRecScope | 0b00000000100000000000L -> ValInRecScope true | 0b00000001000000000000L -> ValInRecScope false | _ -> failwith "unreachable" - member x.WithRecursiveValInfo recValInfo = - let flags = + member x.WithRecursiveValInfo recValInfo = + let flags = (flags &&& ~~~0b00000001100000000000L) ||| (match recValInfo with | ValNotInRecScope -> 0b00000000000000000000L | ValInRecScope true -> 0b00000000100000000000L - | ValInRecScope false -> 0b00000001000000000000L) + | ValInRecScope false -> 0b00000001000000000000L) ValFlags flags member x.MakesNoCriticalTailcalls = (flags &&& 0b00000010000000000000L) <> 0L @@ -253,7 +253,7 @@ type ValFlags(flags: int64) = member x.WithIsParameter = ValFlags(flags ||| 0b10000000000000000000000L) /// Get the flags as included in the F# binary metadata - member x.PickledBits = + member x.PickledBits = // Clear the RecursiveValInfo, only used during inference and irrelevant across assembly boundaries // Clear the IsCompiledAsStaticPropertyWithoutField, only used to determine whether to use a true field for a value, and to eliminate the optimization info for observable bindings // Clear the HasBeenReferenced, only used to report "unreferenced variable" warnings and to help collect 'it' values in FSI.EXE @@ -279,9 +279,9 @@ type ValFlags(flags: int64) = /// Represents the kind of a type parameter [] -type TyparKind = +type TyparKind = - | Type + | Type | Measure @@ -299,18 +299,18 @@ type TyparKind = | TyparKind.Measure -> "measure" /// Indicates if the type variable can be solved or given new constraints. The status of a type variable -/// evolves towards being either rigid or solved. +/// evolves towards being either rigid or solved. [] -type TyparRigidity = +type TyparRigidity = /// Indicates the type parameter can't be solved - | Rigid + | Rigid /// Indicates the type parameter can't be solved, but the variable is not set to "rigid" until after inference is complete - | WillBeRigid + | WillBeRigid /// Indicates we give a warning if the type parameter is ever solved - | WarnIfNotRigid + | WarnIfNotRigid /// Indicates the type parameter is an inference variable may be solved | Flexible @@ -326,11 +326,11 @@ type TyparRigidity = member x.WarnIfMissingConstraint = match x with TyparRigidity.WillBeRigid -> true | _ -> false -/// Encode typar flags into a bit field +/// Encode typar flags into a bit field [] type TyparFlags(flags: int32) = - new (kind: TyparKind, rigidity: TyparRigidity, isFromError: bool, isCompGen: bool, staticReq: TyparStaticReq, dynamicReq: TyparDynamicReq, equalityDependsOn: bool, comparisonDependsOn: bool, supportsNullFlex: bool) = + new (kind: TyparKind, rigidity: TyparRigidity, isFromError: bool, isCompGen: bool, staticReq: TyparStaticReq, dynamicReq: TyparDynamicReq, equalityDependsOn: bool, comparisonDependsOn: bool, supportsNullFlex: bool) = TyparFlags((if isFromError then 0b00000000000000010 else 0) ||| (if isCompGen then 0b00000000000000100 else 0) ||| (match staticReq with @@ -344,13 +344,13 @@ type TyparFlags(flags: int32) = | TyparRigidity.Anon -> 0b00000000010000000) ||| (match kind with | TyparKind.Type -> 0b00000000000000000 - | TyparKind.Measure -> 0b00000000100000000) ||| - (if comparisonDependsOn then + | TyparKind.Measure -> 0b00000000100000000) ||| + (if comparisonDependsOn then 0b00000001000000000 else 0) ||| (match dynamicReq with | TyparDynamicReq.No -> 0b00000000000000000 | TyparDynamicReq.Yes -> 0b00000010000000000) ||| - (if equalityDependsOn then + (if equalityDependsOn then 0b00000100000000000 else 0) ||| // 0b00001000100000000 is being checked by x.Kind, but never set in this version of the code // 0b00010000000000000 is taken by compat flex @@ -360,20 +360,20 @@ type TyparFlags(flags: int32) = /// Indicates if the type inference variable was generated after an error when type checking expressions or patterns member x.IsFromError = (flags &&& 0b00000000000000010) <> 0x0 - /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable + /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable member x.IsCompilerGenerated = (flags &&& 0b00000000000000100) <> 0x0 /// Indicates if the type variable has a static "head type" requirement, i.e. ^a variables used in FSharp.Core and member constraints. - member x.StaticReq = - match (flags &&& 0b00000000000001000) with + member x.StaticReq = + match (flags &&& 0b00000000000001000) with | 0b00000000000000000 -> TyparStaticReq.None | 0b00000000000001000 -> TyparStaticReq.HeadType | _ -> failwith "unreachable" /// Indicates if the type variable can be solved or given new constraints. The status of a type variable - /// generally always evolves towards being either rigid or solved. - member x.Rigidity = - match (flags &&& 0b00000000011100000) with + /// generally always evolves towards being either rigid or solved. + member x.Rigidity = + match (flags &&& 0b00000000011100000) with | 0b00000000000000000 -> TyparRigidity.Rigid | 0b00000000000100000 -> TyparRigidity.WillBeRigid | 0b00000000001000000 -> TyparRigidity.WarnIfNotRigid @@ -382,8 +382,8 @@ type TyparFlags(flags: int32) = | _ -> failwith "unreachable" /// Indicates whether a type variable can be instantiated by types or units-of-measure. - member x.Kind = - match (flags &&& 0b00001000100000000) with + member x.Kind = + match (flags &&& 0b00001000100000000) with | 0b00000000000000000 -> TyparKind.Type | 0b00000000100000000 -> TyparKind.Measure | _ -> failwith "unreachable" @@ -394,61 +394,61 @@ type TyparFlags(flags: int32) = (flags &&& 0b00000001000000000) <> 0x0 /// Indicates if a type parameter is needed at runtime and may not be eliminated - member x.DynamicReq = - match (flags &&& 0b00000010000000000) with + member x.DynamicReq = + match (flags &&& 0b00000010000000000) with | 0b00000000000000000 -> TyparDynamicReq.No | 0b00000010000000000 -> TyparDynamicReq.Yes | _ -> failwith "unreachable" /// Indicates that whether or not a generic type definition satisfies the equality constraint is dependent on whether this type variable satisfies the equality constraint. - member x.EqualityConditionalOn = + member x.EqualityConditionalOn = (flags &&& 0b00000100000000000) <> 0x0 /// Indicates that whether this type parameter is a compat-flex type parameter (i.e. where "expr :> tp" only emits an optional warning) - member x.IsCompatFlex = + member x.IsCompatFlex = (flags &&& 0b00010000000000000) <> 0x0 - member x.WithCompatFlex b = - if b then + member x.WithCompatFlex b = + if b then TyparFlags(flags ||| 0b00010000000000000) else TyparFlags(flags &&& ~~~0b00010000000000000) /// Indicates that whether this type parameter is flexible for 'supports null' constraint, e.g. in the case of assignment to a mutable value - member x.IsSupportsNullFlex = + member x.IsSupportsNullFlex = (flags &&& 0b00100000000000000) <> 0x0 - member x.WithSupportsNullFlex b = - if b then + member x.WithSupportsNullFlex b = + if b then TyparFlags(flags ||| 0b00100000000000000) else TyparFlags(flags &&& ~~~0b00100000000000000) - member x.WithStaticReq staticReq = - TyparFlags(x.Kind, x.Rigidity, x.IsFromError, x.IsCompilerGenerated, staticReq, x.DynamicReq, x.EqualityConditionalOn, x.ComparisonConditionalOn, x.IsSupportsNullFlex) + member x.WithStaticReq staticReq = + TyparFlags(x.Kind, x.Rigidity, x.IsFromError, x.IsCompilerGenerated, staticReq, x.DynamicReq, x.EqualityConditionalOn, x.ComparisonConditionalOn, x.IsSupportsNullFlex) /// Get the flags as included in the F# binary metadata. We pickle this as int64 to allow for future expansion - member x.PickledBits = flags + member x.PickledBits = flags /// Encode entity flags into a bit field. We leave lots of space to allow for future expansion. [] type EntityFlags(flags: int64) = - new (usesPrefixDisplay, isModuleOrNamespace, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, isStructRecordOrUnionType) = + new (usesPrefixDisplay, isModuleOrNamespace, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, isStructRecordOrUnionType) = EntityFlags((if isModuleOrNamespace then 0b000000000000001L else 0L) ||| (if usesPrefixDisplay then 0b000000000000010L else 0L) ||| (if preEstablishedHasDefaultCtor then 0b000000000000100L else 0L) ||| (if hasSelfReferentialCtor then 0b000000000001000L else 0L) ||| - (if isStructRecordOrUnionType then 0b000000000100000L else 0L)) + (if isStructRecordOrUnionType then 0b000000000100000L else 0L)) /// Indicates the Entity is actually a module or namespace, not a type definition member x.IsModuleOrNamespace = (flags &&& 0b000000000000001L) <> 0x0L - /// Indicates the type prefers the "tycon" syntax for display etc. + /// Indicates the type prefers the "tycon" syntax for display etc. member x.IsPrefixDisplay = (flags &&& 0b000000000000010L) <> 0x0L - + // This bit is not pickled, only used while establishing a type constructor. It is needed because the type constructor // is known to satisfy the default constructor constraint even before any of its members have been established. member x.PreEstablishedHasDefaultConstructor = (flags &&& 0b000000000000100L) <> 0x0L @@ -465,51 +465,51 @@ type EntityFlags(flags: int64) = member x.IsStructRecordOrUnionType = (flags &&& 0b000000000100000L) <> 0x0L /// These two bits represents the on-demand analysis about whether the entity has the IsByRefLike attribute - member x.TryIsByRefLike = (flags &&& 0b000000011000000L) - |> function + member x.TryIsByRefLike = (flags &&& 0b000000011000000L) + |> function | 0b000000011000000L -> ValueSome true | 0b000000010000000L -> ValueSome false | _ -> ValueNone /// Adjust the on-demand analysis about whether the entity has the IsByRefLike attribute - member x.WithIsByRefLike flag = - let flags = + member x.WithIsByRefLike flag = + let flags = (flags &&& ~~~0b000000011000000L) ||| (match flag with | true -> 0b000000011000000L - | false -> 0b000000010000000L) + | false -> 0b000000010000000L) EntityFlags flags /// These two bits represents the on-demand analysis about whether the entity has the IsReadOnly attribute - member x.TryIsReadOnly = (flags &&& 0b000001100000000L) - |> function + member x.TryIsReadOnly = (flags &&& 0b000001100000000L) + |> function | 0b000001100000000L -> ValueSome true | 0b000001000000000L -> ValueSome false | _ -> ValueNone /// Adjust the on-demand analysis about whether the entity has the IsReadOnly attribute - member x.WithIsReadOnly flag = - let flags = + member x.WithIsReadOnly flag = + let flags = (flags &&& ~~~0b000001100000000L) ||| (match flag with | true -> 0b000001100000000L - | false -> 0b000001000000000L) + | false -> 0b000001000000000L) EntityFlags flags /// These two bits represents the on-demand analysis about whether the entity is assumed to be a readonly struct - member x.TryIsAssumedReadOnly = (flags &&& 0b000110000000000L) - |> function + member x.TryIsAssumedReadOnly = (flags &&& 0b000110000000000L) + |> function | 0b000110000000000L -> ValueSome true | 0b000100000000000L -> ValueSome false | _ -> ValueNone /// Adjust the on-demand analysis about whether the entity is assumed to be a readonly struct - member x.WithIsAssumedReadOnly flag = - let flags = + member x.WithIsAssumedReadOnly flag = + let flags = (flags &&& ~~~0b000110000000000L) ||| (match flag with | true -> 0b000110000000000L - | false -> 0b000100000000000L) + | false -> 0b000100000000000L) EntityFlags flags /// Get the flags as included in the F# binary metadata @@ -517,29 +517,29 @@ type EntityFlags(flags: int64) = -exception UndefinedName of - depth: int * - error: (RichText -> RichText) * - id: Ident * +exception UndefinedName of + depth: int * + error: (RichText -> RichText) * + id: Ident * suggestions: Suggestions exception InternalUndefinedItemRef of (string * string * string -> int * RichText) * string * string * string [] -type ModuleOrNamespaceKind = +type ModuleOrNamespaceKind = - /// Indicates that a module is compiled to a class with the "Module" suffix added. - | FSharpModuleWithSuffix + /// Indicates that a module is compiled to a class with the "Module" suffix added. + | FSharpModuleWithSuffix - /// Indicates that a module is compiled to a class with the same name as the original module - | ModuleOrType + /// Indicates that a module is compiled to a class with the same name as the original module + | ModuleOrType - /// Indicates that a 'module' is really a namespace + /// Indicates that a 'module' is really a namespace | Namespace of /// Indicates that the sourcecode had a namespace. - /// If false, this namespace was implicitly constructed during type checking. + /// If false, this namespace was implicitly constructed during type checking. isExplicit: bool - + override this.Equals other = match other with | :? ModuleOrNamespaceKind as kind -> @@ -581,12 +581,12 @@ type CompilationPath = member x.NestedCompPath n moduleKind = CompPath(x.ILScopeRef, x.SyntaxAccess, x.AccessPath@[(n, moduleKind)]) - member x.DemangledPath = + member x.DemangledPath = x.AccessPath |> List.map (fun (nm, k) -> CompilationPath.DemangleEntityName nm k) /// String 'Module' off an F# module name, if FSharpModuleWithSuffix is used - static member DemangleEntityName nm k = - match k with + static member DemangleEntityName nm k = + match k with | FSharpModuleWithSuffix -> String.dropSuffix nm FSharpModuleSuffix | _ -> nm @@ -620,44 +620,44 @@ type PublicPath = [] type EntityOptionalData = { - /// The name of the type, possibly with `n mangling - // MUTABILITY; used only when establishing tycons. + /// The name of the type, possibly with `n mangling + // MUTABILITY; used only when establishing tycons. mutable entity_compiled_name: string option // MUTABILITY: the signature is adjusted when it is checked - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation mutable entity_other_range: (range * bool) option - // MUTABILITY; used only when establishing tycons. + // MUTABILITY; used only when establishing tycons. mutable entity_kind: TyparKind - /// The declared documentation for the type or module + /// The declared documentation for the type or module // MUTABILITY: only for unpickle linkage mutable entity_xmldoc: XmlDoc - + /// the signature xml doc for an item in an implementation file. mutable entity_other_xmldoc : XmlDoc option - + /// The XML document signature for this entity mutable entity_xmldocsig: string - /// If non-None, indicates the type is an abbreviation for another type. + /// If non-None, indicates the type is an abbreviation for another type. // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_tycon_abbrev: TType option + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_tycon_abbrev: TType option - /// The declared accessibility of the representation, not taking signatures into account + /// The declared accessibility of the representation, not taking signatures into account mutable entity_tycon_repr_accessibility: Accessibility /// Indicates how visible is the entity is. // MUTABILITY: only for unpickle linkage - mutable entity_accessibility: Accessibility + mutable entity_accessibility: Accessibility /// Field used when the 'tycon' is really an exception definition - // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_exn_info: ExceptionInfo + // + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_exn_info: ExceptionInfo } [] @@ -666,60 +666,60 @@ type EntityOptionalData = override x.ToString() = "EntityOptionalData(...)" /// Represents a type definition, exception definition, module definition or namespace definition. -[] -type Entity = +[] +type Entity = { - /// The declared type parameters of the type - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_typars: LazyWithContext - + /// The declared type parameters of the type + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_typars: LazyWithContext + mutable entity_flags: EntityFlags - - /// The unique stamp of the "tycon blob". Note the same tycon in signature and implementation get different stamps + + /// The unique stamp of the "tycon blob". Note the same tycon in signature and implementation get different stamps // MUTABILITY: only for unpickle linkage mutable entity_stamp: Stamp - /// The name of the type, possibly with `n mangling + /// The name of the type, possibly with `n mangling // MUTABILITY: only for unpickle linkage mutable entity_logical_name: string - /// The declaration location for the type constructor + /// The declaration location for the type constructor mutable entity_range: range - - /// The declared attributes for the type - // MUTABILITY; used during creation and remapping of tycons + + /// The declared attributes for the type + // MUTABILITY; used during creation and remapping of tycons // MUTABILITY; used when propagating signature attributes into the implementation. - mutable entity_attribs: WellKnownEntityAttribs - - /// The declared representation of the type, i.e. record, union, class etc. + mutable entity_attribs: WellKnownEntityAttribs + + /// The declared representation of the type, i.e. record, union, class etc. // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_tycon_repr: TyconRepresentation - - /// The methods and properties of the type + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_tycon_repr: TyconRepresentation + + /// The methods and properties of the type // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_tycon_tcaug: TyconAugmentation - - /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions and nested modules + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_tycon_tcaug: TyconAugmentation | null + + /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions and nested modules // - // MUTABILITY: only used during creation and remapping of tycons and - // when compiling fslib to fixup compiler forward references to internal items - mutable entity_modul_type: MaybeLazy + // MUTABILITY: only used during creation and remapping of tycons and + // when compiling fslib to fixup compiler forward references to internal items + mutable entity_modul_type: MaybeLazy | null /// The stable path to the type, e.g. Microsoft.FSharp.Core.FSharpFunc`2 // MUTABILITY: only for unpickle linkage - mutable entity_cpath: CompilationPath option + mutable entity_cpath: CompilationPath option - /// Used during codegen to hold the ILX representation indicating how to access the type + /// Used during codegen to hold the ILX representation indicating how to access the type // MUTABILITY: only for unpickle linkage and caching mutable entity_il_repr_cache: CompiledTypeRepr cache | null mutable entity_opt_data: EntityOptionalData option } - static member NewEmptyEntityOptData() = - { entity_compiled_name = None + static member NewEmptyEntityOptData() = + { entity_compiled_name = None entity_other_range = None entity_kind = TyparKind.Type entity_xmldoc = XmlDoc.Empty @@ -730,19 +730,19 @@ type Entity = entity_accessibility = TAccess [] entity_exn_info = TExnNone } - /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException + /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException member x.LogicalName = x.entity_logical_name - /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException - member x.CompiledName = - match x.entity_opt_data with + /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException + member x.CompiledName = + match x.entity_opt_data with | Some { entity_compiled_name = Some s } -> s - | _ -> x.LogicalName + | _ -> x.LogicalName member x.EntityCompiledName = - match x.entity_opt_data with + match x.entity_opt_data with | Some optData -> optData.entity_compiled_name - | _ -> None + | _ -> None member x.SetCompiledName name = match x.entity_opt_data with @@ -776,30 +776,30 @@ type Entity = x.GetDisplayName(coreName=false, withStaticParameters=true, withUnderscoreTypars=false) #if !NO_TYPEPROVIDERS - member x.IsStaticInstantiationTycon = + member x.IsStaticInstantiationTycon = x.IsProvidedErasedTycon && let _nm, args = DemangleProvidedTypeName x.LogicalName - args.Length > 0 + args.Length > 0 #endif member x.GetDisplayName(coreName, ?withStaticParameters, ?withUnderscoreTypars) = let withStaticParameters = defaultArg withStaticParameters false let withUnderscoreTypars = defaultArg withUnderscoreTypars false let nm = x.LogicalName - if x.IsModuleOrNamespace then x.DemangledModuleOrNamespaceName + if x.IsModuleOrNamespace then x.DemangledModuleOrNamespaceName #if !NO_TYPEPROVIDERS - elif x.IsProvidedErasedTycon then + elif x.IsProvidedErasedTycon then let nm, args = DemangleProvidedTypeName nm - if withStaticParameters && args.Length > 0 then + if withStaticParameters && args.Length > 0 then nm + "<" + String.concat "," (Array.map snd args) + ">" else nm #endif else ignore withStaticParameters - match x.Typars with + match x.Typars with | [] -> nm - | tps -> + | tps -> let nm = DemangleGenericTypeName nm let isArray = nm.StartsWithOrdinal("[") && nm.EndsWithOrdinal("]") let nm = if coreName || isArray then nm else ConvertLogicalNameToDisplayName nm @@ -810,40 +810,40 @@ type Entity = nm /// The code location where the module, namespace or type is defined. - member x.Range = -#if !NO_TYPEPROVIDERS + member x.Range = +#if !NO_TYPEPROVIDERS match x.TypeReprInfo with | TProvidedTypeRepr info -> match Construct.ComputeDefinitionLocationOfProvidedItem info.ProvidedType with | Some range -> range | None -> x.entity_range - | _ -> + | _ -> #endif x.entity_range /// The range in the implementation, adjusted for an item in a signature - member x.DefinitionRange = - match x.entity_opt_data with + member x.DefinitionRange = + match x.entity_opt_data with | Some { entity_other_range = Some (r, true) } -> r | _ -> x.Range - member x.SigRange = - match x.entity_opt_data with + member x.SigRange = + match x.entity_opt_data with | Some { entity_other_range = Some (r, false) } -> r | _ -> x.Range - member x.SetOtherRange m = - match x.entity_opt_data with + member x.SetOtherRange m = + match x.entity_opt_data with | Some optData -> optData.entity_other_range <- Some m | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_other_range = Some m } - + member x.SetOtherXmlDoc xmlDoc = match x.entity_opt_data with | Some optData -> optData.entity_other_xmldoc <- Some xmlDoc | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_other_xmldoc = Some xmlDoc } - /// A unique stamp for this module, namespace or type definition within the context of this compilation. - /// Note that because of signatures, there are situations where in a single compilation the "same" + /// A unique stamp for this module, namespace or type definition within the context of this compilation. + /// Note that because of signatures, there are situations where in a single compilation the "same" /// module, namespace or type may have two distinct Entity objects that have distinct stamps. member x.Stamp = x.entity_stamp @@ -855,15 +855,15 @@ type Entity = /// The XML documentation of the entity, if any. If the entity is backed by provided metadata /// then this _does_ include this documentation. If the entity is backed by Abstract IL metadata - /// or comes from another F# assembly then it does not (because the documentation will get read from + /// or comes from another F# assembly then it does not (because the documentation will get read from /// an XML file). - member x.XmlDoc = + member x.XmlDoc = #if !NO_TYPEPROVIDERS match x.TypeReprInfo with | TProvidedTypeRepr info -> let lines = info.ProvidedType.PUntaintNoFailure(fun st -> (st :> IProvidedCustomAttributeProvider).GetXmlDocAttributes(info.ProvidedType.TypeProvider.PUntaintNoFailure id)) XmlDoc (lines, x.DefinitionRange) - | _ -> + | _ -> #endif match x.entity_opt_data with | Some optData -> @@ -877,8 +877,8 @@ type Entity = /// The XML documentation sig-string of the entity, if any, to use to lookup an .xml doc file. This also acts /// as a cache for this sig-string computation. - member x.XmlDocSig - with get() = + member x.XmlDocSig + with get() = match x.entity_opt_data with | Some optData -> optData.entity_xmldocsig | _ -> "" @@ -888,10 +888,16 @@ type Entity = | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_xmldocsig = v } /// The logical contents of the entity when it is a module or namespace fragment. - member x.ModuleOrNamespaceType = x.entity_modul_type.Force() + member x.ModuleOrNamespaceType = + match x.entity_modul_type with + | null -> Construct.NewEmptyModuleOrNamespaceType ModuleOrType + | entity_modul_type -> entity_modul_type.Force() /// The logical contents of the entity when it is a type definition. - member x.TypeContents = x.entity_tycon_tcaug + member x.TypeContents = + match x.entity_tycon_tcaug with + | null -> TyconAugmentation.Create() + | entity_tycon_tcaug -> entity_tycon_tcaug /// The kind of the type definition - is it a measure definition or a type definition? member x.TypeOrMeasureKind = @@ -910,7 +916,7 @@ type Entity = /// The information about the r.h.s. of a type definition, if any. For example, the r.h.s. of a union or record type. member x.TypeReprInfo = x.entity_tycon_repr - /// The information about the r.h.s. of an F# exception definition, if any. + /// The information about the r.h.s. of an F# exception definition, if any. member x.ExceptionInfo = match x.entity_opt_data with | Some optData -> optData.entity_exn_info @@ -931,19 +937,19 @@ type Entity = CompilationPath.DemangleEntityName x.LogicalName x.ModuleOrNamespaceType.ModuleOrNamespaceKind else x.LogicalName - + /// Get the type parameters for an entity that is a type declaration, otherwise return the empty list. /// /// Lazy because it may read metadata. Uses the entity's own range for error context. member x.Typars: Typars = x.entity_typars.Force x.Range /// Get the type abbreviated by this type definition, if it is an F# type abbreviation definition - member x.TypeAbbrev = + member x.TypeAbbrev = match x.entity_opt_data with | Some optData -> optData.entity_tycon_abbrev | _ -> None - member x.SetTypeAbbrev tycon_abbrev = + member x.SetTypeAbbrev tycon_abbrev = match x.entity_opt_data with | Some optData -> optData.entity_tycon_abbrev <- tycon_abbrev | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_tycon_abbrev = tycon_abbrev } @@ -980,7 +986,7 @@ type Entity = | Some optData -> optData.entity_accessibility | _ -> TAccess [] - /// Indicates the type prefers the "tycon" syntax for display etc. + /// Indicates the type prefers the "tycon" syntax for display etc. member x.IsPrefixDisplay = x.entity_flags.IsPrefixDisplay /// Indicates the Entity is actually a module or namespace, not a type definition @@ -988,7 +994,7 @@ type Entity = /// Indicates if the entity is a namespace member x.IsNamespace = x.IsModuleOrNamespace && (match x.ModuleOrNamespaceType.ModuleOrNamespaceKind with Namespace _ -> true | _ -> false) - + /// Indicates if the entity has an implicit namespace member x.IsImplicitNamespace = (match x.ModuleOrNamespaceType.ModuleOrNamespaceKind with Namespace false -> true | _ -> false) @@ -997,59 +1003,59 @@ type Entity = #if !NO_TYPEPROVIDERS /// Indicates if the entity is a provided type or namespace definition - member x.IsProvided = - match x.TypeReprInfo with + member x.IsProvided = + match x.TypeReprInfo with | TProvidedTypeRepr _ -> true | TProvidedNamespaceRepr _ -> true | _ -> false /// Indicates if the entity is a provided namespace fragment - member x.IsProvidedNamespace = - match x.TypeReprInfo with + member x.IsProvidedNamespace = + match x.TypeReprInfo with | TProvidedNamespaceRepr _ -> true | _ -> false /// Indicates if the entity is an erased provided type definition - member x.IsProvidedErasedTycon = - match x.TypeReprInfo with + member x.IsProvidedErasedTycon = + match x.TypeReprInfo with | TProvidedTypeRepr info -> info.IsErased | _ -> false /// Indicates if the entity is a generated provided type definition, i.e. not erased. - member x.IsProvidedGeneratedTycon = - match x.TypeReprInfo with + member x.IsProvidedGeneratedTycon = + match x.TypeReprInfo with | TProvidedTypeRepr info -> info.IsGenerated | _ -> false #endif /// Indicates if the entity is erased, either a measure definition, or an erased provided type definition - member x.IsErased = - x.IsMeasureableReprTycon + member x.IsErased = + x.IsMeasureableReprTycon #if !NO_TYPEPROVIDERS || x.IsProvidedErasedTycon #endif /// Get a blob of data indicating how this type is nested inside other namespaces, modules and types. - member x.CompilationPathOpt = x.entity_cpath + member x.CompilationPathOpt = x.entity_cpath /// Get a blob of data indicating how this type is nested inside other namespaces, modules and types. - member x.CompilationPath = - match x.CompilationPathOpt with - | Some cpath -> cpath + member x.CompilationPath = + match x.CompilationPathOpt with + | Some cpath -> cpath | None -> let tag = if x.IsModuleOrNamespace then TextTag.Module else TextTag.Class error(Error(FSComp.SR.tastTypeOrModuleNotConcrete(RichText.ofTag tag x.LogicalName), x.Range)) - + /// Get a table of fields for all the F#-defined record, struct and class fields in this type definition, including /// static fields, 'val' declarations and hidden fields from the compilation of implicit class constructions. - member x.AllFieldTable = - match x.TypeReprInfo with + member x.AllFieldTable = + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_rfields=x} -> x - | _ -> - match x.ExceptionInfo with + | _ -> + match x.ExceptionInfo with | TExnFresh x -> x - | _ -> - { FieldsByIndex = [| |] + | _ -> + { FieldsByIndex = [| |] FieldsByName = NameMap.empty } /// Get an array of fields for all the F#-defined record, struct and class fields in this type definition, including @@ -1081,39 +1087,39 @@ type Entity = /// Indicate if this is a type whose r.h.s. is known to be a union type definition. member x.IsUnionTycon = - match x.TypeReprInfo with + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_kind=TFSharpUnion} -> true | _ -> false /// Get the union cases and other union-type information for a type, if any - member x.UnionTypeInfo = - match x.TypeReprInfo with + member x.UnionTypeInfo = + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_kind=TFSharpUnion; fsobjmodel_cases=x} -> ValueSome x | _ -> ValueNone /// Get the union cases for a type, if any - member x.UnionCasesArray = - match x.UnionTypeInfo with - | ValueSome x -> x.CasesTable.CasesByIndex - | ValueNone -> [| |] + member x.UnionCasesArray = + match x.UnionTypeInfo with + | ValueSome x -> x.CasesTable.CasesByIndex + | ValueNone -> [| |] /// Get the union cases for a type, if any, as a list member x.UnionCasesAsList = x.UnionCasesArray |> Array.toList /// Get a union case of a type by name member x.GetUnionCaseByName n = - match x.UnionTypeInfo with + match x.UnionTypeInfo with | ValueSome x -> NameMap.tryFind n x.CasesTable.CasesByName | ValueNone -> None - + /// Create a new entity with empty, unlinked data. Only used during unpickling of F# metadata. - static member NewUnlinked() : Entity = - { entity_typars = Unchecked.defaultof<_> + static member NewUnlinked() : Entity = + { entity_typars = LazyWithContext.NotLazy [] entity_flags = Unchecked.defaultof<_> entity_stamp = Unchecked.defaultof<_> - entity_logical_name = Unchecked.defaultof<_> - entity_range = Unchecked.defaultof<_> + entity_logical_name = "" + entity_range = Unchecked.defaultof<_> entity_attribs = Unchecked.defaultof<_> entity_tycon_repr= Unchecked.defaultof<_> entity_tycon_tcaug= Unchecked.defaultof<_> @@ -1126,27 +1132,27 @@ type Entity = static member New _reason (data: Entity) : Entity = data /// Link an entity based on empty, unlinked data to the given data. Only used during unpickling of F# metadata. - member x.Link (tg: EntityData) = - x.entity_typars <- tg.entity_typars - x.entity_flags <- tg.entity_flags - x.entity_stamp <- tg.entity_stamp - x.entity_logical_name <- tg.entity_logical_name - x.entity_range <- tg.entity_range - x.entity_attribs <- tg.entity_attribs + member x.Link (tg: EntityData) = + x.entity_typars <- tg.entity_typars + x.entity_flags <- tg.entity_flags + x.entity_stamp <- tg.entity_stamp + x.entity_logical_name <- tg.entity_logical_name + x.entity_range <- tg.entity_range + x.entity_attribs <- tg.entity_attribs x.entity_tycon_repr <- tg.entity_tycon_repr x.entity_tycon_tcaug <- tg.entity_tycon_tcaug x.entity_modul_type <- tg.entity_modul_type x.entity_cpath <- tg.entity_cpath - x.entity_il_repr_cache <- tg.entity_il_repr_cache + x.entity_il_repr_cache <- tg.entity_il_repr_cache match tg.entity_opt_data with | Some tg -> - x.entity_opt_data <- + x.entity_opt_data <- Some { entity_compiled_name = tg.entity_compiled_name entity_other_range = tg.entity_other_range entity_kind = tg.entity_kind entity_xmldoc = tg.entity_xmldoc entity_xmldocsig = tg.entity_xmldocsig - entity_other_xmldoc = tg.entity_other_xmldoc + entity_other_xmldoc = tg.entity_other_xmldoc entity_tycon_abbrev = tg.entity_tycon_abbrev entity_tycon_repr_accessibility = tg.entity_tycon_repr_accessibility entity_accessibility = tg.entity_accessibility @@ -1158,15 +1164,15 @@ type Entity = member x.IsLinked = not (obj.ReferenceEquals(x.entity_attribs.AsList(), null)) /// Get the blob of information associated with an F# object-model type definition, i.e. class, interface, struct etc. - member x.FSharpTyconRepresentationData = - match x.TypeReprInfo with - | TFSharpTyconRepr x -> x + member x.FSharpTyconRepresentationData = + match x.TypeReprInfo with + | TFSharpTyconRepr x -> x | _ -> failwith "not an F# object model type definition" /// Indicate if this is a type definition backed by Abstract IL metadata. member x.IsILTycon = match x.TypeReprInfo with | TILObjectRepr _ -> true | _ -> false - /// Get the Abstract IL scope, nesting and metadata for this + /// Get the Abstract IL scope, nesting and metadata for this /// type definition, assuming it is backed by Abstract IL metadata. member x.ILTyconInfo = match x.TypeReprInfo with | TILObjectRepr data -> data | _ -> failwith "not a .NET type definition" @@ -1175,13 +1181,13 @@ type Entity = /// Indicates if this is an F# type definition whose r.h.s. is known to be a record type definition. member x.IsRecordTycon = - match x.TypeReprInfo with + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_kind=TFSharpRecord} -> true | _ -> false /// Indicates if this is an F# type definition whose r.h.s. is known to be a record type definition that is a value type. member x.IsStructRecordOrUnionTycon = - match x.TypeReprInfo with + match x.TypeReprInfo with | TFSharpTyconRepr { fsobjmodel_kind=TFSharpRecord } -> x.entity_flags.IsStructRecordOrUnionType | TFSharpTyconRepr { fsobjmodel_kind=TFSharpUnion } -> x.entity_flags.IsStructRecordOrUnionType | _ -> false @@ -1190,19 +1196,19 @@ type Entity = member x.TryIsByRefLike = x.entity_flags.TryIsByRefLike /// Set the on-demand analysis about whether the entity has the IsByRefLike attribute - member x.SetIsByRefLike b = x.entity_flags <- x.entity_flags.WithIsByRefLike b + member x.SetIsByRefLike b = x.entity_flags <- x.entity_flags.WithIsByRefLike b /// These two bits represents the on-demand analysis about whether the entity has the IsReadOnly attribute member x.TryIsReadOnly = x.entity_flags.TryIsReadOnly /// Set the on-demand analysis about whether the entity has the IsReadOnly attribute - member x.SetIsReadOnly b = x.entity_flags <- x.entity_flags.WithIsReadOnly b + member x.SetIsReadOnly b = x.entity_flags <- x.entity_flags.WithIsReadOnly b /// These two bits represents the on-demand analysis about whether the entity is assumed to be a readonly struct member x.TryIsAssumedReadOnly = x.entity_flags.TryIsAssumedReadOnly /// Set the on-demand analysis about whether the entity is assumed to be a readonly struct - member x.SetIsAssumedReadOnly b = x.entity_flags <- x.entity_flags.WithIsAssumedReadOnly b + member x.SetIsAssumedReadOnly b = x.entity_flags <- x.entity_flags.WithIsAssumedReadOnly b /// Indicates if this is an F# type definition known to be an F# class, interface, struct, /// delegate or enum. This isn't generally a particularly useful thing to know, @@ -1220,7 +1226,7 @@ type Entity = | TFSharpEnum -> true | _ -> false - /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses + /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses /// an assembly-code representation for the type, e.g. the primitive array type constructor. member x.IsAsmReprTycon = match x.TypeReprInfo with | TAsmRepr _ -> true | _ -> false @@ -1232,26 +1238,26 @@ type Entity = /// which in F# is called a 'unknown representation' type). member x.IsHiddenReprTycon = match x.TypeAbbrev, x.TypeReprInfo with | None, TNoRepr -> true | _ -> false - /// Indicates if this is an F#-defined interface type definition + /// Indicates if this is an F#-defined interface type definition member x.IsFSharpInterfaceTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpInterface -> true | _ -> false - /// Indicates if this is an F#-defined delegate type definition + /// Indicates if this is an F#-defined delegate type definition member x.IsFSharpDelegateTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpDelegate _ -> true | _ -> false - /// Indicates if this is an F#-defined enum type definition + /// Indicates if this is an F#-defined enum type definition member x.IsFSharpEnumTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpEnum -> true | _ -> false - /// Indicates if this is an F#-defined class type definition + /// Indicates if this is an F#-defined class type definition member x.IsFSharpClassTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpClass -> true | _ -> false - /// Indicates if this is a .NET-defined enum type definition + /// Indicates if this is a .NET-defined enum type definition member x.IsILEnumTycon = x.IsILTycon && x.ILTyconRawMetadata.IsEnum - /// Indicates if this is an enum type definition - member x.IsEnumTycon = + /// Indicates if this is an enum type definition + member x.IsEnumTycon = #if !NO_TYPEPROVIDERS - match x.TypeReprInfo with - | TProvidedTypeRepr info -> info.IsEnum + match x.TypeReprInfo with + | TProvidedTypeRepr info -> info.IsEnum | TProvidedNamespaceRepr _ -> false | _ -> #endif @@ -1269,14 +1275,14 @@ type Entity = /// Indicates if this is a .NET-defined struct or enum type definition, i.e. a value type definition member x.IsILStructOrEnumTycon = - x.IsILTycon && + x.IsILTycon && x.ILTyconRawMetadata.IsStructOrEnum /// Indicates if this is a struct or enum type definition, i.e. a value type definition, including struct records and unions - member x.IsStructOrEnumTycon = + member x.IsStructOrEnumTycon = #if !NO_TYPEPROVIDERS - match x.TypeReprInfo with - | TProvidedTypeRepr info -> info.IsStructOrEnum + match x.TypeReprInfo with + | TProvidedTypeRepr info -> info.IsStructOrEnum | TProvidedNamespaceRepr _ -> false | _ -> #endif @@ -1293,17 +1299,17 @@ type Entity = /// Gets the immediate members of an F# type definition, excluding compiler-generated ones. /// Note: result is alphabetically sorted, then for each name the results are in declaration order member x.MembersOfFSharpTyconSorted = - x.TypeContents.tcaug_adhoc - |> NameMultiMap.rangeReversingEachBucket + x.TypeContents.tcaug_adhoc + |> NameMultiMap.rangeReversingEachBucket |> List.filter (fun vref -> not vref.IsCompilerGenerated) /// Gets all immediate members of an F# type definition keyed by name, including compiler-generated ones. /// Note: result is a indexed table, and for each name the results are in reverse declaration order member x.MembersOfFSharpTyconByName = - x.TypeContents.tcaug_adhoc + x.TypeContents.tcaug_adhoc /// Gets any implicit hash/equals (with comparer argument) methods added to an F# record, union or struct type definition. - member x.GeneratedHashAndEqualsWithComparerValues = x.TypeContents.tcaug_hash_and_equals_withc + member x.GeneratedHashAndEqualsWithComparerValues = x.TypeContents.tcaug_hash_and_equals_withc /// Gets any implicit CompareTo (with comparer argument) methods added to an F# record, union or struct type definition. member x.GeneratedCompareToWithComparerValues = x.TypeContents.tcaug_compare_withc @@ -1315,8 +1321,8 @@ type Entity = member x.GeneratedHashAndEqualsValues = x.TypeContents.tcaug_equals /// Gets all implicit hash/equals/compare methods added to an F# record, union or struct type definition. - member x.AllGeneratedInterfaceImplsAndOverrides = - [ match x.GeneratedCompareToValues with + member x.AllGeneratedInterfaceImplsAndOverrides = + [ match x.GeneratedCompareToValues with | None -> () | Some (vref1, vref2) -> yield vref1; yield vref2 match x.GeneratedCompareToWithComparerValues with @@ -1329,20 +1335,20 @@ type Entity = | None -> () // vref4 is compiled as a sealed instance member, not an interface impl or an override. | Some (vref1, vref2, vref3, _) -> yield vref1; yield vref2; yield vref3 ] - + /// Gets the data indicating the compiled representation of a type or module in terms of Abstract IL data structures. member x.CompiledRepresentation = #if !NO_TYPEPROVIDERS - match x.TypeReprInfo with + match x.TypeReprInfo with // We should never be computing this property for erased types - | TProvidedTypeRepr info when info.IsErased -> + | TProvidedTypeRepr info when info.IsErased -> failwith "No compiled representation for provided erased type" - + // Generated types that are not relocated just point straight to the generated backing assembly, computed from "st". // These are used when running F# Interactive, which does not use static linking of provider-generated assemblies, // and also for types with relocation suppressed. - | TProvidedTypeRepr info when info.IsGenerated && info.IsSuppressRelocate -> + | TProvidedTypeRepr info when info.IsGenerated && info.IsSuppressRelocate -> let st = info.ProvidedType let tref = GetILTypeRefOfProvidedType (st, x.Range) let boxity = if x.IsStructOrEnumTycon then AsValue else AsObject @@ -1350,43 +1356,43 @@ type Entity = | TProvidedNamespaceRepr _ -> failwith "No compiled representation for provided namespace" | _ -> #endif - let ilTypeRefForCompilationPath (CompPath(sref, _, p)) item = - let rec top racc p = - match p with + let ilTypeRefForCompilationPath (CompPath(sref, _, p)) item = + let rec top racc p = + match p with | [] -> ILTypeRef.Create(sref, [], textOfPath (List.rev (item :: racc))) - | (h, isType) :: t -> - match isType with - | FSharpModuleWithSuffix | ModuleOrType -> + | (h, isType) :: t -> + match isType with + | FSharpModuleWithSuffix | ModuleOrType -> let outerTypeName = (textOfPath (List.rev (h :: racc))) ILTypeRef.Create(sref, (outerTypeName :: List.map fst t), item) - | _ -> + | _ -> top (h :: racc) t - top [] p + top [] p - cached x.CompiledReprCache (fun () -> - match x.ExceptionInfo with + cached x.CompiledReprCache (fun () -> + match x.ExceptionInfo with | TExnAbbrevRepr ecref2 -> ecref2.CompiledRepresentation | TExnAsmRepr tref -> CompiledTypeRepr.ILAsmNamed(tref, AsObject, Some (mkILTy AsObject (mkILTySpec (tref, [])))) - | _ -> - match x.TypeReprInfo with + | _ -> + match x.TypeReprInfo with | TAsmRepr ty -> CompiledTypeRepr.ILAsmOpen ty - | _ -> + | _ -> let boxity = if x.IsStructOrEnumTycon then AsValue else AsObject - let ilTypeRef = - match x.TypeReprInfo with + let ilTypeRef = + match x.TypeReprInfo with | TILObjectRepr (TILObjectReprData(ilScopeRef, ilEnclosingTypeDefs, ilTypeDef)) -> mkRefForNestedILTypeDef ilScopeRef (ilEnclosingTypeDefs, ilTypeDef) | _ -> ilTypeRefForCompilationPath x.CompilationPath x.CompiledName // Pre-allocate a ILType for monomorphic types, to reduce memory usage from Abstract IL nodes - let ilTypeOpt = - match x.Typars with - | [] -> Some (mkILTy boxity (mkILTySpec (ilTypeRef, []))) + let ilTypeOpt = + match x.Typars with + | [] -> Some (mkILTy boxity (mkILTySpec (ilTypeRef, []))) | _ -> None CompiledTypeRepr.ILAsmNamed (ilTypeRef, boxity, ilTypeOpt)) /// Gets the data indicating the compiled representation of a named type or module in terms of Abstract IL data structures. member x.CompiledRepresentationForNamedType = - match x.CompiledRepresentation with + match x.CompiledRepresentation with | CompiledTypeRepr.ILAsmNamed(tref, _, _) -> tref | CompiledTypeRepr.ILAsmOpen _ -> invalidOp (FSComp.SR.tastTypeHasAssemblyCodeRepresentation(x.DisplayNameWithStaticParametersAndUnderscoreTypars)) @@ -1396,7 +1402,7 @@ type Entity = /// Indicates if we have pre-determined that a type definition has a self-referential constructor using 'as x' member x.HasSelfReferentialConstructor = x.entity_flags.HasSelfReferentialConstructor - + member x.HasSignatureFile = x.SigRange <> x.DefinitionRange /// Set the custom attributes on an F# type definition. @@ -1432,28 +1438,28 @@ type Entity = type EntityData = Entity /// Represents the parent entity of a type definition, if any -type ParentRef = +type ParentRef = | Parent of parent: EntityRef | ParentNone - + /// Specifies the compiled representations of type and exception definitions. Basically -/// just an ILTypeRef. Computed and cached by later phases. Stored in -/// type and exception definitions. Not pickled. Store an optional ILType object for +/// just an ILTypeRef. Computed and cached by later phases. Stored in +/// type and exception definitions. Not pickled. Store an optional ILType object for /// non-generic types. [] -type CompiledTypeRepr = +type CompiledTypeRepr = /// An AbstractIL type representation that is just the name of a type. /// /// CompiledTypeRepr.ILAsmNamed (ilTypeRef, ilBoxity, ilTypeOpt) - /// + /// /// The ilTypeOpt is present for non-generic types. It is an ILType corresponding to the first two elements of the case. This /// prevents reallocation of the ILType each time we need to generate it. For generic types, it is None. - | ILAsmNamed of - ilTypeRef: ILTypeRef * - ilBoxity: ILBoxity * + | ILAsmNamed of + ilTypeRef: ILTypeRef * + ilBoxity: ILBoxity * ilTypeOpt: ILType option - + /// An AbstractIL type representation that may include type variables // This case is only used for types defined in the F# library by their translation to ILASM types, e.g. // type ``[]``<'T> = (# "!0[]" #) @@ -1462,7 +1468,7 @@ type CompiledTypeRepr = // type byref<'T> = (# "!0&" #) // type nativeptr<'T when 'T: unmanaged> = (# "native int" #) // type ilsigptr<'T> = (# "!0*" #) - | ILAsmOpen of ilType: ILType + | ILAsmOpen of ilType: ILType [] member x.DebugText = x.ToString() @@ -1470,32 +1476,32 @@ type CompiledTypeRepr = override x.ToString() = "CompiledTypeRepr(...)" [] -type TyconAugmentation = +type TyconAugmentation = { - /// This is the value implementing the auto-generated comparison - /// semantics if any. It is not present if the type defines its own implementation - /// of IComparable or if the type doesn't implement IComparable implicitly. + /// This is the value implementing the auto-generated comparison + /// semantics if any. It is not present if the type defines its own implementation + /// of IComparable or if the type doesn't implement IComparable implicitly. mutable tcaug_compare: (ValRef * ValRef) option - + /// This is the value implementing the auto-generated comparison /// semantics if any. It is not present if the type defines its own implementation /// of IStructuralComparable or if the type doesn't implement IComparable implicitly. - mutable tcaug_compare_withc: ValRef option + mutable tcaug_compare_withc: ValRef option - /// This is the value implementing the auto-generated equality - /// semantics if any. It is not present if the type defines its own implementation - /// of Object.Equals or if the type doesn't override Object.Equals implicitly. + /// This is the value implementing the auto-generated equality + /// semantics if any. It is not present if the type defines its own implementation + /// of Object.Equals or if the type doesn't override Object.Equals implicitly. mutable tcaug_equals: (ValRef * ValRef) option /// This is the value implementing the auto-generated equality /// semantics if any. It is not present if the type defines its own implementation /// of IStructuralEquatable or if the type doesn't override Object.Equals implicitly. - mutable tcaug_hash_and_equals_withc: (ValRef * ValRef * ValRef * ValRef option) option + mutable tcaug_hash_and_equals_withc: (ValRef * ValRef * ValRef * ValRef option) option - /// True if the type defined an Object.GetHashCode method. In this + /// True if the type defined an Object.GetHashCode method. In this /// case we give a warning if we auto-generate a hash method since the semantics may not match up - mutable tcaug_hasObjectGetHashCode: bool - + mutable tcaug_hasObjectGetHashCode: bool + /// Properties, methods etc. in declaration order. The boolean flag for each indicates if the /// member is known to be an explicit interface implementation. This must be computed and /// saved prior to remapping assembly information. @@ -1507,18 +1513,18 @@ type TyconAugmentation = /// Properties, methods etc. as lookup table mutable tcaug_adhoc: NameMultiMap - - /// Interface implementations - boolean indicates compiler-generated - mutable tcaug_interfaces: (TType * bool * range) list - - /// Super type, if any - mutable tcaug_super: TType option - - /// Set to true at the end of the scope where proper augmentations are allowed - mutable tcaug_closed: bool - - /// Set to true if the type is determined to be abstract - mutable tcaug_abstract: bool + + /// Interface implementations - boolean indicates compiler-generated + mutable tcaug_interfaces: (TType * bool * range) list + + /// Super type, if any + mutable tcaug_super: TType option + + /// Set to true at the end of the scope where proper augmentations are allowed + mutable tcaug_closed: bool + + /// Set to true if the type is determined to be abstract + mutable tcaug_abstract: bool } /// Record a member in declaration order, allocating the list on first use @@ -1550,16 +1556,16 @@ type TyconAugmentation = member tcaug.SetHasObjectGetHashCode b = tcaug.tcaug_hasObjectGetHashCode <- b static member Create() = - { tcaug_compare=None - tcaug_compare_withc=None - tcaug_equals=None - tcaug_hash_and_equals_withc=None - tcaug_hasObjectGetHashCode=false - tcaug_adhoc=NameMultiMap.empty + { tcaug_compare=None + tcaug_compare_withc=None + tcaug_equals=None + tcaug_hash_and_equals_withc=None + tcaug_hasObjectGetHashCode=false + tcaug_adhoc=NameMultiMap.empty tcaug_adhoc_list=null tcaug_super=None - tcaug_interfaces=[] - tcaug_closed=false + tcaug_interfaces=[] + tcaug_closed=false tcaug_abstract=false } [] @@ -1569,15 +1575,15 @@ type TyconAugmentation = /// The information for the contents of a type. Also used for a provided namespace. [] -type TyconRepresentation = +type TyconRepresentation = - /// Indicates the type is a class, struct, enum, delegate or interface + /// Indicates the type is a class, struct, enum, delegate or interface | TFSharpTyconRepr of FSharpTyconData /// Indicates the type is a type from a .NET assembly without F# metadata. | TILObjectRepr of TILObjectReprData - /// Indicates the type is implemented as IL assembly code using the given closed Abstract IL type + /// Indicates the type is implemented as IL assembly code using the given closed Abstract IL type | TAsmRepr of ILType /// Indicates the type is parameterized on a measure (e.g. float<_>) but erases to some other type (e.g. float) @@ -1586,21 +1592,21 @@ type TyconRepresentation = #if !NO_TYPEPROVIDERS /// TProvidedTypeRepr /// - /// Indicates the representation information for a provided type. + /// Indicates the representation information for a provided type. | TProvidedTypeRepr of TProvidedTypeInfo - /// Indicates the representation information for a provided namespace. + /// Indicates the representation information for a provided namespace. // // Note, the list could probably be a list of IProvidedNamespace rather than ITypeProvider | TProvidedNamespaceRepr of ResolutionEnvironment * Tainted list #endif - /// The 'NoRepr' value here has four meanings: + /// The 'NoRepr' value here has four meanings: /// (1) it indicates 'not yet known' during the first 2 phases of establishing type definitions /// (2) it indicates 'no representation', i.e. 'type X' in signatures /// (3) it is the setting used for exception definitions (!) /// (4) it is the setting used for modules and namespaces. - /// + /// /// It would be better to separate the "not yet known" and other cases out. /// The information for exception definitions should be folded into here. | TNoRepr @@ -1608,11 +1614,11 @@ type TyconRepresentation = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x [] -type TILObjectReprData = - | TILObjectReprData of scope: ILScopeRef * nesting: ILTypeDef list * definition: ILTypeDef +type TILObjectReprData = + | TILObjectReprData of scope: ILScopeRef * nesting: ILTypeDef list * definition: ILTypeDef [] member x.DebugText = x.ToString() @@ -1624,8 +1630,8 @@ type TILObjectReprData = /// The information kept about a provided type [] -type TProvidedTypeInfo = - { +type TProvidedTypeInfo = + { /// The parameters given to the provider that provided to this type. ResolutionEnvironment: ResolutionEnvironment @@ -1636,36 +1642,36 @@ type TProvidedTypeInfo = /// The base type of the type. We use it to compute the compiled representation of the type for erased types. /// Reading is delayed, since it does an import on the underlying type - LazyBaseType: LazyWithContext + LazyBaseType: LazyWithContext /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsClass: bool + IsClass: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsSealed: bool + IsSealed: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsAbstract: bool + IsAbstract: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsInterface: bool + IsInterface: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsStructOrEnum: bool + IsStructOrEnum: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsEnum: bool + IsEnum: bool /// A type read from the provided type and used to compute basic properties of the type definition. /// Reading is delayed, since it does an import on the underlying type - UnderlyingTypeOfEnum: unit -> TType + UnderlyingTypeOfEnum: unit -> TType /// A flag read from the provided type and used to compute basic properties of the type definition. /// Reading is delayed, since it looks at the .BaseType - IsDelegate: unit -> bool + IsDelegate: unit -> bool /// Indicates the type is erased - IsErased: bool + IsErased: bool /// Indicates the type is generated, but type-relocation is suppressed IsSuppressRelocate: bool @@ -1675,8 +1681,8 @@ type TProvidedTypeInfo = member info.IsGenerated = not info.IsErased /// Gets the base type of an erased provided type - member info.BaseTypeForErased (m, objTy) = - if info.IsErased then info.LazyBaseType.Force (m, objTy) + member info.BaseTypeForErased (m, objTy) = + if info.IsErased then info.LazyBaseType.Force (m, objTy) else failwith "expect erased type" [] @@ -1686,7 +1692,7 @@ type TProvidedTypeInfo = #endif -type FSharpTyconKind = +type FSharpTyconKind = /// Indicates the type is an F#-declared record | TFSharpRecord @@ -1694,34 +1700,34 @@ type FSharpTyconKind = | TFSharpUnion /// Indicates the type is an F#-declared class (also used for units-of-measure) - | TFSharpClass + | TFSharpClass - /// Indicates the type is an F#-declared interface - | TFSharpInterface + /// Indicates the type is an F#-declared interface + | TFSharpInterface - /// Indicates the type is an F#-declared struct - | TFSharpStruct + /// Indicates the type is an F#-declared struct + | TFSharpStruct - /// Indicates the type is an F#-declared delegate with the given Invoke signature - | TFSharpDelegate of slotSig: SlotSig + /// Indicates the type is an F#-declared delegate with the given Invoke signature + | TFSharpDelegate of slotSig: SlotSig - /// Indicates the type is an F#-declared enumeration + /// Indicates the type is an F#-declared enumeration | TFSharpEnum - + /// Represents member values and class fields relating to the F# object model [] -type FSharpTyconData = - { +type FSharpTyconData = + { /// Indicates the cases of a union type fsobjmodel_cases: TyconUnionData - /// Indicates whether the type declaration is an F# class, interface, enum, delegate or struct + /// Indicates whether the type declaration is an F# class, interface, enum, delegate or struct fsobjmodel_kind: FSharpTyconKind - /// The declared abstract slots of the class, interface or struct + /// The declared abstract slots of the class, interface or struct fsobjmodel_vslots: ValRef list - /// The fields of the class, struct or enum + /// The fields of the class, struct or enum fsobjmodel_rfields: TyconRecdFields } @@ -1732,18 +1738,18 @@ type FSharpTyconData = /// Represents record fields in an F# type definition [] -type TyconRecdFields = - { - /// The fields of the record, in declaration order. +type TyconRecdFields = + { + /// The fields of the record, in declaration order. FieldsByIndex: RecdField[] - - /// The fields of the record, indexed by name. + + /// The fields of the record, indexed by name. FieldsByName: NameMap } /// Get a field by index - member x.FieldByIndex n = - if n >= 0 && n < x.FieldsByIndex.Length then x.FieldsByIndex[n] + member x.FieldByIndex n = + if n >= 0 && n < x.FieldsByIndex.Length then x.FieldsByIndex[n] else failwith "FieldByIndex" /// Get a field by name @@ -1753,10 +1759,10 @@ type TyconRecdFields = member x.AllFieldsAsList = x.FieldsByIndex |> Array.toList /// Get all non-compiler-generated fields as a list - member x.TrueFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsCompilerGenerated) + member x.TrueFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsCompilerGenerated) /// Get all non-compiler-generated instance fields as a list - member x.TrueInstanceFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsStatic && not f.IsCompilerGenerated) + member x.TrueInstanceFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsStatic && not f.IsCompilerGenerated) [] member x.DebugText = x.ToString() @@ -1765,18 +1771,18 @@ type TyconRecdFields = /// Represents union cases in an F# type definition [] -type TyconUnionCases = - { - /// The cases of the discriminated union, in declaration order. +type TyconUnionCases = + { + /// The cases of the discriminated union, in declaration order. CasesByIndex: UnionCase[] - /// The cases of the discriminated union, indexed by name. + /// The cases of the discriminated union, indexed by name. CasesByName: NameMap } /// Get a union case by index - member x.GetUnionCaseByIndex n = - if n >= 0 && n < x.CasesByIndex.Length then x.CasesByIndex[n] + member x.GetUnionCaseByIndex n = + if n >= 0 && n < x.CasesByIndex.Length then x.CasesByIndex[n] else invalidArg "n" "GetUnionCaseByIndex" /// Get the union cases as a list @@ -1792,11 +1798,11 @@ type TyconUnionCases = type TyconUnionData = { - /// The cases contained in the discriminated union. + /// The cases contained in the discriminated union. CasesTable: TyconUnionCases - /// The ILX data structure representing the discriminated union. - CompiledRepresentation: IlxUnionRef cache + /// The ILX data structure representing the discriminated union. + CompiledRepresentation: IlxUnionRef cache } /// Get the union cases as a list @@ -1810,63 +1816,63 @@ type TyconUnionData = /// Represents a union case in an F# type definition [] type UnionCase = - { - /// Data carried by the case. + { + /// Data carried by the case. FieldTable: TyconRecdFields - /// Return type constructed by the case. Normally exactly the type of the enclosing type, sometimes an abbreviation of it + /// Return type constructed by the case. Normally exactly the type of the enclosing type, sometimes an abbreviation of it ReturnType: TType - /// Documentation for the case + /// Documentation for the case OwnXmlDoc: XmlDoc - + /// Documentation for the case from signature file mutable OtherXmlDoc: XmlDoc /// XML documentation signature for the case mutable XmlDocSig: string - /// Name/range of the case - Id: Ident + /// Name/range of the case + Id: Ident - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation // MUTABILITY: used when propagating signature attributes into the implementation. mutable OtherRangeOpt: (range * bool) option - /// Indicates the declared visibility of the union constructor, not taking signatures into account - Accessibility: Accessibility + /// Indicates the declared visibility of the union constructor, not taking signatures into account + Accessibility: Accessibility - /// Attributes, attached to the generated static method to make instances of the case + /// Attributes, attached to the generated static method to make instances of the case // MUTABILITY: used when propagating signature attributes into the implementation. mutable Attribs: Attribs } - - /// Documentation for the case + + /// Documentation for the case member uc.XmlDoc: XmlDoc = if not uc.OwnXmlDoc.IsEmpty then uc.OwnXmlDoc else uc.OtherXmlDoc - + /// Get the declaration location of the union case member uc.Range = uc.Id.idRange /// Get the definition location of the union case - member uc.DefinitionRange = - match uc.OtherRangeOpt with + member uc.DefinitionRange = + match uc.OtherRangeOpt with | Some (m, true) -> m - | _ -> uc.Range + | _ -> uc.Range /// Get the signature location of the union case - member uc.SigRange = - match uc.OtherRangeOpt with + member uc.SigRange = + match uc.OtherRangeOpt with | Some (m, false) -> m - | _ -> uc.Range + | _ -> uc.Range member x.SetOtherXmlDoc xmlDoc = x.OtherXmlDoc <- xmlDoc - + /// Get the logical name of the union case member uc.LogicalName = uc.Id.idText @@ -1894,7 +1900,7 @@ type UnionCase = else idText /// Get the full array of fields of the union case - member uc.RecdFieldsArray = uc.FieldTable.FieldsByIndex + member uc.RecdFieldsArray = uc.FieldTable.FieldsByIndex /// Get the full list of fields of the union case member uc.RecdFields = uc.FieldTable.FieldsByIndex |> Array.toList @@ -1918,103 +1924,103 @@ type UnionCase = [] type RecdField = { - /// Is the field declared mutable in F#? + /// Is the field declared mutable in F#? rfield_mutable: bool - /// Documentation for the field + /// Documentation for the field rfield_xmldoc: XmlDoc - - /// Documentation for the field from signature file + + /// Documentation for the field from signature file mutable rfield_otherxmldoc: XmlDoc /// XML Documentation signature for the field mutable rfield_xmldocsig: string - /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor + /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor rfield_type: TType - /// Indicates a static field + /// Indicates a static field rfield_static: bool - /// Indicates a volatile field + /// Indicates a volatile field rfield_volatile: bool - /// Indicates a compiler generated field, not visible to Intellisense or name resolution + /// Indicates a compiler generated field, not visible to Intellisense or name resolution rfield_secret: bool - /// The default initialization info, for static literals - rfield_const: Const option + /// The default initialization info, for static literals + rfield_const: Const option - /// Indicates the declared visibility of the field, not taking signatures into account - rfield_access: Accessibility + /// Indicates the declared visibility of the field, not taking signatures into account + rfield_access: Accessibility - /// Attributes attached to generated property + /// Attributes attached to generated property // MUTABILITY: used when propagating signature attributes into the implementation. - mutable rfield_pattribs: Attribs + mutable rfield_pattribs: Attribs - /// Attributes attached to generated field + /// Attributes attached to generated field // MUTABILITY: used when propagating signature attributes into the implementation. - mutable rfield_fattribs: Attribs + mutable rfield_fattribs: Attribs - /// Name/declaration-location of the field + /// Name/declaration-location of the field rfield_id: Ident rfield_name_generated: bool - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation // MUTABILITY: used when propagating signature attributes into the implementation. mutable rfield_other_range: (range * bool) option } - /// Indicates the declared visibility of the field, not taking signatures into account + /// Indicates the declared visibility of the field, not taking signatures into account member v.Accessibility = v.rfield_access - /// Attributes attached to generated property + /// Attributes attached to generated property member v.PropertyAttribs = v.rfield_pattribs - /// Attributes attached to generated field + /// Attributes attached to generated field member v.FieldAttribs = v.rfield_fattribs - /// Get the declaration location of the field + /// Get the declaration location of the field member v.Range = v.rfield_id.idRange - /// Get the definition location of the field - member v.DefinitionRange = - match v.rfield_other_range with + /// Get the definition location of the field + member v.DefinitionRange = + match v.rfield_other_range with | Some (m, true) -> m - | _ -> v.Range + | _ -> v.Range - /// Get the signature location of the field - member v.SigRange = - match v.rfield_other_range with + /// Get the signature location of the field + member v.SigRange = + match v.rfield_other_range with | Some (m, false) -> m - | _ -> v.Range + | _ -> v.Range - /// Name/declaration-location of the field + /// Name/declaration-location of the field member v.Id = v.rfield_id - /// Name of the field + /// Name of the field member v.LogicalName = v.rfield_id.idText /// Name of the field. For fields this is the same as the logical name. member v.DisplayNameCore = v.LogicalName - /// Name of the field + /// Name of the field member v.DisplayName = v.DisplayNameCore |> ConvertLogicalNameToDisplayName - /// Indicates a compiler generated field, not visible to Intellisense or name resolution + /// Indicates a compiler generated field, not visible to Intellisense or name resolution member v.IsCompilerGenerated = v.rfield_secret - /// Is the field declared mutable in F#? + /// Is the field declared mutable in F#? member v.IsMutable = v.rfield_mutable - /// Indicates a static field + /// Indicates a static field member v.IsStatic = v.rfield_static - /// Indicates a volatile field + /// Indicates a volatile field member v.IsVolatile = v.rfield_volatile - /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor + /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor member v.FormalType = v.rfield_type /// XML Documentation signature for the field @@ -2023,27 +2029,27 @@ type RecdField = v.rfield_xmldoc else v.rfield_otherxmldoc - + member v.SetOtherXmlDoc (xmlDoc: XmlDoc) = - v.rfield_otherxmldoc <- xmlDoc + v.rfield_otherxmldoc <- xmlDoc /// Get or set the XML documentation signature for the field member v.XmlDocSig with get() = v.rfield_xmldocsig and set x = v.rfield_xmldocsig <- x - /// The default initialization info, for static literals - member v.LiteralValue = - match v.rfield_const with + /// The default initialization info, for static literals + member v.LiteralValue = + match v.rfield_const with | None -> None | Some Const.Zero -> None | Some k -> Some k /// Indicates if the field is zero-initialized - member v.IsZeroInit = - match v.rfield_const with - | None -> false - | Some Const.Zero -> true + member v.IsZeroInit = + match v.rfield_const with + | None -> false + | Some Const.Zero -> true | _ -> false [] @@ -2055,30 +2061,30 @@ type RecdField = [] type ExceptionInfo = - /// Indicates that an exception is an abbreviation for the given exception - | TExnAbbrevRepr of TyconRef + /// Indicates that an exception is an abbreviation for the given exception + | TExnAbbrevRepr of TyconRef - /// Indicates that an exception is shorthand for the given .NET exception type + /// Indicates that an exception is shorthand for the given .NET exception type | TExnAsmRepr of ILTypeRef - /// Indicates that an exception carries the given record of values + /// Indicates that an exception carries the given record of values | TExnFresh of TyconRecdFields - /// Indicates that an exception is abstract, i.e. is in a signature file, and we do not know the representation + /// Indicates that an exception is abstract, i.e. is in a signature file, and we do not know the representation | TExnNone // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// Represents the contents of a module or namespace [] -type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, entities: QueueList) = +type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, entities: QueueList) = /// Mutation used during compilation of FSharp.Core.dll - let mutable entities = entities + let mutable entities = entities #if !NO_TYPEPROVIDERS // One Entity per provided type or namespace even when linked concurrently from several files (graph-based checking). @@ -2090,7 +2096,7 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en // recomputes after a concurrent provided-type append instead of trusting a stale memo. Accessed via // Interlocked/Volatile rather than [] so its address can be taken for those intrinsics. let mutable entitiesVersion = 0 - + // Lookup tables keyed the way various clients expect them to be keyed. // We attach them here so we don't need to store lookup tables via any other technique. // @@ -2118,15 +2124,15 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en let mutable allValsAndMembersByPartialLinkageKeyCache: MultiMap option = None let mutable allValsByLogicalNameCache: NameMap option = None - - /// Namespace or module-compiled-as-type? - member _.ModuleOrNamespaceKind = kind - - /// Values, including members in F# types in this module-or-namespace-fragment. + + /// Namespace or module-compiled-as-type? + member _.ModuleOrNamespaceKind = kind + + /// Values, including members in F# types in this module-or-namespace-fragment. member _.AllValsAndMembers = vals - /// Type, mapping mangled name to Tycon, e.g. - //// "Dictionary`2" --> Tycon + /// Type, mapping mangled name to Tycon, e.g. + //// "Dictionary`2" --> Tycon //// "ListModule" --> Tycon with module info //// "FooException" --> Tycon with exception info member _.AllEntities = entities @@ -2175,127 +2181,127 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en let entity = create () mtyp.AddModuleOrNamespaceByMutation entity entity) -#endif - +#endif + /// Return a new module or namespace type with an entity added. - member _.AddEntity(tycon: Tycon) = + member _.AddEntity(tycon: Tycon) = ModuleOrNamespaceType(kind, vals, entities.AppendOne tycon) - + /// Return a new module or namespace type with a value added. - member _.AddVal(vspec: Val) = + member _.AddVal(vspec: Val) = ModuleOrNamespaceType(kind, vals.AppendOne vspec, entities) - + /// Get a table of the active patterns defined in this module. member _.ActivePatternElemRefLookupTable = activePatternElemRefCache - - /// Get a list of types defined within this module, namespace or type. + + /// Get a list of types defined within this module, namespace or type. member _.TypeDefinitions = entities |> Seq.filter (fun x -> not x.IsFSharpException && not x.IsModuleOrNamespace) |> Seq.toList - /// Get a list of F# exception definitions defined within this module, namespace or type. + /// Get a list of F# exception definitions defined within this module, namespace or type. member _.ExceptionDefinitions = entities |> Seq.filter (fun x -> x.IsFSharpException) |> Seq.toList - /// Get a list of module and namespace definitions defined within this module, namespace or type. + /// Get a list of module and namespace definitions defined within this module, namespace or type. member _.ModuleAndNamespaceDefinitions = entities |> Seq.filter (fun x -> x.IsModuleOrNamespace) |> Seq.toList - /// Get a list of type and exception definitions defined within this module, namespace or type. + /// Get a list of type and exception definitions defined within this module, namespace or type. member _.TypeAndExceptionDefinitions = entities |> Seq.filter (fun x -> not x.IsModuleOrNamespace) |> Seq.toList - /// Get a table of types defined within this module, namespace or type. The - /// table is indexed by both name and generic arity. This means that for generic + /// Get a table of types defined within this module, namespace or type. The + /// table is indexed by both name and generic arity. This means that for generic /// types "List`1", the entry (List, 1) will be present. - member mtyp.TypesByDemangledNameAndArity = + member mtyp.TypesByDemangledNameAndArity = let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &tyconsByDemangledNameAndArityCache (fun () -> + cacheOptByrefByVersion version &tyconsByDemangledNameAndArityCache (fun () -> LayeredMap.Empty.AddMany( mtyp.TypeAndExceptionDefinitions |> List.map (fun (tc: Tycon) -> Construct.KeyTyconByDecodedName tc.LogicalName tc) |> List.toArray)) - /// Get a table of types defined within this module, namespace or type. The + /// Get a table of types defined within this module, namespace or type. The /// table is indexed by both name and, for generic types, also by mangled name. - member mtyp.TypesByAccessNames = + member mtyp.TypesByAccessNames = let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &tyconsByAccessNamesCache (fun () -> + cacheOptByrefByVersion version &tyconsByAccessNamesCache (fun () -> LayeredMultiMap.Empty.AddMany (mtyp.TypeAndExceptionDefinitions |> List.toArray |> Array.collect (fun (tc: Tycon) -> Construct.KeyTyconByAccessNames tc.LogicalName tc))) // REVIEW: we can remove this lookup and use AllEntitiesByMangledName instead? - member mtyp.TypesByMangledName = - let addTyconByMangledName (x: Tycon) tab = NameMap.add x.LogicalName x tab + member mtyp.TypesByMangledName = + let addTyconByMangledName (x: Tycon) tab = NameMap.add x.LogicalName x tab let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &tyconsByMangledNameCache (fun () -> + cacheOptByrefByVersion version &tyconsByMangledNameCache (fun () -> List.foldBack addTyconByMangledName mtyp.TypeAndExceptionDefinitions Map.empty) /// Get a table of entities indexed by both logical and compiled names - member mtyp.AllEntitiesByCompiledAndLogicalMangledNames: NameMap = - let addEntityByMangledName (x: Entity) tab = + member mtyp.AllEntitiesByCompiledAndLogicalMangledNames: NameMap = + let addEntityByMangledName (x: Entity) tab = let name1 = x.LogicalName let name2 = x.CompiledName - let tab = NameMap.add name1 x tab + let tab = NameMap.add name1 x tab if name1 = name2 then tab - else NameMap.add name2 x tab - + else NameMap.add name2 x tab + let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &allEntitiesByMangledNameCache (fun () -> + cacheOptByrefByVersion version &allEntitiesByMangledNameCache (fun () -> QueueList.foldBack addEntityByMangledName entities Map.empty) /// Get a table of entities indexed by both logical name - member _.AllEntitiesByLogicalMangledName: NameMap = - let addEntityByMangledName (x: Entity) tab = NameMap.add x.LogicalName x tab + member _.AllEntitiesByLogicalMangledName: NameMap = + let addEntityByMangledName (x: Entity) tab = NameMap.add x.LogicalName x tab QueueList.foldBack addEntityByMangledName entities Map.empty /// Get a table of values and members indexed by partial linkage key, which includes name, the mangled name of the parent type (if any), /// and the method argument count (if any). - member _.AllValsAndMembersByPartialLinkageKey = - let addValByMangledName (x: Val) tab = + member _.AllValsAndMembersByPartialLinkageKey = + let addValByMangledName (x: Val) tab = if x.IsCompiledAsTopLevel then let key = x.GetLinkagePartialKey() - MultiMap.add key x tab + MultiMap.add key x tab else tab - cacheOptByref &allValsAndMembersByPartialLinkageKeyCache (fun () -> + cacheOptByref &allValsAndMembersByPartialLinkageKeyCache (fun () -> QueueList.foldBack addValByMangledName vals MultiMap.empty) /// Try to find the member with the given linkage key in the given module. - member mtyp.TryLinkVal(ccu: CcuThunk, key: ValLinkageFullKey) = + member mtyp.TryLinkVal(ccu: CcuThunk, key: ValLinkageFullKey) = mtyp.AllValsAndMembersByPartialLinkageKey |> MultiMap.find key.PartialKey - |> List.tryFind (fun v -> match key.TypeForLinkage with + |> List.tryFind (fun v -> match key.TypeForLinkage with | None -> true | Some keyTy -> ccu.MemberSignatureEquality(keyTy, v.Type)) |> ValueOption.ofOption /// Get a table of values indexed by logical name - member _.AllValsByLogicalName = - let addValByName (x: Val) tab = + member _.AllValsByLogicalName = + let addValByName (x: Val) tab = // Note: names may occur twice prior to raising errors about this in PostTypeCheckSemanticChecks // Earlier ones take precedence since we report errors about the later ones - if not x.IsMember && not x.IsCompilerGenerated then - NameMap.add x.LogicalName x tab + if not x.IsMember && not x.IsCompilerGenerated then + NameMap.add x.LogicalName x tab else tab - cacheOptByref &allValsByLogicalNameCache (fun () -> + cacheOptByref &allValsByLogicalNameCache (fun () -> QueueList.foldBack addValByName vals Map.empty) /// Compute a table of values and members indexed by logical name. - member _.AllValsAndMembersByLogicalNameUncached = - let addValByName (x: Val) tab = - if not x.IsCompilerGenerated then - MultiMap.add x.LogicalName x tab + member _.AllValsAndMembersByLogicalNameUncached = + let addValByName (x: Val) tab = + if not x.IsCompilerGenerated then + MultiMap.add x.LogicalName x tab else tab QueueList.foldBack addValByName vals MultiMap.empty /// Get a table of F# exception definitions indexed by demangled name, so 'FailureException' is indexed by 'Failure' - member mtyp.ExceptionDefinitionsByDemangledName = + member mtyp.ExceptionDefinitionsByDemangledName = let add (tycon: Tycon) acc = NameMap.add tycon.LogicalName tycon acc - cacheOptByref &exconsByDemangledNameCache (fun () -> + cacheOptByref &exconsByDemangledNameCache (fun () -> List.foldBack add mtyp.ExceptionDefinitions Map.empty) /// Get a table of nested module and namespace fragments indexed by demangled name (so 'ListModule' becomes 'List') - member _.ModulesAndNamespacesByDemangledName = - let add (entity: Entity) acc = - if entity.IsModuleOrNamespace then + member _.ModulesAndNamespacesByDemangledName = + let add (entity: Entity) acc = + if entity.IsModuleOrNamespace then NameMap.add entity.DemangledModuleOrNamespaceName entity acc else acc let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &modulesByDemangledNameCache (fun () -> + cacheOptByrefByVersion version &modulesByDemangledNameCache (fun () -> QueueList.foldBack add entities Map.empty) [] @@ -2304,13 +2310,13 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en override _.ToString() = "ModuleOrNamespaceType(...)" /// Represents a module or namespace definition in the typed AST -type ModuleOrNamespace = Entity +type ModuleOrNamespace = Entity /// Represents a type or exception definition in the typed AST type Tycon = Entity -let getNameOfScopeRef sref = - match sref with +let getNameOfScopeRef sref = + match sref with | ILScopeRef.Local -> "" | ILScopeRef.Module mref -> mref.Name | ILScopeRef.Assembly aref -> aref.Name @@ -2324,7 +2330,7 @@ let private isInternalCompPath x = let private (|Public|Internal|Private|) (TAccess p) = match p with | [] -> Public - | _ when List.forall isInternalCompPath p -> Internal + | _ when List.forall isInternalCompPath p -> Internal | _ -> Private let getSyntaxAccessForCompPath (TAccess a) = match a with | CompPath(_, sa, _) :: _ -> sa | _ -> SyntaxAccess.Unknown @@ -2337,7 +2343,7 @@ let updateSyntaxAccessForCompPath access syntaxAccess = /// Represents the constraint on access for a construct [] type Accessibility = - /// Indicates the construct can only be accessed from any code in the given type constructor, module or assembly. [] indicates global scope. + /// Indicates the construct can only be accessed from any code in the given type constructor, module or assembly. [] indicates global scope. | TAccess of compilationPaths: CompilationPath list member public x.IsPublic = match x with Public -> true | _ -> false @@ -2368,7 +2374,7 @@ type Accessibility = override x.ToString() = match x with | TAccess paths -> - let mangledTextOfCompPath (CompPath(scoref, _, path)) = getNameOfScopeRef scoref + "/" + textOfPath (List.map fst path) + let mangledTextOfCompPath (CompPath(scoref, _, path)) = getNameOfScopeRef scoref + "/" + textOfPath (List.map fst path) let scopename = if x.IsPublic then "public" elif x.IsInternal then "internal" @@ -2384,18 +2390,18 @@ type Accessibility = [] type TyparOptionalData = { - /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation + /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation /// The storage for the IL name for the type parameter. - mutable typar_il_name: string option + mutable typar_il_name: string option /// The documentation for the type parameter. Empty for inference variables. /// MUTABILITY: for linking when unpickling mutable typar_xmldoc: XmlDoc /// The inferred constraints for the type parameter or inference variable. - mutable typar_constraints: TyparConstraint list + mutable typar_constraints: TyparConstraint list - /// The declared attributes of the type parameter. Empty for type inference variables. + /// The declared attributes of the type parameter. Empty for type inference variables. mutable typar_attribs: Attribs /// Set to true if the typar is contravariant, i.e. declared as in C# @@ -2414,30 +2420,30 @@ type TyparData = Typar /// A declared generic type/measure parameter, or a type/measure inference variable. [] -type Typar = +type Typar = { - /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation + /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation /// The identifier for the type parameter - mutable typar_id: Ident - + mutable typar_id: Ident + /// The flag data for the type parameter mutable typar_flags: TyparFlags - + /// The unique stamp of the type parameter /// MUTABILITY: for linking when unpickling - mutable typar_stamp: Stamp - - /// An inferred equivalence for a type inference variable. + mutable typar_stamp: Stamp + + /// An inferred equivalence for a type inference variable. mutable typar_solution: TType option /// A cached TAST type used when this type variable is used as type. mutable typar_astype: TType - + /// The optional data for the type parameter mutable typar_opt_data: TyparOptionalData option } - /// The name of the type parameter + /// The name of the type parameter member x.Name = x.typar_id.idText /// The range of the identifier for the type parameter definition @@ -2453,16 +2459,16 @@ type Typar = member x.Solution = x.typar_solution /// The inferred constraints for the type inference variable, if any - member x.Constraints = + member x.Constraints = match x.typar_opt_data with | Some optData -> optData.typar_constraints | _ -> [] - /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable + /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable member x.IsCompilerGenerated = x.typar_flags.IsCompilerGenerated /// Indicates if the type variable can be solved or given new constraints. The status of a type variable - /// generally always evolves towards being either rigid or solved. + /// generally always evolves towards being either rigid or solved. member x.Rigidity = x.typar_flags.Rigidity /// Indicates if a type parameter is needed at runtime and may not be eliminated @@ -2495,13 +2501,13 @@ type Typar = member x.IsErased = match x.Kind with TyparKind.Type -> false | _ -> true /// The declared attributes of the type parameter. Empty for type inference variables and parameters from .NET. - member x.Attribs = + member x.Attribs = match x.typar_opt_data with | Some optData -> optData.typar_attribs | _ -> [] /// Set the attributes on the type parameter - member x.SetAttribs attribs = + member x.SetAttribs attribs = match attribs, x.typar_opt_data with | [], None -> () | [], Some { typar_il_name = None; typar_xmldoc = doc; typar_constraints = []; typar_is_contravariant = false; typar_declared_name = None } when doc.IsEmpty -> @@ -2553,14 +2559,14 @@ type Typar = | _ -> x.typar_opt_data <- Some { typar_il_name = None; typar_xmldoc = XmlDoc.Empty; typar_constraints = cs; typar_attribs = []; typar_is_contravariant = false; typar_declared_name = None } /// Marks the typar as being contravariant - member x.MarkAsContravariant() = + member x.MarkAsContravariant() = match x.typar_opt_data with | Some optData -> optData.typar_is_contravariant <- true | _ -> x.typar_opt_data <- Some { typar_il_name = None; typar_xmldoc = XmlDoc.Empty; typar_constraints = []; typar_attribs = []; typar_is_contravariant = true; typar_declared_name = None } /// Creates a type variable that contains empty data, and is not yet linked. Only used during unpickling of F# metadata. - static member NewUnlinked() : Typar = + static member NewUnlinked() : Typar = { typar_id = Unchecked.defaultof<_> typar_flags = Unchecked.defaultof<_> typar_stamp = -1L @@ -2572,37 +2578,37 @@ type Typar = static member New (data: TyparData) : Typar = data /// Links a previously unlinked type variable to the given data. Only used during unpickling of F# metadata. - member x.Link (tg: TyparData) = + member x.Link (tg: TyparData) = x.typar_id <- tg.typar_id x.typar_flags <- tg.typar_flags x.typar_stamp <- tg.typar_stamp x.typar_solution <- tg.typar_solution match tg.typar_opt_data with - | Some tg -> + | Some tg -> let optData = { typar_il_name = tg.typar_il_name; typar_xmldoc = tg.typar_xmldoc; typar_constraints = tg.typar_constraints; typar_attribs = tg.typar_attribs; typar_is_contravariant = tg.typar_is_contravariant; typar_declared_name = tg.typar_declared_name } x.typar_opt_data <- Some optData | None -> () /// Links a previously unlinked type variable to the given data. Only used during unpickling of F# metadata. - member x.AsType nullness = - match nullness with - | Nullness.Known NullnessInfo.AmbivalentToNull -> + member x.AsType nullness = + match nullness with + | Nullness.Known NullnessInfo.AmbivalentToNull -> let ty = x.typar_astype - match box ty with - | null -> + match box ty with + | null -> let ty2 = TType_var (x, Nullness.Known NullnessInfo.AmbivalentToNull) x.typar_astype <- ty2 ty2 | _ -> ty - | _ -> + | _ -> TType_var (x, nullness) /// Indicates if a type variable has been linked. Only used during unpickling of F# metadata. member x.IsLinked = x.typar_stamp <> -1L /// Indicates if a type variable has been solved. - member x.IsSolved = - match x.Solution with + member x.IsSolved = + match x.Solution with | None -> false | _ -> true @@ -2612,12 +2618,12 @@ type Typar = /// Sets the rigidity of a type variable member x.SetRigidity b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, b, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, b, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) /// Sets whether a type variable is compiler generated member x.SetCompilerGenerated b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, b, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, b, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) /// Sets whether a type variable has a static requirement member x.SetStaticReq b = @@ -2626,17 +2632,17 @@ type Typar = /// Sets whether a type variable is required at runtime member x.SetDynamicReq b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, b, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, b, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) - /// Sets whether the equality constraint of a type definition depends on this type variable + /// Sets whether the equality constraint of a type definition depends on this type variable member x.SetEqualityDependsOn b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, b, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, b, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) - /// Sets whether the comparison constraint of a type definition depends on this type variable + /// Sets whether the comparison constraint of a type definition depends on this type variable member x.SetComparisonDependsOn b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, b, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, b, flags.IsSupportsNullFlex) [] member x.DebugText = x.ToString() @@ -2645,65 +2651,65 @@ type Typar = /// Represents a constraint on a type parameter or type [] -type TyparConstraint = +type TyparConstraint = - /// A constraint that a type is a subtype of the given type + /// A constraint that a type is a subtype of the given type | CoercesTo of ty: TType * range: range - /// A constraint for a default value for an inference type variable should it be neither generalized nor solved - | DefaultsTo of priority: int * ty: TType * range: range - - /// A constraint that a type has a 'null' value - | SupportsNull of range: range - + /// A constraint for a default value for an inference type variable should it be neither generalized nor solved + | DefaultsTo of priority: int * ty: TType * range: range + + /// A constraint that a type has a 'null' value + | SupportsNull of range: range + /// A constraint that a type doesn't support nullness - | NotSupportsNull of range - - /// A constraint that a type has a member with the given signature + | NotSupportsNull of range + + /// A constraint that a type has a member with the given signature | MayResolveMember of constraintInfo: TraitConstraintInfo * range: range - - /// A constraint that a type is a non-Nullable value type - /// These are part of .NET's model of generic constraints, and in order to - /// generate verifiable code we must attach them to F# generalized type variables as well. - | IsNonNullableStruct of range: range - - /// A constraint that a type is a reference type - | IsReferenceType of range: range - - /// A constraint that a type is a simple choice between one of the given ground types. Only arises from 'printf' format strings. See format.fs - | SimpleChoice of tys: TTypes * range: range - - /// A constraint that a type has a parameterless constructor - | RequiresDefaultConstructor of range: range - - /// A constraint that a type is an enum with the given underlying - | IsEnum of ty: TType * range: range - + + /// A constraint that a type is a non-Nullable value type + /// These are part of .NET's model of generic constraints, and in order to + /// generate verifiable code we must attach them to F# generalized type variables as well. + | IsNonNullableStruct of range: range + + /// A constraint that a type is a reference type + | IsReferenceType of range: range + + /// A constraint that a type is a simple choice between one of the given ground types. Only arises from 'printf' format strings. See format.fs + | SimpleChoice of tys: TTypes * range: range + + /// A constraint that a type has a parameterless constructor + | RequiresDefaultConstructor of range: range + + /// A constraint that a type is an enum with the given underlying + | IsEnum of ty: TType * range: range + /// A constraint that a type implements IComparable, with special rules for some known structural container types - | SupportsComparison of range: range - + | SupportsComparison of range: range + /// A constraint that a type does not have the Equality(false) attribute, or is not a structural type with this attribute, with special rules for some known structural container types - | SupportsEquality of range: range - + | SupportsEquality of range: range + /// A constraint that a type is a delegate from the given tuple of args to the given return type - | IsDelegate of aty: TType * bty: TType * range: range - + | IsDelegate of aty: TType * bty: TType * range: range + /// A constraint that a type is .NET unmanaged type | IsUnmanaged of range: range - + /// An anti-constraint indicating that ref structs (e.g. Span<>) are allowed here | AllowsRefStruct of range:range // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - - override x.ToString() = sprintf "%+A" x - + + override x.ToString() = sprintf "%+A" x + [] -type TraitWitnessInfo = +type TraitWitnessInfo = | TraitWitnessInfo of tys: TTypes * memberName: string * memberFlags: SynMemberFlags * objAndArgTys: TTypes * returnTy: TType option - + /// Get the member name associated with the member constraint. member x.MemberName = (let (TraitWitnessInfo(_, b, _, _, _)) = x in b) @@ -2714,7 +2720,7 @@ type TraitWitnessInfo = member x.DebugText = x.ToString() override x.ToString() = "TraitWitnessInfo(" + x.MemberName + ")" - + /// Non-generic marker interface for storing in TraitConstraintInfo. /// The actual typed contract is ITraitContext<'AccessRights, 'MethodInfo, 'InfoReader>. type ITraitContext = interface end @@ -2726,20 +2732,20 @@ type ITraitContext<'AccessRights, 'MethodInfo, 'InfoReader> = abstract SelectExtensionMethods: traitInfo: TraitConstraintInfo * range: range * infoReader: 'InfoReader -> (TType * 'MethodInfo) list abstract AccessRights: 'AccessRights -/// The specification of a member constraint that must be solved +/// The specification of a member constraint that must be solved [] -type TraitConstraintInfo = +type TraitConstraintInfo = /// Indicates the signature of a member constraint. Contains a mutable solution cell /// to store the inferred solution of the constraint. And a mutable source cell to store /// the name of the type or member that defined the constraint. | TTrait of - tys: TTypes * - memberName: string * - memberFlags: SynMemberFlags * - objAndArgTys: TTypes * - returnTyOpt: TType option * - source: string option ref * + tys: TTypes * + memberName: string * + memberFlags: SynMemberFlags * + objAndArgTys: TTypes * + returnTyOpt: TType option * + source: string option ref * solution: TraitConstraintSln option ref * traitCtxt: ITraitContext option @@ -2753,12 +2759,12 @@ type TraitConstraintInfo = member x.MemberFlags = (let (TTrait(memberFlags = flags)) = x in flags) member x.CompiledObjectAndArgumentTypes = (let (TTrait(objAndArgTys = objAndArgTys)) = x in objAndArgTys) - + /// Get the optional return type recorded in the member constraint. member x.CompiledReturnType = (let (TTrait(returnTyOpt = retTy)) = x in retTy) - + /// Get or set the solution of the member constraint during inference - member x.Solution + member x.Solution with get() = (let (TTrait(solution = sln)) = x in sln.Value) and set v = (let (TTrait(solution = sln)) = x in sln.Value <- v) @@ -2780,17 +2786,17 @@ type TraitConstraintInfo = override x.ToString() = "TTrait(" + x.MemberLogicalName + ")" let traitCtxtNone : ITraitContext option = None - + /// Represents the solution of a member constraint during inference. [] -type TraitConstraintSln = +type TraitConstraintSln = /// FSMethSln(ty, vref, minst) /// /// Indicates a trait is solved by an F# method. /// ty -- the type and its instantiation /// vref -- the method that solves the trait constraint - /// minst -- the generic method instantiation + /// minst -- the generic method instantiation /// staticTyOpt -- the static type governing a static virtual call, if any | FSMethSln of ty: TType * vref: ValRef * minst: TypeInst * staticTyOpt: TType option @@ -2811,14 +2817,14 @@ type TraitConstraintSln = /// ty -- the type and its instantiation /// extOpt -- information about an extension member, if any /// ilMethodRef -- the method that solves the trait constraint - /// minst -- the generic method instantiation + /// minst -- the generic method instantiation /// staticTyOpt -- the static type governing a static virtual call, if any | ILMethSln of ty: TType * extOpt: ILTypeRef option * ilMethodRef: ILMethodRef * minst: TypeInst * staticTyOpt: TType option /// ClosedExprSln expr /// /// Indicates a trait is solved by an erased provided expression - | ClosedExprSln of expr: Expr + | ClosedExprSln of expr: Expr /// Indicates a trait is solved by a 'fake' instance of an operator, like '+' on integers | BuiltInSln @@ -2827,24 +2833,24 @@ type TraitConstraintSln = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// The partial information used to index the methods of all those in a ModuleOrNamespace. [] -type ValLinkagePartialKey = +type ValLinkagePartialKey = { /// The name of the type with which the member is associated. None for non-member values. - MemberParentMangledName: string option + MemberParentMangledName: string option - /// Indicates if the member is an override. - MemberIsOverride: bool + /// Indicates if the member is an override. + MemberIsOverride: bool - /// Indicates the logical name of the member. - LogicalName: string + /// Indicates the logical name of the member. + LogicalName: string /// Indicates the total argument count of the member. TotalArgCount: int - } + } [] member x.DebugText = x.ToString() @@ -2873,26 +2879,26 @@ type ValOptionalData = /// MUTABILITY: for unpickle linkage mutable val_compiled_name: string option - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation - mutable val_other_range: (range * bool) option + mutable val_other_range: (range * bool) option mutable val_const: Const option - - /// What is the original, unoptimized, closed-term definition, if any? + + /// What is the original, unoptimized, closed-term definition, if any? /// Used to implement [] - mutable val_defn: Expr option + mutable val_defn: Expr option /// Records the "extra information" for a value compiled as a method (rather /// than a closure or a local), including argument names, attributes etc. // - // MUTABILITY CLEANUP: mutability of this field is used by - // -- adjustAllUsesOfRecValue + // MUTABILITY CLEANUP: mutability of this field is used by + // -- adjustAllUsesOfRecValue // -- TLR optimizations // -- LinearizeTopMatch // - // For example, we use mutability to replace the empty arity initially assumed with an arity garnered from the - // type-checked expression. + // For example, we use mutability to replace the empty arity initially assumed with an arity garnered from the + // type-checked expression. mutable val_repr_info: ValReprInfo option /// Records the "extra information" for display purposes for expression-level function definitions @@ -2903,23 +2909,23 @@ type ValOptionalData = /// them with lambda arguments. mutable arg_repr_info_for_display: ArgReprInfo option - /// How visible is this? + /// How visible is this? /// MUTABILITY: for unpickle linkage - mutable val_access: Accessibility + mutable val_access: Accessibility /// XML documentation attached to a value. /// MUTABILITY: for unpickle linkage mutable val_xmldoc: XmlDoc - + /// the signature xml doc for an item in an implementation file. mutable val_other_xmldoc : XmlDoc option - /// Is the value actually an instance method/property/event that augments + /// Is the value actually an instance method/property/event that augments /// a type, and if so what name does it take in the IL? /// MUTABILITY: for unpickle linkage mutable val_member_info: ValMemberInfo option - // MUTABILITY CLEANUP: mutability of this field is used by + // MUTABILITY CLEANUP: mutability of this field is used by // -- LinearizeTopMatch // // The fresh temporary should just be created with the right parent @@ -2928,8 +2934,8 @@ type ValOptionalData = /// XML documentation signature for the value mutable val_xmldocsig: string - /// Custom attributes attached to the value. These contain references to other values (i.e. constructors in types). Mutable to fixup - /// these value references after copying a collection of values. + /// Custom attributes attached to the value. These contain references to other values (i.e. constructors in types). Mutable to fixup + /// these value references after copying a collection of values. mutable val_attribs: WellKnownValAttribs } @@ -2941,7 +2947,7 @@ type ValOptionalData = type ValData = Val [] -type Val = +type Val = { /// Mutable for unpickle linkage mutable val_logical_name: string @@ -2952,7 +2958,7 @@ type Val = mutable val_type: TType /// Mutable for unpickle linkage - mutable val_stamp: Stamp + mutable val_stamp: Stamp /// See vflags section further below for encoding/decodings here mutable val_flags: ValFlags @@ -2975,59 +2981,59 @@ type Val = val_xmldocsig = String.Empty val_attribs = WellKnownValAttribs.Empty } - /// Range of the definition (implementation) of the value, used by Visual Studio - member x.DefinitionRange = + /// Range of the definition (implementation) of the value, used by Visual Studio + member x.DefinitionRange = match x.val_opt_data with | Some { val_other_range = Some(m, true) } -> m | _ -> x.val_range - /// Range of the definition (signature) of the value, used by Visual Studio + /// Range of the definition (signature) of the value, used by Visual Studio member x.SigRange = match x.val_opt_data with | Some { arg_repr_info_for_display = Some { OtherRange = Some m } } -> m | Some { val_other_range = Some(m, false) } -> m | _ -> x.val_range - /// The place where the value was defined. + /// The place where the value was defined. member x.Range = x.val_range - /// A unique stamp within the context of this invocation of the compiler process + /// A unique stamp within the context of this invocation of the compiler process member x.Stamp = x.val_stamp - /// The type of the value. - /// May be a TType_forall for a generic value. - /// May be a type variable or type containing type variables during type inference. + /// The type of the value. + /// May be a TType_forall for a generic value. + /// May be a type variable or type containing type variables during type inference. // - // Note: this data is mutated during inference by adjustAllUsesOfRecValue when we replace the inferred type with a schema. + // Note: this data is mutated during inference by adjustAllUsesOfRecValue when we replace the inferred type with a schema. member x.Type = x.val_type /// How visible is this value, function or member? - member x.Accessibility = + member x.Accessibility = match x.val_opt_data with | Some optData -> optData.val_access | _ -> TAccess [] - /// The value of a value or member marked with [] - member x.LiteralValue = + /// The value of a value or member marked with [] + member x.LiteralValue = match x.val_opt_data with | Some optData -> optData.val_const | _ -> None /// Records the "extra information" for a value compiled as a method. /// - /// This indicates the number of arguments in each position for a curried + /// This indicates the number of arguments in each position for a curried /// functions, and relates to the F# spec for arity analysis. - /// For module-defined values, the currying is based - /// on the number of lambdas, and in each position the elements are - /// based on attempting to deconstruct the type of the argument as a - /// tuple-type. + /// For module-defined values, the currying is based + /// on the number of lambdas, and in each position the elements are + /// based on attempting to deconstruct the type of the argument as a + /// tuple-type. /// - /// The field is mutable because arities for recursive - /// values are only inferred after the r.h.s. is analyzed, but the - /// value itself is created before the r.h.s. is analyzed. + /// The field is mutable because arities for recursive + /// values are only inferred after the r.h.s. is analyzed, but the + /// value itself is created before the r.h.s. is analyzed. /// - /// TLR also sets this for inner bindings that it wants to - /// represent as "top level" bindings. + /// TLR also sets this for inner bindings that it wants to + /// represent as "top level" bindings. member x.ValReprInfo: ValReprInfo option = match x.val_opt_data with | Some optData -> optData.val_repr_info @@ -3049,22 +3055,22 @@ type Val = /// instance member), rather than an "inner" binding that may result in a closure. /// /// This is implied by IsMemberOrModuleBinding, however not vice versa, for two reasons. - /// Some optimizations mutate this value when they decide to change the representation of a + /// Some optimizations mutate this value when they decide to change the representation of a /// binding to be IsCompiledAsTopLevel. Second, even immediately after type checking we expect - /// some non-module, non-member bindings to be marked IsCompiledAsTopLevel, e.g. 'y' in + /// some non-module, non-member bindings to be marked IsCompiledAsTopLevel, e.g. 'y' in /// 'let x = let y = 1 in y + y' (NOTE: check this, don't take it as gospel) - member x.IsCompiledAsTopLevel = x.ValReprInfo.IsSome + member x.IsCompiledAsTopLevel = x.ValReprInfo.IsSome /// The partial information used to index the methods of all those in a ModuleOrNamespace. - member x.GetLinkagePartialKey() : ValLinkagePartialKey = + member x.GetLinkagePartialKey() : ValLinkagePartialKey = assert x.IsCompiledAsTopLevel - { LogicalName = x.LogicalName + { LogicalName = x.LogicalName MemberParentMangledName = (if x.IsMember then Some x.MemberApparentEntity.LogicalName else None) MemberIsOverride = x.IsOverrideOrExplicitImpl TotalArgCount = if x.IsMember then x.ValReprInfo.Value.TotalArgCount else 0 } /// The full information used to identify a specific overloaded method amongst all those in a ModuleOrNamespace. - member x.GetLinkageFullKey() : ValLinkageFullKey = + member x.GetLinkageFullKey() : ValLinkageFullKey = assert x.IsCompiledAsTopLevel let key = x.GetLinkagePartialKey() ValLinkageFullKey(key, (if x.IsMember then Some x.Type else None)) @@ -3085,7 +3091,7 @@ type Val = /// /// Note, the value may still be (a) an extension member or (b) and abstract slot without /// a true body. These cases are often causes of bugs in the compiler. - member x.MemberInfo = + member x.MemberInfo = match x.val_opt_data with | Some optData -> optData.val_member_info | _ -> None @@ -3097,12 +3103,12 @@ type Val = member x.IsIntrinsicMember = x.IsMember && not x.IsExtensionMember /// Indicates if this is an F#-defined value in a module, or an extension member, but excluding compiler generated bindings from optimizations - member x.IsModuleBinding = x.IsMemberOrModuleBinding && not x.IsMember + member x.IsModuleBinding = x.IsMemberOrModuleBinding && not x.IsMember /// Indicates if this is something compiled into a module, i.e. a user-defined value, an extension member or a compiler-generated value member x.IsCompiledIntoModule = x.IsExtensionMember || x.IsModuleBinding - /// Indicates if this is an F#-defined instance member. + /// Indicates if this is an F#-defined instance member. /// /// Note, the value may still be (a) an extension member or (b) and abstract slot without /// a true body. These cases are often causes of bugs in the compiler. @@ -3110,34 +3116,34 @@ type Val = /// Indicates if this is an F#-defined 'new' constructor member member x.IsConstructor = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo when not x.IsExtensionMember && (memberInfo.MemberFlags.MemberKind = SynMemberKind.Constructor) -> true | _ -> false /// Indicates if this is a compiler-generated class constructor member member x.IsClassConstructor = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo when not x.IsExtensionMember && (memberInfo.MemberFlags.MemberKind = SynMemberKind.ClassConstructor) -> true | _ -> false /// Indicates if this value was a member declared 'override' or an implementation of an interface slot member x.IsOverrideOrExplicitImpl = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo when memberInfo.MemberFlags.IsOverrideOrExplicitImpl -> true | _ -> false - + /// Gets the dispatch slots implemented by this method member x.ImplementedSlotSigs = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo -> memberInfo.ImplementedSlotSigs | _ -> [] - + /// Indicates if this is declared 'mutable' member x.IsMutable = (match x.val_flags.MutabilityInfo with Immutable -> false | Mutable -> true) /// Indicates if this is inferred to be a method or function that definitely makes no critical tailcalls? member x.MakesNoCriticalTailcalls = x.val_flags.MakesNoCriticalTailcalls - + /// Indicates if this is ever referenced? member x.HasBeenReferenced = x.val_flags.HasBeenReferenced @@ -3183,7 +3189,7 @@ type Val = member x.HasSignatureFile = x.SigRange <> x.DefinitionRange - + /// Get the inline declaration on the value member x.InlineInfo = x.val_flags.InlineInfo @@ -3210,7 +3216,7 @@ type Val = x.MemberInfo |> Option.exists (fun m -> m.MemberFlags.GetterOrSetterIsCompilerGenerated) /// Get the declared attributes for the value - member x.Attribs = + member x.Attribs = match x.val_opt_data with | Some optData -> optData.val_attribs.AsList() | _ -> [] @@ -3232,20 +3238,20 @@ type Val = | Some xmlDoc -> xmlDoc | None -> XmlDoc.Empty | _ -> XmlDoc.Empty - + ///Get the signature for the value's XML documentation - member x.XmlDocSig - with get() = - match x.val_opt_data with - | Some optData -> optData.val_xmldocsig + member x.XmlDocSig + with get() = + match x.val_opt_data with + | Some optData -> optData.val_xmldocsig | _ -> String.Empty - and set v = - match x.val_opt_data with - | Some optData -> optData.val_xmldocsig <- v + and set v = + match x.val_opt_data with + | Some optData -> optData.val_xmldocsig <- v | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_xmldocsig = v } /// The parent type or module, if any (None for expression bindings and parameters) - member x.TryDeclaringEntity = + member x.TryDeclaringEntity = match x.val_opt_data with | Some optData -> optData.val_declaring_entity | _ -> ParentNone @@ -3253,33 +3259,33 @@ type Val = /// Get the actual parent entity for the value (a module or a type), i.e. the entity under which the /// value will appear in compiled code. For extension members this is the module where the extension member /// is declared. - member x.DeclaringEntity = - match x.TryDeclaringEntity with + member x.DeclaringEntity = + match x.TryDeclaringEntity with | Parent tcref -> tcref | ParentNone -> error(InternalError("DeclaringEntity: does not have a parent", x.Range)) - member x.HasDeclaringEntity = - match x.TryDeclaringEntity with + member x.HasDeclaringEntity = + match x.TryDeclaringEntity with | Parent _ -> true | ParentNone -> false - + /// Get the apparent parent entity for a member - member x.MemberApparentEntity: TyconRef = - match x.MemberInfo with + member x.MemberApparentEntity: TyconRef = + match x.MemberInfo with | Some membInfo -> membInfo.ApparentEnclosingEntity | None -> error(InternalError("MemberApparentEntity", x.Range)) /// Get the number of 'this'/'self' object arguments for the member. Instance extension members return '1'. member v.NumObjArgs = - match v.MemberInfo with + match v.MemberInfo with | Some membInfo -> if membInfo.MemberFlags.IsInstance then 1 else 0 | None -> 0 /// Get the apparent parent entity for the value, i.e. the entity under with which the /// value is associated. For extension members this is the nominal type the member extends. /// For other values it is just the actual parent. - member x.ApparentEnclosingEntity = - match x.MemberInfo with + member x.ApparentEnclosingEntity = + match x.MemberInfo with | Some membInfo -> Parent(membInfo.ApparentEnclosingEntity) | None -> x.TryDeclaringEntity @@ -3291,56 +3297,56 @@ type Val = // - in ilxgen.fs: when compiling fslib, we bind an entry for the value in a global table (see bind_escaping_local_vspec) // - in opt.fs: (fullDebugTextOfValRef) for error reporting of non-inlinable values // - in service.fs (output_item_description): to display the full text of a value's binding location - // - in check.fs: as a boolean to detect public values for saving quotations - // - in ilxgen.fs: as a boolean to detect public values for saving quotations + // - in check.fs: as a boolean to detect public values for saving quotations + // - in ilxgen.fs: as a boolean to detect public values for saving quotations // - in MakeExportRemapping, to build non-local references for values - member x.PublicPath = - match x.TryDeclaringEntity with - | Parent eref -> + member x.PublicPath = + match x.TryDeclaringEntity with + | Parent eref -> match eref.PublicPath with | ValueNone -> None | ValueSome p -> Some(ValPubPath(p, x.GetLinkageFullKey())) - | ParentNone -> + | ParentNone -> None /// Indicates if this member is an F#-defined dispatch slot. - member x.IsDispatchSlot = - match x.MemberInfo with - | Some membInfo -> membInfo.MemberFlags.IsDispatchSlot + member x.IsDispatchSlot = + match x.MemberInfo with + | Some membInfo -> membInfo.MemberFlags.IsDispatchSlot | _ -> false /// Get the type of the value including any generic type parameters - member x.GeneralizedType = - match x.Type with + member x.GeneralizedType = + match x.Type with | TType_forall(tps, tau) -> tps, tau | ty -> [], ty /// Get the type of the value after removing any generic type parameters - member x.TauType = - match x.Type with + member x.TauType = + match x.Type with | TType_forall(_, tau) -> tau | ty -> ty /// Get the generic type parameters for the value - member x.Typars = - match x.Type with + member x.Typars = + match x.Type with | TType_forall(tps, _) -> tps | _ -> [] - /// The name of the method. + /// The name of the method. /// - If this is a property then this is 'get_Foo' or 'set_Foo' /// - If this is an implementation of an abstract slot then this is the name of the method implemented by the abstract slot /// - If this is an extension member then this will be the simple name - member x.LogicalName = - match x.MemberInfo with + member x.LogicalName = + match x.MemberInfo with | None -> x.val_logical_name - | Some membInfo -> - match membInfo.ImplementedSlotSigs with + | Some membInfo -> + match membInfo.ImplementedSlotSigs with | slotsig :: _ -> slotsig.Name | _ -> x.val_logical_name // Set the logical name of the value - member x.SetLogicalName(nm) = + member x.SetLogicalName(nm) = x.val_logical_name <- nm member x.ValCompiledName = @@ -3354,12 +3360,12 @@ type Val = /// - If this is an extension member then this will be a mangled name /// - If this is an operator then this is 'op_Addition' member x.CompiledName (compilerGlobalState:CompilerGlobalState option) = - let givenName = - match x.val_opt_data with + let givenName = + match x.val_opt_data with | Some { val_compiled_name = Some n } -> n - | _ -> x.LogicalName + | _ -> x.LogicalName // These cases must get stable unique names for their static field & static property. This name - // must be stable across quotation generation and IL code generation (quotations can refer to the + // must be stable across quotation generation and IL code generation (quotations can refer to the // properties implicit in these) // // Variable 'x' here, which is compiled as a top level static: @@ -3367,19 +3373,19 @@ type Val = // // The implicit 'patternInput' variable here: // let [x] = expr in ... // IsMemberOrModuleBinding = true, IsCompiledAsTopLevel = true, IsMember = false, CompilerGenerated=true - // + // // The implicit 'copyOfStruct' variables here: // let dt = System.DateTime.Now - System.DateTime.Now // IsMemberOrModuleBinding = false, IsCompiledAsTopLevel = true, IsMember = false, CompilerGenerated=true - // + // // However we don't need this for CompilerGenerated members such as the implementations of IComparable match compilerGlobalState with | Some state when x.IsCompiledAsTopLevel && not x.IsMember && (x.IsCompilerGenerated || not x.IsMemberOrModuleBinding) -> - state.StableNameGenerator.GetUniqueCompilerGeneratedName(givenName, x.Range, x.Stamp) + state.StableNameGenerator.GetUniqueCompilerGeneratedName(givenName, x.Range, x.Stamp) | _ -> givenName /// The name of the property. - /// - If this is a property then this is 'Foo' - member x.PropertyName = + /// - If this is a property then this is 'Foo' + member x.PropertyName = let logicalName = x.LogicalName ChopPropertyName logicalName @@ -3394,14 +3400,14 @@ type Val = /// - If this is an active pattern --> |A|_| /// - If this is an operator --> op_Addition /// - If this is an identifier needing backticks --> A-B - member x.DisplayNameCoreMangled = - match x.MemberInfo with - | Some membInfo -> - match membInfo.MemberFlags.MemberKind with - | SynMemberKind.ClassConstructor - | SynMemberKind.Constructor + member x.DisplayNameCoreMangled = + match x.MemberInfo with + | Some membInfo -> + match membInfo.MemberFlags.MemberKind with + | SynMemberKind.ClassConstructor + | SynMemberKind.Constructor | SynMemberKind.Member -> x.LogicalName - | SynMemberKind.PropertyGetSet + | SynMemberKind.PropertyGetSet | SynMemberKind.PropertySet | SynMemberKind.PropertyGet -> x.PropertyName | None -> x.LogicalName @@ -3409,7 +3415,7 @@ type Val = /// The display name of the value or method with operator names decompiled but without backticks etc. /// /// Note: here "Core" means "without added backticks or parens" - member x.DisplayNameCore = + member x.DisplayNameCore = x.DisplayNameCoreMangled |> ConvertValLogicalNameToDisplayNameCore /// The full text for the value to show in error messages and to use in code. @@ -3422,14 +3428,14 @@ type Val = /// - If this is an identifier needing backticks --> ``A-B`` /// - If this is a base value --> base /// - If this is a value named ``base`` --> ``base`` - member x.DisplayName = + member x.DisplayName = ConvertValLogicalNameToDisplayName x.IsBaseVal x.DisplayNameCoreMangled - member x.SetValRec b = x.val_flags <- x.val_flags.WithRecursiveValInfo b + member x.SetValRec b = x.val_flags <- x.val_flags.WithRecursiveValInfo b - member x.SetIsCompilerGenerated(v) = x.val_flags <- x.val_flags.WithIsCompilerGenerated(v) + member x.SetIsCompilerGenerated(v) = x.val_flags <- x.val_flags.WithIsCompilerGenerated(v) - member x.SetIsMemberOrModuleBinding() = x.val_flags <- x.val_flags.WithIsMemberOrModuleBinding + member x.SetIsMemberOrModuleBinding() = x.val_flags <- x.val_flags.WithIsMemberOrModuleBinding member x.SetMakesNoCriticalTailcalls() = x.val_flags <- x.val_flags.WithMakesNoCriticalTailcalls @@ -3449,12 +3455,12 @@ type Val = member x.SetIsParameter() = x.val_flags <- x.val_flags.WithIsParameter - member x.SetValReprInfo info = + member x.SetValReprInfo info = match x.val_opt_data with | Some optData -> optData.val_repr_info <- info | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_repr_info = info } - member x.SetValReprInfoForDisplay info = + member x.SetValReprInfoForDisplay info = match x.val_opt_data with | Some optData -> optData.val_repr_info_for_display <- info | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_repr_info_for_display = info } @@ -3475,13 +3481,13 @@ type Val = match x.val_opt_data with | Some optData -> optData.val_other_xmldoc <- Some xmlDoc | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_other_xmldoc = Some xmlDoc } - - member x.SetDeclaringEntity parent = + + member x.SetDeclaringEntity parent = match x.val_opt_data with | Some optData -> optData.val_declaring_entity <- parent | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_declaring_entity = parent } - member x.SetAttribs (attribs: Attribs) = + member x.SetAttribs (attribs: Attribs) = let wa = WellKnownValAttribs.Create(attribs) match x.val_opt_data with | Some optData -> optData.val_attribs <- wa @@ -3498,18 +3504,18 @@ type Val = if changed then x.SetValAttribs(waNew) result - member x.SetMemberInfo member_info = + member x.SetMemberInfo member_info = match x.val_opt_data with | Some optData -> optData.val_member_info <- Some member_info | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_member_info = Some member_info } - member x.SetValDefn val_defn = + member x.SetValDefn val_defn = match x.val_opt_data with | Some optData -> optData.val_defn <- Some val_defn | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_defn = Some val_defn } /// Create a new value with empty, unlinked data. Only used during unpickling of F# metadata. - static member NewUnlinked() : Val = + static member NewUnlinked() : Val = { val_logical_name = Unchecked.defaultof<_> val_range = Unchecked.defaultof<_> val_type = Unchecked.defaultof<_> @@ -3525,15 +3531,15 @@ type Val = member x.Link (tg: ValData) = x.SetData tg /// Set all the data on a value - member x.SetData (tg: ValData) = - x.val_logical_name <- tg.val_logical_name - x.val_range <- tg.val_range - x.val_type <- tg.val_type - x.val_stamp <- tg.val_stamp - x.val_flags <- tg.val_flags + member x.SetData (tg: ValData) = + x.val_logical_name <- tg.val_logical_name + x.val_range <- tg.val_range + x.val_type <- tg.val_type + x.val_stamp <- tg.val_stamp + x.val_flags <- tg.val_flags match tg.val_opt_data with - | Some tg -> - x.val_opt_data <- + | Some tg -> + x.val_opt_data <- Some { val_compiled_name = tg.val_compiled_name val_other_range = tg.val_other_range val_const = tg.val_const @@ -3551,26 +3557,26 @@ type Val = | None -> () /// Indicates if a value is linked to backing data yet. Only used during unpickling of F# metadata. - member x.IsLinked = match box x.val_logical_name with null -> false | _ -> true + member x.IsLinked = match box x.val_logical_name with null -> false | _ -> true [] member x.DebugText = x.ToString() override x.ToString() = x.LogicalName - - + + /// Represents the extra information stored for a member [] -type ValMemberInfo = +type ValMemberInfo = { - /// The parent type. For an extension member this is the type being extended - ApparentEnclosingEntity: TyconRef + /// The parent type. For an extension member this is the type being extended + ApparentEnclosingEntity: TyconRef - /// Updated with the full implemented slotsig after interface implementation relation is checked - mutable ImplementedSlotSigs: SlotSig list + /// Updated with the full implemented slotsig after interface implementation relation is checked + mutable ImplementedSlotSigs: SlotSig list - /// Gets updated with 'true' if an abstract slot is implemented in the file being typechecked. Internal only. - mutable IsImplemented: bool + /// Gets updated with 'true' if an abstract slot is implemented in the file being typechecked. Internal only. + mutable IsImplemented: bool MemberFlags: SynMemberFlags } @@ -3581,10 +3587,10 @@ type ValMemberInfo = override x.ToString() = "ValMemberInfo(...)" [] -type NonLocalValOrMemberRef = +type NonLocalValOrMemberRef = { /// A reference to the entity containing the value or member. This will always be a non-local reference - EnclosingEntity: EntityRef + EnclosingEntity: EntityRef /// The name of the value, or the full signature of the member ItemKey: ValLinkageFullKey @@ -3602,11 +3608,11 @@ type NonLocalValOrMemberRef = /// For debugging override x.ToString() = !! x.EnclosingEntity.nlr.ToString() + "::" + x.ItemKey.PartialKey.LogicalName - + /// Represents the path information for a reference to a value or member in another assembly, disassociated /// from any particular reference. [] -type ValPublicPath = +type ValPublicPath = | ValPubPath of PublicPath * ValLinkageFullKey [] @@ -3616,14 +3622,14 @@ type ValPublicPath = /// Represents an index into the namespace/module structure of an assembly [] -type NonLocalEntityRef = +type NonLocalEntityRef = | NonLocalEntityRef of CcuThunk * string[] /// Try to find the entity corresponding to the given path in the given CCU - static member TryDerefEntityPath(ccu: CcuThunk, path: string[], i: int, entity: Entity) = + static member TryDerefEntityPath(ccu: CcuThunk, path: string[], i: int, entity: Entity) = if i >= path.Length then ValueSome entity - else - match entity.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames.TryGetValue path[i] with + else + match entity.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames.TryGetValue path[i] with | true, res -> NonLocalEntityRef.TryDerefEntityPath(ccu, path, (i+1), res) #if !NO_TYPEPROVIDERS | _ -> NonLocalEntityRef.TryDerefEntityPathViaProvidedType(ccu, path, i, entity) @@ -3633,21 +3639,21 @@ type NonLocalEntityRef = #if !NO_TYPEPROVIDERS /// Try to find the entity corresponding to the given path, using type-providers to link the data - static member TryDerefEntityPathViaProvidedType(ccu: CcuThunk, path: string[], i: int, entity: Entity) = + static member TryDerefEntityPathViaProvidedType(ccu: CcuThunk, path: string[], i: int, entity: Entity) = // Errors during linking are not necessarily given good ranges. This has always been the case in F# 2.0, but also applies to // type provider type linking errors in F# 3.0. let m = range0 match entity.TypeReprInfo with - | TProvidedTypeRepr info -> + | TProvidedTypeRepr info -> let resolutionEnvironment = info.ResolutionEnvironment let st = info.ProvidedType - + // In this case, we're safely in the realm of types. Just iterate through the nested // types until i = path.Length-1. Create the Tycon's as needed - let rec tryResolveNestedTypeOf(parentEntity: Entity, resolutionEnvironment, st: Tainted, i) = + let rec tryResolveNestedTypeOf(parentEntity: Entity, resolutionEnvironment, st: Tainted, i) = match st.PApply((fun st -> st.GetNestedType path[i]), m) with | Tainted.Null -> ValueNone - | Tainted.NonNull st -> + | Tainted.NonNull st -> let canonicalEntity = parentEntity.ModuleOrNamespaceType.GetOrInternProvidedEntity( path[i], @@ -3657,9 +3663,9 @@ type NonLocalEntityRef = tryResolveNestedTypeOf(entity, resolutionEnvironment, st, i) - | TProvidedNamespaceRepr(resolutionEnvironment, resolvers) -> + | TProvidedNamespaceRepr(resolutionEnvironment, resolvers) -> - // In this case, we're still in the realm of extensible namespaces. + // In this case, we're still in the realm of extensible namespaces. // <----entity--> // 0 .........i-1..i .......... j ..... path.Length-1 // @@ -3669,16 +3675,16 @@ type NonLocalEntityRef = // <----entity--> <---resolver----> <--loop---> // 0 .........i-1..i ............. j ..... path.Length-1 // - // We now query the resolvers with - // moduleOrNamespace = path.[0..j-1] - // typeName = path.[j] + // We now query the resolvers with + // moduleOrNamespace = path.[0..j-1] + // typeName = path.[j] // starting with j = i and then progressively increasing j - + // This function queries at 'j' - let tryResolvePrefix j = + let tryResolvePrefix j = assert (j >= 0) assert (j <= path.Length - 1) - let matched = + let matched = [ for resolver in resolvers do let moduleOrNamespace = if j = 0 then [| |] else path[0..j-1] let typename = path[j] @@ -3694,8 +3700,8 @@ type NonLocalEntityRef = // 'entity' is at position i in the dereference chain. We resolved to position 'j'. // Inject namespaces until we're an position j, and then inject the type. // Note: this is similar to code in CompileOps.fs - let rec injectNamespacesFromIToJ (entity: Entity) k = - if k = j then + let rec injectNamespacesFromIToJ (entity: Entity) k = + if k = j then entity.ModuleOrNamespaceType.GetOrInternProvidedEntity( path[j], (fun () -> Construct.NewProvidedTycon(resolutionEnvironment, st, ccu.ImportProvidedType, false, m))) @@ -3705,22 +3711,22 @@ type NonLocalEntityRef = path[k], (fun () -> let cpath = entity.CompilationPath.NestedCompPath entity.LogicalName (ModuleOrNamespaceKind.Namespace false) - Construct.NewModuleOrNamespace - (Some cpath) - (TAccess []) (ident(path[k], m)) XmlDoc.Empty [] + Construct.NewModuleOrNamespace + (Some cpath) + (TAccess []) (ident(path[k], m)) XmlDoc.Empty [] (MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType (Namespace true))))) injectNamespacesFromIToJ newEntity (k+1) let newEntity = injectNamespacesFromIToJ entity i - + // newEntity is at 'j' - NonLocalEntityRef.TryDerefEntityPath(ccu, path, (j+1), newEntity) + NonLocalEntityRef.TryDerefEntityPath(ccu, path, (j+1), newEntity) - | [] -> ValueNone + | [] -> ValueNone | _ -> failwith "Unexpected" - let rec tryResolvePrefixes j = + let rec tryResolvePrefixes j = if j >= path.Length then ValueNone - else match tryResolvePrefix j with + else match tryResolvePrefix j with | ValueNone -> tryResolvePrefixes (j+1) | ValueSome res -> ValueSome res @@ -3728,11 +3734,11 @@ type NonLocalEntityRef = | _ -> ValueNone #endif - + /// Try to link a non-local entity reference to an actual entity - member nleref.TryDeref canError = - let (NonLocalEntityRef(ccu, path)) = nleref - if canError then + member nleref.TryDeref canError = + let (NonLocalEntityRef(ccu, path)) = nleref + if canError then ccu.EnsureDerefable path if ccu.IsUnresolvedReference then ValueNone else @@ -3742,62 +3748,62 @@ type NonLocalEntityRef = | ValueNone -> // OK, the lookup failed. Check if we can redirect through a type forwarder on this assembly. // Look for a forwarder for each prefix-path - let rec tryForwardPrefixPath i = - if i < path.Length then + let rec tryForwardPrefixPath i = + if i < path.Length then match ccu.TryForward(path[0..i-1], path[i]) with // OK, found a forwarder, now continue with the lookup to find the nested type - | Some tcref -> NonLocalEntityRef.TryDerefEntityPath(ccu, path, (i+1), tcref.Deref) + | Some tcref -> NonLocalEntityRef.TryDerefEntityPath(ccu, path, (i+1), tcref.Deref) | None -> tryForwardPrefixPath (i+1) else ValueNone tryForwardPrefixPath 0 - + /// Get the CCU referenced by the nonlocal reference. member nleref.Ccu = - let (NonLocalEntityRef(ccu, _)) = nleref + let (NonLocalEntityRef(ccu, _)) = nleref ccu /// Get the path into the CCU referenced by the nonlocal reference. member nleref.Path = - let (NonLocalEntityRef(_, p)) = nleref + let (NonLocalEntityRef(_, p)) = nleref p member nleref.DisplayName = String.concat "." nleref.Path /// Get the mangled name of the last item in the path of the nonlocal reference. - member nleref.LastItemMangledName = + member nleref.LastItemMangledName = let p = nleref.Path p[p.Length-1] /// Get the all-but-last names of the path of the nonlocal reference. - member nleref.EnclosingMangledPath = + member nleref.EnclosingMangledPath = let p = nleref.Path p[0..p.Length-2] - + /// Get the name of the assembly referenced by the nonlocal reference. member nleref.AssemblyName = nleref.Ccu.AssemblyName /// Dereference the nonlocal reference, and raise an error if this fails. - member nleref.Deref = - match nleref.TryDeref(canError=true) with + member nleref.Deref = + match nleref.TryDeref(canError=true) with | ValueSome res -> res - | ValueNone -> - errorR (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefModuleNamespace, nleref.DisplayName, nleref.AssemblyName, "")) + | ValueNone -> + errorR (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefModuleNamespace, nleref.DisplayName, nleref.AssemblyName, "")) raise (KeyNotFoundException()) [] member x.DebugText = x.ToString() override x.ToString() = x.DisplayName - + [] -type EntityRef = +type EntityRef = { - /// Indicates a reference to something bound in this CCU + /// Indicates a reference to something bound in this CCU mutable binding: NonNullSlot - /// Indicates a reference to something bound in another CCU + /// Indicates a reference to something bound in another CCU nlr: NonLocalEntityRef } @@ -3811,36 +3817,36 @@ type EntityRef = member x.ResolvedTarget = x.binding /// Resolve the reference - member private tcr.Resolve canError = + member private tcr.Resolve canError = let res = tcr.nlr.TryDeref canError - match res with - | ValueSome r -> - tcr.binding <- nullableSlotFull r - | ValueNone -> + match res with + | ValueSome r -> + tcr.binding <- nullableSlotFull r + | ValueNone -> () /// Dereference the TyconRef to a Tycon. Amortize the cost of doing this. /// This path should not allocate in the amortized case - member tcr.Deref = - match box tcr.binding with + member tcr.Deref = + match box tcr.binding with | null -> tcr.Resolve(canError=true) - match box tcr.binding with + match box tcr.binding with | null -> error (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefModuleNamespaceType, String.concat "." tcr.nlr.EnclosingMangledPath, tcr.nlr.AssemblyName, tcr.nlr.LastItemMangledName)) | _ -> tcr.binding - | _ -> + | _ -> tcr.binding /// Dereference the TyconRef to a Tycon option. - member tcr.TryDeref = - match box tcr.binding with - | null -> + member tcr.TryDeref = + match box tcr.binding with + | null -> tcr.Resolve(canError=false) - match box tcr.binding with + match box tcr.binding with | null -> ValueNone | _ -> ValueSome tcr.binding - | _ -> + | _ -> ValueSome tcr.binding /// Is the destination assembly available? @@ -3858,10 +3864,10 @@ type EntityRef = /// The signature definition location of the namespace, module or type member x.SigRange = x.Deref.SigRange - /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException + /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException member x.LogicalName = x.Deref.LogicalName - /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException + /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException member x.CompiledName = x.Deref.CompiledName /// The display name of the namespace, module or type, e.g. List instead of List`1, not including static parameters @@ -3887,8 +3893,8 @@ type EntityRef = /// The code location where the module, namespace or type is defined. member x.Range = x.Deref.Range - /// A unique stamp for this module, namespace or type definition within the context of this compilation. - /// Note that because of signatures, there are situations where in a single compilation the "same" + /// A unique stamp for this module, namespace or type definition within the context of this compilation. + /// Note that because of signatures, there are situations where in a single compilation the "same" /// module, namespace or type may have two distinct Entity objects that have distinct stamps. member x.Stamp = x.Deref.Stamp @@ -3898,7 +3904,7 @@ type EntityRef = /// The XML documentation of the entity, if any. If the entity is backed by provided metadata /// then this _does_ include this documentation. If the entity is backed by Abstract IL metadata - /// or comes from another F# assembly then it does not (because the documentation will get read from + /// or comes from another F# assembly then it does not (because the documentation will get read from /// an XML file). member x.XmlDoc = if not x.Deref.XmlDoc.IsEmpty then @@ -3907,7 +3913,7 @@ type EntityRef = x.Deref.entity_opt_data |> Option.bind (fun d -> d.entity_other_xmldoc) |> Option.defaultValue XmlDoc.Empty - + member x.SetOtherXmlDoc (xmlDoc: XmlDoc) = x.Deref.SetOtherXmlDoc(xmlDoc) /// The XML documentation sig-string of the entity, if any, to use to lookup an .xml doc file. This also acts @@ -3916,7 +3922,7 @@ type EntityRef = /// The logical contents of the entity when it is a module or namespace fragment. member x.ModuleOrNamespaceType = x.Deref.ModuleOrNamespaceType - + /// Demangle the module name, if FSharpModuleWithSuffix is used member x.DemangledModuleOrNamespaceName = x.Deref.DemangledModuleOrNamespaceName @@ -3932,12 +3938,12 @@ type EntityRef = /// The information about the r.h.s. of a type definition, if any. For example, the r.h.s. of a union or record type. member x.TypeReprInfo = x.Deref.TypeReprInfo - /// The information about the r.h.s. of an F# exception definition, if any. + /// The information about the r.h.s. of an F# exception definition, if any. member x.ExceptionInfo = x.Deref.ExceptionInfo /// Indicates if the entity represents an F# exception declaration. member x.IsFSharpException = x.Deref.IsFSharpException - + /// Get the type parameters for an entity that is a type declaration, otherwise return the empty list. /// /// Lazy because it may read metadata. Uses the entity's own range for error context. @@ -3961,10 +3967,10 @@ type EntityRef = /// Get the value representing the accessibility of an F# type definition or module. member x.Accessibility = x.Deref.Accessibility - /// Indicates the type prefers the "tycon" syntax for display etc. + /// Indicates the type prefers the "tycon" syntax for display etc. member x.IsPrefixDisplay = x.Deref.IsPrefixDisplay - /// Indicates the "tycon blob" is actually a module + /// Indicates the "tycon blob" is actually a module member x.IsModuleOrNamespace = x.Deref.IsModuleOrNamespace /// Indicates if the entity is a namespace @@ -4058,7 +4064,7 @@ type EntityRef = /// Indicates if this is a struct or enum type definition, i.e. a value type definition, including struct records and unions member x.IsStructOrEnumTycon = x.Deref.IsStructOrEnumTycon - /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses + /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses /// an assembly-code representation for the type, e.g. the primitive array type constructor. member x.IsAsmReprTycon = x.Deref.IsAsmReprTycon @@ -4068,7 +4074,7 @@ type EntityRef = /// Indicates if the entity is erased, either a measure definition, or an erased provided type definition member x.IsErased = x.Deref.IsErased - + /// Gets any implicit hash/equals (with comparer argument) methods added to an F# record, union or struct type definition. member x.GeneratedHashAndEqualsWithComparerValues = x.Deref.GeneratedHashAndEqualsWithComparerValues @@ -4080,11 +4086,11 @@ type EntityRef = /// Gets any implicit hash/equals methods added to an F# record, union or struct type definition. member x.GeneratedHashAndEqualsValues = x.Deref.GeneratedHashAndEqualsValues - + /// Indicate if this is a type definition backed by Abstract IL metadata. member x.IsILTycon = x.Deref.IsILTycon - /// Get the Abstract IL scope, nesting and metadata for this + /// Get the Abstract IL scope, nesting and metadata for this /// type definition, assuming it is backed by Abstract IL metadata. member x.ILTyconInfo = x.Deref.ILTyconInfo @@ -4124,19 +4130,19 @@ type EntityRef = /// which in F# is called a 'unknown representation' type). member x.IsHiddenReprTycon = x.Deref.IsHiddenReprTycon - /// Indicates if this is an F#-defined interface type definition + /// Indicates if this is an F#-defined interface type definition member x.IsFSharpInterfaceTycon = x.Deref.IsFSharpInterfaceTycon - /// Indicates if this is an F#-defined delegate type definition + /// Indicates if this is an F#-defined delegate type definition member x.IsFSharpDelegateTycon = x.Deref.IsFSharpDelegateTycon - /// Indicates if this is an F#-defined enum type definition + /// Indicates if this is an F#-defined enum type definition member x.IsFSharpEnumTycon = x.Deref.IsFSharpEnumTycon - /// Indicates if this is a .NET-defined enum type definition + /// Indicates if this is a .NET-defined enum type definition member x.IsILEnumTycon = x.Deref.IsILEnumTycon - /// Indicates if this is an enum type definition + /// Indicates if this is an enum type definition member x.IsEnumTycon = x.Deref.IsEnumTycon /// Indicates if this is an F#-defined value type definition, including struct records and unions @@ -4164,11 +4170,11 @@ type EntityRef = [] member x.DebugText = x.ToString() - override x.ToString() = - if x.IsLocalRef then - x.ResolvedTarget.DisplayName - else - x.nlr.DisplayName + override x.ToString() = + if x.IsLocalRef then + x.ResolvedTarget.DisplayName + else + x.nlr.DisplayName /// Represents a module-or-namespace reference in the typed abstract syntax. type ModuleOrNamespaceRef = EntityRef @@ -4178,12 +4184,12 @@ type TyconRef = EntityRef /// References are either local or nonlocal [] -type ValRef = +type ValRef = { - /// Indicates a reference to something bound in this CCU + /// Indicates a reference to something bound in this CCU mutable binding: NonNullSlot - /// Indicates a reference to something bound in another CCU + /// Indicates a reference to something bound in another CCU nlr: NonLocalValOrMemberRef } @@ -4194,35 +4200,35 @@ type ValRef = member x.ResolvedTarget = x.binding /// Dereference the ValRef to a Val. - member x.Deref = + member x.Deref = if obj.ReferenceEquals(x.binding, null) then - let res = - let nlr = x.nlr - let e = nlr.EnclosingEntity.Deref + let res = + let nlr = x.nlr + let e = nlr.EnclosingEntity.Deref let possible = e.ModuleOrNamespaceType.TryLinkVal(nlr.EnclosingEntity.nlr.Ccu, nlr.ItemKey) - match possible with + match possible with | ValueNone -> error (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefVal, e.DisplayNameWithStaticParameters, nlr.AssemblyName, sprintf "%+A" nlr.ItemKey.PartialKey)) | ValueSome h -> h - x.binding <- nullableSlotFull res - res + x.binding <- nullableSlotFull res + res else x.binding /// Dereference the ValRef to a Val option. - member x.TryDeref = + member x.TryDeref = if obj.ReferenceEquals(x.binding, null) then - let resOpt = - match x.nlr.EnclosingEntity.TryDeref with + let resOpt = + match x.nlr.EnclosingEntity.TryDeref with | ValueNone -> ValueNone | ValueSome e -> e.ModuleOrNamespaceType.TryLinkVal(x.nlr.EnclosingEntity.nlr.Ccu, x.nlr.ItemKey) - match resOpt with + match resOpt with | ValueNone -> () - | ValueSome res -> - x.binding <- nullableSlotFull res + | ValueSome res -> + x.binding <- nullableSlotFull res resOpt else ValueSome x.binding - /// The type of the value. May be a TType_forall for a generic value. - /// May be a type variable or type containing type variables during type inference. + /// The type of the value. May be a TType_forall for a generic value. + /// May be a type variable or type containing type variables during type inference. member x.Type = x.Deref.Type /// Get the type of the value including any generic type parameters @@ -4258,29 +4264,29 @@ type ValRef = member x.SigRange = x.Deref.SigRange - /// The value of a value or member marked with [] + /// The value of a value or member marked with [] member x.LiteralValue = x.Deref.LiteralValue member x.Id = x.Deref.Id /// Get the name of the value, assuming it is compiled as a property. - /// - If this is a property then this is 'Foo' + /// - If this is a property then this is 'Foo' /// - If this is an implementation of an abstract slot then this is the name of the property implemented by the abstract slot member x.PropertyName = x.Deref.PropertyName /// Indicates whether this value represents a property getter. - member x.IsPropertyGetterMethod = + member x.IsPropertyGetterMethod = match x.MemberInfo with | None -> false | Some (memInfo: ValMemberInfo) -> memInfo.MemberFlags.MemberKind = SynMemberKind.PropertyGet || memInfo.MemberFlags.MemberKind = SynMemberKind.PropertyGetSet /// Indicates whether this value represents a property setter. - member x.IsPropertySetterMethod = + member x.IsPropertySetterMethod = match x.MemberInfo with | None -> false | Some (memInfo: ValMemberInfo) -> memInfo.MemberFlags.MemberKind = SynMemberKind.PropertySet || memInfo.MemberFlags.MemberKind = SynMemberKind.PropertyGetSet - /// A unique stamp within the context of this invocation of the compiler process + /// A unique stamp within the context of this invocation of the compiler process member x.Stamp = x.Deref.Stamp /// Is this represented as a "top level" static binding (i.e. a static field, static member, @@ -4318,7 +4324,7 @@ type ValRef = /// Indicates if this is an F#-defined value in a module, or an extension member, but excluding compiler generated bindings from optimizations member x.IsModuleBinding = x.Deref.IsModuleBinding - /// Indicates if this is an F#-defined instance member. + /// Indicates if this is an F#-defined instance member. /// /// Note, the value may still be (a) an extension member or (b) and abstract slot without /// a true body. These cases are often causes of bugs in the compiler. @@ -4412,13 +4418,13 @@ type ValRef = [] member x.DebugText = x.ToString() - override x.ToString() = - if x.IsLocalRef then x.ResolvedTarget.DisplayName + override x.ToString() = + if x.IsLocalRef then x.ResolvedTarget.DisplayName else x.nlr.ToString() /// Represents a reference to a case of a union type [] -type UnionCaseRef = +type UnionCaseRef = | UnionCaseRef of tyconRef: TyconRef * caseName: string /// Get a reference to the type containing this union case @@ -4431,14 +4437,14 @@ type UnionCaseRef = member x.Tycon = x.TyconRef.Deref /// Dereference the reference to the union case - member x.UnionCase = - match x.TyconRef.GetUnionCaseByName x.CaseName with + member x.UnionCase = + match x.TyconRef.GetUnionCaseByName x.CaseName with | Some res -> res | None -> error(InternalError(sprintf "union case %s not found in type %s" x.CaseName x.TyconRef.LogicalName, x.TyconRef.Range)) - /// Try to dereference the reference + /// Try to dereference the reference member x.TryUnionCase = - x.TyconRef.TryDeref + x.TyconRef.TryDeref |> ValueOption.bind (fun tcref -> tcref.GetUnionCaseByName x.CaseName |> ValueOption.ofOption) /// Get the attributes associated with the union case @@ -4454,11 +4460,11 @@ type UnionCaseRef = member x.SigRange = x.UnionCase.SigRange /// Get the index of the union case amongst the cases - member x.Index = - try - // REVIEW: this could be faster, e.g. by storing the index in the NameMap - x.TyconRef.UnionCasesArray |> Array.findIndex (fun uc -> uc.LogicalName = x.CaseName) - with :? KeyNotFoundException -> + member x.Index = + try + // REVIEW: this could be faster, e.g. by storing the index in the NameMap + x.TyconRef.UnionCasesArray |> Array.findIndex (fun uc -> uc.LogicalName = x.CaseName) + with :? KeyNotFoundException -> error(InternalError(sprintf "union case %s not found in type %s" x.CaseName x.TyconRef.LogicalName, x.TyconRef.Range)) /// Get the fields of the union case @@ -4490,7 +4496,7 @@ let findLogicalFieldIndexOfRecordField (tcref:TyconRef) (id:string) = /// Represents a reference to a field in a record, class or struct [] -type RecdFieldRef = +type RecdFieldRef = | RecdFieldRef of tyconRef: TyconRef * fieldName: string /// Get a reference to the type containing this record field @@ -4505,34 +4511,34 @@ type RecdFieldRef = /// Get the Entity for the type containing this record field member x.Tycon = x.TyconRef.Deref - /// Dereference the reference - member x.RecdField = + /// Dereference the reference + member x.RecdField = let (RecdFieldRef(tcref, id)) = x - match tcref.GetFieldByName id with + match tcref.GetFieldByName id with | Some res -> res | None -> error(InternalError(sprintf "field %s not found in type %s" id tcref.LogicalName, tcref.Range)) - /// Try to dereference the reference - member x.TryRecdField = - x.TyconRef.TryDeref + /// Try to dereference the reference + member x.TryRecdField = + x.TyconRef.TryDeref |> ValueOption.bind (fun tcref -> tcref.GetFieldByName x.FieldName |> ValueOption.ofOption) - /// Get the attributes associated with the compiled property of the record field + /// Get the attributes associated with the compiled property of the record field member x.PropertyAttribs = x.RecdField.PropertyAttribs - /// Get the declaration range of the record field + /// Get the declaration range of the record field member x.Range = x.RecdField.Range - /// Get the definition range of the record field + /// Get the definition range of the record field member x.DefinitionRange = x.RecdField.DefinitionRange - /// Get the signature range of the record field + /// Get the signature range of the record field member x.SigRange = x.RecdField.SigRange member x.Index = let (RecdFieldRef(tcref, id)) = x findLogicalFieldIndexOfRecordField tcref id - + [] member x.DebugText = x.ToString() @@ -4540,7 +4546,7 @@ type RecdFieldRef = override x.ToString() = x.FieldName [] -type Nullness = +type Nullness = | Known of NullnessInfo | Variable of NullnessVar /// The value is known to be non-null because it was produced by a constructor call. @@ -4553,14 +4559,14 @@ type Nullness = | KnownFromConstructor -> Known NullnessInfo.WithoutNull | n -> n - member n.Evaluate() = - match n with + member n.Evaluate() = + match n with | Known info -> info | Variable v -> v.Evaluate() | KnownFromConstructor -> NullnessInfo.WithoutNull - member n.TryEvaluate() = - match n with + member n.TryEvaluate() = + match n with | Known info -> ValueSome info | Variable v -> v.TryEvaluate() | KnownFromConstructor -> NullnessInfo.WithoutNull |> ValueSome @@ -4571,42 +4577,42 @@ type Nullness = // Note, nullness variables are only created if the nullness checking feature is on [] -type NullnessVar() = +type NullnessVar() = let mutable solution: Nullness option = None - member nv.Evaluate() = - match solution with + member nv.Evaluate() = + match solution with | None -> NullnessInfo.WithoutNull | Some soln -> soln.Evaluate() - member nv.TryEvaluate() = - match solution with + member nv.TryEvaluate() = + match solution with | None -> ValueNone | Some soln -> soln.TryEvaluate() member nv.IsSolved = solution.IsSome - member nv.IsFullySolved = + member nv.IsFullySolved = match solution with | None -> false | Some (Nullness.Known _) -> true | Some (Nullness.KnownFromConstructor) -> true | Some (Nullness.Variable v) -> v.IsFullySolved - member nv.Set(nullness) = - assert (not nv.IsSolved) + member nv.Set(nullness) = + assert (not nv.IsSolved) solution <- Some nullness - member nv.Unset() = + member nv.Unset() = assert nv.IsSolved solution <- None - member nv.Solution = + member nv.Solution = assert nv.IsSolved solution.Value [] -type NullnessInfo = +type NullnessInfo = /// we know that there is an extra null value in the type | WithNull @@ -4621,7 +4627,7 @@ type NullnessInfo = [] type TType = - /// Indicates the type is a universal type, only used for types of values and members + /// Indicates the type is a universal type, only used for types of values and members | TType_forall of typars: Typars * bodyTy: TType /// TType_app(tyconRef, typeInstantiation, nullness). @@ -4637,7 +4643,7 @@ type TType = /// TType_fun(domainType, rangeType, nullness). /// - /// Indicates the type is a function type + /// Indicates the type is a function type | TType_fun of domainType: TType * rangeType: TType * nullness: Nullness /// Indicates the type is a non-F#-visible type representing a "proof" that a union value belongs to a particular union case @@ -4645,7 +4651,7 @@ type TType = /// the temporaries arising out of pattern matching on union values. | TType_ucase of unionCaseRef: UnionCaseRef * typeInstantiation: TypeInst - /// Indicates the type is a variable type, whether declared, generalized or an inference type parameter + /// Indicates the type is a variable type, whether declared, generalized or an inference type parameter | TType_var of typar: Typar * nullness: Nullness /// Indicates the type is a unit-of-measure expression being used as an argument to a type or member @@ -4669,24 +4675,24 @@ type TType = [] member x.DebugText = x.LimitedToString(4) - member x.LimitedToString(maxDepth:int) = - match x with + member x.LimitedToString(maxDepth:int) = + match x with | TType_forall (_tps, ty) -> "forall ... " + ty.ToString() - | TType_app (tcref, tinst, nullness) -> tcref.DisplayName + (match tinst with [] -> "" | tys -> "<" + String.concat "," (List.map string tys) + ">") + nullness.ToString() - | TType_tuple (tupInfo, tinst) -> - (match tupInfo with + | TType_app (tcref, tinst, nullness) -> tcref.DisplayName + (match tinst with [] -> "" | tys -> "<" + String.concat "," (List.map string tys) + ">") + nullness.ToString() + | TType_tuple (tupInfo, tinst) -> + (match tupInfo with | TupInfo.Const false -> "" | TupInfo.Const true -> "struct ") + String.concat "," (List.map string tinst) + ")" - | TType_anon (anonInfo, tinst) -> - (match anonInfo.TupInfo with + | TType_anon (anonInfo, tinst) -> + (match anonInfo.TupInfo with | TupInfo.Const false -> "" | TupInfo.Const true -> "struct ") + "{|" + String.concat "," (Seq.map2 (fun nm ty -> nm + " " + string ty + ";") anonInfo.SortedNames tinst) + "|}" | TType_fun (domainTy, retTy, nullness) -> "(" + string domainTy + " -> " + string retTy + ")" + nullness.ToString() | TType_ucase (uc, tinst) -> "ucase " + uc.CaseName + (match tinst with [] -> "" | tys -> "<" + String.concat "," (List.map string tys) + ">") - | TType_var (tp, _) -> - match tp.Solution with + | TType_var (tp, _) -> + match tp.Solution with | None -> tp.DisplayName | Some t -> tp.DisplayName + $" (solved: {if maxDepth < 0 then Boolean.TrueString else t.LimitedToString(maxDepth-1)})" | TType_measure ms -> ms.ToString() @@ -4694,16 +4700,16 @@ type TType = override x.ToString() = x.LimitedToString(4) -type TypeInst = TType list +type TypeInst = TType list -type TTypes = TType list +type TTypes = TType list -/// Represents the information identifying an anonymous record -[] -type AnonRecdTypeInfo = +/// Represents the information identifying an anonymous record +[] +type AnonRecdTypeInfo = { // Mutability for pickling/unpickling only - mutable Assembly: CcuThunk + mutable Assembly: CcuThunk mutable TupInfo: TupInfo @@ -4717,7 +4723,7 @@ type AnonRecdTypeInfo = } /// Create an AnonRecdTypeInfo from the basic data - static member Create(ccu: CcuThunk, tupInfo, ids: Ident[]) = + static member Create(ccu: CcuThunk, tupInfo, ids: Ident[]) = let sortedIds = ids |> Array.sortBy (fun id -> id.idText) // Hash all the data to form a unique stamp. @@ -4726,9 +4732,9 @@ type AnonRecdTypeInfo = let stamp = sha1HashInt64 [| for c in ccu.AssemblyName do yield byte c; yield byte (int32 c >>> 8) - match tupInfo with + match tupInfo with | TupInfo.Const b -> yield (if b then 0uy else 1uy) - for id in sortedIds do + for id in sortedIds do for c in id.idText do yield byte c; yield byte (int32 c >>> 8) yield 0uy |] @@ -4746,11 +4752,11 @@ type AnonRecdTypeInfo = { Assembly = ccu; TupInfo = tupInfo; SortedIds = sortedIds; Stamp = stamp; SortedNames = sortedNames; IlTypeName = ilName } /// Get the ILTypeRef for the generated type implied by the anonymous type - member x.ILTypeRef = + member x.ILTypeRef = let ilTypeName = sprintf "<>f__AnonymousType%s%u`%d" (match x.TupInfo with TupInfo.Const b -> if b then "1000" else "") (uint32 x.IlTypeName) x.SortedIds.Length mkILTyRef(x.Assembly.ILScopeRef, ilTypeName) - static member NewUnlinked() : AnonRecdTypeInfo = + static member NewUnlinked() : AnonRecdTypeInfo = { Assembly = Unchecked.defaultof<_> TupInfo = Unchecked.defaultof<_> SortedIds = Unchecked.defaultof<_> @@ -4758,7 +4764,7 @@ type AnonRecdTypeInfo = SortedNames = Unchecked.defaultof<_> IlTypeName = Unchecked.defaultof<_> } - member x.Link d = + member x.Link d = let sortedNames = Array.map textOfId d.SortedIds x.Assembly <- d.Assembly x.TupInfo <- d.TupInfo @@ -4768,19 +4774,19 @@ type AnonRecdTypeInfo = x.IlTypeName <- d.IlTypeName member x.IsLinked = (match box x.SortedIds with null -> true | _ -> false) - + member x.DisplayNameCoreByIdx idx = x.SortedNames[idx] member x.DisplayNameByIdx idx = x.SortedNames[idx] |> ConvertLogicalNameToDisplayName -[] -type TupInfo = +[] +type TupInfo = /// Some constant, e.g. true or false for tupInfo | Const of bool /// Represents a unit of measure in the typed AST [] -type Measure = +type Measure = /// A variable unit-of-measure | Var of typar: Typar @@ -4797,17 +4803,17 @@ type Measure = /// The unit of measure '1', e.g. float = float<1> | One of range: range - /// Raising a measure to a rational power + /// Raising a measure to a rational power | RationalPower of measure: Measure * power: Rational // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - + override x.ToString() = sprintf "%+A" x - - member x.Range = - match x with + + member x.Range = + match x with | Var(typar) -> typar.Range | Const(range= m) -> m | Prod(range= m) -> m @@ -4851,26 +4857,26 @@ module WellKnownValAttribs = let CreateWithFlags(attribs: Attrib list, flags: WellKnownValAttributes) = WellKnownAttribs(attribs, flags) -type Attribs = Attrib list +type Attribs = Attrib list [] -type AttribKind = +type AttribKind = - /// Indicates an attribute refers to a type defined in an imported .NET assembly - | ILAttrib of ilMethodRef: ILMethodRef + /// Indicates an attribute refers to a type defined in an imported .NET assembly + | ILAttrib of ilMethodRef: ILMethodRef - /// Indicates an attribute refers to a type defined in an imported F# assembly + /// Indicates an attribute refers to a type defined in an imported F# assembly | FSAttrib of valRef: ValRef // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// Attrib(tyconRef, kind, unnamedArgs, propVal, appliedToAGetterOrSetter, targetsOpt, range) [] -type Attrib = +type Attrib = | Attrib of tyconRef: TyconRef * @@ -4892,10 +4898,10 @@ type Attrib = /// We keep both source expression and evaluated expression around to help intellisense and signature printing [] -type AttribExpr = +type AttribExpr = /// AttribExpr(source, evaluated) - | AttribExpr of source: Expr * evaluated: Expr + | AttribExpr of source: Expr * evaluated: Expr [] member x.DebugText = x.ToString() @@ -4904,7 +4910,7 @@ type AttribExpr = /// AttribNamedArg(name, type, isField, value) [] -type AttribNamedArg = +type AttribNamedArg = | AttribNamedArg of (string*TType*bool*AttribExpr) [] @@ -4914,7 +4920,7 @@ type AttribNamedArg = /// Constants in expressions [] -type Const = +type Const = | Bool of bool | SByte of sbyte | Byte of byte @@ -4930,9 +4936,9 @@ type Const = | Double of double | Char of char | String of string - | Decimal of Decimal + | Decimal of Decimal | Unit - | Zero // null/zero-bit-pattern + | Zero // null/zero-bit-pattern [] member x.DebugText = x.ToString() @@ -4964,12 +4970,12 @@ type Const = /// the decision tree are labelled by integers that are unique for that /// particular tree. [] -type DecisionTree = +type DecisionTree = /// TDSwitch(input, cases, default, range) /// - /// Indicates a decision point in a decision tree. - /// input -- The expression being tested. If switching over a struct union this + /// Indicates a decision point in a decision tree. + /// input -- The expression being tested. If switching over a struct union this /// must be the address of the expression being tested. /// cases -- The list of tests and their subsequent decision trees /// default -- The default decision tree, if any @@ -4981,11 +4987,11 @@ type DecisionTree = /// Indicates the decision tree has terminated with success, transferring control to the given target with the given parameters. /// results -- the expressions to be bound to the variables at the target /// target -- the target number for the continuation - | TDSuccess of results: Exprs * targetNum: int + | TDSuccess of results: Exprs * targetNum: int /// TDBind(binding, body) /// - /// Bind the given value through the remaining cases of the dtree. + /// Bind the given value through the remaining cases of the dtree. /// These arise from active patterns and some optimizations to prevent /// repeated computations in decision trees. /// binding -- the value and the expression it is bound to @@ -4996,11 +5002,11 @@ type DecisionTree = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// Represents a test and a subsequent decision tree [] -type DecisionTreeCase = +type DecisionTreeCase = | TCase of discriminator: DecisionTreeTest * caseTree: DecisionTree /// Get the discriminator associated with the case @@ -5013,49 +5019,49 @@ type DecisionTreeCase = member x.DebugText = x.ToString() override x.ToString() = sprintf "DecisionTreeCase(...)" - + [] type ActivePatternReturnKind = | RefTypeWrapper | StructTypeWrapper | Boolean - member this.IsStruct with get () = + member this.IsStruct with get () = match this with | RefTypeWrapper -> false | StructTypeWrapper | Boolean -> true [] -type DecisionTreeTest = +type DecisionTreeTest = /// Test if the input to a decision tree matches the given union case | UnionCase of caseRef: UnionCaseRef * tinst: TypeInst - /// Test if the input to a decision tree is an array of the given length - | ArrayLength of length: int * ty: TType + /// Test if the input to a decision tree is an array of the given length + | ArrayLength of length: int * ty: TType - /// Test if the input to a decision tree is the given constant value + /// Test if the input to a decision tree is the given constant value | Const of value: Const - /// Test if the input to a decision tree is null - | IsNull + /// Test if the input to a decision tree is null + | IsNull /// IsInst(source, target) /// - /// Test if the input to a decision tree is an instance of the given type + /// Test if the input to a decision tree is an instance of the given type | IsInst of source: TType * target: TType /// Test.ActivePatternCase(activePatExpr, activePatResTys, activePatRetKind, activePatIdentity, idx, activePatInfo) /// - /// Run the active pattern and bind a successful result to a - /// variable in the remaining tree. + /// Run the active pattern and bind a successful result to a + /// variable in the remaining tree. /// activePatExpr -- The active pattern function being called, perhaps applied to some active pattern parameters. /// activePatResTys -- The result types (case types) of the active pattern. /// activePatRetKind -- Indicating what is returning from the active pattern - /// activePatIdentity -- The value and the types it is applied to. If there are any active pattern parameters then this is empty. + /// activePatIdentity -- The value and the types it is applied to. If there are any active pattern parameters then this is empty. /// idx -- The case number of the active pattern which the test relates to. /// activePatternInfo -- The extracted info for the active pattern. | ActivePatternCase of - activePatExpr: Expr * + activePatExpr: Expr * activePatResTys: TTypes * activePatRetKind: ActivePatternReturnKind * activePatIdentity: (ValRef * TypeInst) option * @@ -5069,16 +5075,16 @@ type DecisionTreeTest = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x -/// A target of a decision tree. Can be thought of as a little function, though is compiled as a local block. +/// A target of a decision tree. Can be thought of as a little function, though is compiled as a local block. /// -- boundVals - The values bound at the target, matching the valuesin the TDSuccess /// -- targetExpr - The expression to evaluate if we branch to the target /// -- debugPoint - The debug point for the target /// -- isStateVarFlags - Indicates which, if any, of the values are represents as state machine variables [] -type DecisionTreeTarget = - | TTarget of +type DecisionTreeTarget = + | TTarget of boundVals: Val list * targetExpr: Expr * isStateVarFlags: bool list option @@ -5098,7 +5104,7 @@ type Bindings = Binding list /// -- expr: The expression to execute to get the value /// -- debugPoint: The debug point for the binding [] -type Binding = +type Binding = | TBind of var: Val * expr: Expr * @@ -5118,10 +5124,10 @@ type Binding = override x.ToString() = sprintf "TBind(%s, ...)" (x.Var.CompiledName None) -/// Represents a reference to an active pattern element. The -/// integer indicates which choice in the target set is being selected by this item. +/// Represents a reference to an active pattern element. The +/// integer indicates which choice in the target set is being selected by this item. [] -type ActivePatternElemRef = +type ActivePatternElemRef = | APElemRef of activePatternInfo: ActivePatternInfo * activePatternVal: ValRef * @@ -5137,7 +5143,7 @@ type ActivePatternElemRef = /// Get a reference to the value for the active pattern being referred to member x.ActivePatternRetKind = (let (APElemRef(_, _, _, activePatRetKind)) = x in activePatRetKind) - /// Get the index of the active pattern element within the overall active pattern + /// Get the index of the active pattern element within the overall active pattern member x.CaseIndex = (let (APElemRef(_, _, n, _)) = x in n) [] @@ -5148,12 +5154,12 @@ type ActivePatternElemRef = /// Records the "extra information" for a value compiled as a method (rather /// than a closure or a local), including argument names, attributes etc. [] -type ValReprInfo = +type ValReprInfo = /// ValReprInfo (typars, args, result) | ValReprInfo of typars: TyparReprInfo list * args: ArgReprInfo list list * - result: ArgReprInfo + result: ArgReprInfo /// Get the extra information about the arguments for the value member x.ArgInfos = (let (ValReprInfo(_, args, _)) = x in args) @@ -5173,17 +5179,17 @@ type ValReprInfo = /// Get the kind of each type parameter member x.KindsOfTypars = (let (ValReprInfo(n, _, _)) = x in n |> List.map (fun (TyparReprInfo(_, k)) -> k)) - /// Get the total number of arguments - member x.TotalArgCount = + /// Get the total number of arguments + member x.TotalArgCount = let (ValReprInfo(_, args, _)) = x // This is List.sumBy List.length args // We write this by hand as it can be a performance bottleneck in LinkagePartialKey - let rec loop (args: ArgReprInfo list list) acc = - match args with - | [] -> acc - | [] :: t -> loop t acc - | [_] :: t -> loop t (acc+1) - | (_ :: _ :: h) :: t -> loop t (acc + h.Length + 2) + let rec loop (args: ArgReprInfo list list) acc = + match args with + | [] -> acc + | [] :: t -> loop t acc + | [_] :: t -> loop t (acc+1) + | (_ :: _ :: h) :: t -> loop t (acc + h.Length + 2) loop args 0 member x.ArgNames = @@ -5205,7 +5211,7 @@ type ArgReprInfo = { /// The attributes for the argument // MUTABILITY: used when propagating signature attributes into the implementation. - mutable Attribs: WellKnownValAttribs + mutable Attribs: WellKnownValAttribs /// The name for the argument at this position, if any // MUTABILITY: used when propagating names of parameters from signature into the implementation. @@ -5222,13 +5228,13 @@ type ArgReprInfo = override _.ToString() = "ArgReprInfo(...)" /// Records the extra metadata stored about typars for type parameters -/// compiled as "real" IL type parameters, specifically for values with +/// compiled as "real" IL type parameters, specifically for values with /// ValReprInfo. Any information here is propagated from signature through /// to the compiled code. type TyparReprInfo = TyparReprInfo of Ident * TyparKind type Typars = Typar list - + type Exprs = Expr list type Vals = Val list @@ -5236,42 +5242,42 @@ type Vals = Val list /// Represents an expression in the typed abstract syntax [] type Expr = - /// A constant expression. + /// A constant expression. | Const of value: Const * range: range * constType: TType - /// Reference a value. The flag is only relevant if the value is an object model member - /// and indicates base calls and special uses of object constructors. + /// Reference a value. The flag is only relevant if the value is an object model member + /// and indicates base calls and special uses of object constructors. | Val of valRef: ValRef * flags: ValUseFlag * range: range - /// Sequence expressions, used for "a;b", "let a = e in b;a" and "a then b" (the last an OO constructor). + /// Sequence expressions, used for "a;b", "let a = e in b;a" and "a then b" (the last an OO constructor). | Sequential of expr1: Expr * expr2: Expr * kind: SequentialOpKind * range: range - /// Lambda expressions. - - /// Why multiple vspecs? A Expr.Lambda taking multiple arguments really accepts a tuple. - /// But it is in a convenient form to be compile accepting multiple - /// arguments, e.g. if compiled as a toplevel static method. + /// Lambda expressions. + + /// Why multiple vspecs? A Expr.Lambda taking multiple arguments really accepts a tuple. + /// But it is in a convenient form to be compile accepting multiple + /// arguments, e.g. if compiled as a toplevel static method. | Lambda of unique: Unique * ctorThisValOpt: Val option * baseValOpt: Val option * valParams: Val list * - bodyExpr: Expr * + bodyExpr: Expr * range: range * overallType: TType - /// Type lambdas. These are used for the r.h.s. of polymorphic 'let' bindings and - /// for expressions that implement first-class polymorphic values. + /// Type lambdas. These are used for the r.h.s. of polymorphic 'let' bindings and + /// for expressions that implement first-class polymorphic values. | TyLambda of unique: Unique * typeParams: Typars * @@ -5280,45 +5286,45 @@ type Expr = overallType: TType /// Applications. - /// Applications combine type and term applications, and are normalized so - /// that sequential applications are combined, so "(f x y)" becomes "f [[x];[y]]". - /// The type attached to the function is the formal function type, used to ensure we don't build application - /// nodes that over-apply when instantiating at function types. + /// Applications combine type and term applications, and are normalized so + /// that sequential applications are combined, so "(f x y)" becomes "f [[x];[y]]". + /// The type attached to the function is the formal function type, used to ensure we don't build application + /// nodes that over-apply when instantiating at function types. | App of funcExpr: Expr * formalType: TType * typeArgs: TypeInst * args: Exprs * - range: range + range: range - /// Bind a recursive set of values. + /// Bind a recursive set of values. | LetRec of bindings: Bindings * bodyExpr: Expr * range: range * frees: FreeVarsCache - /// Bind a value. + /// Bind a value. | Let of binding: Binding * bodyExpr: Expr * range: range * frees: FreeVarsCache - // Object expressions: A closure that implements an interface or a base type. - // The base object type might be a delegate type. - | Obj of - unique: Unique * + // Object expressions: A closure that implements an interface or a base type. + // The base object type might be a delegate type. + | Obj of + unique: Unique * objTy: TType * (* <-- NOTE: specifies type parameters for base type *) - baseVal: Val option * - ctorCall: Expr * - overrides: ObjExprMethod list * - interfaceImpls: (TType * ObjExprMethod list) list * + baseVal: Val option * + ctorCall: Expr * + overrides: ObjExprMethod list * + interfaceImpls: (TType * ObjExprMethod list) list * range: range - /// Matches are a more complicated form of "let" with multiple possible destinations - /// and possibly multiple ways to get to each destination. - /// The first range is that of the expression being matched, which is used + /// Matches are a more complicated form of "let" with multiple possible destinations + /// and possibly multiple ways to get to each destination. + /// The first range is that of the expression being matched, which is used /// as the range for all the decision making and binding that happens during the decision tree /// execution. | Match of @@ -5329,24 +5335,24 @@ type Expr = fullRange: range * exprType: TType - /// If we statically know some information then in many cases we can use a more optimized expression - /// This is primarily used by terms in the standard library, particularly those implementing overloaded - /// operators. + /// If we statically know some information then in many cases we can use a more optimized expression + /// This is primarily used by terms in the standard library, particularly those implementing overloaded + /// operators. | StaticOptimization of conditions: StaticOptimization list * expr: Expr * alternativeExpr: Expr * range: range - /// An intrinsic applied to some (strictly evaluated) arguments - /// A few of intrinsics (TOp_try, TOp.While, TOp.IntegerForLoop) expect arguments kept in a normal form involving lambdas + /// An intrinsic applied to some (strictly evaluated) arguments + /// A few of intrinsics (TOp_try, TOp.While, TOp.IntegerForLoop) expect arguments kept in a normal form involving lambdas | Op of op: TOp * typeArgs: TypeInst * args: Exprs * range: range - /// Indicates the expression is a quoted expression tree. + /// Indicates the expression is a quoted expression tree. /// // MUTABILITY: this use of mutability is awkward and perhaps should be removed | Quote of @@ -5354,8 +5360,8 @@ type Expr = quotationInfo: ((ILTypeRef list * TTypes * Exprs * ExprData) * (ILTypeRef list * TTypes * Exprs * ExprData)) option ref * isFromQueryExpression: bool * range: range * - quotedType: TType - + quotedType: TType + /// Used in quotation generation to indicate a witness argument, spliced into a quotation literal. /// /// For example: @@ -5369,28 +5375,28 @@ type Expr = /// /// f$W(witnessForSin, x) { return Deserialize(<@ sin$W _spliceHole1 _spliceHole2 @>, [| WitnessArg(witnessForSin), x |]) } /// - /// where _spliceHole1 will be the location of the witness argument in the quotation data, and + /// where _spliceHole1 will be the location of the witness argument in the quotation data, and /// witnessArg is the lambda for the witness - /// + /// | WitnessArg of traitInfo: TraitConstraintInfo * range: range - /// Indicates a free choice of typars that arises due to - /// minimization of polymorphism at let-rec bindings. These are - /// resolved to a concrete instantiation on subsequent rewrites. + /// Indicates a free choice of typars that arises due to + /// minimization of polymorphism at let-rec bindings. These are + /// resolved to a concrete instantiation on subsequent rewrites. | TyChoose of typeParams: Typars * bodyExpr: Expr * range: range - /// An instance of a link node occurs for every use of a recursively bound variable. When type-checking - /// the recursive bindings a dummy expression is stored in the mutable reference cell. - /// After type checking the bindings this is replaced by a use of the variable, perhaps at an - /// appropriate type instantiation. These are immediately eliminated on subsequent rewrites. + /// An instance of a link node occurs for every use of a recursively bound variable. When type-checking + /// the recursive bindings a dummy expression is stored in the mutable reference cell. + /// After type checking the bindings this is replaced by a use of the variable, perhaps at an + /// appropriate type instantiation. These are immediately eliminated on subsequent rewrites. | Link of Expr ref - /// Indicates a debug point should be placed prior to the expression. + /// Indicates a debug point should be placed prior to the expression. | DebugPoint of DebugPointAtLeafExpr * Expr [] @@ -5398,14 +5404,14 @@ type Expr = override expr.ToString() = expr.ToDebugString(3) - member expr.ToDebugString(depth: int) : string = + member expr.ToDebugString(depth: int) : string = if depth = 0 then ".." else let depth = depth - 1 - match expr with + match expr with | Const (c, _, _) -> string c | Val (v, _, _) -> v.LogicalName | Sequential (e1, e2, _, _) -> "Sequential(" + e1.ToDebugString(depth) + ", " + e2.ToDebugString(depth) + ")" - | Lambda (_, _, _, vs, body, _, _) -> sprintf "Lambda(%+A, " vs + body.ToDebugString(depth) + ")" + | Lambda (_, _, _, vs, body, _, _) -> sprintf "Lambda(%+A, " vs + body.ToDebugString(depth) + ")" | TyLambda (_, tps, body, _, _) -> sprintf "TyLambda(%+A, " tps + body.ToDebugString(depth) + ")" | App (f, _, _, args, _) -> "App(" + f.ToDebugString(depth) + ", [" + String.concat ", " (args |> List.map (fun e -> e.ToDebugString(depth))) + "])" | LetRec _ -> "LetRec(..)" @@ -5424,24 +5430,24 @@ type Expr = member expr.Range = match expr with | Expr.Val (_, _, m) | Expr.Op (_, _, _, m) | Expr.Const (_, m, _) | Expr.Quote (_, _, _, m, _) - | Expr.Obj (_, _, _, _, _, _, m) | Expr.App (_, _, _, _, m) | Expr.Sequential (_, _, _, m) - | Expr.StaticOptimization (_, _, _, m) | Expr.Lambda (_, _, _, _, _, m, _) + | Expr.Obj (_, _, _, _, _, _, m) | Expr.App (_, _, _, _, m) | Expr.Sequential (_, _, _, m) + | Expr.StaticOptimization (_, _, _, m) | Expr.Lambda (_, _, _, _, _, m, _) | Expr.WitnessArg (_, m) | Expr.TyLambda (_, _, _, m, _)| Expr.TyChoose (_, _, m) | Expr.LetRec (_, _, m, _) | Expr.Let (_, _, m, _) | Expr.Match (_, _, _, _, m, _) -> m | Expr.Link eref -> eref.Value.Range | Expr.DebugPoint (_, e2) -> e2.Range - + [] type TOp = /// An operation representing the creation of a union value of the particular union case - | UnionCase of UnionCaseRef + | UnionCase of UnionCaseRef /// An operation representing the creation of an exception value using an F# exception declaration | ExnConstr of TyconRef /// An operation representing the creation of a tuple value - | Tuple of TupInfo + | Tuple of TupInfo /// An operation representing the creation of an anonymous record | AnonRecd of AnonRecdTypeInfo @@ -5456,7 +5462,7 @@ type TOp = | Bytes of byte[] /// Constant uint16 arrays (used for parser tables) - | UInt16s of uint16[] + | UInt16s of uint16[] /// An operation representing a lambda-encoded while loop. The special while loop marker is used to mark compilations of 'foreach' expressions | While of spWhile: DebugPointAtWhile * marker: SpecialWhileLoopMarker @@ -5470,30 +5476,30 @@ type TOp = /// An operation representing a lambda-encoded try/finally | TryFinally of spTry: DebugPointAtTry * spFinally: DebugPointAtFinally - /// Construct a record or object-model value. The ValRef is for self-referential class constructors, otherwise - /// it indicates that we're in a constructor and the purpose of the expression is to - /// fill in the fields of a pre-created but uninitialized object, and to assign the initialized - /// version of the object into the optional mutable cell pointed to be the given value. + /// Construct a record or object-model value. The ValRef is for self-referential class constructors, otherwise + /// it indicates that we're in a constructor and the purpose of the expression is to + /// fill in the fields of a pre-created but uninitialized object, and to assign the initialized + /// version of the object into the optional mutable cell pointed to be the given value. | Recd of RecordConstructionInfo * TyconRef - + /// An operation representing setting a record or class field - | ValFieldSet of RecdFieldRef + | ValFieldSet of RecdFieldRef /// An operation representing getting a record or class field - | ValFieldGet of RecdFieldRef + | ValFieldGet of RecdFieldRef /// An operation representing getting the address of a record field - | ValFieldGetAddr of RecdFieldRef * readonly: bool + | ValFieldGetAddr of RecdFieldRef * readonly: bool /// An operation representing getting an integer tag for a union value representing the union case number - | UnionCaseTagGet of TyconRef + | UnionCaseTagGet of TyconRef /// An operation representing a coercion that proves a union value is of a particular union case. This is not a test, its /// simply added proof to enable us to generate verifiable code for field access on union types | UnionCaseProof of UnionCaseRef /// An operation representing a field-get from a union value, where that value has been proven to be of the corresponding union case. - | UnionCaseFieldGet of UnionCaseRef * int + | UnionCaseFieldGet of UnionCaseRef * int /// An operation representing a field-get from a union value, where that value has been proven to be of the corresponding union case. | UnionCaseFieldGetAddr of UnionCaseRef * int * readonly: bool @@ -5502,27 +5508,27 @@ type TOp = | UnionCaseFieldSet of UnionCaseRef * int /// An operation representing a field-get from an F# exception value. - | ExnFieldGet of TyconRef * int + | ExnFieldGet of TyconRef * int /// An operation representing a field-set on an F# exception value. - | ExnFieldSet of TyconRef * int + | ExnFieldSet of TyconRef * int /// An operation representing a field-get from an F# tuple value. - | TupleFieldGet of TupInfo * int + | TupleFieldGet of TupInfo * int - /// IL assembly code - type list are the types pushed on the stack - | ILAsm of - instrs: ILInstr list * - retTypes: TTypes + /// IL assembly code - type list are the types pushed on the stack + | ILAsm of + instrs: ILInstr list * + retTypes: TTypes - /// Generate a ldflda on an 'a ref. + /// Generate a ldflda on an 'a ref. | RefAddrGet of bool - /// Conversion node, compiled via type-directed translation or to box/unbox - | Coerce + /// Conversion node, compiled via type-directed translation or to box/unbox + | Coerce - /// Represents a "rethrow" operation. May not be rebound, or used outside of try-finally, expecting a unit argument - | Reraise + /// Represents a "rethrow" operation. May not be rebound, or used outside of try-finally, expecting a unit argument + | Reraise /// Used for state machine compilation | Return @@ -5533,34 +5539,34 @@ type TOp = /// Used for state machine compilation | Label of ILCodeLabel - /// Pseudo method calls. This is used for overloaded operations like op_Addition. - | TraitCall of TraitConstraintInfo + /// Pseudo method calls. This is used for overloaded operations like op_Addition. + | TraitCall of TraitConstraintInfo - /// Operation nodes representing C-style operations on byrefs and mutable vals (l-values) - | LValueOp of LValueOperation * ValRef + /// Operation nodes representing C-style operations on byrefs and mutable vals (l-values) + | LValueOp of LValueOperation * ValRef /// IL method calls. - /// isProperty -- used for quotation reflection, property getters & setters - /// noTailCall - DllImport? if so don't tailcall + /// isProperty -- used for quotation reflection, property getters & setters + /// noTailCall - DllImport? if so don't tailcall /// retTypes -- the types of pushed values, if any - | ILCall of - isVirtual: bool * - isProtected: bool * - isStruct: bool * - isCtor: bool * - valUseFlag: ValUseFlag * - isProperty: bool * - noTailCall: bool * - ilMethRef: ILMethodRef * - enclTypeInst: TypeInst * - methInst: TypeInst * - retTypes: TTypes + | ILCall of + isVirtual: bool * + isProtected: bool * + isStruct: bool * + isCtor: bool * + valUseFlag: ValUseFlag * + isProperty: bool * + noTailCall: bool * + ilMethRef: ILMethodRef * + enclTypeInst: TypeInst * + methInst: TypeInst * + retTypes: TTypes [] member x.DebugText = x.ToString() - - override op.ToString() = - match op with + + override op.ToString() = + match op with | UnionCase ucref -> "UnionCase(" + ucref.CaseName + ")" | ExnConstr ecref -> "ExnConstr(" + ecref.LogicalName + ")" | Tuple _tupinfo -> "Tuple" @@ -5597,13 +5603,13 @@ type TOp = | ILCall (_,_,_,_,_,_,_,ilMethRef,_,_,_) -> "ILCall(" + ilMethRef.ToString() + ",..)" /// Represents the kind of record construction operation. -type RecordConstructionInfo = +type RecordConstructionInfo = - /// We're in an explicit constructor. The purpose of the record expression is to - /// fill in the fields of a pre-created but uninitialized object + /// We're in an explicit constructor. The purpose of the record expression is to + /// fill in the fields of a pre-created but uninitialized object | RecdExprIsObjInit - /// Normal record construction + /// Normal record construction | RecdExpr /// If this is Some ty then it indicates that a .NET 2.0 constrained call is required, with the given type as the @@ -5611,52 +5617,52 @@ type RecordConstructionInfo = type ConstrainedCallInfo = TType option /// Represents the kind of looping operation. -type SpecialWhileLoopMarker = +type SpecialWhileLoopMarker = | NoSpecialWhileLoopMarker /// Marks the compiled form of a 'for ... in ... do ' expression | WhileLoopForCompiledForEachExprMarker - + /// Represents the kind of looping operation. -type ForLoopStyle = +type ForLoopStyle = /// Evaluate start and end once, loop up - | FSharpForLoopUp + | FSharpForLoopUp /// Evaluate start and end once, loop down - | FSharpForLoopDown + | FSharpForLoopDown /// Evaluate start once and end multiple times, loop up | CSharpForLoopUp /// Indicates what kind of pointer operation this is. -type LValueOperation = +type LValueOperation = - /// In C syntax this is: &localv + /// In C syntax this is: &localv | LAddrOf of readonly: bool - /// In C syntax this is: *localv_ptr - | LByrefGet + /// In C syntax this is: *localv_ptr + | LByrefGet /// In C syntax this is: localv = e, note == *(&localv) = e == LAddrOf; LByrefSet - | LSet + | LSet - /// In C syntax this is: *localv_ptr = e - | LByrefSet + /// In C syntax this is: *localv_ptr = e + | LByrefSet /// Represents the kind of sequential operation, i.e. "normal" or "to a before returning b" -type SequentialOpKind = - /// a ; b - | NormalSeq +type SequentialOpKind = + /// a ; b + | NormalSeq - /// let res = a in b;res - | ThenDoSeq + /// let res = a in b;res + | ThenDoSeq /// Indicates how a value, function or member is being used at a particular usage point. type ValUseFlag = /// Indicates a use of a value represents a call to a method that may require - /// a .NET 2.0 constrained call. A constrained call is only used for calls where + /// a .NET 2.0 constrained call. A constrained call is only used for calls where // the object argument is a value type or generic type, and the call is to a method // on System.Object, System.ValueType, System.Enum or an interface methods. | PossibleConstrainedCall of ty: TType @@ -5672,21 +5678,21 @@ type ValUseFlag = /// A call to a base method, e.g. 'base.OnPaint(args)' | VSlotDirectCall - + /// Represents the kind of an F# core library static optimization construct -type StaticOptimization = +type StaticOptimization = /// Indicates the static optimization applies when a type equality holds | TTyconEqualsTycon of ty1: TType * ty2: TType /// Indicates the static optimization applies when a type is a struct - | TTyconIsStruct of ty: TType - -/// A representation of a method in an object expression. + | TTyconIsStruct of ty: TType + +/// A representation of a method in an object expression. /// /// TObjExprMethod(slotsig, attribs, methTyparsOfOverridingMethod, methodParams, methodBodyExpr, m) [] -type ObjExprMethod = +type ObjExprMethod = | TObjExprMethod of slotSig: SlotSig * @@ -5707,7 +5713,7 @@ type ObjExprMethod = /// /// TSlotSig(methodName, declaringType, declaringTypeParameters, methodTypeParameters, slotParameters, returnTy) [] -type SlotSig = +type SlotSig = | TSlotSig of methodName: string * declaringType: TType * @@ -5719,7 +5725,7 @@ type SlotSig = /// The name of the method member ss.Name = let (TSlotSig(nm, _, _, _, _, _)) = ss in nm - /// The (instantiated) type which the slot is logically a part of + /// The (instantiated) type which the slot is logically a part of member ss.DeclaringType = let (TSlotSig(_, ty, _, _, _, _)) = ss in ty /// The class type parameters of the slot @@ -5739,11 +5745,11 @@ type SlotSig = override ss.ToString() = sprintf "TSlotSig(%s, ...)" ss.Name -/// Represents a parameter to an abstract method slot. +/// Represents a parameter to an abstract method slot. /// /// TSlotParam(nm, ty, inFlag, outFlag, optionalFlag, attribs) [] -type SlotParam = +type SlotParam = | TSlotParam of paramName: string option * paramType: TType * @@ -5763,19 +5769,19 @@ type SlotParam = type OpenDeclaration = { /// Syntax after 'open' as it's presented in source code. Target: SynOpenDeclTarget - + /// Full range of the open declaration. Range: range option /// Modules or namespaces which is opened with this declaration. - Modules: ModuleOrNamespaceRef list - + Modules: ModuleOrNamespaceRef list + /// Types whose static content is opened with this declaration. Types: TType list /// Scope in which open declaration is visible. - AppliedScope: range - + AppliedScope: range + /// If it's `namespace Xxx.Yyy` declaration. IsOwnNamespace: bool } @@ -5784,24 +5790,24 @@ type OpenDeclaration = static member Create(target: SynOpenDeclTarget, modules: ModuleOrNamespaceRef list, types: TType list, appliedScope: range, isOwnNamespace: bool) = { Target = target Range = - match target with + match target with | SynOpenDeclTarget.ModuleOrNamespace (range=m) | SynOpenDeclTarget.Type (range=m) -> Some m Types = types Modules = modules AppliedScope = appliedScope IsOwnNamespace = isOwnNamespace } - -/// The contents of a module-or-namespace-fragment definition + +/// The contents of a module-or-namespace-fragment definition [] -type ModuleOrNamespaceContents = - /// Indicates the module fragment is made of several module fragments in succession - | TMDefs of defs: ModuleOrNamespaceContents list +type ModuleOrNamespaceContents = + /// Indicates the module fragment is made of several module fragments in succession + | TMDefs of defs: ModuleOrNamespaceContents list /// Indicates the given 'open' declarations are active | TMDefOpens of openDecls: OpenDeclaration list - /// Indicates the module fragment is a 'let' definition + /// Indicates the module fragment is a 'let' definition | TMDefLet of binding: Binding * range: range /// Indicates the module fragment is an evaluation of expression for side-effects @@ -5814,19 +5820,19 @@ type ModuleOrNamespaceContents = //[] member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x -/// A named module-or-namespace-fragment definition +/// A named module-or-namespace-fragment definition [] -type ModuleOrNamespaceBinding = +type ModuleOrNamespaceBinding = - | Binding of binding: Binding + | Binding of binding: Binding /// The moduleOrNamespace represents the signature of the module. /// The moduleOrNamespaceContents contains the definitions of the module. /// The same set of entities are bound in the ModuleOrNamespace as in the ModuleOrNamespaceContents. - | Module of - moduleOrNamespace: ModuleOrNamespace * + | Module of + moduleOrNamespace: ModuleOrNamespace * moduleOrNamespaceContents: ModuleOrNamespaceContents [] @@ -5841,16 +5847,16 @@ type NamedDebugPointKey = override x.GetHashCode() = hash x.Name + hash x.Range - override x.Equals(yobj: objnull) = - match yobj with + override x.Equals(yobj: objnull) = + match yobj with | :? NamedDebugPointKey as y -> equals x.Range y.Range && x.Name = y.Name | _ -> false interface IComparable with member x.CompareTo(yobj: obj) = - match yobj with - | :? NamedDebugPointKey as y -> - let c = rangeOrder.Compare(x.Range, y.Range) + match yobj with + | :? NamedDebugPointKey as y -> + let c = rangeOrder.Compare(x.Range, y.Range) if c <> 0 then c else compare x.Name y.Name | _ -> -1 @@ -5859,8 +5865,8 @@ type NamedDebugPointKey = /// /// CheckedImplFile (qualifiedNameOfFile, pragmas, signature, contents, hasExplicitEntryPoint, isScript, anonRecdTypeInfo) [] -type CheckedImplFile = - | CheckedImplFile of +type CheckedImplFile = + | CheckedImplFile of qualifiedNameOfFile: QualifiedNameOfFile * signature: ModuleOrNamespaceType * contents: ModuleOrNamespaceContents * @@ -5886,8 +5892,8 @@ type CheckedImplFile = /// Represents a complete typechecked assembly, made up of multiple implementation files. [] -type CheckedImplFileAfterOptimization = - { ImplFile: CheckedImplFile +type CheckedImplFileAfterOptimization = + { ImplFile: CheckedImplFile OptimizeDuringCodeGen: bool -> Expr -> Expr } [] @@ -5897,7 +5903,7 @@ type CheckedImplFileAfterOptimization = /// Represents a complete typechecked assembly, made up of multiple implementation files. [] -type CheckedAssemblyAfterOptimization = +type CheckedAssemblyAfterOptimization = | CheckedAssemblyAfterOptimization of CheckedImplFileAfterOptimization list [] @@ -5906,53 +5912,53 @@ type CheckedAssemblyAfterOptimization = override x.ToString() = "CheckedAssemblyAfterOptimization(...)" [] -type CcuData = +type CcuData = { - /// Holds the file name for the DLL, if any - FileName: string option - - /// Holds the data indicating how this assembly/module is referenced from the code being compiled. + /// Holds the file name for the DLL, if any + FileName: string option + + /// Holds the data indicating how this assembly/module is referenced from the code being compiled. ILScopeRef: ILScopeRef - - /// A unique stamp for this DLL + + /// A unique stamp for this DLL Stamp: Stamp - - /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations - QualifiedName: string option - - /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) - SourceCodeDirectory: string - + + /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations + QualifiedName: string option + + /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) + SourceCodeDirectory: string + /// Indicates that this DLL was compiled using the F# compiler and has F# metadata - IsFSharp: bool - + IsFSharp: bool + #if !NO_TYPEPROVIDERS /// Is the CCu an assembly injected by a type provider - IsProviderGenerated: bool + IsProviderGenerated: bool /// Triggered when the contents of the CCU are invalidated - InvalidateEvent: IEvent + InvalidateEvent: IEvent - /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality + /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality /// logic in tastops.fs - ImportProvidedType: Tainted -> TType - + ImportProvidedType: Tainted -> TType + #endif /// Indicates that this DLL uses pre-F#-4.0 quotation literals somewhere. This is used to implement a restriction on static linking mutable UsesFSharp20PlusQuotations: bool - + /// A handle to the full specification of the contents of the module contained in this ccu - // NOTE: may contain transient state during typechecking + // NOTE: may contain transient state during typechecking mutable Contents: ModuleOrNamespace - - /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality + + /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality /// logic in tastops.fs - TryGetILModuleDef: unit -> ILModuleDef option - - /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality + TryGetILModuleDef: unit -> ILModuleDef option + + /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality /// logic in tastops.fs - MemberSignatureEquality: TType -> TType -> bool - + MemberSignatureEquality: TType -> TType -> bool + /// The table of .NET CLI type forwarders for this assembly TypeForwarders: CcuTypeForwarderTable @@ -5995,7 +6001,7 @@ type CcuTypeForwarderTable = Root : CcuTypeForwarderTree } - static member Empty : CcuTypeForwarderTable = { Root = CcuTypeForwarderTree.Empty } + static member Empty : CcuTypeForwarderTable = { Root = CcuTypeForwarderTree.Empty } member this.TryGetValue (path:string array) (item:string): Lazy option = CcuTypeForwarderTable.findInTree (ArraySegment path) item this.Root @@ -6020,7 +6026,7 @@ type CcuReference = string // ILAssemblyRef // the data structure, or it is a delayed fixup, i.e. an invalid dangling // reference that has not had an appropriate fixup applied. [] -type CcuThunk = +type CcuThunk = { /// ccu.target is null when a reference is missing in the transitive closure of static references that /// may potentially be required for the metadata of referenced DLLs. @@ -6028,36 +6034,36 @@ type CcuThunk = name: CcuReference } - /// Dereference the assembly reference - member ccu.Deref = - if isNull (box ccu.target) then + /// Dereference the assembly reference + member ccu.Deref = + if isNull (box ccu.target) then raise(UnresolvedReferenceNoRange ccu.name) ccu.target - + /// Indicates if this assembly reference is unresolved member ccu.IsUnresolvedReference = isNull (box ccu.target) /// Ensure the ccu is derefable in advance. Supply a path to attach to any resulting error message. - member ccu.EnsureDerefable(requiringPath: string[]) = - if ccu.IsUnresolvedReference then + member ccu.EnsureDerefable(requiringPath: string[]) = + if ccu.IsUnresolvedReference then let path = String.Join(".", requiringPath) raise(UnresolvedPathReferenceNoRange(ccu.name, path)) - + /// Indicates that this DLL uses F# 2.0+ quotation literals somewhere. This is used to implement a restriction on static linking. - member ccu.UsesFSharp20PlusQuotations - with get() = ccu.Deref.UsesFSharp20PlusQuotations + member ccu.UsesFSharp20PlusQuotations + with get() = ccu.Deref.UsesFSharp20PlusQuotations and set v = ccu.Deref.UsesFSharp20PlusQuotations <- v /// The short name of the assembly being referenced member ccu.AssemblyName = ccu.name - /// Holds the data indicating how this assembly/module is referenced from the code being compiled. + /// Holds the data indicating how this assembly/module is referenced from the code being compiled. member ccu.ILScopeRef = ccu.Deref.ILScopeRef /// A unique stamp for this assembly member ccu.Stamp = ccu.Deref.Stamp - /// Holds the file name for the assembly, if any + /// Holds the file name for the assembly, if any member ccu.FileName = ccu.Deref.FileName /// Try to get the .NET Assembly, if known. May not be present for `IsFSharp` for @@ -6072,17 +6078,17 @@ type CcuThunk = member ccu.ImportProvidedType ty: TType = ccu.Deref.ImportProvidedType ty #endif - /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations + /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations member ccu.QualifiedName = ccu.Deref.QualifiedName - /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) + /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) member ccu.SourceCodeDirectory = ccu.Deref.SourceCodeDirectory /// Indicates that this DLL was compiled using the F# compiler and has F# metadata member ccu.IsFSharp = ccu.Deref.IsFSharp /// A handle to the full specification of the contents of the module contained in this ccu - // NOTE: may contain transient state during typechecking + // NOTE: may contain transient state during typechecking member ccu.Contents = ccu.Deref.Contents /// The table of type forwarders for this assembly @@ -6095,41 +6101,41 @@ type CcuThunk = member ccu.RootTypeAndExceptionDefinitions = ccu.Contents.ModuleOrNamespaceType.TypeAndExceptionDefinitions /// Create a CCU with the given name and contents - static member Create(nm, x) = - { target = x + static member Create(nm, x) = + { target = x name = nm } /// Create a CCU with the given name but where the contents have not yet been specified - static member CreateDelayed nm = - { target = Unchecked.defaultof<_> + static member CreateDelayed nm = + { target = Unchecked.defaultof<_> name = nm } /// Fixup a CCU to have the given contents - member x.Fixup(avail: CcuThunk) = + member x.Fixup(avail: CcuThunk) = match box x.target with | null -> () - | _ -> + | _ -> // In the IDE we tolerate a double-fixup of FSHarp.Core when editing the FSharp.Core project itself - if x.AssemblyName <> "FSharp.Core" then + if x.AssemblyName <> "FSharp.Core" then errorR(Failure("internal error: Fixup: the ccu thunk for assembly "+x.AssemblyName+" not delayed!")) assert (avail.AssemblyName = x.AssemblyName) - x.target <- + x.target <- match box avail.target with | null -> error(Failure("internal error: ccu thunk '"+avail.name+"' not fixed up!")) | _ -> avail.target /// Try to resolve a path into the CCU by referencing the .NET/CLI type forwarder table of the CCU - member ccu.TryForward(nlpath: string[], item: string) : EntityRef option = + member ccu.TryForward(nlpath: string[], item: string) : EntityRef option = ccu.EnsureDerefable nlpath ccu.TypeForwarders.TryGetValue nlpath item |> Option.map (fun entity -> entity.Force()) /// Used to make forward calls into the type/assembly loader when comparing member signatures during linking - member ccu.MemberSignatureEquality(ty1: TType, ty2: TType) = + member ccu.MemberSignatureEquality(ty1: TType, ty2: TType) = ccu.Deref.MemberSignatureEquality ty1 ty2 - + [] member x.DebugText = x.ToString() @@ -6179,12 +6185,12 @@ type FreeLocals = Zset /// (never cached type checking). Cached in expressions. Not pickled. type FreeTypars = Zset -/// Represents a set of 'free' named type definitions. Used to collect the named type definitions referred to +/// Represents a set of 'free' named type definitions. Used to collect the named type definitions referred to /// from a type or expression. Computed and cached by later phases (never cached type checking). Cached /// in expressions. Not pickled. type FreeTycons = Zset -/// Represents a set of 'free' record field definitions. Used to collect the record field definitions referred to +/// Represents a set of 'free' record field definitions. Used to collect the record field definitions referred to /// from an expression. type FreeRecdFields = Zset @@ -6194,17 +6200,17 @@ type FreeUnionCases = Zset /// Represents a set of 'free' type-related elements, including named types, trait solutions, union cases and /// record fields. [] -type FreeTyvars = +type FreeTyvars = { - /// The summary of locally defined type definitions used in the expression. These may be made private by a signature - /// and we have to check various conditions associated with that. + /// The summary of locally defined type definitions used in the expression. These may be made private by a signature + /// and we have to check various conditions associated with that. FreeTycons: FreeTycons /// The summary of values used as trait solutions FreeTraitSolutions: FreeLocals - - /// The summary of type parameters used in the expression. These may not escape the enclosing generic construct - /// and we have to check various conditions associated with that. + + /// The summary of type parameters used in the expression. These may not escape the enclosing generic construct + /// and we have to check various conditions associated with that. FreeTypars: FreeTypars } @@ -6218,36 +6224,36 @@ type FreeVarsCache = FreeVars cache /// Represents the set of free variables in an expression [] -type FreeVars = +type FreeVars = { - /// The summary of locally defined variables used in the expression. These may be hidden at let bindings etc. - /// or made private by a signature or marked 'internal' or 'private', and we have to check various conditions associated with that. + /// The summary of locally defined variables used in the expression. These may be hidden at let bindings etc. + /// or made private by a signature or marked 'internal' or 'private', and we have to check various conditions associated with that. FreeLocals: FreeLocals - - /// Indicates if the expression contains a call to a protected member or a base call. - /// Calls to protected members and direct calls to super classes can't escape, also code can't be inlined - UsesMethodLocalConstructs: bool - /// Indicates if the expression contains a call to rethrow that is not bound under a (try-)with branch. - /// Rethrow may only occur in such locations. - UsesUnboundRethrow: bool + /// Indicates if the expression contains a call to a protected member or a base call. + /// Calls to protected members and direct calls to super classes can't escape, also code can't be inlined + UsesMethodLocalConstructs: bool + + /// Indicates if the expression contains a call to rethrow that is not bound under a (try-)with branch. + /// Rethrow may only occur in such locations. + UsesUnboundRethrow: bool /// Indicates if the expression contains a direct IL field load/store — a cheap over-approximate /// gate the optimizer refines to protected (family) fields (issue #19963). Never read by escape checks. - ContainsILFieldAccess: bool + ContainsILFieldAccess: bool - /// The summary of locally defined tycon representations used in the expression. These may be made private by a signature - /// or marked 'internal' or 'private' and we have to check various conditions associated with that. - FreeLocalTyconReprs: FreeTycons + /// The summary of locally defined tycon representations used in the expression. These may be made private by a signature + /// or marked 'internal' or 'private' and we have to check various conditions associated with that. + FreeLocalTyconReprs: FreeTycons - /// The summary of fields used in the expression. These may be made private by a signature - /// or marked 'internal' or 'private' and we have to check various conditions associated with that. + /// The summary of fields used in the expression. These may be made private by a signature + /// or marked 'internal' or 'private' and we have to check various conditions associated with that. FreeRecdFields: FreeRecdFields - + /// The summary of union constructors used in the expression. These may be /// marked 'internal' or 'private' and we have to check various conditions associated with that. FreeUnionCases: FreeUnionCases - + /// See FreeTyvars above. FreeTyvars: FreeTyvars } @@ -6257,27 +6263,27 @@ type FreeVars = override x.ToString() = "FreeVars(...)" /// A set of static methods for constructing types. -type Construct() = +type Construct() = + + static let taccessPublic = TAccess [] - static let taccessPublic = TAccess [] - /// Key a Tycon or TyconRef by decoded name - static member KeyTyconByDecodedName<'T> (nm: string) (x: 'T) : KeyValuePair = + static member KeyTyconByDecodedName<'T> (nm: string) (x: 'T) : KeyValuePair = KeyValuePair(DecodeGenericTypeName nm, x) /// Key a Tycon or TyconRef by both mangled and demangled name. /// Generic types can be accessed either by 'List' or 'List`1'. /// This lists both keys. - static member KeyTyconByAccessNames<'T> (nm: string) (x: 'T) : KeyValuePair[] = + static member KeyTyconByAccessNames<'T> (nm: string) (x: 'T) : KeyValuePair[] = match TryDemangleGenericNameAndPos nm with | ValueSome pos -> - let dnm = DemangleGenericTypeNameWithPos pos nm + let dnm = DemangleGenericTypeNameWithPos pos nm [| KeyValuePair(nm, x); KeyValuePair(dnm, x) |] | _ -> [| KeyValuePair(nm, x) |] /// Create a new node for the contents of a module or namespace - static member NewModuleOrNamespaceType mkind tycons vals = + static member NewModuleOrNamespaceType mkind tycons vals = ModuleOrNamespaceType(mkind, QueueList.ofList vals, QueueList.ofList tycons) /// Create a new node for an empty module or namespace contents @@ -6287,37 +6293,37 @@ type Construct() = static member NewEmptyFSharpTyconData kind = { fsobjmodel_cases = Construct.MakeUnionCases [] - fsobjmodel_kind = kind + fsobjmodel_kind = kind fsobjmodel_vslots = [] fsobjmodel_rfields = Construct.MakeRecdFieldsTable [] } #if !NO_TYPEPROVIDERS /// Create a new node for the representation information for a provided type definition - static member NewProvidedTyconRepr(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m) = + static member NewProvidedTyconRepr(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m) = let isErased = st.PUntaint((fun st -> st.IsErased), m) - let lazyBaseTy = - LazyWithContext.Create - ((fun (m, objTy) -> + let lazyBaseTy = + LazyWithContext.Create + ((fun (m, objTy) -> let baseSystemTy = st.PApplyOption((fun st -> match st.BaseType with null -> None | ty -> Some ty), m) - match baseSystemTy with - | None -> objTy + match baseSystemTy with + | None -> objTy | Some t -> importProvidedType t), findOriginalException) - TProvidedTypeRepr + TProvidedTypeRepr { ResolutionEnvironment=resolutionEnvironment ProvidedType=st LazyBaseType=lazyBaseTy UnderlyingTypeOfEnum = (fun () -> importProvidedType (st.PApply((fun st -> st.GetEnumUnderlyingType()), m))) - IsDelegate = (fun () -> st.PUntaint((fun st -> - let baseType = st.BaseType - match baseType with + IsDelegate = (fun () -> st.PUntaint((fun st -> + let baseType = st.BaseType + match baseType with | Null -> false - | NonNull x -> - match x with + | NonNull x -> + match x with | x when x.IsGenericType -> false | x when x.DeclaringType <> null -> false | x -> x.FullName = "System.Delegate" || x.FullName = "System.MulticastDelegate"), m)) @@ -6331,8 +6337,8 @@ type Construct() = IsSuppressRelocate = isSuppressRelocate } /// Create a new entity node for a provided type definition - static member NewProvidedTycon(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m, ?access, ?cpath) = - let stamp = newStamp() + static member NewProvidedTycon(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m, ?access, ?cpath) = + let stamp = newStamp() let name = st.PUntaint((fun st -> st.Name), m) let kind = let isMeasure = @@ -6342,13 +6348,13 @@ type Construct() = .PUntaintNoFailure(Operators.id) if isMeasure then TyparKind.Measure else TyparKind.Type - let access = - match access with - | Some a -> a + let access = + match access with + | Some a -> a | None -> TAccess [] - let cpath = - match cpath with - | None -> + let cpath = + match cpath with + | None -> let ilScopeRef = st.TypeProviderAssemblyRef let enclosingName = GetFSharpPathToProvidedType(st, m) CompPath(ilScopeRef, SyntaxAccess.Unknown, enclosingName |> List.map(fun id->id, ModuleOrNamespaceKind.Namespace true)) @@ -6374,13 +6380,13 @@ type Construct() = | TyparKind.Type, TAccess [] -> None | _ -> Some { Entity.NewEmptyEntityOptData() with entity_kind = kind - entity_accessibility = access } } + entity_accessibility = access } } #endif /// Create a new entity node for a module or namespace - static member NewModuleOrNamespace cpath access (id: Ident) (xml: XmlDoc) attribs mtype = - let stamp = newStamp() - // Put the module suffix on if needed + static member NewModuleOrNamespace cpath access (id: Ident) (xml: XmlDoc) attribs mtype = + let stamp = newStamp() + // Put the module suffix on if needed Tycon.New "mspec" { entity_logical_name=id.idText entity_range = id.idRange @@ -6399,20 +6405,20 @@ type Construct() = | _ -> Some { Entity.NewEmptyEntityOptData() with entity_xmldoc = xml entity_tycon_repr_accessibility = access - entity_accessibility = access } } + entity_accessibility = access } } /// Create a new unfilled cache for free variable calculations static member NewFreeVarsCache() = newCache () /// Create the field tables for a record or class type - static member MakeRecdFieldsTable ucs: TyconRecdFields = - { FieldsByIndex = Array.ofList ucs + static member MakeRecdFieldsTable ucs: TyconRecdFields = + { FieldsByIndex = Array.ofList ucs FieldsByName = ucs |> NameMap.ofKeyedList (fun rfld -> rfld.LogicalName) } /// Create the union case tables for a union type - static member MakeUnionCases ucs: TyconUnionData = - { CasesTable = - { CasesByIndex = Array.ofList ucs + static member MakeUnionCases ucs: TyconUnionData = + { CasesTable = + { CasesByIndex = Array.ofList ucs CasesByName = NameMap.ofKeyedList (fun uc -> uc.LogicalName) ucs } CompiledRepresentation=newCache() } @@ -6428,11 +6434,11 @@ type Construct() = TFSharpTyconRepr repr /// Create a new type parameter node - static member NewTypar (kind, rigid, SynTypar(id, staticReq, isCompGen), isFromError, dynamicReq, attribs, eqDep, compDep) = + static member NewTypar (kind, rigid, SynTypar(id, staticReq, isCompGen), isFromError, dynamicReq, attribs, eqDep, compDep) = Typar.New - { typar_id = id - typar_stamp = newStamp() - typar_flags= TyparFlags(kind, rigid, isFromError, isCompGen, staticReq, dynamicReq, eqDep, compDep, false) + { typar_id = id + typar_stamp = newStamp() + typar_flags= TyparFlags(kind, rigid, isFromError, isCompGen, staticReq, dynamicReq, eqDep, compDep, false) typar_solution = None typar_astype = Unchecked.defaultof<_> typar_opt_data = @@ -6445,7 +6451,7 @@ type Construct() = Construct.NewTypar (TyparKind.Type, TyparRigidity.Rigid, SynTypar(mkSynId m nm, TyparStaticReq.None, true), false, TyparDynamicReq.Yes, [], false, false) /// Create a new union case node - static member NewUnionCase id tys retTy attribs docOption access: UnionCase = + static member NewUnionCase id tys retTy attribs docOption access: UnionCase = { Id = id OwnXmlDoc = docOption OtherXmlDoc = XmlDoc.Empty @@ -6453,11 +6459,11 @@ type Construct() = Accessibility = access FieldTable = Construct.MakeRecdFieldsTable tys ReturnType = retTy - Attribs = attribs - OtherRangeOpt = None } + Attribs = attribs + OtherRangeOpt = None } /// Create a new TAST Entity node for an F# exception definition - static member NewExn cpath (id: Ident) access repr attribs (doc: XmlDoc) = + static member NewExn cpath (id: Ident) access repr attribs (doc: XmlDoc) = Tycon.New "exnc" { entity_stamp = newStamp() entity_attribs = WellKnownEntityAttribs.Create(attribs) @@ -6473,7 +6479,7 @@ type Construct() = entity_opt_data = match doc, access, repr with | doc, TAccess [], TExnNone when doc.IsEmpty -> None - | _ -> Some { Entity.NewEmptyEntityOptData() with entity_xmldoc = doc; entity_accessibility = access; entity_tycon_repr_accessibility = access; entity_exn_info = repr } } + | _ -> Some { Entity.NewEmptyEntityOptData() with entity_xmldoc = doc; entity_accessibility = access; entity_tycon_repr_accessibility = access; entity_exn_info = repr } } /// Create a new TAST RecdField node for an F# class, struct or record field static member NewRecdField stat konst id nameGenerated ty isMutable isVolatile pattribs fattribs docOption access secret = @@ -6492,10 +6498,10 @@ type Construct() = rfield_id = id rfield_name_generated = nameGenerated rfield_other_range = None } - + /// Create a new type definition node static member NewTycon (cpath, nm, m, access, reprAccess, kind, typars, doc: XmlDoc, usesPrefixDisplay, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, mtyp) = - let stamp = newStamp() + let stamp = newStamp() Tycon.New "tycon" { entity_stamp=stamp entity_logical_name=nm @@ -6511,7 +6517,7 @@ type Construct() = entity_opt_data = match kind, doc, reprAccess, access with | TyparKind.Type, doc, TAccess [], TAccess [] when doc.IsEmpty -> None - | _ -> Some { Entity.NewEmptyEntityOptData() with entity_kind = kind; entity_xmldoc = doc; entity_tycon_repr_accessibility = reprAccess; entity_accessibility=access } } + | _ -> Some { Entity.NewEmptyEntityOptData() with entity_kind = kind; entity_xmldoc = doc; entity_tycon_repr_accessibility = reprAccess; entity_accessibility=access } } /// Create a new type definition node for a .NET type definition static member NewILTycon nlpath (nm, m) tps (scoref: ILScopeRef, enc, tdef: ILTypeDef) mtyp = @@ -6547,10 +6553,10 @@ type Construct() = actualParent) : Val = let stamp = newStamp() - let optData = + let optData = match compiledName, arity, konst, access, doc, specialRepr, actualParent, attribs with | None, None, None, TAccess [], doc, None, ParentNone, [] when doc.IsEmpty -> None - | _ -> + | _ -> { Val.NewEmptyValOptData() with val_compiled_name = (match compiledName with Some v when v <> logicalName -> compiledName | _ -> None) val_repr_info = arity @@ -6577,26 +6583,28 @@ type Construct() = static member NewCcuContents sref m nm mty = Construct.NewModuleOrNamespace (Some(CompPath(sref, SyntaxAccess.Unknown, []))) taccessPublic (ident(nm, m)) XmlDoc.Empty [] (MaybeLazy.Strict mty) - /// Create a tycon based on an existing one using the function 'f'. - /// We require that we be given the new parent for the new tycon. - /// We pass the new tycon to 'f' in case it needs to reparent the - /// contents of the tycon. - static member NewModifiedTycon f (orig: Tycon) = + /// Create a tycon based on an existing one using the function 'f'. + /// We require that we be given the new parent for the new tycon. + /// We pass the new tycon to 'f' in case it needs to reparent the + /// contents of the tycon. + static member NewModifiedTycon f (orig: Tycon) = let data = { orig with entity_stamp = newStamp() } - Tycon.New "NewModifiedTycon" (f data) - - /// Create a module Tycon based on an existing one using the function 'f'. - /// We require that we be given the parent for the new module. - /// We pass the new module to 'f' in case it needs to reparent the - /// contents of the module. - static member NewModifiedModuleOrNamespace f orig = - orig |> Construct.NewModifiedTycon (fun d -> - { d with entity_modul_type = MaybeLazy.Strict (f (d.entity_modul_type.Force())) }) - - /// Create a Val based on an existing one using the function 'f'. - /// We require that we be given the parent for the new Val. - static member NewModifiedVal f (orig: Val) = - let stamp = newStamp() + Tycon.New "NewModifiedTycon" (f data) + + /// Create a module Tycon based on an existing one using the function 'f'. + /// We require that we be given the parent for the new module. + /// We pass the new module to 'f' in case it needs to reparent the + /// contents of the module. + static member NewModifiedModuleOrNamespace f orig = + orig |> Construct.NewModifiedTycon (fun d -> + match d.entity_modul_type with + | null -> d + | entity_modul_type -> { d with entity_modul_type = MaybeLazy.Strict (f (entity_modul_type.Force())) }) + + /// Create a Val based on an existing one using the function 'f'. + /// We require that we be given the parent for the new Val. + static member NewModifiedVal f (orig: Val) = + let stamp = newStamp() let data' = f { orig with val_stamp=stamp } Val.New data' @@ -6614,9 +6622,9 @@ type Construct() = let attrs = p.PUntaintNoFailure(fun x -> x.GetDefinitionLocationAttribute(p.TypeProvider.PUntaintNoFailure id)) match attrs with | None | Some (Null, _, _) -> None - | Some (NonNull filePath, line, column) -> + | Some (NonNull filePath, line, column) -> // Coordinates from type provider are 1-based for lines and columns // Coordinates internally in the F# compiler are 1-based for lines and 0-based for columns - let pos = Position.mkPos line (max 0 (column - 1)) + let pos = Position.mkPos line (max 0 (column - 1)) mkRange !!filePath pos pos |> Some #endif diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index d706237e468..92e12c1cb1e 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -453,10 +453,10 @@ type Entity = mutable entity_tycon_repr: TyconRepresentation /// The methods type properties of the type - mutable entity_tycon_tcaug: TyconAugmentation + mutable entity_tycon_tcaug: TyconAugmentation | null /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions type nested modules - mutable entity_modul_type: MaybeLazy + mutable entity_modul_type: MaybeLazy | null /// The stable path to the type, e.g. Microsoft.FSharp.Core.FSharpFunc`2 mutable entity_cpath: CompilationPath option diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs index f8564249d03..8b118c70f64 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs @@ -2177,25 +2177,28 @@ module internal ExprRemapping = | TAsmRepr _ -> repr | TMeasureableRepr x -> TMeasureableRepr(remapType tmenv x) - and remapTyconAug tmenv (x: TyconAugmentation) = - { x with - tcaug_equals = x.tcaug_equals |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) - tcaug_compare = x.tcaug_compare |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) - tcaug_compare_withc = x.tcaug_compare_withc |> Option.map (remapValRef tmenv) - tcaug_hash_and_equals_withc = - x.tcaug_hash_and_equals_withc - |> Option.map (mapQuadruple (remapValRef tmenv, remapValRef tmenv, remapValRef tmenv, Option.map (remapValRef tmenv))) - tcaug_adhoc = x.tcaug_adhoc |> NameMap.map (List.map (remapValRef tmenv)) - tcaug_adhoc_list = - let remapped: ResizeArray | null = - match x.tcaug_adhoc_list with - | null -> null - | l -> l |> ResizeArray.map (fun (flag, vref) -> (flag, remapValRef tmenv vref)) - - remapped - tcaug_super = x.tcaug_super |> Option.map (remapType tmenv) - tcaug_interfaces = x.tcaug_interfaces |> List.map (map1Of3 (remapType tmenv)) - } + and remapTyconAug tmenv (x: TyconAugmentation | null) = + match x with + | null -> null + | x -> + { x with + tcaug_equals = x.tcaug_equals |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) + tcaug_compare = x.tcaug_compare |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) + tcaug_compare_withc = x.tcaug_compare_withc |> Option.map (remapValRef tmenv) + tcaug_hash_and_equals_withc = + x.tcaug_hash_and_equals_withc + |> Option.map (mapQuadruple (remapValRef tmenv, remapValRef tmenv, remapValRef tmenv, Option.map (remapValRef tmenv))) + tcaug_adhoc = x.tcaug_adhoc |> NameMap.map (List.map (remapValRef tmenv)) + tcaug_adhoc_list = + let remapped: ResizeArray | null = + match x.tcaug_adhoc_list with + | null -> null + | l -> l |> ResizeArray.map (fun (flag, vref) -> (flag, remapValRef tmenv vref)) + + remapped + tcaug_super = x.tcaug_super |> Option.map (remapType tmenv) + tcaug_interfaces = x.tcaug_interfaces |> List.map (map1Of3 (remapType tmenv)) + } and remapTyconExnInfo ctxt tmenv inp = match inp with @@ -2300,7 +2303,10 @@ module internal ExprRemapping = tcdR.entity_tycon_repr <- tcd.entity_tycon_repr |> remapTyconRepr ctxt tmenvinner2 let typeAbbrevR = tcd.TypeAbbrev |> Option.map (remapType tmenvinner2) tcdR.entity_tycon_tcaug <- tcd.entity_tycon_tcaug |> remapTyconAug tmenvinner2 - tcdR.entity_modul_type <- MaybeLazy.Strict(tcd.entity_modul_type.Value |> mapImmediateValsAndTycons lookupTycon lookupVal) + tcdR.entity_modul_type <- + match tcd.entity_modul_type with + | null -> null + | ty -> MaybeLazy.Strict(ty.Force() |> mapImmediateValsAndTycons lookupTycon lookupVal) let exnInfoR = tcd.ExceptionInfo |> remapTyconExnInfo ctxt tmenvinner2 match tcdR.entity_opt_data with diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi index 5372d2a2511..1e4da87eedc 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi @@ -253,7 +253,7 @@ module internal ExprRemapping = val remapTyconRepr: RemapContext -> Remap -> TyconRepresentation -> TyconRepresentation - val remapTyconAug: Remap -> TyconAugmentation -> TyconAugmentation + val remapTyconAug: Remap -> TyconAugmentation | null -> TyconAugmentation | null val remapTyconExnInfo: RemapContext -> Remap -> ExceptionInfo -> ExceptionInfo diff --git a/src/Compiler/Utilities/lib.fs b/src/Compiler/Utilities/lib.fs index a1395e5589e..7a53f1993e7 100755 --- a/src/Compiler/Utilities/lib.fs +++ b/src/Compiler/Utilities/lib.fs @@ -419,7 +419,7 @@ type DisposablesTracker() = let items = Stack() /// Register some items to dispose - member _.Register (i:#IDisposable | null) = + member _.Register (i:#IDisposable | null) = match box i with | null -> () | _ -> items.Push (!!i) @@ -498,6 +498,10 @@ module WeakMap = | true, value -> value | false, _ -> let value = valueFactory key - if shouldCache value then + if shouldCache value then +#if NETSTANDARD2_0 try table.Add(key, value) with _ -> () +#else + table.TryAdd(key, value) |> ignore +#endif value From 3aac5d05abd2d67fcc371790769d67175e8999d6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 20 Aug 2026 00:05:30 +0200 Subject: [PATCH 005/133] Fix accessing `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug` * Used safe `ModuleOrNamespaceType` property instead of `entity_modul_type`. * Used safe `TypeContents` property instead of `entity_tycon_tcaug`. * Optimized `TextViewEventsHandler` memory usage with `voption` --- .../TypedTree/TypedTreeOps.Transforms.fs | 2 +- src/Compiler/TypedTree/TypedTreePickle.fs | 15 +++--- .../src/FSharp.Editor/Common/Extensions.fs | 46 +++++++++---------- .../FSharpProjectOptionsManager.fs | 37 ++++++++------- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs b/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs index 7ee5f290b23..6b6495d3219 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs @@ -987,7 +987,7 @@ module internal Rewriting = let modulContentsR = MaybeLazy.Strict( - d.entity_modul_type.Value + d.ModuleOrNamespaceType |> mapImmediateValsAndTycons (remapTyconToNonLocal ctxt tmenv) (remapValToNonLocal ctxt tmenv) ) diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index 9d599189afe..f1f2f0f7927 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -733,8 +733,6 @@ let private p_lazy_impl p v st = let p_lazy p x st = p_lazy_impl p (InterruptibleLazy.force x) st -let p_maybe_lazy p (x: MaybeLazy<_>) st = p_lazy_impl p x.Value st - let p_hole () = let mutable h = None @@ -2723,7 +2721,7 @@ let rec p_tycon_repr x st = let allFieldsText = fields - |> Array.map (fun f -> f.LogicalName) + |> Seq.map _.LogicalName |> String.concat System.Environment.NewLine raise (Error(FSComp.SR.pickleFsharpCoreBackwardsCompatible ("fields in union", allFieldsText), firstFieldRange)) @@ -2840,7 +2838,7 @@ and p_entity_spec_data (x: Entity) st = p_attribs (x.entity_attribs.AsList()) st let flagBit = p_tycon_repr x.entity_tycon_repr st p_option p_ty x.TypeAbbrev st - p_tcaug x.entity_tycon_tcaug st + p_tcaug x.TypeContents st p_string System.String.Empty st p_kind x.TypeOrMeasureKind st @@ -2853,7 +2851,7 @@ and p_entity_spec_data (x: Entity) st = st p_option p_cpath x.entity_cpath st - p_maybe_lazy p_modul_typ x.entity_modul_type st + p_lazy_impl p_modul_typ x.ModuleOrNamespaceType st p_exnc_repr x.ExceptionInfo st if st.oInMem then @@ -2861,7 +2859,7 @@ and p_entity_spec_data (x: Entity) st = else p_space 1 () st -and p_tcaug p st = +and p_tcaug (p : TyconAugmentation) st = p_tup9 (p_option (p_tup2 (p_vref "compare_obj") (p_vref "compare"))) (p_option (p_vref "compare_withc")) @@ -2882,8 +2880,9 @@ and p_tcaug p st = // in order to get check the well-formedness of an interface. // Keeping them across assembly boundaries is not valid, because relinking their ValRefs // does not work correctly (they may get incorrectly relinked to a default member) - |> List.filter (fun (isExplicitImpl, _) -> not isExplicitImpl) - |> List.map (fun (_, vref) -> vref.LogicalName, vref)), + |> Seq.filter (fun (isExplicitImpl, _) -> not isExplicitImpl) + |> Seq.map (fun (_, vref) -> vref.LogicalName, vref) + |> Seq.toList), p.tcaug_interfaces, p.tcaug_super, p.tcaug_abstract, diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 82d161a7e64..75e26983f9e 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -71,14 +71,14 @@ type Project with type TextViewEventsHandler ( - onChangeCaretHandler: (IVsTextView * int * int -> unit) option, - onKillFocus: (IVsTextView -> unit) option, - onSetFocus: (IVsTextView -> unit) option + onChangeCaretHandler: (IVsTextView * int * int -> unit) voption, + onKillFocus: (IVsTextView -> unit) voption, + onSetFocus: (IVsTextView -> unit) voption ) = interface IVsTextViewEvents with member this.OnChangeCaretLine(view: IVsTextView, newline: int, oldline: int) = onChangeCaretHandler - |> Option.iter (fun handler -> handler (view, newline, oldline)) + |> ValueOption.iter (fun handler -> handler (view, newline, oldline)) member this.OnChangeScrollInfo (_view: IVsTextView, _iBar: int, _iMinUnit: int, _iMaxUnits: int, _iVisibleUnits: int, _iFirstVisibleUnit: int) @@ -86,14 +86,14 @@ type TextViewEventsHandler () member this.OnKillFocus(view: IVsTextView) = - onKillFocus |> Option.iter (fun handler -> handler (view)) + onKillFocus |> ValueOption.iter (fun handler -> handler (view)) member this.OnSetBuffer(_view: IVsTextView, _buffer: IVsTextLines) = () member this.OnSetFocus(view: IVsTextView) = - onSetFocus |> Option.iter (fun handler -> handler (view)) + onSetFocus |> ValueOption.iter (fun handler -> handler (view)) -type ConnectionPointSubscription = System.IDisposable option +type ConnectionPointSubscription = System.IDisposable voption // Usage example: // If a handler is None, to not handle that event @@ -108,16 +108,16 @@ let subscribeToTextViewEvents (textView: IVsTextView, onChangeCaretHandler, onKi let mutable cookie = 0u match cpContainer.FindConnectionPoint(ref riid) with - | null -> None + | null -> ValueNone | cp -> - Some( + ValueSome( cp.Advise(handler, &cookie) { new IDisposable with member _.Dispose() = cp.Unadvise(cookie) } ) - | _ -> None + | _ -> ValueNone type Document with @@ -129,7 +129,7 @@ type Document with | null -> None | languageServices -> languageServices.GetService<'T>() |> Some - member this.TryGetIVsTextView() : IVsTextView option = + member this.TryGetIVsTextView() : IVsTextView voption = match ServiceProvider.GlobalProvider.GetService(typeof) with | :? IVsTextManager as textManager -> // Grab IVsRunningDocumentTable @@ -140,20 +140,20 @@ type Document with match Marshal.GetObjectForIUnknown docData with | :? IVsTextBuffer as ivsTextBuffer -> match textManager.GetActiveView(0, ivsTextBuffer) with - | hr, vsTextView when ErrorHandler.Succeeded(hr) -> Some vsTextView - | _ -> None - | _ -> None - | _ -> None - | _ -> None - | _ -> None - - member this.TryGetTextViewAndCaretPos() : (IVsTextView * Position) option = + | hr, vsTextView when ErrorHandler.Succeeded(hr) -> ValueSome vsTextView + | _ -> ValueNone + | _ -> ValueNone + | _ -> ValueNone + | _ -> ValueNone + | _ -> ValueNone + + member this.TryGetTextViewAndCaretPos() : (IVsTextView * Position) voption = match this.TryGetIVsTextView() with - | Some textView -> + | ValueSome textView -> match textView.GetCaretPos() with - | hr, line, column when ErrorHandler.Succeeded(hr) -> Some(textView, Position.fromZ line column) - | _ -> None - | None -> None + | hr, line, column when ErrorHandler.Succeeded(hr) -> ValueSome(textView, Position.fromZ line column) + | _ -> ValueNone + | ValueNone -> ValueNone member this.IsFSharpScript = isScriptFile this.FilePath diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 08bfbbddaa8..fd821f7c361 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -200,7 +200,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = cancellableTask { let! ct = CancellableTask.getCancellationToken () let! fileStamp = document.GetTextVersionAsync(ct) - let textViewAndCaret () : (IVsTextView * Position) option = document.TryGetTextViewAndCaretPos() + let textViewAndCaret () : (IVsTextView * Position) voption = document.TryGetTextViewAndCaretPos() match singleFileCache.TryGetValue(document.Id) with | false, _ -> @@ -210,7 +210,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let caret = textViewAndCaret () match caret with - | None -> + | ValueNone -> checker.GetProjectOptionsFromScript( document.FilePath, sourceText.ToFSharpSourceText(), @@ -219,7 +219,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = userOpName = userOpName ) - | Some(_, caret) -> + | ValueSome(_, caret) -> checker.GetProjectOptionsFromScript( document.FilePath, sourceText.ToFSharpSourceText(), @@ -234,14 +234,12 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let otherOptions = if project.IsFSharpMetadata then - project.ProjectReferences - |> Seq.map (fun x -> "-r:" + project.Solution.GetProject(x.ProjectId).OutputFilePath) - |> Array.ofSeq - |> Array.append ( - project.MetadataReferences.OfType() - |> Seq.map (fun x -> "-r:" + x.FilePath) - |> Array.ofSeq - ) + [| + for x in project.ProjectReferences do + yield "-r:" + project.Solution.GetProject(x.ProjectId).OutputFilePath + for x in project.MetadataReferences.OfType() do + yield "-r:" + x.FilePath + |] else [||] @@ -284,9 +282,14 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | projectId, fileStamp, parsingOptions, projectOptions, _ -> let subscription = match textViewAndCaret () with - | Some(textView, _) -> - subscribeToTextViewEvents (textView, (Some onChangeCaretHandler), (Some onKillFocus), (Some onSetFocus)) - | None -> None + | ValueSome(textView, _) -> + subscribeToTextViewEvents ( + textView, + (ValueSome onChangeCaretHandler), + (ValueSome onKillFocus), + (ValueSome onSetFocus) + ) + | ValueNone -> ValueNone (projectId, fileStamp, parsingOptions, projectOptions, subscription) @@ -294,7 +297,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = document.Id, // The key to the cache (fun _ value -> addToCacheAndSubscribe value), // Function to add the cached value if the key does not exist (fun _ _ value -> value), // Function to update the value if the key exists - (document.Project, fileStamp, parsingOptions, projectOptions, None) // The value to add or update + (document.Project, fileStamp, parsingOptions, projectOptions, ValueNone) // The value to add or update ) |> ignore @@ -303,7 +306,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | true, (oldProject, oldFileStamp, parsingOptions, projectOptions, _) -> if fileStamp <> oldFileStamp || isProjectInvalidated document.Project oldProject ct then match singleFileCache.TryRemove(document.Id) with - | true, (_, _, _, _, Some subscription) -> subscription.Dispose() + | true, (_, _, _, _, ValueSome subscription) -> subscription.Dispose() | _ -> () return! tryComputeOptionsBySingleScriptOrFile document userOpName @@ -519,7 +522,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | true, (_, _, _, projectOptions, subscription) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore checker.ClearCache([ projectOptions ]) - subscription |> Option.iter (fun handler -> handler.Dispose()) + subscription |> ValueOption.iter (fun handler -> handler.Dispose()) | _ -> () } From 816acb0dcf3473e22c49f761dec67943caa0744e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 20 Aug 2026 02:37:14 +0200 Subject: [PATCH 006/133] Fix comment #20274#discussion_r3815216161 Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`. --- src/Compiler/TypedTree/TypedTree.fs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index b1a739752fe..7949ac042be 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -890,14 +890,18 @@ type Entity = /// The logical contents of the entity when it is a module or namespace fragment. member x.ModuleOrNamespaceType = match x.entity_modul_type with - | null -> Construct.NewEmptyModuleOrNamespaceType ModuleOrType - | entity_modul_type -> entity_modul_type.Force() + | null -> x.entity_modul_type <- MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType ModuleOrType) + | _ -> () + x.entity_modul_type + |> Unchecked.nonNull + |> _.Force() /// The logical contents of the entity when it is a type definition. member x.TypeContents = match x.entity_tycon_tcaug with - | null -> TyconAugmentation.Create() - | entity_tycon_tcaug -> entity_tycon_tcaug + | null -> x.entity_tycon_tcaug <- TyconAugmentation.Create() + | _ -> () + !!x.entity_tycon_tcaug /// The kind of the type definition - is it a measure definition or a type definition? member x.TypeOrMeasureKind = @@ -1555,7 +1559,7 @@ type TyconAugmentation = member tcaug.SetHasObjectGetHashCode b = tcaug.tcaug_hasObjectGetHashCode <- b - static member Create() = + static member Create() : TyconAugmentation = { tcaug_compare=None tcaug_compare_withc=None tcaug_equals=None @@ -6370,7 +6374,7 @@ type Construct() = entity_attribs=WellKnownEntityAttribs.Empty // fetched on demand via est.fs API entity_typars= LazyWithContext.NotLazy [] entity_tycon_repr = repr - entity_tycon_tcaug=TyconAugmentation.Create() + entity_tycon_tcaug= TyconAugmentation.Create() entity_modul_type = MaybeLazy.Lazy(InterruptibleLazy(fun _ -> ModuleOrNamespaceType(Namespace true, QueueList.ofList [], QueueList.ofList []))) // Generated types get internal accessibility entity_cpath = Some cpath @@ -6510,7 +6514,7 @@ type Construct() = entity_attribs=WellKnownEntityAttribs.Empty // fixed up after entity_typars=typars entity_tycon_repr = TNoRepr - entity_tycon_tcaug=TyconAugmentation.Create() + entity_tycon_tcaug= TyconAugmentation.Create() entity_modul_type = mtyp entity_cpath = cpath entity_il_repr_cache = null From ad16668469e2c98d441c3ce747cfe78d9e54a6fb Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 03:22:25 +0200 Subject: [PATCH 007/133] Reduce allocations in Md5Hasher and fix broken ThreadLocal reuse Fixes dotnet/fsharp#20204 --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Facilities/Hashing.fs | 46 +++++++++++++++---- src/Compiler/Facilities/Hashing.fsi | 10 +++- src/Compiler/Facilities/prim-lexing.fs | 15 +++--- src/Compiler/Service/FSharpProjectSnapshot.fs | 6 +-- 5 files changed, 55 insertions(+), 23 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 18ceff70d83..f71c9dc4296 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -205,6 +205,7 @@ * Lower string-typed interpolated strings to `System.String.Concat` rather than the reflection-based `printf` engine, making them trim- and NativeAOT-compatible. This generalizes and ungates the previous all-string `String.Concat` optimization, so it now applies to every string-typed interpolation. ([Language suggestion #1108](https://github.com/fsharp/fslang-suggestions/issues/1108), [PR #19971](https://github.com/dotnet/fsharp/pull/19971)) * Stabilized several `preview` language features into F# 11.0 (`--langversion:11.0`, enabled by default with a .NET 11 SDK): `MethodOverloadsCache`, `ErrorOnMissingSignatureAttribute`, `DirectDelegateConstruction`, `AccessProtectedBaseFieldFromClosure`, and `RecordSpreads`. `FromEndSlicing` intentionally remains in `preview`. ([PR #20199](https://github.com/dotnet/fsharp/pull/20199)) * Interpolated string holes (e.g. `$"{x}"`) are now formatted with invariant culture (via the `string` operator) instead of the current thread culture. ([PR #19971](https://github.com/dotnet/fsharp/pull/19971)) +* `Md5Hasher` now reuses the thread-local `MD5` instance instead of allocating a new one per call, and hashes strings without allocating an intermediate UTF8-encoded byte array by renting a buffer from the shared `ArrayPool`. ([Issue #20204](https://github.com/dotnet/fsharp/issues/20204), [PR #20275](https://github.com/dotnet/fsharp/pull/20275)) * Lines starting with `#:` are now ignored ([Language suggestion 1440](https://github.com/fsharp/fslang-suggestions/issues/1440), [RFC FS-1337](https://github.com/fsharp/fslang-design/pull/830), [PR #20212](https://github.com/dotnet/fsharp/pull/20212)) * `[]` attributes written in front of a binding are again reported by `SynBinding.attributes` in the untyped syntax tree, with their original grouping and `[< >]` ranges. The rotation into `SynValInfo.retInfo` added by [PR #19738](https://github.com/dotnet/fsharp/pull/19738) now happens while normalizing a binding for checking instead of in the parser, so both fixes from that PR are unchanged while tools reading the parse tree (formatters, analyzers, source generators) again see what was written. ([PR #20356](https://github.com/dotnet/fsharp/pull/20356)) * Calculate Entity.PublicPath instead of storing ([PR #20285](https://github.com/dotnet/fsharp/pull/20285)) diff --git a/src/Compiler/Facilities/Hashing.fs b/src/Compiler/Facilities/Hashing.fs index d67f879ef14..90de010c67d 100644 --- a/src/Compiler/Facilities/Hashing.fs +++ b/src/Compiler/Facilities/Hashing.fs @@ -1,6 +1,7 @@ namespace Internal.Utilities.Hashing open System +open System.Security.Cryptography open System.Threading /// Tools for hashing things with MD5 into a string that can be used as a cache key. @@ -43,19 +44,46 @@ module internal Md5StringHasher = module internal Md5Hasher = - let private md5 = - new ThreadLocal<_>(fun () -> System.Security.Cryptography.MD5.Create()) +#if NETSTANDARD2_0 + let private md5 = new ThreadLocal<_>(fun () -> MD5.Create()) - let computeHash (bytes: byte array) = - // md5.Value.ComputeHash(bytes) TODO: the threadlocal is not working in new VS extension - ignore md5 - let md5 = System.Security.Cryptography.MD5.Create() - md5.ComputeHash(bytes) + let computeHash (bytes: byte array) = md5.Value.ComputeHash(bytes) +#else + let computeHash (bytes: byte array) = MD5.HashData(bytes) +#endif let empty = Array.empty - let hashString (s: string) = - s |> System.Text.Encoding.UTF8.GetBytes |> computeHash + /// Computes the MD5 hash of a string directly into a caller-allocated 16-byte buffer, + /// avoiding the extra allocation of an intermediate hash-result array. + /// The UTF8 encoding buffer is rented from the shared ArrayPool to avoid allocating + /// a byte array the size of the input string on every call. + let hashStringInto (s: string) (destination: Span) = + let encoding = System.Text.Encoding.UTF8 + let maxByteCount = encoding.GetMaxByteCount(s.Length) + let rented = System.Buffers.ArrayPool.Shared.Rent(maxByteCount) + + try + let byteCount = encoding.GetBytes(s, 0, s.Length, rented, 0) +#if NETSTANDARD2_0 + let hash = md5.Value.ComputeHash(rented, 0, byteCount) + hash.CopyTo(destination) +#else + let mutable bytesWritten = 0 + + MD5.TryHashData(ReadOnlySpan(rented, 0, byteCount), destination, &bytesWritten) + |> ignore +#endif + finally + System.Buffers.ArrayPool.Shared.Return(rented) + + /// Computes the MD5 hash of a string and returns it as a hex string (matching the format of + /// `toString`), without allocating an intermediate byte array for the UTF8-encoded input + /// (only the 16-byte hash result is allocated). + let hashStringToString (s: string) = + let bytes = Array.zeroCreate 16 + hashStringInto s (Span bytes) + BitConverter.ToString(bytes) let addBytes (bytes: byte array) (s: byte array) = diff --git a/src/Compiler/Facilities/Hashing.fsi b/src/Compiler/Facilities/Hashing.fsi index c154fd66078..0f4bafbb867 100644 --- a/src/Compiler/Facilities/Hashing.fsi +++ b/src/Compiler/Facilities/Hashing.fsi @@ -1,5 +1,7 @@ namespace Internal.Utilities.Hashing +open System + /// Tools for hashing things with MD5 into a string that can be used as a cache key. module internal Md5StringHasher = @@ -25,7 +27,13 @@ module internal Md5Hasher = val empty: 'a array - val hashString: s: string -> byte array + /// Computes the MD5 hash of a string directly into a caller-allocated 16-byte buffer, + /// avoiding the extra allocation of an intermediate hash-result array. + val hashStringInto: s: string -> destination: Span -> unit + + /// Computes the MD5 hash of a string and returns it as a hex string, without allocating + /// an intermediate byte array for the UTF8-encoded input (only the 16-byte hash result is allocated). + val hashStringToString: s: string -> string val addBytes: bytes: byte array -> s: byte array -> byte array diff --git a/src/Compiler/Facilities/prim-lexing.fs b/src/Compiler/Facilities/prim-lexing.fs index 21b93b12880..f6f4967ecef 100644 --- a/src/Compiler/Facilities/prim-lexing.fs +++ b/src/Compiler/Facilities/prim-lexing.fs @@ -7,8 +7,8 @@ namespace FSharp.Compiler.Text open System open System.IO open System.Collections.Immutable +open System.Runtime.InteropServices open Internal.Utilities.Library - open Internal.Utilities.Hashing type ISourceText = @@ -154,9 +154,9 @@ type StringText(str: string) = sb.Append(lastLine.Substring(0, range.EndColumn)).ToString() member _.GetChecksum() = - str - |> Md5Hasher.hashString - |> fun byteArray -> ImmutableArray.Create(byteArray, 0, byteArray.Length) + let bytes = Array.zeroCreate 16 + Md5Hasher.hashStringInto str (Span bytes) + ImmutableCollectionsMarshal.AsImmutableArray(bytes) module SourceText = @@ -190,10 +190,9 @@ module SourceTextNew = member _.GetSubTextFromRange(x) = sourceText.GetSubTextFromRange(x) member _.GetChecksum() = - // TODO: something better... - !!sourceText.ToString() - |> Md5Hasher.hashString - |> fun byteArray -> ImmutableArray.Create(byteArray, 0, byteArray.Length) + let bytes = Array.zeroCreate 16 + Md5Hasher.hashStringInto (!!sourceText.ToString()) (Span bytes) + ImmutableCollectionsMarshal.AsImmutableArray(bytes) } // NOTE: the code in this file is a drop-in replacement runtime for Lexing.fs from the FsLexYacc repository diff --git a/src/Compiler/Service/FSharpProjectSnapshot.fs b/src/Compiler/Service/FSharpProjectSnapshot.fs index 02c368e7f46..71354fe3adb 100644 --- a/src/Compiler/Service/FSharpProjectSnapshot.fs +++ b/src/Compiler/Service/FSharpProjectSnapshot.fs @@ -72,11 +72,7 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta FSharpFileSnapshot(fileName, version, getSource) static member CreateFromString(filename: string, content: string) = - FSharpFileSnapshot( - filename, - Md5Hasher.hashString content |> Md5Hasher.toString, - fun () -> Task.FromResult(SourceTextNew.ofString content) - ) + FSharpFileSnapshot(filename, Md5Hasher.hashStringToString content, fun () -> Task.FromResult(SourceTextNew.ofString content)) static member CreateFromFileSystem(fileName: string) = FSharpFileSnapshot( From a2f8b916169b862184df5f928bc4ad90b89bb60c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 3 Aug 2026 01:20:17 +0200 Subject: [PATCH 008/133] Add version-stamp cache to FSharpDocumentDiagnosticAnalyzer to avoid recomputing diagnostics when document/project version is unchanged --- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index a1a2bbe9f90..3cb80f18e26 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -1,8 +1,9 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition +open System.Collections.Concurrent open System.Collections.Immutable open System.Collections.Generic open System.Threading @@ -27,6 +28,9 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = let shouldProduceDiagnostics (document: Document) = document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics() + static let cache = + ConcurrentDictionary>() + static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -72,6 +76,27 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = let! ct = CancellableTask.getCancellationToken () + let! textVersion = document.GetTextVersionAsync(ct) + + let! projectVersion = + match diagnosticType with + | DiagnosticsType.Syntax -> CancellableTask.singleton VersionStamp.Default + | DiagnosticsType.Semantic -> (fun ct -> document.Project.GetDependentVersionAsync(ct)) + + let cacheKey = struct (document.Id, diagnosticType) + + let cached = + match cache.TryGetValue(cacheKey) with + | true, (cachedTextVersion, cachedProjectVersion, cachedDiagnostics) when + cachedTextVersion = textVersion && cachedProjectVersion = projectVersion + -> + ValueSome cachedDiagnostics + | _ -> ValueNone + + match cached with + | ValueSome cachedDiagnostics -> return cachedDiagnostics + | ValueNone -> + let! sourceText = document.GetTextAsync(ct) let filePath = document.FilePath @@ -98,35 +123,39 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document | _ -> CancellableTask.singleton ImmutableArray.Empty - if errors.Count = 0 && unnecessaryParentheses.IsEmpty then - return ImmutableArray.Empty - else - let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length) + let result = + if errors.Count = 0 && unnecessaryParentheses.IsEmpty then + ImmutableArray.Empty + else + let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length) + + for diagnostic in errors do + if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then + let linePositionSpan = + LinePositionSpan( + LinePosition(diagnostic.StartLine - 1, diagnostic.StartColumn), + LinePosition(diagnostic.EndLine - 1, diagnostic.EndColumn) + ) - for diagnostic in errors do - if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then - let linePositionSpan = - LinePositionSpan( - LinePosition(diagnostic.StartLine - 1, diagnostic.StartColumn), - LinePosition(diagnostic.EndLine - 1, diagnostic.EndColumn) - ) + let textSpan = sourceText.Lines.GetTextSpan(linePositionSpan) - let textSpan = sourceText.Lines.GetTextSpan(linePositionSpan) + // F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries + let correctedTextSpan = + if textSpan.End <= sourceText.Length then + textSpan + else + let start = min textSpan.Start (sourceText.Length - 1) |> max 0 - // F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries - let correctedTextSpan = - if textSpan.End <= sourceText.Length then - textSpan - else - let start = min textSpan.Start (sourceText.Length - 1) |> max 0 + TextSpan.FromBounds(start, sourceText.Length) - TextSpan.FromBounds(start, sourceText.Length) + let location = Location.Create(filePath, correctedTextSpan, linePositionSpan) + iab.Add(RoslynHelpers.ConvertError(diagnostic, location)) - let location = Location.Create(filePath, correctedTextSpan, linePositionSpan) - iab.Add(RoslynHelpers.ConvertError(diagnostic, location)) + iab.AddRange unnecessaryParentheses + iab.ToImmutable() - iab.AddRange unnecessaryParentheses - return iab.ToImmutable() + cache.[cacheKey] <- (textVersion, projectVersion, result) + return result } interface IFSharpDocumentDiagnosticAnalyzer with From 05deac200ecf03bbfc87332bcd6a4abd74550b79 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 13 Aug 2026 23:05:23 +0200 Subject: [PATCH 009/133] Perf: Refactor diagnostics cache with metadata record * Refactored the diagnostics cache in `FSharpDocumentDiagnosticAnalyzer` to use a new `CachedDiagnosticsEntry` record, storing `TextVersion`, `ProjectVersion`, `FilePath`, `IsRemoveParensEnabled`, and cached `Diagnostics`. * The cache key remains `(DocumentId * DiagnosticsType)`, but the value is now the new record. Cache lookup now checks all relevant fields for equality, ensuring diagnostics are reused only when context matches. * Added `evictRemovedDocuments` to remove cache entries for deleted documents. * Updated logic for "Remove Parentheses" diagnostics to use the cached flag, and updated cache storage to the new structure. --- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 146 +++++++++++------- 1 file changed, 90 insertions(+), 56 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 3cb80f18e26..9d0c8ca66f5 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -22,6 +22,15 @@ type internal DiagnosticsType = | Syntax | Semantic +type private CachedDiagnosticsEntry = + { + TextVersion: VersionStamp + ProjectVersion: VersionStamp + FilePath: string + IsRemoveParensEnabled: bool + Diagnostics: ImmutableArray + } + [)>] type internal FSharpDocumentDiagnosticAnalyzer [] () = @@ -29,7 +38,14 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics() static let cache = - ConcurrentDictionary>() + ConcurrentDictionary() + + static let evictRemovedDocuments (solution: Solution) = + for entry in cache do + let struct (documentId, _) = entry.Key + + if isNull (solution.GetDocument(documentId)) then + cache.TryRemove(entry.Key) |> ignore static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -83,79 +99,97 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = | DiagnosticsType.Syntax -> CancellableTask.singleton VersionStamp.Default | DiagnosticsType.Semantic -> (fun ct -> document.Project.GetDependentVersionAsync(ct)) + let filePath = document.FilePath + + let isRemoveParensEnabled = + match diagnosticType with + | DiagnosticsType.Syntax -> document.Project.IsFsharpRemoveParensEnabled + | DiagnosticsType.Semantic -> false + + evictRemovedDocuments document.Project.Solution + let cacheKey = struct (document.Id, diagnosticType) let cached = match cache.TryGetValue(cacheKey) with - | true, (cachedTextVersion, cachedProjectVersion, cachedDiagnostics) when - cachedTextVersion = textVersion && cachedProjectVersion = projectVersion + | true, cachedEntry when + cachedEntry.TextVersion = textVersion + && cachedEntry.ProjectVersion = projectVersion + && cachedEntry.FilePath = filePath + && cachedEntry.IsRemoveParensEnabled = isRemoveParensEnabled -> - ValueSome cachedDiagnostics + ValueSome cachedEntry.Diagnostics | _ -> ValueNone match cached with | ValueSome cachedDiagnostics -> return cachedDiagnostics | ValueNone -> - let! sourceText = document.GetTextAsync(ct) - let filePath = document.FilePath + let! sourceText = document.GetTextAsync(ct) - let errors = HashSet(diagnosticEqualityComparer) + let errors = HashSet(diagnosticEqualityComparer) - let! parseResults = document.GetFSharpParseResultsAsync("GetDiagnostics") + let! parseResults = document.GetFSharpParseResultsAsync("GetDiagnostics") - match diagnosticType with - | DiagnosticsType.Syntax -> - for diagnostic in parseResults.Diagnostics do - errors.Add(diagnostic) |> ignore + match diagnosticType with + | DiagnosticsType.Syntax -> + for diagnostic in parseResults.Diagnostics do + errors.Add(diagnostic) |> ignore - | DiagnosticsType.Semantic -> - let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync("GetDiagnostics") + | DiagnosticsType.Semantic -> + let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync("GetDiagnostics") - for diagnostic in checkResults.Diagnostics do - errors.Add(diagnostic) |> ignore + for diagnostic in checkResults.Diagnostics do + errors.Add(diagnostic) |> ignore - errors.ExceptWith(parseResults.Diagnostics) + errors.ExceptWith(parseResults.Diagnostics) - let! unnecessaryParentheses = - match diagnosticType with - | DiagnosticsType.Syntax when document.Project.IsFsharpRemoveParensEnabled -> - UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document - | _ -> CancellableTask.singleton ImmutableArray.Empty - - let result = - if errors.Count = 0 && unnecessaryParentheses.IsEmpty then - ImmutableArray.Empty - else - let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length) - - for diagnostic in errors do - if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then - let linePositionSpan = - LinePositionSpan( - LinePosition(diagnostic.StartLine - 1, diagnostic.StartColumn), - LinePosition(diagnostic.EndLine - 1, diagnostic.EndColumn) - ) - - let textSpan = sourceText.Lines.GetTextSpan(linePositionSpan) - - // F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries - let correctedTextSpan = - if textSpan.End <= sourceText.Length then - textSpan - else - let start = min textSpan.Start (sourceText.Length - 1) |> max 0 - - TextSpan.FromBounds(start, sourceText.Length) - - let location = Location.Create(filePath, correctedTextSpan, linePositionSpan) - iab.Add(RoslynHelpers.ConvertError(diagnostic, location)) - - iab.AddRange unnecessaryParentheses - iab.ToImmutable() - - cache.[cacheKey] <- (textVersion, projectVersion, result) - return result + let! unnecessaryParentheses = + match diagnosticType with + | DiagnosticsType.Syntax when isRemoveParensEnabled -> UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document + | _ -> CancellableTask.singleton ImmutableArray.Empty + + let result = + if errors.Count = 0 && unnecessaryParentheses.IsEmpty then + ImmutableArray.Empty + else + let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length) + + for diagnostic in errors do + if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then + let linePositionSpan = + LinePositionSpan( + LinePosition(diagnostic.StartLine - 1, diagnostic.StartColumn), + LinePosition(diagnostic.EndLine - 1, diagnostic.EndColumn) + ) + + let textSpan = sourceText.Lines.GetTextSpan(linePositionSpan) + + // F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries + let correctedTextSpan = + if textSpan.End <= sourceText.Length then + textSpan + else + let start = min textSpan.Start (sourceText.Length - 1) |> max 0 + + TextSpan.FromBounds(start, sourceText.Length) + + let location = Location.Create(filePath, correctedTextSpan, linePositionSpan) + iab.Add(RoslynHelpers.ConvertError(diagnostic, location)) + + iab.AddRange unnecessaryParentheses + iab.ToImmutable() + + cache.[cacheKey] <- + { + TextVersion = textVersion + ProjectVersion = projectVersion + FilePath = filePath + IsRemoveParensEnabled = isRemoveParensEnabled + Diagnostics = result + } + + return result } interface IFSharpDocumentDiagnosticAnalyzer with From 68a8e9a536747efda056d61303e4fe0e6e31abc1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 13 Aug 2026 23:06:23 +0200 Subject: [PATCH 010/133] Perf: Add release notes (#20121) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ef64e1a75c4..a0fe282be61 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -9,6 +9,7 @@ * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Avoid using `cancellableTask` in `DocumentCache`; the editor cache now uses direct `CancellationToken`-aware `task` wrappers, avoiding the background `Task.Run` offload and a larger wrapper closure from the `cancellableTask` builder. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) +* Prevent stale and leaked per-document diagnostics by tightening cache validity and evicting entries for removed documents. ([PR #20121](https://github.com/dotnet/fsharp/pull/20121)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) From deee5393616ca56493133eb635336eae7e440f0a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 20 Aug 2026 17:41:19 +0200 Subject: [PATCH 011/133] evict diagnostics cache on workspace events * `FSharpDocumentDiagnosticAnalyzer` now evicts cached diagnostics when documents/projects/solution are removed, using `WorkspaceChanged` events. * Replaces `evictRemovedDocuments` with targeted evictDocument/evictProject. * Cache eviction is now event-driven, not on-demand. * Also injects optional VisualStudioWorkspace and marks analyzer `[Shared]` for MEF. --- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 9d0c8ca66f5..3b24e4f9f68 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -12,6 +12,7 @@ open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics +open Microsoft.VisualStudio.LanguageServices open FSharp.Compiler.Diagnostics open CancellableTasks @@ -31,8 +32,8 @@ type private CachedDiagnosticsEntry = Diagnostics: ImmutableArray } -[)>] -type internal FSharpDocumentDiagnosticAnalyzer [] () = +[); Shared>] +type internal FSharpDocumentDiagnosticAnalyzer [] ([] workspace: VisualStudioWorkspace | null) = let shouldProduceDiagnostics (document: Document) = document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics() @@ -40,13 +41,30 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = static let cache = ConcurrentDictionary() - static let evictRemovedDocuments (solution: Solution) = + static let evictDocument (documentId: DocumentId) = + cache.TryRemove(struct (documentId, DiagnosticsType.Syntax)) |> ignore + cache.TryRemove(struct (documentId, DiagnosticsType.Semantic)) |> ignore + + static let evictProject (projectId: ProjectId) = for entry in cache do let struct (documentId, _) = entry.Key - if isNull (solution.GetDocument(documentId)) then + if documentId.ProjectId = projectId then cache.TryRemove(entry.Key) |> ignore + do + match workspace with + | null -> () + | workspace -> + workspace.WorkspaceChanged.Add(fun args -> + match args.Kind with + | WorkspaceChangeKind.DocumentRemoved when not (isNull args.DocumentId) -> evictDocument args.DocumentId + | WorkspaceChangeKind.ProjectRemoved when not (isNull args.ProjectId) -> evictProject args.ProjectId + | WorkspaceChangeKind.SolutionCleared + | WorkspaceChangeKind.SolutionRemoved -> cache.Clear() + | _ -> () + ) + static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -106,8 +124,6 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = | DiagnosticsType.Syntax -> document.Project.IsFsharpRemoveParensEnabled | DiagnosticsType.Semantic -> false - evictRemovedDocuments document.Project.Solution - let cacheKey = struct (document.Id, diagnosticType) let cached = From 5bc89a179e62cded8a423428926bbe3014c3a733 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 3 Aug 2026 02:26:54 +0200 Subject: [PATCH 012/133] Perf: async isProjectInvalidated + debounce script updateProjectOptions Fixes #20125, #20124 --- .../FSharpProjectOptionsManager.fs | 333 ++++++++++++------ 1 file changed, 227 insertions(+), 106 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index fd821f7c361..d85ffe6a39b 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. namespace Microsoft.VisualStudio.FSharp.Editor @@ -8,19 +8,17 @@ open System.Collections.Concurrent open System.Collections.Immutable open System.IO open System.Linq +open System.Runtime.CompilerServices +open System.Threading +open System.Threading.Tasks open Microsoft.CodeAnalysis open FSharp.Compiler open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Text open Microsoft.VisualStudio.FSharp.Editor -open System.Threading open Microsoft.VisualStudio.FSharp.Interactive.Session -open System.Runtime.CompilerServices -open CancellableTasks -open Microsoft.VisualStudio.FSharp.Editor.Extensions -open System.Windows -open Microsoft.VisualStudio -open FSharp.Compiler.Text open Microsoft.VisualStudio.TextManager.Interop +open CancellableTasks #nowarn "57" @@ -66,40 +64,58 @@ module private FSharpProjectOptionsHelpers = let inline hasProjectVersionChanged (oldProject: Project) (newProject: Project) = oldProject.Version <> newProject.Version - let hasDependentVersionChanged (oldProject: Project) (newProject: Project) (ct: CancellationToken) = - let oldProjectMetadataRefs = oldProject.MetadataReferences - let newProjectMetadataRefs = newProject.MetadataReferences + let hasDependentVersionChanged (oldProject: Project) (newProject: Project) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let oldProjectMetadataRefs = oldProject.MetadataReferences + let newProjectMetadataRefs = newProject.MetadataReferences - if oldProjectMetadataRefs.Count <> newProjectMetadataRefs.Count then - true - else + if oldProjectMetadataRefs.Count <> newProjectMetadataRefs.Count then + return true + else - let oldProjectRefs = oldProject.ProjectReferences - let newProjectRefs = newProject.ProjectReferences - - oldProjectRefs.Count() <> newProjectRefs.Count() - || (oldProjectRefs, newProjectRefs) - ||> Seq.exists2 (fun p1 p2 -> - ct.ThrowIfCancellationRequested() - let doesProjectIdDiffer = p1.ProjectId <> p2.ProjectId - let p1 = oldProject.Solution.GetProject(p1.ProjectId) - let p2 = newProject.Solution.GetProject(p2.ProjectId) - - doesProjectIdDiffer - || (if p1.IsFSharp then - p1.Version <> p2.Version - else - let v1 = p1.GetDependentVersionAsync(ct).Result - let v2 = p2.GetDependentVersionAsync(ct).Result - v1 <> v2)) - - let isProjectInvalidated (oldProject: Project) (newProject: Project) ct = - let hasProjectVersionChanged = hasProjectVersionChanged oldProject newProject - - if newProject.AreFSharpInMemoryCrossProjectReferencesEnabled then - hasProjectVersionChanged || hasDependentVersionChanged oldProject newProject ct - else - hasProjectVersionChanged + let oldProjectRefs = oldProject.ProjectReferences + let newProjectRefs = newProject.ProjectReferences + + if oldProjectRefs.Count() <> newProjectRefs.Count() then + return true + else + let mutable result = false + let mutable enum1 = oldProjectRefs.GetEnumerator() + let mutable enum2 = newProjectRefs.GetEnumerator() + + while not result && enum1.MoveNext() && enum2.MoveNext() do + ct.ThrowIfCancellationRequested() + let p1 = enum1.Current + let p2 = enum2.Current + let doesProjectIdDiffer = p1.ProjectId <> p2.ProjectId + let p1 = oldProject.Solution.GetProject(p1.ProjectId) + let p2 = newProject.Solution.GetProject(p2.ProjectId) + + if doesProjectIdDiffer then + result <- true + elif p1.IsFSharp then + result <- p1.Version <> p2.Version + else + let! v1 = p1.GetDependentVersionAsync(ct) + let! v2 = p2.GetDependentVersionAsync(ct) + result <- v1 <> v2 + + return result + } + + let isProjectInvalidated (oldProject: Project) (newProject: Project) = + cancellableTask { + let hasProjectVersionChanged = hasProjectVersionChanged oldProject newProject + + if newProject.AreFSharpInMemoryCrossProjectReferencesEnabled then + if hasProjectVersionChanged then + return true + else + return! hasDependentVersionChanged oldProject newProject + else + return hasProjectVersionChanged + } [] type private FSharpProjectOptionsMessage = @@ -128,73 +144,152 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = ConcurrentDictionary() // This is used to not constantly emit the same compilation. + // However, when C# projects churn, Roslyn creates new Compilation instances with the same project ID and version, + // which makes ConditionalWeakTable defeat the purpose. We use a nested ConcurrentDictionary keyed by ProjectId and VersionStamp + // to map to the FSharpReferencedProject, ensuring stable references across churns. + let emitCache = ConcurrentDictionary>() let weakPEReferences = ConditionalWeakTable() let lastSuccessfulCompilations = ConcurrentDictionary() let scriptUpdatedEvent = Event() let createPEReference (referencedProject: Project) (comp: Compilation) = - let projectId = referencedProject.Id - - match weakPEReferences.TryGetValue comp with - | true, fsRefProj -> fsRefProj - | _ -> - let mutable strongComp = comp - let weakComp = WeakReference(comp) - let mutable stamp = DateTime.UtcNow - - // Getting a C# reference assembly can fail if there are compilation errors that cannot be resolved. - // To mitigate this, we store the last successful compilation of a C# project and re-use it until we get a new successful compilation. - let getStream = - fun ct -> - let tryStream (comp: Compilation) = - let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. - - let emitOptions = - Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) - - try - let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) - - if result.Success then - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - lastSuccessfulCompilations.[projectId] <- comp - ms.Position <- 0L - ms :> Stream |> Some - else - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - ms.Dispose() // it failed, dispose of stream - None - with - | :? OperationCanceledException -> - // Since we cancelled, do not null out the strong compilation ref and update the stamp. - stamp <- DateTime.UtcNow - ms.Dispose() - None - | _ -> - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - ms.Dispose() // it failed, dispose of stream - None - - let resultOpt = - match weakComp.TryGetTarget() with - | true, comp -> tryStream comp - | _ -> None - - match resultOpt with - | Some _ -> resultOpt + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let projectId = referencedProject.Id + let! stamp = referencedProject.GetDependentVersionAsync(ct) + + match emitCache.TryGetValue(projectId) with + | true, versionCache -> + match versionCache.TryGetValue(stamp) with + | true, fsRefProj -> return fsRefProj + | _ -> + match weakPEReferences.TryGetValue comp with + | true, fsRefProj -> return fsRefProj | _ -> - match lastSuccessfulCompilations.TryGetValue(projectId) with - | true, comp -> tryStream comp - | _ -> None - - let getStamp = fun () -> stamp - - let fsRefProj = - FSharpReferencedProject.PEReference(getStamp, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) - - weakPEReferences.Add(comp, fsRefProj) - fsRefProj + let mutable strongComp = comp + let weakComp = WeakReference(comp) + let mutable stampTime = DateTime.UtcNow + + // Getting a C# reference assembly can fail if there are compilation errors that cannot be resolved. + // To mitigate this, we store the last successful compilation of a C# project and re-use it until we get a new successful compilation. + let getStream = + fun ct -> + let tryStream (comp: Compilation) = + let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. + + let emitOptions = + Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) + + try + let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) + + if result.Success then + strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. + lastSuccessfulCompilations.[projectId] <- comp + ms.Position <- 0L + ms :> Stream |> Some + else + strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. + ms.Dispose() // it failed, dispose of stream + None + with + | :? OperationCanceledException -> + // Since we cancelled, do not null out the strong compilation ref and update the stamp. + stampTime <- DateTime.UtcNow + ms.Dispose() + None + | _ -> + strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. + ms.Dispose() // it failed, dispose of stream + None + + let resultOpt = + match weakComp.TryGetTarget() with + | true, comp -> tryStream comp + | _ -> None + + match resultOpt with + | Some _ -> resultOpt + | _ -> + match lastSuccessfulCompilations.TryGetValue(projectId) with + | true, comp -> tryStream comp + | _ -> None + + let getStampTime = fun () -> stampTime + + let fsRefProj = + FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) + + weakPEReferences.Add(comp, fsRefProj) + versionCache.[stamp] <- fsRefProj + return fsRefProj + | _ -> + // Initialize for this project + let versionCache = ConcurrentDictionary() + emitCache.[projectId] <- versionCache + + match weakPEReferences.TryGetValue comp with + | true, fsRefProj -> return fsRefProj + | _ -> + let mutable strongComp = comp + let weakComp = WeakReference(comp) + let mutable stampTime = DateTime.UtcNow + + // Getting a C# reference assembly can fail if there are compilation errors that cannot be resolved. + // To mitigate this, we store the last successful compilation of a C# project and re-use it until we get a new successful compilation. + let getStream = + fun ct -> + let tryStream (comp: Compilation) = + let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. + + let emitOptions = + Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) + + try + let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) + + if result.Success then + strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. + lastSuccessfulCompilations.[projectId] <- comp + ms.Position <- 0L + ms :> Stream |> Some + else + strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. + ms.Dispose() // it failed, dispose of stream + None + with + | :? OperationCanceledException -> + // Since we cancelled, do not null out the strong compilation ref and update the stamp. + stampTime <- DateTime.UtcNow + ms.Dispose() + None + | _ -> + strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. + ms.Dispose() // it failed, dispose of stream + None + + let resultOpt = + match weakComp.TryGetTarget() with + | true, comp -> tryStream comp + | _ -> None + + match resultOpt with + | Some _ -> resultOpt + | _ -> + match lastSuccessfulCompilations.TryGetValue(projectId) with + | true, comp -> tryStream comp + | _ -> None + + let getStampTime = fun () -> stampTime + + let fsRefProj = + FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) + + weakPEReferences.Add(comp, fsRefProj) + versionCache.[stamp] <- fsRefProj + return fsRefProj + } let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = cancellableTask { @@ -264,14 +359,28 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) + let mutable debounceCts = null: CancellationTokenSource + let updateProjectOptions () = - async { + let cts = new CancellationTokenSource() + + match Interlocked.Exchange(&debounceCts, cts) with + | null -> () + | previousCts -> + previousCts.Cancel() + previousCts.Dispose() + + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + do! Task.Delay(500, ct) + let! scriptProjectOptions, _ = getProjectOptionsFromScript textViewAndCaret checker.NotifyFileChanged(document.FilePath, scriptProjectOptions) |> Async.Start } - |> Async.Start + |> CancellableTask.start cts.Token + |> ignore let onChangeCaretHandler (_, _newline: int, _oldline: int) = updateProjectOptions () let onKillFocus (_) = updateProjectOptions () @@ -304,7 +413,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = return ValueSome(parsingOptions, projectOptions) | true, (oldProject, oldFileStamp, parsingOptions, projectOptions, _) -> - if fileStamp <> oldFileStamp || isProjectInvalidated document.Project oldProject ct then + let! isInvalidated = + if fileStamp <> oldFileStamp then + CancellableTask.singleton true + else + isProjectInvalidated document.Project oldProject + + if isInvalidated then match singleFileCache.TryRemove(document.Id) with | true, (_, _, _, _, ValueSome subscription) -> subscription.Dispose() | _ -> () @@ -351,7 +466,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = ) elif referencedProject.SupportsCompilation then let! comp = referencedProject.GetCompilationAsync(ct) - let peRef = createPEReference referencedProject comp + let! peRef = createPEReference referencedProject comp referencedProjects.Add(peRef) if canBail then @@ -424,7 +539,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = lastSuccessfulCompilations.ToArray() |> Array.iter (fun pair -> if not (currentSolution.ContainsProject(pair.Key)) then - lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore) + lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore + emitCache.TryRemove(pair.Key) |> ignore) checker.InvalidateConfiguration(projectOptions, userOpName = "tryComputeOptions") @@ -435,7 +551,9 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = return ValueSome(parsingOptions, projectOptions) | true, (oldProject, parsingOptions, projectOptions) -> - if isProjectInvalidated oldProject project ct then + let! isInvalidated = isProjectInvalidated oldProject project + + if isInvalidated then cache.TryRemove(projectId) |> ignore return! tryComputeOptions project ct else @@ -513,6 +631,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match cache.TryRemove(projectId) with | true, (_, _, projectOptions) -> lastSuccessfulCompilations.TryRemove(projectId) |> ignore + emitCache.TryRemove(projectId) |> ignore checker.ClearCache([ projectOptions ]) | _ -> () @@ -521,6 +640,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match singleFileCache.TryRemove(documentId) with | true, (_, _, _, projectOptions, subscription) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore + emitCache.TryRemove(documentId.ProjectId) |> ignore checker.ClearCache([ projectOptions ]) subscription |> ValueOption.iter (fun handler -> handler.Dispose()) | _ -> () @@ -558,6 +678,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = cache.Clear() singleFileCache.Clear() lastSuccessfulCompilations.Clear() + emitCache.Clear() member _.ScriptUpdated = scriptUpdatedEvent.Publish From 104818fdad46f097cb5fa89b249d9f09328f0956 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 3 Aug 2026 03:01:16 +0200 Subject: [PATCH 013/133] Fix: keep updateProjectOptions debounce as plain async to avoid FS3401 resumable-code composition error; async invalidation via cancellableTask and stable emitCache for C# PE references --- .../LanguageService/FSharpProjectOptionsManager.fs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index d85ffe6a39b..a519fbe356f 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -370,17 +370,14 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = previousCts.Cancel() previousCts.Dispose() - cancellableTask { - let! ct = CancellableTask.getCancellationToken () - do! Task.Delay(500, ct) + async { + do! Task.Delay(500, cts.Token) |> Async.AwaitTask let! scriptProjectOptions, _ = getProjectOptionsFromScript textViewAndCaret - checker.NotifyFileChanged(document.FilePath, scriptProjectOptions) - |> Async.Start + do! checker.NotifyFileChanged(document.FilePath, scriptProjectOptions) } - |> CancellableTask.start cts.Token - |> ignore + |> Async.Start let onChangeCaretHandler (_, _newline: int, _oldline: int) = updateProjectOptions () let onKillFocus (_) = updateProjectOptions () @@ -555,7 +552,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = if isInvalidated then cache.TryRemove(projectId) |> ignore - return! tryComputeOptions project ct + return! tryComputeOptions project else return ValueSome(parsingOptions, projectOptions) } From 8841294b9c7d21e618ed1e1737e563e5f70028c1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 14 Aug 2026 19:02:06 +0200 Subject: [PATCH 014/133] Perf: fix PR comments --- .../FSharpProjectOptionsManager.fs | 316 +++++++++--------- 1 file changed, 152 insertions(+), 164 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index a519fbe356f..fc0d8897188 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -37,8 +37,10 @@ module private FSharpProjectOptionsHelpers = member _.Description = project.Name member _.CompilationSourceFiles = sourcePaths - member _.CompilationOptions = - Array.concat [ options; referencePaths |> Array.map (fun r -> "-r:" + r) ] + member _.CompilationOptions = [| + yield! options; + yield! referencePaths |> Seq.map (fun r -> "-r:" + r) + |] member _.CompilationReferences = referencePaths @@ -117,6 +119,22 @@ module private FSharpProjectOptionsHelpers = return hasProjectVersionChanged } +type SingleFileCacheEntry = + { + Project: Project + FileStamp: VersionStamp + ParsingOptions: FSharpParsingOptions + ProjectOptions: FSharpProjectOptions + Subscription: ConnectionPointSubscription + } + +type PEReferenceCacheEntry = + { + Stamp: VersionStamp + Compilation: Compilation + Reference: FSharpReferencedProject + } + [] type private FSharpProjectOptionsMessage = | TryGetOptionsByDocument of @@ -140,155 +158,96 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let cache = ConcurrentDictionary() - let singleFileCache = - ConcurrentDictionary() + let singleFileCache = ConcurrentDictionary() - // This is used to not constantly emit the same compilation. - // However, when C# projects churn, Roslyn creates new Compilation instances with the same project ID and version, - // which makes ConditionalWeakTable defeat the purpose. We use a nested ConcurrentDictionary keyed by ProjectId and VersionStamp - // to map to the FSharpReferencedProject, ensuring stable references across churns. - let emitCache = ConcurrentDictionary>() + let emitCache = ConcurrentDictionary() let weakPEReferences = ConditionalWeakTable() let lastSuccessfulCompilations = ConcurrentDictionary() let scriptUpdatedEvent = Event() + let disposeSingleFileCacheEntry ({ Subscription = subscription }: SingleFileCacheEntry) = + subscription |> ValueOption.iter (fun subscription -> subscription.Dispose()) + + let tryGetCachedPEReference projectId stamp comp = + match emitCache.TryGetValue(projectId) with + | true, { Stamp = cachedStamp; Compilation = cachedCompilation; Reference = fsRefProj } + when cachedStamp = stamp && obj.ReferenceEquals(cachedCompilation, comp) -> + ValueSome fsRefProj + | _ -> + ValueNone + + let cachePEReference projectId stamp comp fsRefProj = + emitCache.[projectId] <- { Stamp = stamp; Compilation = comp; Reference = fsRefProj } + fsRefProj + + let createNewPEReference projectId (referencedProject: Project) (comp: Compilation) = + let mutable strongComp = comp + let weakComp = WeakReference(comp) + let mutable stampTime = DateTime.UtcNow + + let getStream = + fun ct -> + let tryStream (comp: Compilation) = + let ms = new MemoryStream() + + let emitOptions = + Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) + + try + let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) + + if result.Success then + strongComp <- Unchecked.defaultof<_> + lastSuccessfulCompilations.[projectId] <- comp + ms.Position <- 0L + ms :> Stream |> Some + else + strongComp <- Unchecked.defaultof<_> + ms.Dispose() + None + with + | :? OperationCanceledException -> + stampTime <- DateTime.UtcNow + ms.Dispose() + None + | _ -> + strongComp <- Unchecked.defaultof<_> + ms.Dispose() + None + + let resultOpt = + match weakComp.TryGetTarget() with + | true, comp -> tryStream comp + | _ -> None + + match resultOpt with + | Some _ -> resultOpt + | _ -> + match lastSuccessfulCompilations.TryGetValue(projectId) with + | true, comp -> tryStream comp + | _ -> None + + let getStampTime () = stampTime + FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) + let createPEReference (referencedProject: Project) (comp: Compilation) = cancellableTask { let! ct = CancellableTask.getCancellationToken () let projectId = referencedProject.Id let! stamp = referencedProject.GetDependentVersionAsync(ct) - match emitCache.TryGetValue(projectId) with - | true, versionCache -> - match versionCache.TryGetValue(stamp) with - | true, fsRefProj -> return fsRefProj - | _ -> - match weakPEReferences.TryGetValue comp with - | true, fsRefProj -> return fsRefProj - | _ -> - let mutable strongComp = comp - let weakComp = WeakReference(comp) - let mutable stampTime = DateTime.UtcNow - - // Getting a C# reference assembly can fail if there are compilation errors that cannot be resolved. - // To mitigate this, we store the last successful compilation of a C# project and re-use it until we get a new successful compilation. - let getStream = - fun ct -> - let tryStream (comp: Compilation) = - let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. - - let emitOptions = - Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) - - try - let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) - - if result.Success then - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - lastSuccessfulCompilations.[projectId] <- comp - ms.Position <- 0L - ms :> Stream |> Some - else - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - ms.Dispose() // it failed, dispose of stream - None - with - | :? OperationCanceledException -> - // Since we cancelled, do not null out the strong compilation ref and update the stamp. - stampTime <- DateTime.UtcNow - ms.Dispose() - None - | _ -> - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - ms.Dispose() // it failed, dispose of stream - None - - let resultOpt = - match weakComp.TryGetTarget() with - | true, comp -> tryStream comp - | _ -> None - - match resultOpt with - | Some _ -> resultOpt - | _ -> - match lastSuccessfulCompilations.TryGetValue(projectId) with - | true, comp -> tryStream comp - | _ -> None - - let getStampTime = fun () -> stampTime - - let fsRefProj = - FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) - - weakPEReferences.Add(comp, fsRefProj) - versionCache.[stamp] <- fsRefProj - return fsRefProj - | _ -> - // Initialize for this project - let versionCache = ConcurrentDictionary() - emitCache.[projectId] <- versionCache - + match tryGetCachedPEReference projectId stamp comp with + | ValueSome fsRefProj -> + return fsRefProj + | ValueNone -> match weakPEReferences.TryGetValue comp with - | true, fsRefProj -> return fsRefProj + | true, fsRefProj -> + return cachePEReference projectId stamp comp fsRefProj | _ -> - let mutable strongComp = comp - let weakComp = WeakReference(comp) - let mutable stampTime = DateTime.UtcNow - - // Getting a C# reference assembly can fail if there are compilation errors that cannot be resolved. - // To mitigate this, we store the last successful compilation of a C# project and re-use it until we get a new successful compilation. - let getStream = - fun ct -> - let tryStream (comp: Compilation) = - let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. - - let emitOptions = - Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) - - try - let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) - - if result.Success then - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - lastSuccessfulCompilations.[projectId] <- comp - ms.Position <- 0L - ms :> Stream |> Some - else - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - ms.Dispose() // it failed, dispose of stream - None - with - | :? OperationCanceledException -> - // Since we cancelled, do not null out the strong compilation ref and update the stamp. - stampTime <- DateTime.UtcNow - ms.Dispose() - None - | _ -> - strongComp <- Unchecked.defaultof<_> // Stop strongly holding the compilation since we have a result. - ms.Dispose() // it failed, dispose of stream - None - - let resultOpt = - match weakComp.TryGetTarget() with - | true, comp -> tryStream comp - | _ -> None - - match resultOpt with - | Some _ -> resultOpt - | _ -> - match lastSuccessfulCompilations.TryGetValue(projectId) with - | true, comp -> tryStream comp - | _ -> None - - let getStampTime = fun () -> stampTime - - let fsRefProj = - FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) - + let fsRefProj = createNewPEReference projectId referencedProject comp weakPEReferences.Add(comp, fsRefProj) - versionCache.[stamp] <- fsRefProj - return fsRefProj + return cachePEReference projectId stamp comp fsRefProj } let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = @@ -361,8 +320,16 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let mutable debounceCts = null: CancellationTokenSource + let disposeDebounceCts () = + match Interlocked.Exchange(&debounceCts, null) with + | null -> () + | cts -> + cts.Cancel() + cts.Dispose() + let updateProjectOptions () = let cts = new CancellationTokenSource() + let debounceToken = cts.Token match Interlocked.Exchange(&debounceCts, cts) with | null -> () @@ -370,46 +337,64 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = previousCts.Cancel() previousCts.Dispose() - async { - do! Task.Delay(500, cts.Token) |> Async.AwaitTask + task { + try + do! Task.Delay(500, debounceToken) - let! scriptProjectOptions, _ = getProjectOptionsFromScript textViewAndCaret + let! scriptProjectOptions, _ = getProjectOptionsFromScript textViewAndCaret - do! checker.NotifyFileChanged(document.FilePath, scriptProjectOptions) + do! checker.NotifyFileChanged(document.FilePath, scriptProjectOptions) + with + | :? OperationCanceledException + | :? TaskCanceledException -> () } - |> Async.Start + |> ignore let onChangeCaretHandler (_, _newline: int, _oldline: int) = updateProjectOptions () let onKillFocus (_) = updateProjectOptions () let onSetFocus (_) = updateProjectOptions () - let addToCacheAndSubscribe value = - match value with - | projectId, fileStamp, parsingOptions, projectOptions, _ -> - let subscription = - match textViewAndCaret () with - | ValueSome(textView, _) -> - subscribeToTextViewEvents ( - textView, - (ValueSome onChangeCaretHandler), - (ValueSome onKillFocus), - (ValueSome onSetFocus) - ) - | ValueNone -> ValueNone + let addToCacheAndSubscribe ({ Project = _; FileStamp = fileStamp; ParsingOptions = parsingOptions; ProjectOptions = projectOptions; Subscription = _ }: SingleFileCacheEntry) = + let textViewSubscription = + textViewAndCaret () + |> ValueOption.bind (fun (textView, _) -> + subscribeToTextViewEvents (textView, (ValueSome onChangeCaretHandler), (ValueSome onKillFocus), (ValueSome onSetFocus)) + ) + + let subscription = + ValueSome + { new IDisposable with + member _.Dispose() = + textViewSubscription |> ValueOption.iter _.Dispose() + disposeDebounceCts () + } - (projectId, fileStamp, parsingOptions, projectOptions, subscription) + { + Project = document.Project + FileStamp = fileStamp + ParsingOptions = parsingOptions + ProjectOptions = projectOptions + Subscription = subscription + } singleFileCache.AddOrUpdate( - document.Id, // The key to the cache - (fun _ value -> addToCacheAndSubscribe value), // Function to add the cached value if the key does not exist - (fun _ _ value -> value), // Function to update the value if the key exists - (document.Project, fileStamp, parsingOptions, projectOptions, ValueNone) // The value to add or update + document.Id, + (fun _ -> + addToCacheAndSubscribe + { + Project = document.Project + FileStamp = fileStamp + ParsingOptions = parsingOptions + ProjectOptions = projectOptions + Subscription = ValueNone + }), + (fun _ existing -> addToCacheAndSubscribe existing) ) |> ignore return ValueSome(parsingOptions, projectOptions) - | true, (oldProject, oldFileStamp, parsingOptions, projectOptions, _) -> + | true, { Project = oldProject; FileStamp = oldFileStamp; ParsingOptions = parsingOptions; ProjectOptions = projectOptions; Subscription = _ } -> let! isInvalidated = if fileStamp <> oldFileStamp then CancellableTask.singleton true @@ -418,7 +403,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = if isInvalidated then match singleFileCache.TryRemove(document.Id) with - | true, (_, _, _, _, ValueSome subscription) -> subscription.Dispose() + | true, cacheEntry -> disposeSingleFileCacheEntry cacheEntry | _ -> () return! tryComputeOptionsBySingleScriptOrFile document userOpName @@ -635,12 +620,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = legacyProjectSites.TryRemove(projectId) |> ignore | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> match singleFileCache.TryRemove(documentId) with - | true, (_, _, _, projectOptions, subscription) -> + | true, ({ ProjectOptions = projectOptions } as cacheEntry) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore emitCache.TryRemove(documentId.ProjectId) |> ignore checker.ClearCache([ projectOptions ]) - subscription |> ValueOption.iter (fun handler -> handler.Dispose()) + disposeSingleFileCacheEntry cacheEntry | _ -> () + } let agent = @@ -673,6 +659,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = commandLineOptions.Clear() legacyProjectSites.Clear() cache.Clear() + singleFileCache.Values |> Seq.iter disposeSingleFileCacheEntry singleFileCache.Clear() lastSuccessfulCompilations.Clear() emitCache.Clear() @@ -680,7 +667,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = member _.ScriptUpdated = scriptUpdatedEvent.Publish interface IDisposable with - member _.Dispose() = + member this.Dispose() = + this.ClearAllCaches() cancellationTokenSource.Cancel() cancellationTokenSource.Dispose() (agent :> IDisposable).Dispose() From 2aedc93aa12af7890ee033dca89d08a11bc52ead Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 01:17:07 +0200 Subject: [PATCH 015/133] Refactor pattern matches and record usage for clarity Expanded record field syntax in pattern matches and constructions for maintainability. Rewrote `CompilationOptions` and `otherOptions` logic using array comprehensions for conciseness and readability. Updated `debounceCts` to allow null values explicitly. Replaced some `Array.iter` usages with `for` loops for consistency. --- .../FSharpProjectOptionsManager.fs | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index fc0d8897188..552765f62b7 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -37,10 +37,8 @@ module private FSharpProjectOptionsHelpers = member _.Description = project.Name member _.CompilationSourceFiles = sourcePaths - member _.CompilationOptions = [| - yield! options; - yield! referencePaths |> Seq.map (fun r -> "-r:" + r) - |] + member _.CompilationOptions = + [| yield! options; yield! referencePaths |> Seq.map (fun r -> "-r:" + r) |] member _.CompilationReferences = referencePaths @@ -171,14 +169,22 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let tryGetCachedPEReference projectId stamp comp = match emitCache.TryGetValue(projectId) with - | true, { Stamp = cachedStamp; Compilation = cachedCompilation; Reference = fsRefProj } - when cachedStamp = stamp && obj.ReferenceEquals(cachedCompilation, comp) -> - ValueSome fsRefProj - | _ -> - ValueNone + | true, + { + Stamp = cachedStamp + Compilation = cachedCompilation + Reference = fsRefProj + } when cachedStamp = stamp && obj.ReferenceEquals(cachedCompilation, comp) -> ValueSome fsRefProj + | _ -> ValueNone let cachePEReference projectId stamp comp fsRefProj = - emitCache.[projectId] <- { Stamp = stamp; Compilation = comp; Reference = fsRefProj } + emitCache.[projectId] <- + { + Stamp = stamp + Compilation = comp + Reference = fsRefProj + } + fsRefProj let createNewPEReference projectId (referencedProject: Project) (comp: Compilation) = @@ -238,12 +244,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let! stamp = referencedProject.GetDependentVersionAsync(ct) match tryGetCachedPEReference projectId stamp comp with - | ValueSome fsRefProj -> - return fsRefProj + | ValueSome fsRefProj -> return fsRefProj | ValueNone -> match weakPEReferences.TryGetValue comp with - | true, fsRefProj -> - return cachePEReference projectId stamp comp fsRefProj + | true, fsRefProj -> return cachePEReference projectId stamp comp fsRefProj | _ -> let fsRefProj = createNewPEReference projectId referencedProject comp weakPEReferences.Add(comp, fsRefProj) @@ -289,10 +293,11 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let otherOptions = if project.IsFSharpMetadata then [| - for x in project.ProjectReferences do - yield "-r:" + project.Solution.GetProject(x.ProjectId).OutputFilePath - for x in project.MetadataReferences.OfType() do - yield "-r:" + x.FilePath + for projectReference in project.ProjectReferences do + yield "-r:" + project.Solution.GetProject(projectReference.ProjectId).OutputFilePath + + for metadataReference in project.MetadataReferences.OfType() do + yield "-r:" + metadataReference.FilePath |] else [||] @@ -318,7 +323,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) - let mutable debounceCts = null: CancellationTokenSource + let mutable debounceCts: CancellationTokenSource | null = null let disposeDebounceCts () = match Interlocked.Exchange(&debounceCts, null) with @@ -354,7 +359,15 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let onKillFocus (_) = updateProjectOptions () let onSetFocus (_) = updateProjectOptions () - let addToCacheAndSubscribe ({ Project = _; FileStamp = fileStamp; ParsingOptions = parsingOptions; ProjectOptions = projectOptions; Subscription = _ }: SingleFileCacheEntry) = + let addToCacheAndSubscribe + ({ + Project = _ + FileStamp = fileStamp + ParsingOptions = parsingOptions + ProjectOptions = projectOptions + Subscription = _ + }: SingleFileCacheEntry) + = let textViewSubscription = textViewAndCaret () |> ValueOption.bind (fun (textView, _) -> @@ -394,7 +407,14 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = return ValueSome(parsingOptions, projectOptions) - | true, { Project = oldProject; FileStamp = oldFileStamp; ParsingOptions = parsingOptions; ProjectOptions = projectOptions; Subscription = _ } -> + | true, + { + Project = oldProject + FileStamp = oldFileStamp + ParsingOptions = parsingOptions + ProjectOptions = projectOptions + Subscription = _ + } -> let! isInvalidated = if fileStamp <> oldFileStamp then CancellableTask.singleton true @@ -518,11 +538,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = checker.ClearCache(options, userOpName = "tryComputeOptions") - lastSuccessfulCompilations.ToArray() - |> Array.iter (fun pair -> + for pair in lastSuccessfulCompilations.ToArray() do if not (currentSolution.ContainsProject(pair.Key)) then lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore - emitCache.TryRemove(pair.Key) |> ignore) + emitCache.TryRemove(pair.Key) |> ignore checker.InvalidateConfiguration(projectOptions, userOpName = "tryComputeOptions") From 59ce1b205005b36c925989c25b6c9a5e7d6ad754 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 01:23:13 +0200 Subject: [PATCH 016/133] Perf: Add release notes (#20126) --- docs/release-notes/.VisualStudio/18.vNext.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index a0fe282be61..761f3a506aa 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -10,6 +10,8 @@ * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Avoid using `cancellableTask` in `DocumentCache`; the editor cache now uses direct `CancellationToken`-aware `task` wrappers, avoiding the background `Task.Run` offload and a larger wrapper closure from the `cancellableTask` builder. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) * Prevent stale and leaked per-document diagnostics by tightening cache validity and evicting entries for removed documents. ([PR #20121](https://github.com/dotnet/fsharp/pull/20121)) +* Reduce excessive metadata-only `Compilation.Emit` churn for C# project references in the IDE. ([Issue #20118](https://github.com/dotnet/fsharp/issues/20118), [PR #20119](https://github.com/dotnet/fsharp/pull/20119)) +* Avoid blocking the VS project-options mailbox and unnecessary script option recomputation while editing by making invalidation async and debouncing caret-driven updates. ([Issue #20124](https://github.com/dotnet/fsharp/issues/20124), [Issue #20125](https://github.com/dotnet/fsharp/issues/20125), [PR #20126](https://github.com/dotnet/fsharp/pull/20126)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) From 7370e6ccdc8a7044a45bfd93c784d2c41647727a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 20 Aug 2026 18:23:09 +0200 Subject: [PATCH 017/133] Switch project checker notification async block from `task` to `backgroundTask` --- .../LanguageService/FSharpProjectOptionsManager.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 552765f62b7..daa489e833c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -342,7 +342,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = previousCts.Cancel() previousCts.Dispose() - task { + backgroundTask { try do! Task.Delay(500, debounceToken) From 70970afd327332817d126dcc9c6bdc8532e5503c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 20 Aug 2026 19:36:13 +0200 Subject: [PATCH 018/133] Use ConditionalWeakTable for DocumentDiagnosticAnalyzer caches Replace ConcurrentDictionary + WorkspaceChanged eviction with two ConditionalWeakTable<Document, CachedDiagnosticsEntry> (syntax/semantic), keyed by Document snapshot identity matching ProjectCache.Projects idiom. Drop TextVersion/FilePath/GetTextVersionAsync. Keep ProjectVersion for Semantic. Builds/tests clean, no behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 3b24e4f9f68..d1663995bf9 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -3,16 +3,15 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition -open System.Collections.Concurrent open System.Collections.Immutable open System.Collections.Generic +open System.Runtime.CompilerServices open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics -open Microsoft.VisualStudio.LanguageServices open FSharp.Compiler.Diagnostics open CancellableTasks @@ -25,45 +24,19 @@ type internal DiagnosticsType = type private CachedDiagnosticsEntry = { - TextVersion: VersionStamp ProjectVersion: VersionStamp - FilePath: string IsRemoveParensEnabled: bool Diagnostics: ImmutableArray } [); Shared>] -type internal FSharpDocumentDiagnosticAnalyzer [] ([] workspace: VisualStudioWorkspace | null) = +type internal FSharpDocumentDiagnosticAnalyzer [] () = let shouldProduceDiagnostics (document: Document) = document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics() - static let cache = - ConcurrentDictionary() - - static let evictDocument (documentId: DocumentId) = - cache.TryRemove(struct (documentId, DiagnosticsType.Syntax)) |> ignore - cache.TryRemove(struct (documentId, DiagnosticsType.Semantic)) |> ignore - - static let evictProject (projectId: ProjectId) = - for entry in cache do - let struct (documentId, _) = entry.Key - - if documentId.ProjectId = projectId then - cache.TryRemove(entry.Key) |> ignore - - do - match workspace with - | null -> () - | workspace -> - workspace.WorkspaceChanged.Add(fun args -> - match args.Kind with - | WorkspaceChangeKind.DocumentRemoved when not (isNull args.DocumentId) -> evictDocument args.DocumentId - | WorkspaceChangeKind.ProjectRemoved when not (isNull args.ProjectId) -> evictProject args.ProjectId - | WorkspaceChangeKind.SolutionCleared - | WorkspaceChangeKind.SolutionRemoved -> cache.Clear() - | _ -> () - ) + static let syntaxCache = ConditionalWeakTable() + static let semanticCache = ConditionalWeakTable() static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -110,8 +83,6 @@ type internal FSharpDocumentDiagnosticAnalyzer [] ([ CancellableTask.singleton VersionStamp.Default @@ -124,14 +95,15 @@ type internal FSharpDocumentDiagnosticAnalyzer [] ([ document.Project.IsFsharpRemoveParensEnabled | DiagnosticsType.Semantic -> false - let cacheKey = struct (document.Id, diagnosticType) + let cache = + match diagnosticType with + | DiagnosticsType.Syntax -> syntaxCache + | DiagnosticsType.Semantic -> semanticCache let cached = - match cache.TryGetValue(cacheKey) with + match cache.TryGetValue document with | true, cachedEntry when - cachedEntry.TextVersion = textVersion - && cachedEntry.ProjectVersion = projectVersion - && cachedEntry.FilePath = filePath + cachedEntry.ProjectVersion = projectVersion && cachedEntry.IsRemoveParensEnabled = isRemoveParensEnabled -> ValueSome cachedEntry.Diagnostics @@ -196,14 +168,16 @@ type internal FSharpDocumentDiagnosticAnalyzer [] ([ ignore + + cache.Add( + document, { - TextVersion = textVersion ProjectVersion = projectVersion - FilePath = filePath IsRemoveParensEnabled = isRemoveParensEnabled Diagnostics = result } + ) return result } From 2207832dbb0c4fb038f9be6a4e47901de22fc0c7 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 20 Aug 2026 19:37:20 +0200 Subject: [PATCH 019/133] fixup! Fix NRE on unlinked placeholder entities and add release notes --- src/Compiler/Utilities/lib.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Utilities/lib.fs b/src/Compiler/Utilities/lib.fs index 7a53f1993e7..bb8e5220f00 100755 --- a/src/Compiler/Utilities/lib.fs +++ b/src/Compiler/Utilities/lib.fs @@ -500,7 +500,7 @@ module WeakMap = let value = valueFactory key if shouldCache value then #if NETSTANDARD2_0 - try table.Add(key, value) with _ -> () + try table.Add(key, value) with | :? ArgumentException -> () #else table.TryAdd(key, value) |> ignore #endif From e6f0fdc99ce0ce5c0c3ddba2764c47bffedced87 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 31 Aug 2026 01:02:44 +0200 Subject: [PATCH 020/133] Rent the exact UTF8 byte count rather than the upper bound Encoding.UTF8.GetMaxByteCount(n) returns 3n+3, which crosses ArrayPool.Shared's 1 MB limit at 349,525 characters. Past that the pool cannot serve or reclaim the rental, so it allocates a fresh array roughly three times the size of the file on every call and drops it on Return -- worse than the plain GetBytes allocation this replaced. Over the 402 sources of one FSharp.Compiler.Service compile that is 3 unpooled rentals totalling 4971 KB; sizing the rental with GetByteCount brings it to 0 and keeps every rental two buckets lower. Co-Authored-By: Claude Opus 5 (1M context) --- src/Compiler/Facilities/Hashing.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Facilities/Hashing.fs b/src/Compiler/Facilities/Hashing.fs index 90de010c67d..89dd6af9833 100644 --- a/src/Compiler/Facilities/Hashing.fs +++ b/src/Compiler/Facilities/Hashing.fs @@ -60,11 +60,11 @@ module internal Md5Hasher = /// a byte array the size of the input string on every call. let hashStringInto (s: string) (destination: Span) = let encoding = System.Text.Encoding.UTF8 - let maxByteCount = encoding.GetMaxByteCount(s.Length) - let rented = System.Buffers.ArrayPool.Shared.Rent(maxByteCount) + let byteCount = encoding.GetByteCount(s) + let rented = System.Buffers.ArrayPool.Shared.Rent(byteCount) try - let byteCount = encoding.GetBytes(s, 0, s.Length, rented, 0) + encoding.GetBytes(s, 0, s.Length, rented, 0) |> ignore #if NETSTANDARD2_0 let hash = md5.Value.ComputeHash(rented, 0, byteCount) hash.CopyTo(destination) From 6cc7cb066dc38121879702bfa287eda454de75a9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 30 Aug 2026 23:10:48 +0200 Subject: [PATCH 021/133] Address review: weak DocumentId cache, complete validity key, race-free update - Key the caches by DocumentId instead of Document, so entries survive solution snapshots while the weak table still drops them when a document leaves the solution - no eviction pass on the request path. - Cache validity now covers everything the diagnostics depend on: file path (locations embed it, so a rename keeping the DocumentId invalidates), text version, project version (parsing options for syntax, dependent version for semantics) and the RemoveParens setting. - Replace the non-atomic Remove + Add with a locked update; ConditionalWeakTable has no atomic replace on .NET Framework and the pair races between concurrent syntax/semantic passes over the same document. - Semantic pass reuses the parse results returned by GetFSharpParseAndCheckResultsAsync instead of parsing a second time. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 47 +++++++++++++------ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 761f3a506aa..b12e3d5b25b 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -12,6 +12,7 @@ * Prevent stale and leaked per-document diagnostics by tightening cache validity and evicting entries for removed documents. ([PR #20121](https://github.com/dotnet/fsharp/pull/20121)) * Reduce excessive metadata-only `Compilation.Emit` churn for C# project references in the IDE. ([Issue #20118](https://github.com/dotnet/fsharp/issues/20118), [PR #20119](https://github.com/dotnet/fsharp/pull/20119)) * Avoid blocking the VS project-options mailbox and unnecessary script option recomputation while editing by making invalidation async and debouncing caret-driven updates. ([Issue #20124](https://github.com/dotnet/fsharp/issues/20124), [Issue #20125](https://github.com/dotnet/fsharp/issues/20125), [PR #20126](https://github.com/dotnet/fsharp/pull/20126)) +* Cache document diagnostics by version stamp, so an unchanged document is not reanalyzed on every crawler pass. ([Issue #20120](https://github.com/dotnet/fsharp/issues/20120), [PR #20121](https://github.com/dotnet/fsharp/pull/20121)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index d1663995bf9..d2a0208fa83 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. namespace Microsoft.VisualStudio.FSharp.Editor +open System open System.Composition open System.Collections.Immutable open System.Collections.Generic @@ -22,8 +23,12 @@ type internal DiagnosticsType = | Syntax | Semantic -type private CachedDiagnosticsEntry = +[] +type private CachedDiagnostics = { + /// Diagnostic locations embed it, so a rename that keeps the DocumentId must still invalidate. + FilePath: string + TextVersion: VersionStamp ProjectVersion: VersionStamp IsRemoveParensEnabled: bool Diagnostics: ImmutableArray @@ -35,8 +40,13 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = let shouldProduceDiagnostics (document: Document) = document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics() - static let syntaxCache = ConditionalWeakTable() - static let semanticCache = ConditionalWeakTable() + // DocumentId keys survive solution snapshots, and holding them weakly means a document dropped from the + // solution takes its cached diagnostics with it, so no eviction pass is needed on the request path. + static let syntaxCache = ConditionalWeakTable() + static let semanticCache = ConditionalWeakTable() + + // ConditionalWeakTable has no atomic replace on .NET Framework, and Remove + Add races with a concurrent pass. + static let cacheGate = obj () static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -85,9 +95,12 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = let! projectVersion = match diagnosticType with - | DiagnosticsType.Syntax -> CancellableTask.singleton VersionStamp.Default + // Syntax diagnostics depend on the parsing options (defines, langversion), which the project version covers. + | DiagnosticsType.Syntax -> CancellableTask.singleton document.Project.Version | DiagnosticsType.Semantic -> (fun ct -> document.Project.GetDependentVersionAsync(ct)) + let! textVersion = document.GetTextVersionAsync(ct) + let filePath = document.FilePath let isRemoveParensEnabled = @@ -101,9 +114,11 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = | DiagnosticsType.Semantic -> semanticCache let cached = - match cache.TryGetValue document with + match cache.TryGetValue document.Id with | true, cachedEntry when - cachedEntry.ProjectVersion = projectVersion + String.Equals(cachedEntry.FilePath, filePath, StringComparison.Ordinal) + && cachedEntry.TextVersion = textVersion + && cachedEntry.ProjectVersion = projectVersion && cachedEntry.IsRemoveParensEnabled = isRemoveParensEnabled -> ValueSome cachedEntry.Diagnostics @@ -117,15 +132,15 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = let errors = HashSet(diagnosticEqualityComparer) - let! parseResults = document.GetFSharpParseResultsAsync("GetDiagnostics") - match diagnosticType with | DiagnosticsType.Syntax -> + let! parseResults = document.GetFSharpParseResultsAsync("GetDiagnostics") + for diagnostic in parseResults.Diagnostics do errors.Add(diagnostic) |> ignore | DiagnosticsType.Semantic -> - let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync("GetDiagnostics") + let! parseResults, checkResults = document.GetFSharpParseAndCheckResultsAsync("GetDiagnostics") for diagnostic in checkResults.Diagnostics do errors.Add(diagnostic) |> ignore @@ -168,16 +183,18 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = iab.AddRange unnecessaryParentheses iab.ToImmutable() - cache.Remove(document) |> ignore - - cache.Add( - document, + let entry = { + FilePath = filePath + TextVersion = textVersion ProjectVersion = projectVersion IsRemoveParensEnabled = isRemoveParensEnabled Diagnostics = result } - ) + + lock cacheGate (fun () -> + cache.Remove document.Id |> ignore + cache.Add(document.Id, entry)) return result } From 257642bf4d45e1c6a1270efda7dd17d2e1245136 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 30 Aug 2026 23:39:40 +0200 Subject: [PATCH 022/133] Lock on the cache table itself instead of a separate gate object Both tables are private statics that are never handed out, and ConditionalWeakTable takes its own private lock internally, so locking the table is safe and gives the syntax and semantic caches independent locks instead of serialising their writes through one shared object. Co-Authored-By: Claude Opus 5 (1M context) --- .../FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index d2a0208fa83..3e487b57a8c 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -45,9 +45,6 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = static let syntaxCache = ConditionalWeakTable() static let semanticCache = ConditionalWeakTable() - // ConditionalWeakTable has no atomic replace on .NET Framework, and Remove + Add races with a concurrent pass. - static let cacheGate = obj () - static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -192,7 +189,8 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = Diagnostics = result } - lock cacheGate (fun () -> + // net472 has no ConditionalWeakTable.AddOrUpdate, and Remove + Add races with a concurrent pass. + lock cache (fun () -> cache.Remove document.Id |> ignore cache.Add(document.Id, entry)) From 5add4c951a571901f7014a0b45038d3d2575693c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 29 Aug 2026 01:55:11 +0200 Subject: [PATCH 023/133] Dispose single-file cache subscriptions exactly once `ClearAllCaches` disposed every entry by iterating `singleFileCache.Values` and then called `Clear()`. The agent loop disposes the entries it removes via `TryRemove`, so the same entry could be disposed twice: the second `cp.Unadvise` throws, and the message loop has no `try/with` around it, so the loop died for the rest of the VS session and project options silently stopped updating. Make `TryRemove` the single gate for disposal - `ClearAllCaches` now removes each entry and disposes only what it actually took ownership of. This also fixes a subscription leak: the old `Clear()` dropped any entry added between the `Values` snapshot and the `Clear()` without disposing it. Co-Authored-By: Claude Opus 5 (1M context) --- .../FSharpProjectOptionsManager.fs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index daa489e833c..6a6f97d7440 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -371,8 +371,12 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let textViewSubscription = textViewAndCaret () |> ValueOption.bind (fun (textView, _) -> - subscribeToTextViewEvents (textView, (ValueSome onChangeCaretHandler), (ValueSome onKillFocus), (ValueSome onSetFocus)) - ) + subscribeToTextViewEvents ( + textView, + (ValueSome onChangeCaretHandler), + (ValueSome onKillFocus), + (ValueSome onSetFocus) + )) let subscription = ValueSome @@ -678,8 +682,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = commandLineOptions.Clear() legacyProjectSites.Clear() cache.Clear() - singleFileCache.Values |> Seq.iter disposeSingleFileCacheEntry - singleFileCache.Clear() + // Dispose only the entries we actually removed: the subscription is not idempotent + // and the agent loop disposes what it removes on its own. + for entry in singleFileCache do + match singleFileCache.TryRemove(entry.Key) with + | true, cacheEntry -> disposeSingleFileCacheEntry cacheEntry + | _ -> () + lastSuccessfulCompilations.Clear() emitCache.Clear() From 99dbe6218af0e5d821dff73fe7822b32bea3b24a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 29 Aug 2026 02:07:47 +0200 Subject: [PATCH 024/133] Make text view event subscriptions idempotent to dispose `IConnectionPoint.Unadvise` throws when called twice with the same cookie, so a duplicate `Dispose()` on a subscription escalates into an exception on whatever thread performed it. Guard the cookie with an interlocked flag so the second and later disposals are no-ops. Defence in depth on top of the `TryRemove`-gated disposal in `FSharpProjectOptionsReactor`: ownership still decides who disposes, this just keeps a future double-dispose from being fatal. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/FSharp.Editor/Common/Extensions.fs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 75e26983f9e..4057a079af3 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -110,11 +110,18 @@ let subscribeToTextViewEvents (textView: IVsTextView, onChangeCaretHandler, onKi match cpContainer.FindConnectionPoint(ref riid) with | null -> ValueNone | cp -> - ValueSome( - cp.Advise(handler, &cookie) + cp.Advise(handler, &cookie) + + // `Unadvise` is not idempotent: a second call with the same cookie throws. + // Guard it so that a duplicate `Dispose()` is a no-op rather than an exception + // surfacing on whatever thread happened to dispose the subscription twice. + let unadvised = ref 0 + ValueSome( { new IDisposable with - member _.Dispose() = cp.Unadvise(cookie) + member _.Dispose() = + if Interlocked.Exchange(&unadvised.contents, 1) = 0 then + cp.Unadvise(cookie) } ) | _ -> ValueNone From 290d5771ed75865444dc2ad3402c69453e5dc828 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 31 Aug 2026 23:03:16 +0200 Subject: [PATCH 025/133] Offer F# declarations to the Copilot chat "#" mention picker Copilot's built-in symbol provider reads symbols off the Roslyn compilation, which F# projects do not have, so F# declarations never appeared in the picker shown for "#". Proffer a brokered service from FSharp.Editor implementing Copilot's context-provider and mention-queryable contracts. Declarations come from the NavigateTo parse-tree cache, so the picker answers without waiting for a project check; that cache moves into a shared FSharpNavigableItemsCache used by both features. A picked mention resolves by fully qualified name against the current solution, so it survives a file moving, and carries the whole declaration - doc comment included - as its snippet. FSharpPackage now registers the provider moniker with Copilot after package load. The override is no longer DEBUG-only, so it calls its base implementation, which registers the editor factories. Co-Authored-By: Claude Fable 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + eng/Packages.props | 3 + .../src/FSharp.Editor/Common/Constants.fs | 5 + .../Copilot/CopilotContextProvider.fs | 327 ++++++++++++++++++ .../Copilot/CopilotSymbolMapping.fs | 62 ++++ .../Copilot/CopilotSymbolSnippets.fs | 40 +++ .../src/FSharp.Editor/FSharp.Editor.fsproj | 4 + .../LanguageService/LanguageService.fs | 40 ++- .../Navigation/NavigateToSearchService.fs | 72 ++-- .../CopilotContextProviderTests.fs | 110 ++++++ .../FSharp.Editor.Tests.fsproj | 1 + 11 files changed, 633 insertions(+), 32 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs create mode 100644 vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs create mode 100644 vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index b12e3d5b25b..3cef90bc875 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,6 +2,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) +* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ### Fixed diff --git a/eng/Packages.props b/eng/Packages.props index c6b2eabb7a2..5c5405d922d 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -64,6 +64,9 @@ ComponentModelHost would otherwise stay at 17.x; that 17.x/18.x split makes S/IComponentModel ambiguous (CS0433). Pin to the SDK 18.9.496 version so those types resolve to a single assembly. --> + + diff --git a/vsintegration/src/FSharp.Editor/Common/Constants.fs b/vsintegration/src/FSharp.Editor/Common/Constants.fs index ead451467cf..d0f493af6df 100644 --- a/vsintegration/src/FSharp.Editor/Common/Constants.fs +++ b/vsintegration/src/FSharp.Editor/Common/Constants.fs @@ -43,6 +43,11 @@ module internal FSharpConstants = /// "F# Language Service" let FSharpLanguageServiceCallbackName = "F# Language Service" + [] + /// Brokered service offering F# declarations to the Copilot chat "#" mention picker. + let copilotSymbolProviderName = + "Microsoft.VisualStudio.FSharp.CopilotSymbolContextProvider" + [] /// "FSharp" let FSharpLanguageLongName = "FSharp" diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs new file mode 100644 index 00000000000..c11ac6f22ad --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Collections.Generic +open System.ComponentModel.Composition +open System.IO +open System.Threading.Tasks + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation +open Microsoft.CodeAnalysis.Text +open Microsoft.ServiceHub.Framework +open Microsoft.VisualStudio.Copilot +open Microsoft.VisualStudio.LanguageServices +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.ServiceBroker + +open FSharp.Compiler.EditorServices +open CancellableTasks + +/// Solution-wide lookup of F# declarations behind the Copilot chat "#" mention picker. +/// Kept apart from the brokered service so it can be exercised without a Visual Studio workspace. +module internal CopilotSymbolQuery = + + [] + let private MaxMentions = 20 + + /// Overloads and partial definitions share one fully qualified name; a handful of them is plenty of context. + [] + let private MaxDeclarations = 4 + + [] + let private UserOpName = "CopilotSymbolContext" + + let private fsharpDocuments (solution: Solution) = + seq { + for project in solution.Projects do + if project.Language = FSharpConstants.FSharpLanguageName then + yield! project.Documents + } + + let describe (item: NavigableItem) (document: Document) = + let container = + match item.Container.FullName with + | "" -> Path.GetFileName document.FilePath + | name -> name + + if document.IsFSharpSignatureFile then + $"signature, {container} - {document.Project.Name}" + else + $"{container} - {document.Project.Name}" + + /// Declarations whose fully qualified name matches `searchText`, best match first, one entry per name. + let search (cache: FSharpNavigableItemsCache) (solution: Solution) (searchText: string) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let tryMatch = cache.CreateMatcherFor searchText + let hits = ResizeArray() + + for document in fsharpDocuments solution do + ct.ThrowIfCancellationRequested() + let! items = cache.GetNavigableItems document + + for item in items do + match tryMatch item with + | ValueSome patternMatch -> hits.Add(struct (patternMatch.Kind, item, document)) + | ValueNone -> () + + return + hits + |> Seq.sortBy (fun (struct (kind, item: NavigableItem, document: Document)) -> + document.IsFSharpSignatureFile, kind, item.Name.Length) + |> Seq.distinctBy (fun (struct (_, item, _)) -> CopilotSymbolMapping.fullyQualifiedName item) + |> Seq.truncate MaxMentions + |> Seq.map (fun (struct (_, item, document)) -> struct (item, document)) + |> Seq.toArray + } + + /// Declarations carrying exactly this fully qualified name. Signature files answer only when no + /// implementation declares the name. + let declarationsOf (cache: FSharpNavigableItemsCache) (solution: Solution) (fullyQualifiedName: string) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let hits = ResizeArray() + + for document in fsharpDocuments solution do + ct.ThrowIfCancellationRequested() + let! items = cache.GetNavigableItems document + + for item in items do + if String.Equals(CopilotSymbolMapping.fullyQualifiedName item, fullyQualifiedName, StringComparison.Ordinal) then + hits.Add(struct (item, document)) + + let implementations = + hits + |> Seq.filter (fun (struct (_, document: Document)) -> not document.IsFSharpSignatureFile) + + let preferred = + if Seq.isEmpty implementations then + hits :> _ seq + else + implementations + + return preferred |> Seq.truncate MaxDeclarations |> Seq.toArray + } + + /// The source of the whole declaration `item` names, together with the span it occupies. + let snippetOf (item: NavigableItem) (document: Document) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync ct + let! parseResults = document.GetFSharpParseResultsAsync UserOpName + + let sourceLines = + Array.init sourceText.Lines.Count (fun line -> sourceText.Lines[line].ToString()) + + let scopes = Structure.getOutliningRanges sourceLines parseResults.ParseTree + let firstLine, lastLine = CopilotSymbolSnippets.definitionLines scopes item + + let firstLine = max 1 firstLine + let lastLine = min sourceText.Lines.Count lastLine + + let span = + TextSpan.FromBounds(sourceText.Lines[firstLine - 1].Start, sourceText.Lines[lastLine - 1].End) + + return struct (sourceText.GetSubText(span).ToString(), span) + } + + let symbolContext (cache: FSharpNavigableItemsCache) (solution: Solution) (fullyQualifiedName: string) = + cancellableTask { + let! declarations = declarationsOf cache solution fullyQualifiedName + + match Array.tryHead declarations with + | None -> return ValueNone + | Some(struct (first, _)) -> + let snippets = ResizeArray() + let locations = ResizeArray() + + for struct (item, document) in declarations do + let! struct (text, span) = snippetOf item document + snippets.Add text + locations.Add(SnippetLocation(document.FilePath, CopilotSpan(span.Start, span.Length))) + + return + ValueSome( + CopilotSymbolContext( + fullyQualifiedName, + first.Name, + String.Join(Environment.NewLine + Environment.NewLine, snippets), + CopilotSymbolMapping.symbolContextType first.Kind, + locations.ToArray() + ) + ) + } + +/// Offers F# declarations to Copilot chat, which merges them into the picker shown for "#". +/// Copilot's own symbol provider reads the Roslyn compilation, which F# projects do not have. +[; typeof |], + Audience = (ServiceAudience.PublicSdk ||| ServiceAudience.Local))>] +type internal FSharpCopilotContextProvider + [] + (cache: FSharpNavigableItemsCache, [] workspace: VisualStudioWorkspace) = + + static let moniker = + ServiceMoniker(FSharpConstants.copilotSymbolProviderName, Version CopilotDescriptors.CurrentContextProviderVersion) + + static let descriptor = + CopilotContextDescriptor( + CopilotSymbolMapping.SymbolMember, + "An F# type, module, member or value declared in the current solution.", + CopilotDefaultTypes.SymbolContextName, + [| + CopilotInputDescriptor( + CopilotSymbolMapping.FullyQualifiedNameInput, + "Fully qualified name of the F# declaration.", + CopilotDefaultTypes.StringName, + IsRequired = true + ) + |] + ) + + static let members = [| descriptor |] :> IReadOnlyList + + static let memberNames = [| CopilotSymbolMapping.SymbolMember |] :> IReadOnlyList + + static let noMentions = + Array.empty :> IReadOnlyCollection + + let mentionFor (item: NavigableItem) (document: Document) = + let inputs = Dictionary(StringComparer.Ordinal) + + inputs[CopilotSymbolMapping.FullyQualifiedNameInput] <- + CopilotValue(CopilotDefaultTypes.StringName, CopilotSymbolMapping.fullyQualifiedName item) + + let description = CopilotSymbolQuery.describe item document + + CopilotQueriedContextMention( + moniker, + descriptor, + inputs, + item.Name, + Description = description, + Tooltip = description, + Icon = Nullable(CopilotSymbolMapping.icon item.Kind), + IsNavigable = true + ) + :> CopilotQueriedMention + + /// The user is still typing, so the trailing input is the search text. It is preceded by the member + /// name once the mention has been committed, as in "#fsharpSymbol:Namespace.Type". + let searchTextOf (query: CopilotMentionQuery) = + match query.Type, query.Inputs with + | CopilotMentionType.Context, null -> ValueNone + | CopilotMentionType.Context, inputs when inputs.Count > 0 -> + match inputs[inputs.Count - 1] with + | text when String.IsNullOrWhiteSpace text -> ValueNone + | text when String.Equals(text, CopilotSymbolMapping.SymbolMember, StringComparison.Ordinal) -> ValueNone + | text -> ValueSome text + | _ -> ValueNone + + let queryMentions (query: CopilotMentionQuery) = + cancellableTask { + match workspace, searchTextOf query with + | null, _ + | _, ValueNone -> return noMentions + | workspace, ValueSome searchText -> + let! hits = CopilotSymbolQuery.search cache workspace.CurrentSolution searchText + + return + hits |> Array.map (fun (struct (item, document)) -> mentionFor item document) + :> IReadOnlyCollection + } + + let fullyQualifiedNameOf (inputs: IReadOnlyDictionary) = + match inputs with + | null -> ValueNone + | inputs -> + match inputs.TryGetValue CopilotSymbolMapping.FullyQualifiedNameInput with + | true, value -> + match value.TryGetValue() with + | true, name when not (String.IsNullOrWhiteSpace name) -> ValueSome name + | _ -> ValueNone + | _ -> ValueNone + + interface IExportedBrokeredService with + member _.Descriptor = CopilotDescriptors.CreateContextProviderDescriptor moniker + + member _.InitializeAsync _cancellationToken = Task.CompletedTask + + interface ICopilotContextReducer with + member _.ReduceAsync(context, _reduction, _counter, _cancellationToken) = Task.FromResult context + + interface ICopilotContextProvider with + member _.GetMembersAsync _cancellationToken = + ValueTask> members + + member _.GetMembersAsync(_requestId, _cancellationToken) = Task.FromResult memberNames + + member _.StoreAsync(_requestId, _cancellationToken) = ValueTask() + + member _.ReleaseAsync(_requestId, _cancellationToken) = ValueTask() + + member _.GetContextAsync(requestId, memberName, inputs, cancellationToken) : Task = + match workspace, fullyQualifiedNameOf inputs with + | null, _ + | _, ValueNone -> Task.FromResult null + | workspace, ValueSome fullyQualifiedName when + String.Equals(memberName, CopilotSymbolMapping.SymbolMember, StringComparison.Ordinal) + -> + cancellableTask { + let! symbol = CopilotSymbolQuery.symbolContext cache workspace.CurrentSolution fullyQualifiedName + + match symbol with + | ValueNone -> return null + | ValueSome symbol -> return CopilotContext(moniker, descriptor, requestId, symbol, CanReduce = false) + } + |> CancellableTask.start cancellationToken + | _ -> Task.FromResult null + + interface ICopilotMentionQueryable with + member _.QueryMentionAsync(query, cancellationToken) : Task> = + queryMentions query |> CancellableTask.start cancellationToken + + member _.NavigateToMentionableAsync(mention, cancellationToken) : Task = + match workspace, fullyQualifiedNameOf mention.Inputs with + | null, _ + | _, ValueNone -> Task.FromResult false + | workspace, ValueSome fullyQualifiedName -> + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let solution = workspace.CurrentSolution + let! declarations = CopilotSymbolQuery.declarationsOf cache solution fullyQualifiedName + + match Array.tryHead declarations with + | None -> return false + | Some(struct (item, document)) -> + let! sourceText = document.GetTextAsync ct + + match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) with + | ValueNone -> return false + | ValueSome span -> + do! ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync ct + + let navigation = + solution.Workspace.Services.GetService() + + return navigation.TryNavigateToSpan(solution.Workspace, document.Id, span, ct) + } + |> CancellableTask.start cancellationToken + + // Copilot's own picker providers answer through the batch interface, one result collection per query. + interface ICopilotMentionBatchQueryable with + member _.QueryMentionBatchAsync(queries, cancellationToken) : Task>> = + cancellableTask { + let results = ResizeArray queries.Count + + for query in queries do + let! mentions = queryMentions query + results.Add mentions + + return results :> IReadOnlyList> + } + |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs new file mode 100644 index 00000000000..b155ccdc1d8 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open Microsoft.VisualStudio.Copilot +open Microsoft.VisualStudio.Imaging + +open FSharp.Compiler.EditorServices + +/// Translates F# navigable items into the shapes the Copilot chat "#" mention picker understands. +module internal CopilotSymbolMapping = + + /// Name of the context member. It becomes the mention prefix the user sees and re-types, + /// as in "#fsharpSymbol:Namespace.Type.Member". + [] + let SymbolMember = "fsharpSymbol" + + [] + let FullyQualifiedNameInput = "fullyQualifiedName" + + /// The parse tree cannot tell an interface, struct or record apart from a plain class, so every + /// type-like declaration is reported as a class. + let symbolContextType kind = + match kind with + | NavigableItemKind.Module + | NavigableItemKind.ModuleAbbreviation + | NavigableItemKind.Exception + | NavigableItemKind.Type -> CopilotSymbolContextType.Class + | NavigableItemKind.ModuleValue -> CopilotSymbolContextType.Function + | NavigableItemKind.Field + | NavigableItemKind.Property -> CopilotSymbolContextType.Field + | NavigableItemKind.Constructor + | NavigableItemKind.Member -> CopilotSymbolContextType.Method + | NavigableItemKind.EnumCase -> CopilotSymbolContextType.Constant + | NavigableItemKind.UnionCase -> CopilotSymbolContextType.Union + + let private imageId kind = + match kind with + | NavigableItemKind.Module + | NavigableItemKind.ModuleAbbreviation -> KnownImageIds.ModulePublic + | NavigableItemKind.Exception -> KnownImageIds.ExceptionPublic + | NavigableItemKind.Type -> KnownImageIds.ClassPublic + | NavigableItemKind.ModuleValue + | NavigableItemKind.Constructor + | NavigableItemKind.Member -> KnownImageIds.MethodPublic + | NavigableItemKind.Field -> KnownImageIds.FieldPublic + | NavigableItemKind.Property -> KnownImageIds.PropertyPublic + | NavigableItemKind.EnumCase + | NavigableItemKind.UnionCase -> KnownImageIds.EnumerationItemPublic + + let icon kind = + let mutable moniker = CopilotImageMoniker() + moniker.Guid <- KnownImageIds.ImageCatalogGuid + moniker.Id <- imageId kind + moniker + + /// Dotted path that both drives the picker's pattern matching and identifies a picked mention + /// when it is resolved back to source. + let fullyQualifiedName (item: NavigableItem) = + match item.Container.FullName with + | "" -> item.Name + | container -> $"{container}.{item.Name}" diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs new file mode 100644 index 00000000000..a3c2b4b58e1 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open FSharp.Compiler.EditorServices + +/// Widens the identifier range of a navigable item to the declaration a reader would recognise. +module internal CopilotSymbolSnippets = + + /// A module scope can span a whole file, which is more than a chat prompt can usefully carry. + [] + let MaxSnippetLines = 200 + + /// Inclusive, 1-based line bounds of the declaration `item` names, including its doc comment. + let definitionLines (scopes: Structure.ScopeRange seq) (item: NavigableItem) = + let declarationLine = item.Range.StartLine + + // A construct's outlining range reaches back over the doc comment in front of it, so it is the + // collapse range - the body proper - that tells which construct is declared on this line. + let declaredHere (scope: Structure.ScopeRange) = + scope.CollapseRange.StartLine = declarationLine + && scope.Range.EndLine >= item.Range.EndLine + && scope.Scope <> Structure.Scope.Comment + && scope.Scope <> Structure.Scope.XmlDocComment + + let mutable widest = ValueNone + + for scope in scopes do + if declaredHere scope then + match widest with + | ValueSome(previous: Structure.ScopeRange) when previous.Range.EndLine >= scope.Range.EndLine -> () + | _ -> widest <- ValueSome scope + + // A one-line member declares no scope of its own; it stands for itself rather than for the type around it. + let firstLine, lastLine = + match widest with + | ValueSome scope -> scope.Range.StartLine, scope.Range.EndLine + | ValueNone -> declarationLine, item.Range.EndLine + + firstLine, min lastLine (firstLine + MaxSnippetLines - 1) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..3176e1b964c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -94,6 +94,9 @@ + + + @@ -179,6 +182,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 427baf0c6ab..995bb2d56c1 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -6,6 +6,7 @@ open System open System.ComponentModel.Design open System.Runtime.InteropServices open System.Threading +open System.Threading.Tasks open System.IO open System.Collections.Immutable open Microsoft.CodeAnalysis @@ -13,13 +14,16 @@ open Microsoft.CodeAnalysis.Options open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.NativeInterop +open Microsoft.ServiceHub.Framework open Microsoft.VisualStudio +open Microsoft.VisualStudio.Copilot open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.LanguageServices open Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.Shell.ServiceBroker open Microsoft.VisualStudio.Text.Outlining open Microsoft.CodeAnalysis.ExternalAccess.FSharp open Microsoft.CodeAnalysis.Host.Mef @@ -408,8 +412,42 @@ type internal FSharpPackage() as this = |> CancellableTask.startAsTask cancellationToken) ) + override this.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks: PackageLoadTasks) = + base.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks) + + afterPackageLoadedTasks.AddTask( + false, + fun _ cancellationToken -> + task { + let! container = this.GetServiceAsync(typeof) + + match container with + | :? IBrokeredServiceContainer as container -> + // The Interactions service also serves the registration interface. It is absent when + // GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker. + let! registration = + container + .GetFullAccessServiceBroker() + .GetProxyAsync(CopilotDescriptors.InteractionService, cancellationToken) + + use registration = registration + + match registration with + | null -> () + | registration -> + let moniker = + ServiceMoniker( + FSharpConstants.copilotSymbolProviderName, + Version CopilotDescriptors.CurrentContextProviderVersion + ) + + do! registration.RegisterContextProviderAsync(moniker, cancellationToken) + | _ -> () + } + :> Task + ) + #if DEBUG - override _.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks: PackageLoadTasks) = afterPackageLoadedTasks.AddTask( false, fun _ _ -> diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index 546b00e1b16..b3273e36a19 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -19,8 +19,10 @@ open Microsoft.VisualStudio.Text.PatternMatching open FSharp.Compiler.EditorServices open CancellableTasks -[); Shared>] -type internal FSharpNavigateToSearchService +/// Parse-tree navigable items per document, cached on the document's text version. +/// Shared by NavigateTo and by the Copilot chat mention provider. +[] +type internal FSharpNavigableItemsCache [] (patternMatcherFactory: IPatternMatcherFactory, [] workspace: VisualStudioWorkspace) = @@ -33,7 +35,7 @@ type internal FSharpNavigateToSearchService if e.NewSolution.Id <> e.OldSolution.Id then cache.Clear() - let getNavigableItems (document: Document) = + member _.GetNavigableItems(document: Document) = cancellableTask { let! ct = CancellableTask.getCancellationToken () let! currentVersion = document.GetTextVersionAsync(ct) @@ -41,12 +43,45 @@ type internal FSharpNavigateToSearchService match cache.TryGetValue document.Id with | true, (version, items) when version = currentVersion -> return items | _ -> - let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigateToSearchService)) + let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigableItemsCache)) let items = NavigateTo.GetNavigableItems parseResults.ParseTree cache[document.Id] <- currentVersion, items return items } + member _.CreateMatcherFor(searchPattern: string) = + let patternMatcher = + patternMatcherFactory.CreatePatternMatcher( + searchPattern, + PatternMatcherCreationOptions( + cultureInfo = CultureInfo.CurrentUICulture, + flags = PatternMatcherCreationFlags.AllowFuzzyMatching, + containerSplitCharacters = [ '.' ] + ) + ) + + fun (item: NavigableItem) -> + // PatternMatcher will not match operators and some backtick escaped identifiers. + // To handle them, we fall back to simple substring match. + let name = item.Name + + if item.NeedsBackticks then + match name.IndexOf(searchPattern, StringComparison.CurrentCultureIgnoreCase) with + | i when i > 0 -> ValueSome(PatternMatch(PatternMatchKind.Substring, false, false)) + | 0 when name.Length = searchPattern.Length -> ValueSome(PatternMatch(PatternMatchKind.Exact, false, false)) + | 0 -> ValueSome(PatternMatch(PatternMatchKind.Prefix, false, false)) + | _ -> ValueNone + else + // full name with dots allows for path matching, e.g. + // "f.c.so.elseif" will match "Fantomas.Core.SyntaxOak.ElseIfNode" + patternMatcher.TryMatch $"{item.Container.FullName}.{name}" + |> ValueOption.ofNullable + +[); Shared>] +type internal FSharpNavigateToSearchService [] (itemsCache: FSharpNavigableItemsCache) = + + let getNavigableItems (document: Document) = itemsCache.GetNavigableItems document + let kindsProvided = ImmutableHashSet.Create( FSharpNavigateToItemKind.Module, @@ -115,33 +150,8 @@ type internal FSharpNavigateToSearchService | PatternMatchKind.Fuzzy -> FSharpNavigateToMatchKind.Fuzzy | _ -> FSharpNavigateToMatchKind.None - let createMatcherFor searchPattern = - let patternMatcher = - patternMatcherFactory.CreatePatternMatcher( - searchPattern, - PatternMatcherCreationOptions( - cultureInfo = CultureInfo.CurrentUICulture, - flags = PatternMatcherCreationFlags.AllowFuzzyMatching, - containerSplitCharacters = [ '.' ] - ) - ) - - fun (item: NavigableItem) -> - // PatternMatcher will not match operators and some backtick escaped identifiers. - // To handle them, we fall back to simple substring match. - let name = item.Name - - if item.NeedsBackticks then - match name.IndexOf(searchPattern, StringComparison.CurrentCultureIgnoreCase) with - | i when i > 0 -> ValueSome(PatternMatch(PatternMatchKind.Substring, false, false)) - | 0 when name.Length = searchPattern.Length -> ValueSome(PatternMatch(PatternMatchKind.Exact, false, false)) - | 0 -> ValueSome(PatternMatch(PatternMatchKind.Prefix, false, false)) - | _ -> ValueNone - else - // full name with dots allows for path matching, e.g. - // "f.c.so.elseif" will match "Fantomas.Core.SyntaxOak.ElseIfNode" - patternMatcher.TryMatch $"{item.Container.FullName}.{name}" - |> ValueOption.ofNullable + let createMatcherFor (searchPattern: string) = + itemsCache.CreateMatcherFor searchPattern let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet) (document: Document) = cancellableTask { diff --git a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs new file mode 100644 index 00000000000..cd20c8e2d4d --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Editor.Tests + +open System.Threading + +open Xunit + +open Microsoft.VisualStudio.Copilot +open Microsoft.VisualStudio.FSharp.Editor + +open FSharp.Editor.Tests.Helpers +open CancellableTasks + +module CopilotContextProviderTests = + + let fileContents = + """ +module Widgets + +/// Counts things that matter. +type Counter(start: int) = + let mutable value = start + + member _.Value = value + + member _.Bump() = + value <- value + 1 + value + +type Shape = + | Circle of radius: float + | Square of side: float + +let describeShape shape = + match shape with + | Circle r -> $"circle {r}" + | Square s -> $"square {s}" +""" + + let solution = RoslynTestHelpers.CreateSolution fileContents + + let private cache = + MefHelpers.createExportProvider().GetExportedValue() + + let private run computation = + computation |> CancellableTask.start CancellationToken.None |> _.Result + + let private search pattern = + CopilotSymbolQuery.search cache solution pattern + |> run + |> Array.map (fun (struct (item, _)) -> CopilotSymbolMapping.fullyQualifiedName item) + + let private symbolContext name = + CopilotSymbolQuery.symbolContext cache solution name |> run + + let private contextOf name = + match symbolContext name with + | ValueSome context -> context + | ValueNone -> failwith $"expected a symbol context for {name}" + + [] + [] + [] + [] + [] + let ``search finds a declaration by its fully qualified name`` (pattern: string, expected: string) = + Assert.Contains(expected, search pattern) + + [] + let ``search reports each declaration once`` () = + let names = search "Counter" + Assert.Equal((Array.distinct names).Length, names.Length) + + [] + let ``an unknown name has no context`` () = + Assert.True((symbolContext "Widgets.NoSuchThing").IsNone) + + [] + let ``a type context carries the whole declaration and its doc comment`` () = + let context = contextOf "Widgets.Counter" + + Assert.Equal("Widgets.Counter", context.FullyQualifiedName) + Assert.Equal("Counter", context.UnqualifiedName) + Assert.Contains("Counts things that matter.", context.Snippet) + Assert.Contains("member _.Bump()", context.Snippet) + + [] + let ``a member context carries the member body alone`` () = + let context = contextOf "Widgets.Counter.Bump" + + Assert.Contains("value <- value + 1", context.Snippet) + Assert.DoesNotContain("type Counter", context.Snippet) + + [] + [] + [] + [] + [] + [] + let ``declaration kinds map onto Copilot symbol types`` (name: string, expected: CopilotSymbolContextType) = + Assert.Equal(expected, (contextOf name).SymbolType) + + [] + let ``a context points back at the source it was taken from`` () = + let context = contextOf "Widgets.Counter" + let location = Assert.Single context.SnippetLocations + + Assert.Equal("C:\\test.fs", location.FilePath) + Assert.Equal(context.Snippet.Length, location.Span.Length) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index ecce1205b8c..eadb8905ab4 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -32,6 +32,7 @@ + From d2f14bebc6cb3754ae87d7ade76cd8903f39890a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 00:41:18 +0200 Subject: [PATCH 026/133] Parallelize Copilot symbol lookup and cut allocations on the hot cache path Sequential per-document scanning made "search" and "declarationsOf" as slow as the slowest single file; run them across documents concurrently instead, throttled the same way FindReferencesAsync throttles its per-document typechecks, so a solution-wide scan does not launch a parse per document all at once. FSharpNavigableItemsCache's version-stamp entries move to struct tuples and its null workspace check to a match, matching this repo's allocation and null-narrowing conventions on a path every keystroke in the mention picker hits. CopilotSymbolMapping collapses its wrapping module into a single qualified top-level module declaration. Co-Authored-By: Claude Fable 5 --- .../Copilot/CopilotContextProvider.fs | 79 +++++++++----- .../Copilot/CopilotSymbolMapping.fs | 103 +++++++++--------- .../Copilot/CopilotSymbolSnippets.fs | 64 ++++++----- .../LanguageService/LanguageService.fs | 14 +-- .../Navigation/NavigateToSearchService.fs | 51 ++++----- 5 files changed, 157 insertions(+), 154 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs index c11ac6f22ad..f63398e8c69 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs @@ -35,11 +35,9 @@ module internal CopilotSymbolQuery = let private UserOpName = "CopilotSymbolContext" let private fsharpDocuments (solution: Solution) = - seq { - for project in solution.Projects do - if project.Language = FSharpConstants.FSharpLanguageName then - yield! project.Documents - } + solution.Projects + |> Seq.where (fun project -> project.Language = FSharpConstants.FSharpLanguageName) + |> Seq.collect _.Documents let describe (item: NavigableItem) (document: Document) = let container = @@ -57,19 +55,28 @@ module internal CopilotSymbolQuery = cancellableTask { let! ct = CancellableTask.getCancellationToken () let tryMatch = cache.CreateMatcherFor searchText - let hits = ResizeArray() - for document in fsharpDocuments solution do - ct.ThrowIfCancellationRequested() - let! items = cache.GetNavigableItems document + let matchesIn (document: Document) = + cancellableTask { + ct.ThrowIfCancellationRequested() + let! items = cache.GetNavigableItems document + + return + items + |> Seq.chooseV (fun item -> + tryMatch item + |> ValueOption.map (fun patternMatch -> struct (patternMatch.Kind, item, document))) + } - for item in items do - match tryMatch item with - | ValueSome patternMatch -> hits.Add(struct (patternMatch.Kind, item, document)) - | ValueNone -> () + let! hits = + fsharpDocuments solution + |> Seq.map matchesIn + // Throttle to avoid launching a parse per document in the solution all at once. + |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) return hits + |> Seq.collect id |> Seq.sortBy (fun (struct (kind, item: NavigableItem, document: Document)) -> document.IsFSharpSignatureFile, kind, item.Name.Length) |> Seq.distinctBy (fun (struct (_, item, _)) -> CopilotSymbolMapping.fullyQualifiedName item) @@ -83,15 +90,29 @@ module internal CopilotSymbolQuery = let declarationsOf (cache: FSharpNavigableItemsCache) (solution: Solution) (fullyQualifiedName: string) = cancellableTask { let! ct = CancellableTask.getCancellationToken () - let hits = ResizeArray() - for document in fsharpDocuments solution do - ct.ThrowIfCancellationRequested() - let! items = cache.GetNavigableItems document + let matchesIn (document: Document) = + cancellableTask { + ct.ThrowIfCancellationRequested() + let! items = cache.GetNavigableItems document + + return + items + |> Seq.chooseV (fun item -> + if + String.Equals(CopilotSymbolMapping.fullyQualifiedName item, fullyQualifiedName, StringComparison.Ordinal) + then + ValueSome struct (item, document) + else + ValueNone) + } - for item in items do - if String.Equals(CopilotSymbolMapping.fullyQualifiedName item, fullyQualifiedName, StringComparison.Ordinal) then - hits.Add(struct (item, document)) + let! hits = + fsharpDocuments solution + |> Seq.map matchesIn + // Throttle to avoid launching a parse per document in the solution all at once. + |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) + |> CancellableTask.map (Seq.collect id) let implementations = hits @@ -117,7 +138,7 @@ module internal CopilotSymbolQuery = Array.init sourceText.Lines.Count (fun line -> sourceText.Lines[line].ToString()) let scopes = Structure.getOutliningRanges sourceLines parseResults.ParseTree - let firstLine, lastLine = CopilotSymbolSnippets.definitionLines scopes item + let struct (firstLine, lastLine) = CopilotSymbolSnippets.definitionLines scopes item let firstLine = max 1 firstLine let lastLine = min sourceText.Lines.Count lastLine @@ -132,9 +153,9 @@ module internal CopilotSymbolQuery = cancellableTask { let! declarations = declarationsOf cache solution fullyQualifiedName - match Array.tryHead declarations with - | None -> return ValueNone - | Some(struct (first, _)) -> + match Array.tryHeadV declarations with + | ValueNone -> return ValueNone + | ValueSome(struct (first, _)) -> let snippets = ResizeArray() let locations = ResizeArray() @@ -163,7 +184,7 @@ module internal CopilotSymbolQuery = Audience = (ServiceAudience.PublicSdk ||| ServiceAudience.Local))>] type internal FSharpCopilotContextProvider [] - (cache: FSharpNavigableItemsCache, [] workspace: VisualStudioWorkspace) = + (cache: FSharpNavigableItemsCache, [] workspace: VisualStudioWorkspace | null) = static let moniker = ServiceMoniker(FSharpConstants.copilotSymbolProviderName, Version CopilotDescriptors.CurrentContextProviderVersion) @@ -235,7 +256,7 @@ type internal FSharpCopilotContextProvider :> IReadOnlyCollection } - let fullyQualifiedNameOf (inputs: IReadOnlyDictionary) = + let fullyQualifiedNameOf (inputs: IReadOnlyDictionary | null) = match inputs with | null -> ValueNone | inputs -> @@ -295,9 +316,9 @@ type internal FSharpCopilotContextProvider let solution = workspace.CurrentSolution let! declarations = CopilotSymbolQuery.declarationsOf cache solution fullyQualifiedName - match Array.tryHead declarations with - | None -> return false - | Some(struct (item, document)) -> + match Array.tryHeadV declarations with + | ValueNone -> return false + | ValueSome(struct (item, document)) -> let! sourceText = document.GetTextAsync ct match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) with diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs index b155ccdc1d8..a23d7aab14d 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs @@ -1,62 +1,57 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. -namespace Microsoft.VisualStudio.FSharp.Editor +/// Translates F# navigable items into the shapes the Copilot chat "#" mention picker understands. +module internal Microsoft.VisualStudio.FSharp.Editor.CopilotSymbolMapping open Microsoft.VisualStudio.Copilot open Microsoft.VisualStudio.Imaging open FSharp.Compiler.EditorServices -/// Translates F# navigable items into the shapes the Copilot chat "#" mention picker understands. -module internal CopilotSymbolMapping = - - /// Name of the context member. It becomes the mention prefix the user sees and re-types, - /// as in "#fsharpSymbol:Namespace.Type.Member". - [] - let SymbolMember = "fsharpSymbol" - - [] - let FullyQualifiedNameInput = "fullyQualifiedName" - - /// The parse tree cannot tell an interface, struct or record apart from a plain class, so every - /// type-like declaration is reported as a class. - let symbolContextType kind = - match kind with - | NavigableItemKind.Module - | NavigableItemKind.ModuleAbbreviation - | NavigableItemKind.Exception - | NavigableItemKind.Type -> CopilotSymbolContextType.Class - | NavigableItemKind.ModuleValue -> CopilotSymbolContextType.Function - | NavigableItemKind.Field - | NavigableItemKind.Property -> CopilotSymbolContextType.Field - | NavigableItemKind.Constructor - | NavigableItemKind.Member -> CopilotSymbolContextType.Method - | NavigableItemKind.EnumCase -> CopilotSymbolContextType.Constant - | NavigableItemKind.UnionCase -> CopilotSymbolContextType.Union - - let private imageId kind = - match kind with - | NavigableItemKind.Module - | NavigableItemKind.ModuleAbbreviation -> KnownImageIds.ModulePublic - | NavigableItemKind.Exception -> KnownImageIds.ExceptionPublic - | NavigableItemKind.Type -> KnownImageIds.ClassPublic - | NavigableItemKind.ModuleValue - | NavigableItemKind.Constructor - | NavigableItemKind.Member -> KnownImageIds.MethodPublic - | NavigableItemKind.Field -> KnownImageIds.FieldPublic - | NavigableItemKind.Property -> KnownImageIds.PropertyPublic - | NavigableItemKind.EnumCase - | NavigableItemKind.UnionCase -> KnownImageIds.EnumerationItemPublic - - let icon kind = - let mutable moniker = CopilotImageMoniker() - moniker.Guid <- KnownImageIds.ImageCatalogGuid - moniker.Id <- imageId kind - moniker - - /// Dotted path that both drives the picker's pattern matching and identifies a picked mention - /// when it is resolved back to source. - let fullyQualifiedName (item: NavigableItem) = - match item.Container.FullName with - | "" -> item.Name - | container -> $"{container}.{item.Name}" +/// Name of the context member. It becomes the mention prefix the user sees and re-types, +/// as in "#fsharpSymbol:Namespace.Type.Member". +[] +let SymbolMember = "fsharpSymbol" + +[] +let FullyQualifiedNameInput = "fullyQualifiedName" + +/// The parse tree cannot tell an interface, struct or record apart from a plain class, so every +/// type-like declaration is reported as a class. +let symbolContextType kind = + match kind with + | NavigableItemKind.Module + | NavigableItemKind.ModuleAbbreviation + | NavigableItemKind.Exception + | NavigableItemKind.Type -> CopilotSymbolContextType.Class + | NavigableItemKind.ModuleValue -> CopilotSymbolContextType.Function + | NavigableItemKind.Field + | NavigableItemKind.Property -> CopilotSymbolContextType.Field + | NavigableItemKind.Constructor + | NavigableItemKind.Member -> CopilotSymbolContextType.Method + | NavigableItemKind.EnumCase -> CopilotSymbolContextType.Constant + | NavigableItemKind.UnionCase -> CopilotSymbolContextType.Union + +let private imageId kind = + match kind with + | NavigableItemKind.Module + | NavigableItemKind.ModuleAbbreviation -> KnownImageIds.ModulePublic + | NavigableItemKind.Exception -> KnownImageIds.ExceptionPublic + | NavigableItemKind.Type -> KnownImageIds.ClassPublic + | NavigableItemKind.ModuleValue + | NavigableItemKind.Constructor + | NavigableItemKind.Member -> KnownImageIds.MethodPublic + | NavigableItemKind.Field -> KnownImageIds.FieldPublic + | NavigableItemKind.Property -> KnownImageIds.PropertyPublic + | NavigableItemKind.EnumCase + | NavigableItemKind.UnionCase -> KnownImageIds.EnumerationItemPublic + +let icon kind = + CopilotImageMoniker(Guid = KnownImageIds.ImageCatalogGuid, Id = imageId kind) + +/// Dotted path that both drives the picker's pattern matching and identifies a picked mention +/// when it is resolved back to source. +let fullyQualifiedName (item: NavigableItem) = + match item.Container.FullName with + | "" -> item.Name + | container -> $"{container}.{item.Name}" diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs index a3c2b4b58e1..b2bb73958df 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs @@ -1,40 +1,38 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. -namespace Microsoft.VisualStudio.FSharp.Editor +/// Widens the identifier range of a navigable item to the declaration a reader would recognise. +module internal Microsoft.VisualStudio.FSharp.Editor.CopilotSymbolSnippets open FSharp.Compiler.EditorServices -/// Widens the identifier range of a navigable item to the declaration a reader would recognise. -module internal CopilotSymbolSnippets = - - /// A module scope can span a whole file, which is more than a chat prompt can usefully carry. - [] - let MaxSnippetLines = 200 - - /// Inclusive, 1-based line bounds of the declaration `item` names, including its doc comment. - let definitionLines (scopes: Structure.ScopeRange seq) (item: NavigableItem) = - let declarationLine = item.Range.StartLine - - // A construct's outlining range reaches back over the doc comment in front of it, so it is the - // collapse range - the body proper - that tells which construct is declared on this line. - let declaredHere (scope: Structure.ScopeRange) = - scope.CollapseRange.StartLine = declarationLine - && scope.Range.EndLine >= item.Range.EndLine - && scope.Scope <> Structure.Scope.Comment - && scope.Scope <> Structure.Scope.XmlDocComment - - let mutable widest = ValueNone - - for scope in scopes do - if declaredHere scope then - match widest with - | ValueSome(previous: Structure.ScopeRange) when previous.Range.EndLine >= scope.Range.EndLine -> () - | _ -> widest <- ValueSome scope - - // A one-line member declares no scope of its own; it stands for itself rather than for the type around it. - let firstLine, lastLine = +/// A module scope can span a whole file, which is more than a chat prompt can usefully carry. +[] +let MaxSnippetLines = 200 + +/// Inclusive, 1-based line bounds of the declaration `item` names, including its doc comment. +let definitionLines (scopes: Structure.ScopeRange seq) (item: NavigableItem) = + let declarationLine = item.Range.StartLine + + // A construct's outlining range reaches back over the doc comment in front of it, so it is the + // collapse range - the body proper - that tells which construct is declared on this line. + let declaredHere (scope: Structure.ScopeRange) = + scope.CollapseRange.StartLine = declarationLine + && scope.Range.EndLine >= item.Range.EndLine + && scope.Scope <> Structure.Scope.Comment + && scope.Scope <> Structure.Scope.XmlDocComment + + let mutable widest = ValueNone + + for scope in scopes do + if declaredHere scope then match widest with - | ValueSome scope -> scope.Range.StartLine, scope.Range.EndLine - | ValueNone -> declarationLine, item.Range.EndLine + | ValueSome(previous: Structure.ScopeRange) when previous.Range.EndLine >= scope.Range.EndLine -> () + | _ -> widest <- ValueSome scope + + // A one-line member declares no scope of its own; it stands for itself rather than for the type around it. + let firstLine, lastLine = + match widest with + | ValueSome scope -> scope.Range.StartLine, scope.Range.EndLine + | ValueNone -> declarationLine, item.Range.EndLine - firstLine, min lastLine (firstLine + MaxSnippetLines - 1) + struct (firstLine, min lastLine (firstLine + MaxSnippetLines - 1)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 995bb2d56c1..6a20a250533 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -10,10 +10,9 @@ open System.Threading.Tasks open System.IO open System.Collections.Immutable open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp +open Microsoft.CodeAnalysis.Host.Mef open Microsoft.CodeAnalysis.Options -open FSharp.Compiler -open FSharp.Compiler.CodeAnalysis -open FSharp.NativeInterop open Microsoft.ServiceHub.Framework open Microsoft.VisualStudio open Microsoft.VisualStudio.Copilot @@ -25,12 +24,13 @@ open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.Shell.ServiceBroker open Microsoft.VisualStudio.Text.Outlining -open Microsoft.CodeAnalysis.ExternalAccess.FSharp -open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.VisualStudio.Editor open Microsoft.VisualStudio.FSharp.Editor.Telemetry -open CancellableTasks +open FSharp.Compiler +open FSharp.Compiler.CodeAnalysis +open FSharp.NativeInterop open FSharp.Compiler.Text -open Microsoft.VisualStudio.Editor +open CancellableTasks #nowarn "9" // NativePtr.toNativeInt #nowarn "57" // Experimental stuff diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index b3273e36a19..75a349040b2 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -7,8 +7,9 @@ open System.IO open System.Composition open System.Collections.Immutable open System.Collections.Concurrent -open System.Threading.Tasks open System.Globalization +open System.Linq +open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation @@ -26,14 +27,16 @@ type internal FSharpNavigableItemsCache [] (patternMatcherFactory: IPatternMatcherFactory, [] workspace: VisualStudioWorkspace) = - let cache = ConcurrentDictionary() + let cache = + ConcurrentDictionary() do - if workspace <> null then - workspace.WorkspaceChanged.Add - <| fun e -> + match workspace with + | null -> () + | workspace -> + workspace.WorkspaceChanged.Add(fun e -> if e.NewSolution.Id <> e.OldSolution.Id then - cache.Clear() + cache.Clear()) member _.GetNavigableItems(document: Document) = cancellableTask { @@ -41,11 +44,11 @@ type internal FSharpNavigableItemsCache let! currentVersion = document.GetTextVersionAsync(ct) match cache.TryGetValue document.Id with - | true, (version, items) when version = currentVersion -> return items + | true, struct (version, items) when version = currentVersion -> return items | _ -> let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigableItemsCache)) let items = NavigateTo.GetNavigableItems parseResults.ParseTree - cache[document.Id] <- currentVersion, items + cache[document.Id] <- struct (currentVersion, items) return items } @@ -162,7 +165,7 @@ type internal FSharpNavigateToSearchService [] (itemsCache let! items = getNavigableItems document let processed = - [| + seq { for item in items do let contains = kinds.Contains(navigateToItemKindToRoslynKind item.Kind) let patternMatch = tryMatch item @@ -192,9 +195,9 @@ type internal FSharpNavigateToSearchService [] (itemsCache ) ) | _ -> () - |] + } - return processed + return processed |> Seq.toImmutableArray } interface IFSharpNavigateToSearchService with @@ -204,31 +207,17 @@ type internal FSharpNavigateToSearchService [] (itemsCache cancellableTask { let tryMatch = createMatcherFor searchPattern - let tasks = - [| - for doc in project.Documents do - yield processDocument tryMatch kinds doc - |] - - let! results = CancellableTask.whenAll tasks - - let results' = ImmutableArray.CreateBuilder() - - for navResults in results do - for navResult in navResults do - results'.Add navResult - - return results'.ToImmutable() + let! results = + project.Documents + |> Seq.map (processDocument tryMatch kinds) + |> CancellableTask.whenAll + return results |> Seq.collect _.AsEnumerable() |> Seq.toImmutableArray } |> CancellableTask.start cancellationToken member _.SearchDocumentAsync(document: Document, searchPattern, kinds, cancellationToken) = - cancellableTask { - let! result = processDocument (createMatcherFor searchPattern) kinds document - return Array.toImmutableArray result - } - |> CancellableTask.start cancellationToken + processDocument (createMatcherFor searchPattern) kinds document cancellationToken member _.KindsProvided = kindsProvided From 2f9a0080a96e5a2a2a0d6c31f5a073d8731b602d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 00:54:58 +0200 Subject: [PATCH 027/133] Link the Copilot mention picker release note to its PR Co-Authored-By: Claude Fable 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 3cef90bc875..40c512c08b3 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,7 +2,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) -* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. +* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409)) ### Fixed From 705aee278d15a88a2bdd49c20d8436e639741db0 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 31 Aug 2026 21:49:45 +0200 Subject: [PATCH 028/133] Give FSharpPackage its own VSPackage resource set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FSharpPackage's registration attributes in LanguageService.fs (ProvideOptionPage, ProvideKeyBindingTable, ProvideEditorFactory, ProvideLanguageService, and six ProvideLanguageEditorOptionPage entries) reference numeric resource IDs — 100, 101, 6000, 6001, 6008-6012, 6014 — that the shell resolves via a ResourceManager("VSPackage", ...) against FSharp.Editor's own assembly. No such resource set was ever embedded there, so any lookup throws MissingManifestResourceException. Add VSPackage.resx with those IDs, wired in with ManifestResourceName=VSPackage the same way FSharp.ProjectSystem.FSharp.fsproj already backs its own VSPackage.resx. Every value is copied verbatim from the literal fallback string already passed alongside its resource ID in the attribute that references it. Verified the rebuilt FSharp.Editor.dll's manifest now lists VSPackage.resources, which is what the exception reported missing. (cherry picked from commit 31d8d5c48656f0b8d15f5801a9f83e1972991bb8) --- .../src/FSharp.Editor/FSharp.Editor.fsproj | 5 + .../src/FSharp.Editor/VSPackage.resx | 160 ++++++++++++++++++ .../src/FSharp.Editor/xlf/VSPackage.cs.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.de.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.es.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.fr.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.it.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.ja.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.ko.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.pl.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.ru.xlf | 57 +++++++ .../src/FSharp.Editor/xlf/VSPackage.tr.xlf | 57 +++++++ .../FSharp.Editor/xlf/VSPackage.zh-Hans.xlf | 57 +++++++ .../FSharp.Editor/xlf/VSPackage.zh-Hant.xlf | 57 +++++++ 15 files changed, 906 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/VSPackage.resx create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf create mode 100644 vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 3176e1b964c..10711c971d0 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -23,6 +23,11 @@ true Microsoft.VisualStudio.FSharp.Editor.SR + + + + VSPackage LegacyResolver.txt diff --git a/vsintegration/src/FSharp.Editor/VSPackage.resx b/vsintegration/src/FSharp.Editor/VSPackage.resx new file mode 100644 index 00000000000..9e6d4036e52 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/VSPackage.resx @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + F# + + + + F# Source File + + + + F# Tools + + + + F# Interactive + + + + IntelliSense + + + + QuickInfo + + + + Code Fixes + + + + Performance + + + + Advanced + + + + Formatting + + diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf new file mode 100644 index 00000000000..0318968a3ce --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf new file mode 100644 index 00000000000..93d9f8b1f1b --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf new file mode 100644 index 00000000000..8d296d35980 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf new file mode 100644 index 00000000000..4c5032aa627 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf new file mode 100644 index 00000000000..eb3d61cd192 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf new file mode 100644 index 00000000000..935a1098800 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf new file mode 100644 index 00000000000..aa417bdeb3b --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf new file mode 100644 index 00000000000..ec5c7892abe --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf new file mode 100644 index 00000000000..536ef2def0e --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf new file mode 100644 index 00000000000..82ec6b869a0 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf new file mode 100644 index 00000000000..55fff89c18a --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf new file mode 100644 index 00000000000..c51cc50e501 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf new file mode 100644 index 00000000000..f815940cb36 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf @@ -0,0 +1,57 @@ + + + + + + F# + F# + + + + F# Source File + F# Source File + + + + F# Tools + F# Tools + + + + F# Interactive + F# Interactive + + + + IntelliSense + IntelliSense + + + + QuickInfo + QuickInfo + + + + Code Fixes + Code Fixes + + + + Performance + Performance + + + + Advanced + Advanced + + + + Formatting + Formatting + + + + + \ No newline at end of file From a022e051baeb6a995ee43fec6008e5363e64c55a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 00:36:28 +0200 Subject: [PATCH 029/133] Add release notes entry for VSPackage.resx fix (cherry picked from commit 462c68e9d66aeae2c505ec9459562d1c40a32be7) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 40c512c08b3..45dc02e2fb0 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -6,6 +6,7 @@ ### Fixed +* Fixed `MissingManifestResourceException` when opening F# pages under Tools > Options by giving `FSharpPackage` its own `VSPackage` resource set. ([PR #20408](https://github.com/dotnet/fsharp/pull/20408)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) From 51fc9d1205ce558915bcc6420d378ade6f401d6d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 18:45:29 +0200 Subject: [PATCH 030/133] Seed VSPackage translations and document the resource-set split Address review feedback on #20408. Every one of the ten strings already exists, translated, elsewhere in vsintegration, so borrow those targets instead of shipping the new VSPackage.resx English-only: "F# Source File" and "F# Tools" from FSharp.ProjectSystem.FSharp's VSPackage.resx, the six option-page names from FSharp.UIResources' Strings.resx, "F# Interactive" from the FSI command table. Also record in the resx header why the page-name IDs live here while the sibling *PageKeywords stay in FSharp.Editor.resx: the shell resolves the former through SVsResourceManager against the fixed "VSPackage" basename, the option-page automation resolves the latter itself out of the assembly's default resource set. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 387eb5598108514f74ae3e163bdfc2a52f5a94b8) --- .../src/FSharp.Editor/VSPackage.resx | 10 +++++++++- .../src/FSharp.Editor/xlf/VSPackage.cs.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.de.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.es.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.fr.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.it.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.ja.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.ko.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.pl.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.ru.xlf | 20 +++++++++---------- .../src/FSharp.Editor/xlf/VSPackage.tr.xlf | 20 +++++++++---------- .../FSharp.Editor/xlf/VSPackage.zh-Hans.xlf | 20 +++++++++---------- .../FSharp.Editor/xlf/VSPackage.zh-Hant.xlf | 20 +++++++++---------- 14 files changed, 139 insertions(+), 131 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/VSPackage.resx b/vsintegration/src/FSharp.Editor/VSPackage.resx index 9e6d4036e52..948a9736142 100644 --- a/vsintegration/src/FSharp.Editor/VSPackage.resx +++ b/vsintegration/src/FSharp.Editor/VSPackage.resx @@ -117,11 +117,19 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + F# - + F# Source File diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf index 0318968a3ce..af101d62e2a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.cs.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + Zdrojový soubor F# F# Tools - F# Tools + Nástroje F# F# Interactive - F# Interactive + F# Interactive IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + Stručné informace Code Fixes - Code Fixes + Opravy kódu Performance - Performance + Výkon Advanced - Advanced + Upřesnit Formatting - Formatting + Formátování diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf index 93d9f8b1f1b..b7589dc7b7a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.de.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + F#-Quelldatei F# Tools - F# Tools + F# Tools F# Interactive - F# Interactive + F# Interactive IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Codefehlerbehebungen Performance - Performance + Leistung Advanced - Advanced + Erweitert Formatting - Formatting + Formatierung diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf index 8d296d35980..22371b3720a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.es.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + Archivo de código fuente de F# F# Tools - F# Tools + Herramientas de F# F# Interactive - F# Interactive + F# interactivo IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Correcciones de código Performance - Performance + Rendimiento Advanced - Advanced + Opciones avanzadas Formatting - Formatting + Formato diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf index 4c5032aa627..a5535757dc1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.fr.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + Fichier source F# F# Tools - F# Tools + Outils F# F# Interactive - F# Interactive + F# Interactive IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Correctifs du code Performance - Performance + Performances Advanced - Advanced + Avancé Formatting - Formatting + Mise en forme diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf index eb3d61cd192..254fdfb2e9d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.it.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + File di codice sorgente F# F# Tools - F# Tools + Strumenti F# F# Interactive - F# Interactive + F# Interactive IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + Informazioni rapide Code Fixes - Code Fixes + Correzioni del codice Performance - Performance + Prestazioni Advanced - Advanced + Avanzate Formatting - Formatting + Formattazione diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf index 935a1098800..12c4fb59bc0 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ja.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + F# ソース ファイル F# Tools - F# Tools + F# ツール F# Interactive - F# Interactive + F# インタラクティブ IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + コード修正 Performance - Performance + パフォーマンス Advanced - Advanced + 詳細 Formatting - Formatting + 書式設定 diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf index aa417bdeb3b..8d47b988a4e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ko.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + F# 소스 파일 F# Tools - F# Tools + F# 도구 F# Interactive - F# Interactive + F# 대화형 IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + 코드 수정 사항 Performance - Performance + 성능 Advanced - Advanced + 고급 Formatting - Formatting + 서식 diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf index ec5c7892abe..2b780cc9bc5 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pl.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + Plik źródłowy języka F# F# Tools - F# Tools + Narzędzia języka F# F# Interactive - F# Interactive + F# Interactive IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Poprawki kodu Performance - Performance + Wydajność Advanced - Advanced + Zaawansowane Formatting - Formatting + Formatowanie diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf index 536ef2def0e..b7ccf25e4b2 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.pt-BR.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + Arquivo Fonte F# F# Tools - F# Tools + Ferramentas F# F# Interactive - F# Interactive + F# Interativo IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Correções de Código Performance - Performance + Desempenho Advanced - Advanced + Avançado Formatting - Formatting + Formatação diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf index 82ec6b869a0..58eb92af51d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.ru.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + Исходный файл F# F# Tools - F# Tools + Инструменты F# F# Interactive - F# Interactive + F# Interactive IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Исправления кода Performance - Performance + Производительность Advanced - Advanced + Дополнительный Formatting - Formatting + Форматирование diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf index 55fff89c18a..fe28afe03d2 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.tr.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + F# Kaynak Dosyası F# Tools - F# Tools + F# Araçları F# Interactive - F# Interactive + F# Etkileşimli IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + Kod Düzeltmeleri Performance - Performance + Performans Advanced - Advanced + Gelişmiş Formatting - Formatting + Biçimlendirme diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf index c51cc50e501..7f89b326fec 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hans.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + F# 源文件 F# Tools - F# Tools + F# 工具 F# Interactive - F# Interactive + F# 交互窗口 IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + 代码修补程序 Performance - Performance + 性能 Advanced - Advanced + 高级 Formatting - Formatting + 格式设置 diff --git a/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf index f815940cb36..cab17af78bd 100644 --- a/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/VSPackage.zh-Hant.xlf @@ -4,52 +4,52 @@ F# - F# + F# F# Source File - F# Source File + F# 原始程式檔 F# Tools - F# Tools + F# 工具 F# Interactive - F# Interactive + F# 互動 IntelliSense - IntelliSense + IntelliSense QuickInfo - QuickInfo + QuickInfo Code Fixes - Code Fixes + 程式碼修正 Performance - Performance + 效能 Advanced - Advanced + 進階 Formatting - Formatting + 格式化 From 633f22febe914fcdc81bed8032b1d4559e2612cf Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 11:24:23 +0200 Subject: [PATCH 031/133] Type `DelayedILModuleReader.result` as nullable instead of boxing `result` was declared non-nullable and initialised with `Unchecked.defaultof<_>`, so every read of the fast path had to `box` it just to test for null. Declaring it `ILModuleReader | null` lets the match narrow the type directly, dropping the boxes and the `Unchecked` initialiser. (cherry picked from commit 03d602d10fa9da340a1692de1164330f19e5c24b) --- src/Compiler/Service/FSharpCheckerResults.fs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index 155d1b5f702..6473188a802 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -69,21 +69,21 @@ type DelayedILModuleReader = val private name: string val private gate: obj val mutable private getStream: (CancellationToken -> Stream option) - val mutable private result: ILModuleReader + val mutable private result: ILModuleReader | null new(name, getStream) = { name = name gate = obj () getStream = getStream - result = Unchecked.defaultof<_> + result = null } member this.OutputFile = this.name member this.TryGetILModuleReader() = // fast path - match box this.result with + match this.result with | null -> cancellable { let! ct = Cancellable.token () @@ -91,7 +91,7 @@ type DelayedILModuleReader = return lock this.gate (fun () -> // see if we have a result or not after the lock so we do not evaluate the stream more than once - match box this.result with + match this.result with | null -> try let streamOpt = this.getStream ct @@ -114,9 +114,9 @@ type DelayedILModuleReader = with ex -> Trace.TraceInformation("FCS: Unable to get an ILModuleReader: {0}", ex) None - | _ -> Some this.result) + | result -> Some result) } - | _ -> cancellable.Return(Some this.result) + | result -> cancellable.Return(Some result) [] type FSharpReferencedProject = From 19f161c65f5eaf4a011c81ab2bf82f7d5271cad0 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 10:47:03 +0200 Subject: [PATCH 032/133] Add `Array.tryPickV` and hand back `voption` from `CompilationBinOutputPath` `IProjectSite.CompilationBinOutputPath` is probed for the `-o:` flag on every project site mapping and its result is immediately converted to a string or null. Add an `Array.tryPickV` next to the existing `chooseV` and return `string voption`, so neither the pick nor the result allocates. (cherry picked from commit b869f9fdcc67c90deb254d81117be0eb8fdd61f8) --- vsintegration/src/FSharp.Editor/Common/Extensions.fs | 12 ++++++++++++ .../LanguageService/FSharpProjectOptionsManager.fs | 2 +- .../FSharp.Editor/LanguageService/IProjectSite.fs | 2 +- .../LanguageService/LegacyProjectWorkspaceMap.fs | 4 ++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 4057a079af3..b58ccfb5003 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -534,6 +534,18 @@ module Array = loop 0 + let inline tryPickV ([] chooser: 'T -> 'U voption) (array: 'T[]) = + + let rec loop i = + if i >= array.Length then + ValueNone + else + match chooser array[i] with + | ValueNone -> loop (i + 1) + | res -> res + + loop 0 + let inline chooseV ([] chooser: 'T -> 'U voption) (array: 'T[]) = let mutable i = 0 diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 6a6f97d7440..ac9784723a8 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -44,7 +44,7 @@ module private FSharpProjectOptionsHelpers = member site.CompilationBinOutputPath = site.CompilationOptions - |> Array.tryPick (fun s -> if s.StartsWith("-o:") then Some s.[3..] else None) + |> Array.tryPickV (fun s -> if s.StartsWith("-o:") then ValueSome s.[3..] else ValueNone) member _.ProjectFileName = project.FilePath member _.AdviseProjectSiteChanges(_, _) = () diff --git a/vsintegration/src/FSharp.Editor/LanguageService/IProjectSite.fs b/vsintegration/src/FSharp.Editor/LanguageService/IProjectSite.fs index 57b530309ad..611e3204b62 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/IProjectSite.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/IProjectSite.fs @@ -25,7 +25,7 @@ and internal IProjectSite = abstract CompilationReferences: string[] /// The '-o:' output bin path, without the '-o:' - abstract CompilationBinOutputPath: string option + abstract CompilationBinOutputPath: string voption /// The name of the project file. abstract ProjectFileName: string diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LegacyProjectWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/LegacyProjectWorkspaceMap.fs index 12e2554284f..ef55cea1458 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LegacyProjectWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LegacyProjectWorkspaceMap.fs @@ -117,7 +117,7 @@ type internal LegacyProjectWorkspaceMap optionsAssociation.Add(projectContext, updatedOptions) - projectContext.BinOutputPath <- Option.toObj site.CompilationBinOutputPath + projectContext.BinOutputPath <- ValueOption.toObj site.CompilationBinOutputPath let info = (updatedFiles, updatedRefs) legacyProjectLookup.AddOrUpdate(projectId, info, (fun _ _ -> info)) |> ignore @@ -142,7 +142,7 @@ type internal LegacyProjectWorkspaceMap projectFileName, projectGuid, hierarchy, - Option.toObj site.CompilationBinOutputPath + ValueOption.toObj site.CompilationBinOutputPath ) legacyProjectIdLookup.[projectGuid] <- projectContext.Id From d342033fef2b40a476ac6a32231e7971cfce6948 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 11:24:23 +0200 Subject: [PATCH 033/133] Use struct tuples for the F# project options caches The reactor's `commandLineOptions` and project options `cache` dictionaries, and the `AsyncReplyChannel` payloads of `TryGetOptionsByDocument`/ `TryGetOptionsByProject`, all carried reference tuples that were allocated on every cache write and every reply. They never escape the reactor, so make them `struct` tuples. (cherry picked from commit 2a43faed93c1481d2833b87903d8796046fc381e) --- .../FSharpProjectOptionsManager.fs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index ac9784723a8..a25df7fc3ed 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -25,7 +25,7 @@ open CancellableTasks [] module private FSharpProjectOptionsHelpers = - let mapCpsProjectToSite (project: Project, cpsCommandLineOptions: IDictionary) = + let mapCpsProjectToSite (project: Project, cpsCommandLineOptions: IDictionary) = let sourcePaths, referencePaths, options = match cpsCommandLineOptions.TryGetValue(project.Id) with | true, (sourcePaths, options) -> sourcePaths, [||], options @@ -137,10 +137,13 @@ type PEReferenceCacheEntry = type private FSharpProjectOptionsMessage = | TryGetOptionsByDocument of Document * - AsyncReplyChannel<(FSharpParsingOptions * FSharpProjectOptions) voption> * + AsyncReplyChannel * CancellationToken * userOpName: string - | TryGetOptionsByProject of Project * AsyncReplyChannel<(FSharpParsingOptions * FSharpProjectOptions) voption> * CancellationToken + | TryGetOptionsByProject of + Project * + AsyncReplyChannel * + CancellationToken | ClearOptions of ProjectId | ClearSingleFileOptionsCache of DocumentId @@ -149,12 +152,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let cancellationTokenSource = new CancellationTokenSource() // Store command line options - let commandLineOptions = ConcurrentDictionary() + let commandLineOptions = + ConcurrentDictionary() let legacyProjectSites = ConcurrentDictionary() let cache = - ConcurrentDictionary() + ConcurrentDictionary() let singleFileCache = ConcurrentDictionary() @@ -409,7 +413,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = ) |> ignore - return ValueSome(parsingOptions, projectOptions) + return ValueSome struct (parsingOptions, projectOptions) | true, { @@ -432,7 +436,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = return! tryComputeOptionsBySingleScriptOrFile document userOpName else - return ValueSome(parsingOptions, projectOptions) + return ValueSome struct (parsingOptions, projectOptions) } let tryGetProjectSite (project: Project) = @@ -466,7 +470,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = if referencedProject.Language = FSharpConstants.FSharpLanguageName then match! tryComputeOptions referencedProject with | ValueNone -> canBail <- true - | ValueSome(_, projectOptions) -> + | ValueSome struct (_, projectOptions) -> referencedProjects.Add( FSharpReferencedProject.FSharpReference(referencedProject.OutputFilePath, projectOptions) ) @@ -537,7 +541,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let options = projectsToClearCache |> Seq.map (fun pair -> - let _, _, projectOptions = pair.Value + let struct (_, _, projectOptions) = pair.Value projectOptions) checker.ClearCache(options, userOpName = "tryComputeOptions") @@ -551,18 +555,18 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) - cache.[projectId] <- (project, parsingOptions, projectOptions) + cache.[projectId] <- struct (project, parsingOptions, projectOptions) - return ValueSome(parsingOptions, projectOptions) + return ValueSome struct (parsingOptions, projectOptions) - | true, (oldProject, parsingOptions, projectOptions) -> + | true, struct (oldProject, parsingOptions, projectOptions) -> let! isInvalidated = isProjectInvalidated oldProject project if isInvalidated then cache.TryRemove(projectId) |> ignore return! tryComputeOptions project else - return ValueSome(parsingOptions, projectOptions) + return ValueSome struct (parsingOptions, projectOptions) } let loop (agent: MailboxProcessor) = @@ -634,7 +638,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | FSharpProjectOptionsMessage.ClearOptions(projectId) -> match cache.TryRemove(projectId) with - | true, (_, _, projectOptions) -> + | true, struct (_, _, projectOptions) -> lastSuccessfulCompilations.TryRemove(projectId) |> ignore emitCache.TryRemove(projectId) |> ignore checker.ClearCache([ projectOptions ]) @@ -668,7 +672,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = agent.Post(FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId)) member _.SetCommandLineOptions(projectId, sourcePaths, options) = - commandLineOptions.[projectId] <- (sourcePaths, options) + commandLineOptions.[projectId] <- struct (sourcePaths, options) member _.SetLegacyProjectSite(projectId, projectSite) = legacyProjectSites.[projectId] <- projectSite From 760d241057760ac01968238eb215f07a337832cc Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 11:38:38 +0200 Subject: [PATCH 034/133] Add release notes for the allocation and nullness cleanups (cherry picked from commit 5a48643bc54036e646beea3beef710d29eb82847) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + docs/release-notes/.VisualStudio/18.vNext.md | 1 + 2 files changed, 2 insertions(+) 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 f71c9dc4296..294e0aafb41 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -193,6 +193,7 @@ * IL: use empty tables for members when possible ([PR #20249](https://github.com/dotnet/fsharp/pull/20249)) * Make Entity's adhoc members list lazy ([PR #20286](https://github.com/dotnet/fsharp/pull/20286/changes)) * Constraint solver: `TryD` is now `inline` with `[]` on its always-run continuation, so the argument closures are no longer allocated at the (very hot) constraint-solver call sites; `IgnoreFailedMemberConstraintResolution` is `inline` so its forwarded continuation stays a literal. ([PR #20367](https://github.com/dotnet/fsharp/pull/20367)) +* `DelayedILModuleReader` no longer boxes its cached `ILModuleReader` on every read: the field is typed `ILModuleReader | null` and matched directly. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413)) ### Changed * The `--warnaserror` option now ignores unrecognized diagnostic identifiers in warning lists while still applying recognized F# warning codes. ([PR #20246](https://github.com/dotnet/fsharp/pull/20246)) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 45dc02e2fb0..bf97f69f287 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -20,6 +20,7 @@ * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) * Fix doubled F# diagnostics in tooltips. ([Issue #16360](https://github.com/dotnet/fsharp/issues/16360)) * Fix `NotSupportedException` in the memory-mapped-file optimization when copying `ReadOnlyMemory` into `MemoryMappedFileViewStream`. ([Issue #20263](https://github.com/dotnet/fsharp/issues/20263)) +* Reduce allocations in the VS project options reactor: the command-line options and project options caches and the mailbox reply payloads now hold struct tuples, and `IProjectSite.CompilationBinOutputPath` returns `string voption` picked with a new `Array.tryPickV`. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413)) ### Changed From 71d507fdfc875a544fca8d79e88a1e690eca55fa Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 13:11:03 +0200 Subject: [PATCH 035/133] Fix build: two more CompilationBinOutputPath sites still returned `option` `Project.fs` implements `Microsoft.VisualStudio.FSharp.Editor.IProjectSite` in two more places (`CreateRunningProjectSite` and `CreateStaticProjectSite`) that the earlier `voption` change to `IProjectSite.CompilationBinOutputPath` missed, breaking the build with FS0001. (cherry picked from commit 56aa82ee8645958bcdb997e0ea77b21d92dbd981) --- vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs b/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs index 701e086705d..50e9a1e5715 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs @@ -1388,7 +1388,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem member _.CompilationReferences = x.CompilationReferences member _.CompilationBinOutputPath = let outputPath = x.GetCurrentOutputAssembly() - if String.IsNullOrWhiteSpace(outputPath) then None else Some(outputPath) + if String.IsNullOrWhiteSpace(outputPath) then ValueNone else ValueSome(outputPath) member _.Description = match sourcesAndFlags with @@ -1431,7 +1431,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem member _.CompilationSourceFiles = sourceFiles member _.CompilationOptions = options member _.CompilationReferences = refs - member _.CompilationBinOutputPath = if String.IsNullOrWhiteSpace(outputPath) then None else Some(outputPath) + member _.CompilationBinOutputPath = if String.IsNullOrWhiteSpace(outputPath) then ValueNone else ValueSome(outputPath) member _.ProjectFileName = projFileName member _.BuildErrorReporter with get() = staticBuildErrorReporter From ad49a09680466cf814f89237a7e740e434b453d5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 11:44:52 +0200 Subject: [PATCH 036/133] Return `voption` from `DelayedILModuleReader`'s stream factory `getStream` is called once per PE reference evaluation and its result is consumed immediately, as is `TryGetILModuleReader`'s. Neither escapes, so both move from `option` to `voption`, along with the editor's `Compilation.Emit` stream factory that feeds them. `DelayedILModuleReader`'s constructor is public, so this changes the `FSharp.Compiler.Service` API surface; the netstandard2.0 baseline is updated to match. (cherry picked from commit 66c2aa27d9872abc12eaaf2ea58bddac597804b3) --- src/Compiler/Service/BackgroundCompiler.fs | 2 +- src/Compiler/Service/FSharpCheckerResults.fs | 18 ++++++++---------- src/Compiler/Service/FSharpCheckerResults.fsi | 4 ++-- src/Compiler/Service/TransparentCompiler.fs | 2 +- ...piler.Service.SurfaceArea.netstandard20.bsl | 2 +- .../Compiler/Service/MultiProjectTests.fs | 2 +- .../FSharpProjectOptionsManager.fs | 14 +++++++------- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Compiler/Service/BackgroundCompiler.fs b/src/Compiler/Service/BackgroundCompiler.fs index 50f9881260f..972f02a5c17 100644 --- a/src/Compiler/Service/BackgroundCompiler.fs +++ b/src/Compiler/Service/BackgroundCompiler.fs @@ -314,7 +314,7 @@ type internal BackgroundCompiler let! ilReaderOpt = delayedReader.TryGetILModuleReader() match ilReaderOpt with - | Some ilReader -> + | ValueSome ilReader -> let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs let data = RawFSharpAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData return ProjectAssemblyDataResult.Available data diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index 6473188a802..5235319683d 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -68,7 +68,7 @@ type DocumentSource = type DelayedILModuleReader = val private name: string val private gate: obj - val mutable private getStream: (CancellationToken -> Stream option) + val mutable private getStream: (CancellationToken -> Stream voption) val mutable private result: ILModuleReader | null new(name, getStream) = @@ -94,10 +94,8 @@ type DelayedILModuleReader = match this.result with | null -> try - let streamOpt = this.getStream ct - - match streamOpt with - | Some stream -> + match this.getStream ct with + | ValueSome stream -> let ilReaderOptions: ILReaderOptions = { pdbDirPath = None @@ -109,14 +107,14 @@ type DelayedILModuleReader = let ilReader = OpenILModuleReaderFromStream this.name stream ilReaderOptions this.result <- ilReader this.getStream <- Unchecked.defaultof<_> // clear out the function so we do not hold onto anything - Some ilReader - | _ -> None + ValueSome ilReader + | _ -> ValueNone with ex -> Trace.TraceInformation("FCS: Unable to get an ILModuleReader: {0}", ex) - None - | result -> Some result) + ValueNone + | result -> ValueSome result) } - | result -> cancellable.Return(Some result) + | result -> cancellable.Return(ValueSome result) [] type FSharpReferencedProject = diff --git a/src/Compiler/Service/FSharpCheckerResults.fsi b/src/Compiler/Service/FSharpCheckerResults.fsi index b1b5f78f675..cccfe8373d8 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fsi +++ b/src/Compiler/Service/FSharpCheckerResults.fsi @@ -36,13 +36,13 @@ type DocumentSource = [] type DelayedILModuleReader = - new: name: string * getStream: (CancellationToken -> Stream option) -> DelayedILModuleReader + new: name: string * getStream: (CancellationToken -> Stream voption) -> DelayedILModuleReader member OutputFile: string /// Will lazily create the ILModuleReader. /// Is only evaluated once and can be called by multiple threads. - member internal TryGetILModuleReader: unit -> Cancellable + member internal TryGetILModuleReader: unit -> Cancellable /// Unused in this API type public FSharpUnresolvedReferencesSet = internal FSharpUnresolvedReferencesSet of UnresolvedAssemblyReference list diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 691dde3e802..ecb5d68dc25 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -779,7 +779,7 @@ type internal TransparentCompiler let! ilReaderOpt = delayedReader.TryGetILModuleReader() match ilReaderOpt with - | Some ilReader -> + | ValueSome ilReader -> let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs let data = RawFSharpAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData return ProjectAssemblyDataResult.Available data diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 5c9c346b613..391d2149a4f 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -2065,7 +2065,7 @@ FSharp.Compiler.Cancellable: Void CheckAndThrow() FSharp.Compiler.Cancellable: Void TryCheckAndThrow() FSharp.Compiler.CodeAnalysis.DelayedILModuleReader: System.String OutputFile FSharp.Compiler.CodeAnalysis.DelayedILModuleReader: System.String get_OutputFile() -FSharp.Compiler.CodeAnalysis.DelayedILModuleReader: Void .ctor(System.String, Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,Microsoft.FSharp.Core.FSharpOption`1[System.IO.Stream]]) +FSharp.Compiler.CodeAnalysis.DelayedILModuleReader: Void .ctor(System.String, Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,Microsoft.FSharp.Core.FSharpValueOption`1[System.IO.Stream]]) FSharp.Compiler.CodeAnalysis.DocumentSource+Custom: Microsoft.FSharp.Core.FSharpFunc`2[System.String,Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText]]] Item FSharp.Compiler.CodeAnalysis.DocumentSource+Custom: Microsoft.FSharp.Core.FSharpFunc`2[System.String,Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText]]] get_Item() FSharp.Compiler.CodeAnalysis.DocumentSource+Tags: Int32 Custom diff --git a/tests/fsharp/Compiler/Service/MultiProjectTests.fs b/tests/fsharp/Compiler/Service/MultiProjectTests.fs index 2633494f3e4..ef323dc1ab7 100644 --- a/tests/fsharp/Compiler/Service/MultiProjectTests.fs +++ b/tests/fsharp/Compiler/Service/MultiProjectTests.fs @@ -40,7 +40,7 @@ namespace CSharpTest cs.Emit(ms, cancellationToken = ct) |> ignore ms.Position <- 0L ms :> Stream - |> Some + |> ValueSome let stamp = DateTime.UtcNow let csRefProj = FSharpReferencedProject.PEReference((fun () -> stamp), DelayedILModuleReader("""Z:\csharp_test.dll""", getStream)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index a25df7fc3ed..afc5c9dd6e0 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -211,32 +211,32 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = strongComp <- Unchecked.defaultof<_> lastSuccessfulCompilations.[projectId] <- comp ms.Position <- 0L - ms :> Stream |> Some + ms :> Stream |> ValueSome else strongComp <- Unchecked.defaultof<_> ms.Dispose() - None + ValueNone with | :? OperationCanceledException -> stampTime <- DateTime.UtcNow ms.Dispose() - None + ValueNone | _ -> strongComp <- Unchecked.defaultof<_> ms.Dispose() - None + ValueNone let resultOpt = match weakComp.TryGetTarget() with | true, comp -> tryStream comp - | _ -> None + | _ -> ValueNone match resultOpt with - | Some _ -> resultOpt + | ValueSome _ -> resultOpt | _ -> match lastSuccessfulCompilations.TryGetValue(projectId) with | true, comp -> tryStream comp - | _ -> None + | _ -> ValueNone let getStampTime () = stampTime FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) From d3fe91473e52b54423e3204a06c908f12a07006d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Sep 2026 11:45:47 +0200 Subject: [PATCH 037/133] Add release note for the DelayedILModuleReader API change (cherry picked from commit b08150179e5d97269f5f97416f4fa817df6fb514) --- 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 294e0aafb41..ce7e18eb534 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -213,6 +213,7 @@ * Remove the always-on `NullableOptionalInterop` language feature flag and collapse its guarded code to the enabled path; `--disableLanguageFeature:NullableOptionalInterop` is no longer a recognized feature name. ([Issue #20154](https://github.com/dotnet/fsharp/issues/20154), [PR #20378](https://github.com/dotnet/fsharp/pull/20378)) ### Breaking Changes +* `DelayedILModuleReader`'s constructor now takes a `CancellationToken -> Stream voption` stream factory instead of `CancellationToken -> Stream option`. ([PR #20414](https://github.com/dotnet/fsharp/pull/20414)) * Add `ExtendedLayoutAttribute` support for future .NET runtime interop. `ILTypeDefLayout` has a new `Extended` case. ([Issue #19190](https://github.com/dotnet/fsharp/issues/19190), [PR #19194](https://github.com/dotnet/fsharp/pull/19194)) * `FSharp.Compiler.Syntax.SynInterpolatedStringPart.FillExpr` now carries a `SynInterpolationFormatting` value (separating .NET alignment/format from printf specifiers) instead of an `Ident option`. ([PR #19971](https://github.com/dotnet/fsharp/pull/19971)) From 20cbe54475de70211c2a0a07488a2c8ee115c2dc Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 19:24:32 +0200 Subject: [PATCH 038/133] Harden Copilot provider registration and widen one-line snippets Package load runs its tasks back to back on a single loop, so an exception from the Copilot registration task escaped into F# package load. A Copilot contract version the installed build does not serve would have taken the whole package down; catch and log instead, leaving cancellation alone. A doc comment is only reported as an outlining scope once it spans several lines, so a one-line "///" in front of a declaration was invisible to the scope search and dropped from the snippet. Walk back over the preceding "///" lines directly. Batch mention queries scanned the solution once per query, serially. Distinct search texts now scan concurrently and repeated ones share a single scan. The snippet-location test asserted a hardcoded "C:\test.fs" rather than asking the solution where its document lives. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 9d36d113807e09099e36bd4f8181a44bb7d36f7e) --- .../Copilot/CopilotContextProvider.fs | 22 ++++---- .../Copilot/CopilotSymbolSnippets.fs | 17 +++++- .../LanguageService/LanguageService.fs | 53 ++++++++++--------- .../CopilotContextProviderTests.fs | 14 ++++- 4 files changed, 70 insertions(+), 36 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs index f63398e8c69..71b6457d83e 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs @@ -138,7 +138,9 @@ module internal CopilotSymbolQuery = Array.init sourceText.Lines.Count (fun line -> sourceText.Lines[line].ToString()) let scopes = Structure.getOutliningRanges sourceLines parseResults.ParseTree - let struct (firstLine, lastLine) = CopilotSymbolSnippets.definitionLines scopes item + + let struct (firstLine, lastLine) = + CopilotSymbolSnippets.definitionLines sourceLines scopes item let firstLine = max 1 firstLine let lastLine = min sourceText.Lines.Count lastLine @@ -243,9 +245,9 @@ type internal FSharpCopilotContextProvider | text -> ValueSome text | _ -> ValueNone - let queryMentions (query: CopilotMentionQuery) = + let mentionsFor (searchText: string voption) = cancellableTask { - match workspace, searchTextOf query with + match workspace, searchText with | null, _ | _, ValueNone -> return noMentions | workspace, ValueSome searchText -> @@ -304,7 +306,7 @@ type internal FSharpCopilotContextProvider interface ICopilotMentionQueryable with member _.QueryMentionAsync(query, cancellationToken) : Task> = - queryMentions query |> CancellableTask.start cancellationToken + mentionsFor (searchTextOf query) |> CancellableTask.start cancellationToken member _.NavigateToMentionableAsync(mention, cancellationToken) : Task = match workspace, fullyQualifiedNameOf mention.Inputs with @@ -334,15 +336,15 @@ type internal FSharpCopilotContextProvider |> CancellableTask.start cancellationToken // Copilot's own picker providers answer through the batch interface, one result collection per query. + // Each distinct search text scans the solution once, and the scans run side by side. interface ICopilotMentionBatchQueryable with member _.QueryMentionBatchAsync(queries, cancellationToken) : Task>> = cancellableTask { - let results = ResizeArray queries.Count - - for query in queries do - let! mentions = queryMentions query - results.Add mentions + let searchTexts = queries |> Seq.map searchTextOf |> Seq.toArray + let distinct = Array.distinct searchTexts + let! mentions = distinct |> Array.map mentionsFor |> CancellableTask.whenAll + let byText = Array.zip distinct mentions |> dict - return results :> IReadOnlyList> + return searchTexts |> Array.map (fun text -> byText[text]) :> IReadOnlyList> } |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs index b2bb73958df..59c98c3fd21 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs @@ -3,6 +3,8 @@ /// Widens the identifier range of a navigable item to the declaration a reader would recognise. module internal Microsoft.VisualStudio.FSharp.Editor.CopilotSymbolSnippets +open System + open FSharp.Compiler.EditorServices /// A module scope can span a whole file, which is more than a chat prompt can usefully carry. @@ -10,7 +12,7 @@ open FSharp.Compiler.EditorServices let MaxSnippetLines = 200 /// Inclusive, 1-based line bounds of the declaration `item` names, including its doc comment. -let definitionLines (scopes: Structure.ScopeRange seq) (item: NavigableItem) = +let definitionLines (sourceLines: string array) (scopes: Structure.ScopeRange seq) (item: NavigableItem) = let declarationLine = item.Range.StartLine // A construct's outlining range reaches back over the doc comment in front of it, so it is the @@ -35,4 +37,17 @@ let definitionLines (scopes: Structure.ScopeRange seq) (item: NavigableItem) = | ValueSome scope -> scope.Range.StartLine, scope.Range.EndLine | ValueNone -> declarationLine, item.Range.EndLine + // Outlining reports a doc comment only once it spans several lines, so a one-line "///" in front of + // a declaration is invisible to the scopes above. + let isDocComment line = + sourceLines[line - 1].TrimStart().StartsWith("///", StringComparison.Ordinal) + + let rec docCommentStart line = + if line > 1 && isDocComment (line - 1) then + docCommentStart (line - 1) + else + line + + let firstLine = docCommentStart firstLine + struct (firstLine, min lastLine (firstLine + MaxSnippetLines - 1)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 6a20a250533..68bb0b038e7 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -419,30 +419,35 @@ type internal FSharpPackage() as this = false, fun _ cancellationToken -> task { - let! container = this.GetServiceAsync(typeof) - - match container with - | :? IBrokeredServiceContainer as container -> - // The Interactions service also serves the registration interface. It is absent when - // GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker. - let! registration = - container - .GetFullAccessServiceBroker() - .GetProxyAsync(CopilotDescriptors.InteractionService, cancellationToken) - - use registration = registration - - match registration with - | null -> () - | registration -> - let moniker = - ServiceMoniker( - FSharpConstants.copilotSymbolProviderName, - Version CopilotDescriptors.CurrentContextProviderVersion - ) - - do! registration.RegisterContextProviderAsync(moniker, cancellationToken) - | _ -> () + try + let! container = this.GetServiceAsync(typeof) + + match container with + | :? IBrokeredServiceContainer as container -> + // The Interactions service also serves the registration interface. It is absent when + // GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker. + let! registration = + container + .GetFullAccessServiceBroker() + .GetProxyAsync(CopilotDescriptors.InteractionService, cancellationToken) + + use registration = registration + + match registration with + | null -> () + | registration -> + let moniker = + ServiceMoniker( + FSharpConstants.copilotSymbolProviderName, + Version CopilotDescriptors.CurrentContextProviderVersion + ) + + do! registration.RegisterContextProviderAsync(moniker, cancellationToken) + | _ -> () + // Package load runs its tasks back to back on one loop, so a Copilot failure - a contract + // version the installed build does not serve, say - must not take the F# package down with it. + with ex when not (ex :? OperationCanceledException) -> + DebugHelpers.FSharpOutputPane.logExceptionWithContext (ex, "Registering the Copilot context provider") } :> Task ) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs index cd20c8e2d4d..f7fc6966b52 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs @@ -36,6 +36,9 @@ let describeShape shape = match shape with | Circle r -> $"circle {r}" | Square s -> $"square {s}" + +/// Twice the value. +let twice x = x * 2 """ let solution = RoslynTestHelpers.CreateSolution fileContents @@ -92,6 +95,14 @@ let describeShape shape = Assert.Contains("value <- value + 1", context.Snippet) Assert.DoesNotContain("type Counter", context.Snippet) + [] + let ``a one-line declaration keeps its doc comment`` () = + let context = contextOf "Widgets.twice" + + Assert.Contains("Twice the value.", context.Snippet) + Assert.Contains("let twice x", context.Snippet) + Assert.DoesNotContain("describeShape", context.Snippet) + [] [] [] @@ -105,6 +116,7 @@ let describeShape shape = let ``a context points back at the source it was taken from`` () = let context = contextOf "Widgets.Counter" let location = Assert.Single context.SnippetLocations + let document = solution.Projects |> Seq.exactlyOne |> _.Documents |> Seq.exactlyOne - Assert.Equal("C:\\test.fs", location.FilePath) + Assert.Equal(document.FilePath, location.FilePath) Assert.Equal(context.Snippet.Length, location.Span.Length) From 3e229f1ef83c4b0c2f365ccce88944e01d95558e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 19:37:43 +0200 Subject: [PATCH 039/133] Match declaration names over spans instead of building them Resolving a picked mention walks every declaration in every document of the solution, and asked each one for its dotted path as a fresh string purely to compare it. Compare against the container and name in place instead, so the scan allocates nothing per declaration. The doc-comment probe trimmed each candidate line into a new string for the same reason. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 579180c69abddf446452419badb3c3e355b07e19) --- .../Copilot/CopilotContextProvider.fs | 4 +--- .../Copilot/CopilotSymbolMapping.fs | 17 +++++++++++++++++ .../Copilot/CopilotSymbolSnippets.fs | 2 +- .../CopilotContextProviderTests.fs | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs index 71b6457d83e..7a5da42a1a2 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs @@ -99,9 +99,7 @@ module internal CopilotSymbolQuery = return items |> Seq.chooseV (fun item -> - if - String.Equals(CopilotSymbolMapping.fullyQualifiedName item, fullyQualifiedName, StringComparison.Ordinal) - then + if CopilotSymbolMapping.hasFullyQualifiedName fullyQualifiedName item then ValueSome struct (item, document) else ValueNone) diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs index a23d7aab14d..21e296c0476 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs @@ -3,6 +3,8 @@ /// Translates F# navigable items into the shapes the Copilot chat "#" mention picker understands. module internal Microsoft.VisualStudio.FSharp.Editor.CopilotSymbolMapping +open System + open Microsoft.VisualStudio.Copilot open Microsoft.VisualStudio.Imaging @@ -55,3 +57,18 @@ let fullyQualifiedName (item: NavigableItem) = match item.Container.FullName with | "" -> item.Name | container -> $"{container}.{item.Name}" + +/// Answers what comparing against `fullyQualifiedName` would, without building the dotted path - +/// a solution-wide scan asks this of every declaration it walks past. +let hasFullyQualifiedName (candidate: string) (item: NavigableItem) = + let candidate = candidate.AsSpan() + let container = item.Container.FullName + let name = item.Name.AsSpan() + + if container.Length = 0 then + candidate.Equals(name, StringComparison.Ordinal) + else + candidate.Length = container.Length + 1 + name.Length + && candidate[container.Length] = '.' + && candidate.Slice(0, container.Length).Equals(container.AsSpan(), StringComparison.Ordinal) + && candidate.Slice(container.Length + 1).Equals(name, StringComparison.Ordinal) diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs index 59c98c3fd21..b31d9c192bc 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs @@ -40,7 +40,7 @@ let definitionLines (sourceLines: string array) (scopes: Structure.ScopeRange se // Outlining reports a doc comment only once it spans several lines, so a one-line "///" in front of // a declaration is invisible to the scopes above. let isDocComment line = - sourceLines[line - 1].TrimStart().StartsWith("///", StringComparison.Ordinal) + sourceLines[line - 1].AsSpan().TrimStart().StartsWith("///".AsSpan(), StringComparison.Ordinal) let rec docCommentStart line = if line > 1 && isDocComment (line - 1) then diff --git a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs index f7fc6966b52..c7ad810ab4a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs @@ -70,6 +70,25 @@ let twice x = x * 2 let ``search finds a declaration by its fully qualified name`` (pattern: string, expected: string) = Assert.Contains(expected, search pattern) + [] + [] + [] + [] + [] + [] + [] + let ``a name matches only the declaration it spells out`` (candidate: string, expected: bool) = + let item = + CopilotSymbolQuery.search cache solution "Counter" + |> run + |> Array.pick (fun (struct (item, _)) -> + if CopilotSymbolMapping.fullyQualifiedName item = "Widgets.Counter" then + Some item + else + None) + + Assert.Equal(expected, CopilotSymbolMapping.hasFullyQualifiedName candidate item) + [] let ``search reports each declaration once`` () = let names = search "Counter" From 6a1a08d914815a97480e3624c62b8c2a639fc6ce Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 22:01:34 +0200 Subject: [PATCH 040/133] Slice source text instead of copying it line by line for outlining The editor's block structure built the sourceLines array for Structure.getOutliningRanges by calling ToString() per line, allocating a fresh string for the entire file on every outlining pass, once per keystroke. getOutliningRanges now takes ReadOnlyMemory[] and slices the already-materialized source text once (SourceText.GetLinesAsMemory()) instead. ReadOnlySpanCharExtensions in illib mirrors the existing Ordinal string helpers so span call sites read the same way string call sites do. A local recursive function closing over a ReadOnlySpan-typed sibling cannot be compiled - the CLR disallows instantiating FSharpFunc, _> as a closure field (FS0412) - so commentTypeOf moves to module scope, next to the CommentType it classifies. StructureTests.fs slices its own lines the same way at the call site, and FSharp.Compiler.Service.Tests needs a direct System.Memory PackageReference: FSharp.Compiler.Service's own reference to it is only transitive through the net472 ProjectReference's SetTargetFramework override, mirroring the FSharp.Core pin already in this project for the same reason. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 652ddb44c2589271316dbd9932263b9febd9a67f) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Service/ServiceStructure.fs | 61 ++++++++++--------- src/Compiler/Service/ServiceStructure.fsi | 3 +- src/Compiler/Utilities/illib.fs | 49 +++++++++++++++ src/Compiler/Utilities/illib.fsi | 42 +++++++++++++ ...iler.Service.SurfaceArea.netstandard20.bsl | 2 +- .../FSharp.Compiler.Service.Tests.fsproj | 6 ++ .../StructureTests.fs | 3 +- .../src/FSharp.Editor/Common/Extensions.fs | 8 +++ .../Structure/BlockStructureService.fs | 2 +- 10 files changed, 145 insertions(+), 32 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 ce7e18eb534..67645c6fea9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -220,3 +220,4 @@ * `FSharp.Compiler.Syntax.SynComponentInfo` now holds the type name as `synType: SynType option` instead of the previous `longId: LongIdent` field, so tuple-type extensions such as `type ('T1 * 'T2) with` can be represented. A `member LongIdent` compatibility property returns the long identifier for named types and an empty list for tuple or erroneous type names. AST consumers that pattern-matched on the `longId` field must switch to the `synType` field or the `LongIdent` member. ([PR #19602](https://github.com/dotnet/fsharp/pull/19602)) * Optimizer: don't inline named functions in debug builds ([PR #19548](https://github.com/dotnet/fsharp/pull/19548) * LexFilter: drop non-strict mode ([PR #20106](https://github.com/dotnet/fsharp/pull/20106)) +* `FSharp.Compiler.EditorServices.Structure.getOutliningRanges` now takes the source lines as `ReadOnlyMemory[]` instead of `string[]`, so a caller that already holds the whole text can slice it instead of building a string per line. Callers passing a `string[]` can migrate with `Array.map (fun line -> line.AsMemory())`. diff --git a/src/Compiler/Service/ServiceStructure.fs b/src/Compiler/Service/ServiceStructure.fs index 99dd528eeb0..f686d9ec452 100644 --- a/src/Compiler/Service/ServiceStructure.fs +++ b/src/Compiler/Service/ServiceStructure.fs @@ -2,6 +2,7 @@ namespace FSharp.Compiler.EditorServices +open System open Internal.Utilities.Library open FSharp.Compiler.Syntax open FSharp.Compiler.SyntaxTreeOps @@ -186,12 +187,21 @@ module Structure = } type LineNumber = int - type LineStr = string + type LineStr = ReadOnlyMemory type CommentType = | SingleLine | XmlDoc + /// Determine if a line is a single line or xml documentation comment. + /// Kept at module scope: a local recursive function capturing a `ReadOnlySpan`-typed + /// helper as a closure field would need to instantiate `FSharpFunc, _>`, + /// which the CLR disallows for byref-like type arguments (FS0412). + let commentTypeOf (line: ReadOnlySpan) = + if line.StartsWithOrdinal("///") then ValueSome XmlDoc + elif line.StartsWithOrdinal("//") then ValueSome SingleLine + else ValueNone + [] type CommentList = { @@ -206,7 +216,7 @@ module Structure = } /// Returns outlining ranges for given parsed input. - let getOutliningRanges (sourceLines: string[]) (parsedInput: ParsedInput) = + let getOutliningRanges (sourceLines: ReadOnlyMemory[]) (parsedInput: ParsedInput) = let acc = ResizeArray() /// Validation function to ensure that ranges yielded for outlining span 2 or more lines @@ -661,7 +671,7 @@ module Structure = | r :: rest, last :: _ when r.StartLine = last.EndLine + 1 || sourceLines[last.EndLine .. r.StartLine - 2] - |> Array.forall System.String.IsNullOrWhiteSpace + |> Array.forall (fun line -> line.Span.IsWhiteSpace()) -> loop rest res (r :: currentBulk) | r :: rest, _ -> loop rest (currentBulk :: res) [ r ] @@ -719,7 +729,7 @@ module Structure = let collectConditionalDirectives directives sourceLines = // Adds a fold region from prevRange.Start to the line above nextLine - let addSectionFold (prevRange: range) (nextLine: int) (sourceLines: string array) = + let addSectionFold (prevRange: range) (nextLine: int) (sourceLines: ReadOnlyMemory[]) = let startLineIndex = nextLine - 2 if startLineIndex >= 0 then @@ -753,7 +763,7 @@ module Structure = | ConditionalDirectiveTrivia.Else r -> ValueSome r | _ -> ValueNone - let rec group directives stack (sourceLines: string array) = + let rec group directives stack (sourceLines: ReadOnlyMemory[]) = match directives with | [] -> () | ConditionalDirectiveTrivia.If _ as ifDirective :: directives -> group directives (ifDirective :: stack) sourceLines @@ -822,19 +832,15 @@ module Structure = collectOpens decls List.iter parseDeclaration decls - /// Determine if a line is a single line or xml documentation comment - let (|Comment|_|) (line: string) = - if line.StartsWithOrdinal("///") then Some XmlDoc - elif line.StartsWithOrdinal("//") then Some SingleLine - else None - - let getCommentRanges trivia (lines: string[]) = - let rec loop (lastLineNum, currentComment, result as state) (lines: string list) lineNum = - match lines with - | [] -> state - | lineStr :: rest -> - match lineStr.TrimStart(), currentComment with - | Comment commentType, Some comment -> + let getCommentRanges trivia (lines: ReadOnlyMemory[]) = + let rec loop (lastLineNum, currentComment, result as state) lineNum = + if lineNum = lines.Length then + state + else + let lineStr = lines[lineNum] + + match commentTypeOf (lineStr.Span.TrimStart()), currentComment with + | ValueSome commentType, Some comment -> loop (if comment.Type = commentType && lineNum = lastLineNum + 1 then comment.Lines.Add(lineNum, lineStr) @@ -842,16 +848,15 @@ module Structure = else let comments = CommentList.New commentType (lineNum, lineStr) lineNum, Some comments, comment :: result) - rest (lineNum + 1) - | Comment commentType, None -> + | ValueSome commentType, None -> let comments = CommentList.New commentType (lineNum, lineStr) - loop (lineNum, Some comments, result) rest (lineNum + 1) - | _, Some comment -> loop (lineNum, None, comment :: result) rest (lineNum + 1) - | _ -> loop (lineNum, None, result) rest (lineNum + 1) + loop (lineNum, Some comments, result) (lineNum + 1) + | ValueNone, Some comment -> loop (lineNum, None, comment :: result) (lineNum + 1) + | ValueNone, None -> loop (lineNum, None, result) (lineNum + 1) let comments = - let _, lastComment, comments = loop (-1, None, []) (List.ofArray lines) 0 + let _, lastComment, comments = loop (-1, None, []) 0 match lastComment with | Some comment -> comment :: comments @@ -859,13 +864,13 @@ module Structure = |> List.rev comments - |> List.filter (fun comment -> comment.Lines.Count > 1) - |> List.map (fun comment -> + |> Seq.filter (fun comment -> comment.Lines.Count > 1) + |> Seq.map (fun comment -> let lines = comment.Lines let startLine, startStr = lines[0] let endLine, endStr = lines[lines.Count - 1] - let startCol = startStr.IndexOf '/' - let endCol = endStr.TrimEnd().Length + let startCol = startStr.Span.IndexOf '/' + let endCol = endStr.Span.TrimEnd().Length let scopeType = match comment.Type with diff --git a/src/Compiler/Service/ServiceStructure.fsi b/src/Compiler/Service/ServiceStructure.fsi index 87711629676..3695e7148ac 100644 --- a/src/Compiler/Service/ServiceStructure.fsi +++ b/src/Compiler/Service/ServiceStructure.fsi @@ -2,6 +2,7 @@ namespace FSharp.Compiler.EditorServices +open System open FSharp.Compiler.Syntax open FSharp.Compiler.Text @@ -79,4 +80,4 @@ module public Structure = } /// Returns outlining ranges for given parsed input. - val getOutliningRanges: sourceLines: string[] -> parsedInput: ParsedInput -> seq + val getOutliningRanges: sourceLines: ReadOnlyMemory[] -> parsedInput: ParsedInput -> seq diff --git a/src/Compiler/Utilities/illib.fs b/src/Compiler/Utilities/illib.fs index 5842492b31d..d180caa76a1 100644 --- a/src/Compiler/Utilities/illib.fs +++ b/src/Compiler/Utilities/illib.fs @@ -7,6 +7,7 @@ open System.Collections.Generic open System.Collections.Concurrent open System.Diagnostics open System.IO +open System.Linq open System.Threading open System.Threading.Tasks open System.Runtime.CompilerServices @@ -112,6 +113,54 @@ module internal PervasiveAutoOpens = member inline x.IndexOfOrdinal(value: string, startIndex, count) = x.IndexOf(value, startIndex, count, StringComparison.Ordinal) + [] + type ReadOnlySpanCharExtensions = + + static member inline StartsWithOrdinal(str: ReadOnlySpan, value: ReadOnlySpan) = + str.StartsWith(value, StringComparison.Ordinal) + + static member inline StartsWithOrdinal(str: ReadOnlySpan, value: string) = + str.StartsWith(value.AsSpan(), StringComparison.Ordinal) + + static member inline EndsWithOrdinal(str: ReadOnlySpan, value: ReadOnlySpan) = + str.EndsWith(value, StringComparison.Ordinal) + + static member inline EndsWithOrdinal(str: ReadOnlySpan, value: string) = + str.EndsWith(value.AsSpan(), StringComparison.Ordinal) + + static member inline EndsWithOrdinalIgnoreCase(str: ReadOnlySpan, value: ReadOnlySpan) = + str.EndsWith(value, StringComparison.OrdinalIgnoreCase) + + static member inline EndsWithOrdinalIgnoreCase(str: ReadOnlySpan, value: string) = + str.EndsWith(value.AsSpan(), StringComparison.OrdinalIgnoreCase) + + static member IndexOf(str: ReadOnlySpan, value: char) = + let mutable index = -1 + let mutable i = 0 + + while i < str.Length && index = -1 do + if str[i] = value then index <- i else i <- i + 1 + + index + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan) = + str.IndexOf(value, StringComparison.Ordinal) + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string) = + str.IndexOf(value.AsSpan(), StringComparison.Ordinal) + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan, startIndex) = + str.Slice(startIndex).IndexOf(value, StringComparison.Ordinal) + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string, startIndex) = + str.Slice(startIndex).IndexOf(value.AsSpan(), StringComparison.Ordinal) + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan, startIndex, count) = + str.Slice(startIndex, count).IndexOf(value, StringComparison.Ordinal) + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string, startIndex, count) = + str.Slice(startIndex, count).IndexOf(value.AsSpan(), StringComparison.Ordinal) + /// Get an initialization hole let getHole (r: _ ref) = match r.Value with diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index 7c598ffa7a2..3aa533289eb 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -68,6 +68,48 @@ module internal PervasiveAutoOpens = member inline IndexOfOrdinal: value: string * startIndex: int * count: int -> int + [] + type ReadOnlySpanCharExtensions = + + [] + static member inline StartsWithOrdinal: str : ReadOnlySpan * value: ReadOnlySpan -> bool + + [] + static member inline StartsWithOrdinal: str : ReadOnlySpan * value: string -> bool + + [] + static member inline EndsWithOrdinal: str : ReadOnlySpan * value: ReadOnlySpan -> bool + + [] + static member inline EndsWithOrdinal: str : ReadOnlySpan * value: string -> bool + + [] + static member inline EndsWithOrdinalIgnoreCase: str : ReadOnlySpan * value: ReadOnlySpan -> bool + + [] + static member inline EndsWithOrdinalIgnoreCase: str : ReadOnlySpan * value: string -> bool + + [] + static member IndexOf: str : ReadOnlySpan * value: char -> int + + [] + static member inline IndexOfOrdinal: str : ReadOnlySpan * value: ReadOnlySpan -> int + + [] + static member inline IndexOfOrdinal: str : ReadOnlySpan * value: string -> int + + [] + static member inline IndexOfOrdinal: str : ReadOnlySpan * value: ReadOnlySpan * startIndex: int -> int + + [] + static member inline IndexOfOrdinal: str : ReadOnlySpan * value: string * startIndex: int -> int + + [] + static member inline IndexOfOrdinal: str : ReadOnlySpan * value: ReadOnlySpan * startIndex: int * count: int -> int + + [] + static member inline IndexOfOrdinal: str : ReadOnlySpan * value: string * startIndex: int * count: int -> int + type Async with /// Runs the computation synchronously, always starting on the current thread. diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 391d2149a4f..de6a4b053d6 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -4744,7 +4744,7 @@ FSharp.Compiler.EditorServices.Structure+ScopeRange: Void .ctor(Scope, Collapse, FSharp.Compiler.EditorServices.Structure: FSharp.Compiler.EditorServices.Structure+Collapse FSharp.Compiler.EditorServices.Structure: FSharp.Compiler.EditorServices.Structure+Scope FSharp.Compiler.EditorServices.Structure: FSharp.Compiler.EditorServices.Structure+ScopeRange -FSharp.Compiler.EditorServices.Structure: System.Collections.Generic.IEnumerable`1[FSharp.Compiler.EditorServices.Structure+ScopeRange] getOutliningRanges(System.String[], FSharp.Compiler.Syntax.ParsedInput) +FSharp.Compiler.EditorServices.Structure: System.Collections.Generic.IEnumerable`1[FSharp.Compiler.EditorServices.Structure+ScopeRange] getOutliningRanges(System.ReadOnlyMemory`1[System.Char][], FSharp.Compiler.Syntax.ParsedInput) FSharp.Compiler.EditorServices.ToolTipElement+CompositionError: System.String errorText FSharp.Compiler.EditorServices.ToolTipElement+CompositionError: System.String get_errorText() FSharp.Compiler.EditorServices.ToolTipElement+Group: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.EditorServices.ToolTipElementData] elements diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index e043d8554ad..0183589a540 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -223,4 +223,10 @@ + + + + + diff --git a/tests/FSharp.Compiler.Service.Tests/StructureTests.fs b/tests/FSharp.Compiler.Service.Tests/StructureTests.fs index c0ae0d3fdff..d3bbe73e4b9 100644 --- a/tests/FSharp.Compiler.Service.Tests/StructureTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/StructureTests.fs @@ -1,5 +1,6 @@ module FSharp.Compiler.Service.Tests.StructureTests +open System open System.IO open Xunit open FSharp.Compiler.EditorServices.Structure @@ -35,7 +36,7 @@ let (=>) (source: string) (expectedRanges: (Range * Range) list) = let ast = parseSourceCode(fileName, source) try let actual = - getOutliningRanges lines ast + getOutliningRanges (lines |> Array.map (fun line -> line.AsMemory())) ast |> Seq.filter (fun sr -> sr.Range.StartLine <> sr.Range.EndLine) |> Seq.map (fun sr -> getRange sr.Range, getRange sr.CollapseRange) |> Seq.sort diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index b58ccfb5003..94cb707b3a0 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -303,6 +303,14 @@ type SourceText with member this.ToFSharpSourceText() = SourceText.weakTable.GetValue(this, Runtime.CompilerServices.ConditionalWeakTable<_, _>.CreateValueCallback(SourceText.create)) + /// The lines of the text, as slices of a single string rather than one string per line. + member this.GetLinesAsMemory() = + let text = this.ToString() + + Array.init this.Lines.Count (fun i -> + let line = this.Lines[i] + text.AsMemory(line.Start, line.End - line.Start)) + type NavigationItem with member x.RoslynGlyph: FSharpRoslynGlyph = diff --git a/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs b/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs index d0326d84311..b087cb56782 100644 --- a/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs +++ b/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs @@ -119,7 +119,7 @@ module internal BlockStructure = let ellipsis = "..." let createBlockSpans isBlockStructureEnabled (sourceText: SourceText) (parsedInput: ParsedInput) = - let linetext = sourceText.Lines |> Seq.map (fun x -> x.ToString()) |> Seq.toArray + let linetext = sourceText.GetLinesAsMemory() Structure.getOutliningRanges linetext parsedInput |> Seq.distinctBy (fun x -> x.Range.StartLine) From 279875f2456a0ee4385663ec5e59bdabeec2dd78 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 22:36:11 +0200 Subject: [PATCH 041/133] Track comment lines by number instead of storing their text CommentList kept a copy of every comment line next to its line number, but the number alone identifies the line in the source array the function already holds, and only the first and last lines of a group are ever read back to compute the fold's columns. Store the numbers and index the source at the end, so grouping comments allocates no tuple per line. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit f1d07cfc0c88f86be75e7a6d6c8af3b5a3f87fc0) --- src/Compiler/Service/ServiceStructure.fs | 26 ++-- .../StructureTests.fs | 116 +++++++++--------- 2 files changed, 69 insertions(+), 73 deletions(-) diff --git a/src/Compiler/Service/ServiceStructure.fs b/src/Compiler/Service/ServiceStructure.fs index f686d9ec452..6937981ecec 100644 --- a/src/Compiler/Service/ServiceStructure.fs +++ b/src/Compiler/Service/ServiceStructure.fs @@ -187,7 +187,6 @@ module Structure = } type LineNumber = int - type LineStr = ReadOnlyMemory type CommentType = | SingleLine @@ -205,14 +204,14 @@ module Structure = [] type CommentList = { - Lines: ResizeArray + Lines: ResizeArray Type: CommentType } - static member New ty lineStr = + static member New ty lineNum = { Type = ty - Lines = ResizeArray [ lineStr ] + Lines = ResizeArray [ lineNum ] } /// Returns outlining ranges for given parsed input. @@ -837,20 +836,18 @@ module Structure = if lineNum = lines.Length then state else - let lineStr = lines[lineNum] - - match commentTypeOf (lineStr.Span.TrimStart()), currentComment with + match commentTypeOf (lines[lineNum].Span.TrimStart()), currentComment with | ValueSome commentType, Some comment -> loop (if comment.Type = commentType && lineNum = lastLineNum + 1 then - comment.Lines.Add(lineNum, lineStr) + comment.Lines.Add lineNum lineNum, currentComment, result else - let comments = CommentList.New commentType (lineNum, lineStr) + let comments = CommentList.New commentType lineNum lineNum, Some comments, comment :: result) (lineNum + 1) | ValueSome commentType, None -> - let comments = CommentList.New commentType (lineNum, lineStr) + let comments = CommentList.New commentType lineNum loop (lineNum, Some comments, result) (lineNum + 1) | ValueNone, Some comment -> loop (lineNum, None, comment :: result) (lineNum + 1) | ValueNone, None -> loop (lineNum, None, result) (lineNum + 1) @@ -866,11 +863,10 @@ module Structure = comments |> Seq.filter (fun comment -> comment.Lines.Count > 1) |> Seq.map (fun comment -> - let lines = comment.Lines - let startLine, startStr = lines[0] - let endLine, endStr = lines[lines.Count - 1] - let startCol = startStr.Span.IndexOf '/' - let endCol = endStr.Span.TrimEnd().Length + let startLine = comment.Lines[0] + let endLine = comment.Lines[comment.Lines.Count - 1] + let startCol = lines[startLine].Span.IndexOf '/' + let endCol = lines[endLine].Span.TrimEnd().Length let scopeType = match comment.Type with diff --git a/tests/FSharp.Compiler.Service.Tests/StructureTests.fs b/tests/FSharp.Compiler.Service.Tests/StructureTests.fs index d3bbe73e4b9..322a0fa3bda 100644 --- a/tests/FSharp.Compiler.Service.Tests/StructureTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/StructureTests.fs @@ -36,7 +36,7 @@ let (=>) (source: string) (expectedRanges: (Range * Range) list) = let ast = parseSourceCode(fileName, source) try let actual = - getOutliningRanges (lines |> Array.map (fun line -> line.AsMemory())) ast + getOutliningRanges (lines |> Array.map _.AsMemory()) ast |> Seq.filter (fun sr -> sr.Range.StartLine <> sr.Range.EndLine) |> Seq.map (fun sr -> getRange sr.Range, getRange sr.CollapseRange) |> Seq.sort @@ -152,7 +152,7 @@ module MyModule = // 2 type Color = // 7 { Red: int Green: int - Blue: int + Blue: int } interface IDisposable with // 13 @@ -164,7 +164,7 @@ module MyModule = // 2 type RecordColor = // 19 { Red: int Green: int - Blue: int + Blue: int } interface IDisposable with // 25 @@ -190,31 +190,31 @@ module MyModule = // 2 [] let ``open statements``() = """ -open M -open N - -module M = - let x = 1 - - open M - open N - - module M = - open M - - let x = 1 - - module M = - open M - open N - let x = 1 - -open M -open N -open H - -open G -open H +open M +open N + +module M = + let x = 1 + + open M + open N + + module M = + open M + + let x = 1 + + module M = + open M + open N + let x = 1 + +open M +open N +open H + +open G +open H """ => [ (2, 0, 3, 6), (2, 0, 3, 6) (5, 0, 19, 17), (5, 8, 19, 17) @@ -227,28 +227,28 @@ open H [] let ``hash directives``() = """ -#r @"a" -#r "b" - -#r "c" - -#r "d" -#r "e" -let x = 1 - -#r "f" -#r "g" -#load "x" -#r "y" - -#load "a" - "b" - "c" - -#load "a" - "b" - "c" -#r "d" +#r @"a" +#r "b" + +#r "c" + +#r "d" +#r "e" +let x = 1 + +#r "f" +#r "g" +#load "x" +#r "y" + +#load "a" + "b" + "c" + +#load "a" + "b" + "c" +#r "d" """ => [ (2, 3, 8, 6), (2, 3, 8, 6) (11, 3, 23, 6), (11, 3, 23, 6) ] @@ -326,7 +326,7 @@ seq { // 2 [] let ``list``() = """ -let _ = +let _ = [ 1; 2 3 ] """ @@ -383,7 +383,7 @@ finally // 5 let ``if - then - else``() = """ if true then - let f x = + let f x = () () else @@ -449,7 +449,7 @@ for x = 100 downto 10 do [] let ``for each``() = """ -for x in 0 .. 100 -> +for x in 0 .. 100 -> () () """ @@ -468,7 +468,7 @@ let ``tuple``() = [] let ``do!``() = """ -do! +do! printfn "allo" printfn "allo" """ @@ -478,10 +478,10 @@ do! let ``cexpr yield yield!``() = """ cexpr{ - yield! + yield! cexpr{ - yield - + yield + 10 } } @@ -660,7 +660,7 @@ let ``Abstract members`` () = type T() = abstract Foo: int - + [] abstract Foo: int From baecfb50158c3d73ee460e166738a1e6f23622a5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 22:39:21 +0200 Subject: [PATCH 042/133] Link the outlining release note to its PR Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 27b19d41b369672ce6453659fb3fefeba3ffd995) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 67645c6fea9..fc4acb814d2 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -220,4 +220,4 @@ * `FSharp.Compiler.Syntax.SynComponentInfo` now holds the type name as `synType: SynType option` instead of the previous `longId: LongIdent` field, so tuple-type extensions such as `type ('T1 * 'T2) with` can be represented. A `member LongIdent` compatibility property returns the long identifier for named types and an empty list for tuple or erroneous type names. AST consumers that pattern-matched on the `longId` field must switch to the `synType` field or the `LongIdent` member. ([PR #19602](https://github.com/dotnet/fsharp/pull/19602)) * Optimizer: don't inline named functions in debug builds ([PR #19548](https://github.com/dotnet/fsharp/pull/19548) * LexFilter: drop non-strict mode ([PR #20106](https://github.com/dotnet/fsharp/pull/20106)) -* `FSharp.Compiler.EditorServices.Structure.getOutliningRanges` now takes the source lines as `ReadOnlyMemory[]` instead of `string[]`, so a caller that already holds the whole text can slice it instead of building a string per line. Callers passing a `string[]` can migrate with `Array.map (fun line -> line.AsMemory())`. +* `FSharp.Compiler.EditorServices.Structure.getOutliningRanges` now takes the source lines as `ReadOnlyMemory[]` instead of `string[]`, so a caller that already holds the whole text can slice it instead of building a string per line. Callers passing a `string[]` can migrate with `Array.map (fun line -> line.AsMemory())`. ([PR #20443](https://github.com/dotnet/fsharp/pull/20443)) From 56765289fc852d257286a45c6eac001e2afd0d9e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 22:54:31 +0200 Subject: [PATCH 043/133] Format illib.fsi and add the VisualStudio release note CheckCodeFormatting flagged illib.fsi for a stray space before the colon in the ReadOnlySpanCharExtensions signatures; dotnet fantomas fixes it mechanically, no signature changes. check_release_notes also requires an entry for changes under vsintegration/src. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 2200592bf9003a6c5a5a90c1adacec6126709e3e) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + src/Compiler/Utilities/illib.fsi | 29 +++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index bf97f69f287..eb747453cbc 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -27,3 +27,4 @@ * Rename "inline hints" to "inlay hints" in VS options for consistency with industry terminology. ([PR #19318](https://github.com/dotnet/fsharp/pull/19318)) * Unused analyzers: disable in VS when file has errors ([PR #19892](https://github.com/dotnet/fsharp/pull/19892)) * Move to Roslyn's unified ExternalAccess library ([PR #20099](https://github.com/dotnet/fsharp/pull/20099)) +* Reduce editor allocations when computing block structure (code folding) by slicing the source text once per outlining pass instead of copying every line. ([PR #20443](https://github.com/dotnet/fsharp/pull/20443)) diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index 3aa533289eb..65423b11139 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -72,43 +72,46 @@ module internal PervasiveAutoOpens = type ReadOnlySpanCharExtensions = [] - static member inline StartsWithOrdinal: str : ReadOnlySpan * value: ReadOnlySpan -> bool + static member inline StartsWithOrdinal: str: ReadOnlySpan * value: ReadOnlySpan -> bool [] - static member inline StartsWithOrdinal: str : ReadOnlySpan * value: string -> bool + static member inline StartsWithOrdinal: str: ReadOnlySpan * value: string -> bool [] - static member inline EndsWithOrdinal: str : ReadOnlySpan * value: ReadOnlySpan -> bool + static member inline EndsWithOrdinal: str: ReadOnlySpan * value: ReadOnlySpan -> bool [] - static member inline EndsWithOrdinal: str : ReadOnlySpan * value: string -> bool + static member inline EndsWithOrdinal: str: ReadOnlySpan * value: string -> bool [] - static member inline EndsWithOrdinalIgnoreCase: str : ReadOnlySpan * value: ReadOnlySpan -> bool + static member inline EndsWithOrdinalIgnoreCase: str: ReadOnlySpan * value: ReadOnlySpan -> bool [] - static member inline EndsWithOrdinalIgnoreCase: str : ReadOnlySpan * value: string -> bool + static member inline EndsWithOrdinalIgnoreCase: str: ReadOnlySpan * value: string -> bool [] - static member IndexOf: str : ReadOnlySpan * value: char -> int + static member IndexOf: str: ReadOnlySpan * value: char -> int [] - static member inline IndexOfOrdinal: str : ReadOnlySpan * value: ReadOnlySpan -> int + static member inline IndexOfOrdinal: str: ReadOnlySpan * value: ReadOnlySpan -> int [] - static member inline IndexOfOrdinal: str : ReadOnlySpan * value: string -> int + static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string -> int [] - static member inline IndexOfOrdinal: str : ReadOnlySpan * value: ReadOnlySpan * startIndex: int -> int + static member inline IndexOfOrdinal: + str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int -> int [] - static member inline IndexOfOrdinal: str : ReadOnlySpan * value: string * startIndex: int -> int + static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string * startIndex: int -> int [] - static member inline IndexOfOrdinal: str : ReadOnlySpan * value: ReadOnlySpan * startIndex: int * count: int -> int + static member inline IndexOfOrdinal: + str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int * count: int -> int [] - static member inline IndexOfOrdinal: str : ReadOnlySpan * value: string * startIndex: int * count: int -> int + static member inline IndexOfOrdinal: + str: ReadOnlySpan * value: string * startIndex: int * count: int -> int type Async with From c6eb62668526842170b8d1238907dc0cae6c3e02 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 3 Sep 2026 22:56:07 +0200 Subject: [PATCH 044/133] Scope the System.Memory pin to the net472 test build Plain_Build_Windows and Plain_Build_Linux both failed with NU1510: on the .NET Core inner build System.Memory ships with the framework, and NuGet's package-pruning check treats an unconditional explicit PackageReference to it as an error. The pin is only needed on net472, where FSharp.Compiler.Service's own PackageReference to System.Memory doesn't flow through the netstandard2.0 SetTargetFramework override. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit ad7766c6f1898d5f500e1f91a7ea2795cd821afc) --- .../FSharp.Compiler.Service.Tests.fsproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index 0183589a540..39f9165e8f5 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -223,9 +223,11 @@ - + + transitive here, so the SetTargetFramework override above does not carry it in on net472; pin it directly. + Not needed on the .NET Core inner build: there System.Memory ships with the framework and NuGet's + package-pruning check (NU1510) errors on an explicit reference to it. --> From 496c333288390bbf5edffe4fcbdc657c568556b8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 01:51:46 +0200 Subject: [PATCH 045/133] Address review: XML doc structure and shorthand lambda Wrap commentTypeOf's doc comment in , move the FS0412 rationale into , and reference the types through rather than inline code spans. Use the shorthand lambda for the whitespace check, per review suggestion. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 46ee862b6934fedc606303df97cbebc5cc6e531d) --- src/Compiler/Service/ServiceStructure.fs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Compiler/Service/ServiceStructure.fs b/src/Compiler/Service/ServiceStructure.fs index 6937981ecec..db5eb5581d5 100644 --- a/src/Compiler/Service/ServiceStructure.fs +++ b/src/Compiler/Service/ServiceStructure.fs @@ -192,10 +192,14 @@ module Structure = | SingleLine | XmlDoc - /// Determine if a line is a single line or xml documentation comment. - /// Kept at module scope: a local recursive function capturing a `ReadOnlySpan`-typed - /// helper as a closure field would need to instantiate `FSharpFunc, _>`, - /// which the CLR disallows for byref-like type arguments (FS0412). + /// + /// Determines if a line is a single line or xml documentation comment. + /// + /// + /// Kept at module scope: a local recursive function capturing a -typed + /// helper as a closure field would need to instantiate + /// over it, which the CLR disallows for byref-like type arguments (FS0412). + /// let commentTypeOf (line: ReadOnlySpan) = if line.StartsWithOrdinal("///") then ValueSome XmlDoc elif line.StartsWithOrdinal("//") then ValueSome SingleLine @@ -670,7 +674,7 @@ module Structure = | r :: rest, last :: _ when r.StartLine = last.EndLine + 1 || sourceLines[last.EndLine .. r.StartLine - 2] - |> Array.forall (fun line -> line.Span.IsWhiteSpace()) + |> Array.forall _.Span.IsWhiteSpace() -> loop rest res (r :: currentBulk) | r :: rest, _ -> loop rest (currentBulk :: res) [ r ] From cf7976173411c9fc80ed97d4266e14458ef9d965 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:13:35 +0200 Subject: [PATCH 046/133] Drop the span IndexOfOrdinal overloads that take a start index Slicing before searching makes the result relative to the slice, while the String siblings these mirror return an index into the whole string. A call ported from the string path would land a column short by startIndex, and "not found" would come back as -1 from the slice rather than from the string. Nothing calls them. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit fadd55d302b96f3120d24bd0c981f09907cbb70d) --- src/Compiler/Utilities/illib.fs | 12 ------------ src/Compiler/Utilities/illib.fsi | 15 --------------- 2 files changed, 27 deletions(-) diff --git a/src/Compiler/Utilities/illib.fs b/src/Compiler/Utilities/illib.fs index d180caa76a1..a1f46fe4b48 100644 --- a/src/Compiler/Utilities/illib.fs +++ b/src/Compiler/Utilities/illib.fs @@ -149,18 +149,6 @@ module internal PervasiveAutoOpens = static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string) = str.IndexOf(value.AsSpan(), StringComparison.Ordinal) - static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan, startIndex) = - str.Slice(startIndex).IndexOf(value, StringComparison.Ordinal) - - static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string, startIndex) = - str.Slice(startIndex).IndexOf(value.AsSpan(), StringComparison.Ordinal) - - static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan, startIndex, count) = - str.Slice(startIndex, count).IndexOf(value, StringComparison.Ordinal) - - static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string, startIndex, count) = - str.Slice(startIndex, count).IndexOf(value.AsSpan(), StringComparison.Ordinal) - /// Get an initialization hole let getHole (r: _ ref) = match r.Value with diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index 65423b11139..c3797de6c92 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -98,21 +98,6 @@ module internal PervasiveAutoOpens = [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string -> int - [] - static member inline IndexOfOrdinal: - str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int -> int - - [] - static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string * startIndex: int -> int - - [] - static member inline IndexOfOrdinal: - str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int * count: int -> int - - [] - static member inline IndexOfOrdinal: - str: ReadOnlySpan * value: string * startIndex: int * count: int -> int - type Async with /// Runs the computation synchronously, always starting on the current thread. From 657da41b65b05304649a5cb12ee17510e64b0be9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:28:19 +0200 Subject: [PATCH 047/133] Report an absolute index from the span IndexOfOrdinal overloads Restores the startIndex overloads dropped in c9fbf5a36f, this time reporting the position in the span they were given rather than in the slice they searched, which is what the String siblings they mirror return. A miss still comes back as -1 rather than as startIndex - 1. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 8e642e7751eb220987b37912ab03a5823c703578) --- src/Compiler/Utilities/illib.fs | 21 +++++++++++++++++++++ src/Compiler/Utilities/illib.fsi | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Compiler/Utilities/illib.fs b/src/Compiler/Utilities/illib.fs index a1f46fe4b48..3b09abab981 100644 --- a/src/Compiler/Utilities/illib.fs +++ b/src/Compiler/Utilities/illib.fs @@ -149,6 +149,27 @@ module internal PervasiveAutoOpens = static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string) = str.IndexOf(value.AsSpan(), StringComparison.Ordinal) + // Searching a slice answers with an index into that slice, so the offset goes back on to + // report a position in `str` - what the String siblings these mirror return. A miss stays -1. + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan, startIndex) = + let i = str.Slice(startIndex).IndexOf(value, StringComparison.Ordinal) + if i < 0 then i else i + startIndex + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string, startIndex) = + let i = str.Slice(startIndex).IndexOf(value.AsSpan(), StringComparison.Ordinal) + if i < 0 then i else i + startIndex + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: ReadOnlySpan, startIndex, count) = + let i = str.Slice(startIndex, count).IndexOf(value, StringComparison.Ordinal) + if i < 0 then i else i + startIndex + + static member inline IndexOfOrdinal(str: ReadOnlySpan, value: string, startIndex, count) = + let i = + str.Slice(startIndex, count).IndexOf(value.AsSpan(), StringComparison.Ordinal) + + if i < 0 then i else i + startIndex + /// Get an initialization hole let getHole (r: _ ref) = match r.Value with diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index c3797de6c92..1a201c1b3cf 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -98,6 +98,29 @@ module internal PervasiveAutoOpens = [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string -> int + /// Returns a position in , not in the slice searched, matching the + /// overloads these mirror. -1 when there is no match. + [] + static member inline IndexOfOrdinal: + str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int -> int + + /// Returns a position in , not in the slice searched, matching the + /// overloads these mirror. -1 when there is no match. + [] + static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string * startIndex: int -> int + + /// Returns a position in , not in the slice searched, matching the + /// overloads these mirror. -1 when there is no match. + [] + static member inline IndexOfOrdinal: + str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int * count: int -> int + + /// Returns a position in , not in the slice searched, matching the + /// overloads these mirror. -1 when there is no match. + [] + static member inline IndexOfOrdinal: + str: ReadOnlySpan * value: string * startIndex: int * count: int -> int + type Async with /// Runs the computation synchronously, always starting on the current thread. From 0834d2ea6f4aeceb80d2bf9649163000453160cb Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 19:20:01 +0200 Subject: [PATCH 048/133] Wrap the IndexOfOrdinal startIndex overloads' doc text in summary A doc comment that carries markup like / needs that text inside - otherwise it renders as raw text in the generated XML and in tooltips. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 6c8949ccabdf236b46f358f292fbf53b4de75412) --- src/Compiler/Utilities/illib.fsi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index 1a201c1b3cf..cad5703174b 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -98,25 +98,33 @@ module internal PervasiveAutoOpens = [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string -> int + /// /// Returns a position in , not in the slice searched, matching the /// overloads these mirror. -1 when there is no match. + /// [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int -> int + /// /// Returns a position in , not in the slice searched, matching the /// overloads these mirror. -1 when there is no match. + /// [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string * startIndex: int -> int + /// /// Returns a position in , not in the slice searched, matching the /// overloads these mirror. -1 when there is no match. + /// [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: ReadOnlySpan * startIndex: int * count: int -> int + /// /// Returns a position in , not in the slice searched, matching the /// overloads these mirror. -1 when there is no match. + /// [] static member inline IndexOfOrdinal: str: ReadOnlySpan * value: string * startIndex: int * count: int -> int From 1fb1ebd9ef01798fcac57a7b31e92b2dead1fd03 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 22:20:16 +0200 Subject: [PATCH 049/133] Use a struct tuple for the comment-scan accumulator getCommentRanges recurses once per line, threading a three-way state through every call; a reference tuple heap-allocates on each of those recursive calls, a struct tuple doesn't. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit a3e8f4984012eda2533c9b86bb9f94abe5119d99) --- src/Compiler/Service/ServiceStructure.fs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Service/ServiceStructure.fs b/src/Compiler/Service/ServiceStructure.fs index db5eb5581d5..72e2ddf9943 100644 --- a/src/Compiler/Service/ServiceStructure.fs +++ b/src/Compiler/Service/ServiceStructure.fs @@ -836,7 +836,7 @@ module Structure = List.iter parseDeclaration decls let getCommentRanges trivia (lines: ReadOnlyMemory[]) = - let rec loop (lastLineNum, currentComment, result as state) lineNum = + let rec loop (struct (lastLineNum, currentComment, result) as state) lineNum = if lineNum = lines.Length then state else @@ -845,19 +845,19 @@ module Structure = loop (if comment.Type = commentType && lineNum = lastLineNum + 1 then comment.Lines.Add lineNum - lineNum, currentComment, result + struct (lineNum, currentComment, result) else let comments = CommentList.New commentType lineNum - lineNum, Some comments, comment :: result) + struct (lineNum, Some comments, comment :: result)) (lineNum + 1) | ValueSome commentType, None -> let comments = CommentList.New commentType lineNum - loop (lineNum, Some comments, result) (lineNum + 1) - | ValueNone, Some comment -> loop (lineNum, None, comment :: result) (lineNum + 1) - | ValueNone, None -> loop (lineNum, None, result) (lineNum + 1) + loop (struct (lineNum, Some comments, result)) (lineNum + 1) + | ValueNone, Some comment -> loop (struct (lineNum, None, comment :: result)) (lineNum + 1) + | ValueNone, None -> loop (struct (lineNum, None, result)) (lineNum + 1) let comments = - let _, lastComment, comments = loop (-1, None, []) 0 + let struct (_, lastComment, comments) = loop (struct (-1, None, [])) 0 match lastComment with | Some comment -> comment :: comments From cc79d7e5131c861528274b69be8c7f7f671f03d4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 2 Sep 2026 22:37:31 +0200 Subject: [PATCH 050/133] Fix release notes check: add PR link and VS entry Compiler service note was missing the PR #20274 link; the VS release notes were missing an entry entirely for the vsintegration changes. (cherry picked from commit 1919e0b7b1b0adea33143fd4aa13ff2a5ea6a834) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 fc4acb814d2..0dc17df4d15 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -59,7 +59,7 @@ * Narrow overload-resolution error ranges to the method name only instead of covering the entire expression. ([Issue #14284](https://github.com/dotnet/fsharp/issues/14284), [PR #19505](https://github.com/dotnet/fsharp/pull/19505)) * Fix attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) -* Fix a `NullReferenceException` in typed-tree remapping and semantic classification when an unlinked placeholder entity is encountered while editing broken code. The compiler now treats these placeholder entities as absent instead of crashing the whole file's coloring/classification pipeline. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269)) +* Fix a `NullReferenceException` in typed-tree remapping and semantic classification when an unlinked placeholder entity is encountered while editing broken code. The compiler now treats these placeholder entities as absent instead of crashing the whole file's coloring/classification pipeline. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix abstract event accessors now have SpecialName flag. (Issue [#5834](https://github.com/dotnet/fsharp/issues/5834), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix warning 20 ("expression is implicitly ignored") pointing at the wrong range when the last expression in a sequential block (e.g. inside `for`, `while` loops) is non-unit. The squiggle now correctly highlights only the offending expression. ([Issue #5418](https://github.com/dotnet/fsharp/issues/5418), [PR #19504](https://github.com/dotnet/fsharp/pull/19504)) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index eb747453cbc..d8afa99ffb0 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -21,6 +21,7 @@ * Fix doubled F# diagnostics in tooltips. ([Issue #16360](https://github.com/dotnet/fsharp/issues/16360)) * Fix `NotSupportedException` in the memory-mapped-file optimization when copying `ReadOnlyMemory` into `MemoryMappedFileViewStream`. ([Issue #20263](https://github.com/dotnet/fsharp/issues/20263)) * Reduce allocations in the VS project options reactor: the command-line options and project options caches and the mailbox reply payloads now hold struct tuples, and `IProjectSite.CompilationBinOutputPath` returns `string voption` picked with a new `Array.tryPickV`. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413)) +* Fix `NullReferenceException` that broke syntax coloring for the rest of the file when an unlinked placeholder entity was encountered while editing broken code. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) ### Changed From 4db8ac7e7fa1eaf0657a6833eb62bb0177604b6b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:09:09 +0200 Subject: [PATCH 051/133] Drop conditional compilation in WeakMap.cacheConditionally FCS ships netstandard2.0 only, so the TryAdd branch never compiled; keep the single try/Add form for every TFM. (cherry picked from commit 5131c833dfece0dd3173d634b2c611d3deb45d51) --- src/Compiler/Utilities/lib.fs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Compiler/Utilities/lib.fs b/src/Compiler/Utilities/lib.fs index bb8e5220f00..a344137a34b 100755 --- a/src/Compiler/Utilities/lib.fs +++ b/src/Compiler/Utilities/lib.fs @@ -499,9 +499,10 @@ module WeakMap = | false, _ -> let value = valueFactory key if shouldCache value then -#if NETSTANDARD2_0 - try table.Add(key, value) with | :? ArgumentException -> () -#else - table.TryAdd(key, value) |> ignore -#endif + // TODO: FCS ships netstandard2.0 only right now; switch to table.TryAdd once a newer TFM ships. + try + table.Add(key, value) + with :? ArgumentException -> + () + value From 4d54297a33d5f77fe3ba592dee5656f3b7473723 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:46:35 +0200 Subject: [PATCH 052/133] Address review: fail fast when pickling, no writes from getters The pickle writer went back to nonNull: a genuinely unlinked entity must fail rather than serialize empty contents into metadata. #20269 is a read-path crash, so only the read path is guarded. ModuleOrNamespaceType and TypeContents no longer fill the field in on read - a getter that mutates shared typed-tree state races under parallel checking. They return a shared empty stand-in instead. Adds a regression test for both properties, and drops the whitespace reformat of TypedTree.fs, the lib.fs ifdef and the pickle cleanups that had nothing to do with the fix. (cherry picked from commit 24849b8afe343bd2621bc38d1adf55c9bff4b6cc) --- src/Compiler/TypedTree/TypedTree.fs | 2655 +++++++++-------- src/Compiler/TypedTree/TypedTree.fsi | 8 + src/Compiler/TypedTree/TypedTreePickle.fs | 15 +- src/Compiler/Utilities/lib.fs | 11 +- .../FSharp.Compiler.Service.Tests.fsproj | 1 + .../UnlinkedEntityTests.fs | 33 + 6 files changed, 1383 insertions(+), 1340 deletions(-) create mode 100644 tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index 7949ac042be..3425ce4d911 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. - + /// Defines the typed abstract syntax intermediate representation used throughout the F# compiler. -module internal rec FSharp.Compiler.TypedTree +module internal rec FSharp.Compiler.TypedTree open System open System.Collections.Generic @@ -13,8 +13,8 @@ open Internal.Utilities.Library open Internal.Utilities.Library.Extras open Internal.Utilities.Rational -open FSharp.Compiler -open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler +open FSharp.Compiler.AbstractIL.IL open FSharp.Compiler.AbstractIL.ILX.Types open FSharp.Compiler.CompilerGlobalState open FSharp.Compiler.DiagnosticsLogger @@ -47,62 +47,62 @@ type ValInline = | Never | InlinedDefinition - member x.ShouldInline = - match x with - | ValInline.Always -> true + member x.ShouldInline = + match x with + | ValInline.Always -> true | ValInline.Optional | ValInline.Never | ValInline.InlinedDefinition -> false -/// A flag associated with values that indicates whether the recursive scope of the value is currently being processed, and +/// A flag associated with values that indicates whether the recursive scope of the value is currently being processed, and /// if the value has been generalized or not as yet. type ValRecursiveScopeInfo = - /// Set while the value is within its recursive scope. The flag indicates if the value has been eagerly generalized and accepts generic-recursive calls + /// Set while the value is within its recursive scope. The flag indicates if the value has been eagerly generalized and accepts generic-recursive calls | ValInRecScope of bool - /// The normal value for this flag when the value is not within its recursive scope + /// The normal value for this flag when the value is not within its recursive scope | ValNotInRecScope -type ValMutability = - | Immutable - | Mutable +type ValMutability = + | Immutable + | Mutable /// Indicates if a type parameter is needed at runtime and may not be eliminated [] -type TyparDynamicReq = +type TyparDynamicReq = /// Indicates the type parameter is not needed at runtime and may be eliminated - | No + | No /// Indicates the type parameter is needed at runtime and may not be eliminated | Yes -type ValBaseOrThisInfo = +type ValBaseOrThisInfo = - /// Indicates a ref-cell holding 'this' or the implicit 'this' used throughout an + /// Indicates a ref-cell holding 'this' or the implicit 'this' used throughout an /// implicit constructor to access and set values - | CtorThisVal + | CtorThisVal /// Indicates the value called 'base' available for calling base class members - | BaseVal + | BaseVal /// Indicates a normal value - | NormalVal + | NormalVal /// Indicates the 'this' value specified in a member e.g. 'x' in 'member x.M() = 1' - | MemberThisVal + | MemberThisVal /// Flags on values [] -type ValFlags(flags: int64) = +type ValFlags(flags: int64) = new (recValInfo, baseOrThis, isCompGen, inlineInfo, isMutable, isModuleOrMemberBinding, isExtensionMember, isIncrClassSpecialMember, isTyFunc, allowTypeInst, isGeneratedEventVal) = - let flags = + let flags = (match baseOrThis with | BaseVal -> 0b00000000000000000000L | CtorThisVal -> 0b00000000000000000010L | NormalVal -> 0b00000000000000000100L | MemberThisVal -> 0b00000000000000000110L) ||| - (if isCompGen then 0b00000000000000001000L + (if isCompGen then 0b00000000000000001000L else 0b000000000000000000000L) ||| (match inlineInfo with | ValInline.InlinedDefinition -> 0b00000000000000000000L @@ -137,12 +137,12 @@ type ValFlags(flags: int64) = (match isGeneratedEventVal with | false -> 0b00000000000000000000L - | true -> 0b00100000000000000000L) + | true -> 0b00100000000000000000L) ValFlags flags - member x.BaseOrThisInfo = - match (flags &&& 0b00000000000000000110L) with + member x.BaseOrThisInfo = + match (flags &&& 0b00000000000000000110L) with | 0b00000000000000000000L -> BaseVal | 0b00000000000000000010L -> CtorThisVal | 0b00000000000000000100L -> NormalVal @@ -153,15 +153,15 @@ type ValFlags(flags: int64) = member x.IsCompilerGenerated = (flags &&& 0b00000000000000001000L) <> 0x0L - member x.WithIsCompilerGenerated isCompGen = + member x.WithIsCompilerGenerated isCompGen = let flags = (flags &&& ~~~0b00000000000000001000L) ||| (match isCompGen with | false -> 0b00000000000000000000L | true -> 0b00000000000000001000L) ValFlags flags - member x.InlineInfo = - match (flags &&& 0b00000000000000110000L) with + member x.InlineInfo = + match (flags &&& 0b00000000000000110000L) with | 0b00000000000000000000L -> ValInline.InlinedDefinition | 0b00000000000000010000L -> ValInline.Always | 0b00000000000000100000L -> ValInline.Optional @@ -178,15 +178,15 @@ type ValFlags(flags: int64) = | ValInline.Never -> 0b00000000000000110000L) ValFlags flags - member x.MutabilityInfo = - match (flags &&& 0b00000000000001000000L) with + member x.MutabilityInfo = + match (flags &&& 0b00000000000001000000L) with | 0b00000000000000000000L -> Immutable | 0b00000000000001000000L -> Mutable | _ -> failwith "unreachable" - member x.IsMemberOrModuleBinding = - match (flags &&& 0b00000000000010000000L) with + member x.IsMemberOrModuleBinding = + match (flags &&& 0b00000000000010000000L) with | 0b00000000000000000000L -> false | 0b00000000000010000000L -> true | _ -> failwith "unreachable" @@ -201,19 +201,19 @@ type ValFlags(flags: int64) = member x.IsTypeFunction = (flags &&& 0b00000000010000000000L) <> 0L - member x.RecursiveValInfo = match (flags &&& 0b00000001100000000000L) with + member x.RecursiveValInfo = match (flags &&& 0b00000001100000000000L) with | 0b00000000000000000000L -> ValNotInRecScope | 0b00000000100000000000L -> ValInRecScope true | 0b00000001000000000000L -> ValInRecScope false | _ -> failwith "unreachable" - member x.WithRecursiveValInfo recValInfo = - let flags = + member x.WithRecursiveValInfo recValInfo = + let flags = (flags &&& ~~~0b00000001100000000000L) ||| (match recValInfo with | ValNotInRecScope -> 0b00000000000000000000L | ValInRecScope true -> 0b00000000100000000000L - | ValInRecScope false -> 0b00000001000000000000L) + | ValInRecScope false -> 0b00000001000000000000L) ValFlags flags member x.MakesNoCriticalTailcalls = (flags &&& 0b00000010000000000000L) <> 0L @@ -253,7 +253,7 @@ type ValFlags(flags: int64) = member x.WithIsParameter = ValFlags(flags ||| 0b10000000000000000000000L) /// Get the flags as included in the F# binary metadata - member x.PickledBits = + member x.PickledBits = // Clear the RecursiveValInfo, only used during inference and irrelevant across assembly boundaries // Clear the IsCompiledAsStaticPropertyWithoutField, only used to determine whether to use a true field for a value, and to eliminate the optimization info for observable bindings // Clear the HasBeenReferenced, only used to report "unreferenced variable" warnings and to help collect 'it' values in FSI.EXE @@ -279,9 +279,9 @@ type ValFlags(flags: int64) = /// Represents the kind of a type parameter [] -type TyparKind = +type TyparKind = - | Type + | Type | Measure @@ -299,18 +299,18 @@ type TyparKind = | TyparKind.Measure -> "measure" /// Indicates if the type variable can be solved or given new constraints. The status of a type variable -/// evolves towards being either rigid or solved. +/// evolves towards being either rigid or solved. [] -type TyparRigidity = +type TyparRigidity = /// Indicates the type parameter can't be solved - | Rigid + | Rigid /// Indicates the type parameter can't be solved, but the variable is not set to "rigid" until after inference is complete - | WillBeRigid + | WillBeRigid /// Indicates we give a warning if the type parameter is ever solved - | WarnIfNotRigid + | WarnIfNotRigid /// Indicates the type parameter is an inference variable may be solved | Flexible @@ -326,11 +326,11 @@ type TyparRigidity = member x.WarnIfMissingConstraint = match x with TyparRigidity.WillBeRigid -> true | _ -> false -/// Encode typar flags into a bit field +/// Encode typar flags into a bit field [] type TyparFlags(flags: int32) = - new (kind: TyparKind, rigidity: TyparRigidity, isFromError: bool, isCompGen: bool, staticReq: TyparStaticReq, dynamicReq: TyparDynamicReq, equalityDependsOn: bool, comparisonDependsOn: bool, supportsNullFlex: bool) = + new (kind: TyparKind, rigidity: TyparRigidity, isFromError: bool, isCompGen: bool, staticReq: TyparStaticReq, dynamicReq: TyparDynamicReq, equalityDependsOn: bool, comparisonDependsOn: bool, supportsNullFlex: bool) = TyparFlags((if isFromError then 0b00000000000000010 else 0) ||| (if isCompGen then 0b00000000000000100 else 0) ||| (match staticReq with @@ -344,13 +344,13 @@ type TyparFlags(flags: int32) = | TyparRigidity.Anon -> 0b00000000010000000) ||| (match kind with | TyparKind.Type -> 0b00000000000000000 - | TyparKind.Measure -> 0b00000000100000000) ||| - (if comparisonDependsOn then + | TyparKind.Measure -> 0b00000000100000000) ||| + (if comparisonDependsOn then 0b00000001000000000 else 0) ||| (match dynamicReq with | TyparDynamicReq.No -> 0b00000000000000000 | TyparDynamicReq.Yes -> 0b00000010000000000) ||| - (if equalityDependsOn then + (if equalityDependsOn then 0b00000100000000000 else 0) ||| // 0b00001000100000000 is being checked by x.Kind, but never set in this version of the code // 0b00010000000000000 is taken by compat flex @@ -360,20 +360,20 @@ type TyparFlags(flags: int32) = /// Indicates if the type inference variable was generated after an error when type checking expressions or patterns member x.IsFromError = (flags &&& 0b00000000000000010) <> 0x0 - /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable + /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable member x.IsCompilerGenerated = (flags &&& 0b00000000000000100) <> 0x0 /// Indicates if the type variable has a static "head type" requirement, i.e. ^a variables used in FSharp.Core and member constraints. - member x.StaticReq = - match (flags &&& 0b00000000000001000) with + member x.StaticReq = + match (flags &&& 0b00000000000001000) with | 0b00000000000000000 -> TyparStaticReq.None | 0b00000000000001000 -> TyparStaticReq.HeadType | _ -> failwith "unreachable" /// Indicates if the type variable can be solved or given new constraints. The status of a type variable - /// generally always evolves towards being either rigid or solved. - member x.Rigidity = - match (flags &&& 0b00000000011100000) with + /// generally always evolves towards being either rigid or solved. + member x.Rigidity = + match (flags &&& 0b00000000011100000) with | 0b00000000000000000 -> TyparRigidity.Rigid | 0b00000000000100000 -> TyparRigidity.WillBeRigid | 0b00000000001000000 -> TyparRigidity.WarnIfNotRigid @@ -382,8 +382,8 @@ type TyparFlags(flags: int32) = | _ -> failwith "unreachable" /// Indicates whether a type variable can be instantiated by types or units-of-measure. - member x.Kind = - match (flags &&& 0b00001000100000000) with + member x.Kind = + match (flags &&& 0b00001000100000000) with | 0b00000000000000000 -> TyparKind.Type | 0b00000000100000000 -> TyparKind.Measure | _ -> failwith "unreachable" @@ -394,61 +394,61 @@ type TyparFlags(flags: int32) = (flags &&& 0b00000001000000000) <> 0x0 /// Indicates if a type parameter is needed at runtime and may not be eliminated - member x.DynamicReq = - match (flags &&& 0b00000010000000000) with + member x.DynamicReq = + match (flags &&& 0b00000010000000000) with | 0b00000000000000000 -> TyparDynamicReq.No | 0b00000010000000000 -> TyparDynamicReq.Yes | _ -> failwith "unreachable" /// Indicates that whether or not a generic type definition satisfies the equality constraint is dependent on whether this type variable satisfies the equality constraint. - member x.EqualityConditionalOn = + member x.EqualityConditionalOn = (flags &&& 0b00000100000000000) <> 0x0 /// Indicates that whether this type parameter is a compat-flex type parameter (i.e. where "expr :> tp" only emits an optional warning) - member x.IsCompatFlex = + member x.IsCompatFlex = (flags &&& 0b00010000000000000) <> 0x0 - member x.WithCompatFlex b = - if b then + member x.WithCompatFlex b = + if b then TyparFlags(flags ||| 0b00010000000000000) else TyparFlags(flags &&& ~~~0b00010000000000000) /// Indicates that whether this type parameter is flexible for 'supports null' constraint, e.g. in the case of assignment to a mutable value - member x.IsSupportsNullFlex = + member x.IsSupportsNullFlex = (flags &&& 0b00100000000000000) <> 0x0 - member x.WithSupportsNullFlex b = - if b then + member x.WithSupportsNullFlex b = + if b then TyparFlags(flags ||| 0b00100000000000000) else TyparFlags(flags &&& ~~~0b00100000000000000) - member x.WithStaticReq staticReq = - TyparFlags(x.Kind, x.Rigidity, x.IsFromError, x.IsCompilerGenerated, staticReq, x.DynamicReq, x.EqualityConditionalOn, x.ComparisonConditionalOn, x.IsSupportsNullFlex) + member x.WithStaticReq staticReq = + TyparFlags(x.Kind, x.Rigidity, x.IsFromError, x.IsCompilerGenerated, staticReq, x.DynamicReq, x.EqualityConditionalOn, x.ComparisonConditionalOn, x.IsSupportsNullFlex) /// Get the flags as included in the F# binary metadata. We pickle this as int64 to allow for future expansion - member x.PickledBits = flags + member x.PickledBits = flags /// Encode entity flags into a bit field. We leave lots of space to allow for future expansion. [] type EntityFlags(flags: int64) = - new (usesPrefixDisplay, isModuleOrNamespace, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, isStructRecordOrUnionType) = + new (usesPrefixDisplay, isModuleOrNamespace, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, isStructRecordOrUnionType) = EntityFlags((if isModuleOrNamespace then 0b000000000000001L else 0L) ||| (if usesPrefixDisplay then 0b000000000000010L else 0L) ||| (if preEstablishedHasDefaultCtor then 0b000000000000100L else 0L) ||| (if hasSelfReferentialCtor then 0b000000000001000L else 0L) ||| - (if isStructRecordOrUnionType then 0b000000000100000L else 0L)) + (if isStructRecordOrUnionType then 0b000000000100000L else 0L)) /// Indicates the Entity is actually a module or namespace, not a type definition member x.IsModuleOrNamespace = (flags &&& 0b000000000000001L) <> 0x0L - /// Indicates the type prefers the "tycon" syntax for display etc. + /// Indicates the type prefers the "tycon" syntax for display etc. member x.IsPrefixDisplay = (flags &&& 0b000000000000010L) <> 0x0L - + // This bit is not pickled, only used while establishing a type constructor. It is needed because the type constructor // is known to satisfy the default constructor constraint even before any of its members have been established. member x.PreEstablishedHasDefaultConstructor = (flags &&& 0b000000000000100L) <> 0x0L @@ -465,51 +465,51 @@ type EntityFlags(flags: int64) = member x.IsStructRecordOrUnionType = (flags &&& 0b000000000100000L) <> 0x0L /// These two bits represents the on-demand analysis about whether the entity has the IsByRefLike attribute - member x.TryIsByRefLike = (flags &&& 0b000000011000000L) - |> function + member x.TryIsByRefLike = (flags &&& 0b000000011000000L) + |> function | 0b000000011000000L -> ValueSome true | 0b000000010000000L -> ValueSome false | _ -> ValueNone /// Adjust the on-demand analysis about whether the entity has the IsByRefLike attribute - member x.WithIsByRefLike flag = - let flags = + member x.WithIsByRefLike flag = + let flags = (flags &&& ~~~0b000000011000000L) ||| (match flag with | true -> 0b000000011000000L - | false -> 0b000000010000000L) + | false -> 0b000000010000000L) EntityFlags flags /// These two bits represents the on-demand analysis about whether the entity has the IsReadOnly attribute - member x.TryIsReadOnly = (flags &&& 0b000001100000000L) - |> function + member x.TryIsReadOnly = (flags &&& 0b000001100000000L) + |> function | 0b000001100000000L -> ValueSome true | 0b000001000000000L -> ValueSome false | _ -> ValueNone /// Adjust the on-demand analysis about whether the entity has the IsReadOnly attribute - member x.WithIsReadOnly flag = - let flags = + member x.WithIsReadOnly flag = + let flags = (flags &&& ~~~0b000001100000000L) ||| (match flag with | true -> 0b000001100000000L - | false -> 0b000001000000000L) + | false -> 0b000001000000000L) EntityFlags flags /// These two bits represents the on-demand analysis about whether the entity is assumed to be a readonly struct - member x.TryIsAssumedReadOnly = (flags &&& 0b000110000000000L) - |> function + member x.TryIsAssumedReadOnly = (flags &&& 0b000110000000000L) + |> function | 0b000110000000000L -> ValueSome true | 0b000100000000000L -> ValueSome false | _ -> ValueNone /// Adjust the on-demand analysis about whether the entity is assumed to be a readonly struct - member x.WithIsAssumedReadOnly flag = - let flags = + member x.WithIsAssumedReadOnly flag = + let flags = (flags &&& ~~~0b000110000000000L) ||| (match flag with | true -> 0b000110000000000L - | false -> 0b000100000000000L) + | false -> 0b000100000000000L) EntityFlags flags /// Get the flags as included in the F# binary metadata @@ -517,29 +517,29 @@ type EntityFlags(flags: int64) = -exception UndefinedName of - depth: int * - error: (RichText -> RichText) * - id: Ident * +exception UndefinedName of + depth: int * + error: (RichText -> RichText) * + id: Ident * suggestions: Suggestions exception InternalUndefinedItemRef of (string * string * string -> int * RichText) * string * string * string [] -type ModuleOrNamespaceKind = +type ModuleOrNamespaceKind = - /// Indicates that a module is compiled to a class with the "Module" suffix added. - | FSharpModuleWithSuffix + /// Indicates that a module is compiled to a class with the "Module" suffix added. + | FSharpModuleWithSuffix - /// Indicates that a module is compiled to a class with the same name as the original module - | ModuleOrType + /// Indicates that a module is compiled to a class with the same name as the original module + | ModuleOrType - /// Indicates that a 'module' is really a namespace + /// Indicates that a 'module' is really a namespace | Namespace of /// Indicates that the sourcecode had a namespace. - /// If false, this namespace was implicitly constructed during type checking. + /// If false, this namespace was implicitly constructed during type checking. isExplicit: bool - + override this.Equals other = match other with | :? ModuleOrNamespaceKind as kind -> @@ -581,12 +581,12 @@ type CompilationPath = member x.NestedCompPath n moduleKind = CompPath(x.ILScopeRef, x.SyntaxAccess, x.AccessPath@[(n, moduleKind)]) - member x.DemangledPath = + member x.DemangledPath = x.AccessPath |> List.map (fun (nm, k) -> CompilationPath.DemangleEntityName nm k) /// String 'Module' off an F# module name, if FSharpModuleWithSuffix is used - static member DemangleEntityName nm k = - match k with + static member DemangleEntityName nm k = + match k with | FSharpModuleWithSuffix -> String.dropSuffix nm FSharpModuleSuffix | _ -> nm @@ -620,44 +620,44 @@ type PublicPath = [] type EntityOptionalData = { - /// The name of the type, possibly with `n mangling - // MUTABILITY; used only when establishing tycons. + /// The name of the type, possibly with `n mangling + // MUTABILITY; used only when establishing tycons. mutable entity_compiled_name: string option // MUTABILITY: the signature is adjusted when it is checked - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation mutable entity_other_range: (range * bool) option - // MUTABILITY; used only when establishing tycons. + // MUTABILITY; used only when establishing tycons. mutable entity_kind: TyparKind - /// The declared documentation for the type or module + /// The declared documentation for the type or module // MUTABILITY: only for unpickle linkage mutable entity_xmldoc: XmlDoc - + /// the signature xml doc for an item in an implementation file. mutable entity_other_xmldoc : XmlDoc option - + /// The XML document signature for this entity mutable entity_xmldocsig: string - /// If non-None, indicates the type is an abbreviation for another type. + /// If non-None, indicates the type is an abbreviation for another type. // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_tycon_abbrev: TType option + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_tycon_abbrev: TType option - /// The declared accessibility of the representation, not taking signatures into account + /// The declared accessibility of the representation, not taking signatures into account mutable entity_tycon_repr_accessibility: Accessibility /// Indicates how visible is the entity is. // MUTABILITY: only for unpickle linkage - mutable entity_accessibility: Accessibility + mutable entity_accessibility: Accessibility /// Field used when the 'tycon' is really an exception definition - // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_exn_info: ExceptionInfo + // + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_exn_info: ExceptionInfo } [] @@ -666,39 +666,39 @@ type EntityOptionalData = override x.ToString() = "EntityOptionalData(...)" /// Represents a type definition, exception definition, module definition or namespace definition. -[] -type Entity = +[] +type Entity = { - /// The declared type parameters of the type - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_typars: LazyWithContext - + /// The declared type parameters of the type + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_typars: LazyWithContext + mutable entity_flags: EntityFlags - - /// The unique stamp of the "tycon blob". Note the same tycon in signature and implementation get different stamps + + /// The unique stamp of the "tycon blob". Note the same tycon in signature and implementation get different stamps // MUTABILITY: only for unpickle linkage mutable entity_stamp: Stamp - /// The name of the type, possibly with `n mangling + /// The name of the type, possibly with `n mangling // MUTABILITY: only for unpickle linkage mutable entity_logical_name: string - /// The declaration location for the type constructor + /// The declaration location for the type constructor mutable entity_range: range - - /// The declared attributes for the type - // MUTABILITY; used during creation and remapping of tycons + + /// The declared attributes for the type + // MUTABILITY; used during creation and remapping of tycons // MUTABILITY; used when propagating signature attributes into the implementation. - mutable entity_attribs: WellKnownEntityAttribs - - /// The declared representation of the type, i.e. record, union, class etc. + mutable entity_attribs: WellKnownEntityAttribs + + /// The declared representation of the type, i.e. record, union, class etc. // - // MUTABILITY; used only during creation and remapping of tycons - mutable entity_tycon_repr: TyconRepresentation - - /// The methods and properties of the type + // MUTABILITY; used only during creation and remapping of tycons + mutable entity_tycon_repr: TyconRepresentation + + /// The methods and properties of the type // - // MUTABILITY; used only during creation and remapping of tycons + // MUTABILITY; used only during creation and remapping of tycons mutable entity_tycon_tcaug: TyconAugmentation | null /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions and nested modules @@ -709,17 +709,17 @@ type Entity = /// The stable path to the type, e.g. Microsoft.FSharp.Core.FSharpFunc`2 // MUTABILITY: only for unpickle linkage - mutable entity_cpath: CompilationPath option + mutable entity_cpath: CompilationPath option - /// Used during codegen to hold the ILX representation indicating how to access the type + /// Used during codegen to hold the ILX representation indicating how to access the type // MUTABILITY: only for unpickle linkage and caching mutable entity_il_repr_cache: CompiledTypeRepr cache | null mutable entity_opt_data: EntityOptionalData option } - static member NewEmptyEntityOptData() = - { entity_compiled_name = None + static member NewEmptyEntityOptData() = + { entity_compiled_name = None entity_other_range = None entity_kind = TyparKind.Type entity_xmldoc = XmlDoc.Empty @@ -730,19 +730,19 @@ type Entity = entity_accessibility = TAccess [] entity_exn_info = TExnNone } - /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException + /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException member x.LogicalName = x.entity_logical_name - /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException - member x.CompiledName = - match x.entity_opt_data with + /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException + member x.CompiledName = + match x.entity_opt_data with | Some { entity_compiled_name = Some s } -> s - | _ -> x.LogicalName + | _ -> x.LogicalName member x.EntityCompiledName = - match x.entity_opt_data with + match x.entity_opt_data with | Some optData -> optData.entity_compiled_name - | _ -> None + | _ -> None member x.SetCompiledName name = match x.entity_opt_data with @@ -776,30 +776,30 @@ type Entity = x.GetDisplayName(coreName=false, withStaticParameters=true, withUnderscoreTypars=false) #if !NO_TYPEPROVIDERS - member x.IsStaticInstantiationTycon = + member x.IsStaticInstantiationTycon = x.IsProvidedErasedTycon && let _nm, args = DemangleProvidedTypeName x.LogicalName - args.Length > 0 + args.Length > 0 #endif member x.GetDisplayName(coreName, ?withStaticParameters, ?withUnderscoreTypars) = let withStaticParameters = defaultArg withStaticParameters false let withUnderscoreTypars = defaultArg withUnderscoreTypars false let nm = x.LogicalName - if x.IsModuleOrNamespace then x.DemangledModuleOrNamespaceName + if x.IsModuleOrNamespace then x.DemangledModuleOrNamespaceName #if !NO_TYPEPROVIDERS - elif x.IsProvidedErasedTycon then + elif x.IsProvidedErasedTycon then let nm, args = DemangleProvidedTypeName nm - if withStaticParameters && args.Length > 0 then + if withStaticParameters && args.Length > 0 then nm + "<" + String.concat "," (Array.map snd args) + ">" else nm #endif else ignore withStaticParameters - match x.Typars with + match x.Typars with | [] -> nm - | tps -> + | tps -> let nm = DemangleGenericTypeName nm let isArray = nm.StartsWithOrdinal("[") && nm.EndsWithOrdinal("]") let nm = if coreName || isArray then nm else ConvertLogicalNameToDisplayName nm @@ -810,40 +810,40 @@ type Entity = nm /// The code location where the module, namespace or type is defined. - member x.Range = -#if !NO_TYPEPROVIDERS + member x.Range = +#if !NO_TYPEPROVIDERS match x.TypeReprInfo with | TProvidedTypeRepr info -> match Construct.ComputeDefinitionLocationOfProvidedItem info.ProvidedType with | Some range -> range | None -> x.entity_range - | _ -> + | _ -> #endif x.entity_range /// The range in the implementation, adjusted for an item in a signature - member x.DefinitionRange = - match x.entity_opt_data with + member x.DefinitionRange = + match x.entity_opt_data with | Some { entity_other_range = Some (r, true) } -> r | _ -> x.Range - member x.SigRange = - match x.entity_opt_data with + member x.SigRange = + match x.entity_opt_data with | Some { entity_other_range = Some (r, false) } -> r | _ -> x.Range - member x.SetOtherRange m = - match x.entity_opt_data with + member x.SetOtherRange m = + match x.entity_opt_data with | Some optData -> optData.entity_other_range <- Some m | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_other_range = Some m } - + member x.SetOtherXmlDoc xmlDoc = match x.entity_opt_data with | Some optData -> optData.entity_other_xmldoc <- Some xmlDoc | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_other_xmldoc = Some xmlDoc } - /// A unique stamp for this module, namespace or type definition within the context of this compilation. - /// Note that because of signatures, there are situations where in a single compilation the "same" + /// A unique stamp for this module, namespace or type definition within the context of this compilation. + /// Note that because of signatures, there are situations where in a single compilation the "same" /// module, namespace or type may have two distinct Entity objects that have distinct stamps. member x.Stamp = x.entity_stamp @@ -855,15 +855,15 @@ type Entity = /// The XML documentation of the entity, if any. If the entity is backed by provided metadata /// then this _does_ include this documentation. If the entity is backed by Abstract IL metadata - /// or comes from another F# assembly then it does not (because the documentation will get read from + /// or comes from another F# assembly then it does not (because the documentation will get read from /// an XML file). - member x.XmlDoc = + member x.XmlDoc = #if !NO_TYPEPROVIDERS match x.TypeReprInfo with | TProvidedTypeRepr info -> let lines = info.ProvidedType.PUntaintNoFailure(fun st -> (st :> IProvidedCustomAttributeProvider).GetXmlDocAttributes(info.ProvidedType.TypeProvider.PUntaintNoFailure id)) XmlDoc (lines, x.DefinitionRange) - | _ -> + | _ -> #endif match x.entity_opt_data with | Some optData -> @@ -877,8 +877,8 @@ type Entity = /// The XML documentation sig-string of the entity, if any, to use to lookup an .xml doc file. This also acts /// as a cache for this sig-string computation. - member x.XmlDocSig - with get() = + member x.XmlDocSig + with get() = match x.entity_opt_data with | Some optData -> optData.entity_xmldocsig | _ -> "" @@ -890,18 +890,14 @@ type Entity = /// The logical contents of the entity when it is a module or namespace fragment. member x.ModuleOrNamespaceType = match x.entity_modul_type with - | null -> x.entity_modul_type <- MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType ModuleOrType) - | _ -> () - x.entity_modul_type - |> Unchecked.nonNull - |> _.Force() + | null -> Entity.EmptyModuleOrNamespaceType + | modulType -> modulType.Force() /// The logical contents of the entity when it is a type definition. member x.TypeContents = match x.entity_tycon_tcaug with - | null -> x.entity_tycon_tcaug <- TyconAugmentation.Create() - | _ -> () - !!x.entity_tycon_tcaug + | null -> TyconAugmentation.Empty + | tcaug -> tcaug /// The kind of the type definition - is it a measure definition or a type definition? member x.TypeOrMeasureKind = @@ -920,7 +916,7 @@ type Entity = /// The information about the r.h.s. of a type definition, if any. For example, the r.h.s. of a union or record type. member x.TypeReprInfo = x.entity_tycon_repr - /// The information about the r.h.s. of an F# exception definition, if any. + /// The information about the r.h.s. of an F# exception definition, if any. member x.ExceptionInfo = match x.entity_opt_data with | Some optData -> optData.entity_exn_info @@ -941,19 +937,19 @@ type Entity = CompilationPath.DemangleEntityName x.LogicalName x.ModuleOrNamespaceType.ModuleOrNamespaceKind else x.LogicalName - + /// Get the type parameters for an entity that is a type declaration, otherwise return the empty list. /// /// Lazy because it may read metadata. Uses the entity's own range for error context. member x.Typars: Typars = x.entity_typars.Force x.Range /// Get the type abbreviated by this type definition, if it is an F# type abbreviation definition - member x.TypeAbbrev = + member x.TypeAbbrev = match x.entity_opt_data with | Some optData -> optData.entity_tycon_abbrev | _ -> None - member x.SetTypeAbbrev tycon_abbrev = + member x.SetTypeAbbrev tycon_abbrev = match x.entity_opt_data with | Some optData -> optData.entity_tycon_abbrev <- tycon_abbrev | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_tycon_abbrev = tycon_abbrev } @@ -990,7 +986,7 @@ type Entity = | Some optData -> optData.entity_accessibility | _ -> TAccess [] - /// Indicates the type prefers the "tycon" syntax for display etc. + /// Indicates the type prefers the "tycon" syntax for display etc. member x.IsPrefixDisplay = x.entity_flags.IsPrefixDisplay /// Indicates the Entity is actually a module or namespace, not a type definition @@ -998,7 +994,7 @@ type Entity = /// Indicates if the entity is a namespace member x.IsNamespace = x.IsModuleOrNamespace && (match x.ModuleOrNamespaceType.ModuleOrNamespaceKind with Namespace _ -> true | _ -> false) - + /// Indicates if the entity has an implicit namespace member x.IsImplicitNamespace = (match x.ModuleOrNamespaceType.ModuleOrNamespaceKind with Namespace false -> true | _ -> false) @@ -1007,59 +1003,59 @@ type Entity = #if !NO_TYPEPROVIDERS /// Indicates if the entity is a provided type or namespace definition - member x.IsProvided = - match x.TypeReprInfo with + member x.IsProvided = + match x.TypeReprInfo with | TProvidedTypeRepr _ -> true | TProvidedNamespaceRepr _ -> true | _ -> false /// Indicates if the entity is a provided namespace fragment - member x.IsProvidedNamespace = - match x.TypeReprInfo with + member x.IsProvidedNamespace = + match x.TypeReprInfo with | TProvidedNamespaceRepr _ -> true | _ -> false /// Indicates if the entity is an erased provided type definition - member x.IsProvidedErasedTycon = - match x.TypeReprInfo with + member x.IsProvidedErasedTycon = + match x.TypeReprInfo with | TProvidedTypeRepr info -> info.IsErased | _ -> false /// Indicates if the entity is a generated provided type definition, i.e. not erased. - member x.IsProvidedGeneratedTycon = - match x.TypeReprInfo with + member x.IsProvidedGeneratedTycon = + match x.TypeReprInfo with | TProvidedTypeRepr info -> info.IsGenerated | _ -> false #endif /// Indicates if the entity is erased, either a measure definition, or an erased provided type definition - member x.IsErased = - x.IsMeasureableReprTycon + member x.IsErased = + x.IsMeasureableReprTycon #if !NO_TYPEPROVIDERS || x.IsProvidedErasedTycon #endif /// Get a blob of data indicating how this type is nested inside other namespaces, modules and types. - member x.CompilationPathOpt = x.entity_cpath + member x.CompilationPathOpt = x.entity_cpath /// Get a blob of data indicating how this type is nested inside other namespaces, modules and types. - member x.CompilationPath = - match x.CompilationPathOpt with - | Some cpath -> cpath + member x.CompilationPath = + match x.CompilationPathOpt with + | Some cpath -> cpath | None -> let tag = if x.IsModuleOrNamespace then TextTag.Module else TextTag.Class error(Error(FSComp.SR.tastTypeOrModuleNotConcrete(RichText.ofTag tag x.LogicalName), x.Range)) - + /// Get a table of fields for all the F#-defined record, struct and class fields in this type definition, including /// static fields, 'val' declarations and hidden fields from the compilation of implicit class constructions. - member x.AllFieldTable = - match x.TypeReprInfo with + member x.AllFieldTable = + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_rfields=x} -> x - | _ -> - match x.ExceptionInfo with + | _ -> + match x.ExceptionInfo with | TExnFresh x -> x - | _ -> - { FieldsByIndex = [| |] + | _ -> + { FieldsByIndex = [| |] FieldsByName = NameMap.empty } /// Get an array of fields for all the F#-defined record, struct and class fields in this type definition, including @@ -1091,31 +1087,36 @@ type Entity = /// Indicate if this is a type whose r.h.s. is known to be a union type definition. member x.IsUnionTycon = - match x.TypeReprInfo with + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_kind=TFSharpUnion} -> true | _ -> false /// Get the union cases and other union-type information for a type, if any - member x.UnionTypeInfo = - match x.TypeReprInfo with + member x.UnionTypeInfo = + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_kind=TFSharpUnion; fsobjmodel_cases=x} -> ValueSome x | _ -> ValueNone /// Get the union cases for a type, if any - member x.UnionCasesArray = - match x.UnionTypeInfo with - | ValueSome x -> x.CasesTable.CasesByIndex - | ValueNone -> [| |] + member x.UnionCasesArray = + match x.UnionTypeInfo with + | ValueSome x -> x.CasesTable.CasesByIndex + | ValueNone -> [| |] /// Get the union cases for a type, if any, as a list member x.UnionCasesAsList = x.UnionCasesArray |> Array.toList /// Get a union case of a type by name member x.GetUnionCaseByName n = - match x.UnionTypeInfo with + match x.UnionTypeInfo with | ValueSome x -> NameMap.tryFind n x.CasesTable.CasesByName | ValueNone -> None + + /// Stands in for the contents of an entity that was never linked, so that a broken mid-edit + /// file still classifies instead of crashing. Never reachable from a successfully checked entity. + static member val EmptyModuleOrNamespaceType = + ModuleOrNamespaceType(ModuleOrType, QueueList.Empty, QueueList.Empty) /// Create a new entity with empty, unlinked data. Only used during unpickling of F# metadata. static member NewUnlinked() : Entity = @@ -1136,27 +1137,27 @@ type Entity = static member New _reason (data: Entity) : Entity = data /// Link an entity based on empty, unlinked data to the given data. Only used during unpickling of F# metadata. - member x.Link (tg: EntityData) = - x.entity_typars <- tg.entity_typars - x.entity_flags <- tg.entity_flags - x.entity_stamp <- tg.entity_stamp - x.entity_logical_name <- tg.entity_logical_name - x.entity_range <- tg.entity_range - x.entity_attribs <- tg.entity_attribs + member x.Link (tg: EntityData) = + x.entity_typars <- tg.entity_typars + x.entity_flags <- tg.entity_flags + x.entity_stamp <- tg.entity_stamp + x.entity_logical_name <- tg.entity_logical_name + x.entity_range <- tg.entity_range + x.entity_attribs <- tg.entity_attribs x.entity_tycon_repr <- tg.entity_tycon_repr x.entity_tycon_tcaug <- tg.entity_tycon_tcaug x.entity_modul_type <- tg.entity_modul_type x.entity_cpath <- tg.entity_cpath - x.entity_il_repr_cache <- tg.entity_il_repr_cache + x.entity_il_repr_cache <- tg.entity_il_repr_cache match tg.entity_opt_data with | Some tg -> - x.entity_opt_data <- + x.entity_opt_data <- Some { entity_compiled_name = tg.entity_compiled_name entity_other_range = tg.entity_other_range entity_kind = tg.entity_kind entity_xmldoc = tg.entity_xmldoc entity_xmldocsig = tg.entity_xmldocsig - entity_other_xmldoc = tg.entity_other_xmldoc + entity_other_xmldoc = tg.entity_other_xmldoc entity_tycon_abbrev = tg.entity_tycon_abbrev entity_tycon_repr_accessibility = tg.entity_tycon_repr_accessibility entity_accessibility = tg.entity_accessibility @@ -1168,15 +1169,15 @@ type Entity = member x.IsLinked = not (obj.ReferenceEquals(x.entity_attribs.AsList(), null)) /// Get the blob of information associated with an F# object-model type definition, i.e. class, interface, struct etc. - member x.FSharpTyconRepresentationData = - match x.TypeReprInfo with - | TFSharpTyconRepr x -> x + member x.FSharpTyconRepresentationData = + match x.TypeReprInfo with + | TFSharpTyconRepr x -> x | _ -> failwith "not an F# object model type definition" /// Indicate if this is a type definition backed by Abstract IL metadata. member x.IsILTycon = match x.TypeReprInfo with | TILObjectRepr _ -> true | _ -> false - /// Get the Abstract IL scope, nesting and metadata for this + /// Get the Abstract IL scope, nesting and metadata for this /// type definition, assuming it is backed by Abstract IL metadata. member x.ILTyconInfo = match x.TypeReprInfo with | TILObjectRepr data -> data | _ -> failwith "not a .NET type definition" @@ -1185,13 +1186,13 @@ type Entity = /// Indicates if this is an F# type definition whose r.h.s. is known to be a record type definition. member x.IsRecordTycon = - match x.TypeReprInfo with + match x.TypeReprInfo with | TFSharpTyconRepr {fsobjmodel_kind=TFSharpRecord} -> true | _ -> false /// Indicates if this is an F# type definition whose r.h.s. is known to be a record type definition that is a value type. member x.IsStructRecordOrUnionTycon = - match x.TypeReprInfo with + match x.TypeReprInfo with | TFSharpTyconRepr { fsobjmodel_kind=TFSharpRecord } -> x.entity_flags.IsStructRecordOrUnionType | TFSharpTyconRepr { fsobjmodel_kind=TFSharpUnion } -> x.entity_flags.IsStructRecordOrUnionType | _ -> false @@ -1200,19 +1201,19 @@ type Entity = member x.TryIsByRefLike = x.entity_flags.TryIsByRefLike /// Set the on-demand analysis about whether the entity has the IsByRefLike attribute - member x.SetIsByRefLike b = x.entity_flags <- x.entity_flags.WithIsByRefLike b + member x.SetIsByRefLike b = x.entity_flags <- x.entity_flags.WithIsByRefLike b /// These two bits represents the on-demand analysis about whether the entity has the IsReadOnly attribute member x.TryIsReadOnly = x.entity_flags.TryIsReadOnly /// Set the on-demand analysis about whether the entity has the IsReadOnly attribute - member x.SetIsReadOnly b = x.entity_flags <- x.entity_flags.WithIsReadOnly b + member x.SetIsReadOnly b = x.entity_flags <- x.entity_flags.WithIsReadOnly b /// These two bits represents the on-demand analysis about whether the entity is assumed to be a readonly struct member x.TryIsAssumedReadOnly = x.entity_flags.TryIsAssumedReadOnly /// Set the on-demand analysis about whether the entity is assumed to be a readonly struct - member x.SetIsAssumedReadOnly b = x.entity_flags <- x.entity_flags.WithIsAssumedReadOnly b + member x.SetIsAssumedReadOnly b = x.entity_flags <- x.entity_flags.WithIsAssumedReadOnly b /// Indicates if this is an F# type definition known to be an F# class, interface, struct, /// delegate or enum. This isn't generally a particularly useful thing to know, @@ -1230,7 +1231,7 @@ type Entity = | TFSharpEnum -> true | _ -> false - /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses + /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses /// an assembly-code representation for the type, e.g. the primitive array type constructor. member x.IsAsmReprTycon = match x.TypeReprInfo with | TAsmRepr _ -> true | _ -> false @@ -1242,26 +1243,26 @@ type Entity = /// which in F# is called a 'unknown representation' type). member x.IsHiddenReprTycon = match x.TypeAbbrev, x.TypeReprInfo with | None, TNoRepr -> true | _ -> false - /// Indicates if this is an F#-defined interface type definition + /// Indicates if this is an F#-defined interface type definition member x.IsFSharpInterfaceTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpInterface -> true | _ -> false - /// Indicates if this is an F#-defined delegate type definition + /// Indicates if this is an F#-defined delegate type definition member x.IsFSharpDelegateTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpDelegate _ -> true | _ -> false - /// Indicates if this is an F#-defined enum type definition + /// Indicates if this is an F#-defined enum type definition member x.IsFSharpEnumTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpEnum -> true | _ -> false - /// Indicates if this is an F#-defined class type definition + /// Indicates if this is an F#-defined class type definition member x.IsFSharpClassTycon = x.IsFSharpObjectModelTycon && match x.FSharpTyconRepresentationData.fsobjmodel_kind with TFSharpClass -> true | _ -> false - /// Indicates if this is a .NET-defined enum type definition + /// Indicates if this is a .NET-defined enum type definition member x.IsILEnumTycon = x.IsILTycon && x.ILTyconRawMetadata.IsEnum - /// Indicates if this is an enum type definition - member x.IsEnumTycon = + /// Indicates if this is an enum type definition + member x.IsEnumTycon = #if !NO_TYPEPROVIDERS - match x.TypeReprInfo with - | TProvidedTypeRepr info -> info.IsEnum + match x.TypeReprInfo with + | TProvidedTypeRepr info -> info.IsEnum | TProvidedNamespaceRepr _ -> false | _ -> #endif @@ -1279,14 +1280,14 @@ type Entity = /// Indicates if this is a .NET-defined struct or enum type definition, i.e. a value type definition member x.IsILStructOrEnumTycon = - x.IsILTycon && + x.IsILTycon && x.ILTyconRawMetadata.IsStructOrEnum /// Indicates if this is a struct or enum type definition, i.e. a value type definition, including struct records and unions - member x.IsStructOrEnumTycon = + member x.IsStructOrEnumTycon = #if !NO_TYPEPROVIDERS - match x.TypeReprInfo with - | TProvidedTypeRepr info -> info.IsStructOrEnum + match x.TypeReprInfo with + | TProvidedTypeRepr info -> info.IsStructOrEnum | TProvidedNamespaceRepr _ -> false | _ -> #endif @@ -1303,17 +1304,17 @@ type Entity = /// Gets the immediate members of an F# type definition, excluding compiler-generated ones. /// Note: result is alphabetically sorted, then for each name the results are in declaration order member x.MembersOfFSharpTyconSorted = - x.TypeContents.tcaug_adhoc - |> NameMultiMap.rangeReversingEachBucket + x.TypeContents.tcaug_adhoc + |> NameMultiMap.rangeReversingEachBucket |> List.filter (fun vref -> not vref.IsCompilerGenerated) /// Gets all immediate members of an F# type definition keyed by name, including compiler-generated ones. /// Note: result is a indexed table, and for each name the results are in reverse declaration order member x.MembersOfFSharpTyconByName = - x.TypeContents.tcaug_adhoc + x.TypeContents.tcaug_adhoc /// Gets any implicit hash/equals (with comparer argument) methods added to an F# record, union or struct type definition. - member x.GeneratedHashAndEqualsWithComparerValues = x.TypeContents.tcaug_hash_and_equals_withc + member x.GeneratedHashAndEqualsWithComparerValues = x.TypeContents.tcaug_hash_and_equals_withc /// Gets any implicit CompareTo (with comparer argument) methods added to an F# record, union or struct type definition. member x.GeneratedCompareToWithComparerValues = x.TypeContents.tcaug_compare_withc @@ -1325,8 +1326,8 @@ type Entity = member x.GeneratedHashAndEqualsValues = x.TypeContents.tcaug_equals /// Gets all implicit hash/equals/compare methods added to an F# record, union or struct type definition. - member x.AllGeneratedInterfaceImplsAndOverrides = - [ match x.GeneratedCompareToValues with + member x.AllGeneratedInterfaceImplsAndOverrides = + [ match x.GeneratedCompareToValues with | None -> () | Some (vref1, vref2) -> yield vref1; yield vref2 match x.GeneratedCompareToWithComparerValues with @@ -1339,20 +1340,20 @@ type Entity = | None -> () // vref4 is compiled as a sealed instance member, not an interface impl or an override. | Some (vref1, vref2, vref3, _) -> yield vref1; yield vref2; yield vref3 ] - + /// Gets the data indicating the compiled representation of a type or module in terms of Abstract IL data structures. member x.CompiledRepresentation = #if !NO_TYPEPROVIDERS - match x.TypeReprInfo with + match x.TypeReprInfo with // We should never be computing this property for erased types - | TProvidedTypeRepr info when info.IsErased -> + | TProvidedTypeRepr info when info.IsErased -> failwith "No compiled representation for provided erased type" - + // Generated types that are not relocated just point straight to the generated backing assembly, computed from "st". // These are used when running F# Interactive, which does not use static linking of provider-generated assemblies, // and also for types with relocation suppressed. - | TProvidedTypeRepr info when info.IsGenerated && info.IsSuppressRelocate -> + | TProvidedTypeRepr info when info.IsGenerated && info.IsSuppressRelocate -> let st = info.ProvidedType let tref = GetILTypeRefOfProvidedType (st, x.Range) let boxity = if x.IsStructOrEnumTycon then AsValue else AsObject @@ -1360,43 +1361,43 @@ type Entity = | TProvidedNamespaceRepr _ -> failwith "No compiled representation for provided namespace" | _ -> #endif - let ilTypeRefForCompilationPath (CompPath(sref, _, p)) item = - let rec top racc p = - match p with + let ilTypeRefForCompilationPath (CompPath(sref, _, p)) item = + let rec top racc p = + match p with | [] -> ILTypeRef.Create(sref, [], textOfPath (List.rev (item :: racc))) - | (h, isType) :: t -> - match isType with - | FSharpModuleWithSuffix | ModuleOrType -> + | (h, isType) :: t -> + match isType with + | FSharpModuleWithSuffix | ModuleOrType -> let outerTypeName = (textOfPath (List.rev (h :: racc))) ILTypeRef.Create(sref, (outerTypeName :: List.map fst t), item) - | _ -> + | _ -> top (h :: racc) t - top [] p + top [] p - cached x.CompiledReprCache (fun () -> - match x.ExceptionInfo with + cached x.CompiledReprCache (fun () -> + match x.ExceptionInfo with | TExnAbbrevRepr ecref2 -> ecref2.CompiledRepresentation | TExnAsmRepr tref -> CompiledTypeRepr.ILAsmNamed(tref, AsObject, Some (mkILTy AsObject (mkILTySpec (tref, [])))) - | _ -> - match x.TypeReprInfo with + | _ -> + match x.TypeReprInfo with | TAsmRepr ty -> CompiledTypeRepr.ILAsmOpen ty - | _ -> + | _ -> let boxity = if x.IsStructOrEnumTycon then AsValue else AsObject - let ilTypeRef = - match x.TypeReprInfo with + let ilTypeRef = + match x.TypeReprInfo with | TILObjectRepr (TILObjectReprData(ilScopeRef, ilEnclosingTypeDefs, ilTypeDef)) -> mkRefForNestedILTypeDef ilScopeRef (ilEnclosingTypeDefs, ilTypeDef) | _ -> ilTypeRefForCompilationPath x.CompilationPath x.CompiledName // Pre-allocate a ILType for monomorphic types, to reduce memory usage from Abstract IL nodes - let ilTypeOpt = - match x.Typars with - | [] -> Some (mkILTy boxity (mkILTySpec (ilTypeRef, []))) + let ilTypeOpt = + match x.Typars with + | [] -> Some (mkILTy boxity (mkILTySpec (ilTypeRef, []))) | _ -> None CompiledTypeRepr.ILAsmNamed (ilTypeRef, boxity, ilTypeOpt)) /// Gets the data indicating the compiled representation of a named type or module in terms of Abstract IL data structures. member x.CompiledRepresentationForNamedType = - match x.CompiledRepresentation with + match x.CompiledRepresentation with | CompiledTypeRepr.ILAsmNamed(tref, _, _) -> tref | CompiledTypeRepr.ILAsmOpen _ -> invalidOp (FSComp.SR.tastTypeHasAssemblyCodeRepresentation(x.DisplayNameWithStaticParametersAndUnderscoreTypars)) @@ -1406,7 +1407,7 @@ type Entity = /// Indicates if we have pre-determined that a type definition has a self-referential constructor using 'as x' member x.HasSelfReferentialConstructor = x.entity_flags.HasSelfReferentialConstructor - + member x.HasSignatureFile = x.SigRange <> x.DefinitionRange /// Set the custom attributes on an F# type definition. @@ -1442,28 +1443,28 @@ type Entity = type EntityData = Entity /// Represents the parent entity of a type definition, if any -type ParentRef = +type ParentRef = | Parent of parent: EntityRef | ParentNone - + /// Specifies the compiled representations of type and exception definitions. Basically -/// just an ILTypeRef. Computed and cached by later phases. Stored in -/// type and exception definitions. Not pickled. Store an optional ILType object for +/// just an ILTypeRef. Computed and cached by later phases. Stored in +/// type and exception definitions. Not pickled. Store an optional ILType object for /// non-generic types. [] -type CompiledTypeRepr = +type CompiledTypeRepr = /// An AbstractIL type representation that is just the name of a type. /// /// CompiledTypeRepr.ILAsmNamed (ilTypeRef, ilBoxity, ilTypeOpt) - /// + /// /// The ilTypeOpt is present for non-generic types. It is an ILType corresponding to the first two elements of the case. This /// prevents reallocation of the ILType each time we need to generate it. For generic types, it is None. - | ILAsmNamed of - ilTypeRef: ILTypeRef * - ilBoxity: ILBoxity * + | ILAsmNamed of + ilTypeRef: ILTypeRef * + ilBoxity: ILBoxity * ilTypeOpt: ILType option - + /// An AbstractIL type representation that may include type variables // This case is only used for types defined in the F# library by their translation to ILASM types, e.g. // type ``[]``<'T> = (# "!0[]" #) @@ -1472,7 +1473,7 @@ type CompiledTypeRepr = // type byref<'T> = (# "!0&" #) // type nativeptr<'T when 'T: unmanaged> = (# "native int" #) // type ilsigptr<'T> = (# "!0*" #) - | ILAsmOpen of ilType: ILType + | ILAsmOpen of ilType: ILType [] member x.DebugText = x.ToString() @@ -1480,32 +1481,32 @@ type CompiledTypeRepr = override x.ToString() = "CompiledTypeRepr(...)" [] -type TyconAugmentation = +type TyconAugmentation = { - /// This is the value implementing the auto-generated comparison - /// semantics if any. It is not present if the type defines its own implementation - /// of IComparable or if the type doesn't implement IComparable implicitly. + /// This is the value implementing the auto-generated comparison + /// semantics if any. It is not present if the type defines its own implementation + /// of IComparable or if the type doesn't implement IComparable implicitly. mutable tcaug_compare: (ValRef * ValRef) option - + /// This is the value implementing the auto-generated comparison /// semantics if any. It is not present if the type defines its own implementation /// of IStructuralComparable or if the type doesn't implement IComparable implicitly. - mutable tcaug_compare_withc: ValRef option + mutable tcaug_compare_withc: ValRef option - /// This is the value implementing the auto-generated equality - /// semantics if any. It is not present if the type defines its own implementation - /// of Object.Equals or if the type doesn't override Object.Equals implicitly. + /// This is the value implementing the auto-generated equality + /// semantics if any. It is not present if the type defines its own implementation + /// of Object.Equals or if the type doesn't override Object.Equals implicitly. mutable tcaug_equals: (ValRef * ValRef) option /// This is the value implementing the auto-generated equality /// semantics if any. It is not present if the type defines its own implementation /// of IStructuralEquatable or if the type doesn't override Object.Equals implicitly. - mutable tcaug_hash_and_equals_withc: (ValRef * ValRef * ValRef * ValRef option) option + mutable tcaug_hash_and_equals_withc: (ValRef * ValRef * ValRef * ValRef option) option - /// True if the type defined an Object.GetHashCode method. In this + /// True if the type defined an Object.GetHashCode method. In this /// case we give a warning if we auto-generate a hash method since the semantics may not match up - mutable tcaug_hasObjectGetHashCode: bool - + mutable tcaug_hasObjectGetHashCode: bool + /// Properties, methods etc. in declaration order. The boolean flag for each indicates if the /// member is known to be an explicit interface implementation. This must be computed and /// saved prior to remapping assembly information. @@ -1517,18 +1518,18 @@ type TyconAugmentation = /// Properties, methods etc. as lookup table mutable tcaug_adhoc: NameMultiMap - - /// Interface implementations - boolean indicates compiler-generated - mutable tcaug_interfaces: (TType * bool * range) list - - /// Super type, if any - mutable tcaug_super: TType option - - /// Set to true at the end of the scope where proper augmentations are allowed - mutable tcaug_closed: bool - - /// Set to true if the type is determined to be abstract - mutable tcaug_abstract: bool + + /// Interface implementations - boolean indicates compiler-generated + mutable tcaug_interfaces: (TType * bool * range) list + + /// Super type, if any + mutable tcaug_super: TType option + + /// Set to true at the end of the scope where proper augmentations are allowed + mutable tcaug_closed: bool + + /// Set to true if the type is determined to be abstract + mutable tcaug_abstract: bool } /// Record a member in declaration order, allocating the list on first use @@ -1572,6 +1573,10 @@ type TyconAugmentation = tcaug_closed=false tcaug_abstract=false } + /// Stands in for the augmentation of an entity that was never linked, so that a broken mid-edit + /// file still classifies instead of crashing. Never reachable from a successfully checked entity. + static member val Empty = TyconAugmentation.Create() + [] member x.DebugText = x.ToString() @@ -1579,15 +1584,15 @@ type TyconAugmentation = /// The information for the contents of a type. Also used for a provided namespace. [] -type TyconRepresentation = +type TyconRepresentation = - /// Indicates the type is a class, struct, enum, delegate or interface + /// Indicates the type is a class, struct, enum, delegate or interface | TFSharpTyconRepr of FSharpTyconData /// Indicates the type is a type from a .NET assembly without F# metadata. | TILObjectRepr of TILObjectReprData - /// Indicates the type is implemented as IL assembly code using the given closed Abstract IL type + /// Indicates the type is implemented as IL assembly code using the given closed Abstract IL type | TAsmRepr of ILType /// Indicates the type is parameterized on a measure (e.g. float<_>) but erases to some other type (e.g. float) @@ -1596,21 +1601,21 @@ type TyconRepresentation = #if !NO_TYPEPROVIDERS /// TProvidedTypeRepr /// - /// Indicates the representation information for a provided type. + /// Indicates the representation information for a provided type. | TProvidedTypeRepr of TProvidedTypeInfo - /// Indicates the representation information for a provided namespace. + /// Indicates the representation information for a provided namespace. // // Note, the list could probably be a list of IProvidedNamespace rather than ITypeProvider | TProvidedNamespaceRepr of ResolutionEnvironment * Tainted list #endif - /// The 'NoRepr' value here has four meanings: + /// The 'NoRepr' value here has four meanings: /// (1) it indicates 'not yet known' during the first 2 phases of establishing type definitions /// (2) it indicates 'no representation', i.e. 'type X' in signatures /// (3) it is the setting used for exception definitions (!) /// (4) it is the setting used for modules and namespaces. - /// + /// /// It would be better to separate the "not yet known" and other cases out. /// The information for exception definitions should be folded into here. | TNoRepr @@ -1618,11 +1623,11 @@ type TyconRepresentation = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x [] -type TILObjectReprData = - | TILObjectReprData of scope: ILScopeRef * nesting: ILTypeDef list * definition: ILTypeDef +type TILObjectReprData = + | TILObjectReprData of scope: ILScopeRef * nesting: ILTypeDef list * definition: ILTypeDef [] member x.DebugText = x.ToString() @@ -1634,8 +1639,8 @@ type TILObjectReprData = /// The information kept about a provided type [] -type TProvidedTypeInfo = - { +type TProvidedTypeInfo = + { /// The parameters given to the provider that provided to this type. ResolutionEnvironment: ResolutionEnvironment @@ -1646,36 +1651,36 @@ type TProvidedTypeInfo = /// The base type of the type. We use it to compute the compiled representation of the type for erased types. /// Reading is delayed, since it does an import on the underlying type - LazyBaseType: LazyWithContext + LazyBaseType: LazyWithContext /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsClass: bool + IsClass: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsSealed: bool + IsSealed: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsAbstract: bool + IsAbstract: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsInterface: bool + IsInterface: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsStructOrEnum: bool + IsStructOrEnum: bool /// A flag read eagerly from the provided type and used to compute basic properties of the type definition. - IsEnum: bool + IsEnum: bool /// A type read from the provided type and used to compute basic properties of the type definition. /// Reading is delayed, since it does an import on the underlying type - UnderlyingTypeOfEnum: unit -> TType + UnderlyingTypeOfEnum: unit -> TType /// A flag read from the provided type and used to compute basic properties of the type definition. /// Reading is delayed, since it looks at the .BaseType - IsDelegate: unit -> bool + IsDelegate: unit -> bool /// Indicates the type is erased - IsErased: bool + IsErased: bool /// Indicates the type is generated, but type-relocation is suppressed IsSuppressRelocate: bool @@ -1685,8 +1690,8 @@ type TProvidedTypeInfo = member info.IsGenerated = not info.IsErased /// Gets the base type of an erased provided type - member info.BaseTypeForErased (m, objTy) = - if info.IsErased then info.LazyBaseType.Force (m, objTy) + member info.BaseTypeForErased (m, objTy) = + if info.IsErased then info.LazyBaseType.Force (m, objTy) else failwith "expect erased type" [] @@ -1696,7 +1701,7 @@ type TProvidedTypeInfo = #endif -type FSharpTyconKind = +type FSharpTyconKind = /// Indicates the type is an F#-declared record | TFSharpRecord @@ -1704,34 +1709,34 @@ type FSharpTyconKind = | TFSharpUnion /// Indicates the type is an F#-declared class (also used for units-of-measure) - | TFSharpClass + | TFSharpClass - /// Indicates the type is an F#-declared interface - | TFSharpInterface + /// Indicates the type is an F#-declared interface + | TFSharpInterface - /// Indicates the type is an F#-declared struct - | TFSharpStruct + /// Indicates the type is an F#-declared struct + | TFSharpStruct - /// Indicates the type is an F#-declared delegate with the given Invoke signature - | TFSharpDelegate of slotSig: SlotSig + /// Indicates the type is an F#-declared delegate with the given Invoke signature + | TFSharpDelegate of slotSig: SlotSig - /// Indicates the type is an F#-declared enumeration + /// Indicates the type is an F#-declared enumeration | TFSharpEnum - + /// Represents member values and class fields relating to the F# object model [] -type FSharpTyconData = - { +type FSharpTyconData = + { /// Indicates the cases of a union type fsobjmodel_cases: TyconUnionData - /// Indicates whether the type declaration is an F# class, interface, enum, delegate or struct + /// Indicates whether the type declaration is an F# class, interface, enum, delegate or struct fsobjmodel_kind: FSharpTyconKind - /// The declared abstract slots of the class, interface or struct + /// The declared abstract slots of the class, interface or struct fsobjmodel_vslots: ValRef list - /// The fields of the class, struct or enum + /// The fields of the class, struct or enum fsobjmodel_rfields: TyconRecdFields } @@ -1742,18 +1747,18 @@ type FSharpTyconData = /// Represents record fields in an F# type definition [] -type TyconRecdFields = - { - /// The fields of the record, in declaration order. +type TyconRecdFields = + { + /// The fields of the record, in declaration order. FieldsByIndex: RecdField[] - - /// The fields of the record, indexed by name. + + /// The fields of the record, indexed by name. FieldsByName: NameMap } /// Get a field by index - member x.FieldByIndex n = - if n >= 0 && n < x.FieldsByIndex.Length then x.FieldsByIndex[n] + member x.FieldByIndex n = + if n >= 0 && n < x.FieldsByIndex.Length then x.FieldsByIndex[n] else failwith "FieldByIndex" /// Get a field by name @@ -1763,10 +1768,10 @@ type TyconRecdFields = member x.AllFieldsAsList = x.FieldsByIndex |> Array.toList /// Get all non-compiler-generated fields as a list - member x.TrueFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsCompilerGenerated) + member x.TrueFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsCompilerGenerated) /// Get all non-compiler-generated instance fields as a list - member x.TrueInstanceFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsStatic && not f.IsCompilerGenerated) + member x.TrueInstanceFieldsAsList = x.AllFieldsAsList |> List.filter (fun f -> not f.IsStatic && not f.IsCompilerGenerated) [] member x.DebugText = x.ToString() @@ -1775,18 +1780,18 @@ type TyconRecdFields = /// Represents union cases in an F# type definition [] -type TyconUnionCases = - { - /// The cases of the discriminated union, in declaration order. +type TyconUnionCases = + { + /// The cases of the discriminated union, in declaration order. CasesByIndex: UnionCase[] - /// The cases of the discriminated union, indexed by name. + /// The cases of the discriminated union, indexed by name. CasesByName: NameMap } /// Get a union case by index - member x.GetUnionCaseByIndex n = - if n >= 0 && n < x.CasesByIndex.Length then x.CasesByIndex[n] + member x.GetUnionCaseByIndex n = + if n >= 0 && n < x.CasesByIndex.Length then x.CasesByIndex[n] else invalidArg "n" "GetUnionCaseByIndex" /// Get the union cases as a list @@ -1802,11 +1807,11 @@ type TyconUnionCases = type TyconUnionData = { - /// The cases contained in the discriminated union. + /// The cases contained in the discriminated union. CasesTable: TyconUnionCases - /// The ILX data structure representing the discriminated union. - CompiledRepresentation: IlxUnionRef cache + /// The ILX data structure representing the discriminated union. + CompiledRepresentation: IlxUnionRef cache } /// Get the union cases as a list @@ -1820,63 +1825,63 @@ type TyconUnionData = /// Represents a union case in an F# type definition [] type UnionCase = - { - /// Data carried by the case. + { + /// Data carried by the case. FieldTable: TyconRecdFields - /// Return type constructed by the case. Normally exactly the type of the enclosing type, sometimes an abbreviation of it + /// Return type constructed by the case. Normally exactly the type of the enclosing type, sometimes an abbreviation of it ReturnType: TType - /// Documentation for the case + /// Documentation for the case OwnXmlDoc: XmlDoc - + /// Documentation for the case from signature file mutable OtherXmlDoc: XmlDoc /// XML documentation signature for the case mutable XmlDocSig: string - /// Name/range of the case - Id: Ident + /// Name/range of the case + Id: Ident - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation // MUTABILITY: used when propagating signature attributes into the implementation. mutable OtherRangeOpt: (range * bool) option - /// Indicates the declared visibility of the union constructor, not taking signatures into account - Accessibility: Accessibility + /// Indicates the declared visibility of the union constructor, not taking signatures into account + Accessibility: Accessibility - /// Attributes, attached to the generated static method to make instances of the case + /// Attributes, attached to the generated static method to make instances of the case // MUTABILITY: used when propagating signature attributes into the implementation. mutable Attribs: Attribs } - - /// Documentation for the case + + /// Documentation for the case member uc.XmlDoc: XmlDoc = if not uc.OwnXmlDoc.IsEmpty then uc.OwnXmlDoc else uc.OtherXmlDoc - + /// Get the declaration location of the union case member uc.Range = uc.Id.idRange /// Get the definition location of the union case - member uc.DefinitionRange = - match uc.OtherRangeOpt with + member uc.DefinitionRange = + match uc.OtherRangeOpt with | Some (m, true) -> m - | _ -> uc.Range + | _ -> uc.Range /// Get the signature location of the union case - member uc.SigRange = - match uc.OtherRangeOpt with + member uc.SigRange = + match uc.OtherRangeOpt with | Some (m, false) -> m - | _ -> uc.Range + | _ -> uc.Range member x.SetOtherXmlDoc xmlDoc = x.OtherXmlDoc <- xmlDoc - + /// Get the logical name of the union case member uc.LogicalName = uc.Id.idText @@ -1904,7 +1909,7 @@ type UnionCase = else idText /// Get the full array of fields of the union case - member uc.RecdFieldsArray = uc.FieldTable.FieldsByIndex + member uc.RecdFieldsArray = uc.FieldTable.FieldsByIndex /// Get the full list of fields of the union case member uc.RecdFields = uc.FieldTable.FieldsByIndex |> Array.toList @@ -1928,103 +1933,103 @@ type UnionCase = [] type RecdField = { - /// Is the field declared mutable in F#? + /// Is the field declared mutable in F#? rfield_mutable: bool - /// Documentation for the field + /// Documentation for the field rfield_xmldoc: XmlDoc - - /// Documentation for the field from signature file + + /// Documentation for the field from signature file mutable rfield_otherxmldoc: XmlDoc /// XML Documentation signature for the field mutable rfield_xmldocsig: string - /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor + /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor rfield_type: TType - /// Indicates a static field + /// Indicates a static field rfield_static: bool - /// Indicates a volatile field + /// Indicates a volatile field rfield_volatile: bool - /// Indicates a compiler generated field, not visible to Intellisense or name resolution + /// Indicates a compiler generated field, not visible to Intellisense or name resolution rfield_secret: bool - /// The default initialization info, for static literals - rfield_const: Const option + /// The default initialization info, for static literals + rfield_const: Const option - /// Indicates the declared visibility of the field, not taking signatures into account - rfield_access: Accessibility + /// Indicates the declared visibility of the field, not taking signatures into account + rfield_access: Accessibility - /// Attributes attached to generated property + /// Attributes attached to generated property // MUTABILITY: used when propagating signature attributes into the implementation. - mutable rfield_pattribs: Attribs + mutable rfield_pattribs: Attribs - /// Attributes attached to generated field + /// Attributes attached to generated field // MUTABILITY: used when propagating signature attributes into the implementation. - mutable rfield_fattribs: Attribs + mutable rfield_fattribs: Attribs - /// Name/declaration-location of the field + /// Name/declaration-location of the field rfield_id: Ident rfield_name_generated: bool - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation // MUTABILITY: used when propagating signature attributes into the implementation. mutable rfield_other_range: (range * bool) option } - /// Indicates the declared visibility of the field, not taking signatures into account + /// Indicates the declared visibility of the field, not taking signatures into account member v.Accessibility = v.rfield_access - /// Attributes attached to generated property + /// Attributes attached to generated property member v.PropertyAttribs = v.rfield_pattribs - /// Attributes attached to generated field + /// Attributes attached to generated field member v.FieldAttribs = v.rfield_fattribs - /// Get the declaration location of the field + /// Get the declaration location of the field member v.Range = v.rfield_id.idRange - /// Get the definition location of the field - member v.DefinitionRange = - match v.rfield_other_range with + /// Get the definition location of the field + member v.DefinitionRange = + match v.rfield_other_range with | Some (m, true) -> m - | _ -> v.Range + | _ -> v.Range - /// Get the signature location of the field - member v.SigRange = - match v.rfield_other_range with + /// Get the signature location of the field + member v.SigRange = + match v.rfield_other_range with | Some (m, false) -> m - | _ -> v.Range + | _ -> v.Range - /// Name/declaration-location of the field + /// Name/declaration-location of the field member v.Id = v.rfield_id - /// Name of the field + /// Name of the field member v.LogicalName = v.rfield_id.idText /// Name of the field. For fields this is the same as the logical name. member v.DisplayNameCore = v.LogicalName - /// Name of the field + /// Name of the field member v.DisplayName = v.DisplayNameCore |> ConvertLogicalNameToDisplayName - /// Indicates a compiler generated field, not visible to Intellisense or name resolution + /// Indicates a compiler generated field, not visible to Intellisense or name resolution member v.IsCompilerGenerated = v.rfield_secret - /// Is the field declared mutable in F#? + /// Is the field declared mutable in F#? member v.IsMutable = v.rfield_mutable - /// Indicates a static field + /// Indicates a static field member v.IsStatic = v.rfield_static - /// Indicates a volatile field + /// Indicates a volatile field member v.IsVolatile = v.rfield_volatile - /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor + /// The type of the field, w.r.t. the generic parameters of the enclosing type constructor member v.FormalType = v.rfield_type /// XML Documentation signature for the field @@ -2033,27 +2038,27 @@ type RecdField = v.rfield_xmldoc else v.rfield_otherxmldoc - + member v.SetOtherXmlDoc (xmlDoc: XmlDoc) = - v.rfield_otherxmldoc <- xmlDoc + v.rfield_otherxmldoc <- xmlDoc /// Get or set the XML documentation signature for the field member v.XmlDocSig with get() = v.rfield_xmldocsig and set x = v.rfield_xmldocsig <- x - /// The default initialization info, for static literals - member v.LiteralValue = - match v.rfield_const with + /// The default initialization info, for static literals + member v.LiteralValue = + match v.rfield_const with | None -> None | Some Const.Zero -> None | Some k -> Some k /// Indicates if the field is zero-initialized - member v.IsZeroInit = - match v.rfield_const with - | None -> false - | Some Const.Zero -> true + member v.IsZeroInit = + match v.rfield_const with + | None -> false + | Some Const.Zero -> true | _ -> false [] @@ -2065,30 +2070,30 @@ type RecdField = [] type ExceptionInfo = - /// Indicates that an exception is an abbreviation for the given exception - | TExnAbbrevRepr of TyconRef + /// Indicates that an exception is an abbreviation for the given exception + | TExnAbbrevRepr of TyconRef - /// Indicates that an exception is shorthand for the given .NET exception type + /// Indicates that an exception is shorthand for the given .NET exception type | TExnAsmRepr of ILTypeRef - /// Indicates that an exception carries the given record of values + /// Indicates that an exception carries the given record of values | TExnFresh of TyconRecdFields - /// Indicates that an exception is abstract, i.e. is in a signature file, and we do not know the representation + /// Indicates that an exception is abstract, i.e. is in a signature file, and we do not know the representation | TExnNone // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// Represents the contents of a module or namespace [] -type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, entities: QueueList) = +type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, entities: QueueList) = /// Mutation used during compilation of FSharp.Core.dll - let mutable entities = entities + let mutable entities = entities #if !NO_TYPEPROVIDERS // One Entity per provided type or namespace even when linked concurrently from several files (graph-based checking). @@ -2100,7 +2105,7 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en // recomputes after a concurrent provided-type append instead of trusting a stale memo. Accessed via // Interlocked/Volatile rather than [] so its address can be taken for those intrinsics. let mutable entitiesVersion = 0 - + // Lookup tables keyed the way various clients expect them to be keyed. // We attach them here so we don't need to store lookup tables via any other technique. // @@ -2128,15 +2133,15 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en let mutable allValsAndMembersByPartialLinkageKeyCache: MultiMap option = None let mutable allValsByLogicalNameCache: NameMap option = None - - /// Namespace or module-compiled-as-type? - member _.ModuleOrNamespaceKind = kind - - /// Values, including members in F# types in this module-or-namespace-fragment. + + /// Namespace or module-compiled-as-type? + member _.ModuleOrNamespaceKind = kind + + /// Values, including members in F# types in this module-or-namespace-fragment. member _.AllValsAndMembers = vals - /// Type, mapping mangled name to Tycon, e.g. - //// "Dictionary`2" --> Tycon + /// Type, mapping mangled name to Tycon, e.g. + //// "Dictionary`2" --> Tycon //// "ListModule" --> Tycon with module info //// "FooException" --> Tycon with exception info member _.AllEntities = entities @@ -2185,127 +2190,127 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en let entity = create () mtyp.AddModuleOrNamespaceByMutation entity entity) -#endif - +#endif + /// Return a new module or namespace type with an entity added. - member _.AddEntity(tycon: Tycon) = + member _.AddEntity(tycon: Tycon) = ModuleOrNamespaceType(kind, vals, entities.AppendOne tycon) - + /// Return a new module or namespace type with a value added. - member _.AddVal(vspec: Val) = + member _.AddVal(vspec: Val) = ModuleOrNamespaceType(kind, vals.AppendOne vspec, entities) - + /// Get a table of the active patterns defined in this module. member _.ActivePatternElemRefLookupTable = activePatternElemRefCache - - /// Get a list of types defined within this module, namespace or type. + + /// Get a list of types defined within this module, namespace or type. member _.TypeDefinitions = entities |> Seq.filter (fun x -> not x.IsFSharpException && not x.IsModuleOrNamespace) |> Seq.toList - /// Get a list of F# exception definitions defined within this module, namespace or type. + /// Get a list of F# exception definitions defined within this module, namespace or type. member _.ExceptionDefinitions = entities |> Seq.filter (fun x -> x.IsFSharpException) |> Seq.toList - /// Get a list of module and namespace definitions defined within this module, namespace or type. + /// Get a list of module and namespace definitions defined within this module, namespace or type. member _.ModuleAndNamespaceDefinitions = entities |> Seq.filter (fun x -> x.IsModuleOrNamespace) |> Seq.toList - /// Get a list of type and exception definitions defined within this module, namespace or type. + /// Get a list of type and exception definitions defined within this module, namespace or type. member _.TypeAndExceptionDefinitions = entities |> Seq.filter (fun x -> not x.IsModuleOrNamespace) |> Seq.toList - /// Get a table of types defined within this module, namespace or type. The - /// table is indexed by both name and generic arity. This means that for generic + /// Get a table of types defined within this module, namespace or type. The + /// table is indexed by both name and generic arity. This means that for generic /// types "List`1", the entry (List, 1) will be present. - member mtyp.TypesByDemangledNameAndArity = + member mtyp.TypesByDemangledNameAndArity = let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &tyconsByDemangledNameAndArityCache (fun () -> + cacheOptByrefByVersion version &tyconsByDemangledNameAndArityCache (fun () -> LayeredMap.Empty.AddMany( mtyp.TypeAndExceptionDefinitions |> List.map (fun (tc: Tycon) -> Construct.KeyTyconByDecodedName tc.LogicalName tc) |> List.toArray)) - /// Get a table of types defined within this module, namespace or type. The + /// Get a table of types defined within this module, namespace or type. The /// table is indexed by both name and, for generic types, also by mangled name. - member mtyp.TypesByAccessNames = + member mtyp.TypesByAccessNames = let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &tyconsByAccessNamesCache (fun () -> + cacheOptByrefByVersion version &tyconsByAccessNamesCache (fun () -> LayeredMultiMap.Empty.AddMany (mtyp.TypeAndExceptionDefinitions |> List.toArray |> Array.collect (fun (tc: Tycon) -> Construct.KeyTyconByAccessNames tc.LogicalName tc))) // REVIEW: we can remove this lookup and use AllEntitiesByMangledName instead? - member mtyp.TypesByMangledName = - let addTyconByMangledName (x: Tycon) tab = NameMap.add x.LogicalName x tab + member mtyp.TypesByMangledName = + let addTyconByMangledName (x: Tycon) tab = NameMap.add x.LogicalName x tab let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &tyconsByMangledNameCache (fun () -> + cacheOptByrefByVersion version &tyconsByMangledNameCache (fun () -> List.foldBack addTyconByMangledName mtyp.TypeAndExceptionDefinitions Map.empty) /// Get a table of entities indexed by both logical and compiled names - member mtyp.AllEntitiesByCompiledAndLogicalMangledNames: NameMap = - let addEntityByMangledName (x: Entity) tab = + member mtyp.AllEntitiesByCompiledAndLogicalMangledNames: NameMap = + let addEntityByMangledName (x: Entity) tab = let name1 = x.LogicalName let name2 = x.CompiledName - let tab = NameMap.add name1 x tab + let tab = NameMap.add name1 x tab if name1 = name2 then tab - else NameMap.add name2 x tab - + else NameMap.add name2 x tab + let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &allEntitiesByMangledNameCache (fun () -> + cacheOptByrefByVersion version &allEntitiesByMangledNameCache (fun () -> QueueList.foldBack addEntityByMangledName entities Map.empty) /// Get a table of entities indexed by both logical name - member _.AllEntitiesByLogicalMangledName: NameMap = - let addEntityByMangledName (x: Entity) tab = NameMap.add x.LogicalName x tab + member _.AllEntitiesByLogicalMangledName: NameMap = + let addEntityByMangledName (x: Entity) tab = NameMap.add x.LogicalName x tab QueueList.foldBack addEntityByMangledName entities Map.empty /// Get a table of values and members indexed by partial linkage key, which includes name, the mangled name of the parent type (if any), /// and the method argument count (if any). - member _.AllValsAndMembersByPartialLinkageKey = - let addValByMangledName (x: Val) tab = + member _.AllValsAndMembersByPartialLinkageKey = + let addValByMangledName (x: Val) tab = if x.IsCompiledAsTopLevel then let key = x.GetLinkagePartialKey() - MultiMap.add key x tab + MultiMap.add key x tab else tab - cacheOptByref &allValsAndMembersByPartialLinkageKeyCache (fun () -> + cacheOptByref &allValsAndMembersByPartialLinkageKeyCache (fun () -> QueueList.foldBack addValByMangledName vals MultiMap.empty) /// Try to find the member with the given linkage key in the given module. - member mtyp.TryLinkVal(ccu: CcuThunk, key: ValLinkageFullKey) = + member mtyp.TryLinkVal(ccu: CcuThunk, key: ValLinkageFullKey) = mtyp.AllValsAndMembersByPartialLinkageKey |> MultiMap.find key.PartialKey - |> List.tryFind (fun v -> match key.TypeForLinkage with + |> List.tryFind (fun v -> match key.TypeForLinkage with | None -> true | Some keyTy -> ccu.MemberSignatureEquality(keyTy, v.Type)) |> ValueOption.ofOption /// Get a table of values indexed by logical name - member _.AllValsByLogicalName = - let addValByName (x: Val) tab = + member _.AllValsByLogicalName = + let addValByName (x: Val) tab = // Note: names may occur twice prior to raising errors about this in PostTypeCheckSemanticChecks // Earlier ones take precedence since we report errors about the later ones - if not x.IsMember && not x.IsCompilerGenerated then - NameMap.add x.LogicalName x tab + if not x.IsMember && not x.IsCompilerGenerated then + NameMap.add x.LogicalName x tab else tab - cacheOptByref &allValsByLogicalNameCache (fun () -> + cacheOptByref &allValsByLogicalNameCache (fun () -> QueueList.foldBack addValByName vals Map.empty) /// Compute a table of values and members indexed by logical name. - member _.AllValsAndMembersByLogicalNameUncached = - let addValByName (x: Val) tab = - if not x.IsCompilerGenerated then - MultiMap.add x.LogicalName x tab + member _.AllValsAndMembersByLogicalNameUncached = + let addValByName (x: Val) tab = + if not x.IsCompilerGenerated then + MultiMap.add x.LogicalName x tab else tab QueueList.foldBack addValByName vals MultiMap.empty /// Get a table of F# exception definitions indexed by demangled name, so 'FailureException' is indexed by 'Failure' - member mtyp.ExceptionDefinitionsByDemangledName = + member mtyp.ExceptionDefinitionsByDemangledName = let add (tycon: Tycon) acc = NameMap.add tycon.LogicalName tycon acc - cacheOptByref &exconsByDemangledNameCache (fun () -> + cacheOptByref &exconsByDemangledNameCache (fun () -> List.foldBack add mtyp.ExceptionDefinitions Map.empty) /// Get a table of nested module and namespace fragments indexed by demangled name (so 'ListModule' becomes 'List') - member _.ModulesAndNamespacesByDemangledName = - let add (entity: Entity) acc = - if entity.IsModuleOrNamespace then + member _.ModulesAndNamespacesByDemangledName = + let add (entity: Entity) acc = + if entity.IsModuleOrNamespace then NameMap.add entity.DemangledModuleOrNamespaceName entity acc else acc let version = System.Threading.Volatile.Read(&entitiesVersion) - cacheOptByrefByVersion version &modulesByDemangledNameCache (fun () -> + cacheOptByrefByVersion version &modulesByDemangledNameCache (fun () -> QueueList.foldBack add entities Map.empty) [] @@ -2314,13 +2319,13 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList, en override _.ToString() = "ModuleOrNamespaceType(...)" /// Represents a module or namespace definition in the typed AST -type ModuleOrNamespace = Entity +type ModuleOrNamespace = Entity /// Represents a type or exception definition in the typed AST type Tycon = Entity -let getNameOfScopeRef sref = - match sref with +let getNameOfScopeRef sref = + match sref with | ILScopeRef.Local -> "" | ILScopeRef.Module mref -> mref.Name | ILScopeRef.Assembly aref -> aref.Name @@ -2334,7 +2339,7 @@ let private isInternalCompPath x = let private (|Public|Internal|Private|) (TAccess p) = match p with | [] -> Public - | _ when List.forall isInternalCompPath p -> Internal + | _ when List.forall isInternalCompPath p -> Internal | _ -> Private let getSyntaxAccessForCompPath (TAccess a) = match a with | CompPath(_, sa, _) :: _ -> sa | _ -> SyntaxAccess.Unknown @@ -2347,7 +2352,7 @@ let updateSyntaxAccessForCompPath access syntaxAccess = /// Represents the constraint on access for a construct [] type Accessibility = - /// Indicates the construct can only be accessed from any code in the given type constructor, module or assembly. [] indicates global scope. + /// Indicates the construct can only be accessed from any code in the given type constructor, module or assembly. [] indicates global scope. | TAccess of compilationPaths: CompilationPath list member public x.IsPublic = match x with Public -> true | _ -> false @@ -2378,7 +2383,7 @@ type Accessibility = override x.ToString() = match x with | TAccess paths -> - let mangledTextOfCompPath (CompPath(scoref, _, path)) = getNameOfScopeRef scoref + "/" + textOfPath (List.map fst path) + let mangledTextOfCompPath (CompPath(scoref, _, path)) = getNameOfScopeRef scoref + "/" + textOfPath (List.map fst path) let scopename = if x.IsPublic then "public" elif x.IsInternal then "internal" @@ -2394,18 +2399,18 @@ type Accessibility = [] type TyparOptionalData = { - /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation + /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation /// The storage for the IL name for the type parameter. - mutable typar_il_name: string option + mutable typar_il_name: string option /// The documentation for the type parameter. Empty for inference variables. /// MUTABILITY: for linking when unpickling mutable typar_xmldoc: XmlDoc /// The inferred constraints for the type parameter or inference variable. - mutable typar_constraints: TyparConstraint list + mutable typar_constraints: TyparConstraint list - /// The declared attributes of the type parameter. Empty for type inference variables. + /// The declared attributes of the type parameter. Empty for type inference variables. mutable typar_attribs: Attribs /// Set to true if the typar is contravariant, i.e. declared as in C# @@ -2424,30 +2429,30 @@ type TyparData = Typar /// A declared generic type/measure parameter, or a type/measure inference variable. [] -type Typar = +type Typar = { - /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation + /// MUTABILITY: we set the names of generalized inference type parameters to make the look nice for IL code generation /// The identifier for the type parameter - mutable typar_id: Ident - + mutable typar_id: Ident + /// The flag data for the type parameter mutable typar_flags: TyparFlags - + /// The unique stamp of the type parameter /// MUTABILITY: for linking when unpickling - mutable typar_stamp: Stamp - - /// An inferred equivalence for a type inference variable. + mutable typar_stamp: Stamp + + /// An inferred equivalence for a type inference variable. mutable typar_solution: TType option /// A cached TAST type used when this type variable is used as type. mutable typar_astype: TType - + /// The optional data for the type parameter mutable typar_opt_data: TyparOptionalData option } - /// The name of the type parameter + /// The name of the type parameter member x.Name = x.typar_id.idText /// The range of the identifier for the type parameter definition @@ -2463,16 +2468,16 @@ type Typar = member x.Solution = x.typar_solution /// The inferred constraints for the type inference variable, if any - member x.Constraints = + member x.Constraints = match x.typar_opt_data with | Some optData -> optData.typar_constraints | _ -> [] - /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable + /// Indicates if the type variable is compiler generated, i.e. is an implicit type inference variable member x.IsCompilerGenerated = x.typar_flags.IsCompilerGenerated /// Indicates if the type variable can be solved or given new constraints. The status of a type variable - /// generally always evolves towards being either rigid or solved. + /// generally always evolves towards being either rigid or solved. member x.Rigidity = x.typar_flags.Rigidity /// Indicates if a type parameter is needed at runtime and may not be eliminated @@ -2505,13 +2510,13 @@ type Typar = member x.IsErased = match x.Kind with TyparKind.Type -> false | _ -> true /// The declared attributes of the type parameter. Empty for type inference variables and parameters from .NET. - member x.Attribs = + member x.Attribs = match x.typar_opt_data with | Some optData -> optData.typar_attribs | _ -> [] /// Set the attributes on the type parameter - member x.SetAttribs attribs = + member x.SetAttribs attribs = match attribs, x.typar_opt_data with | [], None -> () | [], Some { typar_il_name = None; typar_xmldoc = doc; typar_constraints = []; typar_is_contravariant = false; typar_declared_name = None } when doc.IsEmpty -> @@ -2563,14 +2568,14 @@ type Typar = | _ -> x.typar_opt_data <- Some { typar_il_name = None; typar_xmldoc = XmlDoc.Empty; typar_constraints = cs; typar_attribs = []; typar_is_contravariant = false; typar_declared_name = None } /// Marks the typar as being contravariant - member x.MarkAsContravariant() = + member x.MarkAsContravariant() = match x.typar_opt_data with | Some optData -> optData.typar_is_contravariant <- true | _ -> x.typar_opt_data <- Some { typar_il_name = None; typar_xmldoc = XmlDoc.Empty; typar_constraints = []; typar_attribs = []; typar_is_contravariant = true; typar_declared_name = None } /// Creates a type variable that contains empty data, and is not yet linked. Only used during unpickling of F# metadata. - static member NewUnlinked() : Typar = + static member NewUnlinked() : Typar = { typar_id = Unchecked.defaultof<_> typar_flags = Unchecked.defaultof<_> typar_stamp = -1L @@ -2582,37 +2587,37 @@ type Typar = static member New (data: TyparData) : Typar = data /// Links a previously unlinked type variable to the given data. Only used during unpickling of F# metadata. - member x.Link (tg: TyparData) = + member x.Link (tg: TyparData) = x.typar_id <- tg.typar_id x.typar_flags <- tg.typar_flags x.typar_stamp <- tg.typar_stamp x.typar_solution <- tg.typar_solution match tg.typar_opt_data with - | Some tg -> + | Some tg -> let optData = { typar_il_name = tg.typar_il_name; typar_xmldoc = tg.typar_xmldoc; typar_constraints = tg.typar_constraints; typar_attribs = tg.typar_attribs; typar_is_contravariant = tg.typar_is_contravariant; typar_declared_name = tg.typar_declared_name } x.typar_opt_data <- Some optData | None -> () /// Links a previously unlinked type variable to the given data. Only used during unpickling of F# metadata. - member x.AsType nullness = - match nullness with - | Nullness.Known NullnessInfo.AmbivalentToNull -> + member x.AsType nullness = + match nullness with + | Nullness.Known NullnessInfo.AmbivalentToNull -> let ty = x.typar_astype - match box ty with - | null -> + match box ty with + | null -> let ty2 = TType_var (x, Nullness.Known NullnessInfo.AmbivalentToNull) x.typar_astype <- ty2 ty2 | _ -> ty - | _ -> + | _ -> TType_var (x, nullness) /// Indicates if a type variable has been linked. Only used during unpickling of F# metadata. member x.IsLinked = x.typar_stamp <> -1L /// Indicates if a type variable has been solved. - member x.IsSolved = - match x.Solution with + member x.IsSolved = + match x.Solution with | None -> false | _ -> true @@ -2622,12 +2627,12 @@ type Typar = /// Sets the rigidity of a type variable member x.SetRigidity b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, b, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, b, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) /// Sets whether a type variable is compiler generated member x.SetCompilerGenerated b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, b, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, b, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) /// Sets whether a type variable has a static requirement member x.SetStaticReq b = @@ -2636,17 +2641,17 @@ type Typar = /// Sets whether a type variable is required at runtime member x.SetDynamicReq b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, b, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, b, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) - /// Sets whether the equality constraint of a type definition depends on this type variable + /// Sets whether the equality constraint of a type definition depends on this type variable member x.SetEqualityDependsOn b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, b, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, b, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex) - /// Sets whether the comparison constraint of a type definition depends on this type variable + /// Sets whether the comparison constraint of a type definition depends on this type variable member x.SetComparisonDependsOn b = let flags = x.typar_flags - x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, b, flags.IsSupportsNullFlex) + x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, b, flags.IsSupportsNullFlex) [] member x.DebugText = x.ToString() @@ -2655,65 +2660,65 @@ type Typar = /// Represents a constraint on a type parameter or type [] -type TyparConstraint = +type TyparConstraint = - /// A constraint that a type is a subtype of the given type + /// A constraint that a type is a subtype of the given type | CoercesTo of ty: TType * range: range - /// A constraint for a default value for an inference type variable should it be neither generalized nor solved - | DefaultsTo of priority: int * ty: TType * range: range - - /// A constraint that a type has a 'null' value - | SupportsNull of range: range - + /// A constraint for a default value for an inference type variable should it be neither generalized nor solved + | DefaultsTo of priority: int * ty: TType * range: range + + /// A constraint that a type has a 'null' value + | SupportsNull of range: range + /// A constraint that a type doesn't support nullness - | NotSupportsNull of range - - /// A constraint that a type has a member with the given signature + | NotSupportsNull of range + + /// A constraint that a type has a member with the given signature | MayResolveMember of constraintInfo: TraitConstraintInfo * range: range - - /// A constraint that a type is a non-Nullable value type - /// These are part of .NET's model of generic constraints, and in order to - /// generate verifiable code we must attach them to F# generalized type variables as well. - | IsNonNullableStruct of range: range - - /// A constraint that a type is a reference type - | IsReferenceType of range: range - - /// A constraint that a type is a simple choice between one of the given ground types. Only arises from 'printf' format strings. See format.fs - | SimpleChoice of tys: TTypes * range: range - - /// A constraint that a type has a parameterless constructor - | RequiresDefaultConstructor of range: range - - /// A constraint that a type is an enum with the given underlying - | IsEnum of ty: TType * range: range - + + /// A constraint that a type is a non-Nullable value type + /// These are part of .NET's model of generic constraints, and in order to + /// generate verifiable code we must attach them to F# generalized type variables as well. + | IsNonNullableStruct of range: range + + /// A constraint that a type is a reference type + | IsReferenceType of range: range + + /// A constraint that a type is a simple choice between one of the given ground types. Only arises from 'printf' format strings. See format.fs + | SimpleChoice of tys: TTypes * range: range + + /// A constraint that a type has a parameterless constructor + | RequiresDefaultConstructor of range: range + + /// A constraint that a type is an enum with the given underlying + | IsEnum of ty: TType * range: range + /// A constraint that a type implements IComparable, with special rules for some known structural container types - | SupportsComparison of range: range - + | SupportsComparison of range: range + /// A constraint that a type does not have the Equality(false) attribute, or is not a structural type with this attribute, with special rules for some known structural container types - | SupportsEquality of range: range - + | SupportsEquality of range: range + /// A constraint that a type is a delegate from the given tuple of args to the given return type - | IsDelegate of aty: TType * bty: TType * range: range - + | IsDelegate of aty: TType * bty: TType * range: range + /// A constraint that a type is .NET unmanaged type | IsUnmanaged of range: range - + /// An anti-constraint indicating that ref structs (e.g. Span<>) are allowed here | AllowsRefStruct of range:range // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - - override x.ToString() = sprintf "%+A" x - + + override x.ToString() = sprintf "%+A" x + [] -type TraitWitnessInfo = +type TraitWitnessInfo = | TraitWitnessInfo of tys: TTypes * memberName: string * memberFlags: SynMemberFlags * objAndArgTys: TTypes * returnTy: TType option - + /// Get the member name associated with the member constraint. member x.MemberName = (let (TraitWitnessInfo(_, b, _, _, _)) = x in b) @@ -2724,7 +2729,7 @@ type TraitWitnessInfo = member x.DebugText = x.ToString() override x.ToString() = "TraitWitnessInfo(" + x.MemberName + ")" - + /// Non-generic marker interface for storing in TraitConstraintInfo. /// The actual typed contract is ITraitContext<'AccessRights, 'MethodInfo, 'InfoReader>. type ITraitContext = interface end @@ -2736,20 +2741,20 @@ type ITraitContext<'AccessRights, 'MethodInfo, 'InfoReader> = abstract SelectExtensionMethods: traitInfo: TraitConstraintInfo * range: range * infoReader: 'InfoReader -> (TType * 'MethodInfo) list abstract AccessRights: 'AccessRights -/// The specification of a member constraint that must be solved +/// The specification of a member constraint that must be solved [] -type TraitConstraintInfo = +type TraitConstraintInfo = /// Indicates the signature of a member constraint. Contains a mutable solution cell /// to store the inferred solution of the constraint. And a mutable source cell to store /// the name of the type or member that defined the constraint. | TTrait of - tys: TTypes * - memberName: string * - memberFlags: SynMemberFlags * - objAndArgTys: TTypes * - returnTyOpt: TType option * - source: string option ref * + tys: TTypes * + memberName: string * + memberFlags: SynMemberFlags * + objAndArgTys: TTypes * + returnTyOpt: TType option * + source: string option ref * solution: TraitConstraintSln option ref * traitCtxt: ITraitContext option @@ -2763,12 +2768,12 @@ type TraitConstraintInfo = member x.MemberFlags = (let (TTrait(memberFlags = flags)) = x in flags) member x.CompiledObjectAndArgumentTypes = (let (TTrait(objAndArgTys = objAndArgTys)) = x in objAndArgTys) - + /// Get the optional return type recorded in the member constraint. member x.CompiledReturnType = (let (TTrait(returnTyOpt = retTy)) = x in retTy) - + /// Get or set the solution of the member constraint during inference - member x.Solution + member x.Solution with get() = (let (TTrait(solution = sln)) = x in sln.Value) and set v = (let (TTrait(solution = sln)) = x in sln.Value <- v) @@ -2790,17 +2795,17 @@ type TraitConstraintInfo = override x.ToString() = "TTrait(" + x.MemberLogicalName + ")" let traitCtxtNone : ITraitContext option = None - + /// Represents the solution of a member constraint during inference. [] -type TraitConstraintSln = +type TraitConstraintSln = /// FSMethSln(ty, vref, minst) /// /// Indicates a trait is solved by an F# method. /// ty -- the type and its instantiation /// vref -- the method that solves the trait constraint - /// minst -- the generic method instantiation + /// minst -- the generic method instantiation /// staticTyOpt -- the static type governing a static virtual call, if any | FSMethSln of ty: TType * vref: ValRef * minst: TypeInst * staticTyOpt: TType option @@ -2821,14 +2826,14 @@ type TraitConstraintSln = /// ty -- the type and its instantiation /// extOpt -- information about an extension member, if any /// ilMethodRef -- the method that solves the trait constraint - /// minst -- the generic method instantiation + /// minst -- the generic method instantiation /// staticTyOpt -- the static type governing a static virtual call, if any | ILMethSln of ty: TType * extOpt: ILTypeRef option * ilMethodRef: ILMethodRef * minst: TypeInst * staticTyOpt: TType option /// ClosedExprSln expr /// /// Indicates a trait is solved by an erased provided expression - | ClosedExprSln of expr: Expr + | ClosedExprSln of expr: Expr /// Indicates a trait is solved by a 'fake' instance of an operator, like '+' on integers | BuiltInSln @@ -2837,24 +2842,24 @@ type TraitConstraintSln = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// The partial information used to index the methods of all those in a ModuleOrNamespace. [] -type ValLinkagePartialKey = +type ValLinkagePartialKey = { /// The name of the type with which the member is associated. None for non-member values. - MemberParentMangledName: string option + MemberParentMangledName: string option - /// Indicates if the member is an override. - MemberIsOverride: bool + /// Indicates if the member is an override. + MemberIsOverride: bool - /// Indicates the logical name of the member. - LogicalName: string + /// Indicates the logical name of the member. + LogicalName: string /// Indicates the total argument count of the member. TotalArgCount: int - } + } [] member x.DebugText = x.ToString() @@ -2883,26 +2888,26 @@ type ValOptionalData = /// MUTABILITY: for unpickle linkage mutable val_compiled_name: string option - /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is + /// If this field is populated, this is the implementation range for an item in a signature, otherwise it is /// the signature range for an item in an implementation - mutable val_other_range: (range * bool) option + mutable val_other_range: (range * bool) option mutable val_const: Const option - - /// What is the original, unoptimized, closed-term definition, if any? + + /// What is the original, unoptimized, closed-term definition, if any? /// Used to implement [] - mutable val_defn: Expr option + mutable val_defn: Expr option /// Records the "extra information" for a value compiled as a method (rather /// than a closure or a local), including argument names, attributes etc. // - // MUTABILITY CLEANUP: mutability of this field is used by - // -- adjustAllUsesOfRecValue + // MUTABILITY CLEANUP: mutability of this field is used by + // -- adjustAllUsesOfRecValue // -- TLR optimizations // -- LinearizeTopMatch // - // For example, we use mutability to replace the empty arity initially assumed with an arity garnered from the - // type-checked expression. + // For example, we use mutability to replace the empty arity initially assumed with an arity garnered from the + // type-checked expression. mutable val_repr_info: ValReprInfo option /// Records the "extra information" for display purposes for expression-level function definitions @@ -2913,23 +2918,23 @@ type ValOptionalData = /// them with lambda arguments. mutable arg_repr_info_for_display: ArgReprInfo option - /// How visible is this? + /// How visible is this? /// MUTABILITY: for unpickle linkage - mutable val_access: Accessibility + mutable val_access: Accessibility /// XML documentation attached to a value. /// MUTABILITY: for unpickle linkage mutable val_xmldoc: XmlDoc - + /// the signature xml doc for an item in an implementation file. mutable val_other_xmldoc : XmlDoc option - /// Is the value actually an instance method/property/event that augments + /// Is the value actually an instance method/property/event that augments /// a type, and if so what name does it take in the IL? /// MUTABILITY: for unpickle linkage mutable val_member_info: ValMemberInfo option - // MUTABILITY CLEANUP: mutability of this field is used by + // MUTABILITY CLEANUP: mutability of this field is used by // -- LinearizeTopMatch // // The fresh temporary should just be created with the right parent @@ -2938,8 +2943,8 @@ type ValOptionalData = /// XML documentation signature for the value mutable val_xmldocsig: string - /// Custom attributes attached to the value. These contain references to other values (i.e. constructors in types). Mutable to fixup - /// these value references after copying a collection of values. + /// Custom attributes attached to the value. These contain references to other values (i.e. constructors in types). Mutable to fixup + /// these value references after copying a collection of values. mutable val_attribs: WellKnownValAttribs } @@ -2951,7 +2956,7 @@ type ValOptionalData = type ValData = Val [] -type Val = +type Val = { /// Mutable for unpickle linkage mutable val_logical_name: string @@ -2962,7 +2967,7 @@ type Val = mutable val_type: TType /// Mutable for unpickle linkage - mutable val_stamp: Stamp + mutable val_stamp: Stamp /// See vflags section further below for encoding/decodings here mutable val_flags: ValFlags @@ -2985,59 +2990,59 @@ type Val = val_xmldocsig = String.Empty val_attribs = WellKnownValAttribs.Empty } - /// Range of the definition (implementation) of the value, used by Visual Studio - member x.DefinitionRange = + /// Range of the definition (implementation) of the value, used by Visual Studio + member x.DefinitionRange = match x.val_opt_data with | Some { val_other_range = Some(m, true) } -> m | _ -> x.val_range - /// Range of the definition (signature) of the value, used by Visual Studio + /// Range of the definition (signature) of the value, used by Visual Studio member x.SigRange = match x.val_opt_data with | Some { arg_repr_info_for_display = Some { OtherRange = Some m } } -> m | Some { val_other_range = Some(m, false) } -> m | _ -> x.val_range - /// The place where the value was defined. + /// The place where the value was defined. member x.Range = x.val_range - /// A unique stamp within the context of this invocation of the compiler process + /// A unique stamp within the context of this invocation of the compiler process member x.Stamp = x.val_stamp - /// The type of the value. - /// May be a TType_forall for a generic value. - /// May be a type variable or type containing type variables during type inference. + /// The type of the value. + /// May be a TType_forall for a generic value. + /// May be a type variable or type containing type variables during type inference. // - // Note: this data is mutated during inference by adjustAllUsesOfRecValue when we replace the inferred type with a schema. + // Note: this data is mutated during inference by adjustAllUsesOfRecValue when we replace the inferred type with a schema. member x.Type = x.val_type /// How visible is this value, function or member? - member x.Accessibility = + member x.Accessibility = match x.val_opt_data with | Some optData -> optData.val_access | _ -> TAccess [] - /// The value of a value or member marked with [] - member x.LiteralValue = + /// The value of a value or member marked with [] + member x.LiteralValue = match x.val_opt_data with | Some optData -> optData.val_const | _ -> None /// Records the "extra information" for a value compiled as a method. /// - /// This indicates the number of arguments in each position for a curried + /// This indicates the number of arguments in each position for a curried /// functions, and relates to the F# spec for arity analysis. - /// For module-defined values, the currying is based - /// on the number of lambdas, and in each position the elements are - /// based on attempting to deconstruct the type of the argument as a - /// tuple-type. + /// For module-defined values, the currying is based + /// on the number of lambdas, and in each position the elements are + /// based on attempting to deconstruct the type of the argument as a + /// tuple-type. /// - /// The field is mutable because arities for recursive - /// values are only inferred after the r.h.s. is analyzed, but the - /// value itself is created before the r.h.s. is analyzed. + /// The field is mutable because arities for recursive + /// values are only inferred after the r.h.s. is analyzed, but the + /// value itself is created before the r.h.s. is analyzed. /// - /// TLR also sets this for inner bindings that it wants to - /// represent as "top level" bindings. + /// TLR also sets this for inner bindings that it wants to + /// represent as "top level" bindings. member x.ValReprInfo: ValReprInfo option = match x.val_opt_data with | Some optData -> optData.val_repr_info @@ -3059,22 +3064,22 @@ type Val = /// instance member), rather than an "inner" binding that may result in a closure. /// /// This is implied by IsMemberOrModuleBinding, however not vice versa, for two reasons. - /// Some optimizations mutate this value when they decide to change the representation of a + /// Some optimizations mutate this value when they decide to change the representation of a /// binding to be IsCompiledAsTopLevel. Second, even immediately after type checking we expect - /// some non-module, non-member bindings to be marked IsCompiledAsTopLevel, e.g. 'y' in + /// some non-module, non-member bindings to be marked IsCompiledAsTopLevel, e.g. 'y' in /// 'let x = let y = 1 in y + y' (NOTE: check this, don't take it as gospel) - member x.IsCompiledAsTopLevel = x.ValReprInfo.IsSome + member x.IsCompiledAsTopLevel = x.ValReprInfo.IsSome /// The partial information used to index the methods of all those in a ModuleOrNamespace. - member x.GetLinkagePartialKey() : ValLinkagePartialKey = + member x.GetLinkagePartialKey() : ValLinkagePartialKey = assert x.IsCompiledAsTopLevel - { LogicalName = x.LogicalName + { LogicalName = x.LogicalName MemberParentMangledName = (if x.IsMember then Some x.MemberApparentEntity.LogicalName else None) MemberIsOverride = x.IsOverrideOrExplicitImpl TotalArgCount = if x.IsMember then x.ValReprInfo.Value.TotalArgCount else 0 } /// The full information used to identify a specific overloaded method amongst all those in a ModuleOrNamespace. - member x.GetLinkageFullKey() : ValLinkageFullKey = + member x.GetLinkageFullKey() : ValLinkageFullKey = assert x.IsCompiledAsTopLevel let key = x.GetLinkagePartialKey() ValLinkageFullKey(key, (if x.IsMember then Some x.Type else None)) @@ -3095,7 +3100,7 @@ type Val = /// /// Note, the value may still be (a) an extension member or (b) and abstract slot without /// a true body. These cases are often causes of bugs in the compiler. - member x.MemberInfo = + member x.MemberInfo = match x.val_opt_data with | Some optData -> optData.val_member_info | _ -> None @@ -3107,12 +3112,12 @@ type Val = member x.IsIntrinsicMember = x.IsMember && not x.IsExtensionMember /// Indicates if this is an F#-defined value in a module, or an extension member, but excluding compiler generated bindings from optimizations - member x.IsModuleBinding = x.IsMemberOrModuleBinding && not x.IsMember + member x.IsModuleBinding = x.IsMemberOrModuleBinding && not x.IsMember /// Indicates if this is something compiled into a module, i.e. a user-defined value, an extension member or a compiler-generated value member x.IsCompiledIntoModule = x.IsExtensionMember || x.IsModuleBinding - /// Indicates if this is an F#-defined instance member. + /// Indicates if this is an F#-defined instance member. /// /// Note, the value may still be (a) an extension member or (b) and abstract slot without /// a true body. These cases are often causes of bugs in the compiler. @@ -3120,34 +3125,34 @@ type Val = /// Indicates if this is an F#-defined 'new' constructor member member x.IsConstructor = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo when not x.IsExtensionMember && (memberInfo.MemberFlags.MemberKind = SynMemberKind.Constructor) -> true | _ -> false /// Indicates if this is a compiler-generated class constructor member member x.IsClassConstructor = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo when not x.IsExtensionMember && (memberInfo.MemberFlags.MemberKind = SynMemberKind.ClassConstructor) -> true | _ -> false /// Indicates if this value was a member declared 'override' or an implementation of an interface slot member x.IsOverrideOrExplicitImpl = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo when memberInfo.MemberFlags.IsOverrideOrExplicitImpl -> true | _ -> false - + /// Gets the dispatch slots implemented by this method member x.ImplementedSlotSigs = - match x.MemberInfo with + match x.MemberInfo with | Some memberInfo -> memberInfo.ImplementedSlotSigs | _ -> [] - + /// Indicates if this is declared 'mutable' member x.IsMutable = (match x.val_flags.MutabilityInfo with Immutable -> false | Mutable -> true) /// Indicates if this is inferred to be a method or function that definitely makes no critical tailcalls? member x.MakesNoCriticalTailcalls = x.val_flags.MakesNoCriticalTailcalls - + /// Indicates if this is ever referenced? member x.HasBeenReferenced = x.val_flags.HasBeenReferenced @@ -3193,7 +3198,7 @@ type Val = member x.HasSignatureFile = x.SigRange <> x.DefinitionRange - + /// Get the inline declaration on the value member x.InlineInfo = x.val_flags.InlineInfo @@ -3220,7 +3225,7 @@ type Val = x.MemberInfo |> Option.exists (fun m -> m.MemberFlags.GetterOrSetterIsCompilerGenerated) /// Get the declared attributes for the value - member x.Attribs = + member x.Attribs = match x.val_opt_data with | Some optData -> optData.val_attribs.AsList() | _ -> [] @@ -3242,20 +3247,20 @@ type Val = | Some xmlDoc -> xmlDoc | None -> XmlDoc.Empty | _ -> XmlDoc.Empty - + ///Get the signature for the value's XML documentation - member x.XmlDocSig - with get() = - match x.val_opt_data with - | Some optData -> optData.val_xmldocsig + member x.XmlDocSig + with get() = + match x.val_opt_data with + | Some optData -> optData.val_xmldocsig | _ -> String.Empty - and set v = - match x.val_opt_data with - | Some optData -> optData.val_xmldocsig <- v + and set v = + match x.val_opt_data with + | Some optData -> optData.val_xmldocsig <- v | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_xmldocsig = v } /// The parent type or module, if any (None for expression bindings and parameters) - member x.TryDeclaringEntity = + member x.TryDeclaringEntity = match x.val_opt_data with | Some optData -> optData.val_declaring_entity | _ -> ParentNone @@ -3263,33 +3268,33 @@ type Val = /// Get the actual parent entity for the value (a module or a type), i.e. the entity under which the /// value will appear in compiled code. For extension members this is the module where the extension member /// is declared. - member x.DeclaringEntity = - match x.TryDeclaringEntity with + member x.DeclaringEntity = + match x.TryDeclaringEntity with | Parent tcref -> tcref | ParentNone -> error(InternalError("DeclaringEntity: does not have a parent", x.Range)) - member x.HasDeclaringEntity = - match x.TryDeclaringEntity with + member x.HasDeclaringEntity = + match x.TryDeclaringEntity with | Parent _ -> true | ParentNone -> false - + /// Get the apparent parent entity for a member - member x.MemberApparentEntity: TyconRef = - match x.MemberInfo with + member x.MemberApparentEntity: TyconRef = + match x.MemberInfo with | Some membInfo -> membInfo.ApparentEnclosingEntity | None -> error(InternalError("MemberApparentEntity", x.Range)) /// Get the number of 'this'/'self' object arguments for the member. Instance extension members return '1'. member v.NumObjArgs = - match v.MemberInfo with + match v.MemberInfo with | Some membInfo -> if membInfo.MemberFlags.IsInstance then 1 else 0 | None -> 0 /// Get the apparent parent entity for the value, i.e. the entity under with which the /// value is associated. For extension members this is the nominal type the member extends. /// For other values it is just the actual parent. - member x.ApparentEnclosingEntity = - match x.MemberInfo with + member x.ApparentEnclosingEntity = + match x.MemberInfo with | Some membInfo -> Parent(membInfo.ApparentEnclosingEntity) | None -> x.TryDeclaringEntity @@ -3301,56 +3306,56 @@ type Val = // - in ilxgen.fs: when compiling fslib, we bind an entry for the value in a global table (see bind_escaping_local_vspec) // - in opt.fs: (fullDebugTextOfValRef) for error reporting of non-inlinable values // - in service.fs (output_item_description): to display the full text of a value's binding location - // - in check.fs: as a boolean to detect public values for saving quotations - // - in ilxgen.fs: as a boolean to detect public values for saving quotations + // - in check.fs: as a boolean to detect public values for saving quotations + // - in ilxgen.fs: as a boolean to detect public values for saving quotations // - in MakeExportRemapping, to build non-local references for values - member x.PublicPath = - match x.TryDeclaringEntity with - | Parent eref -> + member x.PublicPath = + match x.TryDeclaringEntity with + | Parent eref -> match eref.PublicPath with | ValueNone -> None | ValueSome p -> Some(ValPubPath(p, x.GetLinkageFullKey())) - | ParentNone -> + | ParentNone -> None /// Indicates if this member is an F#-defined dispatch slot. - member x.IsDispatchSlot = - match x.MemberInfo with - | Some membInfo -> membInfo.MemberFlags.IsDispatchSlot + member x.IsDispatchSlot = + match x.MemberInfo with + | Some membInfo -> membInfo.MemberFlags.IsDispatchSlot | _ -> false /// Get the type of the value including any generic type parameters - member x.GeneralizedType = - match x.Type with + member x.GeneralizedType = + match x.Type with | TType_forall(tps, tau) -> tps, tau | ty -> [], ty /// Get the type of the value after removing any generic type parameters - member x.TauType = - match x.Type with + member x.TauType = + match x.Type with | TType_forall(_, tau) -> tau | ty -> ty /// Get the generic type parameters for the value - member x.Typars = - match x.Type with + member x.Typars = + match x.Type with | TType_forall(tps, _) -> tps | _ -> [] - /// The name of the method. + /// The name of the method. /// - If this is a property then this is 'get_Foo' or 'set_Foo' /// - If this is an implementation of an abstract slot then this is the name of the method implemented by the abstract slot /// - If this is an extension member then this will be the simple name - member x.LogicalName = - match x.MemberInfo with + member x.LogicalName = + match x.MemberInfo with | None -> x.val_logical_name - | Some membInfo -> - match membInfo.ImplementedSlotSigs with + | Some membInfo -> + match membInfo.ImplementedSlotSigs with | slotsig :: _ -> slotsig.Name | _ -> x.val_logical_name // Set the logical name of the value - member x.SetLogicalName(nm) = + member x.SetLogicalName(nm) = x.val_logical_name <- nm member x.ValCompiledName = @@ -3364,12 +3369,12 @@ type Val = /// - If this is an extension member then this will be a mangled name /// - If this is an operator then this is 'op_Addition' member x.CompiledName (compilerGlobalState:CompilerGlobalState option) = - let givenName = - match x.val_opt_data with + let givenName = + match x.val_opt_data with | Some { val_compiled_name = Some n } -> n - | _ -> x.LogicalName + | _ -> x.LogicalName // These cases must get stable unique names for their static field & static property. This name - // must be stable across quotation generation and IL code generation (quotations can refer to the + // must be stable across quotation generation and IL code generation (quotations can refer to the // properties implicit in these) // // Variable 'x' here, which is compiled as a top level static: @@ -3377,19 +3382,19 @@ type Val = // // The implicit 'patternInput' variable here: // let [x] = expr in ... // IsMemberOrModuleBinding = true, IsCompiledAsTopLevel = true, IsMember = false, CompilerGenerated=true - // + // // The implicit 'copyOfStruct' variables here: // let dt = System.DateTime.Now - System.DateTime.Now // IsMemberOrModuleBinding = false, IsCompiledAsTopLevel = true, IsMember = false, CompilerGenerated=true - // + // // However we don't need this for CompilerGenerated members such as the implementations of IComparable match compilerGlobalState with | Some state when x.IsCompiledAsTopLevel && not x.IsMember && (x.IsCompilerGenerated || not x.IsMemberOrModuleBinding) -> - state.StableNameGenerator.GetUniqueCompilerGeneratedName(givenName, x.Range, x.Stamp) + state.StableNameGenerator.GetUniqueCompilerGeneratedName(givenName, x.Range, x.Stamp) | _ -> givenName /// The name of the property. - /// - If this is a property then this is 'Foo' - member x.PropertyName = + /// - If this is a property then this is 'Foo' + member x.PropertyName = let logicalName = x.LogicalName ChopPropertyName logicalName @@ -3404,14 +3409,14 @@ type Val = /// - If this is an active pattern --> |A|_| /// - If this is an operator --> op_Addition /// - If this is an identifier needing backticks --> A-B - member x.DisplayNameCoreMangled = - match x.MemberInfo with - | Some membInfo -> - match membInfo.MemberFlags.MemberKind with - | SynMemberKind.ClassConstructor - | SynMemberKind.Constructor + member x.DisplayNameCoreMangled = + match x.MemberInfo with + | Some membInfo -> + match membInfo.MemberFlags.MemberKind with + | SynMemberKind.ClassConstructor + | SynMemberKind.Constructor | SynMemberKind.Member -> x.LogicalName - | SynMemberKind.PropertyGetSet + | SynMemberKind.PropertyGetSet | SynMemberKind.PropertySet | SynMemberKind.PropertyGet -> x.PropertyName | None -> x.LogicalName @@ -3419,7 +3424,7 @@ type Val = /// The display name of the value or method with operator names decompiled but without backticks etc. /// /// Note: here "Core" means "without added backticks or parens" - member x.DisplayNameCore = + member x.DisplayNameCore = x.DisplayNameCoreMangled |> ConvertValLogicalNameToDisplayNameCore /// The full text for the value to show in error messages and to use in code. @@ -3432,14 +3437,14 @@ type Val = /// - If this is an identifier needing backticks --> ``A-B`` /// - If this is a base value --> base /// - If this is a value named ``base`` --> ``base`` - member x.DisplayName = + member x.DisplayName = ConvertValLogicalNameToDisplayName x.IsBaseVal x.DisplayNameCoreMangled - member x.SetValRec b = x.val_flags <- x.val_flags.WithRecursiveValInfo b + member x.SetValRec b = x.val_flags <- x.val_flags.WithRecursiveValInfo b - member x.SetIsCompilerGenerated(v) = x.val_flags <- x.val_flags.WithIsCompilerGenerated(v) + member x.SetIsCompilerGenerated(v) = x.val_flags <- x.val_flags.WithIsCompilerGenerated(v) - member x.SetIsMemberOrModuleBinding() = x.val_flags <- x.val_flags.WithIsMemberOrModuleBinding + member x.SetIsMemberOrModuleBinding() = x.val_flags <- x.val_flags.WithIsMemberOrModuleBinding member x.SetMakesNoCriticalTailcalls() = x.val_flags <- x.val_flags.WithMakesNoCriticalTailcalls @@ -3459,12 +3464,12 @@ type Val = member x.SetIsParameter() = x.val_flags <- x.val_flags.WithIsParameter - member x.SetValReprInfo info = + member x.SetValReprInfo info = match x.val_opt_data with | Some optData -> optData.val_repr_info <- info | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_repr_info = info } - member x.SetValReprInfoForDisplay info = + member x.SetValReprInfoForDisplay info = match x.val_opt_data with | Some optData -> optData.val_repr_info_for_display <- info | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_repr_info_for_display = info } @@ -3485,13 +3490,13 @@ type Val = match x.val_opt_data with | Some optData -> optData.val_other_xmldoc <- Some xmlDoc | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_other_xmldoc = Some xmlDoc } - - member x.SetDeclaringEntity parent = + + member x.SetDeclaringEntity parent = match x.val_opt_data with | Some optData -> optData.val_declaring_entity <- parent | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_declaring_entity = parent } - member x.SetAttribs (attribs: Attribs) = + member x.SetAttribs (attribs: Attribs) = let wa = WellKnownValAttribs.Create(attribs) match x.val_opt_data with | Some optData -> optData.val_attribs <- wa @@ -3508,18 +3513,18 @@ type Val = if changed then x.SetValAttribs(waNew) result - member x.SetMemberInfo member_info = + member x.SetMemberInfo member_info = match x.val_opt_data with | Some optData -> optData.val_member_info <- Some member_info | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_member_info = Some member_info } - member x.SetValDefn val_defn = + member x.SetValDefn val_defn = match x.val_opt_data with | Some optData -> optData.val_defn <- Some val_defn | _ -> x.val_opt_data <- Some { Val.NewEmptyValOptData() with val_defn = Some val_defn } /// Create a new value with empty, unlinked data. Only used during unpickling of F# metadata. - static member NewUnlinked() : Val = + static member NewUnlinked() : Val = { val_logical_name = Unchecked.defaultof<_> val_range = Unchecked.defaultof<_> val_type = Unchecked.defaultof<_> @@ -3535,15 +3540,15 @@ type Val = member x.Link (tg: ValData) = x.SetData tg /// Set all the data on a value - member x.SetData (tg: ValData) = - x.val_logical_name <- tg.val_logical_name - x.val_range <- tg.val_range - x.val_type <- tg.val_type - x.val_stamp <- tg.val_stamp - x.val_flags <- tg.val_flags + member x.SetData (tg: ValData) = + x.val_logical_name <- tg.val_logical_name + x.val_range <- tg.val_range + x.val_type <- tg.val_type + x.val_stamp <- tg.val_stamp + x.val_flags <- tg.val_flags match tg.val_opt_data with - | Some tg -> - x.val_opt_data <- + | Some tg -> + x.val_opt_data <- Some { val_compiled_name = tg.val_compiled_name val_other_range = tg.val_other_range val_const = tg.val_const @@ -3561,26 +3566,26 @@ type Val = | None -> () /// Indicates if a value is linked to backing data yet. Only used during unpickling of F# metadata. - member x.IsLinked = match box x.val_logical_name with null -> false | _ -> true + member x.IsLinked = match box x.val_logical_name with null -> false | _ -> true [] member x.DebugText = x.ToString() override x.ToString() = x.LogicalName - - + + /// Represents the extra information stored for a member [] -type ValMemberInfo = +type ValMemberInfo = { - /// The parent type. For an extension member this is the type being extended - ApparentEnclosingEntity: TyconRef + /// The parent type. For an extension member this is the type being extended + ApparentEnclosingEntity: TyconRef - /// Updated with the full implemented slotsig after interface implementation relation is checked - mutable ImplementedSlotSigs: SlotSig list + /// Updated with the full implemented slotsig after interface implementation relation is checked + mutable ImplementedSlotSigs: SlotSig list - /// Gets updated with 'true' if an abstract slot is implemented in the file being typechecked. Internal only. - mutable IsImplemented: bool + /// Gets updated with 'true' if an abstract slot is implemented in the file being typechecked. Internal only. + mutable IsImplemented: bool MemberFlags: SynMemberFlags } @@ -3591,10 +3596,10 @@ type ValMemberInfo = override x.ToString() = "ValMemberInfo(...)" [] -type NonLocalValOrMemberRef = +type NonLocalValOrMemberRef = { /// A reference to the entity containing the value or member. This will always be a non-local reference - EnclosingEntity: EntityRef + EnclosingEntity: EntityRef /// The name of the value, or the full signature of the member ItemKey: ValLinkageFullKey @@ -3612,11 +3617,11 @@ type NonLocalValOrMemberRef = /// For debugging override x.ToString() = !! x.EnclosingEntity.nlr.ToString() + "::" + x.ItemKey.PartialKey.LogicalName - + /// Represents the path information for a reference to a value or member in another assembly, disassociated /// from any particular reference. [] -type ValPublicPath = +type ValPublicPath = | ValPubPath of PublicPath * ValLinkageFullKey [] @@ -3626,14 +3631,14 @@ type ValPublicPath = /// Represents an index into the namespace/module structure of an assembly [] -type NonLocalEntityRef = +type NonLocalEntityRef = | NonLocalEntityRef of CcuThunk * string[] /// Try to find the entity corresponding to the given path in the given CCU - static member TryDerefEntityPath(ccu: CcuThunk, path: string[], i: int, entity: Entity) = + static member TryDerefEntityPath(ccu: CcuThunk, path: string[], i: int, entity: Entity) = if i >= path.Length then ValueSome entity - else - match entity.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames.TryGetValue path[i] with + else + match entity.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames.TryGetValue path[i] with | true, res -> NonLocalEntityRef.TryDerefEntityPath(ccu, path, (i+1), res) #if !NO_TYPEPROVIDERS | _ -> NonLocalEntityRef.TryDerefEntityPathViaProvidedType(ccu, path, i, entity) @@ -3643,21 +3648,21 @@ type NonLocalEntityRef = #if !NO_TYPEPROVIDERS /// Try to find the entity corresponding to the given path, using type-providers to link the data - static member TryDerefEntityPathViaProvidedType(ccu: CcuThunk, path: string[], i: int, entity: Entity) = + static member TryDerefEntityPathViaProvidedType(ccu: CcuThunk, path: string[], i: int, entity: Entity) = // Errors during linking are not necessarily given good ranges. This has always been the case in F# 2.0, but also applies to // type provider type linking errors in F# 3.0. let m = range0 match entity.TypeReprInfo with - | TProvidedTypeRepr info -> + | TProvidedTypeRepr info -> let resolutionEnvironment = info.ResolutionEnvironment let st = info.ProvidedType - + // In this case, we're safely in the realm of types. Just iterate through the nested // types until i = path.Length-1. Create the Tycon's as needed - let rec tryResolveNestedTypeOf(parentEntity: Entity, resolutionEnvironment, st: Tainted, i) = + let rec tryResolveNestedTypeOf(parentEntity: Entity, resolutionEnvironment, st: Tainted, i) = match st.PApply((fun st -> st.GetNestedType path[i]), m) with | Tainted.Null -> ValueNone - | Tainted.NonNull st -> + | Tainted.NonNull st -> let canonicalEntity = parentEntity.ModuleOrNamespaceType.GetOrInternProvidedEntity( path[i], @@ -3667,9 +3672,9 @@ type NonLocalEntityRef = tryResolveNestedTypeOf(entity, resolutionEnvironment, st, i) - | TProvidedNamespaceRepr(resolutionEnvironment, resolvers) -> + | TProvidedNamespaceRepr(resolutionEnvironment, resolvers) -> - // In this case, we're still in the realm of extensible namespaces. + // In this case, we're still in the realm of extensible namespaces. // <----entity--> // 0 .........i-1..i .......... j ..... path.Length-1 // @@ -3679,16 +3684,16 @@ type NonLocalEntityRef = // <----entity--> <---resolver----> <--loop---> // 0 .........i-1..i ............. j ..... path.Length-1 // - // We now query the resolvers with - // moduleOrNamespace = path.[0..j-1] - // typeName = path.[j] + // We now query the resolvers with + // moduleOrNamespace = path.[0..j-1] + // typeName = path.[j] // starting with j = i and then progressively increasing j - + // This function queries at 'j' - let tryResolvePrefix j = + let tryResolvePrefix j = assert (j >= 0) assert (j <= path.Length - 1) - let matched = + let matched = [ for resolver in resolvers do let moduleOrNamespace = if j = 0 then [| |] else path[0..j-1] let typename = path[j] @@ -3704,8 +3709,8 @@ type NonLocalEntityRef = // 'entity' is at position i in the dereference chain. We resolved to position 'j'. // Inject namespaces until we're an position j, and then inject the type. // Note: this is similar to code in CompileOps.fs - let rec injectNamespacesFromIToJ (entity: Entity) k = - if k = j then + let rec injectNamespacesFromIToJ (entity: Entity) k = + if k = j then entity.ModuleOrNamespaceType.GetOrInternProvidedEntity( path[j], (fun () -> Construct.NewProvidedTycon(resolutionEnvironment, st, ccu.ImportProvidedType, false, m))) @@ -3715,22 +3720,22 @@ type NonLocalEntityRef = path[k], (fun () -> let cpath = entity.CompilationPath.NestedCompPath entity.LogicalName (ModuleOrNamespaceKind.Namespace false) - Construct.NewModuleOrNamespace - (Some cpath) - (TAccess []) (ident(path[k], m)) XmlDoc.Empty [] + Construct.NewModuleOrNamespace + (Some cpath) + (TAccess []) (ident(path[k], m)) XmlDoc.Empty [] (MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType (Namespace true))))) injectNamespacesFromIToJ newEntity (k+1) let newEntity = injectNamespacesFromIToJ entity i - + // newEntity is at 'j' - NonLocalEntityRef.TryDerefEntityPath(ccu, path, (j+1), newEntity) + NonLocalEntityRef.TryDerefEntityPath(ccu, path, (j+1), newEntity) - | [] -> ValueNone + | [] -> ValueNone | _ -> failwith "Unexpected" - let rec tryResolvePrefixes j = + let rec tryResolvePrefixes j = if j >= path.Length then ValueNone - else match tryResolvePrefix j with + else match tryResolvePrefix j with | ValueNone -> tryResolvePrefixes (j+1) | ValueSome res -> ValueSome res @@ -3738,11 +3743,11 @@ type NonLocalEntityRef = | _ -> ValueNone #endif - + /// Try to link a non-local entity reference to an actual entity - member nleref.TryDeref canError = - let (NonLocalEntityRef(ccu, path)) = nleref - if canError then + member nleref.TryDeref canError = + let (NonLocalEntityRef(ccu, path)) = nleref + if canError then ccu.EnsureDerefable path if ccu.IsUnresolvedReference then ValueNone else @@ -3752,62 +3757,62 @@ type NonLocalEntityRef = | ValueNone -> // OK, the lookup failed. Check if we can redirect through a type forwarder on this assembly. // Look for a forwarder for each prefix-path - let rec tryForwardPrefixPath i = - if i < path.Length then + let rec tryForwardPrefixPath i = + if i < path.Length then match ccu.TryForward(path[0..i-1], path[i]) with // OK, found a forwarder, now continue with the lookup to find the nested type - | Some tcref -> NonLocalEntityRef.TryDerefEntityPath(ccu, path, (i+1), tcref.Deref) + | Some tcref -> NonLocalEntityRef.TryDerefEntityPath(ccu, path, (i+1), tcref.Deref) | None -> tryForwardPrefixPath (i+1) else ValueNone tryForwardPrefixPath 0 - + /// Get the CCU referenced by the nonlocal reference. member nleref.Ccu = - let (NonLocalEntityRef(ccu, _)) = nleref + let (NonLocalEntityRef(ccu, _)) = nleref ccu /// Get the path into the CCU referenced by the nonlocal reference. member nleref.Path = - let (NonLocalEntityRef(_, p)) = nleref + let (NonLocalEntityRef(_, p)) = nleref p member nleref.DisplayName = String.concat "." nleref.Path /// Get the mangled name of the last item in the path of the nonlocal reference. - member nleref.LastItemMangledName = + member nleref.LastItemMangledName = let p = nleref.Path p[p.Length-1] /// Get the all-but-last names of the path of the nonlocal reference. - member nleref.EnclosingMangledPath = + member nleref.EnclosingMangledPath = let p = nleref.Path p[0..p.Length-2] - + /// Get the name of the assembly referenced by the nonlocal reference. member nleref.AssemblyName = nleref.Ccu.AssemblyName /// Dereference the nonlocal reference, and raise an error if this fails. - member nleref.Deref = - match nleref.TryDeref(canError=true) with + member nleref.Deref = + match nleref.TryDeref(canError=true) with | ValueSome res -> res - | ValueNone -> - errorR (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefModuleNamespace, nleref.DisplayName, nleref.AssemblyName, "")) + | ValueNone -> + errorR (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefModuleNamespace, nleref.DisplayName, nleref.AssemblyName, "")) raise (KeyNotFoundException()) [] member x.DebugText = x.ToString() override x.ToString() = x.DisplayName - + [] -type EntityRef = +type EntityRef = { - /// Indicates a reference to something bound in this CCU + /// Indicates a reference to something bound in this CCU mutable binding: NonNullSlot - /// Indicates a reference to something bound in another CCU + /// Indicates a reference to something bound in another CCU nlr: NonLocalEntityRef } @@ -3821,36 +3826,36 @@ type EntityRef = member x.ResolvedTarget = x.binding /// Resolve the reference - member private tcr.Resolve canError = + member private tcr.Resolve canError = let res = tcr.nlr.TryDeref canError - match res with - | ValueSome r -> - tcr.binding <- nullableSlotFull r - | ValueNone -> + match res with + | ValueSome r -> + tcr.binding <- nullableSlotFull r + | ValueNone -> () /// Dereference the TyconRef to a Tycon. Amortize the cost of doing this. /// This path should not allocate in the amortized case - member tcr.Deref = - match box tcr.binding with + member tcr.Deref = + match box tcr.binding with | null -> tcr.Resolve(canError=true) - match box tcr.binding with + match box tcr.binding with | null -> error (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefModuleNamespaceType, String.concat "." tcr.nlr.EnclosingMangledPath, tcr.nlr.AssemblyName, tcr.nlr.LastItemMangledName)) | _ -> tcr.binding - | _ -> + | _ -> tcr.binding /// Dereference the TyconRef to a Tycon option. - member tcr.TryDeref = - match box tcr.binding with - | null -> + member tcr.TryDeref = + match box tcr.binding with + | null -> tcr.Resolve(canError=false) - match box tcr.binding with + match box tcr.binding with | null -> ValueNone | _ -> ValueSome tcr.binding - | _ -> + | _ -> ValueSome tcr.binding /// Is the destination assembly available? @@ -3868,10 +3873,10 @@ type EntityRef = /// The signature definition location of the namespace, module or type member x.SigRange = x.Deref.SigRange - /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException + /// The name of the namespace, module or type, possibly with mangling, e.g. List`1, List or FailureException member x.LogicalName = x.Deref.LogicalName - /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException + /// The compiled name of the namespace, module or type, e.g. FSharpList`1, ListModule or FailureException member x.CompiledName = x.Deref.CompiledName /// The display name of the namespace, module or type, e.g. List instead of List`1, not including static parameters @@ -3897,8 +3902,8 @@ type EntityRef = /// The code location where the module, namespace or type is defined. member x.Range = x.Deref.Range - /// A unique stamp for this module, namespace or type definition within the context of this compilation. - /// Note that because of signatures, there are situations where in a single compilation the "same" + /// A unique stamp for this module, namespace or type definition within the context of this compilation. + /// Note that because of signatures, there are situations where in a single compilation the "same" /// module, namespace or type may have two distinct Entity objects that have distinct stamps. member x.Stamp = x.Deref.Stamp @@ -3908,7 +3913,7 @@ type EntityRef = /// The XML documentation of the entity, if any. If the entity is backed by provided metadata /// then this _does_ include this documentation. If the entity is backed by Abstract IL metadata - /// or comes from another F# assembly then it does not (because the documentation will get read from + /// or comes from another F# assembly then it does not (because the documentation will get read from /// an XML file). member x.XmlDoc = if not x.Deref.XmlDoc.IsEmpty then @@ -3917,7 +3922,7 @@ type EntityRef = x.Deref.entity_opt_data |> Option.bind (fun d -> d.entity_other_xmldoc) |> Option.defaultValue XmlDoc.Empty - + member x.SetOtherXmlDoc (xmlDoc: XmlDoc) = x.Deref.SetOtherXmlDoc(xmlDoc) /// The XML documentation sig-string of the entity, if any, to use to lookup an .xml doc file. This also acts @@ -3926,7 +3931,7 @@ type EntityRef = /// The logical contents of the entity when it is a module or namespace fragment. member x.ModuleOrNamespaceType = x.Deref.ModuleOrNamespaceType - + /// Demangle the module name, if FSharpModuleWithSuffix is used member x.DemangledModuleOrNamespaceName = x.Deref.DemangledModuleOrNamespaceName @@ -3942,12 +3947,12 @@ type EntityRef = /// The information about the r.h.s. of a type definition, if any. For example, the r.h.s. of a union or record type. member x.TypeReprInfo = x.Deref.TypeReprInfo - /// The information about the r.h.s. of an F# exception definition, if any. + /// The information about the r.h.s. of an F# exception definition, if any. member x.ExceptionInfo = x.Deref.ExceptionInfo /// Indicates if the entity represents an F# exception declaration. member x.IsFSharpException = x.Deref.IsFSharpException - + /// Get the type parameters for an entity that is a type declaration, otherwise return the empty list. /// /// Lazy because it may read metadata. Uses the entity's own range for error context. @@ -3971,10 +3976,10 @@ type EntityRef = /// Get the value representing the accessibility of an F# type definition or module. member x.Accessibility = x.Deref.Accessibility - /// Indicates the type prefers the "tycon" syntax for display etc. + /// Indicates the type prefers the "tycon" syntax for display etc. member x.IsPrefixDisplay = x.Deref.IsPrefixDisplay - /// Indicates the "tycon blob" is actually a module + /// Indicates the "tycon blob" is actually a module member x.IsModuleOrNamespace = x.Deref.IsModuleOrNamespace /// Indicates if the entity is a namespace @@ -4068,7 +4073,7 @@ type EntityRef = /// Indicates if this is a struct or enum type definition, i.e. a value type definition, including struct records and unions member x.IsStructOrEnumTycon = x.Deref.IsStructOrEnumTycon - /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses + /// Indicates if this is an F# type definition which is one of the special types in FSharp.Core.dll which uses /// an assembly-code representation for the type, e.g. the primitive array type constructor. member x.IsAsmReprTycon = x.Deref.IsAsmReprTycon @@ -4078,7 +4083,7 @@ type EntityRef = /// Indicates if the entity is erased, either a measure definition, or an erased provided type definition member x.IsErased = x.Deref.IsErased - + /// Gets any implicit hash/equals (with comparer argument) methods added to an F# record, union or struct type definition. member x.GeneratedHashAndEqualsWithComparerValues = x.Deref.GeneratedHashAndEqualsWithComparerValues @@ -4090,11 +4095,11 @@ type EntityRef = /// Gets any implicit hash/equals methods added to an F# record, union or struct type definition. member x.GeneratedHashAndEqualsValues = x.Deref.GeneratedHashAndEqualsValues - + /// Indicate if this is a type definition backed by Abstract IL metadata. member x.IsILTycon = x.Deref.IsILTycon - /// Get the Abstract IL scope, nesting and metadata for this + /// Get the Abstract IL scope, nesting and metadata for this /// type definition, assuming it is backed by Abstract IL metadata. member x.ILTyconInfo = x.Deref.ILTyconInfo @@ -4134,19 +4139,19 @@ type EntityRef = /// which in F# is called a 'unknown representation' type). member x.IsHiddenReprTycon = x.Deref.IsHiddenReprTycon - /// Indicates if this is an F#-defined interface type definition + /// Indicates if this is an F#-defined interface type definition member x.IsFSharpInterfaceTycon = x.Deref.IsFSharpInterfaceTycon - /// Indicates if this is an F#-defined delegate type definition + /// Indicates if this is an F#-defined delegate type definition member x.IsFSharpDelegateTycon = x.Deref.IsFSharpDelegateTycon - /// Indicates if this is an F#-defined enum type definition + /// Indicates if this is an F#-defined enum type definition member x.IsFSharpEnumTycon = x.Deref.IsFSharpEnumTycon - /// Indicates if this is a .NET-defined enum type definition + /// Indicates if this is a .NET-defined enum type definition member x.IsILEnumTycon = x.Deref.IsILEnumTycon - /// Indicates if this is an enum type definition + /// Indicates if this is an enum type definition member x.IsEnumTycon = x.Deref.IsEnumTycon /// Indicates if this is an F#-defined value type definition, including struct records and unions @@ -4174,11 +4179,11 @@ type EntityRef = [] member x.DebugText = x.ToString() - override x.ToString() = - if x.IsLocalRef then - x.ResolvedTarget.DisplayName - else - x.nlr.DisplayName + override x.ToString() = + if x.IsLocalRef then + x.ResolvedTarget.DisplayName + else + x.nlr.DisplayName /// Represents a module-or-namespace reference in the typed abstract syntax. type ModuleOrNamespaceRef = EntityRef @@ -4188,12 +4193,12 @@ type TyconRef = EntityRef /// References are either local or nonlocal [] -type ValRef = +type ValRef = { - /// Indicates a reference to something bound in this CCU + /// Indicates a reference to something bound in this CCU mutable binding: NonNullSlot - /// Indicates a reference to something bound in another CCU + /// Indicates a reference to something bound in another CCU nlr: NonLocalValOrMemberRef } @@ -4204,35 +4209,35 @@ type ValRef = member x.ResolvedTarget = x.binding /// Dereference the ValRef to a Val. - member x.Deref = + member x.Deref = if obj.ReferenceEquals(x.binding, null) then - let res = - let nlr = x.nlr - let e = nlr.EnclosingEntity.Deref + let res = + let nlr = x.nlr + let e = nlr.EnclosingEntity.Deref let possible = e.ModuleOrNamespaceType.TryLinkVal(nlr.EnclosingEntity.nlr.Ccu, nlr.ItemKey) - match possible with + match possible with | ValueNone -> error (InternalUndefinedItemRef (FSComp.SR.tastUndefinedItemRefVal, e.DisplayNameWithStaticParameters, nlr.AssemblyName, sprintf "%+A" nlr.ItemKey.PartialKey)) | ValueSome h -> h - x.binding <- nullableSlotFull res - res + x.binding <- nullableSlotFull res + res else x.binding /// Dereference the ValRef to a Val option. - member x.TryDeref = + member x.TryDeref = if obj.ReferenceEquals(x.binding, null) then - let resOpt = - match x.nlr.EnclosingEntity.TryDeref with + let resOpt = + match x.nlr.EnclosingEntity.TryDeref with | ValueNone -> ValueNone | ValueSome e -> e.ModuleOrNamespaceType.TryLinkVal(x.nlr.EnclosingEntity.nlr.Ccu, x.nlr.ItemKey) - match resOpt with + match resOpt with | ValueNone -> () - | ValueSome res -> - x.binding <- nullableSlotFull res + | ValueSome res -> + x.binding <- nullableSlotFull res resOpt else ValueSome x.binding - /// The type of the value. May be a TType_forall for a generic value. - /// May be a type variable or type containing type variables during type inference. + /// The type of the value. May be a TType_forall for a generic value. + /// May be a type variable or type containing type variables during type inference. member x.Type = x.Deref.Type /// Get the type of the value including any generic type parameters @@ -4268,29 +4273,29 @@ type ValRef = member x.SigRange = x.Deref.SigRange - /// The value of a value or member marked with [] + /// The value of a value or member marked with [] member x.LiteralValue = x.Deref.LiteralValue member x.Id = x.Deref.Id /// Get the name of the value, assuming it is compiled as a property. - /// - If this is a property then this is 'Foo' + /// - If this is a property then this is 'Foo' /// - If this is an implementation of an abstract slot then this is the name of the property implemented by the abstract slot member x.PropertyName = x.Deref.PropertyName /// Indicates whether this value represents a property getter. - member x.IsPropertyGetterMethod = + member x.IsPropertyGetterMethod = match x.MemberInfo with | None -> false | Some (memInfo: ValMemberInfo) -> memInfo.MemberFlags.MemberKind = SynMemberKind.PropertyGet || memInfo.MemberFlags.MemberKind = SynMemberKind.PropertyGetSet /// Indicates whether this value represents a property setter. - member x.IsPropertySetterMethod = + member x.IsPropertySetterMethod = match x.MemberInfo with | None -> false | Some (memInfo: ValMemberInfo) -> memInfo.MemberFlags.MemberKind = SynMemberKind.PropertySet || memInfo.MemberFlags.MemberKind = SynMemberKind.PropertyGetSet - /// A unique stamp within the context of this invocation of the compiler process + /// A unique stamp within the context of this invocation of the compiler process member x.Stamp = x.Deref.Stamp /// Is this represented as a "top level" static binding (i.e. a static field, static member, @@ -4328,7 +4333,7 @@ type ValRef = /// Indicates if this is an F#-defined value in a module, or an extension member, but excluding compiler generated bindings from optimizations member x.IsModuleBinding = x.Deref.IsModuleBinding - /// Indicates if this is an F#-defined instance member. + /// Indicates if this is an F#-defined instance member. /// /// Note, the value may still be (a) an extension member or (b) and abstract slot without /// a true body. These cases are often causes of bugs in the compiler. @@ -4422,13 +4427,13 @@ type ValRef = [] member x.DebugText = x.ToString() - override x.ToString() = - if x.IsLocalRef then x.ResolvedTarget.DisplayName + override x.ToString() = + if x.IsLocalRef then x.ResolvedTarget.DisplayName else x.nlr.ToString() /// Represents a reference to a case of a union type [] -type UnionCaseRef = +type UnionCaseRef = | UnionCaseRef of tyconRef: TyconRef * caseName: string /// Get a reference to the type containing this union case @@ -4441,14 +4446,14 @@ type UnionCaseRef = member x.Tycon = x.TyconRef.Deref /// Dereference the reference to the union case - member x.UnionCase = - match x.TyconRef.GetUnionCaseByName x.CaseName with + member x.UnionCase = + match x.TyconRef.GetUnionCaseByName x.CaseName with | Some res -> res | None -> error(InternalError(sprintf "union case %s not found in type %s" x.CaseName x.TyconRef.LogicalName, x.TyconRef.Range)) - /// Try to dereference the reference + /// Try to dereference the reference member x.TryUnionCase = - x.TyconRef.TryDeref + x.TyconRef.TryDeref |> ValueOption.bind (fun tcref -> tcref.GetUnionCaseByName x.CaseName |> ValueOption.ofOption) /// Get the attributes associated with the union case @@ -4464,11 +4469,11 @@ type UnionCaseRef = member x.SigRange = x.UnionCase.SigRange /// Get the index of the union case amongst the cases - member x.Index = - try - // REVIEW: this could be faster, e.g. by storing the index in the NameMap - x.TyconRef.UnionCasesArray |> Array.findIndex (fun uc -> uc.LogicalName = x.CaseName) - with :? KeyNotFoundException -> + member x.Index = + try + // REVIEW: this could be faster, e.g. by storing the index in the NameMap + x.TyconRef.UnionCasesArray |> Array.findIndex (fun uc -> uc.LogicalName = x.CaseName) + with :? KeyNotFoundException -> error(InternalError(sprintf "union case %s not found in type %s" x.CaseName x.TyconRef.LogicalName, x.TyconRef.Range)) /// Get the fields of the union case @@ -4500,7 +4505,7 @@ let findLogicalFieldIndexOfRecordField (tcref:TyconRef) (id:string) = /// Represents a reference to a field in a record, class or struct [] -type RecdFieldRef = +type RecdFieldRef = | RecdFieldRef of tyconRef: TyconRef * fieldName: string /// Get a reference to the type containing this record field @@ -4515,34 +4520,34 @@ type RecdFieldRef = /// Get the Entity for the type containing this record field member x.Tycon = x.TyconRef.Deref - /// Dereference the reference - member x.RecdField = + /// Dereference the reference + member x.RecdField = let (RecdFieldRef(tcref, id)) = x - match tcref.GetFieldByName id with + match tcref.GetFieldByName id with | Some res -> res | None -> error(InternalError(sprintf "field %s not found in type %s" id tcref.LogicalName, tcref.Range)) - /// Try to dereference the reference - member x.TryRecdField = - x.TyconRef.TryDeref + /// Try to dereference the reference + member x.TryRecdField = + x.TyconRef.TryDeref |> ValueOption.bind (fun tcref -> tcref.GetFieldByName x.FieldName |> ValueOption.ofOption) - /// Get the attributes associated with the compiled property of the record field + /// Get the attributes associated with the compiled property of the record field member x.PropertyAttribs = x.RecdField.PropertyAttribs - /// Get the declaration range of the record field + /// Get the declaration range of the record field member x.Range = x.RecdField.Range - /// Get the definition range of the record field + /// Get the definition range of the record field member x.DefinitionRange = x.RecdField.DefinitionRange - /// Get the signature range of the record field + /// Get the signature range of the record field member x.SigRange = x.RecdField.SigRange member x.Index = let (RecdFieldRef(tcref, id)) = x findLogicalFieldIndexOfRecordField tcref id - + [] member x.DebugText = x.ToString() @@ -4550,7 +4555,7 @@ type RecdFieldRef = override x.ToString() = x.FieldName [] -type Nullness = +type Nullness = | Known of NullnessInfo | Variable of NullnessVar /// The value is known to be non-null because it was produced by a constructor call. @@ -4563,14 +4568,14 @@ type Nullness = | KnownFromConstructor -> Known NullnessInfo.WithoutNull | n -> n - member n.Evaluate() = - match n with + member n.Evaluate() = + match n with | Known info -> info | Variable v -> v.Evaluate() | KnownFromConstructor -> NullnessInfo.WithoutNull - member n.TryEvaluate() = - match n with + member n.TryEvaluate() = + match n with | Known info -> ValueSome info | Variable v -> v.TryEvaluate() | KnownFromConstructor -> NullnessInfo.WithoutNull |> ValueSome @@ -4581,42 +4586,42 @@ type Nullness = // Note, nullness variables are only created if the nullness checking feature is on [] -type NullnessVar() = +type NullnessVar() = let mutable solution: Nullness option = None - member nv.Evaluate() = - match solution with + member nv.Evaluate() = + match solution with | None -> NullnessInfo.WithoutNull | Some soln -> soln.Evaluate() - member nv.TryEvaluate() = - match solution with + member nv.TryEvaluate() = + match solution with | None -> ValueNone | Some soln -> soln.TryEvaluate() member nv.IsSolved = solution.IsSome - member nv.IsFullySolved = + member nv.IsFullySolved = match solution with | None -> false | Some (Nullness.Known _) -> true | Some (Nullness.KnownFromConstructor) -> true | Some (Nullness.Variable v) -> v.IsFullySolved - member nv.Set(nullness) = - assert (not nv.IsSolved) + member nv.Set(nullness) = + assert (not nv.IsSolved) solution <- Some nullness - member nv.Unset() = + member nv.Unset() = assert nv.IsSolved solution <- None - member nv.Solution = + member nv.Solution = assert nv.IsSolved solution.Value [] -type NullnessInfo = +type NullnessInfo = /// we know that there is an extra null value in the type | WithNull @@ -4631,7 +4636,7 @@ type NullnessInfo = [] type TType = - /// Indicates the type is a universal type, only used for types of values and members + /// Indicates the type is a universal type, only used for types of values and members | TType_forall of typars: Typars * bodyTy: TType /// TType_app(tyconRef, typeInstantiation, nullness). @@ -4647,7 +4652,7 @@ type TType = /// TType_fun(domainType, rangeType, nullness). /// - /// Indicates the type is a function type + /// Indicates the type is a function type | TType_fun of domainType: TType * rangeType: TType * nullness: Nullness /// Indicates the type is a non-F#-visible type representing a "proof" that a union value belongs to a particular union case @@ -4655,7 +4660,7 @@ type TType = /// the temporaries arising out of pattern matching on union values. | TType_ucase of unionCaseRef: UnionCaseRef * typeInstantiation: TypeInst - /// Indicates the type is a variable type, whether declared, generalized or an inference type parameter + /// Indicates the type is a variable type, whether declared, generalized or an inference type parameter | TType_var of typar: Typar * nullness: Nullness /// Indicates the type is a unit-of-measure expression being used as an argument to a type or member @@ -4679,24 +4684,24 @@ type TType = [] member x.DebugText = x.LimitedToString(4) - member x.LimitedToString(maxDepth:int) = - match x with + member x.LimitedToString(maxDepth:int) = + match x with | TType_forall (_tps, ty) -> "forall ... " + ty.ToString() - | TType_app (tcref, tinst, nullness) -> tcref.DisplayName + (match tinst with [] -> "" | tys -> "<" + String.concat "," (List.map string tys) + ">") + nullness.ToString() - | TType_tuple (tupInfo, tinst) -> - (match tupInfo with + | TType_app (tcref, tinst, nullness) -> tcref.DisplayName + (match tinst with [] -> "" | tys -> "<" + String.concat "," (List.map string tys) + ">") + nullness.ToString() + | TType_tuple (tupInfo, tinst) -> + (match tupInfo with | TupInfo.Const false -> "" | TupInfo.Const true -> "struct ") + String.concat "," (List.map string tinst) + ")" - | TType_anon (anonInfo, tinst) -> - (match anonInfo.TupInfo with + | TType_anon (anonInfo, tinst) -> + (match anonInfo.TupInfo with | TupInfo.Const false -> "" | TupInfo.Const true -> "struct ") + "{|" + String.concat "," (Seq.map2 (fun nm ty -> nm + " " + string ty + ";") anonInfo.SortedNames tinst) + "|}" | TType_fun (domainTy, retTy, nullness) -> "(" + string domainTy + " -> " + string retTy + ")" + nullness.ToString() | TType_ucase (uc, tinst) -> "ucase " + uc.CaseName + (match tinst with [] -> "" | tys -> "<" + String.concat "," (List.map string tys) + ">") - | TType_var (tp, _) -> - match tp.Solution with + | TType_var (tp, _) -> + match tp.Solution with | None -> tp.DisplayName | Some t -> tp.DisplayName + $" (solved: {if maxDepth < 0 then Boolean.TrueString else t.LimitedToString(maxDepth-1)})" | TType_measure ms -> ms.ToString() @@ -4704,16 +4709,16 @@ type TType = override x.ToString() = x.LimitedToString(4) -type TypeInst = TType list +type TypeInst = TType list -type TTypes = TType list +type TTypes = TType list -/// Represents the information identifying an anonymous record -[] -type AnonRecdTypeInfo = +/// Represents the information identifying an anonymous record +[] +type AnonRecdTypeInfo = { // Mutability for pickling/unpickling only - mutable Assembly: CcuThunk + mutable Assembly: CcuThunk mutable TupInfo: TupInfo @@ -4727,7 +4732,7 @@ type AnonRecdTypeInfo = } /// Create an AnonRecdTypeInfo from the basic data - static member Create(ccu: CcuThunk, tupInfo, ids: Ident[]) = + static member Create(ccu: CcuThunk, tupInfo, ids: Ident[]) = let sortedIds = ids |> Array.sortBy (fun id -> id.idText) // Hash all the data to form a unique stamp. @@ -4736,9 +4741,9 @@ type AnonRecdTypeInfo = let stamp = sha1HashInt64 [| for c in ccu.AssemblyName do yield byte c; yield byte (int32 c >>> 8) - match tupInfo with + match tupInfo with | TupInfo.Const b -> yield (if b then 0uy else 1uy) - for id in sortedIds do + for id in sortedIds do for c in id.idText do yield byte c; yield byte (int32 c >>> 8) yield 0uy |] @@ -4756,11 +4761,11 @@ type AnonRecdTypeInfo = { Assembly = ccu; TupInfo = tupInfo; SortedIds = sortedIds; Stamp = stamp; SortedNames = sortedNames; IlTypeName = ilName } /// Get the ILTypeRef for the generated type implied by the anonymous type - member x.ILTypeRef = + member x.ILTypeRef = let ilTypeName = sprintf "<>f__AnonymousType%s%u`%d" (match x.TupInfo with TupInfo.Const b -> if b then "1000" else "") (uint32 x.IlTypeName) x.SortedIds.Length mkILTyRef(x.Assembly.ILScopeRef, ilTypeName) - static member NewUnlinked() : AnonRecdTypeInfo = + static member NewUnlinked() : AnonRecdTypeInfo = { Assembly = Unchecked.defaultof<_> TupInfo = Unchecked.defaultof<_> SortedIds = Unchecked.defaultof<_> @@ -4768,7 +4773,7 @@ type AnonRecdTypeInfo = SortedNames = Unchecked.defaultof<_> IlTypeName = Unchecked.defaultof<_> } - member x.Link d = + member x.Link d = let sortedNames = Array.map textOfId d.SortedIds x.Assembly <- d.Assembly x.TupInfo <- d.TupInfo @@ -4778,19 +4783,19 @@ type AnonRecdTypeInfo = x.IlTypeName <- d.IlTypeName member x.IsLinked = (match box x.SortedIds with null -> true | _ -> false) - + member x.DisplayNameCoreByIdx idx = x.SortedNames[idx] member x.DisplayNameByIdx idx = x.SortedNames[idx] |> ConvertLogicalNameToDisplayName -[] -type TupInfo = +[] +type TupInfo = /// Some constant, e.g. true or false for tupInfo | Const of bool /// Represents a unit of measure in the typed AST [] -type Measure = +type Measure = /// A variable unit-of-measure | Var of typar: Typar @@ -4807,17 +4812,17 @@ type Measure = /// The unit of measure '1', e.g. float = float<1> | One of range: range - /// Raising a measure to a rational power + /// Raising a measure to a rational power | RationalPower of measure: Measure * power: Rational // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - + override x.ToString() = sprintf "%+A" x - - member x.Range = - match x with + + member x.Range = + match x with | Var(typar) -> typar.Range | Const(range= m) -> m | Prod(range= m) -> m @@ -4861,26 +4866,26 @@ module WellKnownValAttribs = let CreateWithFlags(attribs: Attrib list, flags: WellKnownValAttributes) = WellKnownAttribs(attribs, flags) -type Attribs = Attrib list +type Attribs = Attrib list [] -type AttribKind = +type AttribKind = - /// Indicates an attribute refers to a type defined in an imported .NET assembly - | ILAttrib of ilMethodRef: ILMethodRef + /// Indicates an attribute refers to a type defined in an imported .NET assembly + | ILAttrib of ilMethodRef: ILMethodRef - /// Indicates an attribute refers to a type defined in an imported F# assembly + /// Indicates an attribute refers to a type defined in an imported F# assembly | FSAttrib of valRef: ValRef // %+A formatting is used, so this is not needed //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// Attrib(tyconRef, kind, unnamedArgs, propVal, appliedToAGetterOrSetter, targetsOpt, range) [] -type Attrib = +type Attrib = | Attrib of tyconRef: TyconRef * @@ -4902,10 +4907,10 @@ type Attrib = /// We keep both source expression and evaluated expression around to help intellisense and signature printing [] -type AttribExpr = +type AttribExpr = /// AttribExpr(source, evaluated) - | AttribExpr of source: Expr * evaluated: Expr + | AttribExpr of source: Expr * evaluated: Expr [] member x.DebugText = x.ToString() @@ -4914,7 +4919,7 @@ type AttribExpr = /// AttribNamedArg(name, type, isField, value) [] -type AttribNamedArg = +type AttribNamedArg = | AttribNamedArg of (string*TType*bool*AttribExpr) [] @@ -4924,7 +4929,7 @@ type AttribNamedArg = /// Constants in expressions [] -type Const = +type Const = | Bool of bool | SByte of sbyte | Byte of byte @@ -4940,9 +4945,9 @@ type Const = | Double of double | Char of char | String of string - | Decimal of Decimal + | Decimal of Decimal | Unit - | Zero // null/zero-bit-pattern + | Zero // null/zero-bit-pattern [] member x.DebugText = x.ToString() @@ -4974,12 +4979,12 @@ type Const = /// the decision tree are labelled by integers that are unique for that /// particular tree. [] -type DecisionTree = +type DecisionTree = /// TDSwitch(input, cases, default, range) /// - /// Indicates a decision point in a decision tree. - /// input -- The expression being tested. If switching over a struct union this + /// Indicates a decision point in a decision tree. + /// input -- The expression being tested. If switching over a struct union this /// must be the address of the expression being tested. /// cases -- The list of tests and their subsequent decision trees /// default -- The default decision tree, if any @@ -4991,11 +4996,11 @@ type DecisionTree = /// Indicates the decision tree has terminated with success, transferring control to the given target with the given parameters. /// results -- the expressions to be bound to the variables at the target /// target -- the target number for the continuation - | TDSuccess of results: Exprs * targetNum: int + | TDSuccess of results: Exprs * targetNum: int /// TDBind(binding, body) /// - /// Bind the given value through the remaining cases of the dtree. + /// Bind the given value through the remaining cases of the dtree. /// These arise from active patterns and some optimizations to prevent /// repeated computations in decision trees. /// binding -- the value and the expression it is bound to @@ -5006,11 +5011,11 @@ type DecisionTree = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x /// Represents a test and a subsequent decision tree [] -type DecisionTreeCase = +type DecisionTreeCase = | TCase of discriminator: DecisionTreeTest * caseTree: DecisionTree /// Get the discriminator associated with the case @@ -5023,49 +5028,49 @@ type DecisionTreeCase = member x.DebugText = x.ToString() override x.ToString() = sprintf "DecisionTreeCase(...)" - + [] type ActivePatternReturnKind = | RefTypeWrapper | StructTypeWrapper | Boolean - member this.IsStruct with get () = + member this.IsStruct with get () = match this with | RefTypeWrapper -> false | StructTypeWrapper | Boolean -> true [] -type DecisionTreeTest = +type DecisionTreeTest = /// Test if the input to a decision tree matches the given union case | UnionCase of caseRef: UnionCaseRef * tinst: TypeInst - /// Test if the input to a decision tree is an array of the given length - | ArrayLength of length: int * ty: TType + /// Test if the input to a decision tree is an array of the given length + | ArrayLength of length: int * ty: TType - /// Test if the input to a decision tree is the given constant value + /// Test if the input to a decision tree is the given constant value | Const of value: Const - /// Test if the input to a decision tree is null - | IsNull + /// Test if the input to a decision tree is null + | IsNull /// IsInst(source, target) /// - /// Test if the input to a decision tree is an instance of the given type + /// Test if the input to a decision tree is an instance of the given type | IsInst of source: TType * target: TType /// Test.ActivePatternCase(activePatExpr, activePatResTys, activePatRetKind, activePatIdentity, idx, activePatInfo) /// - /// Run the active pattern and bind a successful result to a - /// variable in the remaining tree. + /// Run the active pattern and bind a successful result to a + /// variable in the remaining tree. /// activePatExpr -- The active pattern function being called, perhaps applied to some active pattern parameters. /// activePatResTys -- The result types (case types) of the active pattern. /// activePatRetKind -- Indicating what is returning from the active pattern - /// activePatIdentity -- The value and the types it is applied to. If there are any active pattern parameters then this is empty. + /// activePatIdentity -- The value and the types it is applied to. If there are any active pattern parameters then this is empty. /// idx -- The case number of the active pattern which the test relates to. /// activePatternInfo -- The extracted info for the active pattern. | ActivePatternCase of - activePatExpr: Expr * + activePatExpr: Expr * activePatResTys: TTypes * activePatRetKind: ActivePatternReturnKind * activePatIdentity: (ValRef * TypeInst) option * @@ -5079,16 +5084,16 @@ type DecisionTreeTest = //[] //member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x -/// A target of a decision tree. Can be thought of as a little function, though is compiled as a local block. +/// A target of a decision tree. Can be thought of as a little function, though is compiled as a local block. /// -- boundVals - The values bound at the target, matching the valuesin the TDSuccess /// -- targetExpr - The expression to evaluate if we branch to the target /// -- debugPoint - The debug point for the target /// -- isStateVarFlags - Indicates which, if any, of the values are represents as state machine variables [] -type DecisionTreeTarget = - | TTarget of +type DecisionTreeTarget = + | TTarget of boundVals: Val list * targetExpr: Expr * isStateVarFlags: bool list option @@ -5108,7 +5113,7 @@ type Bindings = Binding list /// -- expr: The expression to execute to get the value /// -- debugPoint: The debug point for the binding [] -type Binding = +type Binding = | TBind of var: Val * expr: Expr * @@ -5128,10 +5133,10 @@ type Binding = override x.ToString() = sprintf "TBind(%s, ...)" (x.Var.CompiledName None) -/// Represents a reference to an active pattern element. The -/// integer indicates which choice in the target set is being selected by this item. +/// Represents a reference to an active pattern element. The +/// integer indicates which choice in the target set is being selected by this item. [] -type ActivePatternElemRef = +type ActivePatternElemRef = | APElemRef of activePatternInfo: ActivePatternInfo * activePatternVal: ValRef * @@ -5147,7 +5152,7 @@ type ActivePatternElemRef = /// Get a reference to the value for the active pattern being referred to member x.ActivePatternRetKind = (let (APElemRef(_, _, _, activePatRetKind)) = x in activePatRetKind) - /// Get the index of the active pattern element within the overall active pattern + /// Get the index of the active pattern element within the overall active pattern member x.CaseIndex = (let (APElemRef(_, _, n, _)) = x in n) [] @@ -5158,12 +5163,12 @@ type ActivePatternElemRef = /// Records the "extra information" for a value compiled as a method (rather /// than a closure or a local), including argument names, attributes etc. [] -type ValReprInfo = +type ValReprInfo = /// ValReprInfo (typars, args, result) | ValReprInfo of typars: TyparReprInfo list * args: ArgReprInfo list list * - result: ArgReprInfo + result: ArgReprInfo /// Get the extra information about the arguments for the value member x.ArgInfos = (let (ValReprInfo(_, args, _)) = x in args) @@ -5183,17 +5188,17 @@ type ValReprInfo = /// Get the kind of each type parameter member x.KindsOfTypars = (let (ValReprInfo(n, _, _)) = x in n |> List.map (fun (TyparReprInfo(_, k)) -> k)) - /// Get the total number of arguments - member x.TotalArgCount = + /// Get the total number of arguments + member x.TotalArgCount = let (ValReprInfo(_, args, _)) = x // This is List.sumBy List.length args // We write this by hand as it can be a performance bottleneck in LinkagePartialKey - let rec loop (args: ArgReprInfo list list) acc = - match args with - | [] -> acc - | [] :: t -> loop t acc - | [_] :: t -> loop t (acc+1) - | (_ :: _ :: h) :: t -> loop t (acc + h.Length + 2) + let rec loop (args: ArgReprInfo list list) acc = + match args with + | [] -> acc + | [] :: t -> loop t acc + | [_] :: t -> loop t (acc+1) + | (_ :: _ :: h) :: t -> loop t (acc + h.Length + 2) loop args 0 member x.ArgNames = @@ -5215,7 +5220,7 @@ type ArgReprInfo = { /// The attributes for the argument // MUTABILITY: used when propagating signature attributes into the implementation. - mutable Attribs: WellKnownValAttribs + mutable Attribs: WellKnownValAttribs /// The name for the argument at this position, if any // MUTABILITY: used when propagating names of parameters from signature into the implementation. @@ -5232,13 +5237,13 @@ type ArgReprInfo = override _.ToString() = "ArgReprInfo(...)" /// Records the extra metadata stored about typars for type parameters -/// compiled as "real" IL type parameters, specifically for values with +/// compiled as "real" IL type parameters, specifically for values with /// ValReprInfo. Any information here is propagated from signature through /// to the compiled code. type TyparReprInfo = TyparReprInfo of Ident * TyparKind type Typars = Typar list - + type Exprs = Expr list type Vals = Val list @@ -5246,42 +5251,42 @@ type Vals = Val list /// Represents an expression in the typed abstract syntax [] type Expr = - /// A constant expression. + /// A constant expression. | Const of value: Const * range: range * constType: TType - /// Reference a value. The flag is only relevant if the value is an object model member - /// and indicates base calls and special uses of object constructors. + /// Reference a value. The flag is only relevant if the value is an object model member + /// and indicates base calls and special uses of object constructors. | Val of valRef: ValRef * flags: ValUseFlag * range: range - /// Sequence expressions, used for "a;b", "let a = e in b;a" and "a then b" (the last an OO constructor). + /// Sequence expressions, used for "a;b", "let a = e in b;a" and "a then b" (the last an OO constructor). | Sequential of expr1: Expr * expr2: Expr * kind: SequentialOpKind * range: range - /// Lambda expressions. - - /// Why multiple vspecs? A Expr.Lambda taking multiple arguments really accepts a tuple. - /// But it is in a convenient form to be compile accepting multiple - /// arguments, e.g. if compiled as a toplevel static method. + /// Lambda expressions. + + /// Why multiple vspecs? A Expr.Lambda taking multiple arguments really accepts a tuple. + /// But it is in a convenient form to be compile accepting multiple + /// arguments, e.g. if compiled as a toplevel static method. | Lambda of unique: Unique * ctorThisValOpt: Val option * baseValOpt: Val option * valParams: Val list * - bodyExpr: Expr * + bodyExpr: Expr * range: range * overallType: TType - /// Type lambdas. These are used for the r.h.s. of polymorphic 'let' bindings and - /// for expressions that implement first-class polymorphic values. + /// Type lambdas. These are used for the r.h.s. of polymorphic 'let' bindings and + /// for expressions that implement first-class polymorphic values. | TyLambda of unique: Unique * typeParams: Typars * @@ -5290,45 +5295,45 @@ type Expr = overallType: TType /// Applications. - /// Applications combine type and term applications, and are normalized so - /// that sequential applications are combined, so "(f x y)" becomes "f [[x];[y]]". - /// The type attached to the function is the formal function type, used to ensure we don't build application - /// nodes that over-apply when instantiating at function types. + /// Applications combine type and term applications, and are normalized so + /// that sequential applications are combined, so "(f x y)" becomes "f [[x];[y]]". + /// The type attached to the function is the formal function type, used to ensure we don't build application + /// nodes that over-apply when instantiating at function types. | App of funcExpr: Expr * formalType: TType * typeArgs: TypeInst * args: Exprs * - range: range + range: range - /// Bind a recursive set of values. + /// Bind a recursive set of values. | LetRec of bindings: Bindings * bodyExpr: Expr * range: range * frees: FreeVarsCache - /// Bind a value. + /// Bind a value. | Let of binding: Binding * bodyExpr: Expr * range: range * frees: FreeVarsCache - // Object expressions: A closure that implements an interface or a base type. - // The base object type might be a delegate type. - | Obj of - unique: Unique * + // Object expressions: A closure that implements an interface or a base type. + // The base object type might be a delegate type. + | Obj of + unique: Unique * objTy: TType * (* <-- NOTE: specifies type parameters for base type *) - baseVal: Val option * - ctorCall: Expr * - overrides: ObjExprMethod list * - interfaceImpls: (TType * ObjExprMethod list) list * + baseVal: Val option * + ctorCall: Expr * + overrides: ObjExprMethod list * + interfaceImpls: (TType * ObjExprMethod list) list * range: range - /// Matches are a more complicated form of "let" with multiple possible destinations - /// and possibly multiple ways to get to each destination. - /// The first range is that of the expression being matched, which is used + /// Matches are a more complicated form of "let" with multiple possible destinations + /// and possibly multiple ways to get to each destination. + /// The first range is that of the expression being matched, which is used /// as the range for all the decision making and binding that happens during the decision tree /// execution. | Match of @@ -5339,24 +5344,24 @@ type Expr = fullRange: range * exprType: TType - /// If we statically know some information then in many cases we can use a more optimized expression - /// This is primarily used by terms in the standard library, particularly those implementing overloaded - /// operators. + /// If we statically know some information then in many cases we can use a more optimized expression + /// This is primarily used by terms in the standard library, particularly those implementing overloaded + /// operators. | StaticOptimization of conditions: StaticOptimization list * expr: Expr * alternativeExpr: Expr * range: range - /// An intrinsic applied to some (strictly evaluated) arguments - /// A few of intrinsics (TOp_try, TOp.While, TOp.IntegerForLoop) expect arguments kept in a normal form involving lambdas + /// An intrinsic applied to some (strictly evaluated) arguments + /// A few of intrinsics (TOp_try, TOp.While, TOp.IntegerForLoop) expect arguments kept in a normal form involving lambdas | Op of op: TOp * typeArgs: TypeInst * args: Exprs * range: range - /// Indicates the expression is a quoted expression tree. + /// Indicates the expression is a quoted expression tree. /// // MUTABILITY: this use of mutability is awkward and perhaps should be removed | Quote of @@ -5364,8 +5369,8 @@ type Expr = quotationInfo: ((ILTypeRef list * TTypes * Exprs * ExprData) * (ILTypeRef list * TTypes * Exprs * ExprData)) option ref * isFromQueryExpression: bool * range: range * - quotedType: TType - + quotedType: TType + /// Used in quotation generation to indicate a witness argument, spliced into a quotation literal. /// /// For example: @@ -5379,28 +5384,28 @@ type Expr = /// /// f$W(witnessForSin, x) { return Deserialize(<@ sin$W _spliceHole1 _spliceHole2 @>, [| WitnessArg(witnessForSin), x |]) } /// - /// where _spliceHole1 will be the location of the witness argument in the quotation data, and + /// where _spliceHole1 will be the location of the witness argument in the quotation data, and /// witnessArg is the lambda for the witness - /// + /// | WitnessArg of traitInfo: TraitConstraintInfo * range: range - /// Indicates a free choice of typars that arises due to - /// minimization of polymorphism at let-rec bindings. These are - /// resolved to a concrete instantiation on subsequent rewrites. + /// Indicates a free choice of typars that arises due to + /// minimization of polymorphism at let-rec bindings. These are + /// resolved to a concrete instantiation on subsequent rewrites. | TyChoose of typeParams: Typars * bodyExpr: Expr * range: range - /// An instance of a link node occurs for every use of a recursively bound variable. When type-checking - /// the recursive bindings a dummy expression is stored in the mutable reference cell. - /// After type checking the bindings this is replaced by a use of the variable, perhaps at an - /// appropriate type instantiation. These are immediately eliminated on subsequent rewrites. + /// An instance of a link node occurs for every use of a recursively bound variable. When type-checking + /// the recursive bindings a dummy expression is stored in the mutable reference cell. + /// After type checking the bindings this is replaced by a use of the variable, perhaps at an + /// appropriate type instantiation. These are immediately eliminated on subsequent rewrites. | Link of Expr ref - /// Indicates a debug point should be placed prior to the expression. + /// Indicates a debug point should be placed prior to the expression. | DebugPoint of DebugPointAtLeafExpr * Expr [] @@ -5408,14 +5413,14 @@ type Expr = override expr.ToString() = expr.ToDebugString(3) - member expr.ToDebugString(depth: int) : string = + member expr.ToDebugString(depth: int) : string = if depth = 0 then ".." else let depth = depth - 1 - match expr with + match expr with | Const (c, _, _) -> string c | Val (v, _, _) -> v.LogicalName | Sequential (e1, e2, _, _) -> "Sequential(" + e1.ToDebugString(depth) + ", " + e2.ToDebugString(depth) + ")" - | Lambda (_, _, _, vs, body, _, _) -> sprintf "Lambda(%+A, " vs + body.ToDebugString(depth) + ")" + | Lambda (_, _, _, vs, body, _, _) -> sprintf "Lambda(%+A, " vs + body.ToDebugString(depth) + ")" | TyLambda (_, tps, body, _, _) -> sprintf "TyLambda(%+A, " tps + body.ToDebugString(depth) + ")" | App (f, _, _, args, _) -> "App(" + f.ToDebugString(depth) + ", [" + String.concat ", " (args |> List.map (fun e -> e.ToDebugString(depth))) + "])" | LetRec _ -> "LetRec(..)" @@ -5434,24 +5439,24 @@ type Expr = member expr.Range = match expr with | Expr.Val (_, _, m) | Expr.Op (_, _, _, m) | Expr.Const (_, m, _) | Expr.Quote (_, _, _, m, _) - | Expr.Obj (_, _, _, _, _, _, m) | Expr.App (_, _, _, _, m) | Expr.Sequential (_, _, _, m) - | Expr.StaticOptimization (_, _, _, m) | Expr.Lambda (_, _, _, _, _, m, _) + | Expr.Obj (_, _, _, _, _, _, m) | Expr.App (_, _, _, _, m) | Expr.Sequential (_, _, _, m) + | Expr.StaticOptimization (_, _, _, m) | Expr.Lambda (_, _, _, _, _, m, _) | Expr.WitnessArg (_, m) | Expr.TyLambda (_, _, _, m, _)| Expr.TyChoose (_, _, m) | Expr.LetRec (_, _, m, _) | Expr.Let (_, _, m, _) | Expr.Match (_, _, _, _, m, _) -> m | Expr.Link eref -> eref.Value.Range | Expr.DebugPoint (_, e2) -> e2.Range - + [] type TOp = /// An operation representing the creation of a union value of the particular union case - | UnionCase of UnionCaseRef + | UnionCase of UnionCaseRef /// An operation representing the creation of an exception value using an F# exception declaration | ExnConstr of TyconRef /// An operation representing the creation of a tuple value - | Tuple of TupInfo + | Tuple of TupInfo /// An operation representing the creation of an anonymous record | AnonRecd of AnonRecdTypeInfo @@ -5466,7 +5471,7 @@ type TOp = | Bytes of byte[] /// Constant uint16 arrays (used for parser tables) - | UInt16s of uint16[] + | UInt16s of uint16[] /// An operation representing a lambda-encoded while loop. The special while loop marker is used to mark compilations of 'foreach' expressions | While of spWhile: DebugPointAtWhile * marker: SpecialWhileLoopMarker @@ -5480,30 +5485,30 @@ type TOp = /// An operation representing a lambda-encoded try/finally | TryFinally of spTry: DebugPointAtTry * spFinally: DebugPointAtFinally - /// Construct a record or object-model value. The ValRef is for self-referential class constructors, otherwise - /// it indicates that we're in a constructor and the purpose of the expression is to - /// fill in the fields of a pre-created but uninitialized object, and to assign the initialized - /// version of the object into the optional mutable cell pointed to be the given value. + /// Construct a record or object-model value. The ValRef is for self-referential class constructors, otherwise + /// it indicates that we're in a constructor and the purpose of the expression is to + /// fill in the fields of a pre-created but uninitialized object, and to assign the initialized + /// version of the object into the optional mutable cell pointed to be the given value. | Recd of RecordConstructionInfo * TyconRef - + /// An operation representing setting a record or class field - | ValFieldSet of RecdFieldRef + | ValFieldSet of RecdFieldRef /// An operation representing getting a record or class field - | ValFieldGet of RecdFieldRef + | ValFieldGet of RecdFieldRef /// An operation representing getting the address of a record field - | ValFieldGetAddr of RecdFieldRef * readonly: bool + | ValFieldGetAddr of RecdFieldRef * readonly: bool /// An operation representing getting an integer tag for a union value representing the union case number - | UnionCaseTagGet of TyconRef + | UnionCaseTagGet of TyconRef /// An operation representing a coercion that proves a union value is of a particular union case. This is not a test, its /// simply added proof to enable us to generate verifiable code for field access on union types | UnionCaseProof of UnionCaseRef /// An operation representing a field-get from a union value, where that value has been proven to be of the corresponding union case. - | UnionCaseFieldGet of UnionCaseRef * int + | UnionCaseFieldGet of UnionCaseRef * int /// An operation representing a field-get from a union value, where that value has been proven to be of the corresponding union case. | UnionCaseFieldGetAddr of UnionCaseRef * int * readonly: bool @@ -5512,27 +5517,27 @@ type TOp = | UnionCaseFieldSet of UnionCaseRef * int /// An operation representing a field-get from an F# exception value. - | ExnFieldGet of TyconRef * int + | ExnFieldGet of TyconRef * int /// An operation representing a field-set on an F# exception value. - | ExnFieldSet of TyconRef * int + | ExnFieldSet of TyconRef * int /// An operation representing a field-get from an F# tuple value. - | TupleFieldGet of TupInfo * int + | TupleFieldGet of TupInfo * int - /// IL assembly code - type list are the types pushed on the stack - | ILAsm of - instrs: ILInstr list * - retTypes: TTypes + /// IL assembly code - type list are the types pushed on the stack + | ILAsm of + instrs: ILInstr list * + retTypes: TTypes - /// Generate a ldflda on an 'a ref. + /// Generate a ldflda on an 'a ref. | RefAddrGet of bool - /// Conversion node, compiled via type-directed translation or to box/unbox - | Coerce + /// Conversion node, compiled via type-directed translation or to box/unbox + | Coerce - /// Represents a "rethrow" operation. May not be rebound, or used outside of try-finally, expecting a unit argument - | Reraise + /// Represents a "rethrow" operation. May not be rebound, or used outside of try-finally, expecting a unit argument + | Reraise /// Used for state machine compilation | Return @@ -5543,34 +5548,34 @@ type TOp = /// Used for state machine compilation | Label of ILCodeLabel - /// Pseudo method calls. This is used for overloaded operations like op_Addition. - | TraitCall of TraitConstraintInfo + /// Pseudo method calls. This is used for overloaded operations like op_Addition. + | TraitCall of TraitConstraintInfo - /// Operation nodes representing C-style operations on byrefs and mutable vals (l-values) - | LValueOp of LValueOperation * ValRef + /// Operation nodes representing C-style operations on byrefs and mutable vals (l-values) + | LValueOp of LValueOperation * ValRef /// IL method calls. - /// isProperty -- used for quotation reflection, property getters & setters - /// noTailCall - DllImport? if so don't tailcall + /// isProperty -- used for quotation reflection, property getters & setters + /// noTailCall - DllImport? if so don't tailcall /// retTypes -- the types of pushed values, if any - | ILCall of - isVirtual: bool * - isProtected: bool * - isStruct: bool * - isCtor: bool * - valUseFlag: ValUseFlag * - isProperty: bool * - noTailCall: bool * - ilMethRef: ILMethodRef * - enclTypeInst: TypeInst * - methInst: TypeInst * - retTypes: TTypes + | ILCall of + isVirtual: bool * + isProtected: bool * + isStruct: bool * + isCtor: bool * + valUseFlag: ValUseFlag * + isProperty: bool * + noTailCall: bool * + ilMethRef: ILMethodRef * + enclTypeInst: TypeInst * + methInst: TypeInst * + retTypes: TTypes [] member x.DebugText = x.ToString() - - override op.ToString() = - match op with + + override op.ToString() = + match op with | UnionCase ucref -> "UnionCase(" + ucref.CaseName + ")" | ExnConstr ecref -> "ExnConstr(" + ecref.LogicalName + ")" | Tuple _tupinfo -> "Tuple" @@ -5607,13 +5612,13 @@ type TOp = | ILCall (_,_,_,_,_,_,_,ilMethRef,_,_,_) -> "ILCall(" + ilMethRef.ToString() + ",..)" /// Represents the kind of record construction operation. -type RecordConstructionInfo = +type RecordConstructionInfo = - /// We're in an explicit constructor. The purpose of the record expression is to - /// fill in the fields of a pre-created but uninitialized object + /// We're in an explicit constructor. The purpose of the record expression is to + /// fill in the fields of a pre-created but uninitialized object | RecdExprIsObjInit - /// Normal record construction + /// Normal record construction | RecdExpr /// If this is Some ty then it indicates that a .NET 2.0 constrained call is required, with the given type as the @@ -5621,52 +5626,52 @@ type RecordConstructionInfo = type ConstrainedCallInfo = TType option /// Represents the kind of looping operation. -type SpecialWhileLoopMarker = +type SpecialWhileLoopMarker = | NoSpecialWhileLoopMarker /// Marks the compiled form of a 'for ... in ... do ' expression | WhileLoopForCompiledForEachExprMarker - + /// Represents the kind of looping operation. -type ForLoopStyle = +type ForLoopStyle = /// Evaluate start and end once, loop up - | FSharpForLoopUp + | FSharpForLoopUp /// Evaluate start and end once, loop down - | FSharpForLoopDown + | FSharpForLoopDown /// Evaluate start once and end multiple times, loop up | CSharpForLoopUp /// Indicates what kind of pointer operation this is. -type LValueOperation = +type LValueOperation = - /// In C syntax this is: &localv + /// In C syntax this is: &localv | LAddrOf of readonly: bool - /// In C syntax this is: *localv_ptr - | LByrefGet + /// In C syntax this is: *localv_ptr + | LByrefGet /// In C syntax this is: localv = e, note == *(&localv) = e == LAddrOf; LByrefSet - | LSet + | LSet - /// In C syntax this is: *localv_ptr = e - | LByrefSet + /// In C syntax this is: *localv_ptr = e + | LByrefSet /// Represents the kind of sequential operation, i.e. "normal" or "to a before returning b" -type SequentialOpKind = - /// a ; b - | NormalSeq +type SequentialOpKind = + /// a ; b + | NormalSeq - /// let res = a in b;res - | ThenDoSeq + /// let res = a in b;res + | ThenDoSeq /// Indicates how a value, function or member is being used at a particular usage point. type ValUseFlag = /// Indicates a use of a value represents a call to a method that may require - /// a .NET 2.0 constrained call. A constrained call is only used for calls where + /// a .NET 2.0 constrained call. A constrained call is only used for calls where // the object argument is a value type or generic type, and the call is to a method // on System.Object, System.ValueType, System.Enum or an interface methods. | PossibleConstrainedCall of ty: TType @@ -5682,21 +5687,21 @@ type ValUseFlag = /// A call to a base method, e.g. 'base.OnPaint(args)' | VSlotDirectCall - + /// Represents the kind of an F# core library static optimization construct -type StaticOptimization = +type StaticOptimization = /// Indicates the static optimization applies when a type equality holds | TTyconEqualsTycon of ty1: TType * ty2: TType /// Indicates the static optimization applies when a type is a struct - | TTyconIsStruct of ty: TType - -/// A representation of a method in an object expression. + | TTyconIsStruct of ty: TType + +/// A representation of a method in an object expression. /// /// TObjExprMethod(slotsig, attribs, methTyparsOfOverridingMethod, methodParams, methodBodyExpr, m) [] -type ObjExprMethod = +type ObjExprMethod = | TObjExprMethod of slotSig: SlotSig * @@ -5717,7 +5722,7 @@ type ObjExprMethod = /// /// TSlotSig(methodName, declaringType, declaringTypeParameters, methodTypeParameters, slotParameters, returnTy) [] -type SlotSig = +type SlotSig = | TSlotSig of methodName: string * declaringType: TType * @@ -5729,7 +5734,7 @@ type SlotSig = /// The name of the method member ss.Name = let (TSlotSig(nm, _, _, _, _, _)) = ss in nm - /// The (instantiated) type which the slot is logically a part of + /// The (instantiated) type which the slot is logically a part of member ss.DeclaringType = let (TSlotSig(_, ty, _, _, _, _)) = ss in ty /// The class type parameters of the slot @@ -5749,11 +5754,11 @@ type SlotSig = override ss.ToString() = sprintf "TSlotSig(%s, ...)" ss.Name -/// Represents a parameter to an abstract method slot. +/// Represents a parameter to an abstract method slot. /// /// TSlotParam(nm, ty, inFlag, outFlag, optionalFlag, attribs) [] -type SlotParam = +type SlotParam = | TSlotParam of paramName: string option * paramType: TType * @@ -5773,19 +5778,19 @@ type SlotParam = type OpenDeclaration = { /// Syntax after 'open' as it's presented in source code. Target: SynOpenDeclTarget - + /// Full range of the open declaration. Range: range option /// Modules or namespaces which is opened with this declaration. - Modules: ModuleOrNamespaceRef list - + Modules: ModuleOrNamespaceRef list + /// Types whose static content is opened with this declaration. Types: TType list /// Scope in which open declaration is visible. - AppliedScope: range - + AppliedScope: range + /// If it's `namespace Xxx.Yyy` declaration. IsOwnNamespace: bool } @@ -5794,24 +5799,24 @@ type OpenDeclaration = static member Create(target: SynOpenDeclTarget, modules: ModuleOrNamespaceRef list, types: TType list, appliedScope: range, isOwnNamespace: bool) = { Target = target Range = - match target with + match target with | SynOpenDeclTarget.ModuleOrNamespace (range=m) | SynOpenDeclTarget.Type (range=m) -> Some m Types = types Modules = modules AppliedScope = appliedScope IsOwnNamespace = isOwnNamespace } - -/// The contents of a module-or-namespace-fragment definition + +/// The contents of a module-or-namespace-fragment definition [] -type ModuleOrNamespaceContents = - /// Indicates the module fragment is made of several module fragments in succession - | TMDefs of defs: ModuleOrNamespaceContents list +type ModuleOrNamespaceContents = + /// Indicates the module fragment is made of several module fragments in succession + | TMDefs of defs: ModuleOrNamespaceContents list /// Indicates the given 'open' declarations are active | TMDefOpens of openDecls: OpenDeclaration list - /// Indicates the module fragment is a 'let' definition + /// Indicates the module fragment is a 'let' definition | TMDefLet of binding: Binding * range: range /// Indicates the module fragment is an evaluation of expression for side-effects @@ -5824,19 +5829,19 @@ type ModuleOrNamespaceContents = //[] member x.DebugText = x.ToString() - override x.ToString() = sprintf "%+A" x + override x.ToString() = sprintf "%+A" x -/// A named module-or-namespace-fragment definition +/// A named module-or-namespace-fragment definition [] -type ModuleOrNamespaceBinding = +type ModuleOrNamespaceBinding = - | Binding of binding: Binding + | Binding of binding: Binding /// The moduleOrNamespace represents the signature of the module. /// The moduleOrNamespaceContents contains the definitions of the module. /// The same set of entities are bound in the ModuleOrNamespace as in the ModuleOrNamespaceContents. - | Module of - moduleOrNamespace: ModuleOrNamespace * + | Module of + moduleOrNamespace: ModuleOrNamespace * moduleOrNamespaceContents: ModuleOrNamespaceContents [] @@ -5851,16 +5856,16 @@ type NamedDebugPointKey = override x.GetHashCode() = hash x.Name + hash x.Range - override x.Equals(yobj: objnull) = - match yobj with + override x.Equals(yobj: objnull) = + match yobj with | :? NamedDebugPointKey as y -> equals x.Range y.Range && x.Name = y.Name | _ -> false interface IComparable with member x.CompareTo(yobj: obj) = - match yobj with - | :? NamedDebugPointKey as y -> - let c = rangeOrder.Compare(x.Range, y.Range) + match yobj with + | :? NamedDebugPointKey as y -> + let c = rangeOrder.Compare(x.Range, y.Range) if c <> 0 then c else compare x.Name y.Name | _ -> -1 @@ -5869,8 +5874,8 @@ type NamedDebugPointKey = /// /// CheckedImplFile (qualifiedNameOfFile, pragmas, signature, contents, hasExplicitEntryPoint, isScript, anonRecdTypeInfo) [] -type CheckedImplFile = - | CheckedImplFile of +type CheckedImplFile = + | CheckedImplFile of qualifiedNameOfFile: QualifiedNameOfFile * signature: ModuleOrNamespaceType * contents: ModuleOrNamespaceContents * @@ -5896,8 +5901,8 @@ type CheckedImplFile = /// Represents a complete typechecked assembly, made up of multiple implementation files. [] -type CheckedImplFileAfterOptimization = - { ImplFile: CheckedImplFile +type CheckedImplFileAfterOptimization = + { ImplFile: CheckedImplFile OptimizeDuringCodeGen: bool -> Expr -> Expr } [] @@ -5907,7 +5912,7 @@ type CheckedImplFileAfterOptimization = /// Represents a complete typechecked assembly, made up of multiple implementation files. [] -type CheckedAssemblyAfterOptimization = +type CheckedAssemblyAfterOptimization = | CheckedAssemblyAfterOptimization of CheckedImplFileAfterOptimization list [] @@ -5916,53 +5921,53 @@ type CheckedAssemblyAfterOptimization = override x.ToString() = "CheckedAssemblyAfterOptimization(...)" [] -type CcuData = +type CcuData = { - /// Holds the file name for the DLL, if any - FileName: string option - - /// Holds the data indicating how this assembly/module is referenced from the code being compiled. + /// Holds the file name for the DLL, if any + FileName: string option + + /// Holds the data indicating how this assembly/module is referenced from the code being compiled. ILScopeRef: ILScopeRef - - /// A unique stamp for this DLL + + /// A unique stamp for this DLL Stamp: Stamp - - /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations - QualifiedName: string option - - /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) - SourceCodeDirectory: string - + + /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations + QualifiedName: string option + + /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) + SourceCodeDirectory: string + /// Indicates that this DLL was compiled using the F# compiler and has F# metadata - IsFSharp: bool - + IsFSharp: bool + #if !NO_TYPEPROVIDERS /// Is the CCu an assembly injected by a type provider - IsProviderGenerated: bool + IsProviderGenerated: bool /// Triggered when the contents of the CCU are invalidated - InvalidateEvent: IEvent + InvalidateEvent: IEvent - /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality + /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality /// logic in tastops.fs - ImportProvidedType: Tainted -> TType - + ImportProvidedType: Tainted -> TType + #endif /// Indicates that this DLL uses pre-F#-4.0 quotation literals somewhere. This is used to implement a restriction on static linking mutable UsesFSharp20PlusQuotations: bool - + /// A handle to the full specification of the contents of the module contained in this ccu - // NOTE: may contain transient state during typechecking + // NOTE: may contain transient state during typechecking mutable Contents: ModuleOrNamespace - - /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality + + /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality /// logic in tastops.fs - TryGetILModuleDef: unit -> ILModuleDef option - - /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality + TryGetILModuleDef: unit -> ILModuleDef option + + /// A helper function used to link method signatures using type equality. This is effectively a forward call to the type equality /// logic in tastops.fs - MemberSignatureEquality: TType -> TType -> bool - + MemberSignatureEquality: TType -> TType -> bool + /// The table of .NET CLI type forwarders for this assembly TypeForwarders: CcuTypeForwarderTable @@ -6005,7 +6010,7 @@ type CcuTypeForwarderTable = Root : CcuTypeForwarderTree } - static member Empty : CcuTypeForwarderTable = { Root = CcuTypeForwarderTree.Empty } + static member Empty : CcuTypeForwarderTable = { Root = CcuTypeForwarderTree.Empty } member this.TryGetValue (path:string array) (item:string): Lazy option = CcuTypeForwarderTable.findInTree (ArraySegment path) item this.Root @@ -6030,7 +6035,7 @@ type CcuReference = string // ILAssemblyRef // the data structure, or it is a delayed fixup, i.e. an invalid dangling // reference that has not had an appropriate fixup applied. [] -type CcuThunk = +type CcuThunk = { /// ccu.target is null when a reference is missing in the transitive closure of static references that /// may potentially be required for the metadata of referenced DLLs. @@ -6038,36 +6043,36 @@ type CcuThunk = name: CcuReference } - /// Dereference the assembly reference - member ccu.Deref = - if isNull (box ccu.target) then + /// Dereference the assembly reference + member ccu.Deref = + if isNull (box ccu.target) then raise(UnresolvedReferenceNoRange ccu.name) ccu.target - + /// Indicates if this assembly reference is unresolved member ccu.IsUnresolvedReference = isNull (box ccu.target) /// Ensure the ccu is derefable in advance. Supply a path to attach to any resulting error message. - member ccu.EnsureDerefable(requiringPath: string[]) = - if ccu.IsUnresolvedReference then + member ccu.EnsureDerefable(requiringPath: string[]) = + if ccu.IsUnresolvedReference then let path = String.Join(".", requiringPath) raise(UnresolvedPathReferenceNoRange(ccu.name, path)) - + /// Indicates that this DLL uses F# 2.0+ quotation literals somewhere. This is used to implement a restriction on static linking. - member ccu.UsesFSharp20PlusQuotations - with get() = ccu.Deref.UsesFSharp20PlusQuotations + member ccu.UsesFSharp20PlusQuotations + with get() = ccu.Deref.UsesFSharp20PlusQuotations and set v = ccu.Deref.UsesFSharp20PlusQuotations <- v /// The short name of the assembly being referenced member ccu.AssemblyName = ccu.name - /// Holds the data indicating how this assembly/module is referenced from the code being compiled. + /// Holds the data indicating how this assembly/module is referenced from the code being compiled. member ccu.ILScopeRef = ccu.Deref.ILScopeRef /// A unique stamp for this assembly member ccu.Stamp = ccu.Deref.Stamp - /// Holds the file name for the assembly, if any + /// Holds the file name for the assembly, if any member ccu.FileName = ccu.Deref.FileName /// Try to get the .NET Assembly, if known. May not be present for `IsFSharp` for @@ -6082,17 +6087,17 @@ type CcuThunk = member ccu.ImportProvidedType ty: TType = ccu.Deref.ImportProvidedType ty #endif - /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations + /// The fully qualified assembly reference string to refer to this assembly. This is persisted in quotations member ccu.QualifiedName = ccu.Deref.QualifiedName - /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) + /// A hint as to where does the code for the CCU live (e.g what was the tcConfig.implicitIncludeDir at compilation time for this DLL?) member ccu.SourceCodeDirectory = ccu.Deref.SourceCodeDirectory /// Indicates that this DLL was compiled using the F# compiler and has F# metadata member ccu.IsFSharp = ccu.Deref.IsFSharp /// A handle to the full specification of the contents of the module contained in this ccu - // NOTE: may contain transient state during typechecking + // NOTE: may contain transient state during typechecking member ccu.Contents = ccu.Deref.Contents /// The table of type forwarders for this assembly @@ -6105,41 +6110,41 @@ type CcuThunk = member ccu.RootTypeAndExceptionDefinitions = ccu.Contents.ModuleOrNamespaceType.TypeAndExceptionDefinitions /// Create a CCU with the given name and contents - static member Create(nm, x) = - { target = x + static member Create(nm, x) = + { target = x name = nm } /// Create a CCU with the given name but where the contents have not yet been specified - static member CreateDelayed nm = - { target = Unchecked.defaultof<_> + static member CreateDelayed nm = + { target = Unchecked.defaultof<_> name = nm } /// Fixup a CCU to have the given contents - member x.Fixup(avail: CcuThunk) = + member x.Fixup(avail: CcuThunk) = match box x.target with | null -> () - | _ -> + | _ -> // In the IDE we tolerate a double-fixup of FSHarp.Core when editing the FSharp.Core project itself - if x.AssemblyName <> "FSharp.Core" then + if x.AssemblyName <> "FSharp.Core" then errorR(Failure("internal error: Fixup: the ccu thunk for assembly "+x.AssemblyName+" not delayed!")) assert (avail.AssemblyName = x.AssemblyName) - x.target <- + x.target <- match box avail.target with | null -> error(Failure("internal error: ccu thunk '"+avail.name+"' not fixed up!")) | _ -> avail.target /// Try to resolve a path into the CCU by referencing the .NET/CLI type forwarder table of the CCU - member ccu.TryForward(nlpath: string[], item: string) : EntityRef option = + member ccu.TryForward(nlpath: string[], item: string) : EntityRef option = ccu.EnsureDerefable nlpath ccu.TypeForwarders.TryGetValue nlpath item |> Option.map (fun entity -> entity.Force()) /// Used to make forward calls into the type/assembly loader when comparing member signatures during linking - member ccu.MemberSignatureEquality(ty1: TType, ty2: TType) = + member ccu.MemberSignatureEquality(ty1: TType, ty2: TType) = ccu.Deref.MemberSignatureEquality ty1 ty2 - + [] member x.DebugText = x.ToString() @@ -6189,12 +6194,12 @@ type FreeLocals = Zset /// (never cached type checking). Cached in expressions. Not pickled. type FreeTypars = Zset -/// Represents a set of 'free' named type definitions. Used to collect the named type definitions referred to +/// Represents a set of 'free' named type definitions. Used to collect the named type definitions referred to /// from a type or expression. Computed and cached by later phases (never cached type checking). Cached /// in expressions. Not pickled. type FreeTycons = Zset -/// Represents a set of 'free' record field definitions. Used to collect the record field definitions referred to +/// Represents a set of 'free' record field definitions. Used to collect the record field definitions referred to /// from an expression. type FreeRecdFields = Zset @@ -6204,17 +6209,17 @@ type FreeUnionCases = Zset /// Represents a set of 'free' type-related elements, including named types, trait solutions, union cases and /// record fields. [] -type FreeTyvars = +type FreeTyvars = { - /// The summary of locally defined type definitions used in the expression. These may be made private by a signature - /// and we have to check various conditions associated with that. + /// The summary of locally defined type definitions used in the expression. These may be made private by a signature + /// and we have to check various conditions associated with that. FreeTycons: FreeTycons /// The summary of values used as trait solutions FreeTraitSolutions: FreeLocals - - /// The summary of type parameters used in the expression. These may not escape the enclosing generic construct - /// and we have to check various conditions associated with that. + + /// The summary of type parameters used in the expression. These may not escape the enclosing generic construct + /// and we have to check various conditions associated with that. FreeTypars: FreeTypars } @@ -6228,36 +6233,36 @@ type FreeVarsCache = FreeVars cache /// Represents the set of free variables in an expression [] -type FreeVars = +type FreeVars = { - /// The summary of locally defined variables used in the expression. These may be hidden at let bindings etc. - /// or made private by a signature or marked 'internal' or 'private', and we have to check various conditions associated with that. + /// The summary of locally defined variables used in the expression. These may be hidden at let bindings etc. + /// or made private by a signature or marked 'internal' or 'private', and we have to check various conditions associated with that. FreeLocals: FreeLocals + + /// Indicates if the expression contains a call to a protected member or a base call. + /// Calls to protected members and direct calls to super classes can't escape, also code can't be inlined + UsesMethodLocalConstructs: bool - /// Indicates if the expression contains a call to a protected member or a base call. - /// Calls to protected members and direct calls to super classes can't escape, also code can't be inlined - UsesMethodLocalConstructs: bool - - /// Indicates if the expression contains a call to rethrow that is not bound under a (try-)with branch. - /// Rethrow may only occur in such locations. - UsesUnboundRethrow: bool + /// Indicates if the expression contains a call to rethrow that is not bound under a (try-)with branch. + /// Rethrow may only occur in such locations. + UsesUnboundRethrow: bool /// Indicates if the expression contains a direct IL field load/store — a cheap over-approximate /// gate the optimizer refines to protected (family) fields (issue #19963). Never read by escape checks. - ContainsILFieldAccess: bool + ContainsILFieldAccess: bool - /// The summary of locally defined tycon representations used in the expression. These may be made private by a signature - /// or marked 'internal' or 'private' and we have to check various conditions associated with that. - FreeLocalTyconReprs: FreeTycons + /// The summary of locally defined tycon representations used in the expression. These may be made private by a signature + /// or marked 'internal' or 'private' and we have to check various conditions associated with that. + FreeLocalTyconReprs: FreeTycons - /// The summary of fields used in the expression. These may be made private by a signature - /// or marked 'internal' or 'private' and we have to check various conditions associated with that. + /// The summary of fields used in the expression. These may be made private by a signature + /// or marked 'internal' or 'private' and we have to check various conditions associated with that. FreeRecdFields: FreeRecdFields - + /// The summary of union constructors used in the expression. These may be /// marked 'internal' or 'private' and we have to check various conditions associated with that. FreeUnionCases: FreeUnionCases - + /// See FreeTyvars above. FreeTyvars: FreeTyvars } @@ -6267,27 +6272,27 @@ type FreeVars = override x.ToString() = "FreeVars(...)" /// A set of static methods for constructing types. -type Construct() = - - static let taccessPublic = TAccess [] +type Construct() = + static let taccessPublic = TAccess [] + /// Key a Tycon or TyconRef by decoded name - static member KeyTyconByDecodedName<'T> (nm: string) (x: 'T) : KeyValuePair = + static member KeyTyconByDecodedName<'T> (nm: string) (x: 'T) : KeyValuePair = KeyValuePair(DecodeGenericTypeName nm, x) /// Key a Tycon or TyconRef by both mangled and demangled name. /// Generic types can be accessed either by 'List' or 'List`1'. /// This lists both keys. - static member KeyTyconByAccessNames<'T> (nm: string) (x: 'T) : KeyValuePair[] = + static member KeyTyconByAccessNames<'T> (nm: string) (x: 'T) : KeyValuePair[] = match TryDemangleGenericNameAndPos nm with | ValueSome pos -> - let dnm = DemangleGenericTypeNameWithPos pos nm + let dnm = DemangleGenericTypeNameWithPos pos nm [| KeyValuePair(nm, x); KeyValuePair(dnm, x) |] | _ -> [| KeyValuePair(nm, x) |] /// Create a new node for the contents of a module or namespace - static member NewModuleOrNamespaceType mkind tycons vals = + static member NewModuleOrNamespaceType mkind tycons vals = ModuleOrNamespaceType(mkind, QueueList.ofList vals, QueueList.ofList tycons) /// Create a new node for an empty module or namespace contents @@ -6297,37 +6302,37 @@ type Construct() = static member NewEmptyFSharpTyconData kind = { fsobjmodel_cases = Construct.MakeUnionCases [] - fsobjmodel_kind = kind + fsobjmodel_kind = kind fsobjmodel_vslots = [] fsobjmodel_rfields = Construct.MakeRecdFieldsTable [] } #if !NO_TYPEPROVIDERS /// Create a new node for the representation information for a provided type definition - static member NewProvidedTyconRepr(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m) = + static member NewProvidedTyconRepr(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m) = let isErased = st.PUntaint((fun st -> st.IsErased), m) - let lazyBaseTy = - LazyWithContext.Create - ((fun (m, objTy) -> + let lazyBaseTy = + LazyWithContext.Create + ((fun (m, objTy) -> let baseSystemTy = st.PApplyOption((fun st -> match st.BaseType with null -> None | ty -> Some ty), m) - match baseSystemTy with - | None -> objTy + match baseSystemTy with + | None -> objTy | Some t -> importProvidedType t), findOriginalException) - TProvidedTypeRepr + TProvidedTypeRepr { ResolutionEnvironment=resolutionEnvironment ProvidedType=st LazyBaseType=lazyBaseTy UnderlyingTypeOfEnum = (fun () -> importProvidedType (st.PApply((fun st -> st.GetEnumUnderlyingType()), m))) - IsDelegate = (fun () -> st.PUntaint((fun st -> - let baseType = st.BaseType - match baseType with + IsDelegate = (fun () -> st.PUntaint((fun st -> + let baseType = st.BaseType + match baseType with | Null -> false - | NonNull x -> - match x with + | NonNull x -> + match x with | x when x.IsGenericType -> false | x when x.DeclaringType <> null -> false | x -> x.FullName = "System.Delegate" || x.FullName = "System.MulticastDelegate"), m)) @@ -6341,8 +6346,8 @@ type Construct() = IsSuppressRelocate = isSuppressRelocate } /// Create a new entity node for a provided type definition - static member NewProvidedTycon(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m, ?access, ?cpath) = - let stamp = newStamp() + static member NewProvidedTycon(resolutionEnvironment, st: Tainted, importProvidedType, isSuppressRelocate, m, ?access, ?cpath) = + let stamp = newStamp() let name = st.PUntaint((fun st -> st.Name), m) let kind = let isMeasure = @@ -6352,13 +6357,13 @@ type Construct() = .PUntaintNoFailure(Operators.id) if isMeasure then TyparKind.Measure else TyparKind.Type - let access = - match access with - | Some a -> a + let access = + match access with + | Some a -> a | None -> TAccess [] - let cpath = - match cpath with - | None -> + let cpath = + match cpath with + | None -> let ilScopeRef = st.TypeProviderAssemblyRef let enclosingName = GetFSharpPathToProvidedType(st, m) CompPath(ilScopeRef, SyntaxAccess.Unknown, enclosingName |> List.map(fun id->id, ModuleOrNamespaceKind.Namespace true)) @@ -6374,7 +6379,7 @@ type Construct() = entity_attribs=WellKnownEntityAttribs.Empty // fetched on demand via est.fs API entity_typars= LazyWithContext.NotLazy [] entity_tycon_repr = repr - entity_tycon_tcaug= TyconAugmentation.Create() + entity_tycon_tcaug=TyconAugmentation.Create() entity_modul_type = MaybeLazy.Lazy(InterruptibleLazy(fun _ -> ModuleOrNamespaceType(Namespace true, QueueList.ofList [], QueueList.ofList []))) // Generated types get internal accessibility entity_cpath = Some cpath @@ -6384,13 +6389,13 @@ type Construct() = | TyparKind.Type, TAccess [] -> None | _ -> Some { Entity.NewEmptyEntityOptData() with entity_kind = kind - entity_accessibility = access } } + entity_accessibility = access } } #endif /// Create a new entity node for a module or namespace - static member NewModuleOrNamespace cpath access (id: Ident) (xml: XmlDoc) attribs mtype = - let stamp = newStamp() - // Put the module suffix on if needed + static member NewModuleOrNamespace cpath access (id: Ident) (xml: XmlDoc) attribs mtype = + let stamp = newStamp() + // Put the module suffix on if needed Tycon.New "mspec" { entity_logical_name=id.idText entity_range = id.idRange @@ -6409,20 +6414,20 @@ type Construct() = | _ -> Some { Entity.NewEmptyEntityOptData() with entity_xmldoc = xml entity_tycon_repr_accessibility = access - entity_accessibility = access } } + entity_accessibility = access } } /// Create a new unfilled cache for free variable calculations static member NewFreeVarsCache() = newCache () /// Create the field tables for a record or class type - static member MakeRecdFieldsTable ucs: TyconRecdFields = - { FieldsByIndex = Array.ofList ucs + static member MakeRecdFieldsTable ucs: TyconRecdFields = + { FieldsByIndex = Array.ofList ucs FieldsByName = ucs |> NameMap.ofKeyedList (fun rfld -> rfld.LogicalName) } /// Create the union case tables for a union type - static member MakeUnionCases ucs: TyconUnionData = - { CasesTable = - { CasesByIndex = Array.ofList ucs + static member MakeUnionCases ucs: TyconUnionData = + { CasesTable = + { CasesByIndex = Array.ofList ucs CasesByName = NameMap.ofKeyedList (fun uc -> uc.LogicalName) ucs } CompiledRepresentation=newCache() } @@ -6438,11 +6443,11 @@ type Construct() = TFSharpTyconRepr repr /// Create a new type parameter node - static member NewTypar (kind, rigid, SynTypar(id, staticReq, isCompGen), isFromError, dynamicReq, attribs, eqDep, compDep) = + static member NewTypar (kind, rigid, SynTypar(id, staticReq, isCompGen), isFromError, dynamicReq, attribs, eqDep, compDep) = Typar.New - { typar_id = id - typar_stamp = newStamp() - typar_flags= TyparFlags(kind, rigid, isFromError, isCompGen, staticReq, dynamicReq, eqDep, compDep, false) + { typar_id = id + typar_stamp = newStamp() + typar_flags= TyparFlags(kind, rigid, isFromError, isCompGen, staticReq, dynamicReq, eqDep, compDep, false) typar_solution = None typar_astype = Unchecked.defaultof<_> typar_opt_data = @@ -6455,7 +6460,7 @@ type Construct() = Construct.NewTypar (TyparKind.Type, TyparRigidity.Rigid, SynTypar(mkSynId m nm, TyparStaticReq.None, true), false, TyparDynamicReq.Yes, [], false, false) /// Create a new union case node - static member NewUnionCase id tys retTy attribs docOption access: UnionCase = + static member NewUnionCase id tys retTy attribs docOption access: UnionCase = { Id = id OwnXmlDoc = docOption OtherXmlDoc = XmlDoc.Empty @@ -6463,11 +6468,11 @@ type Construct() = Accessibility = access FieldTable = Construct.MakeRecdFieldsTable tys ReturnType = retTy - Attribs = attribs - OtherRangeOpt = None } + Attribs = attribs + OtherRangeOpt = None } /// Create a new TAST Entity node for an F# exception definition - static member NewExn cpath (id: Ident) access repr attribs (doc: XmlDoc) = + static member NewExn cpath (id: Ident) access repr attribs (doc: XmlDoc) = Tycon.New "exnc" { entity_stamp = newStamp() entity_attribs = WellKnownEntityAttribs.Create(attribs) @@ -6483,7 +6488,7 @@ type Construct() = entity_opt_data = match doc, access, repr with | doc, TAccess [], TExnNone when doc.IsEmpty -> None - | _ -> Some { Entity.NewEmptyEntityOptData() with entity_xmldoc = doc; entity_accessibility = access; entity_tycon_repr_accessibility = access; entity_exn_info = repr } } + | _ -> Some { Entity.NewEmptyEntityOptData() with entity_xmldoc = doc; entity_accessibility = access; entity_tycon_repr_accessibility = access; entity_exn_info = repr } } /// Create a new TAST RecdField node for an F# class, struct or record field static member NewRecdField stat konst id nameGenerated ty isMutable isVolatile pattribs fattribs docOption access secret = @@ -6502,10 +6507,10 @@ type Construct() = rfield_id = id rfield_name_generated = nameGenerated rfield_other_range = None } - + /// Create a new type definition node static member NewTycon (cpath, nm, m, access, reprAccess, kind, typars, doc: XmlDoc, usesPrefixDisplay, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, mtyp) = - let stamp = newStamp() + let stamp = newStamp() Tycon.New "tycon" { entity_stamp=stamp entity_logical_name=nm @@ -6514,14 +6519,14 @@ type Construct() = entity_attribs=WellKnownEntityAttribs.Empty // fixed up after entity_typars=typars entity_tycon_repr = TNoRepr - entity_tycon_tcaug= TyconAugmentation.Create() + entity_tycon_tcaug=TyconAugmentation.Create() entity_modul_type = mtyp entity_cpath = cpath entity_il_repr_cache = null entity_opt_data = match kind, doc, reprAccess, access with | TyparKind.Type, doc, TAccess [], TAccess [] when doc.IsEmpty -> None - | _ -> Some { Entity.NewEmptyEntityOptData() with entity_kind = kind; entity_xmldoc = doc; entity_tycon_repr_accessibility = reprAccess; entity_accessibility=access } } + | _ -> Some { Entity.NewEmptyEntityOptData() with entity_kind = kind; entity_xmldoc = doc; entity_tycon_repr_accessibility = reprAccess; entity_accessibility=access } } /// Create a new type definition node for a .NET type definition static member NewILTycon nlpath (nm, m) tps (scoref: ILScopeRef, enc, tdef: ILTypeDef) mtyp = @@ -6557,10 +6562,10 @@ type Construct() = actualParent) : Val = let stamp = newStamp() - let optData = + let optData = match compiledName, arity, konst, access, doc, specialRepr, actualParent, attribs with | None, None, None, TAccess [], doc, None, ParentNone, [] when doc.IsEmpty -> None - | _ -> + | _ -> { Val.NewEmptyValOptData() with val_compiled_name = (match compiledName with Some v when v <> logicalName -> compiledName | _ -> None) val_repr_info = arity @@ -6587,28 +6592,28 @@ type Construct() = static member NewCcuContents sref m nm mty = Construct.NewModuleOrNamespace (Some(CompPath(sref, SyntaxAccess.Unknown, []))) taccessPublic (ident(nm, m)) XmlDoc.Empty [] (MaybeLazy.Strict mty) - /// Create a tycon based on an existing one using the function 'f'. - /// We require that we be given the new parent for the new tycon. - /// We pass the new tycon to 'f' in case it needs to reparent the - /// contents of the tycon. - static member NewModifiedTycon f (orig: Tycon) = + /// Create a tycon based on an existing one using the function 'f'. + /// We require that we be given the new parent for the new tycon. + /// We pass the new tycon to 'f' in case it needs to reparent the + /// contents of the tycon. + static member NewModifiedTycon f (orig: Tycon) = let data = { orig with entity_stamp = newStamp() } - Tycon.New "NewModifiedTycon" (f data) - - /// Create a module Tycon based on an existing one using the function 'f'. - /// We require that we be given the parent for the new module. - /// We pass the new module to 'f' in case it needs to reparent the - /// contents of the module. - static member NewModifiedModuleOrNamespace f orig = + Tycon.New "NewModifiedTycon" (f data) + + /// Create a module Tycon based on an existing one using the function 'f'. + /// We require that we be given the parent for the new module. + /// We pass the new module to 'f' in case it needs to reparent the + /// contents of the module. + static member NewModifiedModuleOrNamespace f orig = orig |> Construct.NewModifiedTycon (fun d -> match d.entity_modul_type with | null -> d - | entity_modul_type -> { d with entity_modul_type = MaybeLazy.Strict (f (entity_modul_type.Force())) }) + | modulType -> { d with entity_modul_type = MaybeLazy.Strict (f (modulType.Force())) }) - /// Create a Val based on an existing one using the function 'f'. - /// We require that we be given the parent for the new Val. - static member NewModifiedVal f (orig: Val) = - let stamp = newStamp() + /// Create a Val based on an existing one using the function 'f'. + /// We require that we be given the parent for the new Val. + static member NewModifiedVal f (orig: Val) = + let stamp = newStamp() let data' = f { orig with val_stamp=stamp } Val.New data' @@ -6626,9 +6631,9 @@ type Construct() = let attrs = p.PUntaintNoFailure(fun x -> x.GetDefinitionLocationAttribute(p.TypeProvider.PUntaintNoFailure id)) match attrs with | None | Some (Null, _, _) -> None - | Some (NonNull filePath, line, column) -> + | Some (NonNull filePath, line, column) -> // Coordinates from type provider are 1-based for lines and columns // Coordinates internally in the F# compiler are 1-based for lines and 0-based for columns - let pos = Position.mkPos line (max 0 (column - 1)) + let pos = Position.mkPos line (max 0 (column - 1)) mkRange !!filePath pos pos |> Some #endif diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index 92e12c1cb1e..6cbd3ea7f43 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -472,6 +472,10 @@ type Entity = static member NewEmptyEntityOptData: unit -> EntityOptionalData + /// Stands in for the contents of an entity that was never linked, so that a broken mid-edit + /// file still classifies instead of crashing. Never reachable from a successfully checked entity. + static member EmptyModuleOrNamespaceType: ModuleOrNamespaceType + /// Create a new entity with empty, unlinked data. Only used during unpickling of F# metadata. static member NewUnlinked: unit -> Entity @@ -918,6 +922,10 @@ type TyconAugmentation = static member Create: unit -> TyconAugmentation + /// Stands in for the augmentation of an entity that was never linked, so that a broken mid-edit + /// file still classifies instead of crashing. Never reachable from a successfully checked entity. + static member Empty: TyconAugmentation + /// Record a member in declaration order, allocating the list on first use member AddAdhocMember: isExplicitImpl: bool * vref: ValRef -> unit diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index f1f2f0f7927..46a84559405 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -733,6 +733,8 @@ let private p_lazy_impl p v st = let p_lazy p x st = p_lazy_impl p (InterruptibleLazy.force x) st +let p_maybe_lazy p (x: MaybeLazy<_>) st = p_lazy_impl p x.Value st + let p_hole () = let mutable h = None @@ -2721,7 +2723,7 @@ let rec p_tycon_repr x st = let allFieldsText = fields - |> Seq.map _.LogicalName + |> Array.map (fun f -> f.LogicalName) |> String.concat System.Environment.NewLine raise (Error(FSComp.SR.pickleFsharpCoreBackwardsCompatible ("fields in union", allFieldsText), firstFieldRange)) @@ -2838,7 +2840,7 @@ and p_entity_spec_data (x: Entity) st = p_attribs (x.entity_attribs.AsList()) st let flagBit = p_tycon_repr x.entity_tycon_repr st p_option p_ty x.TypeAbbrev st - p_tcaug x.TypeContents st + p_tcaug (nonNull x.entity_tycon_tcaug) st p_string System.String.Empty st p_kind x.TypeOrMeasureKind st @@ -2851,7 +2853,7 @@ and p_entity_spec_data (x: Entity) st = st p_option p_cpath x.entity_cpath st - p_lazy_impl p_modul_typ x.ModuleOrNamespaceType st + p_maybe_lazy p_modul_typ (nonNull x.entity_modul_type) st p_exnc_repr x.ExceptionInfo st if st.oInMem then @@ -2859,7 +2861,7 @@ and p_entity_spec_data (x: Entity) st = else p_space 1 () st -and p_tcaug (p : TyconAugmentation) st = +and p_tcaug p st = p_tup9 (p_option (p_tup2 (p_vref "compare_obj") (p_vref "compare"))) (p_option (p_vref "compare_withc")) @@ -2880,9 +2882,8 @@ and p_tcaug (p : TyconAugmentation) st = // in order to get check the well-formedness of an interface. // Keeping them across assembly boundaries is not valid, because relinking their ValRefs // does not work correctly (they may get incorrectly relinked to a default member) - |> Seq.filter (fun (isExplicitImpl, _) -> not isExplicitImpl) - |> Seq.map (fun (_, vref) -> vref.LogicalName, vref) - |> Seq.toList), + |> List.filter (fun (isExplicitImpl, _) -> not isExplicitImpl) + |> List.map (fun (_, vref) -> vref.LogicalName, vref)), p.tcaug_interfaces, p.tcaug_super, p.tcaug_abstract, diff --git a/src/Compiler/Utilities/lib.fs b/src/Compiler/Utilities/lib.fs index a344137a34b..a1395e5589e 100755 --- a/src/Compiler/Utilities/lib.fs +++ b/src/Compiler/Utilities/lib.fs @@ -419,7 +419,7 @@ type DisposablesTracker() = let items = Stack() /// Register some items to dispose - member _.Register (i:#IDisposable | null) = + member _.Register (i:#IDisposable | null) = match box i with | null -> () | _ -> items.Push (!!i) @@ -498,11 +498,6 @@ module WeakMap = | true, value -> value | false, _ -> let value = valueFactory key - if shouldCache value then - // TODO: FCS ships netstandard2.0 only right now; switch to table.TryAdd once a newer TFM ships. - try - table.Add(key, value) - with :? ArgumentException -> - () - + if shouldCache value then + try table.Add(key, value) with _ -> () value diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index 39f9165e8f5..4cbc017c3be 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -66,6 +66,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs b/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs new file mode 100644 index 00000000000..f164a55a542 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs @@ -0,0 +1,33 @@ +module FSharp.Compiler.Service.Tests.UnlinkedEntityTests + +open Xunit + +open FSharp.Compiler.TypedTree + +/// (#20269) An entity whose data was never linked has no contents. The IDE reaches these while a +/// file is mid-edit, so reading the contents must yield empty stand-ins rather than throwing a +/// NullReferenceException that takes down classification for the whole file. +[] +let ``Unlinked entity exposes empty contents instead of throwing`` () = + let entity = Entity.NewUnlinked() + + Assert.Empty(entity.ModuleOrNamespaceType.AllEntities) + Assert.Empty(entity.ModuleOrNamespaceType.AllValsAndMembers) + Assert.Empty(entity.TypeContents.tcaug_interfaces) + +/// Reading the contents of an unlinked entity must not fill them in: the getters are hit from +/// parallel checking, and writing shared typed-tree state from a getter is a race. +[] +let ``Reading the contents of an unlinked entity does not link them in`` () = + let entity = Entity.NewUnlinked() + + entity.ModuleOrNamespaceType |> ignore + entity.TypeContents |> ignore + + match entity.entity_modul_type with + | null -> () + | _ -> failwith "reading ModuleOrNamespaceType must not write the entity's contents" + + match entity.entity_tycon_tcaug with + | null -> () + | _ -> failwith "reading TypeContents must not write the entity's augmentation" From 190fef654183db634106a3d96917530cce7bdada Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:19:41 +0200 Subject: [PATCH 053/133] Drop the per-project PE reference cache `emitCache` required a reference-identical `Compilation` to hit, so it could only ever hit where `weakPEReferences` already did - saving no re-emit - while strongly holding the newest `Compilation` per project and keeping the weak table from ever collecting its key. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 9fdbdadb87d057ff11403492445b1cd28eae5ff3) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 - .../FSharpProjectOptionsManager.fs | 58 +++---------------- 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index d8afa99ffb0..42001f08fef 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -12,7 +12,6 @@ * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Avoid using `cancellableTask` in `DocumentCache`; the editor cache now uses direct `CancellationToken`-aware `task` wrappers, avoiding the background `Task.Run` offload and a larger wrapper closure from the `cancellableTask` builder. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) * Prevent stale and leaked per-document diagnostics by tightening cache validity and evicting entries for removed documents. ([PR #20121](https://github.com/dotnet/fsharp/pull/20121)) -* Reduce excessive metadata-only `Compilation.Emit` churn for C# project references in the IDE. ([Issue #20118](https://github.com/dotnet/fsharp/issues/20118), [PR #20119](https://github.com/dotnet/fsharp/pull/20119)) * Avoid blocking the VS project-options mailbox and unnecessary script option recomputation while editing by making invalidation async and debouncing caret-driven updates. ([Issue #20124](https://github.com/dotnet/fsharp/issues/20124), [Issue #20125](https://github.com/dotnet/fsharp/issues/20125), [PR #20126](https://github.com/dotnet/fsharp/pull/20126)) * Cache document diagnostics by version stamp, so an unchanged document is not reanalyzed on every crawler pass. ([Issue #20120](https://github.com/dotnet/fsharp/issues/20120), [PR #20121](https://github.com/dotnet/fsharp/pull/20121)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index afc5c9dd6e0..88d59330129 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -126,13 +126,6 @@ type SingleFileCacheEntry = Subscription: ConnectionPointSubscription } -type PEReferenceCacheEntry = - { - Stamp: VersionStamp - Compilation: Compilation - Reference: FSharpReferencedProject - } - [] type private FSharpProjectOptionsMessage = | TryGetOptionsByDocument of @@ -162,7 +155,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let singleFileCache = ConcurrentDictionary() - let emitCache = ConcurrentDictionary() let weakPEReferences = ConditionalWeakTable() let lastSuccessfulCompilations = ConcurrentDictionary() @@ -171,27 +163,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let disposeSingleFileCacheEntry ({ Subscription = subscription }: SingleFileCacheEntry) = subscription |> ValueOption.iter (fun subscription -> subscription.Dispose()) - let tryGetCachedPEReference projectId stamp comp = - match emitCache.TryGetValue(projectId) with - | true, - { - Stamp = cachedStamp - Compilation = cachedCompilation - Reference = fsRefProj - } when cachedStamp = stamp && obj.ReferenceEquals(cachedCompilation, comp) -> ValueSome fsRefProj - | _ -> ValueNone - - let cachePEReference projectId stamp comp fsRefProj = - emitCache.[projectId] <- - { - Stamp = stamp - Compilation = comp - Reference = fsRefProj - } - - fsRefProj - - let createNewPEReference projectId (referencedProject: Project) (comp: Compilation) = + let createNewPEReference (referencedProject: Project) (comp: Compilation) = + let projectId = referencedProject.Id let mutable strongComp = comp let weakComp = WeakReference(comp) let mutable stampTime = DateTime.UtcNow @@ -242,21 +215,12 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) let createPEReference (referencedProject: Project) (comp: Compilation) = - cancellableTask { - let! ct = CancellableTask.getCancellationToken () - let projectId = referencedProject.Id - let! stamp = referencedProject.GetDependentVersionAsync(ct) - - match tryGetCachedPEReference projectId stamp comp with - | ValueSome fsRefProj -> return fsRefProj - | ValueNone -> - match weakPEReferences.TryGetValue comp with - | true, fsRefProj -> return cachePEReference projectId stamp comp fsRefProj - | _ -> - let fsRefProj = createNewPEReference projectId referencedProject comp - weakPEReferences.Add(comp, fsRefProj) - return cachePEReference projectId stamp comp fsRefProj - } + match weakPEReferences.TryGetValue comp with + | true, fsRefProj -> fsRefProj + | _ -> + let fsRefProj = createNewPEReference referencedProject comp + weakPEReferences.Add(comp, fsRefProj) + fsRefProj let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = cancellableTask { @@ -476,7 +440,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = ) elif referencedProject.SupportsCompilation then let! comp = referencedProject.GetCompilationAsync(ct) - let! peRef = createPEReference referencedProject comp + let peRef = createPEReference referencedProject comp referencedProjects.Add(peRef) if canBail then @@ -549,7 +513,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = for pair in lastSuccessfulCompilations.ToArray() do if not (currentSolution.ContainsProject(pair.Key)) then lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore - emitCache.TryRemove(pair.Key) |> ignore checker.InvalidateConfiguration(projectOptions, userOpName = "tryComputeOptions") @@ -640,7 +603,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match cache.TryRemove(projectId) with | true, struct (_, _, projectOptions) -> lastSuccessfulCompilations.TryRemove(projectId) |> ignore - emitCache.TryRemove(projectId) |> ignore checker.ClearCache([ projectOptions ]) | _ -> () @@ -649,7 +611,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match singleFileCache.TryRemove(documentId) with | true, ({ ProjectOptions = projectOptions } as cacheEntry) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore - emitCache.TryRemove(documentId.ProjectId) |> ignore checker.ClearCache([ projectOptions ]) disposeSingleFileCacheEntry cacheEntry | _ -> () @@ -694,7 +655,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | _ -> () lastSuccessfulCompilations.Clear() - emitCache.Clear() member _.ScriptUpdated = scriptUpdatedEvent.Publish From b9ff74683eb12335b45797f4abe652935297aaa9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 22:10:35 +0200 Subject: [PATCH 054/133] Use and! for parallel async version checks Execute p1.GetDependentVersionAsync and p2.GetDependentVersionAsync in parallel instead of sequentially. Co-Authored-By: Claude Haiku 4.5 (20251001) (cherry picked from commit 862aa1fa1535076524109e258ce686e54b882cbe) --- .../LanguageService/FSharpProjectOptionsManager.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 88d59330129..b8c0b29a034 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -98,7 +98,7 @@ module private FSharpProjectOptionsHelpers = result <- p1.Version <> p2.Version else let! v1 = p1.GetDependentVersionAsync(ct) - let! v2 = p2.GetDependentVersionAsync(ct) + and! v2 = p2.GetDependentVersionAsync(ct) result <- v1 <> v2 return result From 5e4fde8e2c608712a1a6c8e1e9690a0c64c94a93 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 19:24:39 +0200 Subject: [PATCH 055/133] Answer with a voption when F# project options are unavailable Every entry point in WorkspaceExtensions signalled "the project has no options yet" by raising an OperationCanceledException, which the callers then had to tell apart from a real cancellation. They cannot, so they treat both as "cancelled" and return an empty result. For a service Roslyn asks repeatedly while a solution loads, that is a wrong answer, not a missing one. Add `Try` siblings that return ValueNone instead, and keep the raising members as thin wrappers so the call sites that have not moved over still get the same exception with the same message. The four-tuple those members hand out becomes a named record, which is also what ProjectCache stores, so a cache hit hands back the instance it holds rather than rebuilding a tuple. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 2f5c8c15c5798f148137cabb3f09548981ee8e27) --- .../LanguageService/WorkspaceExtensions.fs | 137 +++++++++++++----- 1 file changed, 104 insertions(+), 33 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 2406f3a6e32..ffcf870ec34 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -24,14 +24,22 @@ open System.Text.Json.Nodes #nowarn "57" // Experimental stuff +/// Everything the checker needs for one Roslyn project, resolved once and cached per project. +type internal FSharpCompilationOptions = + { + Checker: FSharpChecker + OptionsManager: FSharpProjectOptionsManager + ParsingOptions: FSharpParsingOptions + ProjectOptions: FSharpProjectOptions + } + [] module internal ProjectCache = /// This is a cache to maintain FSharpParsingOptions and FSharpProjectOptions per Roslyn Project. /// The Roslyn Project is held weakly meaning when it is cleaned up by the GC, the FSharParsingOptions and FSharpProjectOptions will be cleaned up by the GC. /// At some point, this will be the main caching mechanism for FCS projects instead of FCS itself. - let Projects = - ConditionalWeakTable() + let Projects = ConditionalWeakTable() module internal SolutionConfigCache = @@ -170,12 +178,12 @@ module private CheckerExtensions = let exist xs = xs |> Seq.isEmpty |> not - let getFSharpOptionsForProject (this: Project) = + let tryGetFSharpOptionsForProject (this: Project) : CancellableTask = if not this.IsFSharp then - raise (OperationCanceledException("Project is not a FSharp project.")) + CancellableTask.singleton ValueNone else match ProjectCache.Projects.TryGetValue(this) with - | true, result -> CancellableTask.singleton result + | true, result -> CancellableTask.singleton (ValueSome result) | _ -> cancellableTask { @@ -185,14 +193,32 @@ module private CheckerExtensions = let projectOptionsManager = service.FSharpProjectOptionsManager match! projectOptionsManager.TryGetOptionsByProject(this, ct) with - | ValueNone -> return raise (OperationCanceledException("FSharp project options not found.")) + | ValueNone -> return ValueNone | ValueSome(parsingOptions, projectOptions) -> let result = - (service.Checker, projectOptionsManager, parsingOptions, projectOptions) + { + Checker = service.Checker + OptionsManager = projectOptionsManager + ParsingOptions = parsingOptions + ProjectOptions = projectOptions + } - return ProjectCache.Projects.GetValue(this, ConditionalWeakTable<_, _>.CreateValueCallback(fun _ -> result)) + return + ValueSome(ProjectCache.Projects.GetValue(this, ConditionalWeakTable<_, _>.CreateValueCallback(fun _ -> result))) } + // The raising members predate the `Try` ones; they keep their tuple shape until every caller has + // moved over, so a missing result stays an OperationCanceledException with the same message there. + let getFSharpOptionsForProject (this: Project) = + if not this.IsFSharp then + raise (OperationCanceledException("Project is not a FSharp project.")) + else + cancellableTask { + match! tryGetFSharpOptionsForProject this with + | ValueSome options -> return options.Checker, options.OptionsManager, options.ParsingOptions, options.ProjectOptions + | ValueNone -> return raise (OperationCanceledException("FSharp project options not found.")) + } + let documentToSnapshot (document: Document) = cancellableTask { let! version = document.GetTextVersionAsync() @@ -512,13 +538,14 @@ module private CheckerExtensions = type Document with - /// Get the FSharpParsingOptions and FSharpProjectOptions from the F# project that is associated with the given F# document. - member this.GetFSharpCompilationOptionsAsync(userOpName) = + /// Get the compilation options of the F# project that is associated with the given F# document, + /// or ValueNone while the project has none yet (still loading, reloading, a miscellaneous file). + member this.TryGetFSharpCompilationOptionsAsync(userOpName) : CancellableTask = if not this.Project.IsFSharp then - raise (OperationCanceledException("Document is not a FSharp document.")) + CancellableTask.singleton ValueNone else match ProjectCache.Projects.TryGetValue(this.Project) with - | true, result -> CancellableTask.singleton result + | true, result -> CancellableTask.singleton (ValueSome result) | _ -> cancellableTask { let service = this.Project.Solution.GetFSharpWorkspaceService() @@ -526,14 +553,36 @@ type Document with let! ct = CancellableTask.getCancellationToken () match! projectOptionsManager.TryGetOptionsForDocumentOrProject(this, ct, userOpName) with - | ValueNone -> return raise (OperationCanceledException("FSharp project options not found.")) + | ValueNone -> return ValueNone | ValueSome(parsingOptions, projectOptions) -> let result = - (service.Checker, projectOptionsManager, parsingOptions, projectOptions) + { + Checker = service.Checker + OptionsManager = projectOptionsManager + ParsingOptions = parsingOptions + ProjectOptions = projectOptions + } - return ProjectCache.Projects.GetValue(this.Project, ConditionalWeakTable<_, _>.CreateValueCallback(fun _ -> result)) + return + ValueSome( + ProjectCache.Projects.GetValue( + this.Project, + ConditionalWeakTable<_, _>.CreateValueCallback(fun _ -> result) + ) + ) } + /// Get the FSharpParsingOptions and FSharpProjectOptions from the F# project that is associated with the given F# document. + member this.GetFSharpCompilationOptionsAsync(userOpName) = + if not this.Project.IsFSharp then + raise (OperationCanceledException("Document is not a FSharp document.")) + else + cancellableTask { + match! this.TryGetFSharpCompilationOptionsAsync(userOpName) with + | ValueSome options -> return options.Checker, options.OptionsManager, options.ParsingOptions, options.ProjectOptions + | ValueNone -> return raise (OperationCanceledException("FSharp project options not found.")) + } + /// Get the compilation defines and language version from F# project that is associated with the given F# document. member this.GetFsharpParsingOptionsAsync(userOpName) = async { @@ -581,33 +630,55 @@ type Document with return! checker.ParseDocument(this, parsingOptions, userOpName) } + /// Parses and checks the given F# document; ValueNone while its project has no compilation options + /// or the check was aborted. + member this.TryGetFSharpParseAndCheckResultsAsync + (userOpName) + : CancellableTask = + cancellableTask { + match! this.TryGetFSharpCompilationOptionsAsync(userOpName) with + | ValueNone -> return ValueNone + | ValueSome options -> + match! options.Checker.ParseAndCheckDocument(this, options.ProjectOptions, userOpName, allowStaleResults = false) with + | Some(parseResults, checkResults) -> return ValueSome(struct (parseResults, checkResults)) + | None -> return ValueNone + } + /// Parses and checks the given F# document. member this.GetFSharpParseAndCheckResultsAsync(userOpName) = cancellableTask { - let! checker, _, _, projectOptions = this.GetFSharpCompilationOptionsAsync(userOpName) + match! this.TryGetFSharpParseAndCheckResultsAsync(userOpName) with + | ValueSome(struct (parseResults, checkResults)) -> return parseResults, checkResults + | ValueNone -> return raise (OperationCanceledException("Unable to get FSharp parse and check results.")) + } - match! checker.ParseAndCheckDocument(this, projectOptions, userOpName, allowStaleResults = false) with - | Some results -> return results - | _ -> return raise (OperationCanceledException("Unable to get FSharp parse and check results.")) + /// Get the semantic classifications of the given F# document; ValueNone while its project has no + /// compilation options or the background check produced none. + member this.TryGetFSharpSemanticClassificationAsync + (userOpName) + : CancellableTask = + cancellableTask { + match! this.TryGetFSharpCompilationOptionsAsync(userOpName) with + | ValueNone -> return ValueNone + | ValueSome options -> + let! result = + if this.Project.UseTransparentCompiler then + async { + let! projectSnapshot = getProjectSnapshotForDocument (this, options.ProjectOptions) + return! options.Checker.GetBackgroundSemanticClassificationForFile(this.FilePath, projectSnapshot) + } + else + options.Checker.GetBackgroundSemanticClassificationForFile(this.FilePath, options.ProjectOptions) + + return ValueOption.ofOption result } /// Get the semantic classifications of the given F# document. member this.GetFSharpSemanticClassificationAsync(userOpName) = cancellableTask { - let! checker, _, _, projectOptions = this.GetFSharpCompilationOptionsAsync(userOpName) - - let! result = - if this.Project.UseTransparentCompiler then - async { - let! projectSnapshot = getProjectSnapshotForDocument (this, projectOptions) - return! checker.GetBackgroundSemanticClassificationForFile(this.FilePath, projectSnapshot) - } - else - checker.GetBackgroundSemanticClassificationForFile(this.FilePath, projectOptions) - - return - result - |> Option.defaultWith (fun _ -> raise (OperationCanceledException("Unable to get FSharp semantic classification."))) + match! this.TryGetFSharpSemanticClassificationAsync(userOpName) with + | ValueSome classification -> return classification + | ValueNone -> return raise (OperationCanceledException("Unable to get FSharp semantic classification.")) } /// Find F# references in the given F# document. From ef05c3814fb8781dd83c3052c9d4ae16d1dd69ed Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 19:24:40 +0200 Subject: [PATCH 056/133] Cache the whole-file classification of an open document Splitting the semantic classification cache in two (#15954) left the open-document branch reading the opened-documents cache and writing the unopened one, so the opened cache was never populated and every request for an open file re-ran the checker. What it wrote was also only the requested span, keyed by document and text version, so a second request for another span of the same version (scrolling, a split view, Roslyn asking around the viewport) would have hit that entry and sliced nothing out of it. Cache the classification of the whole file instead, and slice it per request, the way the unopened-documents branch already does. A check that cannot complete - the project is loading or reloading, or the check was superseded - must not answer with no classifications either: Roslyn replaces the tags of a span with whatever comes back, so an empty answer strips the colours the user is looking at, while a cancellation carrying Roslyn's own token leaves them alone. Keep the last classification per document and re-emit it for those requests, but only while the text it was computed from is still the current one, as its spans would otherwise land on the wrong characters. `ifCanceledThen` replaces `ifCanceledReturn ()` for the same reason: the checker relabels its internal cancellations with the caller's token, and those must produce the last known colours rather than none. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 6e5d1f969648c16ddd764191d5e6584bf8d4e4e3) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../Classification/ClassificationService.fs | 88 ++++++++-- .../FSharp.Editor/Common/CancellableTasks.fs | 11 ++ .../SemanticClassificationServiceTests.fs | 163 ++++++++++++++++++ 4 files changed, 244 insertions(+), 19 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 42001f08fef..480fe804587 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -21,6 +21,7 @@ * Fix `NotSupportedException` in the memory-mapped-file optimization when copying `ReadOnlyMemory` into `MemoryMappedFileViewStream`. ([Issue #20263](https://github.com/dotnet/fsharp/issues/20263)) * Reduce allocations in the VS project options reactor: the command-line options and project options caches and the mailbox reply payloads now hold struct tuples, and `IProjectSite.CompilationBinOutputPath` returns `string voption` picked with a new `Array.tryPickV`. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413)) * Fix `NullReferenceException` that broke syntax coloring for the rest of the file when an unlinked placeholder entity was encountered while editing broken code. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) +* Fix semantic colours of open documents disappearing: their classification was stored in the unopened-documents cache and only covered the requested span, so scrolling re-ran the checker or got nothing back, and a check that could not complete answered with no classifications, which clears the colours already shown. ([Issue #20445](https://github.com/dotnet/fsharp/issues/20445), [PR #20450](https://github.com/dotnet/fsharp/pull/20450)) ### Changed diff --git a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs index 738003d5e3a..c7dc7738587 100644 --- a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs +++ b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs @@ -8,6 +8,7 @@ open System.Collections.Generic open System.Collections.Immutable open System.Threading open System.Runtime.Caching +open System.Runtime.CompilerServices open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Classification @@ -30,6 +31,12 @@ open Microsoft.VisualStudio.FSharp.Editor.Telemetry type SemanticClassificationData = SemanticClassificationView type SemanticClassificationLookup = IReadOnlyDictionary> +type internal LastGoodSemanticClassification = + { + Text: SourceText + Lookup: SemanticClassificationLookup + } + [)>] type internal FSharpClassificationService [] () = @@ -149,6 +156,41 @@ type internal FSharpClassificationService [] () = static let openedDocumentsSemanticClassificationCache = new DocumentCache("fsharp-opened-documents-semantic-classification-cache", 2.) + // Roslyn replaces a span's semantic tags with whatever this service returns, so answering "nothing" + // while the checker is unavailable (project loading or reloading, a superseded check) strips the + // colours the user already sees. Keep the last whole-file lookup per document, outliving the + // version-keyed cache above, and re-emit it for those requests. + static let lastGoodSemanticClassification = + ConditionalWeakTable() + + static let rememberLastGood (documentId: DocumentId) (text: SourceText) (lookup: SemanticClassificationLookup) = + let lastGood = { Text = text; Lookup = lookup } + // net472 has no ConditionalWeakTable.AddOrUpdate. + lock lastGoodSemanticClassification (fun () -> + lastGoodSemanticClassification.Remove documentId |> ignore + lastGoodSemanticClassification.Add(documentId, lastGood)) + + // Only for the text it was computed from: the lookup names positions, so against edited text it + // would colour the wrong characters. + static let addLastGood (documentId: DocumentId) (sourceText: SourceText) (targetSpan: TextSpan) (result: List) = + match lastGoodSemanticClassification.TryGetValue documentId with + | true, lastGood when lastGood.Text.ContentEquals sourceText -> + addSemanticClassificationByLookup sourceText targetSpan lastGood.Lookup result + | _ -> () + + static let addLastGoodForCurrentText (document: Document) (targetSpan: TextSpan) (result: List) = + match document.TryGetText() with + | true, sourceText -> addLastGood document.Id sourceText targetSpan result + | _ -> () + + // Which of the two caches a document lands in is not observable from its classifications - a miss + // only costs a recheck - so tests reach the caches directly to tell the branches apart. + static member internal OpenedDocumentsSemanticClassificationCache = + openedDocumentsSemanticClassificationCache + + static member internal UnopenedDocumentsSemanticClassificationCache = + unopenedDocumentsSemanticClassificationCache + interface IFSharpClassificationService with // Do not perform classification if we don't have project options (#defines matter) member _.AddLexicalClassifications(_: SourceText, _: TextSpan, _: List, _: CancellationToken) = () @@ -252,11 +294,12 @@ type internal FSharpClassificationService [] () = use _eventDuration = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - let! classificationData = document.GetFSharpSemanticClassificationAsync(nameof (FSharpClassificationService)) - - let classificationDataLookup = toSemanticClassificationLookup classificationData - do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) - addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result + match! document.TryGetFSharpSemanticClassificationAsync(nameof (FSharpClassificationService)) with + | ValueNone -> () + | ValueSome classificationData -> + let classificationDataLookup = toSemanticClassificationLookup classificationData + do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) + addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result else match! openedDocumentsSemanticClassificationCache.TryGetValueAsync document with @@ -288,21 +331,28 @@ type internal FSharpClassificationService [] () = use _eventDuration = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) - - let targetRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) - - let classificationData = - checkResults.GetSemanticClassification(Some targetRange, RelatedSymbolUseKind.All) - - if classificationData.Length > 0 then - let classificationDataLookup = itemToSemanticClassificationLookup classificationData - do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) - - addSemanticClassification sourceText textSpan classificationData result + match! document.TryGetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) with + | ValueNone -> addLastGood document.Id sourceText textSpan result + | ValueSome(struct (_, checkResults)) -> + // The cache is keyed by text version only, so it has to hold the whole file: + // the next request at this version is usually for a different span. + let classificationData = + checkResults.GetSemanticClassification(None, RelatedSymbolUseKind.All) + + // Every checked file resolves at least its enclosing module, so nothing here means + // the classification itself failed (SemanticClassification.fs recovers with an empty + // array). Caching that would pin the version to no colours. + if classificationData.Length = 0 then + addLastGood document.Id sourceText textSpan result + else + let classificationDataLookup = itemToSemanticClassificationLookup classificationData + do! openedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) + rememberLastGood document.Id sourceText classificationDataLookup + addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result } - |> CancellableTask.ifCanceledReturn () + // A cancellation that is not Roslyn's own (a superseded or aborted check surfaces as one) + // must not turn into an empty answer, which Roslyn would paint as "no colours". + |> CancellableTask.ifCanceledThen (fun () -> addLastGoodForCurrentText document textSpan result) |> CancellableTask.startAsTask cancellationToken // Do not perform classification if we don't have project options (#defines matter) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 7520395a084..29e54491a86 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -1160,6 +1160,17 @@ module CancellableTasks = return value } + /// If this CancellableTask gets canceled for another reason than the token being canceled, run the fallback instead. + let inline ifCanceledThen ([] fallback: unit -> unit) (ctask: CancellableTask) = + cancellableTask { + let! ct = getCancellationToken () + + try + return! ctask + with :? OperationCanceledException when ct.IsCancellationRequested = false -> + return fallback () + } + /// [] module MergeSourcesExtensions = diff --git a/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs index 6c58f658ee9..cde75f71e4c 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs @@ -2,12 +2,16 @@ namespace FSharp.Editor.Tests +open System +open System.Threading open Xunit +open Microsoft.CodeAnalysis open Microsoft.VisualStudio.FSharp.Editor open FSharp.Compiler.EditorServices open FSharp.Compiler.Text open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.Classification +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Classification open FSharp.Compiler.CodeAnalysis open FSharp.Editor.Tests.Helpers open FSharp.Test @@ -31,6 +35,53 @@ type SemanticClassificationServiceTests() = |> Option.toList |> List.collect Array.toList + let openDocument (source: string) = + let solution = RoslynTestHelpers.CreateSolution source + let workspace = solution.Workspace + let documentId = (RoslynTestHelpers.GetSingleDocument solution).Id + workspace.OpenDocument documentId + let document = workspace.CurrentSolution.GetDocument documentId + Assert.True(workspace.IsDocumentOpen documentId, "The document under test has to be open.") + document + + let sourceTextOf (document: Document) = + document.GetTextAsync(CancellationToken.None).GetAwaiter().GetResult() + + let classifyWith (ct: CancellationToken) (document: Document) (span: TextSpan) = + let result = ResizeArray() + + (FSharpClassificationService() :> IFSharpClassificationService) + .AddSemanticClassificationsAsync(document, span, result, ct) + .GetAwaiter() + .GetResult() + + List.ofSeq result + + let classify document span = + classifyWith CancellationToken.None document span + + let isCached (cache: DocumentCache) (document: Document) = + (cache.TryGetValueAsync document CancellationToken.None).GetAwaiter().GetResult().IsSome + + let lineSpan (text: SourceText) firstLine lastLine = + TextSpan.FromBounds(text.Lines[firstLine].Start, text.Lines[lastLine].End) + + let clearProjectOptions (document: Document) = + document.Project.Solution.Workspace.Services.GetService().FSharpProjectOptionsManager.ClearAllCaches() + + // A project whose options were never supplied, i.e. one Visual Studio is still loading. + let openDocumentWithoutProjectOptions (source: string) = + let projectId = ProjectId.CreateNewId() + let documentInfo = RoslynTestHelpers.CreateDocumentInfo projectId "test.fs" source + + let projectInfo = + RoslynTestHelpers.CreateProjectInfo projectId "test.fsproj" [ documentInfo ] + + let solution = RoslynTestHelpers.CreateSolution [ projectInfo ] + let documentId = (RoslynTestHelpers.GetSingleDocument solution).Id + solution.Workspace.OpenDocument documentId + solution.Workspace.CurrentSolution.GetDocument documentId + let verifyClassificationAtEndOfMarker (fileContents: string, marker: string, classificationType: string) = let text = SourceText.From(fileContents) let ranges = getRanges fileContents @@ -416,3 +467,115 @@ let result2 = s.(*2*)IsHyperbolicCaseWithLongName && Range.rangeContainsPos item.Range longCasePos) Assert.True(longCaseUnionItems.Length > 0, "Expected a UnionCase classification covering 'IsHyperbolicCaseWithLongName'") + + // Which cache a document lands in is invisible in its classifications - a miss only costs a + // recheck - so these reach the caches directly. Splitting one cache in two (#15954) left the + // open-document branch reading the opened cache and writing the unopened one, so the opened + // cache was never populated and every request for an open file re-ran the checker. + [] + member _.``Semantic classification of an open document is cached for opened documents``() = + let document = openDocument "let x = 1" + let text = sourceTextOf document + + Assert.NotEmpty(classify document (TextSpan(0, text.Length))) + + Assert.True( + isCached FSharpClassificationService.OpenedDocumentsSemanticClassificationCache document, + "Classifying an open document must populate the opened-documents cache." + ) + + Assert.False( + isCached FSharpClassificationService.UnopenedDocumentsSemanticClassificationCache document, + "An open document must not be cached as an unopened one." + ) + + // The cache is keyed by text version only, so what it holds has to cover the whole file: + // Roslyn asks for the visible span, then for other spans of the same version as the user scrolls. + [] + member _.``Semantic classification computed for one span of an open document serves another span at the same version``() = + let source = + [ + "type R = { Doop: int }" + "let r = { Doop = 12 }" + "" + "let mutable first = 12" + "let g () = first" + ] + |> String.concat "\n" + + let document = openDocument source + let text = sourceTextOf document + let spanA = lineSpan text 0 1 + let spanB = lineSpan text 3 4 + Assert.False(spanA.IntersectsWith spanB) + + let first = classify document spanA + Assert.NotEmpty first + + let second = classify document spanB + Assert.NotEmpty second + Assert.All(second, fun span -> Assert.True(spanB.Contains span.TextSpan)) + + // A freshly opened copy has a new DocumentId and therefore cold caches: a direct computation for B. + Assert.Equal(classify (openDocument source) spanB, second) + Assert.Equal(first, classify document spanA) + + // Roslyn replaces a span's tags with whatever comes back, so "no result" must re-emit the last + // good one rather than strip the colours the user already sees. + [] + member _.``Semantic classification serves the last good lookup when project options become unavailable``() = + let source = "let x = 1\nlet y = 2" + let document = openDocument source + let text = sourceTextOf document + Assert.NotEmpty(classify document (TextSpan(0, text.Length))) + + clearProjectOptions document + // A new text version misses the versioned cache; the text itself is unchanged, so the last + // good lookup still describes it. + let reopened = document.WithText(SourceText.From source) + let reopenedText = sourceTextOf reopened + + Assert.NotEmpty(classify reopened (TextSpan(0, reopenedText.Length))) + + Assert.False( + isCached FSharpClassificationService.OpenedDocumentsSemanticClassificationCache reopened, + "A miss must not be cached as a result." + ) + + // The lookup names positions in the text it was computed from, so against edited text it would + // colour the wrong characters. + [] + member _.``Semantic classification does not serve the last good lookup for edited text``() = + let document = openDocument "let x = 1\nlet y = 2" + let text = sourceTextOf document + Assert.NotEmpty(classify document (TextSpan(0, text.Length))) + + clearProjectOptions document + let edited = document.WithText(SourceText.From "// a comment\nlet x = 1\nlet y = 2") + let editedText = sourceTextOf edited + + Assert.Empty(classify edited (TextSpan(0, editedText.Length))) + + [] + member _.``Semantic classification without project options and without an earlier result returns nothing and caches nothing``() = + let document = openDocumentWithoutProjectOptions "let x = 1" + let text = sourceTextOf document + + Assert.Empty(classify document (TextSpan(0, text.Length))) + Assert.False(isCached FSharpClassificationService.OpenedDocumentsSemanticClassificationCache document) + Assert.False(isCached FSharpClassificationService.UnopenedDocumentsSemanticClassificationCache document) + + // Roslyn keeps the previous tags only for a cancellation carrying its own token; nothing may catch it. + [] + member _.``Semantic classification propagates cancellation as a canceled task``() = + let document = openDocument "let x = 1" + let text = sourceTextOf document + + let task = + (FSharpClassificationService() :> IFSharpClassificationService) + .AddSemanticClassificationsAsync(document, TextSpan(0, text.Length), ResizeArray(), CancellationToken(true)) + + Assert.ThrowsAny(fun () -> task.GetAwaiter().GetResult()) + |> ignore + + Assert.True task.IsCanceled From 6cb4662d3e8faab6bc7a34b8d4d032a4ed672b8c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 19:47:33 +0200 Subject: [PATCH 057/133] Share one whole-file classification between requests for a version The classification of an open document covers the whole file, so what it costs is proportional to the file, not to the viewport: on an 8000-line file a single pass takes about a second and a half. Two things made that pass run far more often than the text changed. The entry lived in a MemoryCache with a two-second sliding expiration, so the first scroll after any pause missed it and walked the file again for a text version that had already been classified. The entry now lives in the per-document store that the last-known-good fallback already used, keyed by text version and kept for the life of the document, and the separate opened-documents cache goes away. Requests that overlapped - Roslyn's taggers above and below the viewport, a split view - each walked the file on their own. Requests for the same version now join one in-flight computation; it starts with the first of them and is cancelled only when the last one leaves, so a superseded request never cancels the work its neighbours are still waiting for, and no request outlives its own token. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 5c70d1ee55c004c0699e826adbc6dc498d373d34) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../Classification/ClassificationService.fs | 166 +++++++++++++----- .../SemanticClassificationServiceTests.fs | 87 ++++++++- 3 files changed, 203 insertions(+), 51 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 480fe804587..a0b9fbdd1ec 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -22,6 +22,7 @@ * Reduce allocations in the VS project options reactor: the command-line options and project options caches and the mailbox reply payloads now hold struct tuples, and `IProjectSite.CompilationBinOutputPath` returns `string voption` picked with a new `Array.tryPickV`. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413)) * Fix `NullReferenceException` that broke syntax coloring for the rest of the file when an unlinked placeholder entity was encountered while editing broken code. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Fix semantic colours of open documents disappearing: their classification was stored in the unopened-documents cache and only covered the requested span, so scrolling re-ran the checker or got nothing back, and a check that could not complete answered with no classifications, which clears the colours already shown. ([Issue #20445](https://github.com/dotnet/fsharp/issues/20445), [PR #20450](https://github.com/dotnet/fsharp/pull/20450)) +* Requests for one version of an open document share a single whole-file semantic classification, kept for the life of the document instead of two seconds, so scrolling a large file after a pause no longer re-walks every symbol in it. ([PR #20452](https://github.com/dotnet/fsharp/pull/20452)) ### Changed diff --git a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs index c7dc7738587..8634507303f 100644 --- a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs +++ b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs @@ -4,9 +4,11 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Composition +open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable open System.Threading +open System.Threading.Tasks open System.Runtime.Caching open System.Runtime.CompilerServices @@ -31,12 +33,52 @@ open Microsoft.VisualStudio.FSharp.Editor.Telemetry type SemanticClassificationData = SemanticClassificationView type SemanticClassificationLookup = IReadOnlyDictionary> -type internal LastGoodSemanticClassification = +/// The whole-file semantic classification of one version of an open document. +type internal OpenDocumentClassification = { + Version: VersionStamp Text: SourceText Lookup: SemanticClassificationLookup } +/// One classification of a document version, shared by every request that arrives while it runs. +/// The computation starts with the first waiter and is cancelled only when the last one leaves. +[] +type internal InFlightClassification(version: VersionStamp, compute: CancellationToken -> Task) = + let cts = new CancellationTokenSource() + let job = lazy (compute cts.Token) + let mutable waiters = 0 + + member _.Version = version + + member _.IsCancelled = cts.IsCancellationRequested + + member _.IsCompleted = job.IsValueCreated && job.Value.IsCompleted + + member _.Join(cancellationToken: CancellationToken) : Task = + Interlocked.Increment &waiters |> ignore + let job = job.Value + let left = ref 0 + + let leave () = + if + Interlocked.Exchange(left, 1) = 0 + && Interlocked.Decrement &waiters = 0 + && not job.IsCompleted + then + cts.Cancel() + + task { + use _ = cancellationToken.Register(fun () -> leave ()) + + try + let! _ = Task.WhenAny(job, Task.Delay(Timeout.Infinite, cancellationToken)) + cancellationToken.ThrowIfCancellationRequested() + return! job + finally + leave () + } + [)>] type internal FSharpClassificationService [] () = @@ -153,29 +195,29 @@ type internal FSharpClassificationService [] () = static let unopenedDocumentsSemanticClassificationCache = new DocumentCache("fsharp-unopened-documents-semantic-classification-cache", 5.) - static let openedDocumentsSemanticClassificationCache = - new DocumentCache("fsharp-opened-documents-semantic-classification-cache", 2.) + // The classification of an open document's latest checked version. Roslyn asks for it span by span + // for as long as that version is on screen, and replaces a span's semantic tags with whatever comes + // back - so it is kept for the life of the document rather than expiring, and when the checker + // cannot answer (project loading or reloading, a superseded check) it is re-emitted rather than + // answering "nothing", which would strip the colours the user already sees. + static let openDocumentClassifications = + ConditionalWeakTable() - // Roslyn replaces a span's semantic tags with whatever this service returns, so answering "nothing" - // while the checker is unavailable (project loading or reloading, a superseded check) strips the - // colours the user already sees. Keep the last whole-file lookup per document, outliving the - // version-keyed cache above, and re-emit it for those requests. - static let lastGoodSemanticClassification = - ConditionalWeakTable() + static let inFlightClassifications = + ConcurrentDictionary() - static let rememberLastGood (documentId: DocumentId) (text: SourceText) (lookup: SemanticClassificationLookup) = - let lastGood = { Text = text; Lookup = lookup } + static let remember (documentId: DocumentId) (classification: OpenDocumentClassification) = // net472 has no ConditionalWeakTable.AddOrUpdate. - lock lastGoodSemanticClassification (fun () -> - lastGoodSemanticClassification.Remove documentId |> ignore - lastGoodSemanticClassification.Add(documentId, lastGood)) + lock openDocumentClassifications (fun () -> + openDocumentClassifications.Remove documentId |> ignore + openDocumentClassifications.Add(documentId, classification)) // Only for the text it was computed from: the lookup names positions, so against edited text it // would colour the wrong characters. static let addLastGood (documentId: DocumentId) (sourceText: SourceText) (targetSpan: TextSpan) (result: List) = - match lastGoodSemanticClassification.TryGetValue documentId with - | true, lastGood when lastGood.Text.ContentEquals sourceText -> - addSemanticClassificationByLookup sourceText targetSpan lastGood.Lookup result + match openDocumentClassifications.TryGetValue documentId with + | true, classification when classification.Text.ContentEquals sourceText -> + addSemanticClassificationByLookup sourceText targetSpan classification.Lookup result | _ -> () static let addLastGoodForCurrentText (document: Document) (targetSpan: TextSpan) (result: List) = @@ -183,10 +225,63 @@ type internal FSharpClassificationService [] () = | true, sourceText -> addLastGood document.Id sourceText targetSpan result | _ -> () - // Which of the two caches a document lands in is not observable from its classifications - a miss - // only costs a recheck - so tests reach the caches directly to tell the branches apart. - static member internal OpenedDocumentsSemanticClassificationCache = - openedDocumentsSemanticClassificationCache + static let classifyWholeFile (document: Document) (version: VersionStamp) (sourceText: SourceText) = + cancellableTask { + match! document.TryGetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) with + | ValueNone -> return ValueNone + | ValueSome(struct (_, checkResults)) -> + let classificationData = + checkResults.GetSemanticClassification(None, RelatedSymbolUseKind.All) + + // Every checked file resolves at least its enclosing module, so nothing here means + // the classification itself failed (SemanticClassification.fs recovers with an empty + // array). Remembering that would pin the version to no colours. + if classificationData.Length = 0 then + return ValueNone + else + let classification = + { + Version = version + Text = sourceText + Lookup = itemToSemanticClassificationLookup classificationData + } + + remember document.Id classification + return ValueSome classification + } + + // Requests for the same version that overlap - split views, the taggers above and below the + // viewport - share one classification instead of each walking the whole file. + static let classifyOpenDocument (document: Document) (version: VersionStamp) (sourceText: SourceText) = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + + let start () = + InFlightClassification(version, classifyWholeFile document version sourceText) + + let inFlight = + inFlightClassifications.AddOrUpdate( + document.Id, + (fun _ -> start ()), + fun _ running -> + if running.Version = version && not running.IsCancelled then + running + else + start () + ) + + try + return! inFlight.Join cancellationToken + finally + // A waiter that leaves early keeps the entry for those still waiting. + if inFlight.IsCompleted || inFlight.IsCancelled then + (inFlightClassifications :> ICollection>).Remove(KeyValuePair(document.Id, inFlight)) + |> ignore + } + + // Which store a document lands in is not observable from its classifications - a miss only costs + // a recheck - so tests reach them directly to tell the branches apart. + static member internal OpenDocumentClassifications = openDocumentClassifications static member internal UnopenedDocumentsSemanticClassificationCache = unopenedDocumentsSemanticClassificationCache @@ -302,8 +397,10 @@ type internal FSharpClassificationService [] () = addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result else - match! openedDocumentsSemanticClassificationCache.TryGetValueAsync document with - | ValueSome classificationDataLookup -> + let! version = document.GetTextVersionAsync(cancellationToken) + + match openDocumentClassifications.TryGetValue document.Id with + | true, classification when classification.Version = version -> let eventProps: (string * obj) array = [| "context.document.project.id", document.Project.Id.Id.ToString() @@ -316,8 +413,8 @@ type internal FSharpClassificationService [] () = use _eventDuration = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result - | ValueNone -> + addSemanticClassificationByLookup sourceText textSpan classification.Lookup result + | _ -> let eventProps: (string * obj) array = [| @@ -331,24 +428,9 @@ type internal FSharpClassificationService [] () = use _eventDuration = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - match! document.TryGetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) with + match! classifyOpenDocument document version sourceText with + | ValueSome classification -> addSemanticClassificationByLookup sourceText textSpan classification.Lookup result | ValueNone -> addLastGood document.Id sourceText textSpan result - | ValueSome(struct (_, checkResults)) -> - // The cache is keyed by text version only, so it has to hold the whole file: - // the next request at this version is usually for a different span. - let classificationData = - checkResults.GetSemanticClassification(None, RelatedSymbolUseKind.All) - - // Every checked file resolves at least its enclosing module, so nothing here means - // the classification itself failed (SemanticClassification.fs recovers with an empty - // array). Caching that would pin the version to no colours. - if classificationData.Length = 0 then - addLastGood document.Id sourceText textSpan result - else - let classificationDataLookup = itemToSemanticClassificationLookup classificationData - do! openedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) - rememberLastGood document.Id sourceText classificationDataLookup - addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result } // A cancellation that is not Roslyn's own (a superseded or aborted check surfaces as one) // must not turn into an empty answer, which Roslyn would paint as "no colours". diff --git a/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs index cde75f71e4c..183ad76d1be 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/SemanticClassificationServiceTests.fs @@ -3,7 +3,9 @@ namespace FSharp.Editor.Tests open System +open System.Collections.Generic open System.Threading +open System.Threading.Tasks open Xunit open Microsoft.CodeAnalysis open Microsoft.VisualStudio.FSharp.Editor @@ -63,6 +65,14 @@ type SemanticClassificationServiceTests() = let isCached (cache: DocumentCache) (document: Document) = (cache.TryGetValueAsync document CancellationToken.None).GetAwaiter().GetResult().IsSome + let versionOf (document: Document) = + document.GetTextVersionAsync(CancellationToken.None).GetAwaiter().GetResult() + + let isRemembered (document: Document) = + match FSharpClassificationService.OpenDocumentClassifications.TryGetValue document.Id with + | true, classification -> classification.Version = versionOf document + | _ -> false + let lineSpan (text: SourceText) firstLine lastLine = TextSpan.FromBounds(text.Lines[firstLine].Start, text.Lines[lastLine].End) @@ -479,10 +489,7 @@ let result2 = s.(*2*)IsHyperbolicCaseWithLongName Assert.NotEmpty(classify document (TextSpan(0, text.Length))) - Assert.True( - isCached FSharpClassificationService.OpenedDocumentsSemanticClassificationCache document, - "Classifying an open document must populate the opened-documents cache." - ) + Assert.True(isRemembered document, "Classifying an open document must remember its classification.") Assert.False( isCached FSharpClassificationService.UnopenedDocumentsSemanticClassificationCache document, @@ -537,10 +544,7 @@ let result2 = s.(*2*)IsHyperbolicCaseWithLongName Assert.NotEmpty(classify reopened (TextSpan(0, reopenedText.Length))) - Assert.False( - isCached FSharpClassificationService.OpenedDocumentsSemanticClassificationCache reopened, - "A miss must not be cached as a result." - ) + Assert.False(isRemembered reopened, "A miss must not be remembered as a result.") // The lookup names positions in the text it was computed from, so against edited text it would // colour the wrong characters. @@ -562,7 +566,7 @@ let result2 = s.(*2*)IsHyperbolicCaseWithLongName let text = sourceTextOf document Assert.Empty(classify document (TextSpan(0, text.Length))) - Assert.False(isCached FSharpClassificationService.OpenedDocumentsSemanticClassificationCache document) + Assert.False(isRemembered document) Assert.False(isCached FSharpClassificationService.UnopenedDocumentsSemanticClassificationCache document) // Roslyn keeps the previous tags only for a cancellation carrying its own token; nothing may catch it. @@ -579,3 +583,68 @@ let result2 = s.(*2*)IsHyperbolicCaseWithLongName |> ignore Assert.True task.IsCanceled + + // Roslyn's viewport taggers ask for the same version at once; the file must be walked once for all of them. + [] + member _.``Overlapping requests for one version share a single classification``() = + let gate = TaskCompletionSource() + let computed = ref 0 + + let inFlight = + InFlightClassification( + VersionStamp.Create(), + fun _ -> + Interlocked.Increment computed |> ignore + gate.Task + ) + + let first = inFlight.Join CancellationToken.None + let second = inFlight.Join CancellationToken.None + Assert.False(first.IsCompleted || second.IsCompleted) + + let classification = + { + Version = VersionStamp.Create() + Text = SourceText.From "" + Lookup = Dictionary() + } + + gate.SetResult(ValueSome classification) + + Assert.Same(classification.Lookup, first.Result.Value.Lookup) + Assert.Same(classification.Lookup, second.Result.Value.Lookup) + Assert.Equal(1, computed.Value) + + [] + member _.``A shared classification is cancelled only when its last waiter leaves``() = + let gate = TaskCompletionSource() + let sharedToken = ref CancellationToken.None + + let inFlight = + InFlightClassification( + VersionStamp.Create(), + fun ct -> + sharedToken.Value <- ct + gate.Task + ) + + use first = new CancellationTokenSource() + use second = new CancellationTokenSource() + let firstJoin = inFlight.Join first.Token + let secondJoin = inFlight.Join second.Token + + first.Cancel() + + Assert.ThrowsAny(fun () -> firstJoin.GetAwaiter().GetResult() |> ignore) + |> ignore + + Assert.False(sharedToken.Value.IsCancellationRequested, "One waiter leaving must not cancel the others.") + Assert.False(secondJoin.IsCompleted) + + second.Cancel() + + Assert.ThrowsAny(fun () -> secondJoin.GetAwaiter().GetResult() |> ignore) + |> ignore + + Assert.True(sharedToken.Value.IsCancellationRequested, "The last waiter leaving must cancel the work.") + Assert.True inFlight.IsCancelled From 5d4313cb60f894edd6b737ac4b755f563398aa0e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 19:28:27 +0200 Subject: [PATCH 058/133] Outline with the quick parsing options when a project has none Outlining needs a parse, nothing more, but it asked for the parse of a fully configured project and answered with no regions whenever that was unavailable - while the project loads or reloads, and for any cancellation the checker relabelled with the caller's token. Roslyn takes that literally and expands every region the user had folded. Parse with the quick parsing options in that case, the ones syntactic classification and brace matching already use, and keep the regions. Those options carried no source files, so `ParseFile` threw looking for the last compiland; give them the file being parsed. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit f41f3e460bb905e7730cad64d4ce0ee8ef68c74a) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../FSharpProjectOptionsManager.fs | 2 + .../LanguageService/WorkspaceExtensions.fs | 23 ++++++++---- .../Structure/BlockStructureService.fs | 27 ++++++++++++-- .../BlockStructureServiceTests.fs | 37 +++++++++++++++++++ .../FSharp.Editor.Tests.fsproj | 1 + 6 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/BlockStructureServiceTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index a0b9fbdd1ec..7877ab440e8 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -23,6 +23,7 @@ * Fix `NullReferenceException` that broke syntax coloring for the rest of the file when an unlinked placeholder entity was encountered while editing broken code. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Fix semantic colours of open documents disappearing: their classification was stored in the unopened-documents cache and only covered the requested span, so scrolling re-ran the checker or got nothing back, and a check that could not complete answered with no classifications, which clears the colours already shown. ([Issue #20445](https://github.com/dotnet/fsharp/issues/20445), [PR #20450](https://github.com/dotnet/fsharp/pull/20450)) * Requests for one version of an open document share a single whole-file semantic classification, kept for the life of the document instead of two seconds, so scrolling a large file after a pause no longer re-walks every symbol in it. ([PR #20452](https://github.com/dotnet/fsharp/pull/20452)) +* Keep the collapsed regions of a file while its project is loading or reloading: outlining now parses with the quick parsing options instead of answering with no regions, which expanded everything the user had folded. ([PR #20451](https://github.com/dotnet/fsharp/pull/20451)) ### Changed diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index b8c0b29a034..04cad802278 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -728,7 +728,9 @@ type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Wor match reactor.TryGetCachedOptionsByProjectId(documentId.ProjectId) with | Some(_, parsingOptions, _) -> parsingOptions | _ -> + // ParseFile takes the last entry of SourceFiles as the last compiland; with none it throws. { FSharpParsingOptions.Default with + SourceFiles = [| path |] IsInteractive = CompilerEnvironment.IsScriptFile path } diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index ffcf870ec34..897884d6487 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -619,17 +619,26 @@ type Document with match this.GetFsharpParsingOptions() with | defines, _ -> defines - /// Parses the given F# document. - member this.GetFSharpParseResultsAsync(userOpName) = + /// Parses the given F# document; ValueNone while its project has no compilation options. + member this.TryGetFSharpParseResultsAsync(userOpName) : CancellableTask = cancellableTask { - let! checker, _, parsingOptions, options = this.GetFSharpCompilationOptionsAsync(userOpName) + match! this.TryGetFSharpCompilationOptionsAsync(userOpName) with + | ValueNone -> return ValueNone + | ValueSome options -> + let! parseResults = + if this.Project.UseTransparentCompiler then + options.Checker.ParseDocumentUsingTransparentCompiler(this, options.ProjectOptions, userOpName) + else + options.Checker.ParseDocument(this, options.ParsingOptions, userOpName) - if this.Project.UseTransparentCompiler then - return! checker.ParseDocumentUsingTransparentCompiler(this, options, userOpName) - else - return! checker.ParseDocument(this, parsingOptions, userOpName) + return ValueSome parseResults } + /// Parses the given F# document. + member this.GetFSharpParseResultsAsync(userOpName) = + this.TryGetFSharpParseResultsAsync(userOpName) + |> CancellableTask.map (orRaise "FSharp project options not found.") + /// Parses and checks the given F# document; ValueNone while its project has no compilation options /// or the check was aborted. member this.TryGetFSharpParseAndCheckResultsAsync diff --git a/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs b/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs index b087cb56782..2eb47b09971 100644 --- a/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs +++ b/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs @@ -156,8 +156,6 @@ open CancellableTasks [)>] type internal FSharpBlockStructureService [] () = - let emptyValue = FSharpBlockStructure ImmutableArray.empty - interface IFSharpBlockStructureService with member _.GetBlockStructureAsync(document, cancellationToken) : Task = @@ -166,12 +164,33 @@ type internal FSharpBlockStructureService [] () = let! sourceText = document.GetTextAsync(cancellationToken) - let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpBlockStructureService)) + let! parseResults = + cancellableTask { + // A cancellation of its own making - the checker relabels the internal ones with + // the caller's token - is no more an answer here than missing options are. + let! parseResults = + document.TryGetFSharpParseResultsAsync(nameof (FSharpBlockStructureService)) + |> CancellableTask.ifCanceledReturn ValueNone + + match parseResults with + | ValueSome parseResults -> return parseResults + | ValueNone -> + // Outlining only needs a parse; while the project has no options (loading or + // reloading) parse with the quick ones rather than collapse every region. + return! + document + .GetFSharpChecker() + .ParseFile( + document.FilePath, + sourceText.ToFSharpSourceText(), + document.GetFSharpQuickParsingOptions(), + userOpName = nameof (FSharpBlockStructureService) + ) + } return createBlockSpans document.Project.IsFSharpBlockStructureEnabled sourceText parseResults.ParseTree |> Seq.toImmutableArray |> FSharpBlockStructure } - |> CancellableTask.ifCanceledReturn emptyValue |> CancellableTask.start cancellationToken diff --git a/vsintegration/tests/FSharp.Editor.Tests/BlockStructureServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/BlockStructureServiceTests.fs new file mode 100644 index 00000000000..82ebc297b6a --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/BlockStructureServiceTests.fs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Editor.Tests + +open System.Threading +open Xunit +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Structure +open Microsoft.VisualStudio.FSharp.Editor +open FSharp.Editor.Tests.Helpers + +type BlockStructureServiceTests() = + + let getBlockStructure (document: Document) = + (FSharpBlockStructureService() :> IFSharpBlockStructureService) + .GetBlockStructureAsync(document, CancellationToken.None) + .GetAwaiter() + .GetResult() + + // Outlining needs only a parse, so a project without options yet (Visual Studio still loading it) + // must not answer with no regions and collapse everything the user had folded. + [] + member _.``Block structure falls back to quick parsing options when project options are unavailable``() = + let projectId = ProjectId.CreateNewId() + + let documentInfo = + RoslynTestHelpers.CreateDocumentInfo projectId "test.fs" "module M\n\nlet f x =\n let y = x + 1\n y * 2\n" + + let projectInfo = + RoslynTestHelpers.CreateProjectInfo projectId "test.fsproj" [ documentInfo ] + + let solution = RoslynTestHelpers.CreateSolution [ projectInfo ] + let document = RoslynTestHelpers.GetSingleDocument solution + + let structure = getBlockStructure document + + Assert.NotEmpty structure.Spans diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index eadb8905ab4..9ca2a4e7960 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -84,6 +84,7 @@ + From 43644d47a5300876f1aad13201434c54c9c15d03 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:14:14 +0200 Subject: [PATCH 059/133] Soften task{} guidance in FSharp.instructions.md task{} does have a foothold on netstandard2.0: it's used in Service/FSharpProjectSnapshot.fs and Service/FSharpWorkspaceQuery.fs. Co-Authored-By: Claude Sonnet 5 (cherry picked from commit 9df3bbb528bb44ee9652ba7374291594a7b3ce56) --- .github/instructions/FSharp.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/instructions/FSharp.instructions.md b/.github/instructions/FSharp.instructions.md index 93ccaa8c9c0..18ca30d687c 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -41,7 +41,7 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp ## Async and exceptions -- `src/Compiler` targets `netstandard2.0`: use `async { }` and the repo's `cancellable { }` (`src/Compiler/Utilities/Cancellable.fs`). `task { }` has no foothold there. +- `src/Compiler` targets `netstandard2.0`: prefer `async { }` and the repo's `cancellable { }` (`src/Compiler/Utilities/Cancellable.fs`). `task { }` appears only in `Service/FSharpProjectSnapshot.fs` and `Service/FSharpWorkspaceQuery.fs` – avoid it elsewhere in new core code. - `vsintegration` runs on the VS threading model where `task { }` is at home. When an override must return non-generic `Task`, annotate explicitly – `override _.M(…) : Task = task { … }` – never cast through pipelines. - Thread cancellation through; see `ExpertReview.instructions.md`. - `reraise ()` does not compile inside a `task`/`async` CE (FS0413). There, rethrow with `ExceptionDispatchInfo.Capture(ex).Throw()`; outside CEs plain `reraise ()` is correct. From 30bf1d1c170f54f5c9f9d51d79083217e2d23d7e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:19:35 +0200 Subject: [PATCH 060/133] Prefer one traversal over a chain of transformations A single fold/loop/comprehension allocates nothing per element; a List/Array chain allocates an intermediate collection per stage and a Seq chain an enumerator per stage. Keep the Seq route as the fallback for when the chain reads better than one pass. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 2bf70a2b30ee0073a7d856a6649b7e1ceb8c7fa9) --- .github/instructions/FSharp.instructions.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/instructions/FSharp.instructions.md b/.github/instructions/FSharp.instructions.md index 18ca30d687c..3d2f0ad2790 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -35,7 +35,8 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp - Prefer the `_.Property` shorthand in pipeline position: `tys |> List.map _.Type`. Complex expressions (`fun x -> x.Name = name`, `fun x -> x.A, x.B`) cannot use it. Never add a space – `_.MethodCall ()` breaks parsing. Unrelated to the `member _.Foo` self-identifier. - Eta-reduce: `Seq.map (fun x -> someFunction x)` must become `Seq.map someFunction`. -- Several pipeline stages in a row over a `List`/`Array` allocate an intermediate collection each – route the chain through `Seq` and materialize once at the end. +- Prefer a single traversal – one `fold`, loop, or comprehension – to a chain of transformations: it allocates nothing per element, where a chain allocates at every stage. +- When the chain reads better than one pass, route it through `Seq` and materialize once at the end – a `List`/`Array` chain allocates a whole intermediate collection per stage, a `Seq` chain only an enumerator. - Concatenate with `[ yield! xs; yield! ys ]` / `seq { yield! xs; yield! ys }` rather than `@` or `Seq.append` – `@` forces both sides to lists and is O(n). - Cast sequence items with `Seq.cast`, not `Seq.map (fun item -> item :> Target)`. From 9fe09e96bc8ae4e828fd3f32e0a058aae85936ce Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 5 Sep 2026 00:44:36 +0200 Subject: [PATCH 061/133] Define the orRaise helper the voption options accessors call --- .../src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 897884d6487..f438f8aae4c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -536,6 +536,9 @@ module private CheckerExtensions = checker.ParseAndCheckDocumentWithPossibleStaleResults(document, options, allowStaleResults, userOpName = userOpName) } +let private orRaise message = + ValueOption.defaultWith (fun () -> raise (OperationCanceledException(message: string))) + type Document with /// Get the compilation options of the F# project that is associated with the given F# document, From cf43dd15813f3cf8529b1915d2f1c69714c29553 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 5 Sep 2026 00:44:36 +0200 Subject: [PATCH 062/133] Feed the Copilot snippet lines as memory slices too --- .../src/FSharp.Editor/Copilot/CopilotContextProvider.fs | 3 +-- .../src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs index 7a5da42a1a2..73c1e3b166b 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs @@ -132,8 +132,7 @@ module internal CopilotSymbolQuery = let! sourceText = document.GetTextAsync ct let! parseResults = document.GetFSharpParseResultsAsync UserOpName - let sourceLines = - Array.init sourceText.Lines.Count (fun line -> sourceText.Lines[line].ToString()) + let sourceLines = sourceText.GetLinesAsMemory() let scopes = Structure.getOutliningRanges sourceLines parseResults.ParseTree diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs index b31d9c192bc..fe53cac4f7d 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolSnippets.fs @@ -12,7 +12,7 @@ open FSharp.Compiler.EditorServices let MaxSnippetLines = 200 /// Inclusive, 1-based line bounds of the declaration `item` names, including its doc comment. -let definitionLines (sourceLines: string array) (scopes: Structure.ScopeRange seq) (item: NavigableItem) = +let definitionLines (sourceLines: ReadOnlyMemory array) (scopes: Structure.ScopeRange seq) (item: NavigableItem) = let declarationLine = item.Range.StartLine // A construct's outlining range reaches back over the doc comment in front of it, so it is the @@ -40,7 +40,7 @@ let definitionLines (sourceLines: string array) (scopes: Structure.ScopeRange se // Outlining reports a doc comment only once it spans several lines, so a one-line "///" in front of // a declaration is invisible to the scopes above. let isDocComment line = - sourceLines[line - 1].AsSpan().TrimStart().StartsWith("///".AsSpan(), StringComparison.Ordinal) + sourceLines[line - 1].Span.TrimStart().StartsWith("///".AsSpan(), StringComparison.Ordinal) let rec docCommentStart line = if line > 1 && isDocComment (line - 1) then From 9820cdb2a0c49e5e214282324069b3217a10a44a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 5 Sep 2026 02:43:01 +0200 Subject: [PATCH 063/133] Assert instead of substituting contents of unlinked entities Every empty stand-in for an unlinked Entity's contents is unsafe: the augmentation and module type are mutated in place by their consumers, so a shared sentinel is corrupted and a fresh one loses the write. The fields stay nullable because NewUnlinked really leaves them null; the getters and remapping now nonNull them, which fails fast exactly where main dereferenced null before, but with the type telling the truth. Reverts the NewUnlinked allocation and "" name, the remapTyconAug signature change and the null propagation in remapping. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit e50ee2e67a7682cb865c4f8faf91149368c88137) --- src/Compiler/TypedTree/TypedTree.fs | 29 +++------- src/Compiler/TypedTree/TypedTree.fsi | 8 --- .../TypedTree/TypedTreeOps.Remapping.fs | 53 ++++++++++--------- .../TypedTree/TypedTreeOps.Remapping.fsi | 2 +- .../TypedTree/TypedTreeOps.Transforms.fs | 2 +- 5 files changed, 35 insertions(+), 59 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index 3425ce4d911..d45af205761 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -888,16 +888,10 @@ type Entity = | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_xmldocsig = v } /// The logical contents of the entity when it is a module or namespace fragment. - member x.ModuleOrNamespaceType = - match x.entity_modul_type with - | null -> Entity.EmptyModuleOrNamespaceType - | modulType -> modulType.Force() + member x.ModuleOrNamespaceType = (nonNull x.entity_modul_type).Value /// The logical contents of the entity when it is a type definition. - member x.TypeContents = - match x.entity_tycon_tcaug with - | null -> TyconAugmentation.Empty - | tcaug -> tcaug + member x.TypeContents = nonNull x.entity_tycon_tcaug /// The kind of the type definition - is it a measure definition or a type definition? member x.TypeOrMeasureKind = @@ -1113,17 +1107,12 @@ type Entity = | ValueNone -> None - /// Stands in for the contents of an entity that was never linked, so that a broken mid-edit - /// file still classifies instead of crashing. Never reachable from a successfully checked entity. - static member val EmptyModuleOrNamespaceType = - ModuleOrNamespaceType(ModuleOrType, QueueList.Empty, QueueList.Empty) - /// Create a new entity with empty, unlinked data. Only used during unpickling of F# metadata. static member NewUnlinked() : Entity = - { entity_typars = LazyWithContext.NotLazy [] + { entity_typars = Unchecked.defaultof<_> entity_flags = Unchecked.defaultof<_> entity_stamp = Unchecked.defaultof<_> - entity_logical_name = "" + entity_logical_name = Unchecked.defaultof<_> entity_range = Unchecked.defaultof<_> entity_attribs = Unchecked.defaultof<_> entity_tycon_repr= Unchecked.defaultof<_> @@ -1560,7 +1549,7 @@ type TyconAugmentation = member tcaug.SetHasObjectGetHashCode b = tcaug.tcaug_hasObjectGetHashCode <- b - static member Create() : TyconAugmentation = + static member Create() = { tcaug_compare=None tcaug_compare_withc=None tcaug_equals=None @@ -1573,10 +1562,6 @@ type TyconAugmentation = tcaug_closed=false tcaug_abstract=false } - /// Stands in for the augmentation of an entity that was never linked, so that a broken mid-edit - /// file still classifies instead of crashing. Never reachable from a successfully checked entity. - static member val Empty = TyconAugmentation.Create() - [] member x.DebugText = x.ToString() @@ -6606,9 +6591,7 @@ type Construct() = /// contents of the module. static member NewModifiedModuleOrNamespace f orig = orig |> Construct.NewModifiedTycon (fun d -> - match d.entity_modul_type with - | null -> d - | modulType -> { d with entity_modul_type = MaybeLazy.Strict (f (modulType.Force())) }) + { d with entity_modul_type = MaybeLazy.Strict (f (nonNull d.entity_modul_type).Value) }) /// Create a Val based on an existing one using the function 'f'. /// We require that we be given the parent for the new Val. diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index 6cbd3ea7f43..92e12c1cb1e 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -472,10 +472,6 @@ type Entity = static member NewEmptyEntityOptData: unit -> EntityOptionalData - /// Stands in for the contents of an entity that was never linked, so that a broken mid-edit - /// file still classifies instead of crashing. Never reachable from a successfully checked entity. - static member EmptyModuleOrNamespaceType: ModuleOrNamespaceType - /// Create a new entity with empty, unlinked data. Only used during unpickling of F# metadata. static member NewUnlinked: unit -> Entity @@ -922,10 +918,6 @@ type TyconAugmentation = static member Create: unit -> TyconAugmentation - /// Stands in for the augmentation of an entity that was never linked, so that a broken mid-edit - /// file still classifies instead of crashing. Never reachable from a successfully checked entity. - static member Empty: TyconAugmentation - /// Record a member in declaration order, allocating the list on first use member AddAdhocMember: isExplicitImpl: bool * vref: ValRef -> unit diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs index 8b118c70f64..8228aa108f9 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs @@ -2177,28 +2177,25 @@ module internal ExprRemapping = | TAsmRepr _ -> repr | TMeasureableRepr x -> TMeasureableRepr(remapType tmenv x) - and remapTyconAug tmenv (x: TyconAugmentation | null) = - match x with - | null -> null - | x -> - { x with - tcaug_equals = x.tcaug_equals |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) - tcaug_compare = x.tcaug_compare |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) - tcaug_compare_withc = x.tcaug_compare_withc |> Option.map (remapValRef tmenv) - tcaug_hash_and_equals_withc = - x.tcaug_hash_and_equals_withc - |> Option.map (mapQuadruple (remapValRef tmenv, remapValRef tmenv, remapValRef tmenv, Option.map (remapValRef tmenv))) - tcaug_adhoc = x.tcaug_adhoc |> NameMap.map (List.map (remapValRef tmenv)) - tcaug_adhoc_list = - let remapped: ResizeArray | null = - match x.tcaug_adhoc_list with - | null -> null - | l -> l |> ResizeArray.map (fun (flag, vref) -> (flag, remapValRef tmenv vref)) - - remapped - tcaug_super = x.tcaug_super |> Option.map (remapType tmenv) - tcaug_interfaces = x.tcaug_interfaces |> List.map (map1Of3 (remapType tmenv)) - } + and remapTyconAug tmenv (x: TyconAugmentation) = + { x with + tcaug_equals = x.tcaug_equals |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) + tcaug_compare = x.tcaug_compare |> Option.map (mapPair (remapValRef tmenv, remapValRef tmenv)) + tcaug_compare_withc = x.tcaug_compare_withc |> Option.map (remapValRef tmenv) + tcaug_hash_and_equals_withc = + x.tcaug_hash_and_equals_withc + |> Option.map (mapQuadruple (remapValRef tmenv, remapValRef tmenv, remapValRef tmenv, Option.map (remapValRef tmenv))) + tcaug_adhoc = x.tcaug_adhoc |> NameMap.map (List.map (remapValRef tmenv)) + tcaug_adhoc_list = + let remapped: ResizeArray | null = + match x.tcaug_adhoc_list with + | null -> null + | l -> l |> ResizeArray.map (fun (flag, vref) -> (flag, remapValRef tmenv vref)) + + remapped + tcaug_super = x.tcaug_super |> Option.map (remapType tmenv) + tcaug_interfaces = x.tcaug_interfaces |> List.map (map1Of3 (remapType tmenv)) + } and remapTyconExnInfo ctxt tmenv inp = match inp with @@ -2302,11 +2299,15 @@ module internal ExprRemapping = tcdR.entity_attribs <- WellKnownEntityAttribs.Create(tcd.entity_attribs.AsList() |> remapAttribs ctxt tmenvinner2) tcdR.entity_tycon_repr <- tcd.entity_tycon_repr |> remapTyconRepr ctxt tmenvinner2 let typeAbbrevR = tcd.TypeAbbrev |> Option.map (remapType tmenvinner2) - tcdR.entity_tycon_tcaug <- tcd.entity_tycon_tcaug |> remapTyconAug tmenvinner2 + tcdR.entity_tycon_tcaug <- remapTyconAug tmenvinner2 (nonNull tcd.entity_tycon_tcaug) + tcdR.entity_modul_type <- - match tcd.entity_modul_type with - | null -> null - | ty -> MaybeLazy.Strict(ty.Force() |> mapImmediateValsAndTycons lookupTycon lookupVal) + MaybeLazy.Strict( + (nonNull tcd.entity_modul_type).Value + |> mapImmediateValsAndTycons lookupTycon lookupVal + ) + + let exnInfoR = tcd.ExceptionInfo |> remapTyconExnInfo ctxt tmenvinner2 match tcdR.entity_opt_data with diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi index 1e4da87eedc..5372d2a2511 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fsi @@ -253,7 +253,7 @@ module internal ExprRemapping = val remapTyconRepr: RemapContext -> Remap -> TyconRepresentation -> TyconRepresentation - val remapTyconAug: Remap -> TyconAugmentation | null -> TyconAugmentation | null + val remapTyconAug: Remap -> TyconAugmentation -> TyconAugmentation val remapTyconExnInfo: RemapContext -> Remap -> ExceptionInfo -> ExceptionInfo diff --git a/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs b/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs index 6b6495d3219..a5fa45bd609 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs @@ -983,7 +983,7 @@ module internal Rewriting = let attribsR = d.entity_attribs.AsList() |> remapAttribs ctxt tmenvinner let tyconReprR = d.entity_tycon_repr |> remapTyconRepr ctxt tmenvinner let tyconAbbrevR = d.TypeAbbrev |> Option.map (remapType tmenvinner) - let tyconTcaugR = d.entity_tycon_tcaug |> remapTyconAug tmenvinner + let tyconTcaugR = remapTyconAug tmenvinner (nonNull d.entity_tycon_tcaug) let modulContentsR = MaybeLazy.Strict( From dcb7b3a00ca0a0c6e79674c4b4b55348a67ad5e2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 5 Sep 2026 02:43:02 +0200 Subject: [PATCH 064/133] Recover per resolution in GetSemanticClassification GetSemanticClassification already runs under DiagnosticsScope.Protect, whose recovery returns an empty array for the whole file. That is the symptom in #20269: one resolution naming a never-linked placeholder cost the file all of its colouring. Classify each resolution under its own RecoverableException handler so one bad symbol costs one token. OperationCanceledException still propagates. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 951a332a3745accaf847b73cfd35d137af277494) --- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- docs/release-notes/.VisualStudio/18.vNext.md | 1 + src/Compiler/Service/SemanticClassification.fs | 14 +++++++++++--- 3 files changed, 13 insertions(+), 4 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 0dc17df4d15..383ca1d224f 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -59,7 +59,7 @@ * Narrow overload-resolution error ranges to the method name only instead of covering the entire expression. ([Issue #14284](https://github.com/dotnet/fsharp/issues/14284), [PR #19505](https://github.com/dotnet/fsharp/pull/19505)) * Fix attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) -* Fix a `NullReferenceException` in typed-tree remapping and semantic classification when an unlinked placeholder entity is encountered while editing broken code. The compiler now treats these placeholder entities as absent instead of crashing the whole file's coloring/classification pipeline. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) +* Recover per symbol instead of per file in `GetSemanticClassification`, so one unresolvable symbol no longer drops the whole file's classification; declare the two `Entity` contents fields nullable for unlinked placeholders and fail fast in the pickle writer when an entity was never linked. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix abstract event accessors now have SpecialName flag. (Issue [#5834](https://github.com/dotnet/fsharp/issues/5834), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix warning 20 ("expression is implicitly ignored") pointing at the wrong range when the last expression in a sequential block (e.g. inside `for`, `while` loops) is non-unit. The squiggle now correctly highlights only the offending expression. ([Issue #5418](https://github.com/dotnet/fsharp/issues/5418), [PR #19504](https://github.com/dotnet/fsharp/pull/19504)) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 7877ab440e8..4361a1da23f 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -24,6 +24,7 @@ * Fix semantic colours of open documents disappearing: their classification was stored in the unopened-documents cache and only covered the requested span, so scrolling re-ran the checker or got nothing back, and a check that could not complete answered with no classifications, which clears the colours already shown. ([Issue #20445](https://github.com/dotnet/fsharp/issues/20445), [PR #20450](https://github.com/dotnet/fsharp/pull/20450)) * Requests for one version of an open document share a single whole-file semantic classification, kept for the life of the document instead of two seconds, so scrolling a large file after a pause no longer re-walks every symbol in it. ([PR #20452](https://github.com/dotnet/fsharp/pull/20452)) * Keep the collapsed regions of a file while its project is loading or reloading: outlining now parses with the quick parsing options instead of answering with no regions, which expanded everything the user had folded. ([PR #20451](https://github.com/dotnet/fsharp/pull/20451)) +* Fix syntax coloring being lost for a whole file when one symbol resolves into metadata that could not be read. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) ### Changed diff --git a/src/Compiler/Service/SemanticClassification.fs b/src/Compiler/Service/SemanticClassification.fs index 691d85df6a3..1bf5a752e18 100644 --- a/src/Compiler/Service/SemanticClassification.fs +++ b/src/Compiler/Service/SemanticClassification.fs @@ -231,8 +231,10 @@ module TcResolutionsExtensions = let isDelegateInvokeMemberName name = name = "Invoke" || name = "BeginInvoke" || name = "EndInvoke" - resolutions - |> Array.iter (fun cnr -> + // A resolution can name a placeholder that metadata unpickling never linked; every + // reference field on it is null. Losing one token beats losing the file, which is + // what the enclosing DiagnosticsScope.Protect does. + let classifyResolution (cnr: CapturedNameResolution) = match cnr.Item, cnr.ItemOccurrence, cnr.Range with | (Item.CustomBuilder _ | Item.CustomOperation _), ItemOccurrence.Use, m -> add m SemanticClassificationType.ComputationExpression @@ -440,7 +442,13 @@ module TcResolutionsExtensions = else add m SemanticClassificationType.ReferenceType - | _, _, m -> add m SemanticClassificationType.Plaintext) + | _, _, m -> add m SemanticClassificationType.Plaintext + + for cnr in resolutions do + try + classifyResolution cnr + with RecoverableException e -> + Trace.TraceInformation $"FCS: skipped one resolution in GetSemanticClassification: '%s{e.Message}'" // Classify related symbol uses (e.g., union case testers → UnionCase). // These share ranges with the corresponding property classifications, so we intentionally add a second classification. From eb0ae650203ac9e9bef404ebcba9372223c0660f Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 5 Sep 2026 02:43:02 +0200 Subject: [PATCH 065/133] Fail fast when pickling an entity that was never linked The nonNull guards on the two contents fields could never fire: Typars, entity_logical_name and entity_attribs are dereferenced first and throw a bare NullReferenceException. Check Entity.IsLinked at the top of p_entity_spec_data instead so the writer refuses the entity with a clear message before touching any field. Also restores the Seq pipelines in p_tycon_repr and p_tcaug that an earlier commit on this branch dropped by mistake. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit b12998cf1a5a0f361ab983906ed41855cafdb7aa) --- src/Compiler/TypedTree/TypedTreePickle.fs | 10 ++++-- .../UnlinkedEntityTests.fs | 32 ++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index 46a84559405..fe7962fedb0 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -2723,7 +2723,7 @@ let rec p_tycon_repr x st = let allFieldsText = fields - |> Array.map (fun f -> f.LogicalName) + |> Seq.map _.LogicalName |> String.concat System.Environment.NewLine raise (Error(FSComp.SR.pickleFsharpCoreBackwardsCompatible ("fields in union", allFieldsText), firstFieldRange)) @@ -2826,6 +2826,9 @@ and p_rfield_table x st = p_array p_recdfield_spec x.FieldsByIndex st and p_entity_spec_data (x: Entity) st = + if not x.IsLinked then + pfailwith st "p_entity_spec_data: entity was never linked during unpickling" + p_tyar_specs x.Typars st p_string x.entity_logical_name st p_option p_string x.EntityCompiledName st @@ -2882,8 +2885,9 @@ and p_tcaug p st = // in order to get check the well-formedness of an interface. // Keeping them across assembly boundaries is not valid, because relinking their ValRefs // does not work correctly (they may get incorrectly relinked to a default member) - |> List.filter (fun (isExplicitImpl, _) -> not isExplicitImpl) - |> List.map (fun (_, vref) -> vref.LogicalName, vref)), + |> Seq.filter (fun (isExplicitImpl, _) -> not isExplicitImpl) + |> Seq.map (fun (_, vref) -> vref.LogicalName, vref) + |> Seq.toList), p.tcaug_interfaces, p.tcaug_super, p.tcaug_abstract, diff --git a/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs b/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs index f164a55a542..269d8e2bae8 100644 --- a/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/UnlinkedEntityTests.fs @@ -1,33 +1,19 @@ module FSharp.Compiler.Service.Tests.UnlinkedEntityTests +open System + open Xunit open FSharp.Compiler.TypedTree -/// (#20269) An entity whose data was never linked has no contents. The IDE reaches these while a -/// file is mid-edit, so reading the contents must yield empty stand-ins rather than throwing a -/// NullReferenceException that takes down classification for the whole file. -[] -let ``Unlinked entity exposes empty contents instead of throwing`` () = - let entity = Entity.NewUnlinked() - - Assert.Empty(entity.ModuleOrNamespaceType.AllEntities) - Assert.Empty(entity.ModuleOrNamespaceType.AllValsAndMembers) - Assert.Empty(entity.TypeContents.tcaug_interfaces) - -/// Reading the contents of an unlinked entity must not fill them in: the getters are hit from -/// parallel checking, and writing shared typed-tree state from a getter is a race. +/// (#20269) A placeholder that unpickling never linked is not a readable entity — its +/// representation, attributes, augmentation and contents are all absent, so no stand-in for a +/// subset of them makes it safe. Resilience lives at the IDE read boundary instead. [] -let ``Reading the contents of an unlinked entity does not link them in`` () = +let ``An unlinked entity is not linked and not readable`` () = let entity = Entity.NewUnlinked() - entity.ModuleOrNamespaceType |> ignore - entity.TypeContents |> ignore - - match entity.entity_modul_type with - | null -> () - | _ -> failwith "reading ModuleOrNamespaceType must not write the entity's contents" + Assert.False entity.IsLinked - match entity.entity_tycon_tcaug with - | null -> () - | _ -> failwith "reading TypeContents must not write the entity's augmentation" + Assert.Throws(fun () -> entity.ModuleOrNamespaceType |> ignore) + |> ignore From 3e10f989605980913a4c466bfa77b7c86e782485 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 5 Sep 2026 19:00:36 +0200 Subject: [PATCH 066/133] Keep placeholder fields non-nullable; IsLinked is the contract Entity.NewUnlinked() creates a hole for the OSGN unpickling graph to fill via Link, not a value any legal reader observes. Entity.IsLinked already names that contract (it is the family pattern: Typar/Val/AnonRecdTypeInfo all expose the same shape). Annotating entity_tycon_tcaug and entity_modul_type as nullable described the transient bytes, not the contract, and pushed nonNull/pattern-matching onto every legitimate reader for a case that must not occur there. Revert TypedTree.fs/.fsi, TypedTreeOps.Remapping.fs/.fsi and TypedTreeOps.Transforms.fs to main; the two Entity contents accessors, NewUnlinked, and remapTyconAug's signature go back to what they were. The pickle writer keeps its IsLinked guard (added in the previous commit, still the only guard that actually fires) but drops the now unreachable nonNull calls, since the fields are non-nullable again. Co-Authored-By: Claude Sonnet 5 (cherry picked from commit 86a5d427ea9402b976b04d41fdc282a87637b473) --- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- src/Compiler/TypedTree/TypedTree.fs | 42 +++++++++---------- src/Compiler/TypedTree/TypedTree.fsi | 4 +- .../TypedTree/TypedTreeOps.Remapping.fs | 11 +---- .../TypedTree/TypedTreeOps.Transforms.fs | 4 +- src/Compiler/TypedTree/TypedTreePickle.fs | 4 +- 6 files changed, 30 insertions(+), 37 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 383ca1d224f..24450403072 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -59,7 +59,7 @@ * Narrow overload-resolution error ranges to the method name only instead of covering the entire expression. ([Issue #14284](https://github.com/dotnet/fsharp/issues/14284), [PR #19505](https://github.com/dotnet/fsharp/pull/19505)) * Fix attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) -* Recover per symbol instead of per file in `GetSemanticClassification`, so one unresolvable symbol no longer drops the whole file's classification; declare the two `Entity` contents fields nullable for unlinked placeholders and fail fast in the pickle writer when an entity was never linked. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) +* Recover per symbol instead of per file in `GetSemanticClassification`, so one unresolvable symbol no longer drops the whole file's classification, and fail fast in the pickle writer when an entity was never linked. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix abstract event accessors now have SpecialName flag. (Issue [#5834](https://github.com/dotnet/fsharp/issues/5834), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix warning 20 ("expression is implicitly ignored") pointing at the wrong range when the last expression in a sequential block (e.g. inside `for`, `while` loops) is non-unit. The squiggle now correctly highlights only the offending expression. ([Issue #5418](https://github.com/dotnet/fsharp/issues/5418), [PR #19504](https://github.com/dotnet/fsharp/pull/19504)) diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index d45af205761..5030f59334d 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -699,13 +699,13 @@ type Entity = /// The methods and properties of the type // // MUTABILITY; used only during creation and remapping of tycons - mutable entity_tycon_tcaug: TyconAugmentation | null - - /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions and nested modules + mutable entity_tycon_tcaug: TyconAugmentation + + /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions and nested modules // - // MUTABILITY: only used during creation and remapping of tycons and - // when compiling fslib to fixup compiler forward references to internal items - mutable entity_modul_type: MaybeLazy | null + // MUTABILITY: only used during creation and remapping of tycons and + // when compiling fslib to fixup compiler forward references to internal items + mutable entity_modul_type: MaybeLazy /// The stable path to the type, e.g. Microsoft.FSharp.Core.FSharpFunc`2 // MUTABILITY: only for unpickle linkage @@ -888,10 +888,10 @@ type Entity = | _ -> x.entity_opt_data <- Some { Entity.NewEmptyEntityOptData() with entity_xmldocsig = v } /// The logical contents of the entity when it is a module or namespace fragment. - member x.ModuleOrNamespaceType = (nonNull x.entity_modul_type).Value + member x.ModuleOrNamespaceType = x.entity_modul_type.Force() /// The logical contents of the entity when it is a type definition. - member x.TypeContents = nonNull x.entity_tycon_tcaug + member x.TypeContents = x.entity_tycon_tcaug /// The kind of the type definition - is it a measure definition or a type definition? member x.TypeOrMeasureKind = @@ -1108,12 +1108,12 @@ type Entity = /// Create a new entity with empty, unlinked data. Only used during unpickling of F# metadata. - static member NewUnlinked() : Entity = + static member NewUnlinked() : Entity = { entity_typars = Unchecked.defaultof<_> entity_flags = Unchecked.defaultof<_> entity_stamp = Unchecked.defaultof<_> - entity_logical_name = Unchecked.defaultof<_> - entity_range = Unchecked.defaultof<_> + entity_logical_name = Unchecked.defaultof<_> + entity_range = Unchecked.defaultof<_> entity_attribs = Unchecked.defaultof<_> entity_tycon_repr= Unchecked.defaultof<_> entity_tycon_tcaug= Unchecked.defaultof<_> @@ -1550,16 +1550,16 @@ type TyconAugmentation = member tcaug.SetHasObjectGetHashCode b = tcaug.tcaug_hasObjectGetHashCode <- b static member Create() = - { tcaug_compare=None - tcaug_compare_withc=None - tcaug_equals=None - tcaug_hash_and_equals_withc=None - tcaug_hasObjectGetHashCode=false - tcaug_adhoc=NameMultiMap.empty + { tcaug_compare=None + tcaug_compare_withc=None + tcaug_equals=None + tcaug_hash_and_equals_withc=None + tcaug_hasObjectGetHashCode=false + tcaug_adhoc=NameMultiMap.empty tcaug_adhoc_list=null tcaug_super=None - tcaug_interfaces=[] - tcaug_closed=false + tcaug_interfaces=[] + tcaug_closed=false tcaug_abstract=false } [] @@ -6590,8 +6590,8 @@ type Construct() = /// We pass the new module to 'f' in case it needs to reparent the /// contents of the module. static member NewModifiedModuleOrNamespace f orig = - orig |> Construct.NewModifiedTycon (fun d -> - { d with entity_modul_type = MaybeLazy.Strict (f (nonNull d.entity_modul_type).Value) }) + orig |> Construct.NewModifiedTycon (fun d -> + { d with entity_modul_type = MaybeLazy.Strict (f (d.entity_modul_type.Force())) }) /// Create a Val based on an existing one using the function 'f'. /// We require that we be given the parent for the new Val. diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index 92e12c1cb1e..d706237e468 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -453,10 +453,10 @@ type Entity = mutable entity_tycon_repr: TyconRepresentation /// The methods type properties of the type - mutable entity_tycon_tcaug: TyconAugmentation | null + mutable entity_tycon_tcaug: TyconAugmentation /// This field is used when the 'tycon' is really a module definition. It holds statically nested type definitions type nested modules - mutable entity_modul_type: MaybeLazy | null + mutable entity_modul_type: MaybeLazy /// The stable path to the type, e.g. Microsoft.FSharp.Core.FSharpFunc`2 mutable entity_cpath: CompilationPath option diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs index 8228aa108f9..f8564249d03 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs @@ -2299,15 +2299,8 @@ module internal ExprRemapping = tcdR.entity_attribs <- WellKnownEntityAttribs.Create(tcd.entity_attribs.AsList() |> remapAttribs ctxt tmenvinner2) tcdR.entity_tycon_repr <- tcd.entity_tycon_repr |> remapTyconRepr ctxt tmenvinner2 let typeAbbrevR = tcd.TypeAbbrev |> Option.map (remapType tmenvinner2) - tcdR.entity_tycon_tcaug <- remapTyconAug tmenvinner2 (nonNull tcd.entity_tycon_tcaug) - - tcdR.entity_modul_type <- - MaybeLazy.Strict( - (nonNull tcd.entity_modul_type).Value - |> mapImmediateValsAndTycons lookupTycon lookupVal - ) - - + tcdR.entity_tycon_tcaug <- tcd.entity_tycon_tcaug |> remapTyconAug tmenvinner2 + tcdR.entity_modul_type <- MaybeLazy.Strict(tcd.entity_modul_type.Value |> mapImmediateValsAndTycons lookupTycon lookupVal) let exnInfoR = tcd.ExceptionInfo |> remapTyconExnInfo ctxt tmenvinner2 match tcdR.entity_opt_data with diff --git a/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs b/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs index a5fa45bd609..7ee5f290b23 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Transforms.fs @@ -983,11 +983,11 @@ module internal Rewriting = let attribsR = d.entity_attribs.AsList() |> remapAttribs ctxt tmenvinner let tyconReprR = d.entity_tycon_repr |> remapTyconRepr ctxt tmenvinner let tyconAbbrevR = d.TypeAbbrev |> Option.map (remapType tmenvinner) - let tyconTcaugR = remapTyconAug tmenvinner (nonNull d.entity_tycon_tcaug) + let tyconTcaugR = d.entity_tycon_tcaug |> remapTyconAug tmenvinner let modulContentsR = MaybeLazy.Strict( - d.ModuleOrNamespaceType + d.entity_modul_type.Value |> mapImmediateValsAndTycons (remapTyconToNonLocal ctxt tmenv) (remapValToNonLocal ctxt tmenv) ) diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index fe7962fedb0..0a53436e57f 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -2843,7 +2843,7 @@ and p_entity_spec_data (x: Entity) st = p_attribs (x.entity_attribs.AsList()) st let flagBit = p_tycon_repr x.entity_tycon_repr st p_option p_ty x.TypeAbbrev st - p_tcaug (nonNull x.entity_tycon_tcaug) st + p_tcaug x.entity_tycon_tcaug st p_string System.String.Empty st p_kind x.TypeOrMeasureKind st @@ -2856,7 +2856,7 @@ and p_entity_spec_data (x: Entity) st = st p_option p_cpath x.entity_cpath st - p_maybe_lazy p_modul_typ (nonNull x.entity_modul_type) st + p_maybe_lazy p_modul_typ x.entity_modul_type st p_exnc_repr x.ExceptionInfo st if st.oInMem then From 1fa289973cb37586007c33541a46cd2a5de5d438 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 22:35:05 +0200 Subject: [PATCH 067/133] Record file-watching design review vs Roslyn The FileChangeWatcher worktree is pull-model I/O (OpenFileForReadShimAsync plus last-write timestamps on FSharpFileSnapshot), not a push watcher. Roslyn's FileChangeWatcher is the reference: IVsAsyncFileChangeEx2, directory subscriptions, 500 ms AsyncBatchingWorkQueue, free-threaded sinks, coalesced metadata-reference invalidation. This repo already has two IVsFileChangeEx clients (legacy FileChangeManager and deprecated FSharpSource.SetDependencyFiles). The intended FSharp.Editor replacement lives only in stash@{7} (54465595717b8bb746cb2633d5a4aa834888a481): FileChangeWatcher.fs plus FileChangeWatcherHub, wired to FSharpProjectOptionsReactor for -r: assemblies. It is IVsFileChangeEx + JTF.Run, not IVsAsyncFileChangeEx2. No commit, branch, or GitHub hit implements IVsAsyncFileChangeEx2. Recommended split: ship the async read shim on its own; restore the stash watcher or jump straight to IVsAsyncFileChangeEx2 with directory batching; invalidate FCS via NotifyFileChanged instead of O(N) timestamp polling. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/ide/file-watching-design-review.md | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/ide/file-watching-design-review.md diff --git a/docs/ide/file-watching-design-review.md b/docs/ide/file-watching-design-review.md new file mode 100644 index 00000000000..9d5e52a32f7 --- /dev/null +++ b/docs/ide/file-watching-design-review.md @@ -0,0 +1,71 @@ +# F# file watching: design review vs Roslyn + +## Verdict + +The `FileChangeWatcher` working tree is **not** a file-change watcher. It is a pull-model I/O change: + +- `IFileSystem.OpenFileForReadShimAsync` + `Stream.ReadAllTextAsync` +- `FSharpFileSnapshot.CreateFromFileSystem` still versions the file as `GetLastWriteTimeShim(fileName).Ticks` at construction time + +That is useful (it stops blocking the ThreadPool on disk reads) and orthogonal to watching. It is not comparable to Roslyn's `FileChangeWatcher`. + +Roslyn's implementation is a **push** service over `IVsAsyncFileChangeEx2`: + +- directory subscriptions (`WatchedDirectory`) instead of one cookie per file +- `AsyncBatchingWorkQueue` (500 ms) so advise/unadvise is batched and never blocks the UI / thread pool +- free-threaded sinks (`IVsFreeThreadedFileChangeEvents2`) +- coalesced invalidation of metadata references (`FileWatchedReferenceFactory`) + +## What already exists in this repo + +Three layers, none of them `IVsAsyncFileChangeEx2`: + +| Layer | API | Role | +|---|---|---| +| `vsintegration/src/FSharp.ProjectSystem.Base/FileChangeManager.cs` | `IVsFileChangeEx` | Legacy project-system reload of nested items | +| `vsintegration/src/FSharp.LanguageService/FSharpSource.fs` (`SetDependencyFiles`) | `IVsFileChangeEx` | Deprecated unroslynized LS: watch `#r` / dependency files | +| **uncommitted** `stash@{7}` → `vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs` | `IVsFileChangeEx` | Intended modern replacement in `FSharp.Editor` | + +There is **no commit** (on any branch, stash patch, or GitHub search) that implements `IVsAsyncFileChangeEx2`. The work that was remembered as "the async watcher" is the stash below; it uses the older sync advise API, marshalled onto the UI thread. + +## Recovered implementation (`stash@{7}`) + +Stash: `WIP on revert-20080-t-gro-net11-upgrade: 8bf5dca37` + +Index commit that added the file: + +`54465595717b8bb746cb2633d5a4aa834888a481` + +Shape: + +- `IFileChangeWatcher.WatchFile` → `IVsFileChangeEx.AdviseFileChange` / `UnadviseFileChange` +- `FileChangeWatcherHub`: one cookie per path, ref-counted, 500 ms debounce +- Wired into `FSharpProjectOptionsReactor` for `-r:` reference assemblies +- Exposed as `FSharpProjectOptionsManager.WatchFile` so `WorkspaceExtensions` snapshot cache can share the same subscriptions + +This is the right *place* (FSharp.Editor, reference assemblies, debounce, share across projects). It is the wrong *shell API*: + +- `JoinableTaskFactory.Run` + `SwitchToMainThreadAsync` on every advise/unadvise +- no directory watches → N cookies for a NuGet cache +- no batching of subscribe/unsubscribe +- not free-threaded + +## Target design (Roslyn-shaped) + +1. Keep the async read shim. It is independent and should ship on its own. +2. Restore `FileChangeWatcher.fs` from `stash@{7}`, then replace `IVsFileChangeEx` with `IVsAsyncFileChangeEx2`: + - obtain the service asynchronously (same as Roslyn's `Task`) + - queue advise/unadvise on a 500 ms batching work queue; never `JTF.Run` + - subscribe to directories (NuGet cache, output folders) with extension filters; fall back to per-file only for stray paths + - implement `IVsFreeThreadedFileChangeEvents2` so callbacks do not hop to the UI thread +3. On a coalesced change: `checker.NotifyFileChanged` / `InvalidateConfiguration` for the owning project only. Stop O(N) `stat` of reference timestamps on every incremental check. +4. Scripts: watch `#r` / `#load` paths the same way; drop caret-move `NotifyFileChanged`. +5. Do not invent a second watcher. Project system (`FileChangeManager`) and FCS (`TimeStampCache`) should consume this service or stay on their existing contracts. + +## Suggested split + +- PR 1: async `OpenFileForReadShim` (already in the `FileChangeWatcher` worktree). +- PR 2: restore stash watcher as-is (`IVsFileChangeEx`) behind the existing reactor hook — functional, limited. +- PR 3: swap the shell API to `IVsAsyncFileChangeEx2` + directory batching. + +PR 2 is optional if PR 3 is done immediately. From 234b0c16e4d619bf8a7b88b92b0d05dcccf23a36 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 23:31:12 +0200 Subject: [PATCH 068/133] Add IVsAsyncFileChangeEx2 file change watcher adapter Roslyn-shaped push file watching for FSharp.Editor: - FSharpFileChangeWatcher: batched advise/unadvise (500ms window, coalesced same-kind ops), service obtained via Task without blocking on UI thread - FileChangeContext: free-threaded sink (IVsFreeThreadedFileChangeEvents2), directory subscriptions with extension filters, per-file watches covered by watched directories become no-op tokens - FSharpReferenceChangeTracker: ref-counted reference watching with 2s debounce; default directory watches for DOTNET_ROOT\packs, dotnet\packs, Reference Assemblies, NuGet cache (.dll filter) Modeled on Roslyn FileChangeWatcher/ReferenceFileChangeTracker (all internal there, not reusable from F#). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../LanguageService/FileChangeWatcher.fs | 353 ++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 10711c971d0..b9ecfed5036 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -60,6 +60,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs new file mode 100644 index 00000000000..605864f82c7 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Collections.Concurrent +open System.Collections.Generic +open System.Threading +open System.Threading.Tasks +open Microsoft.VisualStudio +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.Interop + +// Push-based file watching for FSharp.Editor, modelled on Roslyn's +// Microsoft.VisualStudio.LanguageServices FileChangeWatcher (which is internal and not +// exposed through ExternalAccess.FSharp). Uses the free-threaded IVsAsyncFileChangeEx2 +// service: subscriptions are batched off the UI thread and callbacks never marshal to it. + +/// A directory to watch recursively (with optional extension filters) so that individual +/// files under it don't each need their own advise cookie. +[] +type internal WatchedDirectory(path: string, extensionFilters: string list) = + let path = + if path.EndsWith(string IO.Path.DirectorySeparatorChar) then + path + else + path + string IO.Path.DirectorySeparatorChar + + do + for filter in extensionFilters do + if not (filter.StartsWith ".") then + invalidArg (nameof extensionFilters) $"Filter '{filter}' must start with a period." + + member _.Path = path + member _.ExtensionFilters = extensionFilters + + static member FilePathCoveredByWatchedDirectories(watchedDirectories: WatchedDirectory list, filePath: string) = + watchedDirectories + |> List.exists (fun w -> + filePath.StartsWith(w.Path, StringComparison.OrdinalIgnoreCase) + && (w.ExtensionFilters.IsEmpty + || w.ExtensionFilters + |> List.exists (fun f -> filePath.EndsWith(f, StringComparison.OrdinalIgnoreCase)))) + +/// A single watched file; disposing stops watching. +type internal IFSharpWatchedFile = + inherit IDisposable + +/// A group of file/directory watches sharing one event sink. Disposing unsubscribes everything. +type internal IFSharpFileChangeContext = + inherit IDisposable + + [] + abstract FileChanged: IEvent + + /// Starts watching a file without waiting for the OS registration. No-op (but still valid + /// to dispose) when the path is already covered by one of the context's watched directories. + abstract EnqueueWatchingFile: filePath: string -> IFSharpWatchedFile + +type internal IFSharpFileChangeWatcher = + abstract CreateContext: watchedDirectories: WatchedDirectory list -> IFSharpFileChangeContext + +[] +module private FileChangeWatcherImpl = + + // Same flags Roslyn uses for both subscribing and filtering callbacks. + let watchFlags = _VSFILECHANGEFLAGS.VSFILECHG_Size ||| _VSFILECHANGEFLAGS.VSFILECHG_Time + + let relevantFlags = + _VSFILECHANGEFLAGS.VSFILECHG_Time + ||| _VSFILECHANGEFLAGS.VSFILECHG_Add + ||| _VSFILECHANGEFLAGS.VSFILECHG_Del + ||| _VSFILECHANGEFLAGS.VSFILECHG_Size + + /// Empirically strong batching window during high activity (solution open/close); see + /// Roslyn's FileChangeWatcher. + let batchingDelay = TimeSpan.FromMilliseconds 500. + +[] +type internal FSharpWatchedFileToken() = + member val Cookie: uint32 option = None with get, set + +/// Subscription operations queued for batched application against the file change service. +type private WatcherOperation = + | WatchDir of path: string * filters: string list * sink: IVsFreeThreadedFileChangeEvents2 * cookies: List + | WatchFiles of paths: string list * tokens: FSharpWatchedFileToken list * sink: IVsFreeThreadedFileChangeEvents2 + | UnwatchFiles of tokens: FSharpWatchedFileToken list + | UnwatchDirs of cookies: List + +[] +type internal FSharpFileChangeWatcher(fileChangeService: Task) = + + let applyBatch (service: IVsAsyncFileChangeEx2) (ops: WatcherOperation list) = + task { + // Coalesce adjacent same-kind operations into single service calls, preserving order + // between kinds (a watch enqueued before an unwatch must be applied first). + let mutable pending = ops + + while not pending.IsEmpty do + match pending with + | [] -> () + | WatchDir(path, filters, sink, cookies) :: rest -> + pending <- rest + let! cookie = service.AdviseDirChangeAsync(path, true, sink, CancellationToken.None) + cookies.Add cookie + + if not filters.IsEmpty then + do! service.FilterDirectoryChangesAsync(cookie, List.toArray filters, CancellationToken.None) + + | WatchFiles _ :: _ -> + let batch = pending |> List.takeWhile (function WatchFiles _ -> true | _ -> false) + pending <- pending |> List.skip batch.Length + + let paths = batch |> List.collect (function WatchFiles(p, _, _) -> p | _ -> []) + let tokens = batch |> List.collect (function WatchFiles(_, t, _) -> t | _ -> []) + let sink = batch |> List.pick (function WatchFiles(_, _, s) -> Some s | _ -> None) + + let! cookies = service.AdviseFileChangesAsync(List.toArray paths, watchFlags, sink, CancellationToken.None) + + (tokens, List.ofArray cookies) + ||> List.iter2 (fun token cookie -> token.Cookie <- Some cookie) + + | UnwatchFiles _ :: _ -> + let batch = pending |> List.takeWhile (function UnwatchFiles _ -> true | _ -> false) + pending <- pending |> List.skip batch.Length + + let cookies = + batch + |> List.collect (function UnwatchFiles t -> t | _ -> []) + |> List.choose (fun token -> token.Cookie) + + if not cookies.IsEmpty then + let! _ = service.UnadviseFileChangesAsync(List.toArray cookies, CancellationToken.None) + () + + | UnwatchDirs cookies :: rest -> + pending <- rest + + if cookies.Count > 0 then + let! _ = service.UnadviseDirChangesAsync(cookies.ToArray(), CancellationToken.None) + () + } + + // Single consumer loop: waits for the first queued operation, sleeps out the batching + // window, drains the queue and applies everything in one pass. Nothing ever blocks on the + // service being available. + let agent = + MailboxProcessor.Start(fun inbox -> + async { + while true do + try + let! first = inbox.Receive() + do! Async.Sleep(int batchingDelay.TotalMilliseconds) + + let ops = ResizeArray [ first ] + let mutable draining = true + + while draining do + match! inbox.TryReceive 0 with + | Some op -> ops.Add op + | None -> draining <- false + + let! service = fileChangeService |> Async.AwaitTask + do! applyBatch service (List.ofSeq ops) |> Async.AwaitTask + with _ -> + // Never let a failed advise/unadvise (e.g. non-existent path) kill the + // subscription loop; we simply won't get events for that path. + () + }) + + member private _.Enqueue(op: WatcherOperation) = agent.Post op + + /// Production factory: obtains SVsFileChangeEx asynchronously without blocking any + /// background thread on UI-thread availability. + static member CreateDefaultServiceTask() = + task { + let! service = AsyncServiceProvider.GlobalProvider.GetServiceAsync(typeof) + return service :?> IVsAsyncFileChangeEx2 + } + + interface IFSharpFileChangeWatcher with + member _.CreateContext(watchedDirectories) = + new FileChangeContext(agent.Post, watchedDirectories) :> IFSharpFileChangeContext + +and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watchedDirectories: WatchedDirectory list) as this = + + let gate = obj () + let mutable disposed = false + let activeFileTokens = HashSet() + let directoryCookies = List() + let fileChanged = Event() + + let raiseChanges (count: uint32) (files: string[]) (changeFlags: uint32[]) = + for i in 0 .. int count - 1 do + if (enum<_VSFILECHANGEFLAGS> (int changeFlags[i]) &&& relevantFlags) <> enum<_VSFILECHANGEFLAGS> 0 then + fileChanged.Trigger files[i] + + VSConstants.S_OK + + do + for watchedDirectory in watchedDirectories do + enqueue ( + WatchDir( + watchedDirectory.Path, + watchedDirectory.ExtensionFilters, + this :> IVsFreeThreadedFileChangeEvents2, + directoryCookies + ) + ) + + member private _.StopWatchingFile(token: FSharpWatchedFileToken) = + lock gate (fun () -> activeFileTokens.Remove token |> ignore) + enqueue (UnwatchFiles [ token ]) + + interface IFSharpFileChangeContext with + [] + member _.FileChanged = fileChanged.Publish + + member _.EnqueueWatchingFile filePath = + if WatchedDirectory.FilePathCoveredByWatchedDirectories(watchedDirectories, filePath) then + // Covered by a directory watch; nothing extra to subscribe. + { new IFSharpWatchedFile with + member _.Dispose() = () + } + else + let token = FSharpWatchedFileToken() + lock gate (fun () -> activeFileTokens.Add token |> ignore) + enqueue (WatchFiles([ filePath ], [ token ], this :> IVsFreeThreadedFileChangeEvents2)) + + { new IFSharpWatchedFile with + member _.Dispose() = this.StopWatchingFile token + } + + interface IDisposable with + member _.Dispose() = + let alreadyDisposed = lock gate (fun () -> + let d = disposed + disposed <- true + d) + + if not alreadyDisposed then + enqueue (UnwatchDirs directoryCookies) + enqueue (UnwatchFiles(lock gate (fun () -> List.ofSeq activeFileTokens))) + + // Free-threaded sink: callbacks arrive on background threads and stay there. + interface IVsFreeThreadedFileChangeEvents2 with + member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + member _.DirectoryChanged _ = VSConstants.E_NOTIMPL + member _.DirectoryChangedEx(_, _) = VSConstants.E_NOTIMPL + member _.DirectoryChangedEx2(_, cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + + interface IVsFreeThreadedFileChangeEvents with + member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + member _.DirectoryChanged _ = VSConstants.E_NOTIMPL + member _.DirectoryChangedEx(_, _) = VSConstants.E_NOTIMPL + + interface IVsFileChangeEvents with + member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + member _.DirectoryChanged _ = VSConstants.E_NOTIMPL + +/// Ref-counted, debounced watching of reference assemblies (or any other off-workspace files), +/// modelled on Roslyn's ReferenceFileChangeTracker. Multiple projects watching the same dll +/// share one subscription; bursts of writes produce a single callback per path. +[] +type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, onChanged: string -> unit) = + + /// Delay between the last observed change to a path and the callback: a rebuild typically + /// writes a temp file then renames, producing several rapid notifications. + static let notificationDelay = TimeSpan.FromSeconds 2. + + let gate = obj () + let mutable disposed = false + let watchedFiles = Dictionary(StringComparer.OrdinalIgnoreCase) + let pendingTimers = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + + // On each platform there is a place framework reference assemblies live; these rarely change + // but account for most watched paths, so cover them with directory watches up front. + static let defaultWatchedDirectories () = + let dotnetRoot = Environment.GetEnvironmentVariable "DOTNET_ROOT" + + [ + if not (String.IsNullOrEmpty dotnetRoot) then + IO.Path.Combine(dotnetRoot, "packs") + + IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.ProgramFiles, "dotnet", "packs") + + IO.Path.Combine( + Environment.GetFolderPath Environment.SpecialFolder.ProgramFilesX86, + "Reference Assemblies", + "Microsoft", + "Framework" + ) + + IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.UserProfile, ".nuget", "packages") + ] + |> List.distinct + |> List.map (fun d -> WatchedDirectory(d, [ ".dll" ])) + + let context = + lazy + (let ctx = watcher.CreateContext(defaultWatchedDirectories ()) + + ctx.FileChanged.Add(fun path -> + let fire (_: obj) = + pendingTimers.TryRemove path + |> function + | true, timer -> timer.Dispose() + | _ -> () + + // Only notify for paths someone is actually watching; directory watches + // cover whole trees. + let isWatched = lock gate (fun () -> watchedFiles.ContainsKey path) + + if isWatched then + onChanged path + + let timer = pendingTimers.GetOrAdd(path, fun _ -> new Timer(fire, null, Timeout.Infinite, Timeout.Infinite)) + timer.Change(notificationDelay, Timeout.InfiniteTimeSpan) |> ignore) + + ctx) + + /// Starts watching a path, ref-counted. Call StopWatchingReference exactly once per start. + member _.StartWatchingReference(fullFilePath: string) = + lock gate (fun () -> + if not disposed then + match watchedFiles.TryGetValue fullFilePath with + | true, (token, count) -> watchedFiles[fullFilePath] <- (token, count + 1) + | _ -> watchedFiles[fullFilePath] <- (context.Value.EnqueueWatchingFile fullFilePath, 1)) + + member _.StopWatchingReference(fullFilePath: string) = + lock gate (fun () -> + if not disposed then + match watchedFiles.TryGetValue fullFilePath with + | true, (token, 1) -> + watchedFiles.Remove fullFilePath |> ignore + token.Dispose() + | true, (token, count) -> watchedFiles[fullFilePath] <- (token, count - 1) + | _ -> ()) + + interface IDisposable with + member _.Dispose() = + lock gate (fun () -> + if not disposed then + disposed <- true + watchedFiles.Clear() + + for KeyValue(_, timer) in pendingTimers do + timer.Dispose() + + pendingTimers.Clear() + + if context.IsValueCreated then + context.Value.Dispose()) From 8af80f0e9dd6604af449cca1f0cb3c1a77f3beb8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 23:36:03 +0200 Subject: [PATCH 069/133] Wire reference file watching into FSharpProjectOptionsReactor Subscribe each project's on-disk '-r:' reference assemblies via FSharpReferenceChangeTracker when options are computed; on a watched dll change, drop that project's cached options and invalidate the checker configuration. Subscriptions are ref-counted, cleared on project removal and reactor disposal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../FSharpProjectOptionsManager.fs | 51 +++++++++++++++++-- .../LanguageService/LanguageService.fs | 6 ++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 04cad802278..3d9ed40c6b6 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -141,7 +141,7 @@ type private FSharpProjectOptionsMessage = | ClearSingleFileOptionsCache of DocumentId [] -type private FSharpProjectOptionsReactor(checker: FSharpChecker) = +type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatcher: IFSharpFileChangeWatcher option) = let cancellationTokenSource = new CancellationTokenSource() // Store command line options @@ -153,6 +153,41 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let cache = ConcurrentDictionary() + // Push invalidation for on-disk '-r:' reference assemblies (not tracked by the Roslyn + // workspace): when one changes after an external rebuild, drop the cached options of every + // project referencing it instead of waiting for a timestamp poll to notice. + let referenceWatches = ConcurrentDictionary() + + let onWatchedReferenceChanged (path: string) = + for KeyValue(projectId, paths) in referenceWatches do + if paths |> Array.exists (fun p -> String.Equals(p, path, StringComparison.OrdinalIgnoreCase)) then + match cache.TryRemove projectId with + | true, struct (_, _, projectOptions) -> checker.InvalidateConfiguration(projectOptions, userOpName = "onWatchedReferenceChanged") + | _ -> () + + let referenceChangeTracker = + fileChangeWatcher + |> Option.map (fun watcher -> new FSharpReferenceChangeTracker(watcher, onWatchedReferenceChanged)) + + let clearReferenceWatches (projectId: ProjectId) = + match referenceWatches.TryRemove projectId, referenceChangeTracker with + | (true, paths), Some tracker -> paths |> Array.iter (fun p -> tracker.StopWatchingReference p) + | _ -> () + + let watchReferenceFiles (projectId: ProjectId) (projectOptions: FSharpProjectOptions) = + referenceChangeTracker + |> Option.iter (fun tracker -> + clearReferenceWatches projectId + + let paths = + projectOptions.OtherOptions + |> Array.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) + |> Array.map (fun x -> x.Substring "-r:".Length) + + if paths.Length > 0 then + paths |> Array.iter (fun p -> tracker.StartWatchingReference p) + referenceWatches[projectId] <- paths) + let singleFileCache = ConcurrentDictionary() let weakPEReferences = ConditionalWeakTable() @@ -520,6 +555,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = cache.[projectId] <- struct (project, parsingOptions, projectOptions) + watchReferenceFiles projectId projectOptions + return ValueSome struct (parsingOptions, projectOptions) | true, struct (oldProject, parsingOptions, projectOptions) -> @@ -607,6 +644,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | _ -> () legacyProjectSites.TryRemove(projectId) |> ignore + clearReferenceWatches projectId | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> match singleFileCache.TryRemove(documentId) with | true, ({ ProjectOptions = projectOptions } as cacheEntry) -> @@ -656,19 +694,26 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = lastSuccessfulCompilations.Clear() + for projectId in referenceWatches.Keys |> Array.ofSeq do + clearReferenceWatches projectId + member _.ScriptUpdated = scriptUpdatedEvent.Publish interface IDisposable with member this.Dispose() = this.ClearAllCaches() + + referenceChangeTracker + |> Option.iter (fun tracker -> (tracker :> IDisposable).Dispose()) + cancellationTokenSource.Cancel() cancellationTokenSource.Dispose() (agent :> IDisposable).Dispose() /// Manages mappings of Roslyn workspace Projects/Documents to FCS. -type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Workspace) = +type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Workspace, ?fileChangeWatcher: IFSharpFileChangeWatcher) = - let reactor = new FSharpProjectOptionsReactor(checker) + let reactor = new FSharpProjectOptionsReactor(checker, fileChangeWatcher) do // We need to listen to this event for lifecycle purposes. diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 68bb0b038e7..172f3d0f513 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -209,7 +209,11 @@ type internal FSharpWorkspaceServiceFactory |> CancellableTask.startAsTask CancellationToken.None |> ignore) - let optionsManager = FSharpProjectOptionsManager(checker, workspace) + let fileChangeWatcher = + FSharpFileChangeWatcher(FSharpFileChangeWatcher.CreateDefaultServiceTask()) + + let optionsManager = + FSharpProjectOptionsManager(checker, workspace, fileChangeWatcher) { new IFSharpWorkspaceService with member _.Checker = checker From f9bac56c3d3c7458fe9ef91e4e72d897140bb340 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 23:41:26 +0200 Subject: [PATCH 070/133] Push-based invalidation for script references and snapshot reference stamps Scripts: subscribe each single-file/script project's '-r:' references via the change tracker; a changed dll drops that document's cached options instead of waiting for recomputation triggers. Snapshots: ReferenceStampCache serves GetLastWriteTimeUtc for on-disk references from a dictionary invalidated point-wise by watcher notifications, replacing a stat of every reference on each snapshot comparison. Falls back to direct stat when watching is not active. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../FSharpProjectOptionsManager.fs | 78 +++++++++++++++---- .../LanguageService/FileChangeWatcher.fs | 18 +++++ .../LanguageService/WorkspaceExtensions.fs | 2 +- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 3d9ed40c6b6..ee5b16eee67 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -153,25 +153,66 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let cache = ConcurrentDictionary() + let singleFileCache = ConcurrentDictionary() + + let weakPEReferences = ConditionalWeakTable() + let lastSuccessfulCompilations = ConcurrentDictionary() + + let scriptUpdatedEvent = Event() + + let disposeSingleFileCacheEntry ({ Subscription = subscription }: SingleFileCacheEntry) = + subscription |> ValueOption.iter (fun subscription -> subscription.Dispose()) + // Push invalidation for on-disk '-r:' reference assemblies (not tracked by the Roslyn // workspace): when one changes after an external rebuild, drop the cached options of every - // project referencing it instead of waiting for a timestamp poll to notice. + // project and script referencing it instead of waiting for a timestamp poll to notice. let referenceWatches = ConcurrentDictionary() + let scriptReferenceWatches = ConcurrentDictionary() + + let referencePaths (projectOptions: FSharpProjectOptions) = + projectOptions.OtherOptions + |> Array.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) + |> Array.map (fun x -> x.Substring "-r:".Length) let onWatchedReferenceChanged (path: string) = + ReferenceStampCache.Invalidate path + + let isWatched paths = + paths + |> Array.exists (fun p -> String.Equals(p, path, StringComparison.OrdinalIgnoreCase)) + for KeyValue(projectId, paths) in referenceWatches do - if paths |> Array.exists (fun p -> String.Equals(p, path, StringComparison.OrdinalIgnoreCase)) then + if isWatched paths then match cache.TryRemove projectId with - | true, struct (_, _, projectOptions) -> checker.InvalidateConfiguration(projectOptions, userOpName = "onWatchedReferenceChanged") + | true, struct (_, _, projectOptions) -> + checker.InvalidateConfiguration(projectOptions, userOpName = "onWatchedReferenceChanged") + | _ -> () + + for KeyValue(documentId, paths) in scriptReferenceWatches do + if isWatched paths then + match singleFileCache.TryRemove documentId with + | true, ({ ProjectOptions = projectOptions } as entry) -> + disposeSingleFileCacheEntry entry + checker.ClearCache([ projectOptions ]) | _ -> () let referenceChangeTracker = fileChangeWatcher - |> Option.map (fun watcher -> new FSharpReferenceChangeTracker(watcher, onWatchedReferenceChanged)) + |> Option.map (fun watcher -> + ReferenceStampCache.Enable() + new FSharpReferenceChangeTracker(watcher, onWatchedReferenceChanged)) + + let stopWatching (tracker: FSharpReferenceChangeTracker) paths = + paths |> Array.iter (fun p -> tracker.StopWatchingReference p) let clearReferenceWatches (projectId: ProjectId) = match referenceWatches.TryRemove projectId, referenceChangeTracker with - | (true, paths), Some tracker -> paths |> Array.iter (fun p -> tracker.StopWatchingReference p) + | (true, paths), Some tracker -> stopWatching tracker paths + | _ -> () + + let clearScriptReferenceWatches (documentId: DocumentId) = + match scriptReferenceWatches.TryRemove documentId, referenceChangeTracker with + | (true, paths), Some tracker -> stopWatching tracker paths | _ -> () let watchReferenceFiles (projectId: ProjectId) (projectOptions: FSharpProjectOptions) = @@ -179,24 +220,22 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch |> Option.iter (fun tracker -> clearReferenceWatches projectId - let paths = - projectOptions.OtherOptions - |> Array.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) - |> Array.map (fun x -> x.Substring "-r:".Length) + let paths = referencePaths projectOptions if paths.Length > 0 then paths |> Array.iter (fun p -> tracker.StartWatchingReference p) referenceWatches[projectId] <- paths) - let singleFileCache = ConcurrentDictionary() - - let weakPEReferences = ConditionalWeakTable() - let lastSuccessfulCompilations = ConcurrentDictionary() + let watchScriptReferences (documentId: DocumentId) (projectOptions: FSharpProjectOptions) = + referenceChangeTracker + |> Option.iter (fun tracker -> + clearScriptReferenceWatches documentId - let scriptUpdatedEvent = Event() + let paths = referencePaths projectOptions - let disposeSingleFileCacheEntry ({ Subscription = subscription }: SingleFileCacheEntry) = - subscription |> ValueOption.iter (fun subscription -> subscription.Dispose()) + if paths.Length > 0 then + paths |> Array.iter (fun p -> tracker.StartWatchingReference p) + scriptReferenceWatches[documentId] <- paths) let createNewPEReference (referencedProject: Project) (comp: Compilation) = let projectId = referencedProject.Id @@ -412,6 +451,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch ) |> ignore + watchScriptReferences document.Id projectOptions + return ValueSome struct (parsingOptions, projectOptions) | true, @@ -653,6 +694,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch disposeSingleFileCacheEntry cacheEntry | _ -> () + clearScriptReferenceWatches documentId + } let agent = @@ -697,6 +740,9 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch for projectId in referenceWatches.Keys |> Array.ofSeq do clearReferenceWatches projectId + for documentId in scriptReferenceWatches.Keys |> Array.ofSeq do + clearScriptReferenceWatches documentId + member _.ScriptUpdated = scriptUpdatedEvent.Publish interface IDisposable with diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index 605864f82c7..8dd95e8cb60 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -60,6 +60,24 @@ type internal IFSharpFileChangeContext = type internal IFSharpFileChangeWatcher = abstract CreateContext: watchedDirectories: WatchedDirectory list -> IFSharpFileChangeContext +/// Process-wide cache of on-disk reference timestamps. Enabled when push-based file watching +/// is active: watched paths are invalidated point-wise by change notifications, so repeated +/// snapshot creation reads a dictionary instead of stat'ing every '-r:' reference again. +module internal ReferenceStampCache = + let private stamps = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + + let mutable private enabled = false + + let Enable () = enabled <- true + + let Invalidate (path: string) = stamps.TryRemove path |> ignore + + let GetLastWriteTimeUtc (path: string) = + if enabled then + stamps.GetOrAdd(path, (fun p -> IO.File.GetLastWriteTimeUtc p)) + else + IO.File.GetLastWriteTimeUtc path + [] module private FileChangeWatcherImpl = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index f438f8aae4c..567d88a9135 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -246,7 +246,7 @@ module private CheckerExtensions = { Path = path - LastModified = System.IO.File.GetLastWriteTimeUtc path + LastModified = ReferenceStampCache.GetLastWriteTimeUtc path }) |> Seq.toList From 97dd66ba6eb4c7a36a31dc70eb78d049a4f6cbe4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 18 Aug 2026 00:01:53 +0200 Subject: [PATCH 071/133] Add FileChangeWatcher unit tests and VS release notes Cover WatchedDirectory path matching, tracker ref-counting, debounce of burst notifications, and dispose. Tests use an in-memory IFSharpFileChangeWatcher mock so they do not need a live IVsAsyncFileChangeEx2 service. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../LanguageService/FileChangeWatcher.fs | 90 +++++++++---- .../FSharp.Editor.Tests.fsproj | 1 + .../FileChangeWatcherTests.fs | 121 ++++++++++++++++++ 4 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 4361a1da23f..96e179afd39 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -14,6 +14,7 @@ * Prevent stale and leaked per-document diagnostics by tightening cache validity and evicting entries for removed documents. ([PR #20121](https://github.com/dotnet/fsharp/pull/20121)) * Avoid blocking the VS project-options mailbox and unnecessary script option recomputation while editing by making invalidation async and debouncing caret-driven updates. ([Issue #20124](https://github.com/dotnet/fsharp/issues/20124), [Issue #20125](https://github.com/dotnet/fsharp/issues/20125), [PR #20126](https://github.com/dotnet/fsharp/pull/20126)) * Cache document diagnostics by version stamp, so an unchanged document is not reanalyzed on every crawler pass. ([Issue #20120](https://github.com/dotnet/fsharp/issues/20120), [PR #20121](https://github.com/dotnet/fsharp/pull/20121)) +* Watch on-disk `-r:` reference assemblies via `IVsAsyncFileChangeEx2` so F# project options and snapshots invalidate on rebuild instead of polling timestamps. * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index 8dd95e8cb60..c50b1bf72e0 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -64,7 +64,8 @@ type internal IFSharpFileChangeWatcher = /// is active: watched paths are invalidated point-wise by change notifications, so repeated /// snapshot creation reads a dictionary instead of stat'ing every '-r:' reference again. module internal ReferenceStampCache = - let private stamps = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + let private stamps = + ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) let mutable private enabled = false @@ -82,7 +83,8 @@ module internal ReferenceStampCache = module private FileChangeWatcherImpl = // Same flags Roslyn uses for both subscribing and filtering callbacks. - let watchFlags = _VSFILECHANGEFLAGS.VSFILECHG_Size ||| _VSFILECHANGEFLAGS.VSFILECHG_Time + let watchFlags = + _VSFILECHANGEFLAGS.VSFILECHG_Size ||| _VSFILECHANGEFLAGS.VSFILECHG_Time let relevantFlags = _VSFILECHANGEFLAGS.VSFILECHG_Time @@ -126,12 +128,31 @@ type internal FSharpFileChangeWatcher(fileChangeService: Task - let batch = pending |> List.takeWhile (function WatchFiles _ -> true | _ -> false) + let batch = + pending + |> List.takeWhile (function + | WatchFiles _ -> true + | _ -> false) + pending <- pending |> List.skip batch.Length - let paths = batch |> List.collect (function WatchFiles(p, _, _) -> p | _ -> []) - let tokens = batch |> List.collect (function WatchFiles(_, t, _) -> t | _ -> []) - let sink = batch |> List.pick (function WatchFiles(_, _, s) -> Some s | _ -> None) + let paths = + batch + |> List.collect (function + | WatchFiles(p, _, _) -> p + | _ -> []) + + let tokens = + batch + |> List.collect (function + | WatchFiles(_, t, _) -> t + | _ -> []) + + let sink = + batch + |> List.pick (function + | WatchFiles(_, _, s) -> Some s + | _ -> None) let! cookies = service.AdviseFileChangesAsync(List.toArray paths, watchFlags, sink, CancellationToken.None) @@ -139,12 +160,19 @@ type internal FSharpFileChangeWatcher(fileChangeService: Task List.iter2 (fun token cookie -> token.Cookie <- Some cookie) | UnwatchFiles _ :: _ -> - let batch = pending |> List.takeWhile (function UnwatchFiles _ -> true | _ -> false) + let batch = + pending + |> List.takeWhile (function + | UnwatchFiles _ -> true + | _ -> false) + pending <- pending |> List.skip batch.Length let cookies = batch - |> List.collect (function UnwatchFiles t -> t | _ -> []) + |> List.collect (function + | UnwatchFiles t -> t + | _ -> []) |> List.choose (fun token -> token.Cookie) if not cookies.IsEmpty then @@ -210,7 +238,10 @@ and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watc let raiseChanges (count: uint32) (files: string[]) (changeFlags: uint32[]) = for i in 0 .. int count - 1 do - if (enum<_VSFILECHANGEFLAGS> (int changeFlags[i]) &&& relevantFlags) <> enum<_VSFILECHANGEFLAGS> 0 then + if + (enum<_VSFILECHANGEFLAGS> (int changeFlags[i]) &&& relevantFlags) + <> enum<_VSFILECHANGEFLAGS> 0 + then fileChanged.Trigger files[i] VSConstants.S_OK @@ -251,10 +282,11 @@ and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watc interface IDisposable with member _.Dispose() = - let alreadyDisposed = lock gate (fun () -> - let d = disposed - disposed <- true - d) + let alreadyDisposed = + lock gate (fun () -> + let d = disposed + disposed <- true + d) if not alreadyDisposed then enqueue (UnwatchDirs directoryCookies) @@ -262,34 +294,46 @@ and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watc // Free-threaded sink: callbacks arrive on background threads and stay there. interface IVsFreeThreadedFileChangeEvents2 with - member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = + raiseChanges cChanges rgpszFile rggrfChange + member _.DirectoryChanged _ = VSConstants.E_NOTIMPL member _.DirectoryChangedEx(_, _) = VSConstants.E_NOTIMPL - member _.DirectoryChangedEx2(_, cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + + member _.DirectoryChangedEx2(_, cChanges, rgpszFile, rggrfChange) = + raiseChanges cChanges rgpszFile rggrfChange interface IVsFreeThreadedFileChangeEvents with - member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = + raiseChanges cChanges rgpszFile rggrfChange + member _.DirectoryChanged _ = VSConstants.E_NOTIMPL member _.DirectoryChangedEx(_, _) = VSConstants.E_NOTIMPL interface IVsFileChangeEvents with - member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = raiseChanges cChanges rgpszFile rggrfChange + member _.FilesChanged(cChanges, rgpszFile, rggrfChange) = + raiseChanges cChanges rgpszFile rggrfChange + member _.DirectoryChanged _ = VSConstants.E_NOTIMPL /// Ref-counted, debounced watching of reference assemblies (or any other off-workspace files), /// modelled on Roslyn's ReferenceFileChangeTracker. Multiple projects watching the same dll /// share one subscription; bursts of writes produce a single callback per path. [] -type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, onChanged: string -> unit) = +type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, onChanged: string -> unit, ?notificationDelay: TimeSpan) = /// Delay between the last observed change to a path and the callback: a rebuild typically /// writes a temp file then renames, producing several rapid notifications. - static let notificationDelay = TimeSpan.FromSeconds 2. + let notificationDelay = defaultArg notificationDelay (TimeSpan.FromSeconds 2.) let gate = obj () let mutable disposed = false - let watchedFiles = Dictionary(StringComparer.OrdinalIgnoreCase) - let pendingTimers = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + + let watchedFiles = + Dictionary(StringComparer.OrdinalIgnoreCase) + + let pendingTimers = + ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) // On each platform there is a place framework reference assemblies live; these rarely change // but account for most watched paths, so cover them with directory watches up front. @@ -332,7 +376,9 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on if isWatched then onChanged path - let timer = pendingTimers.GetOrAdd(path, fun _ -> new Timer(fire, null, Timeout.Infinite, Timeout.Infinite)) + let timer = + pendingTimers.GetOrAdd(path, fun _ -> new Timer(fire, null, Timeout.Infinite, Timeout.Infinite)) + timer.Change(notificationDelay, Timeout.InfiniteTimeSpan) |> ignore) ctx) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 9ca2a4e7960..dbd8eca3fcc 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -83,6 +83,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs new file mode 100644 index 00000000000..829cfd68226 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Editor.Tests + +open System +open System.Threading +open Xunit +open Microsoft.VisualStudio.FSharp.Editor + +type private MockFileChangeContext() = + let fileChanged = Event() + let watched = ResizeArray() + + member _.WatchedFiles = List.ofSeq watched + member _.Fire path = fileChanged.Trigger path + + interface IFSharpFileChangeContext with + [] + member _.FileChanged = fileChanged.Publish + + member _.EnqueueWatchingFile path = + watched.Add path + + { new IFSharpWatchedFile with + member _.Dispose() = watched.Remove path |> ignore + } + + member _.Dispose() = watched.Clear() + +type private MockFileChangeWatcher() = + let mutable context: MockFileChangeContext option = None + + member _.Context = context + + interface IFSharpFileChangeWatcher with + member _.CreateContext _ = + let ctx = new MockFileChangeContext() + context <- Some ctx + ctx :> IFSharpFileChangeContext + +module FileChangeWatcherTests = + + let private testDelay = TimeSpan.FromMilliseconds 50. + + [] + let ``WatchedDirectory covers files under it matching the extension filter`` () = + let dirs = [ WatchedDirectory(@"C:\refs", [ ".dll" ]) ] + + Assert.True(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refs\sub\a.dll")) + Assert.True(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\REFS\A.DLL")) + Assert.False(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refs\a.xml")) + Assert.False(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\other\a.dll")) + + [] + let ``WatchedDirectory without filters covers any file under it`` () = + let dirs = [ WatchedDirectory(@"C:\refs", []) ] + + Assert.True(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refs\a.xml")) + Assert.False(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refsx\a.xml")) + + [] + let ``Tracker ref-counts subscriptions per path`` () = + let watcher = MockFileChangeWatcher() + use tracker = new FSharpReferenceChangeTracker(watcher, ignore, testDelay) + + tracker.StartWatchingReference @"C:\x\a.dll" + tracker.StartWatchingReference @"C:\x\a.dll" + tracker.StartWatchingReference @"C:\x\b.dll" + + Assert.Equal([ @"C:\x\a.dll"; @"C:\x\b.dll" ], watcher.Context.Value.WatchedFiles) + + tracker.StopWatchingReference @"C:\x\a.dll" + Assert.Contains(@"C:\x\a.dll", watcher.Context.Value.WatchedFiles) + + tracker.StopWatchingReference @"C:\x\a.dll" + Assert.Equal([ @"C:\x\b.dll" ], watcher.Context.Value.WatchedFiles) + + [] + let ``Tracker debounces bursts into a single callback for watched paths only`` () = + let watcher = MockFileChangeWatcher() + let calls = ResizeArray() + use signal = new ManualResetEventSlim(false) + + use tracker = + new FSharpReferenceChangeTracker( + watcher, + (fun path -> + lock calls (fun () -> calls.Add path) + signal.Set()), + testDelay + ) + + tracker.StartWatchingReference @"C:\x\a.dll" + let context = watcher.Context.Value + + context.Fire @"C:\x\a.dll" + context.Fire @"C:\x\a.dll" + context.Fire @"C:\x\unwatched.dll" + + Assert.True(signal.Wait(TimeSpan.FromSeconds 10.)) + // Allow a trailing duplicate timer to fire if one was pending. + Thread.Sleep(testDelay + testDelay) + + Assert.Equal([ @"C:\x\a.dll" ], lock calls (fun () -> List.ofSeq calls)) + + [] + let ``Disposed tracker ignores further changes`` () = + let watcher = MockFileChangeWatcher() + let mutable called = false + + let tracker = + new FSharpReferenceChangeTracker(watcher, (fun _ -> called <- true), testDelay) + + tracker.StartWatchingReference @"C:\x\a.dll" + let context = watcher.Context.Value + (tracker :> IDisposable).Dispose() + + context.Fire @"C:\x\a.dll" + Thread.Sleep(testDelay + testDelay) + + Assert.False called From 4d62aef6589f1d8f9f9cec777ec81ca30e9f685f Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 18 Aug 2026 00:32:04 +0200 Subject: [PATCH 072/133] Watch script #load sources via the reference change tracker Script options already subscribed -r: assemblies. Also watch SourceFiles so an external edit to a #load file drops the single-file cache instead of waiting for the next caret/focus recompute. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../LanguageService/FSharpProjectOptionsManager.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index ee5b16eee67..2e1d6e14529 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -231,7 +231,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch |> Option.iter (fun tracker -> clearScriptReferenceWatches documentId - let paths = referencePaths projectOptions + // Scripts also pull in #load sources that are not Roslyn documents. + let paths = + Array.append (referencePaths projectOptions) projectOptions.SourceFiles + |> Array.distinct if paths.Length > 0 then paths |> Array.iter (fun p -> tracker.StartWatchingReference p) From 55f0b2afb4862350eec1c093fd8ff3b2b0dfa9ef Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 18 Aug 2026 15:09:36 +0200 Subject: [PATCH 073/133] Use `voption` for perf-critical option values * Replaced F# option with voption in hot paths (`DelayedILModuleReader`, `FSharpProjectOptionsManager`, `FSharpCheckerResults` APIs, caches, and reactors). * Updated pattern matches, return types, and tests to use `ValueSome`/`ValueNone`. * Refactored event logic and removes unnecessary `Option` wrappers. * Constructors now require non-optional `IFSharpFileChangeWatcher`. * Minor cleanup and doc updates. --- .../FSharpProjectOptionsManager.fs | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 2e1d6e14529..63cc7f73bbe 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -141,7 +141,7 @@ type private FSharpProjectOptionsMessage = | ClearSingleFileOptionsCache of DocumentId [] -type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatcher: IFSharpFileChangeWatcher option) = +type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatcher: IFSharpFileChangeWatcher) = let cancellationTokenSource = new CancellationTokenSource() // Store command line options @@ -171,8 +171,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let referencePaths (projectOptions: FSharpProjectOptions) = projectOptions.OtherOptions - |> Array.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) - |> Array.map (fun x -> x.Substring "-r:".Length) + |> Seq.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) + |> Seq.map (fun x -> x.Substring "-r:".Length) let onWatchedReferenceChanged (path: string) = ReferenceStampCache.Invalidate path @@ -196,49 +196,47 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch checker.ClearCache([ projectOptions ]) | _ -> () + do ReferenceStampCache.Enable() + let referenceChangeTracker = - fileChangeWatcher - |> Option.map (fun watcher -> - ReferenceStampCache.Enable() - new FSharpReferenceChangeTracker(watcher, onWatchedReferenceChanged)) + new FSharpReferenceChangeTracker(fileChangeWatcher, onWatchedReferenceChanged) - let stopWatching (tracker: FSharpReferenceChangeTracker) paths = - paths |> Array.iter (fun p -> tracker.StopWatchingReference p) + let stopWatching paths = + paths |> Array.iter (fun p -> referenceChangeTracker.StopWatchingReference p) let clearReferenceWatches (projectId: ProjectId) = - match referenceWatches.TryRemove projectId, referenceChangeTracker with - | (true, paths), Some tracker -> stopWatching tracker paths + match referenceWatches.TryRemove projectId with + | true, paths -> stopWatching paths | _ -> () let clearScriptReferenceWatches (documentId: DocumentId) = - match scriptReferenceWatches.TryRemove documentId, referenceChangeTracker with - | (true, paths), Some tracker -> stopWatching tracker paths + match scriptReferenceWatches.TryRemove documentId with + | true, paths -> stopWatching paths | _ -> () let watchReferenceFiles (projectId: ProjectId) (projectOptions: FSharpProjectOptions) = - referenceChangeTracker - |> Option.iter (fun tracker -> - clearReferenceWatches projectId + clearReferenceWatches projectId - let paths = referencePaths projectOptions + let paths = referencePaths projectOptions |> Seq.toArray - if paths.Length > 0 then - paths |> Array.iter (fun p -> tracker.StartWatchingReference p) - referenceWatches[projectId] <- paths) + if paths.Length > 0 then + paths |> Array.iter (fun p -> referenceChangeTracker.StartWatchingReference p) + referenceWatches[projectId] <- paths let watchScriptReferences (documentId: DocumentId) (projectOptions: FSharpProjectOptions) = - referenceChangeTracker - |> Option.iter (fun tracker -> - clearScriptReferenceWatches documentId + clearScriptReferenceWatches documentId - // Scripts also pull in #load sources that are not Roslyn documents. - let paths = - Array.append (referencePaths projectOptions) projectOptions.SourceFiles - |> Array.distinct + let paths = + seq { + yield! referencePaths projectOptions + yield! projectOptions.SourceFiles + } + |> Seq.distinct + |> Seq.toArray - if paths.Length > 0 then - paths |> Array.iter (fun p -> tracker.StartWatchingReference p) - scriptReferenceWatches[documentId] <- paths) + if paths.Length > 0 then + paths |> Array.iter (fun p -> referenceChangeTracker.StartWatchingReference p) + scriptReferenceWatches[documentId] <- paths let createNewPEReference (referencedProject: Project) (comp: Compilation) = let projectId = referencedProject.Id @@ -751,16 +749,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch interface IDisposable with member this.Dispose() = this.ClearAllCaches() - - referenceChangeTracker - |> Option.iter (fun tracker -> (tracker :> IDisposable).Dispose()) - + (referenceChangeTracker :> IDisposable).Dispose() cancellationTokenSource.Cancel() cancellationTokenSource.Dispose() (agent :> IDisposable).Dispose() /// Manages mappings of Roslyn workspace Projects/Documents to FCS. -type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Workspace, ?fileChangeWatcher: IFSharpFileChangeWatcher) = +type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Workspace, fileChangeWatcher: IFSharpFileChangeWatcher) = let reactor = new FSharpProjectOptionsReactor(checker, fileChangeWatcher) From c81abfddd472426234be8b0bff69ac66d8a7a436 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 01:19:16 +0200 Subject: [PATCH 074/133] Sync the file watcher with #20457 after its review rounds Brings FileChangeWatcher.fs, its tests, the design note and the illib helper to the state of vs/file-change-watcher (704a0adc7a), and applies the same review outcomes to this branch's project-options reactor: HashSet-based watch sets diffed on recompute for projects and scripts, InvalidateConfiguration without dropping the cached project options, StartsWithOrdinal helpers. ReferenceStampCache stays, as this branch still carries the snapshot stamp cache and script watching. --- docs/ide/file-watching-design-review.md | 71 ----- docs/ide/file-watching.md | 47 ++++ .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Utilities/illib.fs | 3 + src/Compiler/Utilities/illib.fsi | 2 + .../FSharpProjectOptionsManager.fs | 74 ++--- .../LanguageService/FileChangeWatcher.fs | 263 +++++++++++------- .../LanguageService/LanguageService.fs | 2 +- .../FileChangeWatcherTests.fs | 173 +++++++++++- 9 files changed, 421 insertions(+), 215 deletions(-) delete mode 100644 docs/ide/file-watching-design-review.md create mode 100644 docs/ide/file-watching.md diff --git a/docs/ide/file-watching-design-review.md b/docs/ide/file-watching-design-review.md deleted file mode 100644 index 9d5e52a32f7..00000000000 --- a/docs/ide/file-watching-design-review.md +++ /dev/null @@ -1,71 +0,0 @@ -# F# file watching: design review vs Roslyn - -## Verdict - -The `FileChangeWatcher` working tree is **not** a file-change watcher. It is a pull-model I/O change: - -- `IFileSystem.OpenFileForReadShimAsync` + `Stream.ReadAllTextAsync` -- `FSharpFileSnapshot.CreateFromFileSystem` still versions the file as `GetLastWriteTimeShim(fileName).Ticks` at construction time - -That is useful (it stops blocking the ThreadPool on disk reads) and orthogonal to watching. It is not comparable to Roslyn's `FileChangeWatcher`. - -Roslyn's implementation is a **push** service over `IVsAsyncFileChangeEx2`: - -- directory subscriptions (`WatchedDirectory`) instead of one cookie per file -- `AsyncBatchingWorkQueue` (500 ms) so advise/unadvise is batched and never blocks the UI / thread pool -- free-threaded sinks (`IVsFreeThreadedFileChangeEvents2`) -- coalesced invalidation of metadata references (`FileWatchedReferenceFactory`) - -## What already exists in this repo - -Three layers, none of them `IVsAsyncFileChangeEx2`: - -| Layer | API | Role | -|---|---|---| -| `vsintegration/src/FSharp.ProjectSystem.Base/FileChangeManager.cs` | `IVsFileChangeEx` | Legacy project-system reload of nested items | -| `vsintegration/src/FSharp.LanguageService/FSharpSource.fs` (`SetDependencyFiles`) | `IVsFileChangeEx` | Deprecated unroslynized LS: watch `#r` / dependency files | -| **uncommitted** `stash@{7}` → `vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs` | `IVsFileChangeEx` | Intended modern replacement in `FSharp.Editor` | - -There is **no commit** (on any branch, stash patch, or GitHub search) that implements `IVsAsyncFileChangeEx2`. The work that was remembered as "the async watcher" is the stash below; it uses the older sync advise API, marshalled onto the UI thread. - -## Recovered implementation (`stash@{7}`) - -Stash: `WIP on revert-20080-t-gro-net11-upgrade: 8bf5dca37` - -Index commit that added the file: - -`54465595717b8bb746cb2633d5a4aa834888a481` - -Shape: - -- `IFileChangeWatcher.WatchFile` → `IVsFileChangeEx.AdviseFileChange` / `UnadviseFileChange` -- `FileChangeWatcherHub`: one cookie per path, ref-counted, 500 ms debounce -- Wired into `FSharpProjectOptionsReactor` for `-r:` reference assemblies -- Exposed as `FSharpProjectOptionsManager.WatchFile` so `WorkspaceExtensions` snapshot cache can share the same subscriptions - -This is the right *place* (FSharp.Editor, reference assemblies, debounce, share across projects). It is the wrong *shell API*: - -- `JoinableTaskFactory.Run` + `SwitchToMainThreadAsync` on every advise/unadvise -- no directory watches → N cookies for a NuGet cache -- no batching of subscribe/unsubscribe -- not free-threaded - -## Target design (Roslyn-shaped) - -1. Keep the async read shim. It is independent and should ship on its own. -2. Restore `FileChangeWatcher.fs` from `stash@{7}`, then replace `IVsFileChangeEx` with `IVsAsyncFileChangeEx2`: - - obtain the service asynchronously (same as Roslyn's `Task`) - - queue advise/unadvise on a 500 ms batching work queue; never `JTF.Run` - - subscribe to directories (NuGet cache, output folders) with extension filters; fall back to per-file only for stray paths - - implement `IVsFreeThreadedFileChangeEvents2` so callbacks do not hop to the UI thread -3. On a coalesced change: `checker.NotifyFileChanged` / `InvalidateConfiguration` for the owning project only. Stop O(N) `stat` of reference timestamps on every incremental check. -4. Scripts: watch `#r` / `#load` paths the same way; drop caret-move `NotifyFileChanged`. -5. Do not invent a second watcher. Project system (`FileChangeManager`) and FCS (`TimeStampCache`) should consume this service or stay on their existing contracts. - -## Suggested split - -- PR 1: async `OpenFileForReadShim` (already in the `FileChangeWatcher` worktree). -- PR 2: restore stash watcher as-is (`IVsFileChangeEx`) behind the existing reactor hook — functional, limited. -- PR 3: swap the shell API to `IVsAsyncFileChangeEx2` + directory batching. - -PR 2 is optional if PR 3 is done immediately. diff --git a/docs/ide/file-watching.md b/docs/ide/file-watching.md new file mode 100644 index 00000000000..2e6073f3113 --- /dev/null +++ b/docs/ide/file-watching.md @@ -0,0 +1,47 @@ +# File watching in FSharp.Editor + +`vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs` is the F# counterpart of +Roslyn's `FileChangeWatcher` and `ReferenceFileChangeTracker` (both internal to +`Microsoft.VisualStudio.LanguageServices` and not exposed through `ExternalAccess.FSharp`). + +## Why a watcher + +The Roslyn workspace tracks documents, not the `-r:` references an F# project compiles against. +When a referenced assembly is rebuilt outside VS nothing tells the F# language service; FCS only +notices because it stats every reference again on the next request (`IsReferencesInvalidated` on +the incremental builder, `ReferencesOnDisk` when a snapshot is reused). The watcher turns that into +a push: one notification per changed path, delivered to the projects that reference it. + +## Shape + +- **Service.** `IVsAsyncFileChangeEx2`, obtained asynchronously; nothing ever blocks on the UI + thread. Callbacks arrive through `IVsFreeThreadedFileChangeEvents2` and stay on background + threads. +- **Batching.** Subscribe/unsubscribe operations go through a single-consumer queue with a 500 ms + window (Roslyn's empirical value for solution open/close). Consecutive operations of the same + kind, and for file watches the same sink, are coalesced into one service call. +- **Directory watches.** Each context starts with recursive `.dll` watches on the places + reference assemblies live: `DOTNET_ROOT/packs` and the machine-wide `dotnet/packs`, the .NET + Framework reference assemblies, and the NuGet cache (`NUGET_PACKAGES` or `~/.nuget/packages`). + A file under one of them costs no cookie of its own. Roslyn does not watch the NuGet cache; we + do because every `-r:` is watched uniformly and package assemblies are the bulk of them, so the + alternative is a per-file advise for each. +- **Per-file watches.** Paths outside those directories (project outputs, loose assemblies) get + an individual advise, ref-counted across projects by `FSharpReferenceChangeTracker`. +- **Debounce.** A rebuild writes a temp file and renames it, producing several notifications; the + tracker fires one callback per path after 2 s of quiet. + +## Consumer + +`FSharpProjectOptionsReactor` watches the `-r:` set of every project it computes options for and +calls `FSharpChecker.InvalidateConfiguration` for each project that references a changed path. +The cached options stay valid (same paths); only the FCS build behind them is stale. Watch sets +are diffed on recompute, so an unchanged reference list touches nothing. + +## Follow-ups + +1. A reference-change notification for the incremental builder on the FCS side, the analogue of + `useChangeNotifications` for sources, so `IsReferencesInvalidated` stops stat'ing every + reference on every request. +2. A watcher-invalidated timestamp cache for snapshot reuse (`ReferencesOnDisk`). +3. Scripts: watch `#r` references and `#load` sources the same way. 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 24450403072..e9404e58f6d 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -149,6 +149,7 @@ * IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250)) ### Added +* `Internal.Utilities.Library`: `String.StartsWithOrdinalIgnoreCase` extension, the `StartsWith` sibling of `EndsWithOrdinalIgnoreCase`. ([PR #20457](https://github.com/dotnet/fsharp/pull/20457)) * Added a "most concrete" tiebreaker for overload resolution (`--langversion:preview`). ([RFC FS-1340](https://github.com/fsharp/fslang-design/pull/834), [PR #19277](https://github.com/dotnet/fsharp/pull/19277)) * Added support for `OverloadResolutionPriorityAttribute` in overload resolution (`--langversion:preview`). ([RFC FS-1338](https://github.com/fsharp/fslang-design/pull/828), [PR #19277](https://github.com/dotnet/fsharp/pull/19277)) diff --git a/src/Compiler/Utilities/illib.fs b/src/Compiler/Utilities/illib.fs index 3b09abab981..a4c597f01f6 100644 --- a/src/Compiler/Utilities/illib.fs +++ b/src/Compiler/Utilities/illib.fs @@ -98,6 +98,9 @@ module internal PervasiveAutoOpens = member inline x.StartsWithOrdinal(value: string) = x.StartsWith(value, StringComparison.Ordinal) + member inline x.StartsWithOrdinalIgnoreCase(value: string) = + x.StartsWith(value, StringComparison.OrdinalIgnoreCase) + member inline x.EndsWithOrdinal(value: string) = x.EndsWith(value, StringComparison.Ordinal) diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index cad5703174b..8033c3b0177 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -58,6 +58,8 @@ module internal PervasiveAutoOpens = member inline StartsWithOrdinal: value: string -> bool + member inline StartsWithOrdinalIgnoreCase: value: string -> bool + member inline EndsWithOrdinal: value: string -> bool member inline EndsWithOrdinalIgnoreCase: value: string -> bool diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 63cc7f73bbe..7249fc768ff 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -18,6 +18,7 @@ open FSharp.Compiler.Text open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.FSharp.Interactive.Session open Microsoft.VisualStudio.TextManager.Interop +open Internal.Utilities.Library open CancellableTasks #nowarn "57" @@ -163,33 +164,31 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let disposeSingleFileCacheEntry ({ Subscription = subscription }: SingleFileCacheEntry) = subscription |> ValueOption.iter (fun subscription -> subscription.Dispose()) - // Push invalidation for on-disk '-r:' reference assemblies (not tracked by the Roslyn - // workspace): when one changes after an external rebuild, drop the cached options of every - // project and script referencing it instead of waiting for a timestamp poll to notice. - let referenceWatches = ConcurrentDictionary() - let scriptReferenceWatches = ConcurrentDictionary() + // Push invalidation for on-disk '-r:' reference assemblies, which the Roslyn workspace does not + // track. A project's cached options stay valid (same paths); only the FCS build behind them goes + // stale. A script's options are dropped, since its watched set also covers '#load' sources. + let referenceWatches = ConcurrentDictionary>() + let scriptReferenceWatches = ConcurrentDictionary>() let referencePaths (projectOptions: FSharpProjectOptions) = - projectOptions.OtherOptions - |> Seq.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) - |> Seq.map (fun x -> x.Substring "-r:".Length) + seq { + for option in projectOptions.OtherOptions do + if option.StartsWithOrdinal "-r:" then + option.Substring "-r:".Length + } let onWatchedReferenceChanged (path: string) = ReferenceStampCache.Invalidate path - let isWatched paths = - paths - |> Array.exists (fun p -> String.Equals(p, path, StringComparison.OrdinalIgnoreCase)) - for KeyValue(projectId, paths) in referenceWatches do - if isWatched paths then - match cache.TryRemove projectId with + if paths.Contains path then + match cache.TryGetValue projectId with | true, struct (_, _, projectOptions) -> checker.InvalidateConfiguration(projectOptions, userOpName = "onWatchedReferenceChanged") | _ -> () for KeyValue(documentId, paths) in scriptReferenceWatches do - if isWatched paths then + if paths.Contains path then match singleFileCache.TryRemove documentId with | true, ({ ProjectOptions = projectOptions } as entry) -> disposeSingleFileCacheEntry entry @@ -201,8 +200,9 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let referenceChangeTracker = new FSharpReferenceChangeTracker(fileChangeWatcher, onWatchedReferenceChanged) - let stopWatching paths = - paths |> Array.iter (fun p -> referenceChangeTracker.StopWatchingReference p) + let stopWatching (paths: HashSet) = + for path in paths do + referenceChangeTracker.StopWatchingReference path let clearReferenceWatches (projectId: ProjectId) = match referenceWatches.TryRemove projectId with @@ -214,29 +214,35 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch | true, paths -> stopWatching paths | _ -> () - let watchReferenceFiles (projectId: ProjectId) (projectOptions: FSharpProjectOptions) = - clearReferenceWatches projectId + // Diff against what is already watched under the key, so an unchanged set touches no watches. + let updateWatches (watches: ConcurrentDictionary<'Key, HashSet>) (key: 'Key) (paths: HashSet) = + match watches.TryGetValue key with + | true, previous -> + for path in previous do + if not (paths.Contains path) then + referenceChangeTracker.StopWatchingReference path + + for path in paths do + if not (previous.Contains path) then + referenceChangeTracker.StartWatchingReference path + | _ -> + for path in paths do + referenceChangeTracker.StartWatchingReference path - let paths = referencePaths projectOptions |> Seq.toArray + if paths.Count > 0 then + watches[key] <- paths + else + watches.TryRemove key |> ignore - if paths.Length > 0 then - paths |> Array.iter (fun p -> referenceChangeTracker.StartWatchingReference p) - referenceWatches[projectId] <- paths + let watchReferenceFiles (projectId: ProjectId) (projectOptions: FSharpProjectOptions) = + updateWatches referenceWatches projectId (HashSet(referencePaths projectOptions, StringComparer.OrdinalIgnoreCase)) let watchScriptReferences (documentId: DocumentId) (projectOptions: FSharpProjectOptions) = - clearScriptReferenceWatches documentId - let paths = - seq { - yield! referencePaths projectOptions - yield! projectOptions.SourceFiles - } - |> Seq.distinct - |> Seq.toArray + HashSet(referencePaths projectOptions, StringComparer.OrdinalIgnoreCase) - if paths.Length > 0 then - paths |> Array.iter (fun p -> referenceChangeTracker.StartWatchingReference p) - scriptReferenceWatches[documentId] <- paths + paths.UnionWith projectOptions.SourceFiles + updateWatches scriptReferenceWatches documentId paths let createNewPEReference (referencedProject: Project) (comp: Compilation) = let projectId = referencedProject.Id diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index c50b1bf72e0..4d550c2047a 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -5,12 +5,19 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Collections.Concurrent open System.Collections.Generic +open System.Collections.Immutable open System.Threading open System.Threading.Tasks open Microsoft.VisualStudio open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop +open Internal.Utilities.Library + +open Microsoft.VisualStudio.FSharp.Editor.DebugHelpers + +open CancellableTasks + // Push-based file watching for FSharp.Editor, modelled on Roslyn's // Microsoft.VisualStudio.LanguageServices FileChangeWatcher (which is internal and not // exposed through ExternalAccess.FSharp). Uses the free-threaded IVsAsyncFileChangeEx2 @@ -19,28 +26,27 @@ open Microsoft.VisualStudio.Shell.Interop /// A directory to watch recursively (with optional extension filters) so that individual /// files under it don't each need their own advise cookie. [] -type internal WatchedDirectory(path: string, extensionFilters: string list) = +type internal WatchedDirectory(path: string, extensionFilters: ImmutableArray) = let path = - if path.EndsWith(string IO.Path.DirectorySeparatorChar) then + if path.EndsWithOrdinal(string IO.Path.DirectorySeparatorChar) then path else - path + string IO.Path.DirectorySeparatorChar + $"{path}{IO.Path.DirectorySeparatorChar}" do for filter in extensionFilters do - if not (filter.StartsWith ".") then + if not (filter.StartsWithOrdinal ".") then invalidArg (nameof extensionFilters) $"Filter '{filter}' must start with a period." member _.Path = path member _.ExtensionFilters = extensionFilters - static member FilePathCoveredByWatchedDirectories(watchedDirectories: WatchedDirectory list, filePath: string) = + static member FilePathCoveredByWatchedDirectories(watchedDirectories: ImmutableArray, filePath: string) = watchedDirectories - |> List.exists (fun w -> - filePath.StartsWith(w.Path, StringComparison.OrdinalIgnoreCase) + |> Seq.exists (fun w -> + filePath.StartsWithOrdinalIgnoreCase w.Path && (w.ExtensionFilters.IsEmpty - || w.ExtensionFilters - |> List.exists (fun f -> filePath.EndsWith(f, StringComparison.OrdinalIgnoreCase)))) + || w.ExtensionFilters |> Seq.exists filePath.EndsWithOrdinalIgnoreCase)) /// A single watched file; disposing stops watching. type internal IFSharpWatchedFile = @@ -58,7 +64,7 @@ type internal IFSharpFileChangeContext = abstract EnqueueWatchingFile: filePath: string -> IFSharpWatchedFile type internal IFSharpFileChangeWatcher = - abstract CreateContext: watchedDirectories: WatchedDirectory list -> IFSharpFileChangeContext + abstract CreateContext: watchedDirectories: ImmutableArray -> IFSharpFileChangeContext /// Process-wide cache of on-disk reference timestamps. Enabled when push-based file watching /// is active: watched paths are invalidated point-wise by change notifications, so repeated @@ -94,24 +100,35 @@ module private FileChangeWatcherImpl = /// Empirically strong batching window during high activity (solution open/close); see /// Roslyn's FileChangeWatcher. - let batchingDelay = TimeSpan.FromMilliseconds 500. + let defaultBatchingDelay = TimeSpan.FromMilliseconds 500. + + /// Delay between the last observed change to a path and the callback: a rebuild typically + /// writes a temp file then renames, producing several rapid notifications. + let defaultNotificationDelay = TimeSpan.FromSeconds 2. + + let noOpWatchedFile = + { new IFSharpWatchedFile with + member _.Dispose() = () + } [] type internal FSharpWatchedFileToken() = - member val Cookie: uint32 option = None with get, set + member val Cookie: uint32 voption = ValueNone with get, set /// Subscription operations queued for batched application against the file change service. type private WatcherOperation = - | WatchDir of path: string * filters: string list * sink: IVsFreeThreadedFileChangeEvents2 * cookies: List + | WatchDir of path: string * filters: ImmutableArray * sink: IVsFreeThreadedFileChangeEvents2 * cookies: List | WatchFiles of paths: string list * tokens: FSharpWatchedFileToken list * sink: IVsFreeThreadedFileChangeEvents2 | UnwatchFiles of tokens: FSharpWatchedFileToken list | UnwatchDirs of cookies: List [] -type internal FSharpFileChangeWatcher(fileChangeService: Task) = +type internal FSharpFileChangeWatcher(fileChangeService: Task, batchingDelay: TimeSpan) = let applyBatch (service: IVsAsyncFileChangeEx2) (ops: WatcherOperation list) = - task { + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + // Coalesce adjacent same-kind operations into single service calls, preserving order // between kinds (a watch enqueued before an unwatch must be applied first). let mutable pending = ops @@ -121,98 +138,118 @@ type internal FSharpFileChangeWatcher(fileChangeService: Task () | WatchDir(path, filters, sink, cookies) :: rest -> pending <- rest - let! cookie = service.AdviseDirChangeAsync(path, true, sink, CancellationToken.None) + let! cookie = service.AdviseDirChangeAsync(path, true, sink, ct) cookies.Add cookie if not filters.IsEmpty then - do! service.FilterDirectoryChangesAsync(cookie, List.toArray filters, CancellationToken.None) + do! service.FilterDirectoryChangesAsync(cookie, Seq.toArray filters, ct) - | WatchFiles _ :: _ -> + | WatchFiles(_, _, sink) :: _ -> let batch = pending - |> List.takeWhile (function - | WatchFiles _ -> true + |> Seq.takeWhile (function + | WatchFiles(_, _, s) -> obj.ReferenceEquals(s, sink) | _ -> false) + |> Seq.toArray pending <- pending |> List.skip batch.Length let paths = - batch - |> List.collect (function - | WatchFiles(p, _, _) -> p - | _ -> []) + [| + for op in batch do + match op with + | WatchFiles(p, _, _) -> yield! p + | _ -> () + |] let tokens = - batch - |> List.collect (function - | WatchFiles(_, t, _) -> t - | _ -> []) - - let sink = - batch - |> List.pick (function - | WatchFiles(_, _, s) -> Some s - | _ -> None) + [| + for op in batch do + match op with + | WatchFiles(_, t, _) -> yield! t + | _ -> () + |] - let! cookies = service.AdviseFileChangesAsync(List.toArray paths, watchFlags, sink, CancellationToken.None) + let! cookies = service.AdviseFileChangesAsync(paths, watchFlags, sink, ct) - (tokens, List.ofArray cookies) - ||> List.iter2 (fun token cookie -> token.Cookie <- Some cookie) + (tokens, cookies) + ||> Array.iter2 (fun token cookie -> token.Cookie <- ValueSome cookie) | UnwatchFiles _ :: _ -> let batch = pending - |> List.takeWhile (function + |> Seq.takeWhile (function | UnwatchFiles _ -> true | _ -> false) + |> Seq.toArray pending <- pending |> List.skip batch.Length + // A token whose watch never got a cookie (or was already unadvised) is a no-op. let cookies = - batch - |> List.collect (function - | UnwatchFiles t -> t - | _ -> []) - |> List.choose (fun token -> token.Cookie) - - if not cookies.IsEmpty then - let! _ = service.UnadviseFileChangesAsync(List.toArray cookies, CancellationToken.None) + [| + for op in batch do + match op with + | UnwatchFiles tokens -> + for token in tokens do + match token.Cookie with + | ValueSome cookie -> + token.Cookie <- ValueNone + cookie + | ValueNone -> () + | _ -> () + |] + + if cookies.Length > 0 then + let! _ = service.UnadviseFileChangesAsync(cookies, ct) () | UnwatchDirs cookies :: rest -> pending <- rest if cookies.Count > 0 then - let! _ = service.UnadviseDirChangesAsync(cookies.ToArray(), CancellationToken.None) + let! _ = service.UnadviseDirChangesAsync(cookies.ToArray(), ct) () } + let cancellationTokenSource = new CancellationTokenSource() + // Single consumer loop: waits for the first queued operation, sleeps out the batching // window, drains the queue and applies everything in one pass. Nothing ever blocks on the // service being available. let agent = - MailboxProcessor.Start(fun inbox -> - async { - while true do - try - let! first = inbox.Receive() - do! Async.Sleep(int batchingDelay.TotalMilliseconds) - - let ops = ResizeArray [ first ] - let mutable draining = true - - while draining do - match! inbox.TryReceive 0 with - | Some op -> ops.Add op - | None -> draining <- false - - let! service = fileChangeService |> Async.AwaitTask - do! applyBatch service (List.ofSeq ops) |> Async.AwaitTask - with _ -> - // Never let a failed advise/unadvise (e.g. non-existent path) kill the - // subscription loop; we simply won't get events for that path. - () - }) + MailboxProcessor + .Start( + (fun inbox -> + async { + let! ct = Async.CancellationToken + + while true do + try + let! first = inbox.Receive() + do! Async.Sleep(int batchingDelay.TotalMilliseconds) + + let ops = ResizeArray [ first ] + + while inbox.CurrentQueueLength > 0 do + let! op = inbox.Receive() + ops.Add op + + let! service = fileChangeService |> Async.AwaitTask + + do! + applyBatch service (List.ofSeq ops) + |> CancellableTask.startAsTask ct + |> Async.AwaitTask + with ex when not (ex :? OperationCanceledException) -> + // Never let a failed advise/unadvise (e.g. non-existent path) kill the + // subscription loop; we simply won't get events for that path. + FSharpOutputPane.logExceptionWithContext (ex, nameof FSharpFileChangeWatcher) + }), + cancellationTokenSource.Token + ) + + new(fileChangeService) = new FSharpFileChangeWatcher(fileChangeService, defaultBatchingDelay) member private _.Enqueue(op: WatcherOperation) = agent.Post op @@ -228,7 +265,13 @@ type internal FSharpFileChangeWatcher(fileChangeService: Task IFSharpFileChangeContext -and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watchedDirectories: WatchedDirectory list) as this = + interface IDisposable with + member _.Dispose() = + cancellationTokenSource.Cancel() + cancellationTokenSource.Dispose() + (agent :> IDisposable).Dispose() + +and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watchedDirectories: ImmutableArray) as this = let gate = obj () let mutable disposed = false @@ -267,10 +310,7 @@ and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watc member _.EnqueueWatchingFile filePath = if WatchedDirectory.FilePathCoveredByWatchedDirectories(watchedDirectories, filePath) then - // Covered by a directory watch; nothing extra to subscribe. - { new IFSharpWatchedFile with - member _.Dispose() = () - } + noOpWatchedFile else let token = FSharpWatchedFileToken() lock gate (fun () -> activeFileTokens.Add token |> ignore) @@ -320,11 +360,7 @@ and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watc /// modelled on Roslyn's ReferenceFileChangeTracker. Multiple projects watching the same dll /// share one subscription; bursts of writes produce a single callback per path. [] -type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, onChanged: string -> unit, ?notificationDelay: TimeSpan) = - - /// Delay between the last observed change to a path and the callback: a rebuild typically - /// writes a temp file then renames, producing several rapid notifications. - let notificationDelay = defaultArg notificationDelay (TimeSpan.FromSeconds 2.) +type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, onChanged: string -> unit, notificationDelay: TimeSpan) = let gate = obj () let mutable disposed = false @@ -332,31 +368,37 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on let watchedFiles = Dictionary(StringComparer.OrdinalIgnoreCase) - let pendingTimers = - ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + let pendingTimers = Dictionary(StringComparer.OrdinalIgnoreCase) // On each platform there is a place framework reference assemblies live; these rarely change // but account for most watched paths, so cover them with directory watches up front. static let defaultWatchedDirectories () = let dotnetRoot = Environment.GetEnvironmentVariable "DOTNET_ROOT" + let nugetPackages = Environment.GetEnvironmentVariable "NUGET_PACKAGES" - [ - if not (String.IsNullOrEmpty dotnetRoot) then - IO.Path.Combine(dotnetRoot, "packs") + let directories = + seq { + if not (String.IsNullOrEmpty dotnetRoot) then + IO.Path.Combine(dotnetRoot, "packs") - IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.ProgramFiles, "dotnet", "packs") + IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.ProgramFiles, "dotnet", "packs") - IO.Path.Combine( - Environment.GetFolderPath Environment.SpecialFolder.ProgramFilesX86, - "Reference Assemblies", - "Microsoft", - "Framework" - ) + IO.Path.Combine( + Environment.GetFolderPath Environment.SpecialFolder.ProgramFilesX86, + "Reference Assemblies", + "Microsoft", + "Framework" + ) + + if String.IsNullOrEmpty nugetPackages then + IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.UserProfile, ".nuget", "packages") + else + nugetPackages + } + |> Seq.distinct + |> Seq.map (fun d -> WatchedDirectory(d, ImmutableArray.Create ".dll")) - IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.UserProfile, ".nuget", "packages") - ] - |> List.distinct - |> List.map (fun d -> WatchedDirectory(d, [ ".dll" ])) + directories.ToImmutableArray() let context = lazy @@ -364,25 +406,36 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on ctx.FileChanged.Add(fun path -> let fire (_: obj) = - pendingTimers.TryRemove path - |> function - | true, timer -> timer.Dispose() - | _ -> () + let isWatched = + lock gate (fun () -> + match pendingTimers.TryGetValue path with + | true, timer -> + pendingTimers.Remove path |> ignore + timer.Dispose() + | _ -> () - // Only notify for paths someone is actually watching; directory watches - // cover whole trees. - let isWatched = lock gate (fun () -> watchedFiles.ContainsKey path) + watchedFiles.ContainsKey path) if isWatched then onChanged path - let timer = - pendingTimers.GetOrAdd(path, fun _ -> new Timer(fire, null, Timeout.Infinite, Timeout.Infinite)) + lock gate (fun () -> + // Directory watches cover whole trees; only debounce paths someone watches. + if not disposed && watchedFiles.ContainsKey path then + let timer = + match pendingTimers.TryGetValue path with + | true, timer -> timer + | _ -> + let timer = new Timer(fire, null, Timeout.Infinite, Timeout.Infinite) + pendingTimers[path] <- timer + timer - timer.Change(notificationDelay, Timeout.InfiniteTimeSpan) |> ignore) + timer.Change(notificationDelay, Timeout.InfiniteTimeSpan) |> ignore)) ctx) + new(watcher, onChanged) = new FSharpReferenceChangeTracker(watcher, onChanged, defaultNotificationDelay) + /// Starts watching a path, ref-counted. Call StopWatchingReference exactly once per start. member _.StartWatchingReference(fullFilePath: string) = lock gate (fun () -> diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 172f3d0f513..9843a78275a 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -210,7 +210,7 @@ type internal FSharpWorkspaceServiceFactory |> ignore) let fileChangeWatcher = - FSharpFileChangeWatcher(FSharpFileChangeWatcher.CreateDefaultServiceTask()) + new FSharpFileChangeWatcher(FSharpFileChangeWatcher.CreateDefaultServiceTask()) let optionsManager = FSharpProjectOptionsManager(checker, workspace, fileChangeWatcher) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs index 829cfd68226..d3df726b7b0 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs @@ -3,8 +3,11 @@ namespace FSharp.Editor.Tests open System +open System.Collections.Immutable open System.Threading +open System.Threading.Tasks open Xunit +open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.FSharp.Editor type private MockFileChangeContext() = @@ -28,23 +31,95 @@ type private MockFileChangeContext() = member _.Dispose() = watched.Clear() type private MockFileChangeWatcher() = - let mutable context: MockFileChangeContext option = None + let mutable context: MockFileChangeContext voption = ValueNone member _.Context = context interface IFSharpFileChangeWatcher with member _.CreateContext _ = let ctx = new MockFileChangeContext() - context <- Some ctx + context <- ValueSome ctx ctx :> IFSharpFileChangeContext +type private ServiceCall = + | AdvisedDir of path: string * cookie: uint32 + | FilteredDir of cookie: uint32 * extensions: string list + | AdvisedFiles of paths: string list * sink: obj * cookies: uint32 list + | UnadvisedFiles of cookies: uint32 list + | UnadvisedDirs of cookies: uint32 list + +/// +/// Stands in for : hands out sequential cookies and records every call, so a +/// test can see how the watcher turned its queue into service calls. +/// +type private RecordingFileChangeService() = + let calls = ResizeArray() + let mutable nextCookie = 0u + + let record call = lock calls (fun () -> calls.Add call) + + let newCookie () = + nextCookie <- nextCookie + 1u + nextCookie + + member _.Calls = lock calls (fun () -> List.ofSeq calls) + + member this.WaitForCalls(count: int) = + let deadline = DateTime.UtcNow + TimeSpan.FromSeconds 10. + + while lock calls (fun () -> calls.Count) < count && DateTime.UtcNow < deadline do + Thread.Sleep 10 + + this.Calls + + interface IVsAsyncFileChangeEx2 with + member _.AdviseFileChangesAsync(filenames, _, sink, _) = + let cookies = [| for _ in filenames -> newCookie () |] + record (AdvisedFiles(List.ofSeq filenames, box sink, List.ofArray cookies)) + Task.FromResult cookies + + interface IVsAsyncFileChangeEx with + member _.AdviseFileChangeAsync(_, _, _, _) = Task.FromResult(newCookie ()) + member _.UnadviseFileChangeAsync(_, _) = Task.FromResult "" + + member _.UnadviseFileChangesAsync(cookies, _) = + record (UnadvisedFiles(List.ofSeq cookies)) + Task.FromResult Array.empty + + member _.AdviseDirChangeAsync(directory, _, _, _) = + let cookie = newCookie () + record (AdvisedDir(directory, cookie)) + Task.FromResult cookie + + member _.UnadviseDirChangeAsync(_, _) = Task.FromResult "" + + member _.UnadviseDirChangesAsync(cookies, _) = + record (UnadvisedDirs(List.ofSeq cookies)) + Task.FromResult Array.empty + + member _.SyncFileAsync(_, _) = Task.CompletedTask + member _.IgnoreFileAsync(_, _, _, _) = Task.CompletedTask + member _.IgnoreDirAsync(_, _, _) = Task.CompletedTask + + member _.FilterDirectoryChangesAsync(cookie, extensions, _) = + record (FilteredDir(cookie, List.ofArray extensions)) + Task.CompletedTask + module FileChangeWatcherTests = let private testDelay = TimeSpan.FromMilliseconds 50. + let private batchDelay = TimeSpan.FromMilliseconds 100. + + let private noDirectories = ImmutableArray.Empty + + let private createWatcher (service: RecordingFileChangeService) = + new FSharpFileChangeWatcher(Task.FromResult(service :> IVsAsyncFileChangeEx2), batchDelay) + [] let ``WatchedDirectory covers files under it matching the extension filter`` () = - let dirs = [ WatchedDirectory(@"C:\refs", [ ".dll" ]) ] + let dirs = + ImmutableArray.Create(WatchedDirectory(@"C:\refs", ImmutableArray.Create ".dll")) Assert.True(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refs\sub\a.dll")) Assert.True(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\REFS\A.DLL")) @@ -53,7 +128,8 @@ module FileChangeWatcherTests = [] let ``WatchedDirectory without filters covers any file under it`` () = - let dirs = [ WatchedDirectory(@"C:\refs", []) ] + let dirs = + ImmutableArray.Create(WatchedDirectory(@"C:\refs", ImmutableArray.Empty)) Assert.True(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refs\a.xml")) Assert.False(WatchedDirectory.FilePathCoveredByWatchedDirectories(dirs, @"C:\refsx\a.xml")) @@ -119,3 +195,92 @@ module FileChangeWatcherTests = Thread.Sleep(testDelay + testDelay) Assert.False called + + [] + let ``Consecutive file watches are advised in one service call`` () = + let service = RecordingFileChangeService() + use watcher = createWatcher service + use context = (watcher :> IFSharpFileChangeWatcher).CreateContext noDirectories + + for path in [ @"C:\x\a.dll"; @"C:\x\b.dll"; @"C:\x\c.dll" ] do + context.EnqueueWatchingFile path |> ignore + + match service.WaitForCalls 1 with + | [ AdvisedFiles(paths, _, cookies) ] -> + Assert.Equal([ @"C:\x\a.dll"; @"C:\x\b.dll"; @"C:\x\c.dll" ], paths) + Assert.Equal([ 1u; 2u; 3u ], cookies) + | calls -> failwith $"Unexpected calls: %A{calls}" + + [] + let ``A run of file watches is split when the sink changes`` () = + let service = RecordingFileChangeService() + use watcher = createWatcher service + let factory = watcher :> IFSharpFileChangeWatcher + use first = factory.CreateContext noDirectories + use second = factory.CreateContext noDirectories + + first.EnqueueWatchingFile @"C:\x\a.dll" |> ignore + first.EnqueueWatchingFile @"C:\x\b.dll" |> ignore + second.EnqueueWatchingFile @"C:\x\c.dll" |> ignore + + match service.WaitForCalls 2 with + | [ AdvisedFiles(firstPaths, firstSink, [ 1u; 2u ]); AdvisedFiles(secondPaths, secondSink, [ 3u ]) ] -> + Assert.Equal([ @"C:\x\a.dll"; @"C:\x\b.dll" ], firstPaths) + Assert.Equal([ @"C:\x\c.dll" ], secondPaths) + Assert.False(obj.ReferenceEquals(firstSink, secondSink)) + | calls -> failwith $"Unexpected calls: %A{calls}" + + [] + let ``A watch followed by an unwatch in the same batch unadvises the cookie the watch received`` () = + let service = RecordingFileChangeService() + use watcher = createWatcher service + use context = (watcher :> IFSharpFileChangeWatcher).CreateContext noDirectories + + let watched = context.EnqueueWatchingFile @"C:\x\a.dll" + watched.Dispose() + + match service.WaitForCalls 2 with + | [ AdvisedFiles(_, _, [ advised ]); UnadvisedFiles [ unadvised ] ] -> Assert.Equal(advised, unadvised) + | calls -> failwith $"Unexpected calls: %A{calls}" + + [] + let ``Unwatching a token that holds no cookie is a no-op`` () = + let service = RecordingFileChangeService() + use watcher = createWatcher service + use context = (watcher :> IFSharpFileChangeWatcher).CreateContext noDirectories + + let watched = context.EnqueueWatchingFile @"C:\x\a.dll" + service.WaitForCalls 1 |> ignore + + watched.Dispose() + service.WaitForCalls 2 |> ignore + + watched.Dispose() + Thread.Sleep(batchDelay + batchDelay + batchDelay) + + match service.Calls with + | [ AdvisedFiles(_, _, [ advised ]); UnadvisedFiles [ unadvised ] ] -> Assert.Equal(advised, unadvised) + | calls -> failwith $"Unexpected calls: %A{calls}" + + [] + let ``Disposing a context unadvises its directory and remaining file cookies`` () = + let service = RecordingFileChangeService() + use watcher = createWatcher service + + let directories = + ImmutableArray.Create(WatchedDirectory(@"C:\refs", ImmutableArray.Create ".dll")) + + let context = (watcher :> IFSharpFileChangeWatcher).CreateContext directories + context.EnqueueWatchingFile @"C:\refs\covered.dll" |> ignore + context.EnqueueWatchingFile @"C:\other\a.dll" |> ignore + service.WaitForCalls 3 |> ignore + + context.Dispose() + + match service.WaitForCalls 5 with + | [ AdvisedDir(directory, 1u) + FilteredDir(1u, [ ".dll" ]) + AdvisedFiles([ @"C:\other\a.dll" ], _, [ 2u ]) + UnadvisedDirs [ 1u ] + UnadvisedFiles [ 2u ] ] -> Assert.Equal(@"C:\refs\", directory) + | calls -> failwith $"Unexpected calls: %A{calls}" From 56e1a85dca1f766d98e579f4d1c20d3a2a955e61 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 03:53:48 +0200 Subject: [PATCH 075/133] Keep reference stamps inside the tracker's watch entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FSharpReferenceChangeTracker now records each watched path's last-write stamp in the same entry as its ref-count and token, and drops it on the raw change notification. IReferenceStamps serves a cached stamp only while the path is watched — a notification can still reach it — and stats unwatched paths directly. --- .../LanguageService/FileChangeWatcher.fs | 56 ++++++++++++-- .../FileChangeWatcherTests.fs | 74 +++++++++++++++++++ 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index 4d550c2047a..c609e1955ba 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -84,6 +84,17 @@ module internal ReferenceStampCache = stamps.GetOrAdd(path, (fun p -> IO.File.GetLastWriteTimeUtc p)) else IO.File.GetLastWriteTimeUtc path +/// Last-write stamps of watched reference files; a path nobody watches is stat'd directly. +type internal IReferenceStamps = + abstract GetLastWriteTimeUtc: fullFilePath: string -> DateTime + abstract Invalidate: fullFilePath: string -> unit + +type private WatchedReference = + { + Token: IFSharpWatchedFile + mutable Count: int + mutable Stamp: DateTime voption + } [] module private FileChangeWatcherImpl = @@ -358,7 +369,9 @@ and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watc /// Ref-counted, debounced watching of reference assemblies (or any other off-workspace files), /// modelled on Roslyn's ReferenceFileChangeTracker. Multiple projects watching the same dll -/// share one subscription; bursts of writes produce a single callback per path. +/// share one subscription; bursts of writes produce a single callback per path. The last-write +/// stamp of a path lives inside its watch entry, so a cached stamp is only ever served while a +/// change notification can still reach it. [] type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, onChanged: string -> unit, notificationDelay: TimeSpan) = @@ -366,7 +379,7 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on let mutable disposed = false let watchedFiles = - Dictionary(StringComparer.OrdinalIgnoreCase) + Dictionary(StringComparer.OrdinalIgnoreCase) let pendingTimers = Dictionary(StringComparer.OrdinalIgnoreCase) @@ -421,7 +434,10 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on lock gate (fun () -> // Directory watches cover whole trees; only debounce paths someone watches. - if not disposed && watchedFiles.ContainsKey path then + match watchedFiles.TryGetValue path with + | true, entry when not disposed -> + entry.Stamp <- ValueNone + let timer = match pendingTimers.TryGetValue path with | true, timer -> timer @@ -430,7 +446,8 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on pendingTimers[path] <- timer timer - timer.Change(notificationDelay, Timeout.InfiniteTimeSpan) |> ignore)) + timer.Change(notificationDelay, Timeout.InfiniteTimeSpan) |> ignore + | _ -> ())) ctx) @@ -441,17 +458,40 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on lock gate (fun () -> if not disposed then match watchedFiles.TryGetValue fullFilePath with - | true, (token, count) -> watchedFiles[fullFilePath] <- (token, count + 1) - | _ -> watchedFiles[fullFilePath] <- (context.Value.EnqueueWatchingFile fullFilePath, 1)) + | true, entry -> entry.Count <- entry.Count + 1 + | _ -> + watchedFiles[fullFilePath] <- + { + Token = context.Value.EnqueueWatchingFile fullFilePath + Count = 1 + Stamp = ValueNone + }) member _.StopWatchingReference(fullFilePath: string) = lock gate (fun () -> if not disposed then match watchedFiles.TryGetValue fullFilePath with - | true, (token, 1) -> + | true, { Count = 1; Token = token } -> watchedFiles.Remove fullFilePath |> ignore token.Dispose() - | true, (token, count) -> watchedFiles[fullFilePath] <- (token, count - 1) + | true, entry -> entry.Count <- entry.Count - 1 + | _ -> ()) + + interface IReferenceStamps with + member _.GetLastWriteTimeUtc fullFilePath = + lock gate (fun () -> + match watchedFiles.TryGetValue fullFilePath with + | true, { Stamp = ValueSome stamp } -> stamp + | true, entry -> + let stamp = IO.File.GetLastWriteTimeUtc fullFilePath + entry.Stamp <- ValueSome stamp + stamp + | _ -> IO.File.GetLastWriteTimeUtc fullFilePath) + + member _.Invalidate fullFilePath = + lock gate (fun () -> + match watchedFiles.TryGetValue fullFilePath with + | true, entry -> entry.Stamp <- ValueNone | _ -> ()) interface IDisposable with diff --git a/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs index d3df726b7b0..269f5476603 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FileChangeWatcherTests.fs @@ -4,6 +4,7 @@ namespace FSharp.Editor.Tests open System open System.Collections.Immutable +open System.IO open System.Threading open System.Threading.Tasks open Xunit @@ -284,3 +285,76 @@ module FileChangeWatcherTests = UnadvisedDirs [ 1u ] UnadvisedFiles [ 2u ] ] -> Assert.Equal(@"C:\refs\", directory) | calls -> failwith $"Unexpected calls: %A{calls}" + + let private t0 = DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc) + let private t1 = t0.AddHours 1. + + let private withTempFile (test: string -> unit) = + let path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.dll") + File.WriteAllBytes(path, Array.empty) + File.SetLastWriteTimeUtc(path, t0) + + try + test path + finally + File.Delete path + + [] + let ``Watched path is served from the cache until a change notification`` () = + withTempFile (fun path -> + let watcher = MockFileChangeWatcher() + use tracker = new FSharpReferenceChangeTracker(watcher, ignore, testDelay) + let stamps = tracker :> IReferenceStamps + + tracker.StartWatchingReference path + Assert.Equal(t0, stamps.GetLastWriteTimeUtc path) + + File.SetLastWriteTimeUtc(path, t1) + Assert.Equal(t0, stamps.GetLastWriteTimeUtc path) + + watcher.Context.Value.Fire path + Assert.Equal(t1, stamps.GetLastWriteTimeUtc path)) + + [] + let ``Unwatched path is stat'd on every read`` () = + withTempFile (fun path -> + let watcher = MockFileChangeWatcher() + use tracker = new FSharpReferenceChangeTracker(watcher, ignore, testDelay) + let stamps = tracker :> IReferenceStamps + + Assert.Equal(t0, stamps.GetLastWriteTimeUtc path) + + File.SetLastWriteTimeUtc(path, t1) + Assert.Equal(t1, stamps.GetLastWriteTimeUtc path)) + + [] + let ``Invalidate drops the cached stamp`` () = + withTempFile (fun path -> + let watcher = MockFileChangeWatcher() + use tracker = new FSharpReferenceChangeTracker(watcher, ignore, testDelay) + let stamps = tracker :> IReferenceStamps + + tracker.StartWatchingReference path + Assert.Equal(t0, stamps.GetLastWriteTimeUtc path) + + File.SetLastWriteTimeUtc(path, t1) + stamps.Invalidate path + Assert.Equal(t1, stamps.GetLastWriteTimeUtc path)) + + [] + let ``Stopping the last watch on a path falls back to stat`` () = + withTempFile (fun path -> + let watcher = MockFileChangeWatcher() + use tracker = new FSharpReferenceChangeTracker(watcher, ignore, testDelay) + let stamps = tracker :> IReferenceStamps + + tracker.StartWatchingReference path + tracker.StartWatchingReference path + Assert.Equal(t0, stamps.GetLastWriteTimeUtc path) + + File.SetLastWriteTimeUtc(path, t1) + tracker.StopWatchingReference path + Assert.Equal(t0, stamps.GetLastWriteTimeUtc path) + + tracker.StopWatchingReference path + Assert.Equal(t1, stamps.GetLastWriteTimeUtc path)) From 8ce6db9ed6462f9ff8b69b985b7c89802735d675 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 03:53:48 +0200 Subject: [PATCH 076/133] Watch each project's reference set for its stamps The reactor registers the '-r:' paths of every project it computes options for, diffing against the previous set so an unchanged list touches no watches, and exposes the tracker's stamps. It passes no change handler: invalidating the FCS build is Roslyn's job, which swaps the MetadataReference and bumps Project.Version. --- .../FSharpProjectOptionsManager.fs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 7249fc768ff..6f572fd1b73 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -164,9 +164,11 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let disposeSingleFileCacheEntry ({ Subscription = subscription }: SingleFileCacheEntry) = subscription |> ValueOption.iter (fun subscription -> subscription.Dispose()) - // Push invalidation for on-disk '-r:' reference assemblies, which the Roslyn workspace does not - // track. A project's cached options stay valid (same paths); only the FCS build behind them goes - // stale. A script's options are dropped, since its watched set also covers '#load' sources. + // The '-r:' set of each project is watched for the reference stamps the snapshot-reuse + // guard reads; invalidating a project's FCS build on a change is Roslyn's job (it swaps the + // MetadataReference and bumps Project.Version, which reaches tryComputeOptions). A script's + // options are dropped instead: its watched set also covers '#load' sources, which the + // workspace does not track. let referenceWatches = ConcurrentDictionary>() let scriptReferenceWatches = ConcurrentDictionary>() @@ -180,13 +182,6 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let onWatchedReferenceChanged (path: string) = ReferenceStampCache.Invalidate path - for KeyValue(projectId, paths) in referenceWatches do - if paths.Contains path then - match cache.TryGetValue projectId with - | true, struct (_, _, projectOptions) -> - checker.InvalidateConfiguration(projectOptions, userOpName = "onWatchedReferenceChanged") - | _ -> () - for KeyValue(documentId, paths) in scriptReferenceWatches do if paths.Contains path then match singleFileCache.TryRemove documentId with @@ -583,7 +578,9 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch if not (Seq.isEmpty projectsToClearCache) then projectsToClearCache - |> Seq.iter (fun pair -> cache.TryRemove pair.Key |> ignore) + |> Seq.iter (fun pair -> + cache.TryRemove pair.Key |> ignore + clearReferenceWatches pair.Key) let options = projectsToClearCache @@ -752,6 +749,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch member _.ScriptUpdated = scriptUpdatedEvent.Publish + member _.ReferenceStamps = referenceChangeTracker :> IReferenceStamps + interface IDisposable with member this.Dispose() = this.ClearAllCaches() @@ -834,4 +833,6 @@ type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Wor member _.ClearAllCaches() = reactor.ClearAllCaches() + member _.ReferenceStamps = reactor.ReferenceStamps + member _.Checker = checker From 494422c88fb93dd10b5c709c8d75f077f171a1da Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 03:53:49 +0200 Subject: [PATCH 077/133] Read snapshot reference stamps from the tracker instead of stat'ing The snapshot-reuse guard compared ReferencesOnDisk by stat'ing every '-r:' on each new Project instance, before the same-version fast path. It now reads the tracker's stamps, and on a mismatch drops the project's stamps so a missed notification costs one re-stat rather than a rebuild per Project instance. --- .../FSharpProjectOptionsManager.fs | 8 +--- .../LanguageService/FileChangeWatcher.fs | 18 -------- .../LanguageService/WorkspaceExtensions.fs | 43 ++++++++++++------- 3 files changed, 30 insertions(+), 39 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 6f572fd1b73..4d70eeb120c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -179,9 +179,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch option.Substring "-r:".Length } - let onWatchedReferenceChanged (path: string) = - ReferenceStampCache.Invalidate path - + let onWatchedScriptPathChanged (path: string) = for KeyValue(documentId, paths) in scriptReferenceWatches do if paths.Contains path then match singleFileCache.TryRemove documentId with @@ -190,10 +188,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch checker.ClearCache([ projectOptions ]) | _ -> () - do ReferenceStampCache.Enable() - let referenceChangeTracker = - new FSharpReferenceChangeTracker(fileChangeWatcher, onWatchedReferenceChanged) + new FSharpReferenceChangeTracker(fileChangeWatcher, onWatchedScriptPathChanged) let stopWatching (paths: HashSet) = for path in paths do diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index c609e1955ba..dd29d6f5cf9 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -66,24 +66,6 @@ type internal IFSharpFileChangeContext = type internal IFSharpFileChangeWatcher = abstract CreateContext: watchedDirectories: ImmutableArray -> IFSharpFileChangeContext -/// Process-wide cache of on-disk reference timestamps. Enabled when push-based file watching -/// is active: watched paths are invalidated point-wise by change notifications, so repeated -/// snapshot creation reads a dictionary instead of stat'ing every '-r:' reference again. -module internal ReferenceStampCache = - let private stamps = - ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) - - let mutable private enabled = false - - let Enable () = enabled <- true - - let Invalidate (path: string) = stamps.TryRemove path |> ignore - - let GetLastWriteTimeUtc (path: string) = - if enabled then - stamps.GetOrAdd(path, (fun p -> IO.File.GetLastWriteTimeUtc p)) - else - IO.File.GetLastWriteTimeUtc path /// Last-write stamps of watched reference files; a path nobody watches is stat'd directly. type internal IReferenceStamps = abstract GetLastWriteTimeUtc: fullFilePath: string -> DateTime diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 567d88a9135..43e7e793f60 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -18,6 +18,7 @@ open CancellableTasks open System.IO open Internal.Utilities.Collections +open Internal.Utilities.Library open Newtonsoft.Json open Newtonsoft.Json.Linq open System.Text.Json.Nodes @@ -238,17 +239,33 @@ module private CheckerExtensions = |> CancellableTask.whenAll |> CancellableTask.map (Seq.map (fun x -> x.ToString()) >> Set) - let getOnDiskReferences (options: FSharpProjectOptions) = - options.OtherOptions - |> Seq.filter (fun x -> x.StartsWith("-r:")) - |> Seq.map (fun x -> - let path = x.Substring(3) + let getOnDiskReferences (stamps: IReferenceStamps) (options: FSharpProjectOptions) = + [ + for option in options.OtherOptions do + if option.StartsWithOrdinal "-r:" then + let path = option.Substring "-r:".Length - { - Path = path - LastModified = ReferenceStampCache.GetLastWriteTimeUtc path - }) - |> Seq.toList + { + Path = path + LastModified = stamps.GetLastWriteTimeUtc path + } + ] + + // A snapshot's own ReferencesOnDisk come from FCS stat'ing the files, so a mismatch with the + // cached stamps means one side is behind; dropping the stamps costs one re-stat instead of a + // rebuild on every future Project instance after a missed notification. + let referencesOnDiskChanged (project: Project) (oldSnapshot: FSharpProjectSnapshot) options = + let stamps = + project.Solution.GetFSharpWorkspaceService().FSharpProjectOptionsManager.ReferenceStamps + + let current = getOnDiskReferences stamps options + let changed = current <> oldSnapshot.ProjectSnapshot.ReferencesOnDisk + + if changed then + for reference in current do + stamps.Invalidate reference.Path + + changed let createProjectSnapshot (snapshotAccumulatorOpt) (project: Project) (options: FSharpProjectOptions option) = cancellableTask { @@ -272,9 +289,7 @@ module private CheckerExtensions = System.Diagnostics.Trace.TraceWarning "Reference versions changed" None - | true, (true, (_, _, _, _, oldSnapshot: FSharpProjectSnapshot)) when - oldSnapshot.ProjectSnapshot.ReferencesOnDisk <> (getOnDiskReferences options) - -> + | true, (true, (_, _, _, _, oldSnapshot: FSharpProjectSnapshot)) when referencesOnDiskChanged project oldSnapshot options -> System.Diagnostics.Trace.TraceWarning "References on disk changed" None @@ -321,7 +336,6 @@ module private CheckerExtensions = | _ -> None let! newSnapshot = - match updatedSnapshot with | Some snapshot -> snapshot | _ -> @@ -699,7 +713,6 @@ type Document with let! checker, _, _, projectOptions = this.GetFSharpCompilationOptionsAsync(userOpName) let! symbolUses = - if this.Project.UseTransparentCompiler then checker.FindBackgroundReferencesInFile(this.FilePath, projectSnapshot, symbol) else From 9808777338182babf3928dd5c312baef3ce933c2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 03:53:49 +0200 Subject: [PATCH 078/133] Document the stamp consumer and add its release note --- docs/ide/file-watching.md | 67 +++++++++++++------- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/docs/ide/file-watching.md b/docs/ide/file-watching.md index 2e6073f3113..53c00399a63 100644 --- a/docs/ide/file-watching.md +++ b/docs/ide/file-watching.md @@ -4,13 +4,24 @@ Roslyn's `FileChangeWatcher` and `ReferenceFileChangeTracker` (both internal to `Microsoft.VisualStudio.LanguageServices` and not exposed through `ExternalAccess.FSharp`). -## Why a watcher +## What the workspace already gives us -The Roslyn workspace tracks documents, not the `-r:` references an F# project compiles against. -When a referenced assembly is rebuilt outside VS nothing tells the F# language service; FCS only -notices because it stats every reference again on the next request (`IsReferencesInvalidated` on -the incremental builder, `ReferencesOnDisk` when a snapshot is reused). The watcher turns that into -a push: one notification per changed path, delivered to the projects that reference it. +Not every on-disk change needs this watcher. A `-r:` that the Roslyn workspace holds as a +`MetadataReference` is already watched by Roslyn: `ProjectSystemProjectFactory` advises every +reference path, and when one changes it swaps the reference on the solution, which bumps +`Project.Version`. `FSharpProjectOptionsReactor` sees that version through `isProjectInvalidated`, +recomputes, and calls `InvalidateConfiguration` — measured in VS, that path wins the race against +a watcher subscribed to the same file, because Roslyn batches over 500 ms where this tracker +additionally debounces for 2 s. + +So a second subscription to the same reference set buys nothing. What the workspace does *not* +cover is everything it has no document or reference for, and every stat FCS still performs +internally: + +- `#load` sources of a script: not documents, not references, invisible to the workspace. +- `IsReferencesInvalidated` on the incremental builder, which stats every reference on every + request. +- `ReferencesOnDisk` on snapshot reuse, which did the same per comparison — the consumer below. ## Shape @@ -20,28 +31,36 @@ a push: one notification per changed path, delivered to the projects that refere - **Batching.** Subscribe/unsubscribe operations go through a single-consumer queue with a 500 ms window (Roslyn's empirical value for solution open/close). Consecutive operations of the same kind, and for file watches the same sink, are coalesced into one service call. -- **Directory watches.** Each context starts with recursive `.dll` watches on the places - reference assemblies live: `DOTNET_ROOT/packs` and the machine-wide `dotnet/packs`, the .NET - Framework reference assemblies, and the NuGet cache (`NUGET_PACKAGES` or `~/.nuget/packages`). - A file under one of them costs no cookie of its own. Roslyn does not watch the NuGet cache; we - do because every `-r:` is watched uniformly and package assemblies are the bulk of them, so the - alternative is a per-file advise for each. +- **Directory watches.** A context starts with recursive `.dll` watches on the places reference + assemblies live: `DOTNET_ROOT/packs` and the machine-wide `dotnet/packs`, the .NET Framework + reference assemblies, and the NuGet cache (`NUGET_PACKAGES` or `~/.nuget/packages`). A file + under one of them costs no cookie of its own. Roslyn does not watch the NuGet cache; a consumer + that watches every `-r:` uniformly wants it, since package assemblies are the bulk of them and + the alternative is a per-file advise for each. - **Per-file watches.** Paths outside those directories (project outputs, loose assemblies) get - an individual advise, ref-counted across projects by `FSharpReferenceChangeTracker`. + an individual advise, ref-counted across consumers by `FSharpReferenceChangeTracker`. The + tracker also keeps the last-write stamp of each watched path (see Consumers). - **Debounce.** A rebuild writes a temp file and renames it, producing several notifications; the tracker fires one callback per path after 2 s of quiet. -## Consumer +## Consumers -`FSharpProjectOptionsReactor` watches the `-r:` set of every project it computes options for and -calls `FSharpChecker.InvalidateConfiguration` for each project that references a changed path. -The cached options stay valid (same paths); only the FCS build behind them is stale. Watch sets -are diffed on recompute, so an unchanged reference list touches nothing. +**Snapshot reference stamps.** `FSharpProjectOptionsReactor` watches the `-r:` set of every +project it computes options for, diffed on recompute so an unchanged set touches nothing. The +tracker keeps each path's last-write stamp inside its watch entry and drops it on the raw change +notification, before the debounce. The `ReferencesOnDisk` guard in `createProjectSnapshot` reads +stamps through `IReferenceStamps`, so the comparison that runs for every new `Project` instance is +a dictionary read per reference instead of a stat. A path nobody watches is stat'd directly. A +mismatch against the snapshot's own stamps (FCS stats when it builds a snapshot) drops the +project's stamps, so a missed notification costs one re-stat pass rather than a rebuild per +`Project` instance. The reactor watch exists for these stamps, not to invalidate the FCS build — +Roslyn already does that, as the previous section says. -## Follow-ups +Still to come: -1. A reference-change notification for the incremental builder on the FCS side, the analogue of - `useChangeNotifications` for sources, so `IsReferencesInvalidated` stops stat'ing every - reference on every request. -2. A watcher-invalidated timestamp cache for snapshot reuse (`ReferencesOnDisk`). -3. Scripts: watch `#r` references and `#load` sources the same way. +1. Scripts: watch `#load` sources so an edit outside the editor drops the cached options for + that document. +2. `FSharpProjectSnapshot.FromOptions` stats every `-r:` when a snapshot is built from scratch; + an overload taking host-supplied stamps lets it read the same cache. +3. A reference-change notification for the incremental builder on the FCS side, the analogue of + `useChangeNotifications` for sources, so `IsReferencesInvalidated` stops stat'ing at all. diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 96e179afd39..faf7af65b11 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -14,7 +14,7 @@ * Prevent stale and leaked per-document diagnostics by tightening cache validity and evicting entries for removed documents. ([PR #20121](https://github.com/dotnet/fsharp/pull/20121)) * Avoid blocking the VS project-options mailbox and unnecessary script option recomputation while editing by making invalidation async and debouncing caret-driven updates. ([Issue #20124](https://github.com/dotnet/fsharp/issues/20124), [Issue #20125](https://github.com/dotnet/fsharp/issues/20125), [PR #20126](https://github.com/dotnet/fsharp/pull/20126)) * Cache document diagnostics by version stamp, so an unchanged document is not reanalyzed on every crawler pass. ([Issue #20120](https://github.com/dotnet/fsharp/issues/20120), [PR #20121](https://github.com/dotnet/fsharp/pull/20121)) -* Watch on-disk `-r:` reference assemblies via `IVsAsyncFileChangeEx2` so F# project options and snapshots invalidate on rebuild instead of polling timestamps. +* Transparent compiler snapshot reuse no longer stats every `-r:` reference on each project change; reference timestamps are cached and invalidated by `IVsAsyncFileChangeEx2` notifications. ([PR #20457](https://github.com/dotnet/fsharp/pull/20457)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) From 39bd3943d0a1aef5d541430800e7ccfb1898112b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 04:10:17 +0200 Subject: [PATCH 079/133] Expose Dispose directly on the tracker and sort the reactor's opens FSharpReferenceChangeTracker gets a public Dispose with the interface forwarding to it, the MailboxProcessor pattern, so the reactor disposes it and the agent without casts. The reactor's opens follow the System / FSharp.Compiler / Microsoft / Internal.Utilities grouping. --- .../FSharpProjectOptionsManager.fs | 7 +++--- .../LanguageService/FileChangeWatcher.fs | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 4d70eeb120c..5c6c8875aeb 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -11,13 +11,14 @@ open System.Linq open System.Runtime.CompilerServices open System.Threading open System.Threading.Tasks -open Microsoft.CodeAnalysis open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Text +open Microsoft.CodeAnalysis open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.FSharp.Interactive.Session open Microsoft.VisualStudio.TextManager.Interop + open Internal.Utilities.Library open CancellableTasks @@ -750,10 +751,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch interface IDisposable with member this.Dispose() = this.ClearAllCaches() - (referenceChangeTracker :> IDisposable).Dispose() + referenceChangeTracker.Dispose() cancellationTokenSource.Cancel() cancellationTokenSource.Dispose() - (agent :> IDisposable).Dispose() + agent.Dispose() /// Manages mappings of Roslyn workspace Projects/Documents to FCS. type internal FSharpProjectOptionsManager(checker: FSharpChecker, workspace: Workspace, fileChangeWatcher: IFSharpFileChangeWatcher) = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index dd29d6f5cf9..e4ae0dbee22 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -476,17 +476,19 @@ type internal FSharpReferenceChangeTracker(watcher: IFSharpFileChangeWatcher, on | true, entry -> entry.Stamp <- ValueNone | _ -> ()) - interface IDisposable with - member _.Dispose() = - lock gate (fun () -> - if not disposed then - disposed <- true - watchedFiles.Clear() + member _.Dispose() = + lock gate (fun () -> + if not disposed then + disposed <- true + watchedFiles.Clear() - for KeyValue(_, timer) in pendingTimers do - timer.Dispose() + for KeyValue(_, timer) in pendingTimers do + timer.Dispose() - pendingTimers.Clear() + pendingTimers.Clear() - if context.IsValueCreated then - context.Value.Dispose()) + if context.IsValueCreated then + context.Value.Dispose()) + + interface IDisposable with + member this.Dispose() = this.Dispose() From 62326df2b107d7ed58f2a876fd4abde06b3f7a4f Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 05:19:19 +0200 Subject: [PATCH 080/133] Dispose the watcher's agent directly --- .../src/FSharp.Editor/LanguageService/FileChangeWatcher.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index e4ae0dbee22..a714280fbb2 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -262,7 +262,7 @@ type internal FSharpFileChangeWatcher(fileChangeService: Task IDisposable).Dispose() + agent.Dispose() and [] private FileChangeContext(enqueue: WatcherOperation -> unit, watchedDirectories: ImmutableArray) as this = From 30c9f66c7b7adfd988ef2ad35fb49c7686df3c23 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 05:28:55 +0200 Subject: [PATCH 081/133] Stop Debug builds writing StandardOutput.txt/StandardError.txt into the project directory FxResolver.executeProcess dumped the captured stdout/stderr of the `dotnet --version` probe into workingDir under #if DEBUG. workingDir is the project directory (or the script's directory via GetProjectOptionsFromScript), so a Debug VSIX in Visual Studio left StandardOutput.txt/StandardError.txt inside SDK-style project folders. Every new file in a project cone makes CPS re-evaluate the project, which runs a design-time build, nominates it to NuGet, restores it and its dependents, forks the Roslyn solution and rebuilds every F# snapshot - a loop that kept devenv at 250-500% CPU while idle. The output is already returned to the caller and reported through scriptSdkNotDetermined on failure, so the files carried no extra diagnostics. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Driver/FxResolver.fs | 5 ----- 2 files changed, 1 insertion(+), 5 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 e9404e58f6d..f4e23fb54fd 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Debug builds of FCS no longer write `StandardOutput.txt`/`StandardError.txt` into the project (or script) directory when probing `dotnet --version` for the SDK. With a Debug VSIX in Visual Studio those files landed inside SDK-style project folders and kept re-triggering project re-evaluation -> design-time build -> NuGet restore -> F# re-check. * 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/Driver/FxResolver.fs b/src/Compiler/Driver/FxResolver.fs index 0341d8fe0c8..e82b503255b 100644 --- a/src/Compiler/Driver/FxResolver.fs +++ b/src/Compiler/Driver/FxResolver.fs @@ -96,12 +96,7 @@ type internal FxResolver raise (TimeoutException(sprintf "Timeout executing command '%s' '%s'" psi.FileName psi.Arguments)) else p.WaitForExit() -#if DEBUG - if workingDir.IsSome then - FileSystem.OpenFileForWriteShim(Path.Combine(workingDir.Value, "StandardOutput.txt")).WriteAllLines(outputList) - FileSystem.OpenFileForWriteShim(Path.Combine(workingDir.Value, "StandardError.txt")).WriteAllLines(errorsList) -#endif p.ExitCode, outputList.ToArray(), errorsList.ToArray() else -1, Array.empty, Array.empty From 25c0004bc26e87a28b7f9820fcea0f4dba7d8c19 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 05:31:22 +0200 Subject: [PATCH 082/133] Link the FxResolver release note to PR #20458 Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f4e23fb54fd..241f11546d4 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,6 @@ ### Fixed -* Debug builds of FCS no longer write `StandardOutput.txt`/`StandardError.txt` into the project (or script) directory when probing `dotnet --version` for the SDK. With a Debug VSIX in Visual Studio those files landed inside SDK-style project folders and kept re-triggering project re-evaluation -> design-time build -> NuGet restore -> F# re-check. +* Debug builds of FCS no longer write `StandardOutput.txt`/`StandardError.txt` into the project (or script) directory when probing `dotnet --version` for the SDK. With a Debug VSIX in Visual Studio those files landed inside SDK-style project folders and kept re-triggering project re-evaluation -> design-time build -> NuGet restore -> F# re-check. ([PR #20458](https://github.com/dotnet/fsharp/pull/20458)) * 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)) From f4389d90952adae8bb58b9a421157998961050fc Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 06:16:45 +0200 Subject: [PATCH 083/133] Call String.StartsWith/EndsWith directly instead of the illib helpers Inline members of the internal Internal.Utilities.Library module cannot be inlined into another assembly, InternalsVisibleTo or not: the optimizer drops their optimization data at the assembly boundary, so FSharp.Editor fails with FS1116/FS1118 under --optimize+ (every Windows Release leg of the CI). Debug compiled only because --optimize- never tries to inline them. Also formats WorkspaceExtensions.fs. Co-Authored-By: Claude Fable 5.1 --- .../LanguageService/FSharpProjectOptionsManager.fs | 3 +-- .../LanguageService/FileChangeWatcher.fs | 11 +++++------ .../LanguageService/WorkspaceExtensions.fs | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 5c6c8875aeb..a17e793dc86 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -19,7 +19,6 @@ open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.FSharp.Interactive.Session open Microsoft.VisualStudio.TextManager.Interop -open Internal.Utilities.Library open CancellableTasks #nowarn "57" @@ -176,7 +175,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let referencePaths (projectOptions: FSharpProjectOptions) = seq { for option in projectOptions.OtherOptions do - if option.StartsWithOrdinal "-r:" then + if option.StartsWith("-r:", StringComparison.Ordinal) then option.Substring "-r:".Length } diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs index a714280fbb2..399f24464ed 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs @@ -12,8 +12,6 @@ open Microsoft.VisualStudio open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop -open Internal.Utilities.Library - open Microsoft.VisualStudio.FSharp.Editor.DebugHelpers open CancellableTasks @@ -28,14 +26,14 @@ open CancellableTasks [] type internal WatchedDirectory(path: string, extensionFilters: ImmutableArray) = let path = - if path.EndsWithOrdinal(string IO.Path.DirectorySeparatorChar) then + if path.EndsWith(string IO.Path.DirectorySeparatorChar, StringComparison.Ordinal) then path else $"{path}{IO.Path.DirectorySeparatorChar}" do for filter in extensionFilters do - if not (filter.StartsWithOrdinal ".") then + if not (filter.StartsWith(".", StringComparison.Ordinal)) then invalidArg (nameof extensionFilters) $"Filter '{filter}' must start with a period." member _.Path = path @@ -44,9 +42,10 @@ type internal WatchedDirectory(path: string, extensionFilters: ImmutableArray, filePath: string) = watchedDirectories |> Seq.exists (fun w -> - filePath.StartsWithOrdinalIgnoreCase w.Path + filePath.StartsWith(w.Path, StringComparison.OrdinalIgnoreCase) && (w.ExtensionFilters.IsEmpty - || w.ExtensionFilters |> Seq.exists filePath.EndsWithOrdinalIgnoreCase)) + || w.ExtensionFilters + |> Seq.exists (fun filter -> filePath.EndsWith(filter, StringComparison.OrdinalIgnoreCase)))) /// A single watched file; disposing stops watching. type internal IFSharpWatchedFile = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 43e7e793f60..86f53d1e3fa 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -18,7 +18,6 @@ open CancellableTasks open System.IO open Internal.Utilities.Collections -open Internal.Utilities.Library open Newtonsoft.Json open Newtonsoft.Json.Linq open System.Text.Json.Nodes @@ -242,7 +241,7 @@ module private CheckerExtensions = let getOnDiskReferences (stamps: IReferenceStamps) (options: FSharpProjectOptions) = [ for option in options.OtherOptions do - if option.StartsWithOrdinal "-r:" then + if option.StartsWith("-r:", StringComparison.Ordinal) then let path = option.Substring "-r:".Length { From d17ac6d1ac2c3be2e0bfcfc6fff0c5a88ed76878 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 06:07:23 +0200 Subject: [PATCH 084/133] Let FromOptions take reference stamps from the host FSharpProjectSnapshot.FromOptions stats every -r: reference each time a snapshot is built from options; under the transparent compiler in Visual Studio that is every from-scratch snapshot after an options recompute. A host that already tracks the last-write times of its references can pass getReferenceStamp instead; the default stays FileSystem.GetLastWriteTimeShim. The callback is threaded through the recursive call for referenced F# projects. Co-Authored-By: Claude Fable 5.1 --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Service/FSharpProjectSnapshot.fs | 10 +- .../FSharpChecker/ProjectSnapshot.fs | 154 +++++++----------- ...iler.Service.SurfaceArea.netstandard20.bsl | 2 +- 4 files changed, 64 insertions(+), 103 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 241f11546d4..d5c98fc3867 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -181,6 +181,7 @@ * IL: hold custom attributes in fields rather than a union case ([PR #20287](https://github.com/dotnet/fsharp/pull/20287)) * IL: reuse the cached ILTypeRef in ILTypeInfo.FromType ([PR #20255](https://github.com/dotnet/fsharp/pull/20255)) * Name resolution: group C#-style extension members per `open` and extended type ([PR #20298](https://github.com/dotnet/fsharp/pull/20298)) +* `FSharpProjectSnapshot.FromOptions` takes an optional `getReferenceStamp: string -> DateTime`, so a host that already tracks the last-write times of on-disk references can supply them instead of having every `-r:` stat'd again each time a snapshot is built. ([PR #20459](https://github.com/dotnet/fsharp/pull/20459)) ### Improved diff --git a/src/Compiler/Service/FSharpProjectSnapshot.fs b/src/Compiler/Service/FSharpProjectSnapshot.fs index 71354fe3adb..dbbff3e501c 100644 --- a/src/Compiler/Service/FSharpProjectSnapshot.fs +++ b/src/Compiler/Service/FSharpProjectSnapshot.fs @@ -643,8 +643,11 @@ and [] FSha ProjectSnapshotBase(projectConfig, referencedProjects, sourceFiles) |> FSharpProjectSnapshot - static member FromOptions(options: FSharpProjectOptions, getFileSnapshot, ?snapshotAccumulator) = + static member FromOptions + (options: FSharpProjectOptions, getFileSnapshot, ?snapshotAccumulator, ?getReferenceStamp: string -> DateTime) + = let snapshotAccumulator = defaultArg snapshotAccumulator (Dictionary()) + let getReferenceStamp = defaultArg getReferenceStamp FileSystem.GetLastWriteTimeShim async { @@ -661,7 +664,8 @@ and [] FSha |> Seq.map (function | FSharpReferencedProject.FSharpReference(outputName, options) -> async { - let! snapshot = FSharpProjectSnapshot.FromOptions(options, getFileSnapshot, snapshotAccumulator) + let! snapshot = + FSharpProjectSnapshot.FromOptions(options, getFileSnapshot, snapshotAccumulator, getReferenceStamp) return FSharpReferencedProjectSnapshot.FSharpReference(outputName, snapshot) } @@ -682,7 +686,7 @@ and [] FSha { Path = path - LastModified = FileSystem.GetLastWriteTimeShim(path) + LastModified = getReferenceStamp path }) ) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs index 90e28cc63ac..c6a7a522e33 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs @@ -1,104 +1,60 @@ module FSharpChecker.ProjectSnapshot -open Xunit open System +open System.IO +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.ProjectSnapshot +open Xunit - -// TODO: restore tests - -//[] -//let WithoutImplFilesThatHaveSignatures () = - -// let snapshot = FSharpProjectSnapshot.Create( -// projectFileName = "Dummy.fsproj", -// projectId = None, -// sourceFiles = [ -// { FileName = "A.fsi"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "A.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "B.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "C.fsi"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "C.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// ], -// referencesOnDisk = [], -// otherOptions = [], -// referencedProjects = [], -// isIncompleteTypeCheckEnvironment = true, -// useScriptResolutionRules = false, -// loadTime = DateTime(1234, 5, 6), -// unresolvedReferences = None, -// originalLoadReferences = [], -// stamp = None -// ) - -// let result = snapshot.WithoutImplFilesThatHaveSignatures - -// let expected = [| "A.fsi"; "B.fs"; "C.fsi" |] - -// Assert.Equal(expected, result.SourceFileNames |> List.toArray) - -// Assert.Equal(result.FullVersion, snapshot.SignatureVersion) - -//[] -//let WithoutImplFilesThatHaveSignaturesExceptLastOne () = - -// let snapshot = FSharpProjectSnapshot.Create( -// projectFileName = "Dummy.fsproj", -// projectId = None, -// sourceFiles = [ -// { FileName = "A.fsi"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "A.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "B.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "C.fsi"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "C.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// ], -// referencesOnDisk = [], -// otherOptions = [], -// referencedProjects = [], -// isIncompleteTypeCheckEnvironment = true, -// useScriptResolutionRules = false, -// loadTime = DateTime(1234, 5, 6), -// unresolvedReferences = None, -// originalLoadReferences = [], -// stamp = None -// ) - -// let result = snapshot.WithoutImplFilesThatHaveSignaturesExceptLastOne - -// let expected = [| "A.fsi"; "B.fs"; "C.fsi"; "C.fs" |] - -// Assert.Equal(expected, result.SourceFileNames |> List.toArray) - -// Assert.Equal(result.FullVersion, snapshot.LastFileVersion) - - -//[] -//let WithoutImplFilesThatHaveSignaturesExceptLastOne_2 () = - -// let snapshot = FSharpProjectSnapshot.Create( -// projectFileName = "Dummy.fsproj", -// projectId = None, -// sourceFiles = [ -// { FileName = "A.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "B.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// { FileName = "C.fs"; Version = "1"; GetSource = Unchecked.defaultof<_> } -// ], -// referencesOnDisk = [], -// otherOptions = [], -// referencedProjects = [], -// isIncompleteTypeCheckEnvironment = true, -// useScriptResolutionRules = false, -// loadTime = DateTime(1234, 5, 6), -// unresolvedReferences = None, -// originalLoadReferences = [], -// stamp = None -// ) - -// let result = snapshot.WithoutImplFilesThatHaveSignaturesExceptLastOne - -// let expected = [| "A.fs"; "B.fs"; "C.fs" |] - -// Assert.Equal(expected, result.SourceFileNames |> List.toArray) - -// Assert.Equal(result.FullVersion, snapshot.LastFileVersion) - +#nowarn "57" + +let private projectOptions projectFileName references referencedProjects = + { + ProjectFileName = projectFileName + ProjectId = None + SourceFiles = [| Path.ChangeExtension(projectFileName, ".fs") |] + OtherOptions = [| for path in references -> $"-r:{path}" |] + ReferencedProjects = referencedProjects + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime.UtcNow + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = None + } + +let private emptySource _ path = + async { return FSharpFileSnapshot.CreateFromString(path, "") } + +[] +let ``FromOptions takes reference stamps from the host, including referenced projects`` () = + let stamps = + dict + [ + "MainRef.dll", DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) + "LibRef.dll", DateTime(2026, 2, 2, 0, 0, 0, DateTimeKind.Utc) + ] + + let lib = projectOptions "Lib.fsproj" [ "LibRef.dll" ] [||] + + let main = + projectOptions "Main.fsproj" [ "MainRef.dll" ] [| FSharpReferencedProject.FSharpReference("Lib.dll", lib) |] + + let snapshot = + FSharpProjectSnapshot.FromOptions(main, emptySource, getReferenceStamp = (fun path -> stamps[path])) + |> Async.RunSynchronously + + let libSnapshot = + match snapshot.ReferencedProjects with + | [ FSharpReferencedProjectSnapshot.FSharpReference(_, lib) ] -> lib + | other -> failwith $"Expected one referenced project, got %A{other}" + + Assert.Equal( + [ { Path = "MainRef.dll"; LastModified = stamps["MainRef.dll"] } ], + snapshot.ReferencesOnDisk + ) + + Assert.Equal( + [ { Path = "LibRef.dll"; LastModified = stamps["LibRef.dll"] } ], + libSnapshot.ReferencesOnDisk + ) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index de6a4b053d6..d33433ac3fb 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -2502,7 +2502,7 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FS FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]] OriginalLoadReferences FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]] get_OriginalLoadReferences() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] FromOptions(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, FSharp.Compiler.CodeAnalysis.DocumentSource) -FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] FromOptions(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.CodeAnalysis.FSharpProjectOptions,Microsoft.FSharp.Core.FSharpFunc`2[System.String,Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot]]], Microsoft.FSharp.Core.FSharpOption`1[System.Collections.Generic.Dictionary`2[FSharp.Compiler.CodeAnalysis.FSharpProjectOptions,FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot]]) +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] FromOptions(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.CodeAnalysis.FSharpProjectOptions,Microsoft.FSharp.Core.FSharpFunc`2[System.String,Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot]]], Microsoft.FSharp.Core.FSharpOption`1[System.Collections.Generic.Dictionary`2[FSharp.Compiler.CodeAnalysis.FSharpProjectOptions,FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot]], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[System.String,System.DateTime]]) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] FromOptions(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, System.String, Int32, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.DocumentSource) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpUnresolvedReferencesSet] UnresolvedReferences FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpUnresolvedReferencesSet] get_UnresolvedReferences() From 6e4a76f5b7d36e41aac691a823e4034860477588 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 06:35:19 +0200 Subject: [PATCH 085/133] Make the FromOptions stamp test asynchronous Same test as in #20459 after review: a Task-returning xunit test with task { } instead of Async.RunSynchronously. Co-Authored-By: Claude Fable 5.1 --- .../FSharpChecker/ProjectSnapshot.fs | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs index c6a7a522e33..900b8aa04b0 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/ProjectSnapshot.fs @@ -2,6 +2,7 @@ module FSharpChecker.ProjectSnapshot open System open System.IO +open System.Threading.Tasks open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.ProjectSnapshot open Xunit @@ -27,34 +28,34 @@ let private emptySource _ path = async { return FSharpFileSnapshot.CreateFromString(path, "") } [] -let ``FromOptions takes reference stamps from the host, including referenced projects`` () = - let stamps = - dict - [ - "MainRef.dll", DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) - "LibRef.dll", DateTime(2026, 2, 2, 0, 0, 0, DateTimeKind.Utc) - ] - - let lib = projectOptions "Lib.fsproj" [ "LibRef.dll" ] [||] - - let main = - projectOptions "Main.fsproj" [ "MainRef.dll" ] [| FSharpReferencedProject.FSharpReference("Lib.dll", lib) |] - - let snapshot = - FSharpProjectSnapshot.FromOptions(main, emptySource, getReferenceStamp = (fun path -> stamps[path])) - |> Async.RunSynchronously - - let libSnapshot = - match snapshot.ReferencedProjects with - | [ FSharpReferencedProjectSnapshot.FSharpReference(_, lib) ] -> lib - | other -> failwith $"Expected one referenced project, got %A{other}" - - Assert.Equal( - [ { Path = "MainRef.dll"; LastModified = stamps["MainRef.dll"] } ], - snapshot.ReferencesOnDisk - ) - - Assert.Equal( - [ { Path = "LibRef.dll"; LastModified = stamps["LibRef.dll"] } ], - libSnapshot.ReferencesOnDisk - ) +let ``FromOptions takes reference stamps from the host, including referenced projects`` () : Task = + task { + let stamps = + dict + [ + "MainRef.dll", DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) + "LibRef.dll", DateTime(2026, 2, 2, 0, 0, 0, DateTimeKind.Utc) + ] + + let lib = projectOptions "Lib.fsproj" [ "LibRef.dll" ] [||] + + let main = + projectOptions "Main.fsproj" [ "MainRef.dll" ] [| FSharpReferencedProject.FSharpReference("Lib.dll", lib) |] + + let! snapshot = FSharpProjectSnapshot.FromOptions(main, emptySource, getReferenceStamp = (fun path -> stamps[path])) + + let libSnapshot = + match snapshot.ReferencedProjects with + | [ FSharpReferencedProjectSnapshot.FSharpReference(_, lib) ] -> lib + | other -> failwith $"Expected one referenced project, got %A{other}" + + Assert.Equal( + [ { Path = "MainRef.dll"; LastModified = stamps["MainRef.dll"] } ], + snapshot.ReferencesOnDisk + ) + + Assert.Equal( + [ { Path = "LibRef.dll"; LastModified = stamps["LibRef.dll"] } ], + libSnapshot.ReferencesOnDisk + ) + } From 81329bf5d04f8e93a5e1a6b2ebd77573c4aa47c5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 06:39:35 +0200 Subject: [PATCH 086/133] Reuse the in-memory PE reference of a C# project while its semantic version holds createPEReference keyed the reference on the identity of the Roslyn Compilation and stamped it with DateTime.UtcNow at creation. Roslyn recreates Compilation instances freely - every solution fork, and under memory pressure because it holds the final compilation weakly - so a dependent F# project kept getting a reference with a fresh stamp, which changed its snapshot base version, invalidated BootstrapInfo and re-imported all of its references before re-checking every file. A CPU trace of a 39-project solution showed TcImports.RegisterAndImportReferencedAssemblies recurring every 15-30 seconds for the whole life of the process, with GC at 20-50%. The reference is now cached per referenced project and reused while the project's dependent semantic version is unchanged; a newer Compilation only refreshes the source the delayed reader emits from. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.0.md | 1 + .../FSharpProjectOptionsManager.fs | 73 ++++++++++++++----- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.0.md b/docs/release-notes/.VisualStudio/18.0.md index 6344731e3d5..e802c99e59e 100644 --- a/docs/release-notes/.VisualStudio/18.0.md +++ b/docs/release-notes/.VisualStudio/18.0.md @@ -1,4 +1,5 @@ ### Fixed +* Keep one in-memory PE reference per referenced C# project while its dependent semantic version is unchanged, instead of one per Roslyn `Compilation` instance. A `Compilation` recreated by Roslyn (solution fork, memory pressure) produced a reference with a fresh stamp, which invalidated the FCS bootstrap of every dependent F# project and re-imported all of its references; on a large solution this kept the type checker busy long after the solution had loaded. * Split package init into foreground+background, fix background analysis setting ([Issue #18623](https://github.com/dotnet/fsharp/issues/18623), [Issue #18904](https://github.com/dotnet/fsharp/issues/18904), [PR #18646](https://github.com/dotnet/fsharp/pull/18646)) * Fix incorrect `open` statement placement for modules with multiline attributes. ([Issue #18671](https://github.com/dotnet/fsharp/issues/18671)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index a17e793dc86..1c46f172608 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -8,7 +8,6 @@ open System.Collections.Concurrent open System.Collections.Immutable open System.IO open System.Linq -open System.Runtime.CompilerServices open System.Threading open System.Threading.Tasks open FSharp.Compiler @@ -127,6 +126,32 @@ type SingleFileCacheEntry = Subscription: ConnectionPointSubscription } +/// One in-memory PE reference per referenced project, kept while the project's dependent +/// semantic version is unchanged. Roslyn recreates `Compilation` instances freely (solution +/// forks, memory pressure), and a reference created per instance carries a fresh stamp that +/// invalidates every FCS cache keyed on it. +[] +type private PEReferenceCacheEntry(version: VersionStamp, compilation: Compilation) = + // Pinned until the first successful emit so the reader can always be materialised. + let mutable pinned = compilation + let latest = WeakReference(compilation) + + member _.Version = version + + member _.TryGetCompilation() = + match latest.TryGetTarget() with + | true, compilation -> ValueSome compilation + | _ -> ValueNone + + member _.Refresh(compilation: Compilation) = + latest.SetTarget compilation + + match pinned with + | null -> () + | _ -> pinned <- compilation + + member _.Emitted() = pinned <- Unchecked.defaultof<_> + [] type private FSharpProjectOptionsMessage = | TryGetOptionsByDocument of @@ -156,7 +181,9 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let singleFileCache = ConcurrentDictionary() - let weakPEReferences = ConditionalWeakTable() + let peReferences = + ConcurrentDictionary() + let lastSuccessfulCompilations = ConcurrentDictionary() let scriptUpdatedEvent = Event() @@ -235,10 +262,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch paths.UnionWith projectOptions.SourceFiles updateWatches scriptReferenceWatches documentId paths - let createNewPEReference (referencedProject: Project) (comp: Compilation) = + let createNewPEReference (referencedProject: Project) (entry: PEReferenceCacheEntry) = let projectId = referencedProject.Id - let mutable strongComp = comp - let weakComp = WeakReference(comp) let mutable stampTime = DateTime.UtcNow let getStream = @@ -253,12 +278,12 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let result = comp.Emit(ms, options = emitOptions, cancellationToken = ct) if result.Success then - strongComp <- Unchecked.defaultof<_> + entry.Emitted() lastSuccessfulCompilations.[projectId] <- comp ms.Position <- 0L ms :> Stream |> ValueSome else - strongComp <- Unchecked.defaultof<_> + entry.Emitted() ms.Dispose() ValueNone with @@ -267,14 +292,14 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch ms.Dispose() ValueNone | _ -> - strongComp <- Unchecked.defaultof<_> + entry.Emitted() ms.Dispose() ValueNone let resultOpt = - match weakComp.TryGetTarget() with - | true, comp -> tryStream comp - | _ -> ValueNone + match entry.TryGetCompilation() with + | ValueSome comp -> tryStream comp + | ValueNone -> ValueNone match resultOpt with | ValueSome _ -> resultOpt @@ -286,13 +311,16 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let getStampTime () = stampTime FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) - let createPEReference (referencedProject: Project) (comp: Compilation) = - match weakPEReferences.TryGetValue comp with - | true, fsRefProj -> fsRefProj + let createPEReference (referencedProject: Project) (version: VersionStamp) (comp: Compilation) = + match peReferences.TryGetValue referencedProject.Id with + | true, struct (entry, reference) when entry.Version = version -> + entry.Refresh comp + reference | _ -> - let fsRefProj = createNewPEReference referencedProject comp - weakPEReferences.Add(comp, fsRefProj) - fsRefProj + let entry = PEReferenceCacheEntry(version, comp) + let reference = createNewPEReference referencedProject entry + peReferences[referencedProject.Id] <- struct (entry, reference) + reference let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = cancellableTask { @@ -514,8 +542,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch ) elif referencedProject.SupportsCompilation then let! comp = referencedProject.GetCompilationAsync(ct) - let peRef = createPEReference referencedProject comp - referencedProjects.Add(peRef) + and! version = referencedProject.GetDependentSemanticVersionAsync(ct) + referencedProjects.Add(createPEReference referencedProject version comp) if canBail then return ValueNone @@ -590,6 +618,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch if not (currentSolution.ContainsProject(pair.Key)) then lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore + for projectId in peReferences.Keys |> Array.ofSeq do + if not (currentSolution.ContainsProject projectId) then + peReferences.TryRemove projectId |> ignore + checker.InvalidateConfiguration(projectOptions, userOpName = "tryComputeOptions") let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) @@ -681,6 +713,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch match cache.TryRemove(projectId) with | true, struct (_, _, projectOptions) -> lastSuccessfulCompilations.TryRemove(projectId) |> ignore + peReferences.TryRemove projectId |> ignore checker.ClearCache([ projectOptions ]) | _ -> () @@ -690,6 +723,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch match singleFileCache.TryRemove(documentId) with | true, ({ ProjectOptions = projectOptions } as cacheEntry) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore + peReferences.TryRemove documentId.ProjectId |> ignore checker.ClearCache([ projectOptions ]) disposeSingleFileCacheEntry cacheEntry | _ -> () @@ -736,6 +770,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch | _ -> () lastSuccessfulCompilations.Clear() + peReferences.Clear() for projectId in referenceWatches.Keys |> Array.ofSeq do clearReferenceWatches projectId From 5ad59b6637d3340d30be78ea2e99997d03b683e6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 06:50:11 +0200 Subject: [PATCH 087/133] Move the PE reference release note to 18.vNext.md and link PR #20460 18.0.md is the shipped-version file; upstream has been adding to 18.vNext.md since. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.0.md | 1 - docs/release-notes/.VisualStudio/18.vNext.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.VisualStudio/18.0.md b/docs/release-notes/.VisualStudio/18.0.md index e802c99e59e..6344731e3d5 100644 --- a/docs/release-notes/.VisualStudio/18.0.md +++ b/docs/release-notes/.VisualStudio/18.0.md @@ -1,5 +1,4 @@ ### Fixed -* Keep one in-memory PE reference per referenced C# project while its dependent semantic version is unchanged, instead of one per Roslyn `Compilation` instance. A `Compilation` recreated by Roslyn (solution fork, memory pressure) produced a reference with a fresh stamp, which invalidated the FCS bootstrap of every dependent F# project and re-imported all of its references; on a large solution this kept the type checker busy long after the solution had loaded. * Split package init into foreground+background, fix background analysis setting ([Issue #18623](https://github.com/dotnet/fsharp/issues/18623), [Issue #18904](https://github.com/dotnet/fsharp/issues/18904), [PR #18646](https://github.com/dotnet/fsharp/pull/18646)) * Fix incorrect `open` statement placement for modules with multiline attributes. ([Issue #18671](https://github.com/dotnet/fsharp/issues/18671)) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index faf7af65b11..de67e65cf26 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -6,6 +6,7 @@ ### Fixed +* Reuse the in-memory PE reference of a referenced C# project while its dependent semantic version is unchanged, instead of creating one per Roslyn `Compilation` instance. A recreated `Compilation` produced a reference with a fresh stamp, which invalidated the FCS bootstrap of every dependent F# project and made it re-import all of its references before re-checking every file. ([PR #20460](https://github.com/dotnet/fsharp/pull/20460)) * Fixed `MissingManifestResourceException` when opening F# pages under Tools > Options by giving `FSharpPackage` its own `VSPackage` resource set. ([PR #20408](https://github.com/dotnet/fsharp/pull/20408)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) From 515c68d9e736163381707c834bac2e8a8ea38eeb Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 06:57:06 +0200 Subject: [PATCH 088/133] Wrap the PE reference cache doc comment in and reference Compilation with Co-Authored-By: Claude Opus 5 (1M context) --- .../LanguageService/FSharpProjectOptionsManager.fs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 1c46f172608..47e32a9ebaa 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -126,10 +126,13 @@ type SingleFileCacheEntry = Subscription: ConnectionPointSubscription } -/// One in-memory PE reference per referenced project, kept while the project's dependent -/// semantic version is unchanged. Roslyn recreates `Compilation` instances freely (solution -/// forks, memory pressure), and a reference created per instance carries a fresh stamp that -/// invalidates every FCS cache keyed on it. +/// +/// The in-memory PE reference of a referenced project, kept while the project's dependent +/// semantic version is unchanged. Roslyn recreates +/// instances freely - on every solution fork, +/// and under memory pressure because it holds the final compilation weakly - and a reference +/// created per instance carries a fresh stamp that invalidates every FCS cache keyed on it. +/// [] type private PEReferenceCacheEntry(version: VersionStamp, compilation: Compilation) = // Pinned until the first successful emit so the reader can always be materialised. From 728049e3824eb061736029a190fd724bf494bc8c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 07:41:26 +0200 Subject: [PATCH 089/133] Address PR #20460 review: version-first lookup, no Refresh race, dead removals dropped Mirrors 2dd1a17 on the upstream branch. See https://github.com/dotnet/fsharp/pull/20460 for the reasoning. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- .../FSharpProjectOptionsManager.fs | 57 +++++++++---------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index de67e65cf26..b940219a62b 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -6,7 +6,7 @@ ### Fixed -* Reuse the in-memory PE reference of a referenced C# project while its dependent semantic version is unchanged, instead of creating one per Roslyn `Compilation` instance. A recreated `Compilation` produced a reference with a fresh stamp, which invalidated the FCS bootstrap of every dependent F# project and made it re-import all of its references before re-checking every file. ([PR #20460](https://github.com/dotnet/fsharp/pull/20460)) +* Reuse the in-memory PE reference of a referenced C# project while its dependent semantic version is unchanged, so dependent F# projects no longer re-import every reference and re-check every file each time Roslyn recreates the `Compilation`. ([PR #20460](https://github.com/dotnet/fsharp/pull/20460)) * Fixed `MissingManifestResourceException` when opening F# pages under Tools > Options by giving `FSharpPackage` its own `VSPackage` resource set. ([PR #20408](https://github.com/dotnet/fsharp/pull/20408)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 47e32a9ebaa..a4fdfdcc2d1 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -135,25 +135,21 @@ type SingleFileCacheEntry = /// [] type private PEReferenceCacheEntry(version: VersionStamp, compilation: Compilation) = - // Pinned until the first successful emit so the reader can always be materialised. + // Pinned until the first emit result, so the reader can always be materialised. let mutable pinned = compilation let latest = WeakReference(compilation) member _.Version = version member _.TryGetCompilation() = - match latest.TryGetTarget() with - | true, compilation -> ValueSome compilation - | _ -> ValueNone - - member _.Refresh(compilation: Compilation) = - latest.SetTarget compilation - match pinned with - | null -> () - | _ -> pinned <- compilation + | null -> + match latest.TryGetTarget() with + | true, compilation -> ValueSome compilation + | _ -> ValueNone + | pinned -> ValueSome pinned - member _.Emitted() = pinned <- Unchecked.defaultof<_> + member _.Emitted() = pinned <- null [] type private FSharpProjectOptionsMessage = @@ -265,7 +261,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch paths.UnionWith projectOptions.SourceFiles updateWatches scriptReferenceWatches documentId paths - let createNewPEReference (referencedProject: Project) (entry: PEReferenceCacheEntry) = + let buildPEReference (referencedProject: Project) (entry: PEReferenceCacheEntry) = let projectId = referencedProject.Id let mutable stampTime = DateTime.UtcNow @@ -314,16 +310,16 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch let getStampTime () = stampTime FSharpReferencedProject.PEReference(getStampTime, DelayedILModuleReader(referencedProject.OutputFilePath, getStream)) - let createPEReference (referencedProject: Project) (version: VersionStamp) (comp: Compilation) = + let tryGetPEReference (referencedProject: Project) (version: VersionStamp) = match peReferences.TryGetValue referencedProject.Id with - | true, struct (entry, reference) when entry.Version = version -> - entry.Refresh comp - reference - | _ -> - let entry = PEReferenceCacheEntry(version, comp) - let reference = createNewPEReference referencedProject entry - peReferences[referencedProject.Id] <- struct (entry, reference) - reference + | true, struct (entry, reference) when entry.Version = version -> ValueSome reference + | _ -> ValueNone + + let createPEReference (referencedProject: Project) (version: VersionStamp) (comp: Compilation) = + let entry = PEReferenceCacheEntry(version, comp) + let reference = buildPEReference referencedProject entry + peReferences[referencedProject.Id] <- struct (entry, reference) + reference let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = cancellableTask { @@ -544,9 +540,13 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch FSharpReferencedProject.FSharpReference(referencedProject.OutputFilePath, projectOptions) ) elif referencedProject.SupportsCompilation then - let! comp = referencedProject.GetCompilationAsync(ct) - and! version = referencedProject.GetDependentSemanticVersionAsync(ct) - referencedProjects.Add(createPEReference referencedProject version comp) + let! version = referencedProject.GetDependentSemanticVersionAsync(ct) + + match tryGetPEReference referencedProject version with + | ValueSome reference -> referencedProjects.Add reference + | ValueNone -> + let! comp = referencedProject.GetCompilationAsync(ct) + referencedProjects.Add(createPEReference referencedProject version comp) if canBail then return ValueNone @@ -714,19 +714,16 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch | FSharpProjectOptionsMessage.ClearOptions(projectId) -> match cache.TryRemove(projectId) with - | true, struct (_, _, projectOptions) -> - lastSuccessfulCompilations.TryRemove(projectId) |> ignore - peReferences.TryRemove projectId |> ignore - checker.ClearCache([ projectOptions ]) + | true, struct (_, _, projectOptions) -> checker.ClearCache([ projectOptions ]) | _ -> () + lastSuccessfulCompilations.TryRemove(projectId) |> ignore + peReferences.TryRemove projectId |> ignore legacyProjectSites.TryRemove(projectId) |> ignore clearReferenceWatches projectId | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> match singleFileCache.TryRemove(documentId) with | true, ({ ProjectOptions = projectOptions } as cacheEntry) -> - lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore - peReferences.TryRemove documentId.ProjectId |> ignore checker.ClearCache([ projectOptions ]) disposeSingleFileCacheEntry cacheEntry | _ -> () From ebfb887b59446fc2129ffabbffcff77c58a21b4c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 09:42:41 +0200 Subject: [PATCH 090/133] Build multi-project and multi-target Roslyn solutions in FSharp.Editor tests Test helpers so far put every synthetic file into one Roslyn project. CreateMultiProjectSolution creates one project per synthetic project with project references, the way VS wires project-to-project references; CreateMultiTargetSolution creates one project per target instance sharing the project path and the document paths, the way VS loads a multi-targeted project. Co-Authored-By: Claude Fable 5.1 --- .../Helpers/RoslynHelpers.fs | 146 +++++++++++++++++- 1 file changed, 140 insertions(+), 6 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs index 25509f14ace..89a449eceb7 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs @@ -201,6 +201,14 @@ type TestHostServices() = override this.CreateWorkspaceServices(workspace) = new TestHostWorkspaceServices(this, workspace) +/// One Roslyn project instance of a multi-targeted F# project: its extra defines and the +/// synthetic files left out of it, as VS does per target framework. +type TargetInstance = + { + Defines: string list + ExcludedFileIds: string list + } + [] type RoslynTestHelpers private () = @@ -258,6 +266,33 @@ type RoslynTestHelpers private () = filePath = filePath ) + static member private ProjectInfoFor + (id, name, filePath, outputFilePath, documents, projectReferences: ProjectReference list, metadataReferences: MetadataReference seq) + = + ProjectInfo.Create( + id, + VersionStamp.Create(DateTime.UtcNow), + name, + name, + LanguageNames.FSharp, + filePath = filePath, + outputFilePath = outputFilePath, + documents = documents, + projectReferences = projectReferences, + metadataReferences = metadataReferences + ) + + static member private MetadataReferencesOf(options: FSharpProjectOptions, excludedPaths: string seq) = + let excluded = HashSet(excludedPaths, StringComparer.OrdinalIgnoreCase) + + options.OtherOptions + |> Seq.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal)) + |> Seq.map _.Substring(3) + |> Seq.filter (excluded.Contains >> not) + |> Seq.map MetadataReference.CreateFromFile + |> Seq.cast + |> Seq.toList + static member SetProjectOptions projId (solution: Solution) (options: FSharpProjectOptions) = solution.Workspace.Services .GetService() @@ -331,12 +366,8 @@ type RoslynTestHelpers private () = let options = syntheticProject.GetProjectOptions checker - let metadataReferences = - options.OtherOptions - |> Seq.filter (fun x -> x.StartsWith("-r:")) - |> Seq.map (fun x -> x.Substring(3) |> MetadataReference.CreateFromFile :> MetadataReference) - - let projInfo = projInfo.WithMetadataReferences metadataReferences + let projInfo = + projInfo.WithMetadataReferences(RoslynTestHelpers.MetadataReferencesOf(options, [])) let solution = RoslynTestHelpers.CreateSolution [ projInfo ] @@ -344,6 +375,109 @@ type RoslynTestHelpers private () = solution, checker + /// One Roslyn project per synthetic project, wired with project references the way VS wires + /// project-to-project references, so the options manager builds in-memory F# references. + static member CreateMultiProjectSolution(syntheticProject: SyntheticProject) = + let checker = syntheticProject.SaveAndCheck() + + let projects = + syntheticProject.GetAllProjects() + |> List.distinctBy _.Name + |> List.map (fun project -> project, ProjectId.CreateNewId()) + + let projectIds = dict [ for project, id in projects -> project.Name, id ] + + let projectInfos = + [ + for project, id in projects do + let options = project.GetProjectOptions checker + + RoslynTestHelpers.ProjectInfoFor( + id, + project.Name, + project.ProjectFileName, + project.OutputFilename, + [ + for path in project.SourceFilePaths -> RoslynTestHelpers.CreateDocumentInfo id path (File.ReadAllText path) + ], + [ + for dependency in project.DependsOn -> ProjectReference projectIds[dependency.Name] + ], + RoslynTestHelpers.MetadataReferencesOf(options, project.DependsOn |> List.map _.OutputFilename) + ) + ] + + let solution = RoslynTestHelpers.CreateSolution projectInfos + + for project, id in projects do + project.GetProjectOptions checker + |> RoslynTestHelpers.SetProjectOptions id solution + + solution, checker + + /// One Roslyn project per target instance, all sharing the .fsproj path and the document file + /// paths, like the per-target-framework projects VS creates for a multi-targeted project. + static member CreateMultiTargetSolution(syntheticProject: SyntheticProject, instances: TargetInstance list) = + assert (syntheticProject.DependsOn = []) + + let checker = syntheticProject.SaveAndCheck() + let options = syntheticProject.GetProjectOptions checker + let metadataReferences = RoslynTestHelpers.MetadataReferencesOf(options, []) + + let instances = + [ + for instance in instances -> + let excludedPaths = + HashSet( + [ + for fileId in instance.ExcludedFileIds do + syntheticProject.GetFilePath fileId + + if (syntheticProject.Find fileId).HasSignatureFile then + syntheticProject.GetSignatureFilePath fileId + ], + StringComparer.OrdinalIgnoreCase + ) + + let sourceFiles = + syntheticProject.SourceFilePaths |> List.filter (excludedPaths.Contains >> not) + + let id = ProjectId.CreateNewId() + + let projectInfo = + RoslynTestHelpers.ProjectInfoFor( + id, + syntheticProject.Name, + syntheticProject.ProjectFileName, + syntheticProject.OutputFilename, + [ + for path in sourceFiles -> RoslynTestHelpers.CreateDocumentInfo id path (File.ReadAllText path) + ], + [], + metadataReferences + ) + + let instanceOptions = + { options with + SourceFiles = List.toArray sourceFiles + OtherOptions = + [| + yield! options.OtherOptions + for define in instance.Defines -> $"--define:{define}" + |] + } + + id, projectInfo, instanceOptions + ] + + let solution = + RoslynTestHelpers.CreateSolution [ for _, projectInfo, _ in instances -> projectInfo ] + + for id, _, instanceOptions in instances do + RoslynTestHelpers.SetProjectOptions id solution instanceOptions + + solution, [ for id, _, _ in instances -> id ] + static member GetFsDocument(code, ?customProjectOption: string, ?customEditorOptions) = let customProjectOptions = customProjectOption From 13089bc2f776357a13e51dd51ba33d2c4faaad26 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 09:42:41 +0200 Subject: [PATCH 091/133] Resolve navigation targets through the workspace's current solution The document Roslyn hands to Go To Definition can come from a snapshot taken before every project of the solution finished loading. Deciding that a symbol is external because that snapshot has no document for its file sent F# to F# navigation into a generated signature, and the same lookup silently narrowed the scope of Find All References and Rename. Look the target up in the document's own solution first and in Workspace.CurrentSolution when it is missing, normalising the path and preferring the target-framework instance the origin project depends on. The branch of Go To Definition that already sits on the declaration moves into its own member. Co-Authored-By: Claude Fable 5.1 --- .../Common/CodeAnalysisExtensions.fs | 29 ++ .../LanguageService/SymbolHelpers.fs | 2 +- .../FSharp.Editor/LanguageService/Symbols.fs | 8 +- .../Navigation/FindUsagesService.fs | 9 +- .../Navigation/GoToDefinition.fs | 247 ++++++++---------- 5 files changed, 142 insertions(+), 153 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs b/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs index e0b29c8f9f1..1eaaaed3180 100644 --- a/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs @@ -117,3 +117,32 @@ type Solution with | Some projectId -> self.TryGetDocumentIdFromFSharpRange(range, projectId) | None -> self.TryGetDocumentIdFromFSharpRange range |> Option.map self.GetDocument + +type Document with + + /// Runs a lookup against this document's solution and, when it finds nothing, against the + /// workspace's current solution: the document may come from a snapshot taken before every + /// project of the solution finished loading. + member document.TryFindInSolutions(find: Solution -> 'T voption) = + match find document.Project.Solution with + | ValueSome found -> ValueSome found + | ValueNone -> find document.Project.Solution.Workspace.CurrentSolution + + /// Every document with the file path, from whichever project includes it. + member document.GetSolutionDocumentsWithFilePath(filePath: string) = + let filePath = Path.GetFullPathSafe filePath + + document.TryFindInSolutions(fun solution -> + match solution.GetDocumentIdsWithFilePath filePath with + | ids when ids.IsEmpty -> ValueNone + | ids -> ValueSome [ for id in ids -> solution.GetDocument id ]) + |> ValueOption.defaultValue [] + + member document.TryGetSolutionDocumentFromPath(filePath: string) = + document.GetSolutionDocumentsWithFilePath filePath |> Seq.tryHeadV + + /// The document for the range's file, preferring this document's project or one it depends on. + member document.TryGetSolutionDocumentFromFSharpRange(range: range) = + document.TryFindInSolutions(fun solution -> + solution.TryGetDocumentFromFSharpRange(range, document.Project.Id) + |> ValueOption.ofOption) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 36319820f80..becd935453a 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -124,7 +124,7 @@ module internal SymbolHelpers = let otherFile = getOtherFile currentDocument.FilePath let! otherFileCheckResults = - match currentDocument.Project.Solution.TryGetDocumentFromPath otherFile with + match currentDocument.TryGetSolutionDocumentFromPath otherFile with | ValueSome doc -> cancellableTask { let! _, checkFileResults = doc.GetFSharpParseAndCheckResultsAsync("findReferencedSymbolsAsync") diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs index 19e446f2d08..ecedeb3e536 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs @@ -64,11 +64,9 @@ type FSharpSymbolUse with Some(SymbolScope.Projects([ currentDocument.Project ], isSymbolLocalForProject)) else let projects = - currentDocument.Project.Solution.GetDocumentIdsWithFilePath(filePath) - |> Seq.map (fun x -> x.ProjectId) - |> Seq.distinct - |> Seq.map currentDocument.Project.Solution.GetProject - |> Seq.toList + currentDocument.GetSolutionDocumentsWithFilePath filePath + |> List.map _.Project + |> List.distinctBy _.Id match projects with | [] -> None diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index c313d4f27ee..446fff005ec 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -59,18 +59,15 @@ module FSharpFindUsagesService = } // File can be included in more than one project, hence single `range` may results with multiple `Document`s. - let rangeToDocumentSpans (solution: Solution, range: range, symbolName: string) = + let rangeToDocumentSpans (document: Document, range: range, symbolName: string) = if range.Start = range.End then CancellableTask.singleton [||] else cancellableTask { - let documentIds = solution.GetDocumentIdsWithFilePath(range.FileName) - let! spans = seq { - for documentId in documentIds do + for doc in document.GetSolutionDocumentsWithFilePath range.FileName do cancellableTask { - let doc = solution.GetDocument(documentId) let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = doc.GetTextAsync(cancellationToken) @@ -119,7 +116,7 @@ module FSharpFindUsagesService = let! declarationSpans = match declarationRange with - | Some range -> rangeToDocumentSpans (document.Project.Solution, range, symbol.Ident.idText) + | Some range -> rangeToDocumentSpans (document, range, symbol.Ident.idText) | None -> CancellableTask.singleton [||] let declarationSpans = diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 6bc86ae57a3..8e3159e92b1 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -174,29 +174,26 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = && symbol1.DeclaringEntity.CompiledName = symbol2.DeclaringEntity.CompiledName | _ -> false + /// The navigable item for the range in the document, when the range fits the document's text. + let navigableItemAt (document: Document) (range: range) = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync(cancellationToken) + + return + RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) + |> ValueOption.map (fun textSpan -> FSharpGoToDefinitionNavigableItem(document, textSpan)) + } + /// Use an origin document to provide the solution & workspace used to /// find the corresponding textSpan and INavigableItem for the range let rangeToNavigableItem (range: range, document: Document) = cancellableTask { - let fileName = - try - System.IO.Path.GetFullPath range.FileName - with _ -> - range.FileName - - let refDocumentIds = document.Project.Solution.GetDocumentIdsWithFilePath fileName - - if not refDocumentIds.IsEmpty then - let refDocumentId = refDocumentIds.First() - let refDocument = document.Project.Solution.GetDocument refDocumentId - let! cancellationToken = Async.CancellationToken - let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask - - match RoslynHelpers.TryFSharpRangeToTextSpan(refSourceText, range) with - | ValueNone -> return None - | ValueSome refTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(refDocument, refTextSpan)) - else - return None + match document.TryGetSolutionDocumentFromFSharpRange range with + | ValueNone -> return None + | ValueSome refDocument -> + let! navItem = navigableItemAt refDocument range + return ValueOption.toOption navItem } member _.TryGetExternalDeclarationAsync(targetSymbolUse: FSharpSymbolUse, metadataReferences: seq) = @@ -312,7 +309,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = if not (File.Exists fsfilePath) then return None else - let implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath + let implDoc = originDocument.TryGetSolutionDocumentFromPath fsfilePath match implDoc with | ValueNone -> return None @@ -336,14 +333,9 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | ValueNone -> return None | ValueSome implTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(implDoc, implTextSpan)) else - let targetDocument = - originDocument.Project.Solution.TryGetDocumentFromFSharpRange fsSymbolUse.Range - - match targetDocument with - | None -> return None - | Some targetDocument -> - let! navItem = rangeToNavigableItem (fsSymbolUse.Range, targetDocument) - return navItem + match originDocument.TryGetSolutionDocumentFromFSharpRange fsSymbolUse.Range with + | ValueNone -> return None + | ValueSome targetDocument -> return! rangeToNavigableItem (fsSymbolUse.Range, targetDocument) } /// if the symbol is defined in the given file, return its declaration location, otherwise use the targetSymbol to find the first @@ -373,6 +365,55 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return implSymbol.Range } + /// The navigable item for the target symbol's declaration in the implementation document. + member private this.FindNavigableDeclarationIn(targetSymbolUse: FSharpSymbolUse, implDocument: Document) = + cancellableTask { + let! declarationRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) + + match declarationRange with + | None -> return ValueNone + | Some declarationRange -> return! navigableItemAt implDocument declarationRange + } + + /// The caret is already on the declaration: in a signature file the target is the implementation, + /// in an implementation file it is the signature. + member private this.FindCounterpartOfDeclarationAtCaret + ( + originDocument: Document, + targetSymbolUse: FSharpSymbolUse, + checkFileResults: FSharpCheckFileResults, + lexerSymbol: LexerSymbol, + fcsTextLineNumber: int, + textLineString: string + ) = + cancellableTask { + if isSignatureFile originDocument.FilePath then + let implFilePath = Path.ChangeExtension(originDocument.FilePath, "fs") + + if not (File.Exists implFilePath) then + return ValueNone + else + match originDocument.TryGetSolutionDocumentFromPath implFilePath with + | ValueNone -> return ValueNone + | ValueSome implDocument -> return! this.FindNavigableDeclarationIn(targetSymbolUse, implDocument) + else + let declarations = + checkFileResults.GetDeclarationLocation( + fcsTextLineNumber, + lexerSymbol.Ident.idRange.EndColumn, + textLineString, + lexerSymbol.FullIsland, + true + ) + + match declarations with + | FindDeclResult.DeclFound sigRange -> + match originDocument.TryGetSolutionDocumentFromFSharpRange sigRange with + | ValueNone -> return ValueNone + | ValueSome sigDocument -> return! navigableItemAt sigDocument sigRange + | _ -> return ValueNone + } + member internal this.FindDefinitionAtPosition(originDocument: Document, position: int) = cancellableTask { let userOpName = "FindDefinitionAtPosition" @@ -417,8 +458,9 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = match declarations with | FindDeclResult.ExternalDecl(assembly, targetExternalSym) -> let projectOpt = - originDocument.Project.Solution.Projects - |> Seq.tryFindV (fun p -> p.AssemblyName.Equals(assembly, StringComparison.OrdinalIgnoreCase)) + originDocument.TryFindInSolutions(fun solution -> + solution.Projects + |> Seq.tryFindV (fun p -> p.AssemblyName.Equals(assembly, StringComparison.OrdinalIgnoreCase))) match projectOpt with | ValueSome project -> @@ -456,122 +498,45 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) | FindDeclResult.DeclFound targetRange -> - // If the file is not associated with a document, it's considered external. - if not (originDocument.Project.Solution.ContainsDocumentWithFilePath(targetRange.FileName)) then + match originDocument.TryGetSolutionDocumentFromFSharpRange targetRange with + | ValueNone -> + // No document for the file anywhere in the workspace: the symbol comes from an assembly. let metadataReferences = originDocument.Project.MetadataReferences return ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) - else if - // if goto definition is called as we are already at the declaration location of a symbol in - // either a signature or an implementation file then we jump to its respective position in the document - lexerSymbol.Range = targetRange - then - // jump from signature to the corresponding implementation - if isSignatureFile originDocument.FilePath then - let implFilePath = Path.ChangeExtension(originDocument.FilePath, "fs") - - if not (File.Exists implFilePath) then - return ValueNone + | ValueSome _ when lexerSymbol.Range = targetRange -> + let! navItem = + this.FindCounterpartOfDeclarationAtCaret( + originDocument, + targetSymbolUse, + checkFileResults, + lexerSymbol, + fcsTextLineNumber, + textLineString + ) + + return + navItem + |> ValueOption.map (fun navItem -> FSharpGoToDefinitionResult.NavigableItem navItem, idRange) + | ValueSome targetDocument -> + // gotoDefn origin = signature, destination = signature; origin = implementation, destination = implementation + let! navItem = + if isSignatureFile targetRange.FileName && preferSignature then + navigableItemAt targetDocument targetRange else - let implDocument = - originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - - match implDocument with - | ValueNone -> return ValueNone - | ValueSome implDocument -> - let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) - - match targetRange with - | None -> return ValueNone - | Some targetRange -> - let! implSourceText = implDocument.GetTextAsync(cancellationToken) - - let implTextSpan = - RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, targetRange) - - match implTextSpan with - | ValueNone -> return ValueNone - | ValueSome implTextSpan -> - let navItem = FSharpGoToDefinitionNavigableItem(implDocument, implTextSpan) - return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - - else // jump from implementation to the corresponding signature - let declarations = - checkFileResults.GetDeclarationLocation( - fcsTextLineNumber, - idRange.EndColumn, - textLineString, - lexerSymbol.FullIsland, - true - ) + // Bugfix: apparently the target document is not always a signature file + let implFilePath = + if isSignatureFile targetDocument.FilePath then + Path.ChangeExtension(targetDocument.FilePath, "fs") + else + targetDocument.FilePath - match declarations with - | FindDeclResult.DeclFound targetRange -> - let sigDocument = - originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName - - match sigDocument with - | ValueNone -> return ValueNone - | ValueSome sigDocument -> - let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) - - let sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sigSourceText, targetRange) - - match sigTextSpan with - | ValueNone -> return ValueNone - | ValueSome sigTextSpan -> - let navItem = FSharpGoToDefinitionNavigableItem(sigDocument, sigTextSpan) - return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - | _ -> return ValueNone - // when the target range is different follow the navigation convention of - // - gotoDefn origin = signature , gotoDefn destination = signature - // - gotoDefn origin = implementation, gotoDefn destination = implementation - else - let sigDocument = - originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + match originDocument.TryGetSolutionDocumentFromPath implFilePath with + | ValueNone -> CancellableTask.singleton ValueNone + | ValueSome implDocument -> this.FindNavigableDeclarationIn(targetSymbolUse, implDocument) - match sigDocument with - | ValueNone -> return ValueNone - | ValueSome sigDocument -> - let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) - let sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sigSourceText, targetRange) - - match sigTextSpan with - | ValueNone -> return ValueNone - | ValueSome sigTextSpan -> - // if the gotodef call originated from a signature and the returned target is a signature, navigate there - if isSignatureFile targetRange.FileName && preferSignature then - let navItem = FSharpGoToDefinitionNavigableItem(sigDocument, sigTextSpan) - return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - else // we need to get an FSharpSymbol from the targetRange found in the signature - // that symbol will be used to find the destination in the corresponding implementation file - let implFilePath = - // Bugfix: apparently sigDocument not always is a signature file - if isSignatureFile sigDocument.FilePath then - Path.ChangeExtension(sigDocument.FilePath, "fs") - else - sigDocument.FilePath - - let implDocument = - originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - - match implDocument with - | ValueNone -> return ValueNone - | ValueSome implDocument -> - let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) - - match targetRange with - | None -> return ValueNone - | Some targetRange -> - let! implSourceText = implDocument.GetTextAsync(cancellationToken) - - let implTextSpan = - RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, targetRange) - - match implTextSpan with - | ValueNone -> return ValueNone - | ValueSome implTextSpan -> - let navItem = FSharpGoToDefinitionNavigableItem(implDocument, implTextSpan) - return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) + return + navItem + |> ValueOption.map (fun navItem -> FSharpGoToDefinitionResult.NavigableItem navItem, idRange) | _ -> return ValueNone } From 4da550ed13c69ca98052836199c59ac3f0e74698 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 09:42:41 +0200 Subject: [PATCH 092/133] Test Go To Definition against a solution snapshot that predates the target document Co-Authored-By: Claude Fable 5.1 --- .../GoToDefinitionServiceTests.fs | 112 +++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs index fe10a42a125..68d8755ac89 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs @@ -2,6 +2,8 @@ namespace FSharp.Editor.Tests +open System +open System.Threading open Xunit open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text @@ -9,8 +11,8 @@ open Microsoft.VisualStudio.FSharp.Editor open FSharp.Compiler.EditorServices open FSharp.Compiler.Text open FSharp.Editor.Tests.Helpers +open FSharp.Test.ProjectGeneration open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks -open System.Threading module GoToDefinitionServiceTests = @@ -149,3 +151,111 @@ let f_IWSAM_flex_StaticProperty(x: #IStaticProperty<'T>) = let expected = Some(3, 3, 20, 34) GoToDefinitionTest(fileContents, caretMarker, expected) + + let private symbolUseAt (document: Document) (sourceText: SourceText) position = + maybe { + let textLine = sourceText.Lines.GetLineFromPosition position + let fcsTextLineNumber = Line.fromZ (sourceText.Lines.GetLinePosition position).Line + + let! lexerSymbol = + Tokenizer.getSymbolAtPosition ( + document.Id, + sourceText, + position, + document.FilePath, + [], + SymbolLookupKind.Greedy, + false, + false, + None, + CancellationToken.None + ) + + let _, checkFileResults = + document.GetFSharpParseAndCheckResultsAsync userOpName + |> CancellableTask.runSynchronouslyWithoutCancellation + + return! + checkFileResults.GetSymbolUseAtLocation( + fcsTextLineNumber, + lexerSymbol.Ident.idRange.EndColumn, + textLine.ToString(), + lexerSymbol.FullIsland + ) + } + + /// An app project referencing a library project. The app document comes from a snapshot that + /// predates the library's document, the way Roslyn hands out documents while a solution is + /// still loading, while the workspace's current solution already has it. + module internal StaleSnapshot = + + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + + let app = + { SyntheticProject.Create( + "App", + { sourceFile "App" [ "Library" ] with + ExtraSource = "let mapped = List.map id [ 1 ]" + } + ) with + DependsOn = [ library ] + } + + let solution, _ = RoslynTestHelpers.CreateMultiProjectSolution app + let appPath = app.GetFilePath "App" + let libraryPath = library.GetFilePath "Library" + + let private documentId path = + solution.GetDocumentIdsWithFilePath path |> Seq.exactlyOne + + let appDocument = + solution.RemoveDocument(documentId libraryPath).GetDocument(documentId appPath) + + let appSourceText = appDocument.GetTextAsync(CancellationToken.None).Result + + /// The position of the last character of the text, inside the identifier it ends with. + let positionOf (text: string) = + appSourceText.ToString().IndexOf(text, StringComparison.Ordinal) + text.Length + - 1 + + let findDefinitionAt position = + GoToDefinition(FSharpMetadataAsSourceService()).FindDefinitionAtPosition(appDocument, position) + |> CancellableTask.runSynchronouslyWithoutCancellation + + [] + let ``goto definition finds the target document through the workspace when the origin snapshot predates it`` () = + let position = StaleSnapshot.positionOf "ModuleLibrary.f" + let document = StaleSnapshot.appDocument + + let range = + findDefinition (document, StaleSnapshot.appSourceText, position, [], None) + |> Option.defaultWith (fun () -> failwith "declaration not found") + + Assert.Equal(StaleSnapshot.libraryPath, range.FileName) + Assert.True(Option.isNone (document.Project.Solution.TryGetDocumentFromFSharpRange(range, document.Project.Id))) + + match document.TryGetSolutionDocumentFromFSharpRange range with + | ValueSome target -> Assert.Equal(StaleSnapshot.libraryPath, target.FilePath) + | ValueNone -> failwith "the workspace's current solution has the library document" + + match StaleSnapshot.findDefinitionAt position with + | ValueSome(FSharpGoToDefinitionResult.NavigableItem item, _) -> Assert.Equal(StaleSnapshot.libraryPath, item.Document.FilePath) + | result -> failwith $"expected a navigable item, got %A{result}" + + [] + let ``goto definition treats a symbol whose file is in no solution as external`` () = + match StaleSnapshot.findDefinitionAt (StaleSnapshot.positionOf "List.map") with + | ValueSome(FSharpGoToDefinitionResult.ExternalAssembly _, _) -> () + | result -> failwith $"expected an external assembly, got %A{result}" + + [] + let ``find references scope includes the declaring project the origin snapshot does not know`` () = + let position = StaleSnapshot.positionOf "ModuleLibrary.f" + + let symbolUse = + symbolUseAt StaleSnapshot.appDocument StaleSnapshot.appSourceText position + |> Option.defaultWith (fun () -> failwith "symbol not found") + + match symbolUse.GetSymbolScope StaleSnapshot.appDocument with + | Some(SymbolScope.Projects(projects, _)) -> Assert.Contains(StaleSnapshot.library.Name, projects |> List.map _.Name) + | scope -> failwith $"expected a project scope, got %A{scope}" From 291ccbed89237c93c303f794346c5ae54f6f956a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 09:45:24 +0200 Subject: [PATCH 093/133] Add the release note for PR #20462 Co-Authored-By: Claude Fable 5.1 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index b940219a62b..7b32e0c3512 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -27,6 +27,7 @@ * Requests for one version of an open document share a single whole-file semantic classification, kept for the life of the document instead of two seconds, so scrolling a large file after a pause no longer re-walks every symbol in it. ([PR #20452](https://github.com/dotnet/fsharp/pull/20452)) * Keep the collapsed regions of a file while its project is loading or reloading: outlining now parses with the quick parsing options instead of answering with no regions, which expanded everything the user had folded. ([PR #20451](https://github.com/dotnet/fsharp/pull/20451)) * Fix syntax coloring being lost for a whole file when one symbol resolves into metadata that could not be read. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) +* Go To Definition on a symbol declared in another project of the solution no longer opens a generated signature when the origin document comes from a solution snapshot that predates that project's documents: navigation, Find All References and Rename now look the target up in the workspace's current solution as well. ([PR #20462](https://github.com/dotnet/fsharp/pull/20462)) ### Changed From 29bd30afbbe11fe73d9682271587a4cb6fc0c042 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 09:48:46 +0200 Subject: [PATCH 094/133] Move the cross-language symbol navigation service into its own file Pure move of the doc-comment-id types, FSharpNavigableLocation and FSharpCrossLanguageSymbolNavigationService out of GoToDefinition.fs, compiled after NavigateToSearchService.fs so the service can use the parsed navigable items cache. Co-Authored-By: Claude Fable 5.1 --- .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../CrossLanguageSymbolNavigation.fs | 255 ++++++++++++++++++ .../Navigation/GoToDefinition.fs | 236 ---------------- 3 files changed, 256 insertions(+), 236 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index b9ecfed5036..63173c58e00 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -98,6 +98,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs new file mode 100644 index 00000000000..60e8ecd0e16 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition +open System.Linq +open System.Threading +open System.Threading.Tasks + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation +open Microsoft.VisualStudio +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.LanguageServices + +open FSharp.Compiler.Symbols +open FSharp.Compiler.Text +open CancellableTasks + +[] +type internal SymbolMemberType = + | Event + | Property + | Method + | Constructor + | Other + +type internal SymbolPath = + { + EntityPath: string list + MemberOrValName: string + GenericParameters: int + } + +[] +type internal DocCommentId = + | Member of SymbolPath * SymbolMemberType: SymbolMemberType + | Field of SymbolPath + | Type of EntityPath: string list + | None + +type FSharpNavigableLocation(metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = + interface IFSharpNavigableLocation with + member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = + cancellableTask { + let targetPath = symbolRange.FileName + + let! cancellationToken = CancellableTask.getCancellationToken () + + let targetDoc = + project.Solution.TryGetDocumentFromFSharpRange(symbolRange, project.Id) + + match targetDoc with + | None -> return false + | Some targetDoc -> + let! targetSource = targetDoc.GetTextAsync(cancellationToken) + let gtd = GoToDefinition(metadataAsSource) + + let (|Signature|Implementation|) filepath = + if isSignatureFile filepath then + Signature + else + Implementation + + match targetPath with + | Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange) + | Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange) + } + |> CancellableTask.start cancellationToken + +[)>] +[)>] +type FSharpCrossLanguageSymbolNavigationService() = + let componentModel = + Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel + + let workspace = componentModel.GetService() + + let metadataAsSource = + componentModel.DefaultExportProvider.GetExport().Value + + let tryFindFieldByName (name: string) (e: FSharpEntity) = + let fields = + e.FSharpFields + |> Seq.filter (fun x -> x.DisplayName = name && not x.IsCompilerGenerated) + |> Seq.map (fun e -> e.DeclarationLocation) + + if fields.Count() <= 0 && (e.IsFSharpUnion || e.IsFSharpRecord) then + Seq.singleton e.DeclarationLocation + else + fields + + let tryFindValByNameAndType + (name: string) + (symbolMemberType: SymbolMemberType) + (genericParametersCount: int) + (e: FSharpEntity) + (entities: FSharpMemberOrFunctionOrValue seq) + = + + let defaultFilter (e: FSharpMemberOrFunctionOrValue) = + (e.DisplayName = name || e.CompiledName = name) + && e.GenericParameters.Count = genericParametersCount + + let isProperty (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsProperty + let isConstructor (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsConstructor + + let getLocation (e: FSharpMemberOrFunctionOrValue) = e.DeclarationLocation + + let filteredEntities: range seq = + match symbolMemberType with + | SymbolMemberType.Other + | SymbolMemberType.Method -> entities |> Seq.filter defaultFilter |> Seq.map getLocation + // F# record-specific logic, if navigating to the record's ctor, then navigate to record declaration. + // If we navigating to F# record property, we first check if it's "custom" property, if it's one of the record fields, we search for it in the fields. + | SymbolMemberType.Constructor when e.IsFSharpRecord -> Seq.singleton e.DeclarationLocation + | SymbolMemberType.Property when e.IsFSharpRecord -> + let properties = entities |> Seq.filter isProperty |> Seq.map getLocation + let fields = tryFindFieldByName name e + Seq.append properties fields + | SymbolMemberType.Constructor -> entities |> Seq.filter isConstructor |> Seq.map getLocation + // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. + | SymbolMemberType.Event // Events are just properties` + | SymbolMemberType.Property -> entities |> Seq.filter isProperty |> Seq.map getLocation + + filteredEntities + + let tryFindVal + (name: string) + (documentCommentId: string) + (symbolMemberType: SymbolMemberType) + (genericParametersCount: int) + (e: FSharpEntity) + = + let entities = e.TryGetMembersFunctionsAndValues() + + // First, try and find entity by exact xml signature, return if found, + // otherwise, just try and match by parsed name and number of arguments. + + let entitiesByXmlSig = + entities + |> Seq.filter (fun e -> e.XmlDocSig = documentCommentId) + |> Seq.map (fun e -> e.DeclarationLocation) + + if Seq.isEmpty entitiesByXmlSig then + tryFindValByNameAndType name symbolMemberType genericParametersCount e entities + else + entitiesByXmlSig + + /// Convert a documentation comment ID to a navigation path. + /// Uses the shared XmlDocSigParser from FSharp.Compiler.Symbols. + static member internal DocCommentIdToPath(docId: string) = + // Use the shared parser from FSharp.Compiler.Symbols + match XmlDocSigParser.parseDocCommentId docId with + | ParsedDocCommentId.Type path -> DocCommentId.Type path + + | ParsedDocCommentId.Member(typePath, memberName, genericArity, kind) -> + // Convert constructor name format (.ctor in parser, ``.ctor`` needed for F# lookup) + let memberOrValName = if memberName = ".ctor" then "``.ctor``" else memberName + + let symbolMemberType = + match kind with + | DocCommentIdKind.Method -> + if memberName = ".ctor" then + SymbolMemberType.Constructor + else + SymbolMemberType.Method + | DocCommentIdKind.Property -> SymbolMemberType.Property + | DocCommentIdKind.Event -> SymbolMemberType.Event + | _ -> SymbolMemberType.Other + + DocCommentId.Member( + { + EntityPath = typePath + MemberOrValName = memberOrValName + GenericParameters = genericArity + }, + symbolMemberType + ) + + | ParsedDocCommentId.Field(typePath, fieldName) -> + DocCommentId.Field + { + EntityPath = typePath + MemberOrValName = fieldName + GenericParameters = 0 + } + + | ParsedDocCommentId.None -> DocCommentId.None + + interface IFSharpCrossLanguageSymbolNavigationService with + member _.TryGetNavigableLocationAsync + (assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) + : Task = + let path = + FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath documentationCommentId + + cancellableTask { + let projects = + workspace.CurrentSolution.Projects + |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) + + let mutable locations = Seq.empty + + for project in projects do + let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() + let! result = checker.ParseAndCheckProject(options) + + match path with + | DocCommentId.Member({ + EntityPath = entityPath + MemberOrValName = memberOrVal + GenericParameters = genericParametersCount + }, + memberType) -> + let entity = result.AssemblySignature.FindEntityByPath(entityPath) + + entity + |> Option.iter (fun e -> + locations <- + e + |> tryFindVal memberOrVal documentationCommentId memberType genericParametersCount + |> Seq.map (fun m -> (m, project)) + |> Seq.append locations) + | DocCommentId.Field { + EntityPath = entityPath + MemberOrValName = memberOrVal + } -> + let entity = result.AssemblySignature.FindEntityByPath(entityPath) + + entity + |> Option.iter (fun e -> + locations <- + e + |> tryFindFieldByName memberOrVal + |> Seq.map (fun m -> (m, project)) + |> Seq.append locations) + | DocCommentId.Type entityPath -> + let entity = result.AssemblySignature.FindEntityByPath(entityPath) + + entity + |> Option.iter (fun e -> locations <- Seq.append locations [ e.DeclarationLocation, project ]) + | DocCommentId.None -> () + + // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result + // For now, we only take 1st one, since it's usually going to be only one result (given we process names correctly). + // More results can theoretically be returned in case of method overloads, or when we have both signature and implementation files. + if locations.Count() >= 1 then + let (location, project) = locations.First() + return FSharpNavigableLocation(metadataAsSource, location, project) :> IFSharpNavigableLocation + else + return Unchecked.defaultof<_> // returning null here, so Roslyn can fallback to default source-as-metadata implementation. + } + |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 8e3159e92b1..3fe7514128e 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -787,239 +787,3 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, with exc -> TelemetryReporter.ReportFault(TelemetryEvents.GoToDefinition, FaultSeverity.General, exc) false - -[] -type internal SymbolMemberType = - | Event - | Property - | Method - | Constructor - | Other - -type internal SymbolPath = - { - EntityPath: string list - MemberOrValName: string - GenericParameters: int - } - -[] -type internal DocCommentId = - | Member of SymbolPath * SymbolMemberType: SymbolMemberType - | Field of SymbolPath - | Type of EntityPath: string list - | None - -type FSharpNavigableLocation(metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = - interface IFSharpNavigableLocation with - member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = - cancellableTask { - let targetPath = symbolRange.FileName - - let! cancellationToken = CancellableTask.getCancellationToken () - - let targetDoc = - project.Solution.TryGetDocumentFromFSharpRange(symbolRange, project.Id) - - match targetDoc with - | None -> return false - | Some targetDoc -> - let! targetSource = targetDoc.GetTextAsync(cancellationToken) - let gtd = GoToDefinition(metadataAsSource) - - let (|Signature|Implementation|) filepath = - if isSignatureFile filepath then - Signature - else - Implementation - - match targetPath with - | Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange) - | Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange) - } - |> CancellableTask.start cancellationToken - -[)>] -[)>] -type FSharpCrossLanguageSymbolNavigationService() = - let componentModel = - Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel - - let workspace = componentModel.GetService() - - let metadataAsSource = - componentModel.DefaultExportProvider.GetExport().Value - - let tryFindFieldByName (name: string) (e: FSharpEntity) = - let fields = - e.FSharpFields - |> Seq.filter (fun x -> x.DisplayName = name && not x.IsCompilerGenerated) - |> Seq.map (fun e -> e.DeclarationLocation) - - if fields.Count() <= 0 && (e.IsFSharpUnion || e.IsFSharpRecord) then - Seq.singleton e.DeclarationLocation - else - fields - - let tryFindValByNameAndType - (name: string) - (symbolMemberType: SymbolMemberType) - (genericParametersCount: int) - (e: FSharpEntity) - (entities: FSharpMemberOrFunctionOrValue seq) - = - - let defaultFilter (e: FSharpMemberOrFunctionOrValue) = - (e.DisplayName = name || e.CompiledName = name) - && e.GenericParameters.Count = genericParametersCount - - let isProperty (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsProperty - let isConstructor (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsConstructor - - let getLocation (e: FSharpMemberOrFunctionOrValue) = e.DeclarationLocation - - let filteredEntities: range seq = - match symbolMemberType with - | SymbolMemberType.Other - | SymbolMemberType.Method -> entities |> Seq.filter defaultFilter |> Seq.map getLocation - // F# record-specific logic, if navigating to the record's ctor, then navigate to record declaration. - // If we navigating to F# record property, we first check if it's "custom" property, if it's one of the record fields, we search for it in the fields. - | SymbolMemberType.Constructor when e.IsFSharpRecord -> Seq.singleton e.DeclarationLocation - | SymbolMemberType.Property when e.IsFSharpRecord -> - let properties = entities |> Seq.filter isProperty |> Seq.map getLocation - let fields = tryFindFieldByName name e - Seq.append properties fields - | SymbolMemberType.Constructor -> entities |> Seq.filter isConstructor |> Seq.map getLocation - // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. - | SymbolMemberType.Event // Events are just properties` - | SymbolMemberType.Property -> entities |> Seq.filter isProperty |> Seq.map getLocation - - filteredEntities - - let tryFindVal - (name: string) - (documentCommentId: string) - (symbolMemberType: SymbolMemberType) - (genericParametersCount: int) - (e: FSharpEntity) - = - let entities = e.TryGetMembersFunctionsAndValues() - - // First, try and find entity by exact xml signature, return if found, - // otherwise, just try and match by parsed name and number of arguments. - - let entitiesByXmlSig = - entities - |> Seq.filter (fun e -> e.XmlDocSig = documentCommentId) - |> Seq.map (fun e -> e.DeclarationLocation) - - if Seq.isEmpty entitiesByXmlSig then - tryFindValByNameAndType name symbolMemberType genericParametersCount e entities - else - entitiesByXmlSig - - /// Convert a documentation comment ID to a navigation path. - /// Uses the shared XmlDocSigParser from FSharp.Compiler.Symbols. - static member internal DocCommentIdToPath(docId: string) = - // Use the shared parser from FSharp.Compiler.Symbols - match XmlDocSigParser.parseDocCommentId docId with - | ParsedDocCommentId.Type path -> DocCommentId.Type path - - | ParsedDocCommentId.Member(typePath, memberName, genericArity, kind) -> - // Convert constructor name format (.ctor in parser, ``.ctor`` needed for F# lookup) - let memberOrValName = if memberName = ".ctor" then "``.ctor``" else memberName - - let symbolMemberType = - match kind with - | DocCommentIdKind.Method -> - if memberName = ".ctor" then - SymbolMemberType.Constructor - else - SymbolMemberType.Method - | DocCommentIdKind.Property -> SymbolMemberType.Property - | DocCommentIdKind.Event -> SymbolMemberType.Event - | _ -> SymbolMemberType.Other - - DocCommentId.Member( - { - EntityPath = typePath - MemberOrValName = memberOrValName - GenericParameters = genericArity - }, - symbolMemberType - ) - - | ParsedDocCommentId.Field(typePath, fieldName) -> - DocCommentId.Field - { - EntityPath = typePath - MemberOrValName = fieldName - GenericParameters = 0 - } - - | ParsedDocCommentId.None -> DocCommentId.None - - interface IFSharpCrossLanguageSymbolNavigationService with - member _.TryGetNavigableLocationAsync - (assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) - : Task = - let path = - FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath documentationCommentId - - cancellableTask { - let projects = - workspace.CurrentSolution.Projects - |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) - - let mutable locations = Seq.empty - - for project in projects do - let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() - let! result = checker.ParseAndCheckProject(options) - - match path with - | DocCommentId.Member({ - EntityPath = entityPath - MemberOrValName = memberOrVal - GenericParameters = genericParametersCount - }, - memberType) -> - let entity = result.AssemblySignature.FindEntityByPath(entityPath) - - entity - |> Option.iter (fun e -> - locations <- - e - |> tryFindVal memberOrVal documentationCommentId memberType genericParametersCount - |> Seq.map (fun m -> (m, project)) - |> Seq.append locations) - | DocCommentId.Field { - EntityPath = entityPath - MemberOrValName = memberOrVal - } -> - let entity = result.AssemblySignature.FindEntityByPath(entityPath) - - entity - |> Option.iter (fun e -> - locations <- - e - |> tryFindFieldByName memberOrVal - |> Seq.map (fun m -> (m, project)) - |> Seq.append locations) - | DocCommentId.Type entityPath -> - let entity = result.AssemblySignature.FindEntityByPath(entityPath) - - entity - |> Option.iter (fun e -> locations <- Seq.append locations [ e.DeclarationLocation, project ]) - | DocCommentId.None -> () - - // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result - // For now, we only take 1st one, since it's usually going to be only one result (given we process names correctly). - // More results can theoretically be returned in case of method overloads, or when we have both signature and implementation files. - if locations.Count() >= 1 then - let (location, project) = locations.First() - return FSharpNavigableLocation(metadataAsSource, location, project) :> IFSharpNavigableLocation - else - return Unchecked.defaultof<_> // returning null here, so Roslyn can fallback to default source-as-metadata implementation. - } - |> CancellableTask.start cancellationToken From 0b83c07c6bcc6173584f4cbfce276095f19f09f3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 10:03:05 +0200 Subject: [PATCH 095/133] Locate C# and VB navigation targets in F# without checking the whole project Go To Definition from C# or Visual Basic into an F# project ran a full ParseAndCheckProject for every target-framework instance of the project whose assembly name matched, on every keystroke of F12 and without any cache. Cold, that exceeded the time Roslyn waits for a cross-language location and it fell back to its own decompiled view. The parsed navigable items already cached per document name every type and module a file declares, so the files that can hold the declaration are known without a type check. Only those files are checked, one at a time in compile order, and the member is matched by its exact compiled id first, with the name-and-shape heuristics reserved for the last candidate whose partial signature holds every member of the entity. The whole-project check remains the fallback. One instance per project file goes first; the service gets its dependencies through the MEF constructor so the lookup runs against a plain Solution in tests. Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Editor/Common/CancellableTasks.fs | 11 + .../CrossLanguageSymbolNavigation.fs | 393 ++++++++++++------ 2 files changed, 279 insertions(+), 125 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 29e54491a86..2bef8709f44 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -1147,6 +1147,17 @@ module CancellableTasks = return results } + /// Runs the chooser over the items one at a time and stops at the first ValueSome. + let rec tryPick (chooser: 'T -> CancellableTask<'U voption>) (items: 'T list) : CancellableTask<'U voption> = + match items with + | [] -> singleton ValueNone + | item :: rest -> + cancellableTask { + match! chooser item with + | ValueSome picked -> return ValueSome picked + | ValueNone -> return! tryPick chooser rest + } + let inline ignore ([] ctask: CancellableTask<_>) = toUnit ctask /// If this CancellableTask gets canceled for another reason than the token being canceled, return the specified value. diff --git a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs index 60e8ecd0e16..fad92e13c1e 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs @@ -3,17 +3,16 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System +open System.Collections.Generic open System.Composition -open System.Linq open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation -open Microsoft.VisualStudio -open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.LanguageServices +open FSharp.Compiler.EditorServices open FSharp.Compiler.Symbols open FSharp.Compiler.Text open CancellableTasks @@ -69,29 +68,64 @@ type FSharpNavigableLocation(metadataAsSource: FSharpMetadataAsSourceService, sy } |> CancellableTask.start cancellationToken -[)>] -[)>] -type FSharpCrossLanguageSymbolNavigationService() = - let componentModel = - Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel +/// Locates the F# declaration Roslyn names by assembly and documentation comment id when C# or +/// Visual Basic navigates into an F# project. Kept apart from the MEF service so it can be exercised +/// without a Visual Studio workspace. +module internal CrossLanguageSymbolNavigation = + + [] + let private UserOpName = "CrossLanguageSymbolNavigation" + + let docCommentIdToPath (docId: string) = + match XmlDocSigParser.parseDocCommentId docId with + | ParsedDocCommentId.Type path -> DocCommentId.Type path + + | ParsedDocCommentId.Member(typePath, memberName, genericArity, kind) -> + // The parser reports constructors as .ctor; the F# lookup needs the backticked form. + let memberOrValName = if memberName = ".ctor" then "``.ctor``" else memberName + + let symbolMemberType = + match kind with + | DocCommentIdKind.Method -> + if memberName = ".ctor" then + SymbolMemberType.Constructor + else + SymbolMemberType.Method + | DocCommentIdKind.Property -> SymbolMemberType.Property + | DocCommentIdKind.Event -> SymbolMemberType.Event + | _ -> SymbolMemberType.Other + + DocCommentId.Member( + { + EntityPath = typePath + MemberOrValName = memberOrValName + GenericParameters = genericArity + }, + symbolMemberType + ) - let workspace = componentModel.GetService() + | ParsedDocCommentId.Field(typePath, fieldName) -> + DocCommentId.Field + { + EntityPath = typePath + MemberOrValName = fieldName + GenericParameters = 0 + } - let metadataAsSource = - componentModel.DefaultExportProvider.GetExport().Value + | ParsedDocCommentId.None -> DocCommentId.None - let tryFindFieldByName (name: string) (e: FSharpEntity) = + let private tryFindFieldByName (name: string) (e: FSharpEntity) = let fields = e.FSharpFields |> Seq.filter (fun x -> x.DisplayName = name && not x.IsCompilerGenerated) |> Seq.map (fun e -> e.DeclarationLocation) - if fields.Count() <= 0 && (e.IsFSharpUnion || e.IsFSharpRecord) then + if Seq.isEmpty fields && (e.IsFSharpUnion || e.IsFSharpRecord) then Seq.singleton e.DeclarationLocation else fields - let tryFindValByNameAndType + let private tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) @@ -126,130 +160,239 @@ type FSharpCrossLanguageSymbolNavigationService() = filteredEntities - let tryFindVal - (name: string) - (documentCommentId: string) - (symbolMemberType: SymbolMemberType) - (genericParametersCount: int) - (e: FSharpEntity) + /// The members of the entity the id names: those whose compiled id matches exactly and, when + /// `byShape`, those whose name, kind and arity fit when no id matched. + let private memberLocations + (byShape: bool) + (documentationCommentId: string) + (symbolPath: SymbolPath) + (memberType: SymbolMemberType) + (entity: FSharpEntity) = - let entities = e.TryGetMembersFunctionsAndValues() + let members = entity.TryGetMembersFunctionsAndValues() - // First, try and find entity by exact xml signature, return if found, - // otherwise, just try and match by parsed name and number of arguments. + let exact = + members + |> Seq.filter (fun m -> m.XmlDocSig = documentationCommentId) + |> Seq.map _.DeclarationLocation - let entitiesByXmlSig = - entities - |> Seq.filter (fun e -> e.XmlDocSig = documentCommentId) - |> Seq.map (fun e -> e.DeclarationLocation) - - if Seq.isEmpty entitiesByXmlSig then - tryFindValByNameAndType name symbolMemberType genericParametersCount e entities + if byShape && Seq.isEmpty exact then + tryFindValByNameAndType symbolPath.MemberOrValName memberType symbolPath.GenericParameters entity members else - entitiesByXmlSig - - /// Convert a documentation comment ID to a navigation path. - /// Uses the shared XmlDocSigParser from FSharp.Compiler.Symbols. - static member internal DocCommentIdToPath(docId: string) = - // Use the shared parser from FSharp.Compiler.Symbols - match XmlDocSigParser.parseDocCommentId docId with - | ParsedDocCommentId.Type path -> DocCommentId.Type path - - | ParsedDocCommentId.Member(typePath, memberName, genericArity, kind) -> - // Convert constructor name format (.ctor in parser, ``.ctor`` needed for F# lookup) - let memberOrValName = if memberName = ".ctor" then "``.ctor``" else memberName - - let symbolMemberType = - match kind with - | DocCommentIdKind.Method -> - if memberName = ".ctor" then - SymbolMemberType.Constructor + exact + + let private declarationsIn (byShape: bool) (signature: FSharpAssemblySignature) (documentationCommentId: string) (path: DocCommentId) = + let inEntity entityPath (locationsOf: FSharpEntity -> range seq) = + signature.FindEntityByPath entityPath + |> Option.map locationsOf + |> Option.defaultValue Seq.empty + + match path with + | DocCommentId.Member(symbolPath, memberType) -> + inEntity symbolPath.EntityPath (memberLocations byShape documentationCommentId symbolPath memberType) + | DocCommentId.Field symbolPath -> inEntity symbolPath.EntityPath (tryFindFieldByName symbolPath.MemberOrValName) + | DocCommentId.Type entityPath -> inEntity entityPath (fun entity -> Seq.singleton entity.DeclarationLocation) + | DocCommentId.None -> Seq.empty + + let private entityPathOf (path: DocCommentId) = + match path with + | DocCommentId.Member(symbolPath, _) + | DocCommentId.Field symbolPath -> symbolPath.EntityPath + | DocCommentId.Type entityPath -> entityPath + | DocCommentId.None -> [] + + /// A compiled segment of a doc id against a source segment: the generic arity suffix and the + /// `Module` suffix of `CompilationRepresentation(ModuleSuffix)` exist only in compiled names. + let private segmentMatches (compiled: ReadOnlySpan) (source: ReadOnlySpan) = + let compiled = + match compiled.IndexOf '`' with + | -1 -> compiled + | arity -> compiled.Slice(0, arity) + + compiled.Equals(source, StringComparison.Ordinal) + || (compiled.Length = source.Length + "Module".Length + && compiled.Slice(0, source.Length).Equals(source, StringComparison.Ordinal) + && compiled.Slice(source.Length).Equals("Module".AsSpan(), StringComparison.Ordinal)) + + let rec private pathMatches (entityPath: string list) (source: ReadOnlySpan) = + match entityPath with + | [] -> source.IsEmpty + | [ last ] -> source.IndexOf '.' = -1 && segmentMatches (last.AsSpan()) source + | segment :: rest -> + match source.IndexOf '.' with + | -1 -> false + | dot -> + segmentMatches (segment.AsSpan()) (source.Slice(0, dot)) + && pathMatches rest (source.Slice(dot + 1)) + + /// Whether the parsed item declares the entity the doc id names. + let declaresEntity (entityPath: string list) (item: NavigableItem) = + match item.Kind with + | NavigableItemKind.Module + | NavigableItemKind.Type + | NavigableItemKind.Exception -> + match item.Container.FullName with + | "" -> pathMatches entityPath (item.Name.AsSpan()) + | container -> pathMatches entityPath ($"{container}.{item.Name}".AsSpan()) + | _ -> false + + /// The project's documents whose parse tree declares the entity, in compile order. + let candidateDocuments (itemsCache: FSharpNavigableItemsCache) (entityPath: string list) (project: Project) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let! _, _, _, options = project.GetFSharpCompilationOptionsAsync() + + let compileOrder = Dictionary(StringComparer.OrdinalIgnoreCase) + + options.SourceFiles + |> Array.iteri (fun index path -> compileOrder[path] <- index) + + let declaresIn (document: Document) = + cancellableTask { + ct.ThrowIfCancellationRequested() + let! items = itemsCache.GetNavigableItems document + + if items |> Array.exists (declaresEntity entityPath) then + return ValueSome document else - SymbolMemberType.Method - | DocCommentIdKind.Property -> SymbolMemberType.Property - | DocCommentIdKind.Event -> SymbolMemberType.Event - | _ -> SymbolMemberType.Other + return ValueNone + } - DocCommentId.Member( - { - EntityPath = typePath - MemberOrValName = memberOrValName - GenericParameters = genericArity - }, - symbolMemberType - ) + let! candidates = + project.Documents + |> Seq.filter (fun document -> isFSharpSourceFile document.FilePath) + |> Seq.map declaresIn + // Throttle to avoid launching a parse per document in the project all at once. + |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) + + return + candidates + |> Array.chooseV id + |> Array.sortBy (fun document -> + match compileOrder.TryGetValue document.FilePath with + | true, index -> index + | _ -> Int32.MaxValue) + |> List.ofArray + } + + let private tryLocateInDocument (byShape: bool) (documentationCommentId: string) (path: DocCommentId) (document: Document) = + cancellableTask { + match! document.TryGetFSharpParseAndCheckResultsAsync UserOpName with + | ValueNone -> return ValueNone + | ValueSome(struct (_, checkResults)) -> + return + declarationsIn byShape checkResults.PartialAssemblySignature documentationCommentId path + |> Seq.tryHeadV + } + + /// Checks only the documents that declare the entity. An exact id match in any of them wins; + /// the name-and-shape heuristics run only on the last one, whose partial signature holds every + /// member the entity gets from the files that declare it. + let tryLocateViaNavigableItems + (itemsCache: FSharpNavigableItemsCache) + (documentationCommentId: string) + (path: DocCommentId) + (project: Project) + = + cancellableTask { + let! candidates = candidateDocuments itemsCache (entityPathOf path) project + + match! + candidates + |> CancellableTask.tryPick (tryLocateInDocument false documentationCommentId path) + with + | ValueSome range -> return ValueSome range + | ValueNone -> + match List.tryLast candidates with + | Some last -> return! tryLocateInDocument true documentationCommentId path last + | None -> return ValueNone + } + + /// Checks the whole project. + let tryLocateInProject (documentationCommentId: string) (path: DocCommentId) (project: Project) = + cancellableTask { + let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() + let! result = checker.ParseAndCheckProject(options) + + return + declarationsIn true result.AssemblySignature documentationCommentId path + |> Seq.tryHeadV + } + + /// The declaration's range and the project holding it, for the assembly name and doc id Roslyn passes. + let tryFindDeclaration + (itemsCache: FSharpNavigableItemsCache) + (solution: Solution) + (assemblyName: string) + (documentationCommentId: string) + = + match docCommentIdToPath documentationCommentId with + | DocCommentId.None -> CancellableTask.singleton ValueNone + | path -> + cancellableTask { + // The target frameworks of one project declare the same entities in the same files apart + // from conditional compilation, so one instance per project file goes first. + let instances = + solution.Projects + |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) + |> Seq.groupBy _.FilePath + |> Seq.map (snd >> List.ofSeq) + |> List.ofSeq + + let ordered = + [ + for instance in instances -> instance.Head + for instance in instances do + yield! instance.Tail + ] + + let located (locate: Project -> CancellableTask) (project: Project) = + locate project + |> CancellableTask.map (ValueOption.map (fun range -> struct (range, project))) + + match! + ordered + |> CancellableTask.tryPick (located (tryLocateViaNavigableItems itemsCache documentationCommentId path)) + with + | ValueSome found -> return ValueSome found + | ValueNone -> + return! + ordered + |> CancellableTask.tryPick (located (tryLocateInProject documentationCommentId path)) + } - | ParsedDocCommentId.Field(typePath, fieldName) -> - DocCommentId.Field - { - EntityPath = typePath - MemberOrValName = fieldName - GenericParameters = 0 - } +[)>] +[)>] +type internal FSharpCrossLanguageSymbolNavigationService + [] + ( + itemsCache: FSharpNavigableItemsCache, + metadataAsSource: FSharpMetadataAsSourceService, + [] workspace: VisualStudioWorkspace | null + ) = - | ParsedDocCommentId.None -> DocCommentId.None + static member internal DocCommentIdToPath(docId: string) = + CrossLanguageSymbolNavigation.docCommentIdToPath docId interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync (assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = - let path = - FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath documentationCommentId - cancellableTask { - let projects = - workspace.CurrentSolution.Projects - |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) - - let mutable locations = Seq.empty - - for project in projects do - let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() - let! result = checker.ParseAndCheckProject(options) - - match path with - | DocCommentId.Member({ - EntityPath = entityPath - MemberOrValName = memberOrVal - GenericParameters = genericParametersCount - }, - memberType) -> - let entity = result.AssemblySignature.FindEntityByPath(entityPath) - - entity - |> Option.iter (fun e -> - locations <- - e - |> tryFindVal memberOrVal documentationCommentId memberType genericParametersCount - |> Seq.map (fun m -> (m, project)) - |> Seq.append locations) - | DocCommentId.Field { - EntityPath = entityPath - MemberOrValName = memberOrVal - } -> - let entity = result.AssemblySignature.FindEntityByPath(entityPath) - - entity - |> Option.iter (fun e -> - locations <- - e - |> tryFindFieldByName memberOrVal - |> Seq.map (fun m -> (m, project)) - |> Seq.append locations) - | DocCommentId.Type entityPath -> - let entity = result.AssemblySignature.FindEntityByPath(entityPath) - - entity - |> Option.iter (fun e -> locations <- Seq.append locations [ e.DeclarationLocation, project ]) - | DocCommentId.None -> () - - // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result - // For now, we only take 1st one, since it's usually going to be only one result (given we process names correctly). - // More results can theoretically be returned in case of method overloads, or when we have both signature and implementation files. - if locations.Count() >= 1 then - let (location, project) = locations.First() - return FSharpNavigableLocation(metadataAsSource, location, project) :> IFSharpNavigableLocation - else - return Unchecked.defaultof<_> // returning null here, so Roslyn can fallback to default source-as-metadata implementation. + match workspace with + | null -> return null + | workspace -> + match! + CrossLanguageSymbolNavigation.tryFindDeclaration + itemsCache + workspace.CurrentSolution + assemblyName + documentationCommentId + with + | ValueSome(struct (range, project)) -> + return FSharpNavigableLocation(metadataAsSource, range, project) :> IFSharpNavigableLocation + | ValueNone -> + // Roslyn falls back to its own metadata-as-source when no location comes back. + return null } |> CancellableTask.start cancellationToken From 11a865a733840ef3b7f58152575496871f2beba9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 10:03:06 +0200 Subject: [PATCH 096/133] Test cross-language navigation into F# declarations Co-Authored-By: Claude Fable 5.1 --- .../CrossLanguageSymbolNavigationTests.fs | 177 ++++++++++++++++++ .../FSharp.Editor.Tests.fsproj | 1 + 2 files changed, 178 insertions(+) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs diff --git a/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs new file mode 100644 index 00000000000..17fc1494760 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CrossLanguageSymbolNavigationTests + +open System +open System.Threading +open Xunit +open Microsoft.CodeAnalysis +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Text +open FSharp.Editor.Tests.Helpers +open FSharp.Test.ProjectGeneration + +let private source = + """ +module Widgets + +type Counter(start: int) = + member val Value = start with get, set + member this.Bump() = this.Value <- this.Value + 1 + +let twice x = x * 2 + +[] +let thrice x = x * 3 + +type Shape = + | Circle of radius: float + | Square of side: float + +[] +module Shape = + let area shape = + match shape with + | Circle r -> Math.PI * r * r + | Square s -> s * s + +type Box<'T> = { Value: 'T } + +type Point = { X: int; Y: int } + +exception MyError of string +""" + +let private document = RoslynTestHelpers.GetFsDocument source +let private project = document.Project + +let private cache = + MefHelpers.createExportProvider().GetExportedValue() + +let private run computation = + computation |> CancellableTask.start CancellationToken.None |> _.Result + +/// The 1-based line of the first source line containing the text. +let private lineOf (text: string) = + source.Split('\n') + |> Array.findIndex (fun line -> line.IndexOf(text, StringComparison.Ordinal) >= 0) + |> (+) 1 + +let private items = cache.GetNavigableItems document |> run + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``the fast path finds the declaration and agrees with the whole project check`` (docId: string, declaration: string) = + let path = CrossLanguageSymbolNavigation.docCommentIdToPath docId + + let fast = + CrossLanguageSymbolNavigation.tryLocateViaNavigableItems cache docId path project + |> run + + let full = + CrossLanguageSymbolNavigation.tryLocateInProject docId path project |> run + + match fast, full with + | ValueSome fast, ValueSome full -> + Assert.Equal(full, fast) + Assert.Equal(document.FilePath, fast.FileName) + Assert.Equal(lineOf declaration, fast.StartLine) + | fast, full -> failwith $"fast path: %A{fast}, whole project: %A{full}" + +[] +[] +[] +[] +let ``an unknown or malformed id yields no location`` (docId: string) = + let found = + CrossLanguageSymbolNavigation.tryFindDeclaration cache project.Solution project.AssemblyName docId + |> run + + Assert.True(found.IsNone, $"%A{found}") + +[] +[] +[] +[] +[] +[] +let ``the parsed declarations of an entity are recognised through compiled-name artifacts`` (entityPath: string, declaration: string) = + let declaring = + items + |> Array.filter (CrossLanguageSymbolNavigation.declaresEntity (List.ofArray (entityPath.Split '.'))) + |> Array.map _.Name + + Assert.Equal([ declaration ], List.ofArray declaring |> List.distinct) + +[] +let ``members and unknown entities never pass as declarations`` () = + for entityPath in [ [ "Widgets"; "twice" ]; [ "Widgets"; "Counter"; "Bump" ]; [ "Nope" ] ] do + Assert.Empty(items |> Array.filter (CrossLanguageSymbolNavigation.declaresEntity entityPath)) + +[] +let ``candidate documents come in compile order, signature first`` () = + let syntheticProject = + SyntheticProject.Create( + sourceFile "First" [], + { sourceFile "Second" [] with + SignatureFile = AutoGenerated + }, + sourceFile "Third" [ "Second" ] + ) + + let solution, _ = RoslynTestHelpers.CreateSolution syntheticProject + let project = solution.Projects |> Seq.exactlyOne + + let candidates = + CrossLanguageSymbolNavigation.candidateDocuments cache [ syntheticProject.Name; "ModuleSecond" ] project + |> run + |> List.map _.FilePath + + Assert.Equal( + [ + syntheticProject.GetSignatureFilePath "Second" + syntheticProject.GetFilePath "Second" + ], + candidates + ) + +[] +let ``the first instance of a multi-targeted project answers`` () = + let instance () = + let id = ProjectId.CreateNewId() + + id, RoslynTestHelpers.CreateProjectInfo id "C:\\test.fsproj" [ RoslynTestHelpers.CreateDocumentInfo id "C:\\test.fs" source ] + + let firstId, first = instance () + let secondId, second = instance () + let solution = RoslynTestHelpers.CreateSolution [ first; second ] + + let options = + { RoslynTestHelpers.DefaultProjectOptions with + OtherOptions = [| "--targetprofile:netcore"; "--nowarn:3384" |] + } + + for id in [ firstId; secondId ] do + RoslynTestHelpers.SetProjectOptions id solution options + + let found = + CrossLanguageSymbolNavigation.tryFindDeclaration cache solution "test.dll" "M:Widgets.twice(System.Int32)" + |> run + + match found with + | ValueSome(struct (_, project)) -> Assert.Equal(firstId, project.Id) + | ValueNone -> failwith "declaration not found" diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index dbd8eca3fcc..e965e7e9cac 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -32,6 +32,7 @@ + From 80df6aac3df8f2d257c68d64e0a91dfb8fcf6673 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 10:29:34 +0200 Subject: [PATCH 097/133] Search each file of a multi-targeted project once and throttle across the solution Find All References and Rename searched every target-framework instance of a multi-targeted F# project in full, and built the snapshots of all projects in scope before the first search started. Instances of one project file are now grouped: the instance of the current document (or one in its dependency closure) is searched in full, the others only for files compiled solely there and for files with conditional compilation directives, whose sources can differ between instances. Searches start as soon as a project's snapshot is ready, snapshots are built only for the transparent compiler, and one SemaphoreSlim bounds the concurrent file checks across the whole search instead of per project. With EnableFastFindReferencesAndRename off every instance is still searched in full; the concurrency bound then replaces the previous per-project sequential loop. Co-Authored-By: Claude Fable 5.1 --- .../LanguageService/SymbolHelpers.fs | 123 ++++++++++++------ .../LanguageService/WorkspaceExtensions.fs | 100 +++++++++----- 2 files changed, 155 insertions(+), 68 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index becd935453a..1a87ea92eb4 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -7,6 +7,7 @@ open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable open System.IO +open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis @@ -70,34 +71,86 @@ module internal SymbolHelpers = return symbolUses } - let getSymbolUsesInProjects (symbol: FSharpSymbol, projects: Project list, onFound: Document -> range -> CancellableTask) = - match projects with + /// Ranks the target-framework instances of a project file: the one the current document lives in, + /// then those in its dependency closure, whose references resolve the symbol the same way. + let private rankInstances (currentProject: Project) = + let graph = currentProject.Solution.GetProjectDependencyGraph() + + let related = + HashSet + [ + yield! graph.GetProjectsThatThisProjectTransitivelyDependsOn currentProject.Id + yield! graph.GetProjectsThatTransitivelyDependOnThisProject currentProject.Id + ] + + fun (project: Project) -> + if project.Id = currentProject.Id then 0 + elif related.Contains project.Id then 1 + else 2 + + /// One search per project file: the best-ranked target-framework instance is searched in full, + /// the others only where their sources can differ from it. + let private groupInstances (currentProject: Project) (projects: Project list) = + let rank = rankInstances currentProject + + projects + |> List.groupBy _.FilePath + |> List.map (fun (_, instances) -> + let ordered = List.sortBy rank instances + struct (ordered.Head, ordered.Tail)) + + let getSymbolUsesInProjects + (symbol: FSharpSymbol, currentProject: Project, projects: Project list, onFound: Document -> range -> CancellableTask) + = + match projects |> List.filter _.IsFSharp with | [] -> CancellableTask.singleton () - | firstProject :: _ -> + | firstProject :: _ as projects -> let isFastFindReferencesEnabled = firstProject.IsFastFindReferencesEnabled // TODO: this needs to use already boxed boolean instead of boxing it every time. let props = [| nameof isFastFindReferencesEnabled, isFastFindReferencesEnabled :> obj |] + let groups = + if isFastFindReferencesEnabled then + groupInstances currentProject projects + else + [ for project in projects -> struct (project, []) ] + cancellableTask { // TODO: this needs to be a single event with a duration TelemetryReporter.ReportSingleEvent(TelemetryEvents.GetSymbolUsesInProjectsStarted, props) + let! ct = CancellableTask.getCancellationToken () + // Not disposed: a search started before a later snapshot fails still has to release it. + let throttle = new SemaphoreSlim(max 1 Environment.ProcessorCount) + // Mutated by the checker while a snapshot is built, so snapshots are built one at a time. let snapshotAccumulator = Dictionary() + let searches = ResizeArray() - let! projects = - projects - |> Seq.map (fun project -> - project.GetFSharpProjectSnapshot(snapshotAccumulator) - |> CancellableTask.map (fun s -> project, s)) - |> CancellableTask.sequential + let snapshotFor (project: Project) = + if project.UseTransparentCompiler then + project.GetFSharpProjectSnapshot snapshotAccumulator + |> CancellableTask.map ValueSome + else + CancellableTask.singleton ValueNone - do! - projects - |> Seq.map (fun (project, snapshot) -> - project.FindFSharpReferencesAsync(symbol, snapshot, onFound, "getSymbolUsesInProjects")) - |> CancellableTask.whenAll + let start (project: Project) snapshot searchedInstance = + searches.Add( + project.FindFSharpReferencesAsync + (symbol, snapshot, searchedInstance, throttle, onFound, "getSymbolUsesInProjects") + ct + ) + + for struct (primary, secondaries) in groups do + let! snapshot = snapshotFor primary + start primary snapshot ValueNone + + for secondary in secondaries do + let! snapshot = snapshotFor secondary + start secondary snapshot (ValueSome primary) + + do! Task.WhenAll searches TelemetryReporter.ReportSingleEvent(TelemetryEvents.GetSymbolUsesInProjectsFinished, props) } @@ -140,36 +193,32 @@ module internal SymbolHelpers = do! symbolUses |> Seq.map ((<||) onFound) |> CancellableTask.whenAll - | scope -> + | Some(SymbolScope.Projects(scopeProjects, isLocalForProject)) -> let projectsToCheck = - match scope with - | Some(SymbolScope.CurrentDocument) - | Some(SymbolScope.SignatureAndImplementation) -> - // For current document or signature/implementation, just search current project - [ currentDocument.Project ] - | Some(SymbolScope.Projects(scopeProjects, false)) -> + if isLocalForProject then + scopeProjects + else [ for scopeProject in scopeProjects do yield scopeProject yield! scopeProject.GetDependentProjects() ] |> List.distinct - | Some(SymbolScope.Projects(scopeProjects, true)) -> scopeProjects - // The symbol is declared in .NET framework, an external assembly or in a C# project within the solution. - // Optimization: Only search projects that reference the specific assembly - | None -> - match symbolUse.Symbol.Assembly.FileName with - | Some assemblyPath -> - let referencingProjects = - ProjectFiltering.getProjectsReferencingAssembly assemblyPath currentDocument.Project.Solution - - if List.isEmpty referencingProjects then - Seq.toList currentDocument.Project.Solution.Projects - else - referencingProjects - | None -> Seq.toList currentDocument.Project.Solution.Projects - - do! getSymbolUsesInProjects (symbolUse.Symbol, projectsToCheck, onFound) + + do! getSymbolUsesInProjects (symbolUse.Symbol, currentDocument.Project, projectsToCheck, onFound) + + // The symbol is declared in .NET framework, an external assembly or in a C# project within the solution. + // Optimization: Only search projects that reference the specific assembly + | None -> + let projectsToCheck = + match symbolUse.Symbol.Assembly.FileName with + | Some assemblyPath -> + match ProjectFiltering.getProjectsReferencingAssembly assemblyPath currentDocument.Project.Solution with + | [] -> Seq.toList currentDocument.Project.Solution.Projects + | referencingProjects -> referencingProjects + | None -> Seq.toList currentDocument.Project.Solution.Projects + + do! getSymbolUsesInProjects (symbolUse.Symbol, currentDocument.Project, projectsToCheck, onFound) } let getSymbolUses (symbolUse: FSharpSymbolUse) (currentDocument: Document) (checkFileResults: FSharpCheckFileResults) = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 86f53d1e3fa..5f0825eac41 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -2,7 +2,9 @@ module internal Microsoft.VisualStudio.FSharp.Editor.WorkspaceExtensions open System +open System.Collections.Generic open System.Runtime.CompilerServices +open System.Threading open Microsoft.CodeAnalysis open Microsoft.VisualStudio.FSharp.Editor @@ -11,6 +13,7 @@ open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.ProjectSnapshot open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax open FSharp.Compiler.BuildGraph open CancellableTasks @@ -706,15 +709,18 @@ type Document with | ValueNone -> return raise (OperationCanceledException("Unable to get FSharp semantic classification.")) } - /// Find F# references in the given F# document. - member inline this.FindFSharpReferencesAsync(symbol, projectSnapshot: FSharpProjectSnapshot, [] onFound, userOpName) = + /// Find F# references in the given F# document, through the project snapshot when the transparent + /// compiler is in use. + member inline this.FindFSharpReferencesAsync + (symbol, projectSnapshot: FSharpProjectSnapshot voption, [] onFound, userOpName) + = cancellableTask { let! checker, _, _, projectOptions = this.GetFSharpCompilationOptionsAsync(userOpName) let! symbolUses = - if this.Project.UseTransparentCompiler then - checker.FindBackgroundReferencesInFile(this.FilePath, projectSnapshot, symbol) - else + match projectSnapshot with + | ValueSome projectSnapshot -> checker.FindBackgroundReferencesInFile(this.FilePath, projectSnapshot, symbol) + | ValueNone -> checker.FindBackgroundReferencesInFile( this.FilePath, projectOptions, @@ -752,24 +758,39 @@ type Document with ) } +let private hasConditionalDirectives (parseTree: ParsedInput) = + match parseTree with + | ParsedInput.ImplFile file -> not file.Trivia.ConditionalDirectives.IsEmpty + | ParsedInput.SigFile file -> not file.Trivia.ConditionalDirectives.IsEmpty + type Project with - /// Find F# references in the given project. - member this.FindFSharpReferencesAsync(symbol: FSharpSymbol, projectSnapshot, onFound, userOpName) = + /// Find F# references in the given project. When `searchedInstance` is another target-framework + /// instance of the same project file that has already been searched, only the documents whose + /// sources can differ from it are searched: files compiled only here and files with conditional + /// compilation. + member this.FindFSharpReferencesAsync + ( + symbol: FSharpSymbol, + projectSnapshot: FSharpProjectSnapshot voption, + searchedInstance: Project voption, + throttle: SemaphoreSlim, + onFound, + userOpName + ) = cancellableTask { - - let declarationLocation = - symbol.SignatureLocation - |> Option.map Some - |> Option.defaultValue symbol.DeclarationLocation - let declarationDocument = - declarationLocation |> Option.bind this.Solution.TryGetDocumentFromFSharpRange + symbol.SignatureLocation + |> Option.orElse symbol.DeclarationLocation + |> Option.bind (fun range -> + this.Solution.GetDocumentIdsWithFilePath(Path.GetFullPathSafe range.FileName) + |> Seq.tryFind (fun id -> id.ProjectId = this.Id) + |> Option.map this.GetDocument) - // Can we skip documents, which are above current, since they can't contain symbols from current one. + // Documents before the declaration in compile order cannot refer to it. let! canSkipDocuments = match declarationDocument with - | Some document when this.IsFastFindReferencesEnabled && document.Project = this -> + | Some document when this.IsFastFindReferencesEnabled -> cancellableTask { let! _, _, _, options = document.GetFSharpCompilationOptionsAsync(userOpName) @@ -780,7 +801,6 @@ type Project with null return - options.SourceFiles |> Seq.takeWhile ((<>) document.FilePath) |> Seq.filter ((<>) signatureFile) @@ -788,21 +808,39 @@ type Project with } | _ -> CancellableTask.singleton Set.empty - let documents = + let searchedPaths = + searchedInstance + |> ValueOption.map (fun instance -> HashSet(instance.Documents |> Seq.map _.FilePath, StringComparer.OrdinalIgnoreCase)) + + let needsSearch (document: Document) = + match searchedPaths with + | ValueSome paths when paths.Contains document.FilePath -> + document.GetFSharpParseResultsAsync userOpName + |> CancellableTask.map (fun parseResults -> hasConditionalDirectives parseResults.ParseTree) + | _ -> CancellableTask.singleton true + + let search (document: Document) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + do! throttle.WaitAsync ct + + try + let! needed = needsSearch document + + if needed then + do! document.FindFSharpReferencesAsync(symbol, projectSnapshot, onFound document, userOpName) + finally + throttle.Release() |> ignore + } + + do! this.Documents - |> Seq.filter (fun document -> isFSharpSourceFile document.FilePath) - |> Seq.filter (fun document -> not (canSkipDocuments.Contains document.FilePath)) - - if this.IsFastFindReferencesEnabled then - do! - documents - |> Seq.map (fun doc -> - doc.FindFSharpReferencesAsync(symbol, projectSnapshot, (fun range -> onFound doc range), userOpName)) - // Throttle to avoid launching a typecheck per document in the project all at once. - |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) - else - for doc in documents do - do! doc.FindFSharpReferencesAsync(symbol, projectSnapshot, (onFound doc), userOpName) + |> Seq.filter (fun document -> + isFSharpSourceFile document.FilePath + && not (canSkipDocuments.Contains document.FilePath)) + |> Seq.map search + |> CancellableTask.whenAll + |> CancellableTask.ignore } member this.GetFSharpCompilationOptionsAsync() = this |> getFSharpOptionsForProject From bd441a971b86f73830d7910dcdc9d0dbe06b6816 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 11:15:12 +0200 Subject: [PATCH 098/133] Share the find-usages test context between test files The IFSharpFindUsagesContext stub of FindReferencesTests moves to RoslynTestHelpers.CreateFindUsagesContext so other test files can collect the definitions and references a search reports. Co-Authored-By: Claude Fable 5.1 --- .../FindReferencesTests.fs | 26 +----------------- .../Helpers/RoslynHelpers.fs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs index 5519fdd337b..4d6755736d8 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs @@ -1,12 +1,8 @@ module FSharp.Editor.Tests.FindReferencesTests -open System.Threading.Tasks -open System.Threading open System.IO -open System.Collections.Concurrent open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages -open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages open Microsoft.VisualStudio.FSharp.Editor open Xunit @@ -40,27 +36,7 @@ module FindReferences = let findUsagesService = FSharpFindUsagesService() :> IFSharpFindUsagesService let getContext () = - let foundDefinitions = ConcurrentBag() - let foundReferences = ConcurrentBag() - - let context = - { new IFSharpFindUsagesContext with - - member _.OnDefinitionFoundAsync(definition: FSharpDefinitionItem) = - foundDefinitions.Add definition - Task.CompletedTask - - member _.OnReferenceFoundAsync(reference: FSharpSourceReferenceItem) = - foundReferences.Add reference - Task.CompletedTask - - member _.ReportMessageAsync _ = Task.CompletedTask - member _.ReportProgressAsync(_, _) = Task.CompletedTask - member _.SetSearchTitleAsync _ = Task.CompletedTask - member _.CancellationToken = CancellationToken.None - } - - context, foundDefinitions, foundReferences + RoslynTestHelpers.CreateFindUsagesContext() [] let ``Find references to a document-local symbol`` () = diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs index 89a449eceb7..e42388afae1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs @@ -6,9 +6,14 @@ open System open System.IO open System.Reflection open System.Linq +open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages open Microsoft.VisualStudio.Composition open Microsoft.CodeAnalysis.Host open Microsoft.CodeAnalysis.Text @@ -305,6 +310,28 @@ type RoslynTestHelpers private () = static member SetEditorOptions (solution: Solution) options = solution.Workspace.Services.GetService().With(options) + static member CreateFindUsagesContext() = + let foundDefinitions = ConcurrentBag() + let foundReferences = ConcurrentBag() + + let context = + { new IFSharpFindUsagesContext with + member _.OnDefinitionFoundAsync definition = + foundDefinitions.Add definition + Task.CompletedTask + + member _.OnReferenceFoundAsync reference = + foundReferences.Add reference + Task.CompletedTask + + member _.ReportMessageAsync _ = Task.CompletedTask + member _.ReportProgressAsync(_, _) = Task.CompletedTask + member _.SetSearchTitleAsync _ = Task.CompletedTask + member _.CancellationToken = CancellationToken.None + } + + context, foundDefinitions, foundReferences + static member CreateSolution(source, ?options: FSharpProjectOptions, ?extraFSharpProjectOtherOptions: string array, ?editorOptions) = let projId = ProjectId.CreateNewId() From 14a1b0f01c211133e42973690d0d64f32000ab4a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 11:15:13 +0200 Subject: [PATCH 099/133] Test Find All References across target-framework instances One project loaded as two instances (one without FOO and without the fourth file, one with both): every file is reported once, and Rename gets one document per file, owned by an instance that compiles it. The fixture lives in its own file: the modules of one file share a static initializer, so a second fixture module makes the first module's values observable before they are assigned when xunit runs the classes in parallel. Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Editor.Tests.fsproj | 1 + .../MultiTargetFindReferencesTests.fs | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index e965e7e9cac..e09da139676 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -27,6 +27,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs new file mode 100644 index 00000000000..c9bc77c57d1 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// One project loaded as two target-framework instances: `plain` compiles without the fourth file +/// and without FOO, `foo` compiles everything with FOO defined. +module FSharp.Editor.Tests.MultiTargetFindReferencesTests + +open System +open System.IO +open System.Threading +open Xunit +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open FSharp.Compiler.Text +open FSharp.Editor.Tests.Helpers +open FSharp.Test.ProjectGeneration + +let private project = + SyntheticProject.Create( + { sourceFile "First" [] with + SignatureFile = AutoGenerated + ExtraSource = "let sharedFunc funcParam = funcParam * 2\n" + }, + { sourceFile "Second" [ "First" ] with + ExtraSource = "let plainUse x = ModuleFirst.sharedFunc x" + }, + { sourceFile "Third" [ "First" ] with + ExtraSource = "#if FOO\nlet fooUse x = ModuleFirst.sharedFunc x\n#endif" + }, + { sourceFile "Fourth" [ "First" ] with + ExtraSource = "let fooOnlyFileUse x = ModuleFirst.sharedFunc x" + } + ) + +let private solution, plainId, fooId = + let solution, instances = + RoslynTestHelpers.CreateMultiTargetSolution( + project, + [ + { + Defines = [] + ExcludedFileIds = [ "Fourth" ] + } + { + Defines = [ "FOO" ] + ExcludedFileIds = [] + } + ] + ) + + match instances with + | [ plainId; fooId ] -> solution, plainId, fooId + | _ -> failwith "two instances expected" + +let private firstPath = project.GetFilePath "First" + +let private declarationPosition = + (File.ReadAllText firstPath).IndexOf("sharedFunc", StringComparison.Ordinal) + +let private instanceOf index = if index = 0 then plainId else fooId + +let private documentIn (projectId: ProjectId) path = + solution.GetDocumentIdsWithFilePath path + |> Seq.find (fun id -> id.ProjectId = projectId) + |> solution.GetDocument + +[] +[] +[] +let ``every file is searched once, files under conditional compilation and instance-only files included`` (instance: int) = + let context, foundDefinitions, foundReferences = + RoslynTestHelpers.CreateFindUsagesContext() + + (FSharpFindUsagesService() :> IFSharpFindUsagesService) + .FindReferencesAsync(documentIn (instanceOf instance) firstPath, declarationPosition, context) + .Wait() + + Assert.Equal(1, foundDefinitions.Count) + // The signature, Second, Third under FOO and Fourth compiled only into foo: one hit each, not one per instance. + Assert.Equal(4, foundReferences.Count) + +/// What Rename works from: the symbol's uses grouped by Roslyn document. +let private usesByDocument (document: Document) = + let sourceText = document.GetTextAsync(CancellationToken.None).Result + let textLine = sourceText.Lines.GetLineFromPosition declarationPosition + + let fcsLine = Line.fromZ (sourceText.Lines.GetLinePosition declarationPosition).Line + + let lexerSymbol = + Tokenizer.getSymbolAtPosition ( + document.Id, + sourceText, + declarationPosition, + document.FilePath, + [], + SymbolLookupKind.Greedy, + false, + false, + None, + CancellationToken.None + ) + |> Option.defaultWith (fun () -> failwith "symbol not found") + + let _, checkFileResults = + document.GetFSharpParseAndCheckResultsAsync "MultiTargetFindReferences" + |> CancellableTask.runSynchronouslyWithoutCancellation + + let symbolUse = + checkFileResults.GetSymbolUseAtLocation(fcsLine, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland) + |> Option.defaultWith (fun () -> failwith "symbol use not found") + + SymbolHelpers.getSymbolUsesInSolution (symbolUse, checkFileResults, document) + |> CancellableTask.runSynchronouslyWithoutCancellation + +[] +[] +[] +let ``rename gets one document per file, owned by an instance that compiles it`` (instance: int) = + let uses = usesByDocument (documentIn (instanceOf instance) firstPath) + + let instancesOf fileId = + [ + for id in uses.Keys do + if solution.GetDocument(id).FilePath = project.GetFilePath fileId then + id.ProjectId + ] + + let files = [ for id in uses.Keys -> solution.GetDocument(id).FilePath ] + Assert.Equal(files.Length, List.length (List.distinct files)) + Assert.Equal([ instanceOf instance ], instancesOf "Second") + Assert.Equal([ fooId ], instancesOf "Third") + Assert.Equal([ fooId ], instancesOf "Fourth") From 7c6abbb02be60eb6a0dc35fe9788c2da67df4a14 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 11:15:34 +0200 Subject: [PATCH 100/133] Host C# projects in the editor test workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test host served language services for F# only. It now creates them for any language from the same export provider, imports the C# workspace parts, accepts .cs documents, and gains two helpers: CompileToAssembly builds a synthetic project into its OutputFilename with the checker's options, and AddCSharpProject adds a C# library referencing the framework of the F# options and given assemblies. The assemblies are added after AdhocWorkspace.AddProject, which would otherwise rewrite a reference to a project's output into a project reference; VS keeps a C# → F# reference as metadata. The smoke tests check that the C# compilation resolves the documentation comment id of an F# function, that SymbolFinder finds its call site, and that ProjectFiltering sees the C# project as a consumer of the F# assembly. Co-Authored-By: Claude Fable 5.1 --- eng/Packages.props | 1 + .../FSharp.Editor.Tests.fsproj | 2 + .../FindReferencesFromCSharpTests.fs | 70 ++++++++++++++++ .../Helpers/RoslynHelpers.fs | 80 +++++++++++++++++-- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs diff --git a/eng/Packages.props b/eng/Packages.props index 5c5405d922d..a9622277045 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -36,6 +36,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index e09da139676..119b20eadce 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -28,6 +28,7 @@ + @@ -99,6 +100,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs new file mode 100644 index 00000000000..0d6f76a4414 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// An F# library and a C# project referencing its built assembly, as VS wires a C# → F# project reference. +module FSharp.Editor.Tests.FindReferencesFromCSharpTests + +open System.Collections.Immutable +open System.Threading +open Xunit +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols +open Microsoft.VisualStudio.FSharp.Editor +open FSharp.Editor.Tests.Helpers +open FSharp.Test.ProjectGeneration + +let private library = + SyntheticProject.Create( + { sourceFile "First" [] with + ExtraSource = "let twice x = x * 2\n" + } + ) + +/// The synthetic project puts its modules in a namespace named after the project. +let private moduleName = $"{library.Name}.ModuleFirst" + +let private solution = + let librarySolution, checker = RoslynTestHelpers.CreateMultiProjectSolution library + let assembly = RoslynTestHelpers.CompileToAssembly(library, checker) + + RoslynTestHelpers.AddCSharpProject( + librarySolution, + "Consumer", + $"class Consumer {{ int M() => {moduleName}.twice(1); }}", + library.GetProjectOptions checker, + [ assembly ] + ) + +let private consumer = + solution.Projects |> Seq.find (fun p -> p.Language = LanguageNames.CSharp) + +let private fsharpDocument = + solution.GetDocumentIdsWithFilePath(library.GetFilePath "First") + |> Seq.exactlyOne + |> solution.GetDocument + +[] +let ``the C# compilation resolves the doc comment id of an F# function`` () = + let compilation = consumer.GetCompilationAsync(CancellationToken.None).Result + + let errors = + compilation.GetDiagnostics() + |> Seq.filter (fun d -> d.Severity = DiagnosticSeverity.Error) + + Assert.Empty errors + + let twice = + DocumentationCommentId.GetFirstSymbolForDeclarationId($"M:{moduleName}.twice(System.Int32)", compilation) + + Assert.NotNull twice + + let references = + SymbolFinder.FindReferencesAsync(twice, solution, ImmutableHashSet.CreateRange consumer.Documents, CancellationToken.None).Result + + Assert.Single(references |> Seq.collect _.Locations) |> ignore + +[] +let ``the consumer is found as a project referencing the F# assembly`` () = + let referencing = + ProjectFiltering.getProjectsReferencingAssembly fsharpDocument.Project.OutputFilePath solution + + Assert.Equal([ consumer.Id ], referencing |> List.map _.Id) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs index e42388afae1..fefd998e6bb 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs @@ -12,6 +12,7 @@ open System.Collections.Immutable open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CSharp open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages open Microsoft.VisualStudio.Composition @@ -20,6 +21,7 @@ open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.Editor open Microsoft.CodeAnalysis.Host.Mef open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics open FSharp.Test.ProjectGeneration [] @@ -32,6 +34,7 @@ module MefHelpers = let imports = [| "Microsoft.CodeAnalysis.Workspaces.dll" + "Microsoft.CodeAnalysis.CSharp.Workspaces.dll" "Microsoft.VisualStudio.Shell.15.0.dll" "Microsoft.VisualStudio.Platform.VSEditor.dll" "FSharp.Editor.dll" @@ -175,8 +178,7 @@ type TestHostWorkspaceServices(hostServices: HostServices, workspace: Workspace) |> Seq.distinctBy (fun x -> x.Key) |> System.Collections.Concurrent.ConcurrentDictionary - let langServices = - new TestHostLanguageServices(this, LanguageNames.FSharp, exportProvider) + let languageServices = ConcurrentDictionary() override _.Workspace = workspace @@ -194,9 +196,10 @@ type TestHostWorkspaceServices(hostServices: HostServices, workspace: Workspace) override _.FindLanguageServices(_filter) = Seq.empty override _.GetLanguageServices(languageName) = - match languageName with - | LanguageNames.FSharp -> langServices :> HostLanguageServices - | _ -> raise (NotSupportedException(sprintf "Language '%s' not supported in FSharp VS tests." languageName)) + languageServices.GetOrAdd( + languageName, + (fun language -> new TestHostLanguageServices(this, language, exportProvider) :> HostLanguageServices) + ) override _.HostServices = hostServices @@ -237,7 +240,8 @@ type RoslynTestHelpers private () = match extension with | ".fsx" -> SourceCodeKind.Script - | ".fsi" -> SourceCodeKind.Regular + | ".fsi" + | ".cs" -> SourceCodeKind.Regular | ".fs" -> SourceCodeKind.Regular | _ -> failwith "not supported" @@ -332,6 +336,70 @@ type RoslynTestHelpers private () = context, foundDefinitions, foundReferences + /// Compiles the synthetic project to its OutputFilename with the options the checker sees. + static member CompileToAssembly(syntheticProject: SyntheticProject, checker: FSharpChecker) = + let options = syntheticProject.GetProjectOptions checker + + let diagnostics, exn = + checker.Compile + [| + "fsc.exe" + "--target:library" + $"-o:{syntheticProject.OutputFilename}" + yield! options.OtherOptions + yield! options.SourceFiles + |] + |> Async.RunSynchronously + + exn |> Option.iter raise + + match + diagnostics + |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + with + | [||] -> syntheticProject.OutputFilename + | errors -> failwith $"Compilation of {syntheticProject.Name} failed: %A{errors}" + + /// Adds a C# library that references the framework of `options` and the given assemblies, the way + /// VS references an F# project from C# through its built assembly. + static member AddCSharpProject + (solution: Solution, name: string, source: string, options: FSharpProjectOptions, referencedAssemblies: string list) + = + let projectId = ProjectId.CreateNewId() + let projectDir = $"C:\\{name}" + + let projectInfo = + ProjectInfo.Create( + projectId, + VersionStamp.Create(DateTime.UtcNow), + name, + name, + LanguageNames.CSharp, + filePath = Path.Combine(projectDir, $"{name}.csproj"), + compilationOptions = CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary), + documents = + [ + RoslynTestHelpers.CreateDocumentInfo projectId (Path.Combine(projectDir, "Program.cs")) source + ], + metadataReferences = RoslynTestHelpers.MetadataReferencesOf(options, []) + ) + + let workspace = solution.Workspace :?> AdhocWorkspace + let project = workspace.AddProject projectInfo + + // AdhocWorkspace.AddProject turns a reference to another project's output into a project reference, + // whereas VS keeps a C# → F# reference as metadata; the assemblies are added afterwards. + let withAssemblies = + referencedAssemblies + |> List.fold + (fun (project: Project) assembly -> project.AddMetadataReference(MetadataReference.CreateFromFile assembly)) + project + + if not (workspace.TryApplyChanges withAssemblies.Solution) then + failwith $"Could not add the references of {name}" + + workspace.CurrentSolution + static member CreateSolution(source, ?options: FSharpProjectOptions, ?extraFSharpProjectOtherOptions: string array, ?editorOptions) = let projId = ProjectId.CreateNewId() From c15c44dd7cffb41c3b3318360795a34829a36a38 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 11:15:34 +0200 Subject: [PATCH 101/133] Report C# and VB uses of an F# symbol in Find All References Find All References searched F# documents only: C# and VB projects were never in scope, so call sites of F# functions from C# were missing. After the F# uses are reported, the symbol's documentation comment id (its XmlDocSig, the compiled form Roslyn resolves) is looked up in the compilation of every C# or VB project whose metadata references include the assembly of a project declaring the symbol, and SymbolFinder.FindReferencesAsync reports the locations under the F# definition item. Multi-targeted consumers report each file span once. Only Find All References does this: Find Implementations and Rename keep their F#-only scope, and symbols internal to their project or declared in external assemblies are skipped. Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Editor/LanguageService/Symbols.fs | 16 +++++ .../Navigation/FindUsagesService.fs | 64 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs index ecedeb3e536..e4b08e43648 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs @@ -1,6 +1,7 @@ [] module internal Microsoft.VisualStudio.FSharp.Editor.Symbols +open System open System.IO open Microsoft.CodeAnalysis open FSharp.Compiler.CodeAnalysis @@ -35,6 +36,21 @@ type FSharpSymbol with | :? FSharpField -> not publicOrInternal | _ -> false + /// The documentation comment id of the symbol's compiled form, as C# and VB compilations resolve it. + member this.DocumentationCommentId = + let xmlDocSig = + match this with + | :? FSharpMemberOrFunctionOrValue as value -> value.XmlDocSig + | :? FSharpEntity as entity -> entity.XmlDocSig + | :? FSharpField as field -> field.XmlDocSig + | :? FSharpUnionCase as unionCase -> unionCase.XmlDocSig + | _ -> "" + + if String.IsNullOrEmpty xmlDocSig then + ValueNone + else + ValueSome xmlDocSig + type FSharpSymbolUse with member this.GetSymbolScope(currentDocument: Document) : SymbolScope option = diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index 446fff005ec..d9675dca489 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -2,6 +2,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System.Collections.Generic open System.Collections.Immutable open System.Composition open System.Threading.Tasks @@ -10,6 +11,8 @@ open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages +open Microsoft.CodeAnalysis.FindSymbols +open Microsoft.CodeAnalysis.Text open FSharp.Compiler.EditorServices open FSharp.Compiler.Text @@ -44,7 +47,7 @@ module FSharpFindUsagesService = externalDefinitionItem else definitionItems - |> Array.tryFind (snd >> (=) doc.Project.FilePath) + |> Array.tryFind (fun (_, project: Project) -> project.FilePath = doc.Project.FilePath) |> Option.map (fun (definitionItem, _) -> definitionItem) |> Option.defaultValue externalDefinitionItem @@ -81,6 +84,58 @@ module FSharpFindUsagesService = return spans |> Array.choose id } + let private referencingCompilationProjects (declaringProject: Project) = + match declaringProject.OutputFilePath with + | null -> [] + | outputFilePath -> + ProjectFiltering.getProjectsReferencingAssembly outputFilePath declaringProject.Solution + |> List.filter (fun project -> not project.IsFSharp && project.SupportsCompilation) + + /// Locations in a C# or VB project of the symbol with the given documentation comment id. + let private findRoslynReferences (docId: string) (project: Project) = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + + match! project.GetCompilationAsync cancellationToken with + | null -> return Seq.empty + | compilation -> + match DocumentationCommentId.GetFirstSymbolForDeclarationId(docId, compilation) with + | null -> return Seq.empty + | symbol -> + let! referencedSymbols = + SymbolFinder.FindReferencesAsync( + symbol, + project.Solution, + ImmutableHashSet.CreateRange project.Documents, + cancellationToken + ) + + return referencedSymbols |> Seq.collect _.Locations + } + + /// Reports the uses in C# and VB projects that reference the assembly of a project declaring the symbol. + let private findCrossLanguageReferences + (docId: string) + (definitionItems: (FSharpDefinitionItem * Project)[]) + (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task) + = + cancellableTask { + let reported = HashSet() + + for definitionItem, declaringProject in definitionItems do + for project in referencingCompilationProjects declaringProject do + let! locations = findRoslynReferences docId project + + for location in locations do + let span = location.Location.SourceSpan + + if reported.Add(struct (location.Document.FilePath, span)) then + do! + onReferenceFoundAsync ( + FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(location.Document, span)) + ) + } + let findReferencedSymbolsAsync (document: Document, position: int, context: IFSharpFindUsagesContext, allReferences: bool, userOp: string) : CancellableTask = @@ -136,7 +191,7 @@ module FSharpFindUsagesService = let definitionItems = declarationSpans - |> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project.FilePath) + |> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project) do! definitionItems @@ -157,6 +212,11 @@ module FSharpFindUsagesService = context.OnReferenceFoundAsync do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound + + if allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject then + match symbolUse.Symbol.DocumentationCommentId with + | ValueSome docId -> do! findCrossLanguageReferences docId definitionItems context.OnReferenceFoundAsync + | ValueNone -> () } open FSharpFindUsagesService From 66d560b06432215f89cd5d52ebc3a027dfe441c4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 11:15:35 +0200 Subject: [PATCH 102/133] Test Find All References from an F# declaration into C# An F# library compiled to its assembly and a C# consumer calling its function: Find All References on the F# declaration reports the C# call site, Find Implementations does not, and DocumentationCommentId gives the compiled form for a module, a function and nothing for a local. Co-Authored-By: Claude Fable 5.1 --- .../FindReferencesFromCSharpTests.fs | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs index 0d6f76a4414..162ef981ae1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs @@ -3,12 +3,19 @@ /// An F# library and a C# project referencing its built assembly, as VS wires a C# → F# project reference. module FSharp.Editor.Tests.FindReferencesFromCSharpTests +open System open System.Collections.Immutable +open System.IO +open System.Reflection open System.Threading open Xunit open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages open Microsoft.CodeAnalysis.FindSymbols +open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks open FSharp.Editor.Tests.Helpers open FSharp.Test.ProjectGeneration @@ -37,11 +44,31 @@ let private solution = let private consumer = solution.Projects |> Seq.find (fun p -> p.Language = LanguageNames.CSharp) +let private firstPath = library.GetFilePath "First" + +let private declarationPosition = + (File.ReadAllText firstPath).IndexOf("twice", StringComparison.Ordinal) + let private fsharpDocument = - solution.GetDocumentIdsWithFilePath(library.GetFilePath "First") + solution.GetDocumentIdsWithFilePath firstPath |> Seq.exactlyOne |> solution.GetDocument +let private findUsagesService = + FSharpFindUsagesService() :> IFSharpFindUsagesService + +/// ExternalAccess exposes no span on a reference item; its Roslyn item is read through reflection. +let private documentSpanOf (reference: FSharpSourceReferenceItem) = + let flags = BindingFlags.Instance ||| BindingFlags.NonPublic ||| BindingFlags.Public + + let property (target: obj) name = + target.GetType().GetProperty(name, flags).GetValue target + + let documentSpan = + property (property reference "RoslynSourceReferenceItem") "SourceSpan" + + property documentSpan "Document" :?> Document, property documentSpan "SourceSpan" :?> TextSpan + [] let ``the C# compilation resolves the doc comment id of an F# function`` () = let compilation = consumer.GetCompilationAsync(CancellationToken.None).Result @@ -68,3 +95,46 @@ let ``the consumer is found as a project referencing the F# assembly`` () = ProjectFiltering.getProjectsReferencingAssembly fsharpDocument.Project.OutputFilePath solution Assert.Equal([ consumer.Id ], referencing |> List.map _.Id) + +[] +[] +[] +[] +let ``DocumentationCommentId is the compiled form Roslyn resolves`` (symbolName: string, expectedFormat: string) = + let expected = + expectedFormat + |> ValueOption.ofObj + |> ValueOption.map (fun format -> String.Format(format, moduleName)) + + let _, checkFileResults = + fsharpDocument.GetFSharpParseAndCheckResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + let symbol = + checkFileResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.find (fun symbolUse -> symbolUse.IsFromDefinition && symbolUse.Symbol.DisplayName = symbolName) + |> _.Symbol + + Assert.Equal(expected, symbol.DocumentationCommentId) + +[] +let ``Find All References on an F# function reports its C# call site`` () = + let context, foundDefinitions, foundReferences = + RoslynTestHelpers.CreateFindUsagesContext() + + findUsagesService.FindReferencesAsync(fsharpDocument, declarationPosition, context).Wait() + + Assert.Equal(1, foundDefinitions.Count) + let document, span = documentSpanOf (Assert.Single foundReferences) + Assert.Equal(LanguageNames.CSharp, document.Project.Language) + + let text = document.GetTextAsync(CancellationToken.None).Result + Assert.Equal("twice", text.ToString span) + +[] +let ``Find Implementations on an F# function does not report C# call sites`` () = + let context, _, foundReferences = RoslynTestHelpers.CreateFindUsagesContext() + + findUsagesService.FindImplementationsAsync(fsharpDocument, declarationPosition, context).Wait() + + Assert.Empty foundReferences From fe6fde6d9b9b4dd2bc8475168f9babc49a3885c6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 12:49:28 +0200 Subject: [PATCH 103/133] Add the release note for PR #20463 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 7b32e0c3512..41624c11ce8 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -3,6 +3,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) * F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409)) +* Find All References on an F# symbol also lists its uses in C# and Visual Basic projects that reference the F# project's built assembly. ([PR #20463](https://github.com/dotnet/fsharp/pull/20463)) ### Fixed From 1efdb9d250234f9003aa3c603410c6643ef226b5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 13:04:34 +0200 Subject: [PATCH 104/133] Add the release note for PR #20464 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 41624c11ce8..5e24547187b 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -29,6 +29,7 @@ * Keep the collapsed regions of a file while its project is loading or reloading: outlining now parses with the quick parsing options instead of answering with no regions, which expanded everything the user had folded. ([PR #20451](https://github.com/dotnet/fsharp/pull/20451)) * Fix syntax coloring being lost for a whole file when one symbol resolves into metadata that could not be read. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Go To Definition on a symbol declared in another project of the solution no longer opens a generated signature when the origin document comes from a solution snapshot that predates that project's documents: navigation, Find All References and Rename now look the target up in the workspace's current solution as well. ([PR #20462](https://github.com/dotnet/fsharp/pull/20462)) +* Find All References and Rename search each file of a multi-targeted F# project once instead of once per target framework, revisiting a file in another instance only when it is compiled there alone or under conditional compilation; searches start as each project's snapshot is ready and type checks are bounded across the solution. ([PR #20464](https://github.com/dotnet/fsharp/pull/20464)) ### Changed From 8a82896fe209d6923d3bc2e88c53822501e57115 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 13:55:46 +0200 Subject: [PATCH 105/133] Add the release note for PR #20465 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 5e24547187b..a3e3fcaa66a 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -30,6 +30,7 @@ * Fix syntax coloring being lost for a whole file when one symbol resolves into metadata that could not be read. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) * Go To Definition on a symbol declared in another project of the solution no longer opens a generated signature when the origin document comes from a solution snapshot that predates that project's documents: navigation, Find All References and Rename now look the target up in the workspace's current solution as well. ([PR #20462](https://github.com/dotnet/fsharp/pull/20462)) * Find All References and Rename search each file of a multi-targeted F# project once instead of once per target framework, revisiting a file in another instance only when it is compiled there alone or under conditional compilation; searches start as each project's snapshot is ready and type checks are bounded across the solution. ([PR #20464](https://github.com/dotnet/fsharp/pull/20464)) +* Go To Definition from C# or Visual Basic into an F# project no longer type checks the whole project: the file declaring the symbol is found from parsed declarations and checked alone, with the whole-project check as a fallback. ([PR #20465](https://github.com/dotnet/fsharp/pull/20465)) ### Changed From 09fa0496f337774ab2fddb0ae0a020cb3e97e5e3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 18:40:41 +0200 Subject: [PATCH 106/133] Navigate to union cases and module literals from their compiled members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go To Definition from C# on `Shape.NewCircle(…)`, `shape.IsCircle` or a nullary case property found no F# declaration: the compiled members of a union case are not among the entity's members, so both the exact and the shape lookup came back empty and Roslyn decompiled instead. A module literal has the same fate: C# sees a const field, and the F# side only searched the entity's fields. Both now map back to their declaration. Co-Authored-By: Claude Fable 5.1 --- .../CrossLanguageSymbolNavigation.fs | 39 ++++++++++++++++--- .../CrossLanguageSymbolNavigationTests.fs | 9 +++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs index fad92e13c1e..70862164b45 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs @@ -114,16 +114,26 @@ module internal CrossLanguageSymbolNavigation = | ParsedDocCommentId.None -> DocCommentId.None + /// The fields of the entity, and the literals of a module, which compile to fields. let private tryFindFieldByName (name: string) (e: FSharpEntity) = let fields = e.FSharpFields |> Seq.filter (fun x -> x.DisplayName = name && not x.IsCompilerGenerated) - |> Seq.map (fun e -> e.DeclarationLocation) + |> Seq.map _.DeclarationLocation + + let literals = + e.TryGetMembersFunctionsAndValues() + |> Seq.filter (fun v -> v.LiteralValue.IsSome && (v.CompiledName = name || v.DisplayName = name)) + |> Seq.map _.DeclarationLocation - if Seq.isEmpty fields && (e.IsFSharpUnion || e.IsFSharpRecord) then + if + Seq.isEmpty fields + && Seq.isEmpty literals + && (e.IsFSharpUnion || e.IsFSharpRecord) + then Seq.singleton e.DeclarationLocation else - fields + Seq.append fields literals let private tryFindValByNameAndType (name: string) @@ -160,6 +170,18 @@ module internal CrossLanguageSymbolNavigation = filteredEntities + /// The union case behind its compiled members: the `NewCase` factory, the `IsCase` tester and + /// the `Case` property of a nullary case. + let private unionCaseLocations (name: string) (entity: FSharpEntity) = + if entity.IsFSharpUnion then + entity.UnionCases + |> Seq.filter (fun unionCase -> + let compiled = unionCase.CompiledName + name = compiled || name = $"New{compiled}" || name = $"Is{compiled}") + |> Seq.map _.DeclarationLocation + else + Seq.empty + /// The members of the entity the id names: those whose compiled id matches exactly and, when /// `byShape`, those whose name, kind and arity fit when no id matched. let private memberLocations @@ -172,9 +194,14 @@ module internal CrossLanguageSymbolNavigation = let members = entity.TryGetMembersFunctionsAndValues() let exact = - members - |> Seq.filter (fun m -> m.XmlDocSig = documentationCommentId) - |> Seq.map _.DeclarationLocation + seq { + yield! + members + |> Seq.filter (fun m -> m.XmlDocSig = documentationCommentId) + |> Seq.map _.DeclarationLocation + + yield! unionCaseLocations symbolPath.MemberOrValName entity + } if byShape && Seq.isEmpty exact then tryFindValByNameAndType symbolPath.MemberOrValName memberType symbolPath.GenericParameters entity members diff --git a/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs index 17fc1494760..a8aa600540b 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CrossLanguageSymbolNavigationTests.fs @@ -29,6 +29,7 @@ let thrice x = x * 3 type Shape = | Circle of radius: float | Square of side: float + | Dot [] module Shape = @@ -36,6 +37,10 @@ module Shape = match shape with | Circle r -> Math.PI * r * r | Square s -> s * s + | Dot -> 0.0 + +[] +let Answer = 42 type Box<'T> = { Value: 'T } @@ -70,6 +75,10 @@ let private items = cache.GetNavigableItems document |> run [] [] [] +[] +[] +[] +[] [] [] [] From 361ba95c4c6dcf6869d7bb54e1b3f81055bc9e8d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Sep 2026 18:40:41 +0200 Subject: [PATCH 107/133] Give a literal the field id Roslyn resolves FCS names a module literal `P:` like any module value, but it compiles to a const field, so `DocumentationCommentId.GetFirstSymbolForDeclarationId` found nothing and Find All References showed no C# uses of it. The id now starts with `F:` for a literal. Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Editor/LanguageService/Symbols.fs | 6 +++++- .../FindReferencesFromCSharpTests.fs | 20 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs index e4b08e43648..9c14f6d58fc 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs @@ -40,7 +40,11 @@ type FSharpSymbol with member this.DocumentationCommentId = let xmlDocSig = match this with - | :? FSharpMemberOrFunctionOrValue as value -> value.XmlDocSig + | :? FSharpMemberOrFunctionOrValue as value -> + match value.XmlDocSig with + // A literal compiles to a field, which Roslyn names F: where FCS says P:. + | docSig when value.LiteralValue.IsSome && docSig.StartsWith("P:", StringComparison.Ordinal) -> $"F:{docSig.Substring 2}" + | docSig -> docSig | :? FSharpEntity as entity -> entity.XmlDocSig | :? FSharpField as field -> field.XmlDocSig | :? FSharpUnionCase as unionCase -> unionCase.XmlDocSig diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs index 162ef981ae1..f678f649916 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs @@ -22,7 +22,7 @@ open FSharp.Test.ProjectGeneration let private library = SyntheticProject.Create( { sourceFile "First" [] with - ExtraSource = "let twice x = x * 2\n" + ExtraSource = "let twice x = x * 2\n[]\nlet answer = 42\n" } ) @@ -36,7 +36,7 @@ let private solution = RoslynTestHelpers.AddCSharpProject( librarySolution, "Consumer", - $"class Consumer {{ int M() => {moduleName}.twice(1); }}", + $"class Consumer {{ int M() => {moduleName}.twice(1); int N() => {moduleName}.answer; }}", library.GetProjectOptions checker, [ assembly ] ) @@ -99,6 +99,7 @@ let ``the consumer is found as a project referencing the F# assembly`` () = [] [] [] +[] [] let ``DocumentationCommentId is the compiled form Roslyn resolves`` (symbolName: string, expectedFormat: string) = let expected = @@ -131,6 +132,21 @@ let ``Find All References on an F# function reports its C# call site`` () = let text = document.GetTextAsync(CancellationToken.None).Result Assert.Equal("twice", text.ToString span) +[] +let ``Find All References on an F# literal reports its C# use`` () = + let context, _, foundReferences = RoslynTestHelpers.CreateFindUsagesContext() + + let position = + (File.ReadAllText firstPath).IndexOf("answer", StringComparison.Ordinal) + + findUsagesService.FindReferencesAsync(fsharpDocument, position, context).Wait() + + let document, span = documentSpanOf (Assert.Single foundReferences) + Assert.Equal(LanguageNames.CSharp, document.Project.Language) + + let text = document.GetTextAsync(CancellationToken.None).Result + Assert.Equal("answer", text.ToString span) + [] let ``Find Implementations on an F# function does not report C# call sites`` () = let context, _, foundReferences = RoslynTestHelpers.CreateFindUsagesContext() From 9d60b0dfcf461e2c8a912a47e05857e014b8d860 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 03:58:47 +0200 Subject: [PATCH 108/133] Ignore --pathmap in the IDE's project options A project built with DeterministicSourcePaths or an explicit PathMap hands the IDE a `--pathmap:` option. FCS applies the map when it pickles the ranges of the in-memory reference other projects check against, so every symbol imported from such a project names a mapped, relative file that no workspace document has, and Go To Definition ends in the generated signature instead of the source. The map is a property of the build output; the IDE now drops it. Co-Authored-By: Claude Fable 5.1 --- .../LanguageService/FSharpProjectOptionsManager.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index a4fdfdcc2d1..1731c78aa70 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -559,8 +559,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker, fileChangeWatch [| // Clear any references from CompilationOptions. // We get the references from Project.ProjectReferences/Project.MetadataReferences. + // A path map belongs to the build output: applied here it rewrites the file name of + // every range imported from a referenced project, and navigation finds no document. for x in projectSite.CompilationOptions do - if not (x.Contains("-r:")) then + if not (x.Contains("-r:") || x.StartsWith("--pathmap:", StringComparison.Ordinal)) then x for x in project.MetadataReferences.OfType() do From 88e2509c06ce74247fbca0bda5a7df0a327d8347 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 03:58:48 +0200 Subject: [PATCH 109/133] Test navigation into a project built with a path map Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Editor.Tests.fsproj | 1 + .../PathMapNavigationTests.fs | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/PathMapNavigationTests.fs diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 119b20eadce..c658ba193f4 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -30,6 +30,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/PathMapNavigationTests.fs b/vsintegration/tests/FSharp.Editor.Tests/PathMapNavigationTests.fs new file mode 100644 index 00000000000..0fc8c7ffe77 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/PathMapNavigationTests.fs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// A library whose build maps its source paths, as DeterministicSourcePaths does: the symbols another +/// project imports from it must still name the files of the workspace. +module FSharp.Editor.Tests.PathMapNavigationTests + +open System +open System.IO +open System.Threading +open Xunit +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open FSharp.Editor.Tests.Helpers +open FSharp.Test.ProjectGeneration + +/// As Directory.Build.props would set it: the same map on every project of the solution. +let private pathMap (project: SyntheticProject) = + [ $"--pathmap:{Path.GetDirectoryName project.ProjectDir}=.\\" ] + +let private library = + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + + { library with + OtherOptions = pathMap library + } + +let private app = + let app = SyntheticProject.Create("App", sourceFile "App" [ "Library" ]) + + { app with + DependsOn = [ library ] + OtherOptions = pathMap app + } + +let private solution, _ = RoslynTestHelpers.CreateMultiProjectSolution app + +let private documentOf (project: SyntheticProject) fileId = + solution.GetDocumentIdsWithFilePath(project.GetFilePath fileId) + |> Seq.exactlyOne + |> solution.GetDocument + +[] +let ``the path map of a project is not applied in the IDE`` () = + let _, _, _, options = + (documentOf library "Library").GetFSharpCompilationOptionsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.DoesNotContain(options.OtherOptions, fun option -> option.StartsWith("--pathmap:", StringComparison.Ordinal)) + +[] +let ``goto definition into a project built with a path map reaches its source`` () = + let appDocument = documentOf app "App" + let text = appDocument.GetTextAsync(CancellationToken.None).Result.ToString() + + let position = + text.IndexOf("ModuleLibrary.f", StringComparison.Ordinal) + + "ModuleLibrary.f".Length + - 1 + + let result = + GoToDefinition(FSharpMetadataAsSourceService()).FindDefinitionAtPosition(appDocument, position) + |> CancellableTask.runSynchronouslyWithoutCancellation + + match result with + | ValueSome(FSharpGoToDefinitionResult.NavigableItem item, _) -> Assert.Equal(library.GetFilePath "Library", item.Document.FilePath) + | result -> failwith $"expected a navigable item, got %A{result}" From a42ebf2dd296db3da2915ee2eb4871e4ed093055 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 04:08:07 +0200 Subject: [PATCH 110/133] Add the release note for PR #20470 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index a3e3fcaa66a..c901f7014a3 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -31,6 +31,7 @@ * Go To Definition on a symbol declared in another project of the solution no longer opens a generated signature when the origin document comes from a solution snapshot that predates that project's documents: navigation, Find All References and Rename now look the target up in the workspace's current solution as well. ([PR #20462](https://github.com/dotnet/fsharp/pull/20462)) * Find All References and Rename search each file of a multi-targeted F# project once instead of once per target framework, revisiting a file in another instance only when it is compiled there alone or under conditional compilation; searches start as each project's snapshot is ready and type checks are bounded across the solution. ([PR #20464](https://github.com/dotnet/fsharp/pull/20464)) * Go To Definition from C# or Visual Basic into an F# project no longer type checks the whole project: the file declaring the symbol is found from parsed declarations and checked alone, with the whole-project check as a fallback. ([PR #20465](https://github.com/dotnet/fsharp/pull/20465)) +* Go To Definition into an F# project built with a path map (`DeterministicSourcePaths` or `PathMap`) opens its source instead of a generated signature: the IDE no longer applies `--pathmap` to the project options it checks with. ([PR #20470](https://github.com/dotnet/fsharp/pull/20470)) ### Changed From 090cd93995b4dc8b9b6d765e1c8dcb4c8d99aad6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 111/133] 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 f3dc5086fe4..9f8effbcd7f 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 ecb5d68dc25..86a88fa0c1f 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 1bf046606c3d8834782c20caa719a5ef0bd48ace Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 112/133] 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 | 54 +++++++++++++++++++ 2 files changed, 56 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 cc7e109373f..1d496b7b83b 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -1,4 +1,4 @@ - + @@ -537,6 +537,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..5657052364b --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -0,0 +1,54 @@ +module FSharpChecker.PathMap + +open System.IO +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.RunSynchronously |> 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) = + 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 ] } + + checkWith checker mapped + checkWith checker app + + let appFile = app.GetFilePath "App" + + let _, answer = + checker.ParseAndCheckFileInProject( + appFile, + 0, + SourceText.ofString (File.ReadAllText appFile), + app.GetProjectOptions checker + ) + |> Async.RunSynchronously + + 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 8f44a346dfcb9a295cec8078ab6fd1a106469669 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 12:32:20 +0200 Subject: [PATCH 113/133] 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 d5c98fc3867..43baf245d5f 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 * `Internal.Utilities.Library`: `String.StartsWithOrdinalIgnoreCase` extension, the `StartsWith` sibling of `EndsWithOrdinalIgnoreCase`. ([PR #20457](https://github.com/dotnet/fsharp/pull/20457)) From 71cea4d10541d178c64fb7b4376b46487a9d27d0 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 14:52:59 +0200 Subject: [PATCH 114/133] Make the path-map checker test asynchronous Co-Authored-By: Claude Fable 5.1 --- .../FSharpChecker/PathMap.fs | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 5657052364b..18e99d829ce 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -1,54 +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.RunSynchronously |> ignore + 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) = - 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 ] } - - checkWith checker mapped - checkWith checker app - - let appFile = app.GetFilePath "App" - - let _, answer = - checker.ParseAndCheckFileInProject( - appFile, - 0, - SourceText.ofString (File.ReadAllText appFile), - app.GetProjectOptions checker - ) - |> Async.RunSynchronously - - 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) +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 cc43c2e81896130bbcdd6e0adb06a5b3b3f8f43b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 17:59:02 +0200 Subject: [PATCH 115/133] Guard the stack when checking long sequence expressions `TcSequenceExpression` recursed once per `expr; rest` node of a `seq { }` body through `tcSequenceExprBody`, `tcSequenceExprBodyAsSequenceOrStatement` and `tryTcSequenceExprBody` with no stack guard on that spine; only the leaves were guarded, via `TcExpr`. On a 1 MB thread-pool thread a few hundred (Debug) to a few thousand (Release) implicit-yield elements ran the thread out of stack a few frames past a leaf. With the spine guarded, `CheckNoReraise` then walked the right-nested `Seq.append`/`Seq.delay` tree with `freeInExpr CollectLocals`, whose `stackGuard` is `None`, and overflowed in `accFreeInExprNonLinearImpl`. Run `tcSequenceExprBodyAsSequenceOrStatement` under `cenv.stackGuard` and give `CheckNoReraise` the guarded free-variable options `CheckEscapes` already uses. Co-Authored-By: Claude Opus 5 (1M context) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + .../Expressions/CheckSequenceExpressions.fs | 77 ++++++++++--------- src/Compiler/Checking/PostInferenceChecks.fs | 2 +- .../SequenceExpressionTests.fs | 16 +++- 4 files changed, 56 insertions(+), 40 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 43baf245d5f..9a16061ffe3 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 * Debug builds of FCS no longer write `StandardOutput.txt`/`StandardError.txt` into the project (or script) directory when probing `dotnet --version` for the SDK. With a Debug VSIX in Visual Studio those files landed inside SDK-style project folders and kept re-triggering project re-evaluation -> design-time build -> NuGet restore -> F# re-check. ([PR #20458](https://github.com/dotnet/fsharp/pull/20458)) +* Fix `StackOverflowException` when checking a long `seq { ... }` body. The `expr; expr; ...` spine of a sequence expression was recursed without a stack guard, and the post-inference `CheckNoReraise` walked the resulting right-nested `Seq.append`/`Seq.delay` tree with unguarded free-variable options, so a few hundred (Debug) to a few thousand (Release) implicit-yield elements overflowed a 1 MB thread-pool thread in the IDE. `tcSequenceExprBodyAsSequenceOrStatement` now runs under `cenv.stackGuard` and `CheckNoReraise` uses `CollectLocalsWithStackGuard`. * 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/Checking/Expressions/CheckSequenceExpressions.fs b/src/Compiler/Checking/Expressions/CheckSequenceExpressions.fs index 89c5afea9c6..1e4f6214d6e 100644 --- a/src/Compiler/Checking/Expressions/CheckSequenceExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckSequenceExpressions.fs @@ -412,45 +412,46 @@ let TcSequenceExpression (cenv: TcFileState) env tpenv comp (overallTy: OverallT resExpr, tpenv and tcSequenceExprBodyAsSequenceOrStatement env genOuterTy tpenv comp = - match tryTcSequenceExprBody env genOuterTy tpenv comp with - | Some(expr, tpenv) -> Choice1Of2 expr, tpenv - | None -> - - let env = - { env with - eContextInfo = ContextInfo.SequenceExpression genOuterTy - } - - if enableImplicitYield then - // The body is speculatively type-checked once to classify it as a statement or a yielded - // element. Reporting is buffered so the kept interpretation reports exactly once (else - // format-specifier locations and diagnostics double - #16419): a unit statement keeps the - // probe result and its buffered reporting is committed; a yielded element drops it and - // TcExprFlex re-checks with the element's target type. - let hasTypeUnit, _ty, expr, tpenv = - RunWithBufferedReporting - cenv.tcSink - "SeqImplicitYieldProbe" - (fun () -> TryTcStmt cenv env tpenv comp) - (fun (hasTypeUnit, _, _, _) -> hasTypeUnit) - - if hasTypeUnit then - Choice2Of2 expr, tpenv + cenv.stackGuard.Guard(fun () -> + match tryTcSequenceExprBody env genOuterTy tpenv comp with + | Some(expr, tpenv) -> Choice1Of2 expr, tpenv + | None -> + + let env = + { env with + eContextInfo = ContextInfo.SequenceExpression genOuterTy + } + + if enableImplicitYield then + // The body is speculatively type-checked once to classify it as a statement or a yielded + // element. Reporting is buffered so the kept interpretation reports exactly once (else + // format-specifier locations and diagnostics double - #16419): a unit statement keeps the + // probe result and its buffered reporting is committed; a yielded element drops it and + // TcExprFlex re-checks with the element's target type. + let hasTypeUnit, _ty, expr, tpenv = + RunWithBufferedReporting + cenv.tcSink + "SeqImplicitYieldProbe" + (fun () -> TryTcStmt cenv env tpenv comp) + (fun (hasTypeUnit, _, _, _) -> hasTypeUnit) + + if hasTypeUnit then + Choice2Of2 expr, tpenv + else + let genResultTy = NewInferenceType g + let mExpr = expr.Range + UnifyTypes cenv env mExpr genOuterTy (mkSeqTy cenv.g genResultTy) + let expr, tpenv = TcExprFlex cenv flex true genResultTy env tpenv comp + let exprTy = tyOfExpr cenv.g expr + AddCxTypeMustSubsumeType env.eContextInfo env.DisplayEnv cenv.css mExpr NoTrace genResultTy exprTy + + let resExpr = + mkCallSeqSingleton cenv.g mExpr genResultTy (mkCoerceExpr (expr, genResultTy, mExpr, exprTy)) + + Choice1Of2 resExpr, tpenv else - let genResultTy = NewInferenceType g - let mExpr = expr.Range - UnifyTypes cenv env mExpr genOuterTy (mkSeqTy cenv.g genResultTy) - let expr, tpenv = TcExprFlex cenv flex true genResultTy env tpenv comp - let exprTy = tyOfExpr cenv.g expr - AddCxTypeMustSubsumeType env.eContextInfo env.DisplayEnv cenv.css mExpr NoTrace genResultTy exprTy - - let resExpr = - mkCallSeqSingleton cenv.g mExpr genResultTy (mkCoerceExpr (expr, genResultTy, mExpr, exprTy)) - - Choice1Of2 resExpr, tpenv - else - let stmt, tpenv = TcStmtThatCantBeCtorBody cenv env tpenv comp - Choice2Of2 stmt, tpenv + let stmt, tpenv = TcStmtThatCantBeCtorBody cenv env tpenv comp + Choice2Of2 stmt, tpenv) let coreExpr, tpenv = tcSequenceExprBody env overallTy.Commit tpenv comp let delayedExpr = mkSeqDelayedExpr coreExpr.Range coreExpr diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index 36427480f04..d1d93175a07 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -761,7 +761,7 @@ let (|OptionalCoerce|) expr = let CheckNoReraise cenv freesOpt (body: Expr) = if cenv.reportErrors then // Avoid recomputing the free variables - let fvs = match freesOpt with None -> freeInExpr CollectLocals body | Some fvs -> fvs + let fvs = match freesOpt with None -> freeInExpr (CollectLocalsWithStackGuard()) body | Some fvs -> fvs if fvs.UsesUnboundRethrow then errorR(Error(FSComp.SR.chkErrorContainsCallToRethrow(), body.Range)) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/SequenceExpressions/SequenceExpressionTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/SequenceExpressions/SequenceExpressionTests.fs index 049971fb261..534dc156f27 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/SequenceExpressions/SequenceExpressionTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/SequenceExpressions/SequenceExpressionTests.fs @@ -789,4 +789,18 @@ let f1() = |> asExe |> ignoreWarnings |> compileAndRun - |> shouldSucceed \ No newline at end of file + |> shouldSucceed + + [] + let ``Long sequence of implicit yields does not overflow the stack``() = + let elements = String.concat "\n" [ for i in 1..1000 -> $" {{ Name = \"n{i}\"; Id = {i} }}" ] + FSharp $""" +module M +type Data = {{ Name: string; Id: int }} +let all = + seq {{ +{elements} + }} +""" + |> typecheck + |> shouldSucceed From c10369a2760e3c413527237c211712c2b8d5c635 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 17:59:43 +0200 Subject: [PATCH 116/133] Add the PR link to the release note Co-Authored-By: Claude Opus 5 (1M context) --- 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 9a16061ffe3..4cbb4e2321d 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 @@ * Debug builds of FCS no longer write `StandardOutput.txt`/`StandardError.txt` into the project (or script) directory when probing `dotnet --version` for the SDK. With a Debug VSIX in Visual Studio those files landed inside SDK-style project folders and kept re-triggering project re-evaluation -> design-time build -> NuGet restore -> F# re-check. ([PR #20458](https://github.com/dotnet/fsharp/pull/20458)) * Fix `StackOverflowException` when checking a long `seq { ... }` body. The `expr; expr; ...` spine of a sequence expression was recursed without a stack guard, and the post-inference `CheckNoReraise` walked the resulting right-nested `Seq.append`/`Seq.delay` tree with unguarded free-variable options, so a few hundred (Debug) to a few thousand (Release) implicit-yield elements overflowed a 1 MB thread-pool thread in the IDE. `tcSequenceExprBodyAsSequenceOrStatement` now runs under `cenv.stackGuard` and `CheckNoReraise` uses `CollectLocalsWithStackGuard`. +* Fix `StackOverflowException` when checking a long `seq { ... }` body. The `expr; expr; ...` spine of a sequence expression was recursed without a stack guard, and the post-inference `CheckNoReraise` walked the resulting right-nested `Seq.append`/`Seq.delay` tree with unguarded free-variable options, so a few hundred (Debug) to a few thousand (Release) implicit-yield elements overflowed a 1 MB thread-pool thread in the IDE. `tcSequenceExprBodyAsSequenceOrStatement` now runs under `cenv.stackGuard` and `CheckNoReraise` uses `CollectLocalsWithStackGuard`. ([PR #20480](https://github.com/dotnet/fsharp/pull/20480)) * 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)) From a06b67b07632c9054b61089091e83301e73acaa8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 20:15:02 +0200 Subject: [PATCH 117/133] Search each file of a multi-targeted project once in NavigateTo and read its text only for matches Roslyn's NavigateTo searcher hands the F# service every target-framework instance of a project, one after another, and the service parsed every file of each instance, read the text of every file before matching anything, and started all of that for a project at once. On a solution with 135 project instances that meant one parse and one file read per file per framework, thousands of concurrent tasks, and results that arrived long after the user stopped typing. The first instance of a project file in the solution now searches every file; the other instances only search the files they alone compile and the files whose parse depends on the defines, known from whichever instance parsed the file first. A file's text is read only when one of its declarations matched, and a project's files are searched at most ProcessorCount at a time. Co-Authored-By: Claude Fable 5.1 --- .../Navigation/NavigateToSearchService.fs | 99 ++++++++++++++----- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index 75a349040b2..f5f1620f3df 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -18,6 +18,7 @@ open Microsoft.VisualStudio.LanguageServices open Microsoft.VisualStudio.Text.PatternMatching open FSharp.Compiler.EditorServices +open FSharp.Compiler.Syntax open CancellableTasks /// Parse-tree navigable items per document, cached on the document's text version. @@ -30,13 +31,23 @@ type internal FSharpNavigableItemsCache let cache = ConcurrentDictionary() + /// Whether the file's parse depends on the defines, by file path: known once any instance has parsed it. + let conditionalDirectives = + ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + do match workspace with | null -> () | workspace -> workspace.WorkspaceChanged.Add(fun e -> if e.NewSolution.Id <> e.OldSolution.Id then - cache.Clear()) + cache.Clear() + conditionalDirectives.Clear()) + + let hasConditionalDirectives (parseTree: ParsedInput) = + match parseTree with + | ParsedInput.ImplFile file -> not file.Trivia.ConditionalDirectives.IsEmpty + | ParsedInput.SigFile file -> not file.Trivia.ConditionalDirectives.IsEmpty member _.GetNavigableItems(document: Document) = cancellableTask { @@ -49,9 +60,21 @@ type internal FSharpNavigableItemsCache let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigableItemsCache)) let items = NavigateTo.GetNavigableItems parseResults.ParseTree cache[document.Id] <- struct (currentVersion, items) + + match document.FilePath with + | null -> () + | path -> conditionalDirectives[path] <- hasConditionalDirectives parseResults.ParseTree + return items } + /// Whether the file's parse tree, from whichever instance parsed it first, has `#if` directives. + /// `true` when unknown, so a file no instance has parsed yet is searched everywhere. + member _.HasConditionalDirectives(filePath: string) = + match conditionalDirectives.TryGetValue filePath with + | true, dependsOnDefines -> dependsOnDefines + | _ -> true + member _.CreateMatcherFor(searchPattern: string) = let patternMatcher = patternMatcherFactory.CreatePatternMatcher( @@ -85,6 +108,31 @@ type internal FSharpNavigateToSearchService [] (itemsCache let getNavigableItems (document: Document) = itemsCache.GetNavigableItems document + /// A multi-targeted project is one Roslyn project per target framework over the same files. The + /// first instance in the solution searches every file; the others only the files they alone compile + /// and the files whose parse depends on the defines. + let searchedIn (project: Project) = + match project.FilePath with + | null -> fun (_: Document) -> true + | projectPath -> + let instances = + project.Solution.Projects + |> Seq.filter (fun p -> p.FilePath = projectPath) + |> Seq.map _.Id + |> List.ofSeq + + fun (document: Document) -> + match document.FilePath with + | null -> true + | path -> + let documentIds = project.Solution.GetDocumentIdsWithFilePath path + + let owner = + instances + |> List.find (fun id -> documentIds |> Seq.exists (fun documentId -> documentId.ProjectId = id)) + + owner = project.Id || itemsCache.HasConditionalDirectives path + let kindsProvided = ImmutableHashSet.Create( FSharpNavigateToItemKind.Module, @@ -158,46 +206,45 @@ type internal FSharpNavigateToSearchService [] (itemsCache let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet) (document: Document) = cancellableTask { - let! ct = CancellableTask.getCancellationToken () - - let! sourceText = document.GetTextAsync ct - let! items = getNavigableItems document - let processed = - seq { + let matches = + [| for item in items do - let contains = kinds.Contains(navigateToItemKindToRoslynKind item.Kind) - let patternMatch = tryMatch item + if kinds.Contains(navigateToItemKindToRoslynKind item.Kind) then + match tryMatch item with + | ValueSome m -> yield struct (item, m) + | ValueNone -> () + |] - match contains, patternMatch with - | true, ValueSome m -> - let sourceSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) + // The text, read from disk for a closed document, is only needed to place the matches. + if matches.Length = 0 then + return ImmutableArray.Empty + else + let! ct = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync ct - match sourceSpan with + return + [| + for struct (item, m) in matches do + match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) with | ValueNone -> () | ValueSome sourceSpan -> - let glyph = navigateToItemKindToGlyph item.Kind - let kind = navigateToItemKindToRoslynKind item.Kind - let additionalInfo = formatInfo item.Container document - yield FSharpNavigateToSearchResult( - additionalInfo, - kind, + formatInfo item.Container document, + navigateToItemKindToRoslynKind item.Kind, patternMatchKindToNavigateToMatchKind m.Kind, item.Name, FSharpNavigableItem( - glyph, + navigateToItemKindToGlyph item.Kind, ImmutableArray.Create(TaggedText(TextTags.Text, item.Name)), document, sourceSpan ) ) - | _ -> () - } - - return processed |> Seq.toImmutableArray + |] + |> ImmutableArray.CreateRange } interface IFSharpNavigateToSearchService with @@ -209,8 +256,10 @@ type internal FSharpNavigateToSearchService [] (itemsCache let! results = project.Documents + |> Seq.filter (searchedIn project) |> Seq.map (processDocument tryMatch kinds) - |> CancellableTask.whenAll + // Throttle to avoid launching a parse per document in the project all at once. + |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) return results |> Seq.collect _.AsEnumerable() |> Seq.toImmutableArray } From e9f1e4ea967a4e9e1f7f2fa9789fe8cf5c8c3257 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 20:15:04 +0200 Subject: [PATCH 118/133] Test NavigateTo across the target-framework instances of a project Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Editor.Tests.fsproj | 1 + .../MultiTargetNavigateToSearchTests.fs | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/MultiTargetNavigateToSearchTests.fs diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index c658ba193f4..d84515dacf6 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -37,6 +37,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/MultiTargetNavigateToSearchTests.fs b/vsintegration/tests/FSharp.Editor.Tests/MultiTargetNavigateToSearchTests.fs new file mode 100644 index 00000000000..c1febedfd5e --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/MultiTargetNavigateToSearchTests.fs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// One project loaded as two target-framework instances: `plain` compiles without the fourth file +/// and without FOO, `foo` compiles everything with FOO defined. +module FSharp.Editor.Tests.MultiTargetNavigateToSearchTests + +open System.Collections.Immutable +open System.Threading +open Xunit +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo +open FSharp.Editor.Tests.Helpers +open FSharp.Test.ProjectGeneration + +let private project = + SyntheticProject.Create( + { sourceFile "First" [] with + ExtraSource = "let sharedFunc funcParam = funcParam * 2\n" + }, + { sourceFile "Second" [ "First" ] with + ExtraSource = "let plainUse x = ModuleFirst.sharedFunc x" + }, + { sourceFile "Third" [ "First" ] with + ExtraSource = "#if FOO\nlet fooUse x = ModuleFirst.sharedFunc x\n#endif" + }, + { sourceFile "Fourth" [ "First" ] with + ExtraSource = "let fooOnlyFileUse x = ModuleFirst.sharedFunc x" + } + ) + +let private solution, instances = + RoslynTestHelpers.CreateMultiTargetSolution( + project, + [ + { + Defines = [] + ExcludedFileIds = [ "Fourth" ] + } + { + Defines = [ "FOO" ] + ExcludedFileIds = [] + } + ] + ) + +let private service: IFSharpNavigateToSearchService = + MefHelpers.createExportProvider().GetExportedValue() + +/// Every instance searched in solution order, as the NavigateTo searcher does. +let private search pattern = + [ + for id in instances do + yield! + service + .SearchProjectAsync( + solution.GetProject id, + ImmutableArray.Empty, + pattern, + service.KindsProvided, + CancellationToken.None + ) + .Result + ] + +[] +[] +[] +[] +let ``a declaration is reported once across the instances of a project`` (name: string) = + let names = search name |> List.map _.Name |> List.filter ((=) name) + Assert.Equal([ name ], names) From 0cd5a0f7f331d564fbc27f3cce76ec6667a4dbfb Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 21:55:52 +0200 Subject: [PATCH 119/133] Search the C# and VB consumers alongside the F# projects The cross-language search ran after the F# one and visited the consumers one by one, so on a solution where the F# search takes minutes the C# call sites were the last thing to appear. The search now starts before the F# one and runs a few consumers at a time, each search building a compilation; the results are still reported after the F# uses, each file span once, so the order in the window is unchanged. Co-Authored-By: Claude Fable 5.1 --- .../Navigation/FindUsagesService.fs | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index d9675dca489..fe8115acdf2 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -113,27 +113,37 @@ module FSharpFindUsagesService = return referencedSymbols |> Seq.collect _.Locations } - /// Reports the uses in C# and VB projects that reference the assembly of a project declaring the symbol. - let private findCrossLanguageReferences - (docId: string) - (definitionItems: (FSharpDefinitionItem * Project)[]) + // Every search may build a compilation, and those cost memory, not just a core. + [] + let private ConcurrentCompilations = 4 + + /// The uses in the C# and VB projects that reference the assembly of a project declaring the symbol, + /// each with the definition item to report them under. + let private findCrossLanguageReferences (docId: string) (definitionItems: (FSharpDefinitionItem * Project)[]) = + seq { + for definitionItem, declaringProject in definitionItems do + for project in referencingCompilationProjects declaringProject -> definitionItem, project + } + |> Seq.distinctBy (fun (_, project) -> project.Id) + |> Seq.map (fun (definitionItem, project) -> + findRoslynReferences docId project + |> CancellableTask.map (Seq.map (fun location -> definitionItem, location))) + |> CancellableTask.whenAllThrottled ConcurrentCompilations + |> CancellableTask.map Seq.concat + + /// Reports each file span once: the target-framework instances of a consumer share their files. + let private reportCrossLanguageReferences + (found: (FSharpDefinitionItem * ReferenceLocation) seq) (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task) = cancellableTask { let reported = HashSet() - for definitionItem, declaringProject in definitionItems do - for project in referencingCompilationProjects declaringProject do - let! locations = findRoslynReferences docId project - - for location in locations do - let span = location.Location.SourceSpan + for definitionItem, location in found do + let span = location.Location.SourceSpan - if reported.Add(struct (location.Document.FilePath, span)) then - do! - onReferenceFoundAsync ( - FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(location.Document, span)) - ) + if reported.Add(struct (location.Document.FilePath, span)) then + do! onReferenceFoundAsync (FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(location.Document, span))) } let findReferencedSymbolsAsync @@ -211,12 +221,16 @@ module FSharpFindUsagesService = symbol.Ident.idText context.OnReferenceFoundAsync - do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound - - if allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject then + // Searched alongside the F# projects, reported after them. + let crossLanguageSearch = match symbolUse.Symbol.DocumentationCommentId with - | ValueSome docId -> do! findCrossLanguageReferences docId definitionItems context.OnReferenceFoundAsync - | ValueNone -> () + | ValueSome docId when allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject -> + findCrossLanguageReferences docId definitionItems cancellationToken + | _ -> Task.FromResult Seq.empty + + do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound + let! found = crossLanguageSearch + do! reportCrossLanguageReferences found context.OnReferenceFoundAsync } open FSharpFindUsagesService From 20d1c54b62b930f6f0f091f8f2aae185cf8dd6ba Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 22:26:29 +0200 Subject: [PATCH 120/133] Materialize the project list of a multi-project solution once Co-Authored-By: Claude Fable 5.1 --- .../tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs index fefd998e6bb..38a69096929 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs @@ -477,8 +477,9 @@ type RoslynTestHelpers private () = let projects = syntheticProject.GetAllProjects() - |> List.distinctBy _.Name - |> List.map (fun project -> project, ProjectId.CreateNewId()) + |> Seq.distinctBy _.Name + |> Seq.map (fun project -> project, ProjectId.CreateNewId()) + |> Seq.toList let projectIds = dict [ for project, id in projects -> project.Name, id ] From e7dae625a0ce0c67cb1ee871457d025c8985c414 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 02:16:14 +0200 Subject: [PATCH 121/133] Take the main thread before registering the Copilot context provider GetProxyAsync is an exported brokered service, so calling it from a background package-load task constructs Copilot's MEF part graph on that thread. Its constructor does a blocking JoinableTask wait for the main thread; meanwhile the Git provider asks for the same proxy from the main thread while building its own services at solution open, and blocks inside MEF's PartLifecycleTracker waiting for the part the background thread owns. Neither side can proceed and Visual Studio hangs permanently. Move the registration out of the background package-load task and into LoadComponentsInBackgroundAfterSolutionFullyLoadedAsync (run after the solution is fully loaded, the way Roslyn's AbstractPackage defers this kind of work), and switch to the main thread before asking for the proxy so the two requesters serialise instead of deadlocking. --- .../LanguageService/LanguageService.fs | 86 +++++++++++-------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 9843a78275a..dcc67de052f 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -419,43 +419,6 @@ type internal FSharpPackage() as this = override this.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks: PackageLoadTasks) = base.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks) - afterPackageLoadedTasks.AddTask( - false, - fun _ cancellationToken -> - task { - try - let! container = this.GetServiceAsync(typeof) - - match container with - | :? IBrokeredServiceContainer as container -> - // The Interactions service also serves the registration interface. It is absent when - // GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker. - let! registration = - container - .GetFullAccessServiceBroker() - .GetProxyAsync(CopilotDescriptors.InteractionService, cancellationToken) - - use registration = registration - - match registration with - | null -> () - | registration -> - let moniker = - ServiceMoniker( - FSharpConstants.copilotSymbolProviderName, - Version CopilotDescriptors.CurrentContextProviderVersion - ) - - do! registration.RegisterContextProviderAsync(moniker, cancellationToken) - | _ -> () - // Package load runs its tasks back to back on one loop, so a Copilot failure - a contract - // version the installed build does not serve, say - must not take the F# package down with it. - with ex when not (ex :? OperationCanceledException) -> - DebugHelpers.FSharpOutputPane.logExceptionWithContext (ex, "Registering the Copilot context provider") - } - :> Task - ) - #if DEBUG afterPackageLoadedTasks.AddTask( false, @@ -468,6 +431,55 @@ type internal FSharpPackage() as this = ) #endif + /// Copilot's registration service is an exported brokered service whose MEF part constructor blocks waiting + /// for the main thread. Asking for the proxy from a background thread therefore deadlocks against anyone + /// asking for it from the main thread - the Git provider does, while creating its services at solution open - + /// so take the main thread dependency deliberately, the way Roslyn does for a proxy that has one. + member private this.RegisterCopilotContextProviderAsync(cancellationToken: CancellationToken) : Task = + task { + try + do! this.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield = true, cancellationToken = cancellationToken) + + let! container = this.GetServiceAsync(typeof) + + match container with + | :? IBrokeredServiceContainer as container -> + // The Interactions service also serves the registration interface. It is absent when + // GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker. + let! registration = + container + .GetFullAccessServiceBroker() + .GetProxyAsync(CopilotDescriptors.InteractionService, cancellationToken) + + use registration = registration + + match registration with + | null -> () + | registration -> + let moniker = + ServiceMoniker( + FSharpConstants.copilotSymbolProviderName, + Version CopilotDescriptors.CurrentContextProviderVersion + ) + + do! registration.RegisterContextProviderAsync(moniker, cancellationToken) + | _ -> () + // A Copilot failure - a contract version the installed build does not serve, say - must not take the + // rest of the post-load work down with it. + with ex when not (ex :? OperationCanceledException) -> + DebugHelpers.FSharpOutputPane.logExceptionWithContext (ex, "Registering the Copilot context provider") + } + + override this.LoadComponentsInBackgroundAfterSolutionFullyLoadedAsync(cancellationToken) : Task = + // 'base' cannot be captured by the state machine, so start the base work before entering it. + let baseComponents = + base.LoadComponentsInBackgroundAfterSolutionFullyLoadedAsync(cancellationToken) + + task { + do! baseComponents + do! this.RegisterCopilotContextProviderAsync(cancellationToken) + } + override _.RoslynLanguageName = FSharpConstants.FSharpLanguageName (*override this.CreateWorkspace() = this.ComponentModel.GetService() *) override this.CreateLanguageService() = FSharpLanguageService(this) From 77ad89cdd037aa0080c98dc923de9f4e5ba8133d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 02:18:45 +0200 Subject: [PATCH 122/133] Trace Copilot context-provider registration through the output pane Diagnostic aid: on a large solution the "#" mention picker stays empty and nothing in the Debug pane says why. Log each step of RegisterCopilotContextProviderAsync so a hang or an early return (no brokered service container, a null proxy) is visible without a debugger attached. --- .../FSharp.Editor/LanguageService/LanguageService.fs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index dcc67de052f..dabe74fafbd 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -438,14 +438,18 @@ type internal FSharpPackage() as this = member private this.RegisterCopilotContextProviderAsync(cancellationToken: CancellationToken) : Task = task { try + DebugHelpers.FSharpOutputPane.logInfo "Copilot: registering context provider (switching to main thread)…" do! this.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield = true, cancellationToken = cancellationToken) + DebugHelpers.FSharpOutputPane.logInfo "Copilot: getting brokered service container…" let! container = this.GetServiceAsync(typeof) match container with | :? IBrokeredServiceContainer as container -> // The Interactions service also serves the registration interface. It is absent when // GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker. + DebugHelpers.FSharpOutputPane.logInfo "Copilot: getting registration service proxy…" + let! registration = container .GetFullAccessServiceBroker() @@ -454,8 +458,10 @@ type internal FSharpPackage() as this = use registration = registration match registration with - | null -> () + | null -> DebugHelpers.FSharpOutputPane.logInfo "Copilot: service proxy is null (Copilot not installed)" | registration -> + DebugHelpers.FSharpOutputPane.logInfo "Copilot: registering F# context provider…" + let moniker = ServiceMoniker( FSharpConstants.copilotSymbolProviderName, @@ -463,7 +469,8 @@ type internal FSharpPackage() as this = ) do! registration.RegisterContextProviderAsync(moniker, cancellationToken) - | _ -> () + DebugHelpers.FSharpOutputPane.logInfo "Copilot: registration complete" + | _ -> DebugHelpers.FSharpOutputPane.logInfo "Copilot: container is not IBrokeredServiceContainer" // A Copilot failure - a contract version the installed build does not serve, say - must not take the // rest of the post-load work down with it. with ex when not (ex :? OperationCanceledException) -> From 21e587f3d2484b6efd47fe8e50a80bca76e25991 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 20:04:18 +0200 Subject: [PATCH 123/133] Wait for Go To Definition through the threaded-wait dialog IFSharpGoToDefinitionService.TryGoToDefinition is a synchronous contract Roslyn calls on the UI thread, so the main thread has to wait for the checker. It did so with a bare Task.Wait, which pumps nothing: the VS watchdog showed "Please wait for an editor command to finish" after two seconds and auto-cancelled, while the check itself, and the snapshot version walk under its lazies, kept running on the pool. Pressing F12 again queued another waiter behind the same lazies, and the dialog stealing focus pushed the main thread into a focus-lost handler that blocked on the JTF context lock, so tagger work was cancelled and semantic classification never arrived. Wait the way NavigateTo in the same file already does, through JoinableTaskFactory.Run with the threaded-wait dialog, which keeps the main thread pumping and gives the user a Cancel button. The Roslyn token and the dialog token are linked so either cancels the check. The two TaskCompletionSource bridges in CancellableTasks and RoslynHelpers were created with TaskCreationOptions.None, so TrySetResult ran every awaiting continuation inline on whichever thread finished the F# async - the heavy post-check work of a navigation landed on the pool thread that completed the check. RunContinuationsAsynchronously moves those continuations to the pool instead. Co-Authored-By: Claude Fable 5.1 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../FSharp.Editor/Common/CancellableTasks.fs | 4 +- .../src/FSharp.Editor/Common/RoslynHelpers.fs | 4 +- .../Navigation/GoToDefinition.fs | 51 ++++++++++++------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index c901f7014a3..2079af6eaf0 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -9,6 +9,7 @@ * Reuse the in-memory PE reference of a referenced C# project while its dependent semantic version is unchanged, so dependent F# projects no longer re-import every reference and re-check every file each time Roslyn recreates the `Compilation`. ([PR #20460](https://github.com/dotnet/fsharp/pull/20460)) * Fixed `MissingManifestResourceException` when opening F# pages under Tools > Options by giving `FSharpPackage` its own `VSPackage` resource set. ([PR #20408](https://github.com/dotnet/fsharp/pull/20408)) +* Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 2bef8709f44..ccf16e88712 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -777,7 +777,9 @@ module CancellableTasks = } // try not to yield if on bg thread already - let tcs = new TaskCompletionSource<_>(TaskCreationOptions.None) + let tcs = + new TaskCompletionSource<_>(TaskCreationOptions.RunContinuationsAsynchronously) + let barrier = VolatileBarrier() let reg = diff --git a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs index 2679740fbb3..d7439b18b57 100644 --- a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs @@ -135,7 +135,9 @@ module internal RoslynHelpers = return! computation } - let tcs = new TaskCompletionSource<_>(TaskCreationOptions.None) + let tcs = + new TaskCompletionSource<_>(TaskCreationOptions.RunContinuationsAsynchronously) + let barrier = VolatileBarrier() let reg = diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 3fe7514128e..1db3adff6da 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -759,31 +759,44 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, } member _.TryGoToDefinition(position, cancellationToken) = - // Once we migrate to Roslyn-exposed MAAS and sourcelink (https://github.com/dotnet/fsharp/issues/13951), this can be a "normal" task - // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. - // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. + // Once we migrate to Roslyn-exposed MAAS and sourcelink (https://github.com/dotnet/fsharp/issues/13951), this can be a "normal" task. + // The IFSharpGoToDefinitionService contract is synchronous, so the main thread has to wait here: the threaded-wait dialog + // keeps it pumping and cancellable, where a bare Task.Wait froze it until the VS watchdog auto-cancelled. try use _ = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.GoToDefinition, [||]) let gtd = GoToDefinition(metadataAsSource) - let gtdTask = gtd.FindDefinitionAsync (initialDoc, position) cancellationToken + let navigated = ref false - gtdTask.Wait() - - if gtdTask.Status = TaskStatus.RanToCompletion && gtdTask.Result.IsSome then - match gtdTask.Result with - | ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), _) -> - gtd.NavigateToItem(navItem, cancellationToken) |> ignore - true - | ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _) -> - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken) - |> ignore + ThreadHelper.JoinableTaskFactory.Run( + SR.NavigatingTo(), + (fun _progress dialogCancellationToken -> + task { + use linked = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, dialogCancellationToken) + + return! + cancellableTask { + match! gtd.FindDefinitionAsync(initialDoc, position) with + | ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), _) -> + gtd.NavigateToItem(navItem, linked.Token) |> ignore + navigated.Value <- true + | ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _) -> + gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, linked.Token) + |> ignore + + navigated.Value <- true + | _ -> () + } + |> CancellableTask.start linked.Token + }), + TimeSpan.FromSeconds 1 + ) - true - | _ -> false - else - false - with exc -> + navigated.Value + with + | :? OperationCanceledException -> false + | exc -> TelemetryReporter.ReportFault(TelemetryEvents.GoToDefinition, FaultSeverity.General, exc) false From 84818eb9965f37584c7b1ae562c195845f926554 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 20:46:56 +0200 Subject: [PATCH 124/133] Do not nest cancellableTask inside task in TryGoToDefinition Under --optimize+ the outer task inlines the inner builder's Bind into its own resumable body, and the inner __resumableEntry then reaches IlxGen as a bare value: FS3401 on every Windows CI job, while Debug builds compiled the same code. Build the single cancellableTask the way NavigateTo does and hand it the linked CancellationTokenSource to dispose, so there is one builder and nothing to leak. Co-Authored-By: Claude Fable 5.1 --- .../Navigation/GoToDefinition.fs | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 1db3adff6da..7a05cdfe86e 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -772,25 +772,24 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, ThreadHelper.JoinableTaskFactory.Run( SR.NavigatingTo(), (fun _progress dialogCancellationToken -> - task { - use linked = - CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, dialogCancellationToken) - - return! - cancellableTask { - match! gtd.FindDefinitionAsync(initialDoc, position) with - | ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), _) -> - gtd.NavigateToItem(navItem, linked.Token) |> ignore - navigated.Value <- true - | ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _) -> - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, linked.Token) - |> ignore - - navigated.Value <- true - | _ -> () - } - |> CancellableTask.start linked.Token - }), + let linked = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, dialogCancellationToken) + + cancellableTask { + use _ = linked + + match! gtd.FindDefinitionAsync(initialDoc, position) with + | ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), _) -> + gtd.NavigateToItem(navItem, linked.Token) |> ignore + navigated.Value <- true + | ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _) -> + gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, linked.Token) + |> ignore + + navigated.Value <- true + | _ -> () + } + |> CancellableTask.start linked.Token), TimeSpan.FromSeconds 1 ) From 7dced5e061bb5aa0c28018db4f19de7fae5e702b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 20:05:38 +0200 Subject: [PATCH 125/133] Add the release note link for PR #20482 Co-Authored-By: Claude Fable 5.1 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 2079af6eaf0..1e8f5ed0c14 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -10,6 +10,7 @@ * Reuse the in-memory PE reference of a referenced C# project while its dependent semantic version is unchanged, so dependent F# projects no longer re-import every reference and re-check every file each time Roslyn recreates the `Compilation`. ([PR #20460](https://github.com/dotnet/fsharp/pull/20460)) * Fixed `MissingManifestResourceException` when opening F# pages under Tools > Options by giving `FSharpPackage` its own `VSPackage` resource set. ([PR #20408](https://github.com/dotnet/fsharp/pull/20408)) * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. +* Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) From 505b9cf9497976c3e885b0f4132fa91281086c6f Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 03:05:49 +0200 Subject: [PATCH 126/133] Answer the Copilot mention picker from what is already parsed The picker showed no F# declarations on large solutions. Every query walked every document of every F# project, parsing the ones nobody had opened, and `whenAllThrottled` queued a task per document on one semaphore, so a thousand documents meant a thousand tasks waiting to run while Copilot cancelled the query and took nothing. A query now visits the documents in three groups, stopping as soon as it holds as many declarations as it reports: the documents the user has open, the ones already in the parse cache, and only then the ones that would have to be parsed, which get a time budget of their own. The new cache lookup reads no text, so a closed document costs nothing. `forEachThrottled` pulls documents through a fixed set of workers instead of starting a task per document, and a batch of search texts visits each document once for all of them. Co-Authored-By: Claude Opus 5 --- .../FSharp.Editor/Common/CancellableTasks.fs | 25 ++ .../Copilot/CopilotContextProvider.fs | 219 +++++++++++++----- .../Navigation/NavigateToSearchService.fs | 7 + .../CopilotContextProviderTests.fs | 119 +++++++++- 4 files changed, 302 insertions(+), 68 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index ccf16e88712..43d823e641a 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -1132,6 +1132,31 @@ module CancellableTasks = return! allTask } + /// Runs the work over the items with at most maxDegreeOfParallelism of them in flight. A worker + /// takes the next item when it frees up, so cancellation cancels the workers rather than a + /// pending task per item. + let forEachThrottled maxDegreeOfParallelism (work: 'T -> CancellableTask) (items: 'T seq) = + cancellableTask { + let! ct = getCancellationToken () + let items = Seq.toArray items + let mutable next = -1 + + let worker () = + backgroundTask { + let mutable index = Interlocked.Increment &next + + while index < items.Length do + ct.ThrowIfCancellationRequested() + do! work items[index] ct + index <- Interlocked.Increment &next + } + + let workers = + Array.init (min (max 1 maxDegreeOfParallelism) items.Length) (fun _ -> worker ()) + + do! (Task.WhenAll workers :> Task) + } + let inline whenAllTasks (tasks: CancellableTask seq) = cancellableTask { let! ct = getCancellationToken () diff --git a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs index 73c1e3b166b..fdddf05793e 100644 --- a/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs +++ b/vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs @@ -6,6 +6,8 @@ open System open System.Collections.Generic open System.ComponentModel.Composition open System.IO +open System.Diagnostics +open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis @@ -34,11 +36,36 @@ module internal CopilotSymbolQuery = [] let private UserOpName = "CopilotSymbolContext" + /// How long a query keeps parsing files nobody has opened yet. Copilot cancels on its own schedule + /// and takes no partial results, so an answer from what is already parsed beats a complete answer. + [] + let private ColdSearchBudgetMs = 1500 + + let private parallelism = max 1 (Environment.ProcessorCount - 1) + let private fsharpDocuments (solution: Solution) = solution.Projects |> Seq.where (fun project -> project.Language = FSharpConstants.FSharpLanguageName) |> Seq.collect _.Documents + /// The documents in the order a query visits them: the ones the user has open, the ones already + /// parsed into the cache, and the ones that would have to be parsed to answer. + let private tiers (cache: FSharpNavigableItemsCache) (openDocumentIds: DocumentId seq) (solution: Solution) = + let openIds = HashSet openDocumentIds + let opened = ResizeArray() + let cached = ResizeArray() + let cold = ResizeArray() + + for document in fsharpDocuments solution do + if openIds.Contains document.Id then + opened.Add document + else + match cache.TryGetCachedNavigableItems document.Id with + | ValueSome items -> cached.Add(struct (document, items)) + | ValueNone -> cold.Add document + + struct (opened, cached, cold) + let describe (item: NavigableItem) (document: Document) = let container = match item.Container.FullName with @@ -50,67 +77,117 @@ module internal CopilotSymbolQuery = else $"{container} - {document.Project.Name}" - /// Declarations whose fully qualified name matches `searchText`, best match first, one entry per name. - let search (cache: FSharpNavigableItemsCache) (solution: Solution) (searchText: string) = + /// Declarations whose fully qualified name matches each search text, best match first, one entry + /// per name. Every document is visited once for all of the texts. + let search (cache: FSharpNavigableItemsCache) (openDocumentIds: DocumentId seq) (solution: Solution) (searchTexts: string[]) = cancellableTask { let! ct = CancellableTask.getCancellationToken () - let tryMatch = cache.CreateMatcherFor searchText + let matchers = searchTexts |> Array.map cache.CreateMatcherFor + let hits = Array.init searchTexts.Length (fun _ -> ResizeArray()) + let found = Array.init searchTexts.Length (fun _ -> HashSet StringComparer.Ordinal) + + let collect (document: Document) (items: NavigableItem array) = + lock hits (fun () -> + for index in 0 .. matchers.Length - 1 do + let tryMatch = matchers[index] + + for item in items do + match tryMatch item with + | ValueSome patternMatch -> + hits[index].Add(struct (patternMatch.Kind, item, document)) + + found[index].Add(CopilotSymbolMapping.fullyQualifiedName item) |> ignore + | ValueNone -> ()) - let matchesIn (document: Document) = + let enough () = + lock hits (fun () -> found |> Array.forall (fun names -> names.Count >= MaxMentions)) + + let parseAndCollect (document: Document) = cancellableTask { - ct.ThrowIfCancellationRequested() let! items = cache.GetNavigableItems document - - return - items - |> Seq.chooseV (fun item -> - tryMatch item - |> ValueOption.map (fun patternMatch -> struct (patternMatch.Kind, item, document))) + collect document items } - let! hits = - fsharpDocuments solution - |> Seq.map matchesIn - // Throttle to avoid launching a parse per document in the solution all at once. - |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) + let struct (opened, cached, cold) = tiers cache openDocumentIds solution + + do! opened |> CancellableTask.forEachThrottled parallelism parseAndCollect + + if not (enough ()) then + for struct (document, items) in cached do + ct.ThrowIfCancellationRequested() + collect document items + + if not (enough ()) then + let budget = Stopwatch.StartNew() + + // The budget stops handing out documents rather than cancelling a parse under way: + // the first query of a session has to survive its first parse to answer at all. + let scan (document: Document) = + cancellableTask { + if budget.ElapsedMilliseconds < ColdSearchBudgetMs && not (enough ()) then + do! parseAndCollect document + } + + do! cold |> CancellableTask.forEachThrottled parallelism scan return hits - |> Seq.collect id - |> Seq.sortBy (fun (struct (kind, item: NavigableItem, document: Document)) -> - document.IsFSharpSignatureFile, kind, item.Name.Length) - |> Seq.distinctBy (fun (struct (_, item, _)) -> CopilotSymbolMapping.fullyQualifiedName item) - |> Seq.truncate MaxMentions - |> Seq.map (fun (struct (_, item, document)) -> struct (item, document)) - |> Seq.toArray + |> Array.map ( + Seq.sortBy (fun (struct (kind, item: NavigableItem, document: Document)) -> + document.IsFSharpSignatureFile, kind, item.Name.Length) + >> Seq.distinctBy (fun (struct (_, item, _)) -> CopilotSymbolMapping.fullyQualifiedName item) + >> Seq.truncate MaxMentions + >> Seq.map (fun (struct (_, item, document)) -> struct (item, document)) + >> Seq.toArray + ) } /// Declarations carrying exactly this fully qualified name. Signature files answer only when no - /// implementation declares the name. - let declarationsOf (cache: FSharpNavigableItemsCache) (solution: Solution) (fullyQualifiedName: string) = + /// implementation declares the name, so the search stops once an implementation has answered. + let declarationsOf + (cache: FSharpNavigableItemsCache) + (openDocumentIds: DocumentId seq) + (solution: Solution) + (fullyQualifiedName: string) + = cancellableTask { let! ct = CancellableTask.getCancellationToken () + let hits = ResizeArray() + + let collect (document: Document) (items: NavigableItem array) = + lock hits (fun () -> + for item in items do + if CopilotSymbolMapping.hasFullyQualifiedName fullyQualifiedName item then + hits.Add(struct (item, document))) - let matchesIn (document: Document) = + let declaredInImplementation () = + lock hits (fun () -> + hits + |> Seq.exists (fun (struct (_, document: Document)) -> not document.IsFSharpSignatureFile)) + + let parseAndCollect (document: Document) = cancellableTask { - ct.ThrowIfCancellationRequested() let! items = cache.GetNavigableItems document - - return - items - |> Seq.chooseV (fun item -> - if CopilotSymbolMapping.hasFullyQualifiedName fullyQualifiedName item then - ValueSome struct (item, document) - else - ValueNone) + collect document items } - let! hits = - fsharpDocuments solution - |> Seq.map matchesIn - // Throttle to avoid launching a parse per document in the solution all at once. - |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) - |> CancellableTask.map (Seq.collect id) + let struct (opened, cached, cold) = tiers cache openDocumentIds solution + + do! opened |> CancellableTask.forEachThrottled parallelism parseAndCollect + + if not (declaredInImplementation ()) then + for struct (document, items) in cached do + ct.ThrowIfCancellationRequested() + collect document items + + if not (declaredInImplementation ()) then + let scan (document: Document) = + cancellableTask { + if not (declaredInImplementation ()) then + do! parseAndCollect document + } + + do! cold |> CancellableTask.forEachThrottled parallelism scan let implementations = hits @@ -148,9 +225,14 @@ module internal CopilotSymbolQuery = return struct (sourceText.GetSubText(span).ToString(), span) } - let symbolContext (cache: FSharpNavigableItemsCache) (solution: Solution) (fullyQualifiedName: string) = + let symbolContext + (cache: FSharpNavigableItemsCache) + (openDocumentIds: DocumentId seq) + (solution: Solution) + (fullyQualifiedName: string) + = cancellableTask { - let! declarations = declarationsOf cache solution fullyQualifiedName + let! declarations = declarationsOf cache openDocumentIds solution fullyQualifiedName match Array.tryHeadV declarations with | ValueNone -> return ValueNone @@ -242,17 +324,31 @@ type internal FSharpCopilotContextProvider | text -> ValueSome text | _ -> ValueNone - let mentionsFor (searchText: string voption) = + /// One pass over the solution for the whole batch: Copilot's picker asks for several texts at once + /// and each of them would otherwise walk the same documents. + let mentionsFor (searchTexts: string voption[]) = cancellableTask { - match workspace, searchText with - | null, _ - | _, ValueNone -> return noMentions - | workspace, ValueSome searchText -> - let! hits = CopilotSymbolQuery.search cache workspace.CurrentSolution searchText + let distinct = searchTexts |> Seq.chooseV id |> Seq.distinct |> Seq.toArray + + match workspace with + | null -> return searchTexts |> Array.map (fun _ -> noMentions) + | _ when Array.isEmpty distinct -> return searchTexts |> Array.map (fun _ -> noMentions) + | workspace -> + let! hits = CopilotSymbolQuery.search cache (workspace.GetOpenDocumentIds()) workspace.CurrentSolution distinct + + let byText = Dictionary(StringComparer.Ordinal) + + for index in 0 .. distinct.Length - 1 do + byText[distinct[index]] <- + hits[index] + |> Array.map (fun (struct (item, document)) -> mentionFor item document) + :> IReadOnlyCollection return - hits |> Array.map (fun (struct (item, document)) -> mentionFor item document) - :> IReadOnlyCollection + searchTexts + |> Array.map (function + | ValueSome text -> byText[text] + | ValueNone -> noMentions) } let fullyQualifiedNameOf (inputs: IReadOnlyDictionary | null) = @@ -292,7 +388,8 @@ type internal FSharpCopilotContextProvider String.Equals(memberName, CopilotSymbolMapping.SymbolMember, StringComparison.Ordinal) -> cancellableTask { - let! symbol = CopilotSymbolQuery.symbolContext cache workspace.CurrentSolution fullyQualifiedName + let! symbol = + CopilotSymbolQuery.symbolContext cache (workspace.GetOpenDocumentIds()) workspace.CurrentSolution fullyQualifiedName match symbol with | ValueNone -> return null @@ -303,7 +400,9 @@ type internal FSharpCopilotContextProvider interface ICopilotMentionQueryable with member _.QueryMentionAsync(query, cancellationToken) : Task> = - mentionsFor (searchTextOf query) |> CancellableTask.start cancellationToken + mentionsFor [| searchTextOf query |] + |> CancellableTask.map Array.head + |> CancellableTask.start cancellationToken member _.NavigateToMentionableAsync(mention, cancellationToken) : Task = match workspace, fullyQualifiedNameOf mention.Inputs with @@ -313,7 +412,8 @@ type internal FSharpCopilotContextProvider cancellableTask { let! ct = CancellableTask.getCancellationToken () let solution = workspace.CurrentSolution - let! declarations = CopilotSymbolQuery.declarationsOf cache solution fullyQualifiedName + + let! declarations = CopilotSymbolQuery.declarationsOf cache (workspace.GetOpenDocumentIds()) solution fullyQualifiedName match Array.tryHeadV declarations with | ValueNone -> return false @@ -333,15 +433,8 @@ type internal FSharpCopilotContextProvider |> CancellableTask.start cancellationToken // Copilot's own picker providers answer through the batch interface, one result collection per query. - // Each distinct search text scans the solution once, and the scans run side by side. interface ICopilotMentionBatchQueryable with member _.QueryMentionBatchAsync(queries, cancellationToken) : Task>> = - cancellableTask { - let searchTexts = queries |> Seq.map searchTextOf |> Seq.toArray - let distinct = Array.distinct searchTexts - let! mentions = distinct |> Array.map mentionsFor |> CancellableTask.whenAll - let byText = Array.zip distinct mentions |> dict - - return searchTexts |> Array.map (fun text -> byText[text]) :> IReadOnlyList> - } + mentionsFor (queries |> Seq.map searchTextOf |> Seq.toArray) + |> CancellableTask.map (fun mentions -> mentions :> IReadOnlyList>) |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index f5f1620f3df..f9ff7e88382 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -68,6 +68,13 @@ type internal FSharpNavigableItemsCache return items } + /// The items of the document's last parse, whatever version they came from. Reads no text, so a + /// closed document costs nothing; a caller that needs the items of the current text asks for them. + member _.TryGetCachedNavigableItems(documentId: DocumentId) = + match cache.TryGetValue documentId with + | true, struct (_, items) -> ValueSome items + | _ -> ValueNone + /// Whether the file's parse tree, from whichever instance parsed it first, has `#if` directives. /// `true` when unknown, so a file no instance has parsed yet is searched everywhere. member _.HasConditionalDirectives(filePath: string) = diff --git a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs index c7ad810ab4a..9f98a83ceac 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CopilotContextProviderTests.fs @@ -2,10 +2,12 @@ namespace FSharp.Editor.Tests +open System open System.Threading open Xunit +open Microsoft.CodeAnalysis open Microsoft.VisualStudio.Copilot open Microsoft.VisualStudio.FSharp.Editor @@ -49,13 +51,21 @@ let twice x = x * 2 let private run computation = computation |> CancellableTask.start CancellationToken.None |> _.Result - let private search pattern = - CopilotSymbolQuery.search cache solution pattern - |> run + let private namesOf hits = + hits |> Array.map (fun (struct (item, _)) -> CopilotSymbolMapping.fullyQualifiedName item) + let private searchIn cache openDocumentIds solution pattern = + CopilotSymbolQuery.search cache openDocumentIds solution [| pattern |] + |> run + |> Array.head + |> namesOf + + let private search pattern = + searchIn cache Seq.empty solution pattern + let private symbolContext name = - CopilotSymbolQuery.symbolContext cache solution name |> run + CopilotSymbolQuery.symbolContext cache Seq.empty solution name |> run let private contextOf name = match symbolContext name with @@ -79,8 +89,9 @@ let twice x = x * 2 [] let ``a name matches only the declaration it spells out`` (candidate: string, expected: bool) = let item = - CopilotSymbolQuery.search cache solution "Counter" + CopilotSymbolQuery.search cache Seq.empty solution [| "Counter" |] |> run + |> Array.head |> Array.pick (fun (struct (item, _)) -> if CopilotSymbolMapping.fullyQualifiedName item = "Widgets.Counter" then Some item @@ -98,6 +109,104 @@ let twice x = x * 2 let ``an unknown name has no context`` () = Assert.True((symbolContext "Widgets.NoSuchThing").IsNone) + /// One matcher and one parse cache per test, so what a test leaves parsed cannot answer the next one. + let private freshCache () = + MefHelpers.createExportProvider().GetExportedValue() + + let private solutionOf files = + let projectId = ProjectId.CreateNewId() + + let documents = + files + |> List.map (fun (path, source) -> RoslynTestHelpers.CreateDocumentInfo projectId path source) + + let solution = + RoslynTestHelpers.CreateSolution [ RoslynTestHelpers.CreateProjectInfo projectId "C:\\many.fsproj" documents ] + + { RoslynTestHelpers.DefaultProjectOptions with + SourceFiles = files |> List.map fst |> Array.ofList + } + |> RoslynTestHelpers.SetProjectOptions projectId solution + + solution + + let private documentsOf (solution: Solution) = + solution.Projects |> Seq.exactlyOne |> _.Documents |> Seq.toArray + + let private documentNamed (name: string) solution = + documentsOf solution + |> Array.find _.FilePath.EndsWith(name, StringComparison.Ordinal) + + /// A file holding more declarations matching `name` than one query reports. + let private manyDeclarations name count = + let members = + [ for i in 1..count -> $" member _.{name}{i} = {i}" ] |> String.concat "\n" + + $"module {name}Module\n\ntype {name}Holder() =\n{members}\n" + + let private coldFile = "C:\\cold.fs", "module Cold\n\nlet widgetCounter = 1\n" + + [] + let ``an open document answers without parsing the rest of the solution`` () = + let cache = freshCache () + let solution = solutionOf [ "C:\\open.fs", manyDeclarations "Widget" 25; coldFile ] + let opened = documentNamed "open.fs" solution + + let names = searchIn cache [ opened.Id ] solution "Widget" + + Assert.Equal(20, names.Length) + Assert.All(names, fun name -> Assert.StartsWith("WidgetModule", name, StringComparison.Ordinal)) + Assert.True((cache.TryGetCachedNavigableItems (documentNamed "cold.fs" solution).Id).IsNone) + + [] + let ``documents already parsed answer without parsing the rest`` () = + let cache = freshCache () + let solution = solutionOf [ "C:\\warm.fs", manyDeclarations "Widget" 25; coldFile ] + cache.GetNavigableItems(documentNamed "warm.fs" solution) |> run |> ignore + + let names = searchIn cache Seq.empty solution "Widget" + + Assert.Equal(20, names.Length) + Assert.True((cache.TryGetCachedNavigableItems (documentNamed "cold.fs" solution).Id).IsNone) + + [] + let ``the search stops once it has enough declarations`` () = + let cache = freshCache () + + let solution = + solutionOf + [ + for i in 1..150 -> $"C:\\cold{i}.fs", $"module Cold{i}\n\ntype Counter{i}() =\n member _.Value = {i}\n" + ] + + let names = searchIn cache Seq.empty solution "Counter" + + Assert.Equal(20, names.Length) + + Assert.NotEmpty( + documentsOf solution + |> Array.filter (fun document -> (cache.TryGetCachedNavigableItems document.Id).IsNone) + ) + + [] + let ``a batch of texts answers like the same texts one by one`` () = + let cache = freshCache () + + let solution = + solutionOf + [ + "C:\\a.fs", "module A\n\nlet alpha = 1\n" + "C:\\b.fs", "module B\n\nlet beta = 2\n" + ] + + let batched = + CopilotSymbolQuery.search cache Seq.empty solution [| "alpha"; "beta" |] + |> run + |> Array.map namesOf + + Assert.Equal(searchIn cache Seq.empty solution "alpha", batched[0]) + Assert.Equal(searchIn cache Seq.empty solution "beta", batched[1]) + [] let ``a type context carries the whole declaration and its doc comment`` () = let context = contextOf "Widgets.Counter" From 24b1e788af61eaa4d7197f26515ef3e3c344f372 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 03:28:13 +0200 Subject: [PATCH 127/133] Report the references of a document together and share the cores Three things Find All References did on every search: it searched the whole solution for Find Implementations, which reports no uses at all and threw the result away; it started a task per reference found, each fetching the document's text again; and it swallowed the cancellation that Roslyn raises when the user closes the window or starts another search, so the search ran on. The callback now takes the uses of a document at once, so its text is read once and the reports go out in order, and cancellation propagates. The documents of a project go through a fixed set of workers instead of a task per document parked on the throttle, and that throttle is now one budget for the whole editor - the C# and Visual Basic searches take slots from it too - one core smaller than the machine, so the thread drawing the results keeps one. Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 + .../LanguageService/SymbolHelpers.fs | 31 +++-- .../LanguageService/WorkspaceExtensions.fs | 25 ++-- .../Navigation/FindUsagesService.fs | 131 ++++++++++-------- 4 files changed, 104 insertions(+), 85 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 1e8f5ed0c14..a02aefc833f 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -34,6 +34,8 @@ * Find All References and Rename search each file of a multi-targeted F# project once instead of once per target framework, revisiting a file in another instance only when it is compiled there alone or under conditional compilation; searches start as each project's snapshot is ready and type checks are bounded across the solution. ([PR #20464](https://github.com/dotnet/fsharp/pull/20464)) * Go To Definition from C# or Visual Basic into an F# project no longer type checks the whole project: the file declaring the symbol is found from parsed declarations and checked alone, with the whole-project check as a fallback. ([PR #20465](https://github.com/dotnet/fsharp/pull/20465)) * Go To Definition into an F# project built with a path map (`DeterministicSourcePaths` or `PathMap`) opens its source instead of a generated signature: the IDE no longer applies `--pathmap` to the project options it checks with. ([PR #20470](https://github.com/dotnet/fsharp/pull/20470)) +* Find Implementations no longer searches the whole solution for uses it then discards; it reports the declarations alone. +* Find All References reports the uses of a document in one go instead of starting a task per reference, stops when the request is cancelled, and shares one budget of cores with the C# and Visual Basic searches running alongside it, leaving one core to the editor. ### Changed diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 1a87ea92eb4..787d86489f5 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -99,8 +99,12 @@ module internal SymbolHelpers = let ordered = List.sortBy rank instances struct (ordered.Head, ordered.Tail)) + /// One core is left to the thread that has to stay responsive: every search shares this budget, + /// including the C# and Visual Basic searches that run alongside the F# ones. + let searchThrottle = new SemaphoreSlim(max 1 (Environment.ProcessorCount - 1)) + let getSymbolUsesInProjects - (symbol: FSharpSymbol, currentProject: Project, projects: Project list, onFound: Document -> range -> CancellableTask) + (symbol: FSharpSymbol, currentProject: Project, projects: Project list, onFound: Document -> range seq -> CancellableTask) = match projects |> List.filter _.IsFSharp with | [] -> CancellableTask.singleton () @@ -122,8 +126,6 @@ module internal SymbolHelpers = TelemetryReporter.ReportSingleEvent(TelemetryEvents.GetSymbolUsesInProjectsStarted, props) let! ct = CancellableTask.getCancellationToken () - // Not disposed: a search started before a later snapshot fails still has to release it. - let throttle = new SemaphoreSlim(max 1 Environment.ProcessorCount) // Mutated by the checker while a snapshot is built, so snapshots are built one at a time. let snapshotAccumulator = Dictionary() let searches = ResizeArray() @@ -138,7 +140,7 @@ module internal SymbolHelpers = let start (project: Project) snapshot searchedInstance = searches.Add( project.FindFSharpReferencesAsync - (symbol, snapshot, searchedInstance, throttle, onFound, "getSymbolUsesInProjects") + (symbol, snapshot, searchedInstance, searchThrottle, onFound, "getSymbolUsesInProjects") ct ) @@ -159,7 +161,7 @@ module internal SymbolHelpers = (symbolUse: FSharpSymbolUse) (currentDocument: Document) (checkFileResults: FSharpCheckFileResults) - (onFound: Document -> range -> CancellableTask) + (onFound: Document -> range seq -> CancellableTask) = cancellableTask { match symbolUse.GetSymbolScope currentDocument with @@ -168,10 +170,7 @@ module internal SymbolHelpers = let symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, relatedSymbolKinds = RelatedSymbolUseKind.All) - do! - symbolUses - |> Seq.map (fun symbolUse -> onFound currentDocument symbolUse.Range) - |> CancellableTask.whenAll + do! onFound currentDocument (symbolUses |> Seq.map _.Range) | Some SymbolScope.SignatureAndImplementation -> let otherFile = getOtherFile currentDocument.FilePath @@ -185,13 +184,11 @@ module internal SymbolHelpers = } | ValueNone -> CancellableTask.singleton [] - let symbolUses = - (checkFileResults, currentDocument) :: otherFileCheckResults - |> Seq.collect (fun (checkFileResults, doc) -> + for checkFileResults, doc in (checkFileResults, currentDocument) :: otherFileCheckResults do + let symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, relatedSymbolKinds = RelatedSymbolUseKind.All) - |> Seq.map (fun symbolUse -> (doc, symbolUse.Range))) - do! symbolUses |> Seq.map ((<||) onFound) |> CancellableTask.whenAll + do! onFound doc (symbolUses |> Seq.map _.Range) | Some(SymbolScope.Projects(scopeProjects, isLocalForProject)) -> let projectsToCheck = @@ -226,7 +223,11 @@ module internal SymbolHelpers = let symbolUses = ConcurrentBag() let onFound = - fun document range -> cancellableTask { symbolUses.Add(document, range) } + fun document (ranges: range seq) -> + cancellableTask { + for range in ranges do + symbolUses.Add(document, range) + } do! findSymbolUses symbolUse currentDocument checkFileResults onFound diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 5f0825eac41..7228bb80fb7 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -712,8 +712,12 @@ type Document with /// Find F# references in the given F# document, through the project snapshot when the transparent /// compiler is in use. member inline this.FindFSharpReferencesAsync - (symbol, projectSnapshot: FSharpProjectSnapshot voption, [] onFound, userOpName) - = + ( + symbol, + projectSnapshot: FSharpProjectSnapshot voption, + [] onFound: Text.range seq -> CancellableTask, + userOpName + ) = cancellableTask { let! checker, _, _, projectOptions = this.GetFSharpCompilationOptionsAsync(userOpName) @@ -729,11 +733,7 @@ type Document with fastCheck = this.Project.IsFastFindReferencesEnabled ) - do! - symbolUses - |> Seq.map onFound - |> CancellableTask.whenAll - |> CancellableTask.ignore + do! onFound symbolUses } /// Try to find a F# lexer/token symbol of the given F# document and position. @@ -763,6 +763,11 @@ let private hasConditionalDirectives (parseTree: ParsedInput) = | ParsedInput.ImplFile file -> not file.Trivia.ConditionalDirectives.IsEmpty | ParsedInput.SigFile file -> not file.Trivia.ConditionalDirectives.IsEmpty +/// How many documents of one project a search keeps in flight. The throttle it shares with the other +/// projects decides how many of those actually run. +[] +let private WorkersPerProject = 4 + type Project with /// Find F# references in the given project. When `searchedInstance` is another target-framework @@ -838,9 +843,9 @@ type Project with |> Seq.filter (fun document -> isFSharpSourceFile document.FilePath && not (canSkipDocuments.Contains document.FilePath)) - |> Seq.map search - |> CancellableTask.whenAll - |> CancellableTask.ignore + // Workers take the next document when they free up. Starting one task per document + // instead would leave every document of the solution parked on the throttle at once. + |> CancellableTask.forEachThrottled WorkersPerProject search } member this.GetFSharpCompilationOptionsAsync() = this |> getFSharpOptionsForProject diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index fe8115acdf2..19cd29835d9 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -2,6 +2,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System open System.Collections.Generic open System.Collections.Immutable open System.Composition @@ -20,8 +21,9 @@ open CancellableTasks module FSharpFindUsagesService = + /// Reports the uses found in one document: its text is read once for all of them, and Roslyn takes + /// them one at a time anyway. let onSymbolFound - allReferences declarationRange externalDefinitionItem definitionItems @@ -29,36 +31,38 @@ module FSharpFindUsagesService = symbolName (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task) (doc: Document) - (symbolUse: range) + (symbolUses: range seq) = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = doc.GetTextAsync(cancellationToken) - match declarationRange, RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with - | Some declRange, _ when Range.equals declRange symbolUse -> () - | _, ValueNone -> () - | _, ValueSome _ when not allReferences -> () - | _, ValueSome textSpan -> - match textSpan with - | Tokenizer.FixedSpan sourceText symbolName fixedSpan -> - let definitionItem = - if isExternal then - externalDefinitionItem - else - definitionItems - |> Array.tryFind (fun (_, project: Project) -> project.FilePath = doc.Project.FilePath) - |> Option.map (fun (definitionItem, _) -> definitionItem) - |> Option.defaultValue externalDefinitionItem - - let referenceItem = - FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(doc, fixedSpan)) - // REVIEW: OnReferenceFoundAsync is throwing inside Roslyn, putting a try/with so find-all refs doesn't fail. - try - do! onReferenceFoundAsync referenceItem - with _ -> - () - | _ -> () + let definitionItem = + if isExternal then + externalDefinitionItem + else + definitionItems + |> Array.tryFind (fun (_, project: Project) -> project.FilePath = doc.Project.FilePath) + |> Option.map (fun (definitionItem, _) -> definitionItem) + |> Option.defaultValue externalDefinitionItem + + for symbolUse in symbolUses do + cancellationToken.ThrowIfCancellationRequested() + + match declarationRange, RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with + | Some declRange, _ when Range.equals declRange symbolUse -> () + | _, ValueNone -> () + | _, ValueSome textSpan -> + match textSpan with + | Tokenizer.FixedSpan sourceText symbolName fixedSpan -> + let referenceItem = + FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(doc, fixedSpan)) + // REVIEW: OnReferenceFoundAsync is throwing inside Roslyn, putting a try/with so find-all refs doesn't fail. + try + do! onReferenceFoundAsync referenceItem + with error when not (error :? OperationCanceledException) -> + () + | _ -> () } // File can be included in more than one project, hence single `range` may results with multiple `Document`s. @@ -95,22 +99,27 @@ module FSharpFindUsagesService = let private findRoslynReferences (docId: string) (project: Project) = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () + // Building a compilation and walking it costs the same cores the F# search is using. + do! SymbolHelpers.searchThrottle.WaitAsync cancellationToken - match! project.GetCompilationAsync cancellationToken with - | null -> return Seq.empty - | compilation -> - match DocumentationCommentId.GetFirstSymbolForDeclarationId(docId, compilation) with + try + match! project.GetCompilationAsync cancellationToken with | null -> return Seq.empty - | symbol -> - let! referencedSymbols = - SymbolFinder.FindReferencesAsync( - symbol, - project.Solution, - ImmutableHashSet.CreateRange project.Documents, - cancellationToken - ) - - return referencedSymbols |> Seq.collect _.Locations + | compilation -> + match DocumentationCommentId.GetFirstSymbolForDeclarationId(docId, compilation) with + | null -> return Seq.empty + | symbol -> + let! referencedSymbols = + SymbolFinder.FindReferencesAsync( + symbol, + project.Solution, + ImmutableHashSet.CreateRange project.Documents, + cancellationToken + ) + + return referencedSymbols |> Seq.collect _.Locations + finally + SymbolHelpers.searchThrottle.Release() |> ignore } // Every search may build a compilation, and those cost memory, not just a core. @@ -211,26 +220,28 @@ module FSharpFindUsagesService = if isExternal then do! context.OnDefinitionFoundAsync(externalDefinitionItem) - let onFound = - onSymbolFound - allReferences - declarationRange - externalDefinitionItem - definitionItems - isExternal - symbol.Ident.idText - context.OnReferenceFoundAsync - - // Searched alongside the F# projects, reported after them. - let crossLanguageSearch = - match symbolUse.Symbol.DocumentationCommentId with - | ValueSome docId when allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject -> - findCrossLanguageReferences docId definitionItems cancellationToken - | _ -> Task.FromResult Seq.empty - - do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound - let! found = crossLanguageSearch - do! reportCrossLanguageReferences found context.OnReferenceFoundAsync + // Find Implementations wants the definitions alone: reporting a use is what + // `allReferences` gates, so searching for them would throw the whole search away. + if allReferences then + let onFound = + onSymbolFound + declarationRange + externalDefinitionItem + definitionItems + isExternal + symbol.Ident.idText + context.OnReferenceFoundAsync + + // Searched alongside the F# projects, reported after them. + let crossLanguageSearch = + match symbolUse.Symbol.DocumentationCommentId with + | ValueSome docId when not isExternal && not symbolUse.Symbol.IsInternalToProject -> + findCrossLanguageReferences docId definitionItems cancellationToken + | _ -> Task.FromResult Seq.empty + + do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound + let! found = crossLanguageSearch + do! reportCrossLanguageReferences found context.OnReferenceFoundAsync } open FSharpFindUsagesService From eb8b48cb1d8b0cded3259705a7d0fc1345a1c98f Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 03:46:46 +0200 Subject: [PATCH 128/133] Keep the main thread out of the work that navigation does Going to a definition in an assembly took the main thread first and kept it: the generated signature was written, the document opened, and then that document was type checked and every symbol use in it examined, all in continuations that resume where they were started. The synchronous copy of the same routine, which Ctrl+click used, waited for the check with `runSynchronously` and the tooltip links waited on a modal dialog. Now only opening the document takes the main thread, with the writing before it and the check after it on the pool, and the one routine serves all three callers. Rename keeps the text of the documents its search read so that Visual Studio asking for a span does not read a closed file from disk on the main thread. Peek and Go To Definition pass their cancellation token on, so moving the caret ends the search that is no longer wanted. Finally, a symbol imported from a project with signature files carries the range of its implementation, and using it saves a check of the file navigated to. Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 3 + .../InlineRename/InlineRenameService.fs | 19 +- .../LanguageService/MetadataAsSource.fs | 22 +- .../Navigation/GoToDefinition.fs | 196 ++++++------------ .../Navigation/GoToDefinitionService.fs | 10 +- .../Navigation/NavigableSymbolsService.fs | 21 +- 6 files changed, 108 insertions(+), 163 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index a02aefc833f..303c0e35a7b 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -36,6 +36,9 @@ * Go To Definition into an F# project built with a path map (`DeterministicSourcePaths` or `PathMap`) opens its source instead of a generated signature: the IDE no longer applies `--pathmap` to the project options it checks with. ([PR #20470](https://github.com/dotnet/fsharp/pull/20470)) * Find Implementations no longer searches the whole solution for uses it then discards; it reports the declarations alone. * Find All References reports the uses of a document in one go instead of starting a task per reference, stops when the request is cancelled, and shares one budget of cores with the C# and Visual Basic searches running alongside it, leaving one core to the editor. +* Go To Definition into an assembly no longer type checks the generated signature on the thread the user is typing on: the signature is written and checked in the background, and only opening the document takes the main thread. Ctrl+click and the links in a tooltip follow the same path instead of waiting for it. +* Rename no longer reads a closed file from disk on the main thread when Visual Studio asks for the span of one of its results. +* Go To Definition on a symbol imported from a project with signature files navigates to the implementation without checking that file again when the symbol already carries its range. ### Changed diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs index 8a5debfdc84..0948ca7cfaa 100644 --- a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -3,6 +3,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System +open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable open System.Composition @@ -88,14 +89,21 @@ type internal InlineRenameInfo inherit FSharpInlineRenameInfo() + // Roslyn asks for the span of a location on the UI thread, one call per location, so the text of + // every document holding one is kept from the search that produced them. + let textOfDocument = ConcurrentDictionary() + let getDocumentText (document: Document) = - match document.TryGetText() with + match textOfDocument.TryGetValue document.Id with | true, text -> CancellableTask.singleton text | _ -> - cancellableTask { - let! cancellationToken = CancellableTask.getCancellationToken () - return! document.GetTextAsync(cancellationToken) - } + match document.TryGetText() with + | true, text -> CancellableTask.singleton text + | _ -> + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + return! document.GetTextAsync(cancellationToken) + } let symbolUses = SymbolHelpers.getSymbolUsesInSolution (symbolUse, checkFileResults, document) ct @@ -159,6 +167,7 @@ type internal InlineRenameInfo let! cancellationToken = CancellableTask.getCancellationToken () let document = document.Project.Solution.GetDocument(documentId) let! sourceText = document.GetTextAsync(cancellationToken) + textOfDocument[documentId] <- sourceText return [| diff --git a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs index 0d21ee0b358..f91d655c111 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs @@ -159,19 +159,21 @@ type FSharpMetadataAsSourceService() = let projsArr = projs.ToArray() projsArr |> Array.iter (fun pair -> clear pair.Key pair.Value) - member _.ShowDocument(projInfo: ProjectInfo, filePath: string, text: Text.SourceText) = - match projInfo.Documents |> Seq.tryFindV (fun doc -> doc.FilePath = filePath) with - | ValueSome document -> - let _ = - let directoryName = Path.GetDirectoryName(filePath) + /// Puts the generated signature on disk, where opening it reads it from. Touches no workspace, + /// so the caller can do it before taking the main thread. + member _.WriteDocument(filePath: string, text: Text.SourceText) = + let directoryName = Path.GetDirectoryName filePath - if Directory.Exists(directoryName) |> not then - Directory.CreateDirectory(directoryName) |> ignore + if Directory.Exists directoryName |> not then + Directory.CreateDirectory directoryName |> ignore - use fileStream = new FileStream(filePath, IO.FileMode.Create) - use writer = new StreamWriter(fileStream) - text.Write(writer) + use fileStream = new FileStream(filePath, IO.FileMode.Create) + use writer = new StreamWriter(fileStream) + text.Write writer + member _.ShowDocument(projInfo: ProjectInfo, filePath: string) = + match projInfo.Documents |> Seq.tryFindV (fun doc -> doc.FilePath = filePath) with + | ValueSome document -> let projectFile = Path.ChangeExtension(filePath, "fsproj") let projectContext = createMetadataProjectContext projectFile projInfo document diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 7a05cdfe86e..3b2062aba04 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -221,9 +221,8 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = match textOpt with | None -> CancellableTask.singleton None | Some(text, fileName) -> - foregroundCancellableTask { + cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () - do! ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken) let tmpProjInfo, tmpDocInfo = MetadataAsSource.generateTemporaryDocument ( @@ -232,8 +231,13 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = metadataReferences ) - let tmpShownDocOpt = - metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath, SourceText.From(text.ToString())) + metadataAsSource.WriteDocument(tmpDocInfo.FilePath, SourceText.From(text.ToString())) + + // Opening the document is the only part that belongs to the main thread; checking the + // signature it generates is a type check like any other. + do! ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken) + let tmpShownDocOpt = metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath) + do! TaskScheduler.Default.SwitchTo() match tmpShownDocOpt with | ValueNone -> return None @@ -342,27 +346,33 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = /// instance of its presence in the provided source file. The first case is needed to return proper declaration location for /// recursive type definitions, where the first its usage may not be the declaration. member _.FindSymbolDeclarationInDocument(targetSymbolUse: FSharpSymbolUse, document: Document) = - asyncMaybe { + cancellableTask { let filePath = document.FilePath - let! ct = Async.CancellationToken |> liftAsync - match targetSymbolUse.Symbol.DeclarationLocation with - | Some decl when decl.FileName = filePath -> return decl - | _ -> - let! _, checkFileResults = - document.GetFSharpParseAndCheckResultsAsync("FindSymbolDeclarationInDocument") - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync + // A symbol imported from a project with signature files carries its implementation range + // beside its declaration one; either of them being this file spares a check of the file. + let knownRange = + [| + targetSymbolUse.Symbol.ImplementationLocation + targetSymbolUse.Symbol.DeclarationLocation + targetSymbolUse.Symbol.SignatureLocation + |] + |> Array.tryPickV (function + | Some range when String.Equals(range.FileName, filePath, StringComparison.OrdinalIgnoreCase) -> ValueSome range + | _ -> ValueNone) + + match knownRange with + | ValueSome range -> return Some range + | ValueNone -> + let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync("FindSymbolDeclarationInDocument") let symbolUses = checkFileResults.GetUsesOfSymbolInFile targetSymbolUse.Symbol - let! implSymbol = + return symbolUses - |> Array.sortByDescending (fun x -> x.IsFromDefinition) + |> Array.sortByDescending _.IsFromDefinition |> Array.tryHead - - return implSymbol.Range + |> Option.map _.Range } /// The navigable item for the target symbol's declaration in the implementation document. @@ -611,80 +621,6 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | None -> return false } - member this.NavigateToExternalDeclaration - (targetSymbolUse: FSharpSymbolUse, metadataReferences: seq, cancellationToken: CancellationToken) - = - - let textOpt = - match targetSymbolUse.Symbol with - | :? FSharpEntity as symbol -> symbol.TryGetMetadataText() |> Option.map (fun text -> text, symbol.DisplayName) - | :? FSharpMemberOrFunctionOrValue as symbol -> - symbol.ApparentEnclosingEntity - |> Option.bind (fun entity -> entity.TryGetMetadataText() |> Option.map (fun text -> text, entity.DisplayName)) - | :? FSharpField as symbol -> - match symbol.DeclaringEntity with - | Some entity -> - let text = entity.TryGetMetadataText() - - match text with - | Some text -> Some(text, entity.DisplayName) - | None -> None - | None -> None - | :? FSharpUnionCase as symbol -> - symbol.DeclaringEntity.TryGetMetadataText() - |> Option.map (fun text -> text, symbol.DisplayName) - | _ -> None - - match textOpt with - | Some(text, fileName) -> - let tmpProjInfo, tmpDocInfo = - MetadataAsSource.generateTemporaryDocument ( - AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), - fileName, - metadataReferences - ) - - let tmpShownDocOpt = - metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath, SourceText.From(text.ToString())) - - match tmpShownDocOpt with - | ValueSome tmpShownDoc -> - let goToAsync = - cancellableTask { - - let! cancellationToken = CancellableTask.getCancellationToken () - - let! _, checkResults = tmpShownDoc.GetFSharpParseAndCheckResultsAsync("NavigateToExternalDeclaration") - - let r = - // This tries to find the best possible location of the target symbol's location in the metadata source. - // We really should rely on symbol equality within FCS instead of doing it here, - // but the generated metadata as source isn't perfect for symbol equality. - let symbols = checkResults.GetAllUsesOfAllSymbolsInFile(cancellationToken) - - symbols - |> Seq.tryFindV (tryFindExternalSymbolUse targetSymbolUse) - |> ValueOption.map (fun x -> x.Range) - |> ValueOption.toOption - - match r with - | None -> return TextSpan.empty - | Some r -> - let! text = tmpShownDoc.GetTextAsync(cancellationToken) - - match RoslynHelpers.TryFSharpRangeToTextSpan(text, r) with - | ValueSome span -> return span - | _ -> return TextSpan.empty - - } - - let span = CancellableTask.runSynchronously cancellationToken goToAsync - - let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) - this.NavigateToItem(navItem, cancellationToken) - | _ -> false - | _ -> false - type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, initialDoc: Document, thisSymbolUseRange: range) = let workspace = initialDoc.Project.Solution.Workspace @@ -696,53 +632,47 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, && solution.TryGetDocumentIdFromFSharpRange(range, initialDoc.Project.Id) |> Option.isSome + /// Follows a link in a QuickInfo tooltip, which the user clicks on the main thread. Nothing waits + /// for the result, so the search runs in the background and only the navigation comes back here. member _.NavigateTo(range: range) = - try - ThreadHelper.JoinableTaskFactory.Run( - SR.NavigatingTo(), - (fun _progress cancellationToken -> - cancellableTask { - let targetDoc = solution.TryGetDocumentFromFSharpRange(range, initialDoc.Project.Id) + let navigation = + ThreadHelper.JoinableTaskFactory.RunAsync(fun () -> + cancellableTask { + let targetDoc = solution.TryGetDocumentFromFSharpRange(range, initialDoc.Project.Id) - match targetDoc with - | None -> () - | Some targetDoc -> + match targetDoc with + | None -> () + | Some targetDoc -> - let! cancellationToken = CancellableTask.getCancellationToken () + let! cancellationToken = CancellableTask.getCancellationToken () + + let! targetSource = targetDoc.GetTextAsync(cancellationToken) + let targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(targetSource, range) - let! targetSource = targetDoc.GetTextAsync(cancellationToken) - let targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(targetSource, range) + match targetTextSpan with + | ValueNone -> () + | ValueSome targetTextSpan -> - match targetTextSpan with - | ValueNone -> () - | ValueSome targetTextSpan -> + let gtd = GoToDefinition(metadataAsSource) - let gtd = GoToDefinition(metadataAsSource) + // Whenever possible: + // - signature files (.fsi) should navigate to other signature files + // - implementation files (.fs) should navigate to other implementation files + if isSignatureFile initialDoc.FilePath then + // Target range will point to .fsi file if only there is one so we can just use Roslyn navigation service. + do gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) + else + // Navigation request was made in a .fs file, so we try to find the implementation of the symbol at target range. + // This is the part that may take some time, because of type checks involved. + let! result = gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range) - // Whenever possible: - // - signature files (.fsi) should navigate to other signature files - // - implementation files (.fs) should navigate to other implementation files - if isSignatureFile initialDoc.FilePath then - // Target range will point to .fsi file if only there is one so we can just use Roslyn navigation service. + if not result then + // In case the above fails, we just navigate to target range. do gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) - else - // Navigation request was made in a .fs file, so we try to find the implementation of the symbol at target range. - // This is the part that may take some time, because of type checks involved. - let! result = gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range) + } + |> CancellableTask.startAsTaskWithoutCancellation) - if not result then - // In case the above fails, we just navigate to target range. - do gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) - } - |> CancellableTask.start cancellationToken), - // Default wait time before VS shows the dialog allowing to cancel the long running task is 2 seconds. - // This seems a bit too long to leave the user without any feedback, so we shorten it to 1 second. - // Note: it seems anything less than 1 second will get rounded down to zero, resulting in flashing dialog - // on each navigation, so 1 second is as low as we cen get from JoinableTaskFactory. - TimeSpan.FromSeconds 1 - ) - with :? OperationCanceledException -> - () + navigation.FileAndForget "fsharp/navigateToQuickInfoTarget" member _.FindDefinitionsAsync(position) = cancellableTask { @@ -783,10 +713,8 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, gtd.NavigateToItem(navItem, linked.Token) |> ignore navigated.Value <- true | ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _) -> - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, linked.Token) - |> ignore - - navigated.Value <- true + let! result = gtd.NavigateToExternalDeclarationAsync(targetSymbolUse, metadataReferences) + navigated.Value <- result | _ -> () } |> CancellableTask.start linked.Token), diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index 76233f6fe7e..dd351296c9a 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -17,17 +17,17 @@ open System.Collections.Generic type internal FSharpGoToDefinitionService [] (metadataAsSource: FSharpMetadataAsSourceService) = interface IFSharpGoToDefinitionService with - /// Invoked with Peek Definition. - member _.FindDefinitionsAsync(document: Document, position: int, _cancellationToken: CancellationToken) = + /// Where Go To Definition and Peek Definition arrive: Roslyn searches in the background and + /// navigates to the first item itself, cancelling as the caret moves or the user types. + member _.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) = cancellableTask { let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) let! res = navigation.FindDefinitionsAsync(position) return (res :> IEnumerable<_>) } - |> CancellableTask.startWithoutCancellation + |> CancellableTask.start cancellationToken - /// Invoked with Go to Definition. - /// Try to navigate to the definition of the symbol at the symbolRange in the originDocument + /// The synchronous contract older Roslyn hosts navigate through. member _.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) = let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) navigation.TryGoToDefinition(position, cancellationToken) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs index ac954d2d278..c159aea0f64 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs @@ -6,6 +6,8 @@ open System open System.Threading open System.Threading.Tasks open System.ComponentModel.Composition +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Threading open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation @@ -69,15 +71,16 @@ type internal FSharpNavigableSymbolSource(metadataAsSource) = let nav = { new INavigableSymbol with member _.Navigate(_: INavigableRelationship) = - // Need to new up a CTS here instead of re-using the other one, since VS - // will navigate disconnected from the outer routine, leading to an - // OperationCancelledException if you use the one defined outside. - // TODO: review if it's a proper way of doing it - use ct = new CancellationTokenSource() - - do - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, ct.Token) - |> ignore + // Ctrl+click calls this on the main thread and gives us no token of its + // own: generating the signature and checking it belong to the background, + // and only opening the document comes back here. + let navigation = + ThreadHelper.JoinableTaskFactory.RunAsync(fun () -> + gtd.NavigateToExternalDeclarationAsync(targetSymbolUse, metadataReferences) + |> CancellableTask.startWithoutCancellation + :> Task) + + navigation.FileAndForget "fsharp/navigateToExternalDeclaration" member _.Relationships = seq { yield PredefinedNavigableRelationships.Definition } From aa424f232fc325d397907544cb39cb786a76aab8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 04:06:53 +0200 Subject: [PATCH 129/133] Name the symbol Go To Definition looks for instead of listing them all Navigating from F# to a C# or Visual Basic declaration enumerated every declaration the project holds and mapped each one into an F# external symbol until one matched. That ran on every navigation and on every Ctrl+hover over such a symbol. The compilation can be asked for the symbol by documentation comment id, which F# symbols already carry, so the enumeration is now only what answers when the id cannot be parsed - the operators, mostly. Two more places did work they did not need: the signature-to-implementation paths asked the file system whether a file exists before asking the workspace for it, though a file the workspace does not hold cannot be navigated to either way; and a failed cross-language lookup checked the whole project once per target framework, where the parsed declarations of the other frameworks have already been searched. Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 + .../CrossLanguageSymbolNavigation.fs | 5 +- .../Navigation/GoToDefinition.fs | 94 +++++++++++-------- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 303c0e35a7b..9627c8ceee9 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -39,6 +39,8 @@ * Go To Definition into an assembly no longer type checks the generated signature on the thread the user is typing on: the signature is written and checked in the background, and only opening the document takes the main thread. Ctrl+click and the links in a tooltip follow the same path instead of waiting for it. * Rename no longer reads a closed file from disk on the main thread when Visual Studio asks for the span of one of its results. * Go To Definition on a symbol imported from a project with signature files navigates to the implementation without checking that file again when the symbol already carries its range. +* Go To Definition on a C# or Visual Basic symbol asks the compilation for it by documentation comment id instead of walking every declaration the project holds, which it did on each navigation and each Ctrl+hover; the walk remains for the symbols whose id Roslyn cannot parse. +* Go To Definition from C# or Visual Basic into a multi-targeted F# project checks the whole project at most once instead of once per target framework when the parsed declarations do not answer. ### Changed diff --git a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs index 70862164b45..7099856e1e1 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs @@ -383,8 +383,11 @@ module internal CrossLanguageSymbolNavigation = with | ValueSome found -> return ValueSome found | ValueNone -> + // Checking a whole project is the expensive answer, and the target frameworks of one + // project check the same declarations: whatever only one of them compiles is under + // conditional compilation, which the parsed declarations above already covered. return! - ordered + [ for instance in instances -> instance.Head ] |> CancellableTask.tryPick (located (tryLocateInProject documentationCommentId path)) } diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 3b2062aba04..6c57195d9b2 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -110,6 +110,38 @@ module private ExternalSymbol = | _ -> [] + /// The C# or Visual Basic symbol an F# resolution points at. Its documentation comment id names + /// it outright; walking every declaration of the project is what answers for the symbols whose id + /// Roslyn cannot parse, operators among them. + let tryFind (project: Project) (targetSymbolUse: FSharpSymbolUse) targetExternalSymbol = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + let! compilation = project.GetCompilationAsync cancellationToken + + let named = + match compilation, targetSymbolUse.Symbol.DocumentationCommentId with + | null, _ + | _, ValueNone -> ValueNone + | compilation, ValueSome documentationCommentId -> + match DocumentationCommentId.GetFirstSymbolForDeclarationId(documentationCommentId, compilation) with + | null -> ValueNone + | symbol -> ValueSome symbol + + match named with + | ValueSome symbol -> return ValueSome symbol + | ValueNone -> + let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, (fun _ -> true), cancellationToken) + + return + symbols + |> Seq.collect ofRoslynSymbol + |> Seq.tryPickV (fun (symbol, externalSymbol) -> + if externalSymbol = targetExternalSymbol then + ValueSome symbol + else + ValueNone) + } + type internal FSharpGoToDefinitionNavigableItem(document, sourceSpan) = inherit FSharpNavigableItem(Glyph.BasicFile, ImmutableArray.Empty, document, sourceSpan) @@ -310,32 +342,29 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = if isSignatureFile fsSymbolUse.FileName && preferSignature = false then let fsfilePath = Path.ChangeExtension(originRange.FileName, "fs") - if not (File.Exists fsfilePath) then - return None - else - let implDoc = originDocument.TryGetSolutionDocumentFromPath fsfilePath + // A file the solution does not hold cannot be navigated to, so asking it + // answers what asking the disk would, without leaving the workspace. + match originDocument.TryGetSolutionDocumentFromPath fsfilePath with + | ValueNone -> return None + | ValueSome implDoc -> + let! implSourceText = implDoc.GetTextAsync(ct) - match implDoc with - | ValueNone -> return None - | ValueSome implDoc -> - let! implSourceText = implDoc.GetTextAsync(ct) + let! _, checkFileResults = implDoc.GetFSharpParseAndCheckResultsAsync(userOpName) - let! _, checkFileResults = implDoc.GetFSharpParseAndCheckResultsAsync(userOpName) + let symbolUses = + checkFileResults.GetUsesOfSymbolInFile(symbol, cancellationToken = ct) - let symbolUses = - checkFileResults.GetUsesOfSymbolInFile(symbol, cancellationToken = ct) + let implSymbol = Array.tryHeadV symbolUses - let implSymbol = Array.tryHeadV symbolUses + match implSymbol with + | ValueNone -> return None + | ValueSome implSymbol -> + let implTextSpan = + RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, implSymbol.Range) - match implSymbol with + match implTextSpan with | ValueNone -> return None - | ValueSome implSymbol -> - let implTextSpan = - RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, implSymbol.Range) - - match implTextSpan with - | ValueNone -> return None - | ValueSome implTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(implDoc, implTextSpan)) + | ValueSome implTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(implDoc, implTextSpan)) else match originDocument.TryGetSolutionDocumentFromFSharpRange fsSymbolUse.Range with | ValueNone -> return None @@ -400,12 +429,9 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = if isSignatureFile originDocument.FilePath then let implFilePath = Path.ChangeExtension(originDocument.FilePath, "fs") - if not (File.Exists implFilePath) then - return ValueNone - else - match originDocument.TryGetSolutionDocumentFromPath implFilePath with - | ValueNone -> return ValueNone - | ValueSome implDocument -> return! this.FindNavigableDeclarationIn(targetSymbolUse, implDocument) + match originDocument.TryGetSolutionDocumentFromPath implFilePath with + | ValueNone -> return ValueNone + | ValueSome implDocument -> return! this.FindNavigableDeclarationIn(targetSymbolUse, implDocument) else let declarations = checkFileResults.GetDeclarationLocation( @@ -434,9 +460,6 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let textLineString = textLine.ToString() let fcsTextLineNumber = Line.fromZ textLinePos.Line let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - - let! cancellationToken = CancellableTask.getCancellationToken () - let preferSignature = isSignatureFile originDocument.FilePath let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) @@ -474,18 +497,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = match projectOpt with | ValueSome project -> - let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, (fun _ -> true), cancellationToken) - - let roslynSymbols = Seq.collect ExternalSymbol.ofRoslynSymbol symbols - - let symbol = - Seq.tryPickV - (fun (sym, externalSym) -> - if externalSym = targetExternalSym then - ValueSome sym - else - ValueNone) - roslynSymbols + let! symbol = ExternalSymbol.tryFind project targetSymbolUse targetExternalSym let location = symbol From 7b9e83f6cbd6cd5f8ec8ca60a4ba38141d9e3cf5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 17:52:15 +0200 Subject: [PATCH 130/133] Report a use once when several instances compile its file Searching each file of a multi-targeted project once left two holes, and a solution that uses conditional compilation widely fell through both: rename and Find All References reported the same use once per target framework. A secondary instance re-searched a file whenever it held any conditional directive, so an inactive `#if DEBUG` was enough to search it again under every target framework. Only the defines the two instances disagree on can make a shared file parse differently, so the directives are now read for the idents they test and the file is re-searched only when one of those defines is in that difference. Instances whose defines match skip their shared files outright, which also spares the checker the project builds those searches would force. The uses themselves were never deduplicated on the F# side, so a file two projects compile - the instances of one project file, or two project files sharing a source file - reported its every use twice over. A range carries its file, so the first project to report one keeps it. The test project gained a use beside the `#if FOO` block and a file guarded by a define both instances share: its only conditional use used to sit inside the disabled branch, which is why the duplicates went unnoticed. Co-Authored-By: Claude Opus 5 --- .../LanguageService/SymbolHelpers.fs | 15 +++++ .../LanguageService/WorkspaceExtensions.fs | 58 ++++++++++++++++--- .../MultiTargetFindReferencesTests.fs | 43 +++++++++----- 3 files changed, 92 insertions(+), 24 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 787d86489f5..686b39e1cee 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -126,6 +126,21 @@ module internal SymbolHelpers = TelemetryReporter.ReportSingleEvent(TelemetryEvents.GetSymbolUsesInProjectsStarted, props) let! ct = CancellableTask.getCancellationToken () + + // A file that several projects compile - the target-framework instances of one project + // file, or two project files sharing a source file - is searched in each of them and + // reports the same range every time. The range carries its file, so the first project + // to report a use keeps it and the rest are dropped. + let reported = ConcurrentDictionary() + + let onFound document ranges = + let fresh = + ranges |> Seq.filter (fun range -> reported.TryAdd(range, ())) |> Seq.toArray + + if fresh.Length = 0 then + CancellableTask.singleton () + else + onFound document fresh // Mutated by the checker while a snapshot is built, so snapshots are built one at a time. let snapshotAccumulator = Dictionary() let searches = ResizeArray() diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 7228bb80fb7..89220cf4374 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -14,6 +14,7 @@ open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.ProjectSnapshot open FSharp.Compiler.Symbols open FSharp.Compiler.Syntax +open FSharp.Compiler.SyntaxTrivia open FSharp.Compiler.BuildGraph open CancellableTasks @@ -758,10 +759,32 @@ type Document with ) } -let private hasConditionalDirectives (parseTree: ParsedInput) = - match parseTree with - | ParsedInput.ImplFile file -> not file.Trivia.ConditionalDirectives.IsEmpty - | ParsedInput.SigFile file -> not file.Trivia.ConditionalDirectives.IsEmpty +let rec private definesTestedBy expr = + seq { + match expr with + | IfDirectiveExpression.And(left, right) + | IfDirectiveExpression.Or(left, right) -> + yield! definesTestedBy left + yield! definesTestedBy right + | IfDirectiveExpression.Not expr -> yield! definesTestedBy expr + | IfDirectiveExpression.Ident name -> yield name + } + +/// Whether the file can parse differently under the given defines: it tests one of them in a +/// conditional directive. A file whose `#if` only tests defines the two instances share compiles to +/// the same tree in both, however many directives it has. +let private dependsOnDefines (defines: string Set) (parseTree: ParsedInput) = + let directives = + match parseTree with + | ParsedInput.ImplFile file -> file.Trivia.ConditionalDirectives + | ParsedInput.SigFile file -> file.Trivia.ConditionalDirectives + + directives + |> List.exists (function + | ConditionalDirectiveTrivia.If(expr, _) + | ConditionalDirectiveTrivia.Elif(expr, _) -> definesTestedBy expr |> Seq.exists defines.Contains + | ConditionalDirectiveTrivia.Else _ + | ConditionalDirectiveTrivia.EndIf _ -> false) /// How many documents of one project a search keeps in flight. The throttle it shares with the other /// projects decides how many of those actually run. @@ -772,8 +795,8 @@ type Project with /// Find F# references in the given project. When `searchedInstance` is another target-framework /// instance of the same project file that has already been searched, only the documents whose - /// sources can differ from it are searched: files compiled only here and files with conditional - /// compilation. + /// sources can differ from it are searched: files compiled only here and files whose conditional + /// compilation tests a define the two instances disagree on. member this.FindFSharpReferencesAsync ( symbol: FSharpSymbol, @@ -817,11 +840,30 @@ type Project with searchedInstance |> ValueOption.map (fun instance -> HashSet(instance.Documents |> Seq.map _.FilePath, StringComparer.OrdinalIgnoreCase)) + // Only the defines one instance has and the other lacks can make a shared file parse + // differently; the ones they agree on cannot, however many directives test them. + let definesOf (project: Project) = + getFSharpOptionsForProject project + |> CancellableTask.map (fun (_, _, parsingOptions: FSharpParsingOptions, _) -> Set parsingOptions.ConditionalDefines) + + let! differingDefines = + match searchedInstance with + | ValueNone -> CancellableTask.singleton Set.empty + | ValueSome instance -> + cancellableTask { + let! defines = definesOf this + let! searchedDefines = definesOf instance + return (defines - searchedDefines) + (searchedDefines - defines) + } + let needsSearch (document: Document) = match searchedPaths with | ValueSome paths when paths.Contains document.FilePath -> - document.GetFSharpParseResultsAsync userOpName - |> CancellableTask.map (fun parseResults -> hasConditionalDirectives parseResults.ParseTree) + if differingDefines.IsEmpty then + CancellableTask.singleton false + else + document.GetFSharpParseResultsAsync userOpName + |> CancellableTask.map (fun parseResults -> dependsOnDefines differingDefines parseResults.ParseTree) | _ -> CancellableTask.singleton true let search (document: Document) = diff --git a/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs index c9bc77c57d1..96fe6843f38 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/MultiTargetFindReferencesTests.fs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. /// One project loaded as two target-framework instances: `plain` compiles without the fourth file -/// and without FOO, `foo` compiles everything with FOO defined. +/// and without FOO, `foo` compiles everything with FOO defined. Both define COMMON. module FSharp.Editor.Tests.MultiTargetFindReferencesTests open System @@ -26,10 +26,13 @@ let private project = ExtraSource = "let plainUse x = ModuleFirst.sharedFunc x" }, { sourceFile "Third" [ "First" ] with - ExtraSource = "#if FOO\nlet fooUse x = ModuleFirst.sharedFunc x\n#endif" + ExtraSource = "#if FOO\nlet fooUse x = ModuleFirst.sharedFunc x\n#endif\nlet useBesideFoo x = ModuleFirst.sharedFunc x" }, { sourceFile "Fourth" [ "First" ] with ExtraSource = "let fooOnlyFileUse x = ModuleFirst.sharedFunc x" + }, + { sourceFile "Fifth" [ "First" ] with + ExtraSource = "#if COMMON\nlet commonUse x = ModuleFirst.sharedFunc x\n#endif" } ) @@ -39,11 +42,11 @@ let private solution, plainId, fooId = project, [ { - Defines = [] + Defines = [ "COMMON" ] ExcludedFileIds = [ "Fourth" ] } { - Defines = [ "FOO" ] + Defines = [ "COMMON"; "FOO" ] ExcludedFileIds = [] } ] @@ -77,8 +80,9 @@ let ``every file is searched once, files under conditional compilation and insta .Wait() Assert.Equal(1, foundDefinitions.Count) - // The signature, Second, Third under FOO and Fourth compiled only into foo: one hit each, not one per instance. - Assert.Equal(4, foundReferences.Count) + // The signature, Second, both uses in Third, Fourth compiled only into foo, and Fifth under the + // define both instances share: one hit each, not one per instance. + Assert.Equal(6, foundReferences.Count) /// What Rename works from: the symbol's uses grouped by Roslyn document. let private usesByDocument (document: Document) = @@ -116,18 +120,25 @@ let private usesByDocument (document: Document) = [] [] [] -let ``rename gets one document per file, owned by an instance that compiles it`` (instance: int) = +let ``rename gets every use once, from an instance that compiles its file`` (instance: int) = let uses = usesByDocument (documentIn (instanceOf instance) firstPath) - let instancesOf fileId = + let located = [ - for id in uses.Keys do - if solution.GetDocument(id).FilePath = project.GetFilePath fileId then - id.ProjectId + for KeyValue(documentId, ranges) in uses do + for range in ranges -> solution.GetDocument(documentId).FilePath, range ] - let files = [ for id in uses.Keys -> solution.GetDocument(id).FilePath ] - Assert.Equal(files.Length, List.length (List.distinct files)) - Assert.Equal([ instanceOf instance ], instancesOf "Second") - Assert.Equal([ fooId ], instancesOf "Third") - Assert.Equal([ fooId ], instancesOf "Fourth") + // A file several instances compile is searched in each of them; the same use must reach Rename once. + Assert.Equal<(string * range) list>(List.distinct located, located) + + let files = located |> List.map fst |> List.distinct + + for fileId in [ "Second"; "Third"; "Fourth"; "Fifth" ] do + Assert.Contains(project.GetFilePath fileId, files) + + for documentId in uses.Keys do + let compiledHere = + solution.GetProject(documentId.ProjectId).Documents |> Seq.map _.FilePath + + Assert.Contains(solution.GetDocument(documentId).FilePath, compiledHere) From 0a6ba94d5bebcd3165f93d5ae112d827b7c3afd4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 20:22:09 +0200 Subject: [PATCH 131/133] Take the instance to search in full without ordering the rest Grouping the target-framework instances of a project file sorted each group to read its head, though what follows the head is never ordered: every secondary instance is searched the same way. One pass for the best-ranked instance says that outright, and `Array.minBy` keeps the first of equal rank, which is what the stable sort put at the head. Sorting the whole solution once and letting the grouping keep that order reads better still, but it measures worse - 86 instances over 26 project files, on net472: 14.5 us and 16.1 KB for the pass per group against 17.2 us and 17.4 KB for the single sort, and 15.0 us and 16.7 KB for the list-and-sort this replaces. Neither number matters next to the project checks the grouping schedules; the sort is dropped because it is not paid for. `start` becomes `startSearching`, with the reason it is not awaited written down, and the callback that collects the uses becomes a function rather than a lambda bound to a name. Co-Authored-By: Claude Opus 5 --- .../LanguageService/SymbolHelpers.fs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 686b39e1cee..d213fa12f88 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -90,14 +90,17 @@ module internal SymbolHelpers = /// One search per project file: the best-ranked target-framework instance is searched in full, /// the others only where their sources can differ from it. - let private groupInstances (currentProject: Project) (projects: Project list) = + let private groupInstances (currentProject: Project) (projects: Project seq) = let rank = rankInstances currentProject - projects - |> List.groupBy _.FilePath - |> List.map (fun (_, instances) -> - let ordered = List.sortBy rank instances - struct (ordered.Head, ordered.Tail)) + seq { + for _, instances in projects |> Seq.groupBy _.FilePath do + let instances = Seq.toArray instances + // Ordering the rest buys nothing, and ranking every instance of the solution at once + // measures slower than one pass per group. + let primary = Array.minBy rank instances + struct (primary, instances |> Seq.filter (fun instance -> instance.Id <> primary.Id)) + } /// One core is left to the thread that has to stay responsive: every search shares this budget, /// including the C# and Visual Basic searches that run alongside the F# ones. @@ -119,7 +122,7 @@ module internal SymbolHelpers = if isFastFindReferencesEnabled then groupInstances currentProject projects else - [ for project in projects -> struct (project, []) ] + seq { for project in projects -> struct (project, Seq.empty) } cancellableTask { // TODO: this needs to be a single event with a duration @@ -152,7 +155,8 @@ module internal SymbolHelpers = else CancellableTask.singleton ValueNone - let start (project: Project) snapshot searchedInstance = + // Started, not awaited: the next project's snapshot is built while this one searches. + let startSearching (project: Project) snapshot searchedInstance = searches.Add( project.FindFSharpReferencesAsync (symbol, snapshot, searchedInstance, searchThrottle, onFound, "getSymbolUsesInProjects") @@ -161,11 +165,11 @@ module internal SymbolHelpers = for struct (primary, secondaries) in groups do let! snapshot = snapshotFor primary - start primary snapshot ValueNone + startSearching primary snapshot ValueNone for secondary in secondaries do let! snapshot = snapshotFor secondary - start secondary snapshot (ValueSome primary) + startSearching secondary snapshot (ValueSome primary) do! Task.WhenAll searches @@ -237,12 +241,11 @@ module internal SymbolHelpers = cancellableTask { let symbolUses = ConcurrentBag() - let onFound = - fun document (ranges: range seq) -> - cancellableTask { - for range in ranges do - symbolUses.Add(document, range) - } + let onFound document (ranges: range seq) = + cancellableTask { + for range in ranges do + symbolUses.Add(document, range) + } do! findSymbolUses symbolUse currentDocument checkFileResults onFound From 49f4629cce2d43ca96276acc9a28dc8243d22f3d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 8 Sep 2026 20:44:13 +0200 Subject: [PATCH 132/133] Carry navigation results in value options and struct tuples The cross-language lookup builds a candidate per symbol shape and the usages search carries a definition item beside its project all the way to the report, so both allocate an option and a tuple per element on paths that run over every declaration of a project. Neither escapes to a public surface: the pairs live inside one function or one module, which is where the F# guidance puts value options and struct tuples. `FindDeclExternalType`, `FindDeclExternalParam` and `ofRoslynSymbol` answer with value options and struct tuples; `FindUsagesService` carries `struct (definitionItem, project)` and `struct (definitionItem, location)` through the cross-language search and the report; the navigable-items grouping takes its instances as an array. `Extensions` gains the `ValueOption` counterparts the conversion needs. Co-Authored-By: Claude Opus 5 --- .../src/FSharp.Editor/Common/Extensions.fs | 29 +++++++- .../CrossLanguageSymbolNavigation.fs | 2 +- .../Navigation/FindUsagesService.fs | 36 +++++----- .../Navigation/GoToDefinition.fs | 72 ++++++++++--------- .../Navigation/NavigateToSearchService.fs | 4 +- 5 files changed, 88 insertions(+), 55 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 94cb707b3a0..22b92c9a224 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -424,6 +424,33 @@ module Option = else None +[] +module ValueOption = + + let guard (x: bool) : ValueOption = if x then ValueSome() else ValueNone + + let attempt (f: unit -> 'T) = + try + Some <| f () + with _ -> + None + + /// Returns 'Some list' if all elements in the list are Some, otherwise None + let ofOptionList (xs: 'a option list) : 'a list voption = + + if xs |> List.forall Option.isSome then + xs |> List.map Option.get |> ValueSome + else + ValueNone + + /// Returns 'Some list' if all elements in the list are Some, otherwise None + let ofValueOptionList (xs: 'a ValueOption list) : 'a list ValueOption = + + if xs |> List.forall ValueOption.isSome then + xs |> List.map ValueOption.get |> ValueSome + else + ValueNone + [] module IEnumerator = let chooseV f (e: IEnumerator<'T>) = @@ -513,7 +540,7 @@ module Seq = res - let chooseV (chooser: 'a -> 'b voption) source = + let chooseV< 'a, 'b > (chooser: 'a -> 'b voption) source = revamp (IEnumerator.chooseV chooser) source [] diff --git a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs index 7099856e1e1..96c95055874 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/CrossLanguageSymbolNavigation.fs @@ -364,7 +364,7 @@ module internal CrossLanguageSymbolNavigation = |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) |> Seq.groupBy _.FilePath |> Seq.map (snd >> List.ofSeq) - |> List.ofSeq + |> Seq.toList let ordered = [ diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index 19cd29835d9..0abb251e508 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -42,15 +42,15 @@ module FSharpFindUsagesService = externalDefinitionItem else definitionItems - |> Array.tryFind (fun (_, project: Project) -> project.FilePath = doc.Project.FilePath) - |> Option.map (fun (definitionItem, _) -> definitionItem) - |> Option.defaultValue externalDefinitionItem + |> Array.tryFindV (fun struct (_, project: Project) -> project.FilePath = doc.Project.FilePath) + |> ValueOption.map (fun struct (definitionItem, _) -> definitionItem) + |> ValueOption.defaultValue externalDefinitionItem for symbolUse in symbolUses do cancellationToken.ThrowIfCancellationRequested() match declarationRange, RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with - | Some declRange, _ when Range.equals declRange symbolUse -> () + | ValueSome declRange, _ when Range.equals declRange symbolUse -> () | _, ValueNone -> () | _, ValueSome textSpan -> match textSpan with @@ -128,27 +128,27 @@ module FSharpFindUsagesService = /// The uses in the C# and VB projects that reference the assembly of a project declaring the symbol, /// each with the definition item to report them under. - let private findCrossLanguageReferences (docId: string) (definitionItems: (FSharpDefinitionItem * Project)[]) = + let private findCrossLanguageReferences (docId: string) (definitionItems: struct (FSharpDefinitionItem * Project) seq) = seq { - for definitionItem, declaringProject in definitionItems do - for project in referencingCompilationProjects declaringProject -> definitionItem, project + for struct (definitionItem, declaringProject) in definitionItems do + for project in referencingCompilationProjects declaringProject -> struct (definitionItem, project) } - |> Seq.distinctBy (fun (_, project) -> project.Id) - |> Seq.map (fun (definitionItem, project) -> + |> Seq.distinctBy (fun struct (_, project) -> project.Id) + |> Seq.map (fun struct (definitionItem, project) -> findRoslynReferences docId project - |> CancellableTask.map (Seq.map (fun location -> definitionItem, location))) + |> CancellableTask.map (Seq.map (fun location -> struct (definitionItem, location)))) |> CancellableTask.whenAllThrottled ConcurrentCompilations |> CancellableTask.map Seq.concat /// Reports each file span once: the target-framework instances of a consumer share their files. let private reportCrossLanguageReferences - (found: (FSharpDefinitionItem * ReferenceLocation) seq) + (found: struct (FSharpDefinitionItem * ReferenceLocation) seq) (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task) = cancellableTask { let reported = HashSet() - for definitionItem, location in found do + for struct (definitionItem, location) in found do let span = location.Location.SourceSpan if reported.Add(struct (location.Document.FilePath, span)) then @@ -185,13 +185,13 @@ module FSharpFindUsagesService = let declarationRange = match declaration with - | FindDeclResult.DeclFound range -> Some range - | _ -> None + | FindDeclResult.DeclFound range -> ValueSome range + | _ -> ValueNone let! declarationSpans = match declarationRange with - | Some range -> rangeToDocumentSpans (document, range, symbol.Ident.idText) - | None -> CancellableTask.singleton [||] + | ValueSome range -> rangeToDocumentSpans (document, range, symbol.Ident.idText) + | ValueNone -> CancellableTask.singleton [||] let declarationSpans = declarationSpans @@ -210,11 +210,11 @@ module FSharpFindUsagesService = let definitionItems = declarationSpans - |> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project) + |> Array.map (fun span -> struct (FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project)) do! definitionItems - |> Seq.map (fst >> context.OnDefinitionFoundAsync) + |> Seq.map (fun struct (definitionItem, _) -> context.OnDefinitionFoundAsync(definitionItem)) |> Task.WhenAll if isExternal then diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 6c57195d9b2..5e799d74552 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -44,34 +44,39 @@ module private Symbol = inner [] root |> String.concat "." module private FindDeclExternalType = - let rec tryOfRoslynType (typesym: ITypeSymbol) : FindDeclExternalType option = + let rec tryOfRoslynType (typesym: ITypeSymbol) : FindDeclExternalType voption = match typesym with | :? IPointerTypeSymbol as ptrparam -> tryOfRoslynType ptrparam.PointedAtType - |> Option.map FindDeclExternalType.Pointer - | :? IArrayTypeSymbol as arrparam -> tryOfRoslynType arrparam.ElementType |> Option.map FindDeclExternalType.Array - | :? ITypeParameterSymbol as typaram -> Some(FindDeclExternalType.TypeVar typaram.Name) + |> ValueOption.map FindDeclExternalType.Pointer + | :? IArrayTypeSymbol as arrparam -> + tryOfRoslynType arrparam.ElementType + |> ValueOption.map FindDeclExternalType.Array + | :? ITypeParameterSymbol as typaram -> ValueSome(FindDeclExternalType.TypeVar typaram.Name) | :? INamedTypeSymbol as namedTypeSym -> namedTypeSym.TypeArguments |> Seq.map tryOfRoslynType |> List.ofSeq - |> Option.ofOptionList - |> Option.map (fun genericArgs -> FindDeclExternalType.Type(Symbol.fullName typesym, genericArgs)) + |> ValueOption.ofValueOptionList + |> ValueOption.map (fun genericArgs -> FindDeclExternalType.Type(Symbol.fullName typesym, genericArgs)) | _ -> Debug.Assert(false, sprintf "GoToDefinitionService: Unexpected Roslyn type symbol subclass: %O" (typesym.GetType())) - None + ValueNone module private FindDeclExternalParam = - let tryOfRoslynParameter (param: IParameterSymbol) : FindDeclExternalParam option = + let tryOfRoslynParameter (param: IParameterSymbol) : FindDeclExternalParam voption = FindDeclExternalType.tryOfRoslynType param.Type - |> Option.map (fun ty -> FindDeclExternalParam.Create(ty, param.RefKind <> RefKind.None)) + |> ValueOption.map (fun ty -> FindDeclExternalParam.Create(ty, param.RefKind <> RefKind.None)) - let tryOfRoslynParameters (paramSyms: ImmutableArray) : FindDeclExternalParam list option = - paramSyms |> Seq.map tryOfRoslynParameter |> Seq.toList |> Option.ofOptionList + let tryOfRoslynParameters (paramSyms: ImmutableArray) : FindDeclExternalParam list voption = + paramSyms + |> Seq.map tryOfRoslynParameter + |> Seq.toList + |> ValueOption.ofValueOptionList module private ExternalSymbol = - let rec ofRoslynSymbol (symbol: ISymbol) : (ISymbol * FindDeclExternalSymbol) list = + let rec ofRoslynSymbol (symbol: ISymbol) : struct (ISymbol * FindDeclExternalSymbol) list = let container = Symbol.fullName symbol.ContainingSymbol match symbol with @@ -80,18 +85,18 @@ module private ExternalSymbol = let constructors = typesym.InstanceConstructors - |> Seq.choose<_, ISymbol * FindDeclExternalSymbol> (fun methsym -> + |> Seq.chooseV<_, struct (ISymbol * FindDeclExternalSymbol)> (fun methsym -> FindDeclExternalParam.tryOfRoslynParameters methsym.Parameters - |> Option.map (fun args -> upcast methsym, FindDeclExternalSymbol.Constructor(fullTypeName, args))) + |> ValueOption.map (fun args -> struct (methsym, FindDeclExternalSymbol.Constructor(fullTypeName, args)))) |> List.ofSeq - (symbol, FindDeclExternalSymbol.Type fullTypeName) :: constructors + struct (symbol, FindDeclExternalSymbol.Type fullTypeName) :: constructors | :? IMethodSymbol as methsym -> FindDeclExternalParam.tryOfRoslynParameters methsym.Parameters - |> Option.map (fun args -> - symbol, FindDeclExternalSymbol.Method(container, methsym.MetadataName, args, methsym.TypeParameters.Length)) - |> Option.toList + |> ValueOption.map (fun args -> + struct (symbol, FindDeclExternalSymbol.Method(container, methsym.MetadataName, args, methsym.TypeParameters.Length))) + |> ValueOption.toList | :? IPropertySymbol as propsym -> [ @@ -135,7 +140,7 @@ module private ExternalSymbol = return symbols |> Seq.collect ofRoslynSymbol - |> Seq.tryPickV (fun (symbol, externalSymbol) -> + |> Seq.tryPickV (fun struct (symbol, externalSymbol) -> if externalSymbol = targetExternalSymbol then ValueSome symbol else @@ -378,17 +383,19 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = cancellableTask { let filePath = document.FilePath + let inThisFile = + function + | Some(range: range) when String.Equals(range.FileName, filePath, StringComparison.OrdinalIgnoreCase) -> ValueSome range + | _ -> ValueNone + + let symbol = targetSymbolUse.Symbol + // A symbol imported from a project with signature files carries its implementation range // beside its declaration one; either of them being this file spares a check of the file. let knownRange = - [| - targetSymbolUse.Symbol.ImplementationLocation - targetSymbolUse.Symbol.DeclarationLocation - targetSymbolUse.Symbol.SignatureLocation - |] - |> Array.tryPickV (function - | Some range when String.Equals(range.FileName, filePath, StringComparison.OrdinalIgnoreCase) -> ValueSome range - | _ -> ValueNone) + inThisFile symbol.ImplementationLocation + |> ValueOption.orElseWith (fun () -> inThisFile symbol.DeclarationLocation) + |> ValueOption.orElseWith (fun () -> inThisFile symbol.SignatureLocation) match knownRange with | ValueSome range -> return Some range @@ -399,9 +406,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return symbolUses - |> Array.sortByDescending _.IsFromDefinition - |> Array.tryHead - |> Option.map _.Range + |> Seq.sortByDescending _.IsFromDefinition + |> Seq.tryHeadV + |> ValueOption.map _.Range + |> ValueOption.toOption } /// The navigable item for the target symbol's declaration in the implementation document. @@ -500,9 +508,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let! symbol = ExternalSymbol.tryFind project targetSymbolUse targetExternalSym let location = - symbol - |> ValueOption.map (fun s -> s.Locations) - |> ValueOption.bind Seq.tryHeadV + symbol |> ValueOption.map _.Locations |> ValueOption.bind Seq.tryHeadV match location with | ValueNone -> return ValueNone diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index f9ff7e88382..b8ddc217da0 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -126,7 +126,7 @@ type internal FSharpNavigateToSearchService [] (itemsCache project.Solution.Projects |> Seq.filter (fun p -> p.FilePath = projectPath) |> Seq.map _.Id - |> List.ofSeq + |> Seq.toArray fun (document: Document) -> match document.FilePath with @@ -136,7 +136,7 @@ type internal FSharpNavigateToSearchService [] (itemsCache let owner = instances - |> List.find (fun id -> documentIds |> Seq.exists (fun documentId -> documentId.ProjectId = id)) + |> Array.find (fun id -> documentIds |> Seq.exists (fun documentId -> documentId.ProjectId = id)) owner = project.Id || itemsCache.HasConditionalDirectives path From ddffeff0782cd54c08da2b5743b1aeee0cb3cca1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 01:46:43 +0200 Subject: [PATCH 133/133] Answer the Navigate-To search that runs while the solution loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigate To runs a search of its own during load, dispatched through `IAdvancedNavigateToSearchService`. F# did not implement it, so every F# project was reported complete and searched not at all, and no full search follows by design — nothing F# declares could be found until the user searched again. The search is a parse away. What it lacked is the project's compilation options, which do not exist yet during load, so `GetFSharpParseResultsAsync` raises. `GetFSharpQuickParseResultsAsync` parses with whatever parsing options the project system has already produced, or defaults: a dictionary read, no reactor, no I/O, which is what makes it safe to call for every document of every project while the solution loads. Those defines can be the wrong ones, and the document's version does not change when the real options arrive, so the version stamp alone would let an approximate parse answer the accurate search: a declaration behind `#if` could be missed, or reported from a branch that never compiles. The cache entry carries whether it was approximate, and the accurate path refuses those, reparsing instead. The loading path takes either, since its contract allows out-of-date results. `SearchCachedDocumentsAsync` follows the shape of the C# and VB service: priority documents and the projects that hold them are searched first, results are reported per document rather than per project so the first ones appear while the rest are still parsing, and each project is reported complete once it is done. The parses take turns on the throttle every search shares, so a search during load cannot take the cores away from the load itself. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../LanguageService/WorkspaceExtensions.fs | 6 + .../Navigation/NavigateToSearchService.fs | 148 +++++++++++++++--- .../FSharp.Editor.Tests.fsproj | 1 + .../NavigateToSearchWhileLoadingTests.fs | 132 ++++++++++++++++ 5 files changed, 262 insertions(+), 26 deletions(-) create mode 100644 vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 9627c8ceee9..3bde9f7b251 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -7,6 +7,7 @@ ### Fixed +* Navigate To lists F# declarations while the solution is still loading. Until now the search that runs during load skipped F# entirely, and the full search that follows it is never started, so nothing F# declares could be found until the next search. * Reuse the in-memory PE reference of a referenced C# project while its dependent semantic version is unchanged, so dependent F# projects no longer re-import every reference and re-check every file each time Roslyn recreates the `Compilation`. ([PR #20460](https://github.com/dotnet/fsharp/pull/20460)) * Fixed `MissingManifestResourceException` when opening F# pages under Tools > Options by giving `FSharpPackage` its own `VSPackage` resource set. ([PR #20408](https://github.com/dotnet/fsharp/pull/20408)) * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 89220cf4374..bb0120199a5 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -659,6 +659,12 @@ type Document with this.TryGetFSharpParseResultsAsync(userOpName) |> CancellableTask.map (orRaise "FSharp project options not found.") + /// Parses the given F# document with the parsing options its project has already produced, or with defaults when + /// it has none yet: the only parse available while the project system is still loading. The defines can be the + /// wrong ones, so the tree describes a compilation that may never happen. + member this.GetFSharpQuickParseResultsAsync(userOpName) = + this.GetFSharpChecker().ParseDocument(this, this.GetFSharpQuickParsingOptions(), userOpName) + /// Parses and checks the given F# document; ValueNone while its project has no compilation options /// or the check was aborted. member this.TryGetFSharpParseAndCheckResultsAsync diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index b8ddc217da0..a6319be550d 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -29,7 +29,14 @@ type internal FSharpNavigableItemsCache (patternMatcherFactory: IPatternMatcherFactory, [] workspace: VisualStudioWorkspace) = let cache = - ConcurrentDictionary() + ConcurrentDictionary< + DocumentId, + struct {| + Version: VersionStamp + Approximate: bool + Items: NavigableItem array + |} + >() /// Whether the file's parse depends on the defines, by file path: known once any instance has parsed it. let conditionalDirectives = @@ -49,30 +56,54 @@ type internal FSharpNavigableItemsCache | ParsedInput.ImplFile file -> not file.Trivia.ConditionalDirectives.IsEmpty | ParsedInput.SigFile file -> not file.Trivia.ConditionalDirectives.IsEmpty + let store (document: Document) version approximate (parseTree: ParsedInput) = + let items = NavigateTo.GetNavigableItems parseTree + + cache[document.Id] <- + struct {| + Version = version + Approximate = approximate + Items = items + |} + + match document.FilePath with + | null -> () + | path -> conditionalDirectives[path] <- hasConditionalDirectives parseTree + + items + member _.GetNavigableItems(document: Document) = cancellableTask { let! ct = CancellableTask.getCancellationToken () let! currentVersion = document.GetTextVersionAsync(ct) match cache.TryGetValue document.Id with - | true, struct (version, items) when version = currentVersion -> return items + | true, entry when entry.Version = currentVersion && not entry.Approximate -> return entry.Items | _ -> let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigableItemsCache)) - let items = NavigateTo.GetNavigableItems parseResults.ParseTree - cache[document.Id] <- struct (currentVersion, items) + return store document currentVersion false parseResults.ParseTree + } - match document.FilePath with - | null -> () - | path -> conditionalDirectives[path] <- hasConditionalDirectives parseResults.ParseTree + /// The items of a parse that does not wait for the project's compilation options, for the search that runs while + /// the solution is still loading. A file behind `#if` can be read under the wrong defines, so the entry it leaves + /// behind never answers `GetNavigableItems`. + member _.GetNavigableItemsWhileLoading(document: Document) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let! currentVersion = document.GetTextVersionAsync(ct) - return items + match cache.TryGetValue document.Id with + | true, entry when entry.Version = currentVersion -> return entry.Items + | _ -> + let! parseResults = document.GetFSharpQuickParseResultsAsync(nameof (FSharpNavigableItemsCache)) + return store document currentVersion true parseResults.ParseTree } /// The items of the document's last parse, whatever version they came from. Reads no text, so a /// closed document costs nothing; a caller that needs the items of the current text asks for them. member _.TryGetCachedNavigableItems(documentId: DocumentId) = match cache.TryGetValue documentId with - | true, struct (_, items) -> ValueSome items + | true, entry -> ValueSome entry.Items | _ -> ValueNone /// Whether the file's parse tree, from whichever instance parsed it first, has `#if` directives. @@ -113,8 +144,6 @@ type internal FSharpNavigableItemsCache [); Shared>] type internal FSharpNavigateToSearchService [] (itemsCache: FSharpNavigableItemsCache) = - let getNavigableItems (document: Document) = itemsCache.GetNavigableItems document - /// A multi-targeted project is one Roslyn project per target framework over the same files. The /// first instance in the solution searches every file; the others only the files they alone compile /// and the files whose parse depends on the defines. @@ -211,9 +240,14 @@ type internal FSharpNavigateToSearchService [] (itemsCache let createMatcherFor (searchPattern: string) = itemsCache.CreateMatcherFor searchPattern - let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet) (document: Document) = + let processDocument + (getItems: Document -> CancellableTask) + (tryMatch: NavigableItem -> PatternMatch voption) + (kinds: IImmutableSet) + (document: Document) + = cancellableTask { - let! items = getNavigableItems document + let! items = getItems document let matches = [| @@ -254,27 +288,89 @@ type internal FSharpNavigateToSearchService [] (itemsCache |> ImmutableArray.CreateRange } + let searchProject (project: Project) searchPattern kinds = + cancellableTask { + let tryMatch = createMatcherFor searchPattern + + let! results = + project.Documents + |> Seq.filter (searchedIn project) + |> Seq.map (processDocument itemsCache.GetNavigableItems tryMatch kinds) + // Throttle to avoid launching a parse per document in the project all at once. + |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) + + return results |> Seq.collect _.AsEnumerable() |> Seq.toImmutableArray + } + + /// Priority items first, each half in its original order, as NavigateTo's own service orders its work. + let prioritize isPriority items = + let priority, rest = items |> Seq.toArray |> Array.partition isPriority + [| yield! priority; yield! rest |] + + /// A parse behind the loading search takes a turn on the budget every search shares. + let throttled (work: CancellableTask<'a>) = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + do! SymbolHelpers.searchThrottle.WaitAsync ct + + try + return! work + finally + SymbolHelpers.searchThrottle.Release() |> ignore + } + interface IFSharpNavigateToSearchService with member _.SearchProjectAsync (project, _priorityDocuments, searchPattern, kinds, cancellationToken) : Task> = - cancellableTask { - let tryMatch = createMatcherFor searchPattern - - let! results = - project.Documents - |> Seq.filter (searchedIn project) - |> Seq.map (processDocument tryMatch kinds) - // Throttle to avoid launching a parse per document in the project all at once. - |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) - - return results |> Seq.collect _.AsEnumerable() |> Seq.toImmutableArray - } + searchProject project searchPattern kinds |> CancellableTask.start cancellationToken member _.SearchDocumentAsync(document: Document, searchPattern, kinds, cancellationToken) = - processDocument (createMatcherFor searchPattern) kinds document cancellationToken + processDocument itemsCache.GetNavigableItems (createMatcherFor searchPattern) kinds document cancellationToken member _.KindsProvided = kindsProvided member _.CanFilter = true + + interface IFSharpAdvancedNavigateToSearchService with + member _.SearchCachedDocumentsAsync + ( + _solution, + projects, + priorityDocuments, + searchPattern, + kinds, + _activeDocument, + onResultsFound, + onProjectCompleted, + cancellationToken + ) : Task = + let tryMatch = createMatcherFor searchPattern + let priorityIds = ImmutableHashSet.CreateRange(priorityDocuments |> Seq.map _.Id) + let isPriority (document: Document) = priorityIds.Contains document.Id + + let searchDocumentWhileLoading document = + cancellableTask { + let! results = throttled (processDocument itemsCache.GetNavigableItemsWhileLoading tryMatch kinds document) + + if results.Length > 0 then + do! onResultsFound.Invoke results + } + + let searchProjectWhileLoading (project: Project) = + cancellableTask { + do! + project.Documents + |> Seq.filter (searchedIn project) + |> prioritize isPriority + |> CancellableTask.forEachThrottled Environment.ProcessorCount searchDocumentWhileLoading + + do! onProjectCompleted.Invoke() + } + + projects + |> prioritize (fun project -> project.Documents |> Seq.exists isPriority) + |> Seq.map searchProjectWhileLoading + |> CancellableTask.whenAll + |> CancellableTask.startAsTask cancellationToken diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index d84515dacf6..b744e41554d 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -38,6 +38,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs new file mode 100644 index 00000000000..0f171ded8ca --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchWhileLoadingTests.fs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// Navigate To runs a search of its own while the solution is still loading, and nothing it searches +/// then has its compilation options yet. +module FSharp.Editor.Tests.NavigateToSearchWhileLoadingTests + +open System +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks + +open Xunit + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo + +open FSharp.Editor.Tests.Helpers + +/// A project the project system has not yet handed its command line options: the state every project +/// of the solution is in until it has loaded. +let private loadingProject name source = + let projectId = ProjectId.CreateNewId() + + [ RoslynTestHelpers.CreateDocumentInfo projectId $"C:\\{name}.fs" source ] + |> RoslynTestHelpers.CreateProjectInfo projectId $"C:\\{name}.fsproj" + +let private loadingSolution source = + let project = loadingProject "test" source + let solution = RoslynTestHelpers.CreateSolution [ project ] + project.Id, solution, solution.Projects |> Seq.exactlyOne + +/// One export provider per test: the navigable items are cached in a shared one. +let private searchServices () = + let service: IFSharpNavigateToSearchService = + MefHelpers.createExportProvider().GetExportedValue() + + service, service :?> IFSharpAdvancedNavigateToSearchService + +let private namesFound (results: ImmutableArray) = results |> Seq.map _.Name |> Seq.toList + +/// The loading search as the searcher drives it: results and project completions arrive through callbacks. +/// Returns the names found and how many times a project was reported complete. +let private searchWhileLoading + (service: IFSharpNavigateToSearchService, advanced: IFSharpAdvancedNavigateToSearchService) + (projects: Project list) + pattern + = + task { + let found = ResizeArray() + let completed = ref 0 + + do! + advanced.SearchCachedDocumentsAsync( + (List.head projects).Solution, + ImmutableArray.CreateRange projects, + ImmutableArray.Empty, + pattern, + service.KindsProvided, + null, + (fun results -> + lock found (fun () -> found.AddRange results) + Task.CompletedTask), + (fun () -> + Interlocked.Increment &completed.contents |> ignore + Task.CompletedTask), + CancellationToken.None + ) + + return found |> Seq.map _.Name |> Seq.toList, completed.Value + } + +[] +let ``the loading search finds what the search that waits for the options cannot`` () : Task = + task { + let _, _, project = + loadingSolution "module Sample =\n let declaredWhileLoading = 1\n" + + let (service, _) as services = searchServices () + + let searchAccurately () = + service.SearchProjectAsync(project, ImmutableArray.Empty, "declaredWhileLoading", service.KindsProvided, CancellationToken.None) + :> Task + + let! _ = Assert.ThrowsAnyAsync(fun () -> searchAccurately ()) + + let! names, completed = searchWhileLoading services [ project ] "declaredWhileLoading" + + Assert.Equal([ "declaredWhileLoading" ], names) + Assert.Equal(1, completed) + + // What the loading search left in the cache must not be handed to the accurate search: it was + // read without the project's defines. + let! _ = Assert.ThrowsAnyAsync(fun () -> searchAccurately ()) + () + } + +[] +let ``the loading search reads a file under the wrong defines and does not keep the answer`` () : Task = + task { + let projectId, solution, project = + loadingSolution "#if FOO\nlet fooOnly = 1\n#endif\n" + + let (service, _) as services = searchServices () + + let! namesWhileLoading, _ = searchWhileLoading services [ project ] "fooOnly" + Assert.Equal([], namesWhileLoading) + + { RoslynTestHelpers.DefaultProjectOptions with + OtherOptions = [| "--define:FOO" |] + } + |> RoslynTestHelpers.SetProjectOptions projectId solution + + let! found = service.SearchProjectAsync(project, ImmutableArray.Empty, "fooOnly", service.KindsProvided, CancellationToken.None) + + Assert.Equal([ "fooOnly" ], namesFound found) + } + +[] +let ``every project is reported complete once, whether or not anything is found in it`` () : Task = + task { + let solution = + RoslynTestHelpers.CreateSolution + [ + loadingProject "first" "let found = 1\n" + loadingProject "second" "let other = 2\n" + ] + + let! names, completed = searchWhileLoading (searchServices ()) (solution.Projects |> Seq.toList) "found" + + Assert.Equal([ "found" ], names) + Assert.Equal(2, completed) + }