Skip to content

Fix let-in scoping in light syntax via LexFilter change#19501

Open
Copilot wants to merge 15 commits intomainfrom
copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17
Open

Fix let-in scoping in light syntax via LexFilter change#19501
Copilot wants to merge 15 commits intomainfrom
copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

Addresses the long-standing issue where let ... in with an explicit in keyword in indentation-sensitive F# code causes the parser to greedily capture all subsequent lines as part of the let body, leading to unexpected scoping behavior.

Problem

do
    let x = 1 in x + 1
    x  // incorrectly becomes part of the let body

The parser creates LetOrUse(let x = 1, Sequential(x + 1, x)) instead of the expected Sequential(LetOrUse(let x = 1, x + 1), x), causing x on the second line to resolve to the local binding rather than the outer scope.

Changes Made

  • New language feature: Added LanguageFeature.WarnOnLetInSequenceExpression in LanguageFeatures.fs/fsi, gated at F# 11.0
  • LexFilter change: In LexFilter.fs, when blockLet=true and an explicit IN token is encountered with the body expression starting on the same line as in, a new CtxtSeqBlock with AddBlockEnd is pushed. This creates an OBLOCKBEGIN/OBLOCKEND wrapper around the body, limiting its scope via indentation — tokens on the next line at a lower column are offside and close the block, preventing greedy capture.
  • Intentional multi-line preserved: When the body starts on the next line after in (e.g., chained let ... in / let! ... in / and! patterns), standard behavior is preserved.
  • Regression tests: 4 tests in WarnExpressionTests.fs covering scoping in do blocks, function bodies, no change for single-line usage, and no change on older language versions
  • SyntaxTree baseline: 1 baseline updated for minor range change
  • Release notes: Documented in 11.0.100.md

Testing

  • ✅ All 4 regression tests pass
  • ✅ All 1042 ParseFile SyntaxTree tests pass (1 baseline updated)
  • ✅ All 2129 FSharp.Compiler.Service tests pass
  • ✅ All 524 ErrorMessages tests pass
  • ✅ All 630 BasicGrammarElements tests pass
  • ✅ All 776 Language tests pass
  • ✅ SurfaceAreaTest passes
  • ✅ CodeQL reports no security issues

💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

@T-Gro
Copy link
Copy Markdown
Member

T-Gro commented Mar 27, 2026

@copilot : Try again

@T-Gro
Copy link
Copy Markdown
Member

T-Gro commented Mar 27, 2026

@copilot : Try again.

@abonie
Copy link
Copy Markdown
Member

abonie commented Mar 31, 2026

@copilot Try again

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot Try again

Implemented in 4c00982 and 6259b63. Added warning FS3885 (gated behind LanguageFeature.WarnOnLetInSequenceExpression at F# 11.0) that fires when let ... in with an explicit in keyword has a body that extends to subsequent lines — the common pitfall from issue #7091 where the parser greedily captures all subsequent lines as part of the let body.

Changes:

  • New LanguageFeature.WarnOnLetInSequenceExpression in LanguageFeatures.fs/fsi
  • Warning FS3885 message in FSComp.txt
  • Detection in CheckExpressions.fs: checks for explicit in keyword + Sequential body extending past the in line
  • 4 regression tests (warning in do block, warning in function body, no warning for single-line let-in, no warning without preview langversion)
  • Release notes

All 520 ErrorMessages tests and 630 BasicGrammar tests pass.

Copilot AI changed the title [WIP] Copilot Request Add warning FS3885 for let-in with multi-line body in sequence expressions Mar 31, 2026
Copilot AI requested a review from abonie March 31, 2026 15:20
@T-Gro T-Gro force-pushed the copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17 branch from 6259b63 to e6a52d1 Compare April 9, 2026 11:34
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
LanguageFeatures.fsi docs/release-notes/.Language/preview.md

@T-Gro T-Gro force-pushed the copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17 branch 2 times, most recently from b1c7950 to 10924f8 Compare April 9, 2026 14:26
Comment on lines +6047 to +6052
match letOrUse with
| { Trivia = { InKeyword = Some inRange }; Body = SynExpr.Sequential(expr2 = expr2) }
when g.langVersion.SupportsFeature LanguageFeature.WarnOnLetInSequenceExpression
&& expr2.Range.StartLine > inRange.StartLine ->
warning(Error(FSComp.SR.tcLetExpressionWithInHasMultiLineBody(), inRange))
| _ -> ()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@T-Gro Have you considered changing how this is handled by LexFilter? Can we detect a new line there and drop the let context?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would not be able to avoid breaking changes? A warning can still be silenced, a different decision in LexFilter would affect rest of the compilation process as well.

OR you think this can be done safely?

Copy link
Copy Markdown
Member

@auduchinok auduchinok Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for a breaking change here. Similar to various cases in fsharp/fslang-suggestions#1273, I think there should be not many cases where such code was written intentionally and could be compiled successfully.

The initially reported issue comes from brute-force checking of various cases to test, not from real code 🙂

@T-Gro T-Gro force-pushed the copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17 branch from a37539a to f37cbfc Compare April 10, 2026 10:14
@T-Gro T-Gro marked this pull request as ready for review April 10, 2026 13:21
@T-Gro T-Gro requested a review from a team as a code owner April 10, 2026 13:21
@T-Gro
Copy link
Copy Markdown
Member

T-Gro commented Apr 14, 2026

@copilot :

Lets try in a separate commit to implement a LexFilter change for let-in (as suggested by #19501 (comment) and by #19501 (comment) ). I.e. moving the logic from a type checking warning, to a lexfilter logical change and error.

I first want to see how it will look like.
You will need to run and adjust tests as well (I assume a lot of parser / SyntaxTree tests might need an update).

Do the work and the commit so that I can asses the impact.

@Martin521
Copy link
Copy Markdown
Contributor

As far I understand it, it applies for cases where the indentation matches. The example there doesn't include the in part which is the key part here.

15.1.9 (exceptions to the rules) doesn't include the case that is being fixed here.

I think the spec is very clear here. The rule applies to SeqBlocks and that is what we have here. And of course it does not need to be listed under exceptions because it is no exception.

Beyond the spec discussion, I believe this PR would introduce a gross inconsistency as shown in the comment above and should be rejected even on this reason alone.

@auduchinok
Copy link
Copy Markdown
Member

auduchinok commented Apr 15, 2026

in does start a new context, but not a new SeqBlock.

Hm, then maybe starting a SeqBlock indeed would be a better fix for this inconsistency, thanks. We would need to check if it could cause any other unexpected changes, though.

@Martin521
Copy link
Copy Markdown
Contributor

Hm, then maybe starting a SeqBlock indeed would be a better fix for this inconsistency, thanks. We would need to check if it could cause any other unexpected changes, though.

It is not an inconsistency. A "fix" would create an inconsistency. For good reasons the spec and the current compiler don't start a SeqBlock at in. And I don't see a reason to change that.

@T-Gro T-Gro force-pushed the copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17 branch 2 times, most recently from 4ce0417 to 8a9ab43 Compare April 20, 2026 14:48
@Martin521
Copy link
Copy Markdown
Contributor

@T-Gro
What is your view on my concerns?

@T-Gro
Copy link
Copy Markdown
Member

T-Gro commented Apr 20, 2026

@T-Gro What is your view on my concerns?

I do not want to rush this for sure.

The original issue #7741 is in my view very legit - it simply is not what is expected by the casual reader.

I did start with a warning first for the specific coding pattern in this PR (i.e. not a parser change, but a typechecking detection).

You are right with the behavior w.r.t to the spec.

At the same time, the code shown at #7741 is something not to be encouraged - I would definitely not like keeping the status quo here.

Copilot AI and others added 14 commits April 21, 2026 07:47
Agent-Logs-Url: https://github.com/dotnet/fsharp/sessions/740bb677-5347-48ad-9001-278dfc1631e3

Co-authored-by: abonie <20281641+abonie@users.noreply.github.com>
…tries

- Remove explicit 'in' keyword in HashMultiMap.fs and fsi.fs that
  triggered the new FS3885 warning (treated as error in CI builds)
- Add missing XLF translation entries for featureWarnOnLetInSequenceExpression
  and tcLetExpressionWithInHasMultiLineBody to all 13 locale files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ests.fs triggering FS3885, fix PR number in release notes, add .Language/preview.md entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous session referenced issue #7091 (Fix fsharp47 - test path fix)
but the correct issue is #7741 (Wrong let expression parsing) which
describes the let...in scoping problem this warning addresses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an explicit 'in' keyword is used with a block-scoped 'let' in light
syntax and the body starts on the same line as 'in', push a new seq block
context to limit the body scope based on indentation. This prevents the
parser from greedily capturing subsequent lines as part of the let body.

- Remove type-checking warning from CheckExpressions.fs
- Add LexFilter logic: when blockLet=true and body is on same line as 'in',
  push CtxtSeqBlock with AddBlockEnd to scope the body
- Update WarnExpressionTests to reflect new behavior (no FS3885, different
  parse tree produces different W20 ranges)
- Update SyntaxTree baseline for minor range change

Agent-Logs-Url: https://github.com/dotnet/fsharp/sessions/1c48edbc-5796-4d9b-a80d-3a3d423c2e3f

Co-authored-by: abonie <20281641+abonie@users.noreply.github.com>
- Add 7 SyntaxTree tests (LetIn 01-07) verifying let-in body scoping
  after the LexFilter change: module-level, semicolons, nested let-in,
  triple-nested, if-then-else, if-else with nested let-in, and named
  module counterpart to the anonymous module InKeyword test
- Remove dead FS3885 tcLetExpressionWithInHasMultiLineBody warning from
  FSComp.txt and all xlf files (no code references it after LexFilter change)
- Update feature description to reflect LexFilter behavior

Note on SynExprLetOrUseContainsTheRangeOfInKeyword.fs.bsl (2,0--3,0):
The AnonModule range extends to (3,0) due to the OBLOCKEND token from
the let-in scope block being positioned at EOF. This ONLY affects the
SynModuleOrNamespace range — the LetOrUse range stays correct at
(2,0--2,15). Named modules are unaffected (see LetIn 07.fs.bsl where
the same code produces module range (1,0--3,15), ending at content).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename WarnOnLetInSequenceExpression → LetInBodyScoping (no longer a warning)
- Move release notes from Added to Fixed, describe parser behavior change
- Fix AnonModule range in pars.fsy: use last declaration's Range.End
  instead of rhs (which includes OBLOCKEND at EOF position). This is the
  same pattern as PR #12585/#15369 for fixing block-end-derived ranges.
  Fixes the (2,0--3,0) regression to correct (2,0--2,15).
- Add LetIn 08 (do block scoping from WarnExpressionTests)
- Add LetIn 09 (function body scoping from WarnExpressionTests)
- Add LetIn 10 (indentation continuation: c aligns with b, d is separate)
- Update 204 anonymous module baselines (all consistent: end at last
  content character instead of extending to next-line col 0)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r scoping change

The LexFilter LetInBodyScoping change correctly scopes 'let x = 1 in x + 1'
to just that line. The 'x' on the next line is now in the outer scope,
so Warning 20 targets (6,5)-(6,6) instead of the old (5,5)-(6,6).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The body scope extends to continuation lines at the same or greater
indentation, not just the single line. Updated comment to reflect this.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The AnonModule range change (ending at lastDecl.Range.End instead of
OBLOCKEND/EOF position) broke IDE completions at end-of-file positions.
This caused failures in EditorTests (Find function from var),
CompletionTests (AllowObsolete), and Warning 988 position changes
(CustomEquality01_fs, W_ImplIComparable_fs).

Revert the pars.fsy change and restore SyntaxTree baselines. The
AnonModule range redesign can be done in a separate PR with proper
service-layer adjustments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ostics

- Resolve FSComp.txt number conflict: bump PR entry to 3887 (later removed by LexFilter commit)
- Resolve XLF conflicts: keep main's parsLetBangCannotBeLastInCE and tcListLiteralWithSingleTupleElement entries
- Update Auto property 08/09/10 baselines for improved diagnostic messages from main (#19398)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@T-Gro T-Gro force-pushed the copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17 branch from 4c8787d to 72bcc29 Compare April 21, 2026 06:46
- Revert Auto property 08/09/10 baselines that were incorrectly regenerated
  with a Debug build, leaking #if DEBUG parser internals (NONTERM_* state)
  into error messages
- Replace 4 duplicate Breaking Changes release note entries (referencing
  wrong issues/PRs and stale FS3885 warning) with one correct entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Martin521
Copy link
Copy Markdown
Contributor

@T-Gro Thanks!
The spec can be changed, of course. I just want to make sure that the whole picture is seen and discussed beforehand.
Here are some more considerations.

When the "lightweight syntax" was introduced a LONG time ago, it was designed (I am quoting the spec) as "a conservative extension of the explicit language syntax, in the sense that it simply lets you leave out certain tokens". It was not a fundamental change of the syntax. The main rules are

This does not change other aspects of the syntax, like precedence.
This means that the following three expressions are equivalent:

let x = 1 in printfn $"{x}"; printfn $"{x}"
let x = 1 in printfn $"{x}"
printfn $"{x}"
let x = 1
printfn $"{x}"
printfn $"{x}"

Changing the meaning of the second one (as this PR does) would make the language inconsistent and would make explaining the (already complicated) "light syntax" rules even harder.

Now, while most of the "old" constructs (line ;;, done, begin - end) are hardly seen any more in F# repos, both in and ; (for sequential execution expressions) are still in use. Mostly by people (like myself) who value conciseness. But perhaps it is time to prepare to abandon them like other ML heritage in the F# syntax. Therefore, I believe that adding a warning (as recommended by @dsyme) is a good step.

@auduchinok
Copy link
Copy Markdown
Member

Changing the meaning of the second one (as this PR does) would make the language inconsistent and would make explaining the (already complicated) "light syntax" rules even harder.

I respectfully disagree. This would make it easier to explain it, because the main light syntax "things have to have the same indentation" rule would be applied in a more consistent way by not allowing the sequence expression body to have different indents.

@T-Gro
Copy link
Copy Markdown
Member

T-Gro commented Apr 21, 2026

@Martin521 :

I think it's fair to say that the extension has become the default - the rules should accommodate for that, even though I totally see why the spec is historically layered in a different way.

For the middle example - it feels more consistent with other rules if it would be written like this (accepting a spec change and parser change)

let x = 1 in printfn $"{x}"
             printfn $"{x}"

(following that ; is optional in expr1;expr2 if The expression expr2 occurs on a subsequent line that starts in the same column as expr1. )

@Martin521
Copy link
Copy Markdown
Contributor

I am still not completely convinced, but it seems I am overruled and I accept that.
I just would like to see a small RFC for the change. It should explain how the three examples (or similar ones) are parsed a) in an expression block and b) as declarations. Note that compiler and spec today treat 5 of those 6 cases as "value definition expression" that have a sequence expression (the two printfn expressions) as body while the last example directly in a module is parsed as three "module value definition" declarations (one for the let and two for the do printfn lines). We should know how these six cases will be handled after the change.

@Martin521
Copy link
Copy Markdown
Contributor

As a separate question: how do you feel about phasing out in altogether?

@auduchinok
Copy link
Copy Markdown
Member

As a separate question: how do you feel about phasing out in altogether?

It seems it's often used in one-line expressions inside lambdas and similar places, so we probably shouldn't remove it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

5 participants