From 9ed8d6e4ce7b0eff5cb89c258bcd88d4c0692ff9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 30 Aug 2026 22:38:52 +0200 Subject: [PATCH 1/9] 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 fd867cf1dacf10b2e804f27be093c6fb6bc0e39a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:14:14 +0200 Subject: [PATCH 2/9] 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 --- .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 2341d00bf610a7b00322d4f241836f434d4ca966 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 4 Sep 2026 18:19:35 +0200 Subject: [PATCH 3/9] 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) --- .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 6ed54d64bd1063fe67ef48cc8d4d29f03d3ecbc9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 17:35:52 +0200 Subject: [PATCH 4/9] Correct the string-comparison rule for netstandard2.0 and identifiers Contains has no StringComparison overload on netstandard2.0 - src/ has no such call and uses IndexOf(value, Ordinal) >= 0 instead. Identifiers are case-sensitive, so OrdinalIgnoreCase was wrong guidance for them. Co-Authored-By: Claude Opus 5 --- .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 3d2f0ad2790..1a7ffd07ecd 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -20,7 +20,8 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp - 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. +- An explicit `StringComparison` on every `Equals`, `StartsWith`, `EndsWith`, `IndexOf` and `Compare`, and an explicit comparer on every `HashSet` and `Dictionary`. `Ordinal` by default; culture-sensitive comparison is a decision, never a default. Reach for `OrdinalIgnoreCase` only where the thing compared really is case-insensitive – never for identifiers, which are case-sensitive. +- `Contains` has no `StringComparison` overload on `netstandard2.0`. Spell it `text.IndexOf(value, StringComparison.Ordinal) >= 0`, as `XmlDocInheritance.fs` does. ## Values and types From f2c30d10556bb60faa955a2fed4d798827de165e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 17:36:03 +0200 Subject: [PATCH 5/9] Reserve Seq.cast for untyped input Seq.cast takes a non-generic IEnumerable and unboxes per item, so it is unchecked: [1; 2; 3] |> Seq.cast compiles and throws at run time. Every Seq.cast in src/ already sits on an untyped source; none of them retype a typed sequence, which is what the rule used to recommend. Co-Authored-By: Claude Opus 5 --- .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 1a7ffd07ecd..a7389400334 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -39,7 +39,7 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp - 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)`. +- `Seq.cast` is for genuinely untyped input – a non-generic `IEnumerable` such as `MatchCollection` or `XmlNodeList` (`ServiceParsedInputOps.fs`, `fsihelp.fs`). It unboxes per item and is unchecked: `[ 1; 2; 3 ] |> Seq.cast` compiles and throws at run time. Retype an already-typed sequence through an upcast the compiler checks instead. ## Async and exceptions From 079c0cc81892e35fcb51d8ef70338fdc69620ae9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 17:36:18 +0200 Subject: [PATCH 6/9] Note that scoped nowarn needs F# 10 LanguageFeature.ScopedNowarn maps to languageVersion100, and this file also governs src/FSharp.Build, which is pinned to LangVersion 9 - a mid-file #warnon is FS3350 there. Co-Authored-By: Claude Opus 5 --- .github/instructions/FSharp.instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/instructions/FSharp.instructions.md b/.github/instructions/FSharp.instructions.md index a7389400334..165f2e2a410 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -61,6 +61,8 @@ Enabled in `src/Compiler`, `src/FSharp.Build`, `src/FSharp.Compiler.LanguageServ 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. +Scoped `#nowarn`/`#warnon` is an F# 10 feature (`LanguageFeature.ScopedNowarn`). `src/FSharp.Build` is pinned to `LangVersion 9`, where the same pair is FS3350 – there, suppression stays at the top of the file. + ## 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. From daaaf38b95cf9e7bc8bab11e04751db6c877ab4a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 17:36:32 +0200 Subject: [PATCH 7/9] Note when a lambda must not be eta-reduced Per @smoothdeveloper: CompilerImports.fs keeps an explicit lambda because the Type Provider SDK reflects over the closure and needs the captured field named tcImports; a method group would synthesize the receiver name. Co-Authored-By: Claude Opus 5 --- .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 165f2e2a410..4f4d01fccc5 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -35,7 +35,7 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp ## 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`. +- Eta-reduce: `Seq.map (fun x -> someFunction x)` must become `Seq.map someFunction` – unless the lambda is load-bearing. A method group names its captured receiver with a synthesized name that varies by optimization setting, so a closure something reflects over needs the explicit lambda (the Type Provider SDK's `tcImports` capture in `CompilerImports.fs`). - 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). From 0404e4402d1d9012ab03fa5a79be818942980dc6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 21:45:15 +0200 Subject: [PATCH 8/9] Point at the V-suffixed voption helpers vsintegration already has FSharp.Editor carries voption-returning counterparts of the FSharp.Core collection functions (Seq.tryFindV, Array.tryPickV, List.tryFindV, ...) in Common/Extensions.fs, and they are easy to miss: the module is [], so nothing at a call site names it. Without this, generated code reaches for the option-returning original and then converts, which is exactly what the voption rule above is trying to avoid. Also drops Seq.tryHead/List.tryFind as the examples of "an API hands you 'T option" in that rule - both do have a V counterpart in vsintegration, so they contradicted the new bullet. Co-Authored-By: Claude Opus 5 (1M context) --- .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 4f4d01fccc5..7f4cf66d6b9 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -25,7 +25,8 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp ## 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. +- `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` and has no `voption` counterpart, unwrap with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules. +- `vsintegration` has `voption`-returning counterparts of the FSharp.Core collection functions, suffixed `V`, in `FSharp.Editor/Common/Extensions.fs`: `Seq.tryHeadV`/`tryFindV`/`tryFindIndexV`/`tryPickV`/`chooseV`, `Array.tryHeadV`/`tryFindV`/`tryPickV`/`chooseV`, `List.tryFindV`, `ImmutableArray.tryHeadV`. Reach for those rather than the `option`-returning original. The module is `[]` and compiles before the rest of `FSharp.Editor`, so a file in the `Microsoft.VisualStudio.FSharp.Editor` namespace needs no `open` for them. `src/Compiler` has no equivalents. - `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`. From 6d5b8448fb3c16d52c6cae333245404b50389119 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 12:32:24 +0200 Subject: [PATCH 9/9] Keep voption for fields and cross-thread values, and convert to option last Co-Authored-By: Claude Opus 5 (1M context) --- .github/instructions/FSharp.instructions.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/instructions/FSharp.instructions.md b/.github/instructions/FSharp.instructions.md index 7f4cf66d6b9..60a6a053c08 100644 --- a/.github/instructions/FSharp.instructions.md +++ b/.github/instructions/FSharp.instructions.md @@ -25,7 +25,8 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp ## 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` and has no `voption` counterpart, unwrap with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules. +- `voption` – `ValueSome`/`ValueNone` – over `option`; it is this compiler's option type. Fields, members, parameters and values shared between threads included: none of them is a reason to pick `option`. Exception: when an API hands you `'T option` and has no `voption` counterpart, unwrap with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules. +- The mirror case, an API that *takes* `'T option` (an optional argument `?caret = …`, a field typed `IDisposable option`): stay in `ValueOption` through the whole chain and convert once, last – `x |> ValueOption.bind _.Position |> ValueOption.toOption`, never `x |> ValueOption.toOption |> Option.bind _.Position`. - `vsintegration` has `voption`-returning counterparts of the FSharp.Core collection functions, suffixed `V`, in `FSharp.Editor/Common/Extensions.fs`: `Seq.tryHeadV`/`tryFindV`/`tryFindIndexV`/`tryPickV`/`chooseV`, `Array.tryHeadV`/`tryFindV`/`tryPickV`/`chooseV`, `List.tryFindV`, `ImmutableArray.tryHeadV`. Reach for those rather than the `option`-returning original. The module is `[]` and compiles before the rest of `FSharp.Editor`, so a file in the `Microsoft.VisualStudio.FSharp.Editor` namespace needs no `open` for them. `src/Compiler` has no equivalents. - `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. @@ -35,7 +36,7 @@ When the IDE's F# semantic tools are unavailable, use the `F#` MCP server (`.mcp ## 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. +- 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; a member chain ending in a method call is not complex: `_.LineChanged.Subscribe(handler)`. The shorthand needs its input type known, so pipe the value in first – `position |> ValueOption.map _.Line`, not `ValueOption.map _.Line position` (FS0072). 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` – unless the lambda is load-bearing. A method group names its captured receiver with a synthesized name that varies by optimization setting, so a closure something reflects over needs the explicit lambda (the Type Provider SDK's `tcImports` capture in `CompilerImports.fs`). - 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.