-
Notifications
You must be signed in to change notification settings - Fork 876
Add modern F# idiom instructions for Copilot / Claude Code #20399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xperiandri
wants to merge
9
commits into
dotnet:main
Choose a base branch
from
xperiandri:modern-fsharp-copilot-instructions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+103
−0
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9ed8d6e
Add modern F# idiom instructions for Copilot and Claude Code
xperiandri fd867cf
Soften task{} guidance in FSharp.instructions.md
xperiandri 2341d00
Prefer one traversal over a chain of transformations
xperiandri 6ed54d6
Correct the string-comparison rule for netstandard2.0 and identifiers
xperiandri f2c30d1
Reserve Seq.cast for untyped input
xperiandri 079c0cc
Note that scoped nowarn needs F# 10
xperiandri daaaf38
Note when a lambda must not be eta-reduced
xperiandri 0404e44
Point at the V-suffixed voption helpers vsintegration already has
xperiandri 6d5b844
Keep voption for fields and cross-thread values, and convert to optio…
xperiandri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| paths: | ||
| - "src/**/*.{fs,fsi,fsx}" | ||
| - "vsintegration/src/**/*.{fs,fsi}" | ||
| - "tests/**/*.{fs,fsi,fsx}" | ||
| --- | ||
|
|
||
| @../../.github/instructions/FSharp.instructions.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| --- | ||
| 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`, `IndexOf` and `Compare`, and an explicit comparer on every `HashSet<string>` and `Dictionary<string, _>`. `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 | ||
|
|
||
| - `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 `[<AutoOpen>]` 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 `[<Struct>]` 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 …`. | ||
| - `[<InlineIfLambda>]` 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; 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. | ||
| - 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). | ||
| - `Seq.cast<Target>` 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<string>` compiles and throws at run time. Retype an already-typed sequence through an upcast the compiler checks instead. | ||
|
|
||
| ## Async and exceptions | ||
|
|
||
| - `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. | ||
|
|
||
| ## 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. | ||
|
|
||
| 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. | ||
| - Extension members consumable from C#: `[<AutoOpen; Extension>]` module, `[<Extension; CompiledName "…">]` 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 (`<see/>`, `<c/>`) need their text wrapped in `<summary>`. | ||
|
|
||
| ## 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. | ||
| - `[<TailCall>]` – 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`` () = …``. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖🕵️ This file also applies to
src/FSharp.Build, which targets F# 9. Nested#warnonproduces FS3350 there. Restrict this rule to F# 10 projects or keep an F# 9-compatible form.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ea440d8: added a note that scoped
#nowarn/#warnonis an F# 10 feature (LanguageFeature.ScopedNowarn, gated atlanguageVersion100), and thatsrc/FSharp.Build(pinned toLangVersion 9) needs suppression at the top of the file instead — that's FS3350 there otherwise.