Repository navigation
Fix type, trivia and Stroustrup type argument layouts for 8.1.0 - #3561
Merged
Merged
Conversation
Since fsprojects#3225, every multiline function type in parentheses indented its `->` lines one extra level, to fix fsprojects#3043. The extra level is only needed in some places. A parenthesis opens an offside context at the first token inside it, and a line starting on that column ends it. After `-> (` or `:> (` at the start of a line, with an indent size of 4, that column is exactly where the `->` lines go. Where the `(` starts its line, as in `type T = (a -> b) list`, one level is enough, which is the 7.0.6 layout. Type.Paren now measures instead. After writing `(` it asks indentPast, the measure-and-step helper from fsprojects#3446, to start the `->` lines past the column right after it. That gives the 7.0.6 layout where it parses, the fsprojects#3043 layout after an outer `->`, and the same layout after `:>`. It also clears that column with an indent size of 2, where the blanket extra level put the `->` lines on it. The column is taken before the trivia after the `(`. A block comment there moves the first token right, but a line that starts left of the first token parses, so the `->` lines stay one level in rather than lining up past the comment. Closes fsprojects#3554
A return type annotation set the indent to the column after `: `. One
level from there lands between multiples of the indent size: with an
indent size of 4, the `->` lines of `: (a -> b -> ...)`, the `->` of
`: unit -> {| ... |}` and the arguments of `: cmap<...>` went to 10.
The indent now stays the binding's, so a type that indents its own
lines lands on a multiple of it, 8 in all three.
A line starting on the column of the `:` ends the return type. A
multiline tuple does not indent its own lines: the lines after its
first element start at the indent, which is now that column. Where a
tuple starts the lines of the type, alone or inside parentheses, a
postfix application, an array, a struct tuple, global constraints or
the first parameter of a function type, AtColumn keeps them at the
column after `: `, where they were before.
When a function type does not fit on its line, its `->` lines go one level in. A tuple after one of them that does not fit either breaks after `*` and starts its next element at the indent, the column of the `->` lines. The element after a line-ending `*` starts a block at its own column, so for a tuple in the middle of the function type, the `->` that follows is read as a new statement and the result does not parse. Inside parentheses, a postfix application or an array it parses, but the tuple's lines still line up with the arrows. A type that a tuple starts after an arrow now goes one level further in, as the last operand too. The check whether a tuple starts the lines of a type is the one a return type already made, now shared by both. A struct tuple type that did not fit on its line started its next element at the indent, left of `struct (` and of the element it continues. It now breaks after `struct (`, puts its elements one level in and its `)` back on the indent of the line it started on. As it places its own lines, the check above no longer counts it as a tuple. A line comment after an arrow was held until the next line break, which came after the type that follows, so the comment moved onto that type and left a blank line behind; before a tuple, it moved after the first element and was not idempotent. It now stays after the arrow, and the type goes on the next line, one level in. Closes fsprojects#3556
Discussions about formatting stalled on two things. "Comment" meant either trivia in the formatted F# or a comment in Fantomas' own source, and questions came as fragments of code that could not be judged without the code base open, wrapped beyond reading in the chat. AGENTS.md now asks agents to say trivia for comments in the F# that Fantomas reads or writes, and explanatory or doc comment for those in our source, and to keep the latter accurate as part of every change. A finding or question about formatting carries a full example: the input, the result before and after the change, what is wrong, and for a decision every option, checked with the SDK compilers. A question that needs code goes into question.fs in the repo root, which the user reads in the editor; .fantomasignore lists it, so the formatter hook and the editor leave its examples as written.
When a trivia line comment follows the arrow of a function type, the type after it already starts on a new line one level past the arrows. A multiline tuple there took a second level for its later elements, which only the tuple on the arrow's own line needs: there, the element after a line-ending `*` would otherwise sit on the column of the next `->`, which does not parse. After the comment, the later elements now stay on the indent, under the first one. The explanatory comments for each branch now show the layout they produce, inside the branch they explain. AGENTS.md: a review's findings that come with F# code, nits included, go into an untracked review.fs, written the same way as question.fs. .fantomasignore lists it, so the formatter leaves its examples alone.
With indent_size = 2, the blanket extra level from fsprojects#3225 put the `->` lines of a parenthesized function type after an outer `->` on the column right after the `(`. A line starting there ends the offside context the parenthesis opens, so the result did not parse. Type.Paren now starts those lines past that column whatever the indent size, since the fix for fsprojects#3554. This adds a case for indent_size = 2, which no case covered, and the changelog entry. Closes fsprojects#3557
A type test that did not fit on one line was broken before its operator, like the other typed expressions, which put `:?` on the column of the expression. `:>`, `:?>` and `:` may start a line there, but for the offside rule `:?` is not an infix token, so the parser read it as the start of a new expression and the result did not compile. The `:?` line now goes one level in, so it breaks the same way as the other operators. A multiline tuple type after any of the four operators put the lines after its first element at the indent, the column of the operator line. That compiled, but read as if the type had ended. Where the type starts with a tuple, it now goes one level in, as it does after the `->` of a function type, which keeps its lines on a multiple of the indent size and, in parentheses after `:>` and `:?`, under the first element. No case covered a type test in an expression, or a tuple after any of these operators; this adds one of each. Closes fsprojects#3558
FANTOMAS-SNOBMATCH-001 only spoke for a two armed match on a boolean. A constant against a wildcard asks the same kind of question, whether the scrutinee equals that constant, and rewrites as mechanically: `if scrutinee = constant then ... else ...`, with the scrutinee still written once. Such a match in the type annotation printer went through review unreported. The rule now reports that shape too, with a message of its own. Unit, two constants without a wildcard, and a constant against a binder stay unreported. Nothing in the analyzed code had the shape, so no existing finding comes with it.
A line comment after the `(` of a parenthesized type waits for the next newline, and the type inside started on the same line, so the comment landed after the first token of the type and what followed went on the next line at the indent. For `Map<string, int>` that split the type name from its arguments and did not compile, a tuple was not idempotent, and a function type got a blank line under the comment. When a line comment follows the `(`, the type now starts on the next line, at the first indent level past the `(`, where the context the parenthesis opens allows it. The comment stays where it was written. Closes fsprojects#3559
A long struct tuple type puts its elements on lines of their own and
its `)` on a line by itself. A comment on its own line after the last
element attaches after that element and ends with a newline of its
own; the printer added another before the `)`, so every format added a
blank line. It now unindents the way the list printer does and adds
that newline only when nothing else has.
The comment attached to the `)` instead when it lined up with the first
element of a struct tuple whose elements share a line. The column
search for a line comment follows only the node it directly follows at
each depth, so earlier content on the comment's column was never
reached. When nothing on that path matches but a later child of the
container starts on the column, the comment now follows the
container's last node before it. What opens the container does not
count: its first child, whatever it is (`[`, `<@`, `begin`, `class`,
`task` of `task {`, `foo` of `foo<...>`), and an opening bracket after
it (`{|` of `struct {|`). A comment lined up with those stays before
the closing token, as on main.
The search now reads as two steps: the candidates are the nodes the
comment directly follows, found from the end of the children, and one
match over those on the comment's column picks the outermost that
starts its line, else the innermost, else the first candidate when
later content of the container lines up. Whether a node starts its
line is read from the source text instead of carrying the end line of
the code before it down every depth. The cursor, which never takes
this path, is attached without going through it.
One existing gold changes: a comment on the column of both the first
element of a list and its `]` now goes with the elements, as it
already did when the matching element was the last one.
Since 8.0.8 the `when` of a match clause is a node of its own, so a comment on its own line between the pattern and `when` attaches before that keyword. Writing that comment ends the line, and `when` then started the next one on the column of the `|`, where it ends the clause: the result did not parse. With a comment before `when`, the comment, the guard and the rest of the clause now go one level in. The comment is written before the guard is measured, so its lines do not make the guard count as multiline and the arrow stays on the guard's line when the guard fits. Closes fsprojects#3560
Release the fixes of this branch as 8.1.0, a minor release because two of its changes alter how valid code is formatted. Neither has an issue behind it, so their entries link the pull request: a multiline tuple type after `:?`, `:>`, `:?>` or `:` goes one level in, and a line comment lined up with the first element of its container stays with the elements instead of moving before the closing bracket.
8.1 fixes layouts that did not compile or were not idempotent, and fixing them at the cause also moves a few layouts of code that was already valid. The upgrade guide gets a v8.1 section, so someone who sees their code change after upgrading finds out why without reading the pull request: a function type in parentheses goes one level in rather than two, a return type lands on a multiple of the indent size, a long struct tuple type is written like a long list, a tuple type after a cast or annotation operator goes one level in, and a comment lined up with the elements of a list stays with them. Each comes with the output of 8.0 and of 8.1 for the same input. Several of those choices follow one preference that was written down nowhere: when more than one layout parses, every line starts on a multiple of indent_size. The new end-user page, The rule of multitudes, gives the reasons (both style guides, and alignment that does not depend on name length), how it works where the parser needs a line past a column, and the layouts where it does not hold yet. Design decisions lists it, the upgrade guide links to it, and AGENTS.md points agents to Design decisions when they choose or recommend a layout. ReportingBugs moves one place down to make room.
With Stroustrup, a type application whose only type argument is an
anonymous record printed it as an indented list item, with the closing
`>` on a line of its own and a space before it. It now stays attached,
`convertTo<{|`, fields, `|}>`, for expressions and types alike, as it
was before 6.3.0.
genPrefixApp makes the choice, since it already holds the padding that
keeps a `>` starting a line right of the identifier, where it cannot be
read as the comparison operator. It takes the type arguments as a list
and uses the same rule as every other place that attaches an anonymous
record type: isStroustrupStyleType and canSafelyUseStroustrup, now in
Context.fsi, decided once `<` is written so a comment after it counts.
With trivia before `>`, the list layout stays, so the `>` always
follows `|}` and needs no padding.
This is the design @mmabdpr proposed in fsprojects#3525, after the first version
there was reviewed.
Closes fsprojects#3069
Closes fsprojects#2860
Co-authored-by: Mohammad Mahdi Abdollahpour <git@mohammadmahdi.com>
With fsharp_multiline_bracket_style = stroustrup, a type application
whose only type argument is an anonymous record now keeps `<{|` and
`|}>` together, where 8.0 put the record on lines of its own and the
closing `>` on a line by itself. Users of that setting will see their
code change, so the v8.1 section shows the layout of 8.0 and of 8.1 for
the same input. The section no longer says both ideas in its
introduction are behind every change, as this one follows neither.
Contributor
Author
A function type in parentheses steps its `->` lines past the `(`, but when it started with a tuple, the tuple's lines fell back to the column of the `(`. Main put them one level in through the extra level every parenthesized function type used to get. The parenthesized type now applies one rule to a function type and to a type after a trivia line comment: the lines the type indents clear the first token after the `(`, and the lines at the type's own indent, a leading tuple's or the first after the comment, clear the `(` itself. indentPast measures from the current indent, so a tuple an `->` already put a level in gets nothing more. Other parenthesized types keep their layout. A return type on its own line keeps the binding's indent and goes one level in. With an indent size of 2, `: ` is exactly one level wide, so that level is the column the type starts on: the type arguments of `: Dictionary<` and the `->` lines of a function type started under the type name, with the closing `>` right of them. With an indent size of 2, the type now indents from where it starts, as in 8.0; the upgrade guide says so. Layouts checked by hand at an indent size of 2 and in nested parentheses are now cases, and AGENTS.md asks for that from now on.
nojaf
marked this pull request as ready for review
October 21, 2026 19:25
This was referenced Oct 22, 2026
This was referenced Oct 22, 2026
Merged
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fantomas 8.1.0 fixes six layouts where the formatted code did not compile, or changed again on the next format (#3554, #3556 to #3560), and brings back the Stroustrup layout of a single anonymous record type argument (#3069, #2860). Fixing them at the cause, rather than with a special case each, also moves a few layouts of code that was already valid. If your code changed after updating to 8.1.0, this is why.
Why your code changed
Two ideas are behind every change below:
indent_size. 8.0 added extra indentation in places to keep the compiler happy, and some of it landed on columns like 10 or 14 with an indent size of 4. 8.1.0 measures where the parser needs the next line instead, and stays on the indentation grid.The upgrade guide (
docs/docs/end-users/UpgradeGuide.md, v8.1) has every change below, anddocs/docs/end-users/RuleOfMultitudes.mdthe reason behind the first idea.Every example below is real output for the same input, from 8.0.10 and from 8.1.0, with the default settings.
A function type in parentheses
The
->lines go one level in, not two. Since 8.0 (#3225, for #3043), every multiline function type in parentheses got an extra level. It is only needed after an outer->, where it stays. Elsewhere this is the 7.0.6 layout again. A function type that starts with a tuple keeps the two levels of 8.0: the lines of the tuple take the first level, so the->lines go one past them. (#3554)A return type annotation
The lines of a return type no longer hang off the column after
:. They go one level past the binding, so they land on a multiple of the indent size. Withindent_size = 2,:is itself one level wide, so there the lines are measured from the type, as in 8.0.A long struct tuple type
The elements go on lines of their own, one level in, and the
)back on the indent of the line it started on, the way a long list is written. Before, the elements after the first started left ofstruct (, at a column the parser only accepts in some places. In the middle of a function type, the same shape did not compile (#3556).A tuple type after
:>,:?>,:or:?The lines after the first element go one level in. Before, they went back to the column of the operator line and read as if the type had ended. This is what a tuple after the
->of a function type does as well.A line comment before a closing bracket
A comment written on the column where the elements of a list start now stays with the elements, also when the closing
]is on that column too. Before, it moved before the]. A comment on the column of the opening bracket still stays with the brackets.A single anonymous record type argument with Stroustrup
With
fsharp_multiline_bracket_style = stroustrup, a type application whose only type argument is an anonymous record keeps<{|and|}>together, the way Stroustrup keeps the bracket of a record at the end of the line before it. Since 6.3.0 the record went on lines of its own, with the closing>on a line by itself (#3069). This is also the layout #2860 asked for, which came back with a space in|} >at the time. With more than one type argument, the arguments still go on lines of their own.alignedandcrampedare untouched.This follows the design @mmabdpr worked out in #3525, ported here in a smaller form and credited as co-author.
Fixed
Each of these gave code that did not compile, or that changed again on the next format:
:?gives invalid code (Long type test with :? gives invalid code #3558).These two compiled, but lost the Stroustrup layout:
>of an anonymous record type argument with Stroustrup (Space inserted before final > in type parameter #2860): the record stays attached as<{|and|}>.Closes #3554
Closes #3556
Closes #3557
Closes #3558
Closes #3559
Closes #3560
Closes #3069
Closes #2860