-
-
Notifications
You must be signed in to change notification settings - Fork 205
Description
RFC: How should Fantomas handle lambda expressions that would change semantics on a single line?
Background: how Fantomas works
Fantomas is not a "find-and-replace" tool that tweaks whitespace in your source file. It works more like a Word document: Fantomas re-types your entire source code from scratch according to its rules. It parses your code into an abstract syntax tree (AST), and then prints it back out following the configured style guide. The original text is gone — only the structure and semantics are preserved.
This is a powerful model because it guarantees complete consistency. But it also means Fantomas carries a heavy responsibility: the re-printed code must have the same meaning as the original. If Fantomas changes the structure while collapsing code to a single line, it's a bug.
The problem: fun extends to the end
In F#, a fun lambda captures everything to the right as its body. This is a language-level rule, not a formatting preference:
// These two are NOT the same:
fun x -> x + 1 |> g // means: fun x -> (x + 1 |> g)
(fun x -> x + 1) |> g // means: apply g to the lambdaThis creates a specific class of bug in Fantomas. When multiline code gets collapsed onto a single line, the lambda body can swallow tokens that were originally separate expressions. The same applies to if/then/else expressions.
Here are the known instances of this problem:
1. Infix operator with lambda on the left-hand side (#3274)
// Input (multiline, unambiguous):
let a =
fun x -> {| X = x |}
<*| op
// Collapsed to single line (WRONG — changes semantics):
let a = fun x -> {| X = x |} <*| op
// Parser sees: fun x -> ({| X = x |} <*| op)2. Record with lambda in a non-last field (#3246)
// Input (multiline, unambiguous):
let test () : Rec =
{
A = 1
B = fun x -> x + 1
C = 3
}
// Collapsed to single line (WRONG — changes semantics):
let test () : Rec = { A = 1; B = fun x -> x + 1; C = 3 }
// Parser sees: { A = 1; B = fun x -> (x + 1; C = 3) }3. Tuple with lambda in a non-last position (#3278)
// Input (multiline, unambiguous):
let x =
[
1, fun () -> 1
1, fun () -> 1
]
// Collapsed to single line (WRONG — changes semantics):
let x = [ 1, fun () -> 1; 1, fun () -> 1 ]
// Parser sees the first lambda swallowing everything after itThe core tension
Fantomas needs to handle the case where collapsing to a single line would make a lambda (or if/then/else) swallow subsequent tokens. There are exactly two valid approaches, each with a clear trade-off.
Option A: Add parentheses
Collapse to a single line, but wrap the lambda in parentheses to delimit its body:
// Record:
let test () : Rec = { A = 1; B = (fun x -> x + 1); C = 3 }
// Infix:
let a = (fun x -> {| X = x |}) <*| op
// Tuple:
let x = [ (1, fun () -> 1); (1, fun () -> 1) ]Pro: More compact output. Expressions that fit on one line stay on one line.
Con: Introduces parentheses that were not in the original source. Fantomas is changing your code style to make the single-line form safe.
Option B: Stay multiline
If collapsing would create an ambiguous lambda, don't collapse — keep the multiline layout:
// Record:
let test () : Rec =
{
A = 1
B = fun x -> x + 1
C = 3
}
// Infix:
let a =
fun x -> {| X = x |}
<*| op
// Tuple:
let x =
[
1, fun () -> 1
1, fun () -> 1
]Pro: Never introduces syntax that wasn't in the original source. The multiline form is already unambiguous.
Con: Some short expressions won't collapse to a single line even when they technically could (with parens).
Current state
Fantomas currently uses a mix of both approaches, which is inconsistent:
| Context | Current approach |
|---|---|
Pipe operators (|>, >>) with lambda LHS |
Stay multiline |
Chained infix apps (Expr.SameInfixApps) |
Add parens |
| Record non-last fields | Add parens |
| Tuple non-last elements | Add parens |
| Other infix operators with lambda LHS | Not yet handled (bug) |
Important: this is not about preserving the original source
To be clear: neither option means "keep what the user wrote." That is not how Fantomas works. Fantomas re-types your entire source from scratch — it does not selectively preserve parts of the original formatting. When working with multiple people on the same code base, you should not be able to distinguish who wrote the code. Formatting is an acceptance criterion, not a personal preference.
Both options produce deterministic, consistent output. The question is strictly about which deterministic output Fantomas should produce when a lambda would be ambiguous on a single line.
What we're asking
This is not a general formatting style discussion — we're not debating how code should look. This is a correctness constraint imposed by the F# language: fun extends to the end of the enclosing expression, and Fantomas must account for that.
The question is purely: when Fantomas must choose between adding parentheses or staying multiline to preserve semantics, which should it do?
We want to pick one consistent approach and apply it everywhere. Since we're currently in an alpha release cycle, we have the opportunity to align all code paths.
Timeline: This decision is blocking the v8 stable release. We need to settle this promptly — if there is no clear community consensus within two weeks, the maintainers will make the call.
Please vote by reacting on the corresponding comment below: 🚀 for add parentheses, 🎉 for stay multiline.