Skip to content

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

Open
Copilot wants to merge 16 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 16 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
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
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.

Comment on lines +383 to +386
module Test
let f () =
let x = 1 in printfn "a"
printfn "b"
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 Could you also add it as a syntax tree test, please, so we could be sure what tree structure is produced?

Copy link
Copy Markdown
Member

@auduchinok auduchinok left a comment

Choose a reason for hiding this comment

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

Thanks a lot for fixing the module member ranges!

@Martin521
Copy link
Copy Markdown
Contributor

I'd like to mention again that I don't think we should introduce a breaking change of compiler and spec without good reason.

It'd rather call it a parser bug fix for a case that shouldn't be common in practice, which is a good reason. We've done similar changes before while improving the parser.

The current behavior is according to the spec, so it is not a bug.
Therefore, this PR would need a spec change (RFC).
But t.b.h. I see no reason for a change here.
The following three expressions are equivalent today, and to me that really makes sense.
Why should we change the compiler and spec so that the first one would compile differently?

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

@auduchinok
Copy link
Copy Markdown
Member

The current behavior is according to the spec, so it is not a bug.

Please back up this claim with a link to the place in the spec that allows such special case with different indents inside sequence expressions.

The following three expressions are equivalent today, and to me that really makes sense.

They are equivalent due to a bug in the parser. See my previous reply: #7741 (comment)

Why should we change the compiler and spec so that the first one would compile differently?

We should not change the spec, we only need to fix the bug in the parser.

@Martin521
Copy link
Copy Markdown
Contributor

An interesting case to consider is in #7741 (comment):

let a = 1 in b
             c
d

I'd expect c to continue the block because of the same indentation here.

That is a different animal, but also correctly handled today.
in does start a new context, but not a new SeqBlock. Therefore, the line is a continuation and equivalent to

let a = 1 in b c
d

@Martin521
Copy link
Copy Markdown
Contributor

Please back up this claim with a link to the place in the spec that allows such special case with different indents inside sequence expressions.

The spec says: "When a token occurs directly on the offside line of a SeqBlock on the second or subsequent lines of the block, the $sep token is inserted. This token plays the same role as ; in the grammar rules." (§15.1.7)

@auduchinok
Copy link
Copy Markdown
Member

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.

@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 from 33b741e to 08e543a Compare April 15, 2026 19:18
Copilot AI and others added 11 commits April 20, 2026 15:26
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>
@T-Gro T-Gro force-pushed the copilot/fix-9141961-29048891-3fc9276d-8a02-4a9e-b5d2-cc88eecabc17 branch from 4ce0417 to 8a9ab43 Compare April 20, 2026 14:48
T-Gro and others added 3 commits April 20, 2026 16:59
The approach changed from a type-checking warning (FS3885) to a LexFilter
scoping change, so the 'Added' entries about FS3885 are no longer relevant.
The 'Fixed' entry already correctly describes the LexFilter change.

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>
@Martin521
Copy link
Copy Markdown
Contributor

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

T-Gro and others added 2 commits April 20, 2026 20:07
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@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.

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

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

5 participants