Skip to content

Update FCS - #3400

Merged
nojaf merged 11 commits into
fsprojects:mainfrom
nojaf:update-fcs
Aug 21, 2026
Merged

Update FCS#3400
nojaf merged 11 commits into
fsprojects:mainfrom
nojaf:update-fcs

Conversation

@nojaf

@nojaf nojaf commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Bump the vendored compiler sources from ab1f6cea to d05075e098278aedcea3379159504d664628a495, walking one upstream commit at a time rather than jumping, so a broken build is attributable to a single change.

Nineteen upstream commits touching src/Compiler/SyntaxTree are crossed. Most were clean bumps. Four needed real work.

[<return: Struct>] was being silently deleted

dotnet/fsharp#19738 made mkSynBinding move a [<return: ...>] attribute written in front of a binding out of SynBinding.attributes and into the arity information. Fantomas only reads the binding's attribute list, so those attributes were parsed and then never printed.

Partial active patterns marked [<return: Struct>] are the common case, and the Fantomas code base itself has 34 of them, so self-formatting would have removed every one. The existing tests only covered the return type annotation form, let f x : [<return: Attribute>] int = x, so the entire suite stayed green while source was being dropped. restoreRotatedReturnAttributes puts them back in the list they were written in, leaving annotation attributes alone since the return type node already prints those.

I raised the layering question upstream on dotnet/fsharp#19738: the rotation used to live in the type checker and patched a local copy, so the parse tree stayed faithful to the source. Moving it into the parser makes the untyped tree report no attributes for source that visibly has one, and it is lossy, [<return: A; return: B>] and [<return: A>][<return: B>] now produce identical trees.

Record spreads

dotnet/fsharp#18927, RFC FS-1151. New syntax in three tree positions, so Oak and CodePrinter needed real support:

{ ...source; Field = value }                  // record expression
{| ...source; Field = value |}                // anonymous record expression
type Target = { ...Source; Field: int }       // record type definition, also in signature files

Oak mirrors the upstream shape with two unions, ExprRecordFieldOrSpread and TypeDefnRecordFieldOrSpread, carrying ExprSpreadNode and TypeSpreadNode. Simplified where we can: nominal and anonymous record items keep sharing RecordFieldNode, which upstream splits in two, and the block separator is dropped since the printer decides separators itself. ExprRecordBaseNode.HasFields became HasItems, because a record holding only a spread has no fields but is not empty.

52 tests in a new RecordSpreadTests.fs cover the three positions in implementation and signature files, spreads before, between and after fields, multiple spreads, literals and applications and property gets as sources, struct anonymous records, Stroustrup, and comments attached to a spread.

Interpolated string alignment

dotnet/fsharp#19971 replaced FillExpr's qualifiers: Ident option with a SynInterpolationFormatting that models alignment and printf specifiers separately. {x,10} used to arrive as a tuple inside the fill expression, which is why Fantomas prints {x, 10}.

Absorbed entirely in ASTTransformer so the printer and the formatted output do not move: the alignment is folded back into the expression as the tuple the parser used to hand us. A printf specifier no longer appears in the preceding string part's text, so the fallback used when there is no source text re-appends it, otherwise %d{x} prints as {x} when formatting from an AST.

Alignment had no test coverage at all, which is what would have made this silent. There are four now, including one through the AST path.

$"{value,-10}" previously failed to parse and now formats.

The SR generator had to be vendored

dotnet/fsharp#20097 made diagnostic messages carry classified text, so every SR accessor generated from FSComp.txt returns RichText instead of string. The task that generates them, FSharpEmbedResourceText, ships in the .NET SDK and is older than the sources we vendor, so it cannot emit the classified accessors and the compiler no longer builds against it. Upstream avoids this by bootstrapping its own FSharp.Build.

We vendor that one task file instead and compile it in a new Fantomas.FCS.BuildTasks project. FSComp.txt moves out of EmbeddedText into an item of our own, which keeps the SDK's generator away from it, and a target mirroring the SDK's GenerateFSharpTextResources runs the vendored task over it. The namespace rewrite that already turns FSharp.Compiler into Fantomas.FCS on download also rewrites the namespace the task emits, so the generated SR opens Fantomas.FCS.Text without any extra machinery.

This is build tooling only. The published packages are unchanged.

Also

Each step was probed by hand with scripts/format.fsx, not just run against the suite, because the failure mode of a parser shape change is silently dropped source that no existing assertion notices. Two commits made the parser accept a layout it previously rejected, and Fantomas deliberately does not adopt either, since the vendored compiler runs ahead of the compiler in a published SDK and output only the newer one accepts would not compile for users.

.claude/commands/update-fcs.md documents the walk itself, including the traps found along the way.

nojaf added 11 commits August 21, 2026 13:02
Bump the vendored compiler sources from ab1f6cea to
9487d36e776a5c7315f04a6ddf2887f92ab60d49, spanning three upstream commits
that touch src/Compiler/SyntaxTree:

  0abb33dc  Address signature generation bugs (#19586)
  cd1d1804  Address additional signature generation roundtrip bugs (#19609)
  9487d36e  Fix #17904 and #19020 (#19738)

The last of those makes mkSynBinding move a `[<return: ...>]` attribute
written in front of a binding out of the binding's attribute list and into
the arity information. Fantomas reads attributes from SynBinding only, so
those attributes were parsed and then never printed, silently deleting them
from the formatted output. Partial active patterns marked
`[<return: Struct>]` are the common case, and the Fantomas code base itself
relies on them. The existing tests only covered the return type annotation
form, `let f x : [<return: Attribute>] int = x`, so the whole suite stayed
green while source was being dropped.

restoreRotatedReturnAttributes puts those attributes back in the attribute
list they were written in. Attributes written on a return type annotation
end up in the arity information as well, and are left alone there, because
the return type node already prints them.
Bump the vendored compiler sources to
f4b785f189aedc4a0f1ec22182e3653a0b9dd142 and support the record spread
syntax it introduces, RFC FS-1151, dotnet/fsharp#18927.

A spread reaches the syntax tree in three places, and each one changed
shape:

  SynTypeDefnSimpleRepr.Record   now holds SynFieldOrSpread
  SynExpr.Record                 now holds SynExprRecordFieldOrSpread
  SynExpr.AnonRecd               now holds SynExprAnonRecordFieldOrSpread

Oak mirrors that with two unions, ExprRecordFieldOrSpread for the
expression forms and TypeDefnRecordFieldOrSpread for the record
representation of a type definition, carrying ExprSpreadNode and
TypeSpreadNode. Nominal and anonymous record items keep sharing
RecordFieldNode, which upstream splits in two, and the block separator is
left behind since the printer decides separators itself.

The spread source is an arbitrary expression, the same grammar as the
right-hand side of a field, so it can be multiline. Nothing may come
between the dots and the source, which means a spread has no break to
decide and prints as the dots followed by the expression. That keeps the
existing cramped and aligned field printers untouched.

ExprRecordBaseNode.HasFields becomes HasItems, since a record holding only
a spread has no fields but is not empty, and the value decides whether a
separator follows an inherit or copy-from clause.

Also use the range the parser now provides on SynExprAnonRecordField
instead of reconstructing it from the field name and the value.

Add RecordSpreadTests.fs covering the three positions in implementation
and signature files, spreads before, between and after fields, multiple
spreads, record and anonymous record literals, applications and property
gets as sources, struct anonymous records, Stroustrup, and comments
attached to a spread.
Bump the vendored compiler sources to
d3403caee0e62ae3a64964f19fc93f20a50d6aae, dotnet/fsharp#19971.

The second field of SynInterpolatedStringPart.FillExpr changed from
`qualifiers: Ident option` to a SynInterpolationFormatting, which models
the alignment of `{x,10}` and the specifier of `%d{x}` separately instead
of leaving them inside the fill expression and the preceding string part.

Nothing is lost from the tree, it carries more than before, but Fantomas
read the old locations. Absorb the change in ASTTransformer so the printer
and the Oak model stay as they are, and so the formatted output does not
move. The alignment is folded back into the expression as the tuple the
parser used to hand us, which is why `{x, 10}` keeps its existing space.
The comma has no node of its own, its range is the gap between the
expression and the alignment.

A printf specifier no longer appears in the text of the preceding string
part, though that part's range still covers it. The source text path is
therefore unaffected, but the fallback used when there is no source text
has to look ahead and re-append the specifier, otherwise `%d{x}` prints as
`{x}` when formatting from an AST.

Negative alignment, `$"{value,-10}"`, previously failed to parse and now
formats.

Add tests for alignment in string interpolation, which nothing covered,
including one that goes through the AST rather than the source text so the
reconstructed comma is exercised.
Bump the vendored compiler sources to
ea778bb414648883fca4219bf7a69286bf82dd6b, dotnet/fsharp#20106.

Non-strict indentation is gone from the lexer, so the `strictIndentation`
parameter disappears from the lexbuf constructors in UnicodeLexing,
ParseHelpers and prim-lexing, along with its language feature flag. Drop
the argument at the one call site.

Formatting is unaffected. Fantomas already passed `Some true` there, so it
had opted into strict indentation, which is exactly the mode that survives
upstream.
Bump the vendored compiler sources to
5260b68c6db2a56b973a0fba0d614cadf3599682, dotnet/fsharp#19186.

The commit adds SyntaxTree/XmlDocIncludeExpander.fs and its signature, and
XmlDoc.fs now calls into them, so both vendored file lists need the pair.
Neither list globs, so a new upstream file has to be added by hand to the
Init download list and to the Compile items, in the same position upstream
compiles it, between UnicodeLexing and XmlDoc.

Formatting is unaffected. XmlDoc gained an expansion of <include> tags used
while validating a doc comment, and that validation reads files from disk,
but Fantomas asks for the doc comment without checking it, so the expander
is never invoked.
Bump the vendored compiler sources from 5260b68c to
afe45bfeda02e5eab2b9e8a959174a95fdf9ae5e, spanning two upstream commits
that touch src/Compiler/SyntaxTree:

  34a3053b  Allow closing '>' of multiline nested type arguments to align
            with the opener (#20003)
  afe45bfe  Remove always-on WildCardInForLoop language feature flag
            (#20221)

The first makes the lexer treat a closing '>' as a sequence block element
continuator, so a type argument list may close on a line aligned with its
opener. That widens what parses and changes nothing about what Fantomas
prints. The vendored compiler runs ahead of the compiler in a published
SDK, so output that only the newer one accepts would not compile for
users. Adopting the layout is a separate decision for later.

The second drops a language feature flag that was already always on, which
removes the parseState parameter from idOfPat and makes its wildcard case
unconditional. Both ParseHelpers and the pars.fsy call site are vendored,
so they move together.

Add a test for the wildcard identifier in a for loop, `for _ = 1 to 10`.
It is the syntax the second commit is about and nothing in the suite
covered it, so it was passing on luck rather than on a guarantee.
Bump the vendored compiler sources from afe45bfe to
d05075e098278aedcea3379159504d664628a495, spanning three upstream commits
that touch src/Compiler/SyntaxTree:

  208b7b4b  Add symbol and type highlighting to F# diagnostics (#20097)
  d26c842f  Remove always-on RelaxWhitespace language feature flag (#20226)
  d05075e0  Parser: recover on missing 'when' conditions (#20071)

The first of those makes diagnostic messages carry classified text, so
every SR accessor generated from FSComp.txt returns RichText instead of
string. The task that generates them, FSharpEmbedResourceText, ships in
the .NET SDK and is older than the sources we vendor, so it cannot emit
the classified accessors and the compiler no longer builds against it.
Upstream avoids this by bootstrapping its own FSharp.Build.

Vendor that one task file instead and compile it in a new
Fantomas.FCS.BuildTasks project. FSComp.txt moves out of the EmbeddedText
item group into one of our own, which keeps the SDK's generator away from
it, and a target mirroring the SDK's GenerateFSharpTextResources runs the
vendored task over it. RichText and the sources it needs are marked
CompileFirst so they precede the generated file, matching what upstream
does in its own project.

The namespace rewrite that already turns FSharp.Compiler into Fantomas.FCS
on download also rewrites the namespace the task emits, so the generated
SR opens Fantomas.FCS.Text. The task itself is renamed to
Fantomas.FCS.Build so it cannot be confused with the one in the SDK.

Parse.fs reads the text back out of the diagnostics it maps to Fantomas
diagnostics, since those are reported as plain strings. Only numbered
diagnostics became rich, unnumbered messages are still strings.

Formatting is unaffected. Layout, indentation, when clauses and the text of
parse errors were all checked against the previous hash.
@nojaf
nojaf marked this pull request as ready for review August 21, 2026 13:42
@nojaf nojaf changed the title Update FCS to 'Fix #17904 and #19020', commit 9487d36e Update FCS Aug 21, 2026
@nojaf
nojaf merged commit 2aed98e into fsprojects:main Aug 21, 2026
27 checks passed
@nojaf
nojaf deleted the update-fcs branch August 21, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant