diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
index d5c2087765e..a742aea068e 100644
--- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
+++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
@@ -17,6 +17,7 @@
* Fix `YieldFromFinal`/`ReturnFromFinal` being incorrectly called in non-tail positions (`for`, `use`, `use!`, `try/with` handler). ([Issue #19402](https://github.com/dotnet/fsharp/issues/19402), [PR #19403](https://github.com/dotnet/fsharp/pull/19403))
* Fixed how the source ranges of warn directives are reported (as trivia) in the parser output (by not reporting leading spaces). ([Issue #19405](https://github.com/dotnet/fsharp/issues/19405), [PR #19408]((https://github.com/dotnet/fsharp/pull/19408)))
* Fix UoM value type `ToString()` returning garbage values when `--checknulls+` is enabled, caused by double address-taking in codegen. ([Issue #19435](https://github.com/dotnet/fsharp/issues/19435), [PR #19440](https://github.com/dotnet/fsharp/pull/19440))
+* Fix `let ... in` with explicit `in` keyword in light syntax: body is now scoped to the same line, preventing the parser from greedily capturing subsequent lines as part of the `let` body. ([Issue #7741](https://github.com/dotnet/fsharp/issues/7741), [PR #19501](https://github.com/dotnet/fsharp/pull/19501))
### Added
diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md
index d97ef294125..161ae37eaf8 100644
--- a/docs/release-notes/.Language/preview.md
+++ b/docs/release-notes/.Language/preview.md
@@ -5,4 +5,6 @@
### Fixed
+* Fix `let ... in` with explicit `in` keyword in light syntax: body is now scoped to the same line, preventing the parser from greedily capturing subsequent lines. ([Issue #7741](https://github.com/dotnet/fsharp/issues/7741), [PR #19501](https://github.com/dotnet/fsharp/pull/19501))
+
### Changed
\ No newline at end of file
diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt
index a4007147b9a..fc922f39d5e 100644
--- a/src/Compiler/FSComp.txt
+++ b/src/Compiler/FSComp.txt
@@ -1809,6 +1809,7 @@ featureWarnWhenFunctionValueUsedAsInterpolatedStringArg,"Warn when a function va
featureMethodOverloadsCache,"Support for caching method overload resolution results for improved compilation performance."
featureImplicitDIMCoverage,"Implicit dispatch slot coverage for default interface member implementations"
featurePreprocessorElif,"#elif preprocessor directive"
+featureLetInBodyScoping,"Scope 'let ... in' body to the same line in sequence expressions"
3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s"
3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'."
3882,lexHashElifMustBeFirst,"#elif directive must appear as the first non-whitespace character on a line"
diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs
index 1bc31837052..9cb3a0491fd 100644
--- a/src/Compiler/Facilities/LanguageFeatures.fs
+++ b/src/Compiler/Facilities/LanguageFeatures.fs
@@ -108,6 +108,7 @@ type LanguageFeature =
| MethodOverloadsCache
| ImplicitDIMCoverage
| PreprocessorElif
+ | LetInBodyScoping
/// LanguageVersion management
type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) =
@@ -251,6 +252,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
// Put stabilized features here for F# 11.0 previews via .NET SDK preview channels
LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg, languageVersion110
LanguageFeature.PreprocessorElif, languageVersion110
+ LanguageFeature.LetInBodyScoping, languageVersion110
// Difference between languageVersion110 and preview - 11.0 gets turned on automatically by picking a preview .NET 11 SDK
// previewVersion is only when "preview" is specified explicitly in project files and users also need a preview SDK
@@ -453,6 +455,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
| LanguageFeature.MethodOverloadsCache -> FSComp.SR.featureMethodOverloadsCache ()
| LanguageFeature.ImplicitDIMCoverage -> FSComp.SR.featureImplicitDIMCoverage ()
| LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif ()
+ | LanguageFeature.LetInBodyScoping -> FSComp.SR.featureLetInBodyScoping ()
/// Get a version string associated with the given feature.
static member GetFeatureVersionString feature =
diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi
index fd6182c9d3e..9ff9268c25b 100644
--- a/src/Compiler/Facilities/LanguageFeatures.fsi
+++ b/src/Compiler/Facilities/LanguageFeatures.fsi
@@ -99,6 +99,7 @@ type LanguageFeature =
| MethodOverloadsCache
| ImplicitDIMCoverage
| PreprocessorElif
+ | LetInBodyScoping
/// LanguageVersion management
type LanguageVersion =
diff --git a/src/Compiler/Interactive/fsi.fs b/src/Compiler/Interactive/fsi.fs
index 1ff957e77a8..d1d78a19f65 100644
--- a/src/Compiler/Interactive/fsi.fs
+++ b/src/Compiler/Interactive/fsi.fs
@@ -1532,7 +1532,7 @@ type internal FsiConsoleInput
/// Try to get the first line, if we snarfed it while probing.
member _.TryGetFirstLine() =
- let r = firstLine in
+ let r = firstLine
firstLine <- None
r
diff --git a/src/Compiler/SyntaxTree/LexFilter.fs b/src/Compiler/SyntaxTree/LexFilter.fs
index ed5da9fd043..505d17de22b 100644
--- a/src/Compiler/SyntaxTree/LexFilter.fs
+++ b/src/Compiler/SyntaxTree/LexFilter.fs
@@ -1680,8 +1680,24 @@ type LexFilterImpl (
if debug then dprintf "IN at %a (becomes %s)\n" outputPos tokenStartPos (if blockLet then "ODECLEND" else "IN")
if tokenStartCol < offsidePos.Column then warn tokenTup (FSComp.SR.lexfltIncorrentIndentationOfIn())
popCtxt()
- // Make sure we queue a dummy token at this position to check if any other pop rules apply
- delayToken(pool.UseLocation(tokenTup, ODUMMY token))
+
+ if blockLet && lexbuf.SupportsFeature LanguageFeature.LetInBodyScoping then
+ let nextTokenTup = peekNextTokenTup()
+ let nextTokenStartPos = startPosOfTokenTup nextTokenTup
+
+ if nextTokenStartPos.Line = tokenStartPos.Line then
+ // When the body expression starts on the same line as the 'in' keyword in light syntax,
+ // push a new seq block to limit the body scope to that line. This prevents the parser
+ // from greedily capturing all subsequent lines as part of the let body.
+ pushCtxtSeqBlock tokenTup AddBlockEnd
+ else
+ // Body starts on a new line after 'in' — the user intentionally placed the body
+ // on the next line, so use standard behavior.
+ delayToken(pool.UseLocation(tokenTup, ODUMMY token))
+ else
+ // Make sure we queue a dummy token at this position to check if any other pop rules apply
+ delayToken(pool.UseLocation(tokenTup, ODUMMY token))
+
returnToken tokenLexbufState (if blockLet then ODECLEND(mkSynRange tokenTup.StartPos tokenTup.EndPos, true) else token)
// Balancing rule. Encountering a 'done' balances with a 'do'. i.e. even a non-offside 'done' closes a 'do'
diff --git a/src/Compiler/Utilities/HashMultiMap.fs b/src/Compiler/Utilities/HashMultiMap.fs
index 2688869136e..e2e0832e2fa 100644
--- a/src/Compiler/Utilities/HashMultiMap.fs
+++ b/src/Compiler/Utilities/HashMultiMap.fs
@@ -169,7 +169,7 @@ type internal HashMultiMap<'Key, 'Value when 'Key: not null>(size: int, comparer
| _ -> false
member s.Remove(k: 'Key) =
- let res = s.ContainsKey(k) in
+ let res = s.ContainsKey(k)
s.Remove(k)
res
diff --git a/src/Compiler/pars.fsy b/src/Compiler/pars.fsy
index 9208d5034d8..c3bf0a4fd85 100644
--- a/src/Compiler/pars.fsy
+++ b/src/Compiler/pars.fsy
@@ -616,7 +616,12 @@ fileModuleSpec:
{ let m = (rhs parseState 1)
(fun (mNamespaceOpt, isRec, path, xml) ->
match path with
- | [] -> ParsedSigFileFragment.AnonModule($1, m)
+ | [] ->
+ let m =
+ match List.tryLast $1 with
+ | Some lastDecl -> Range.withEnd lastDecl.Range.End m
+ | None -> m
+ ParsedSigFileFragment.AnonModule($1, m)
| _ ->
let lastDeclRange = List.tryLast $1 |> Option.map (fun decl -> decl.Range) |> Option.defaultValue (rhs parseState 1)
let m = withStart (lhs parseState).Start lastDeclRange
@@ -1200,7 +1205,13 @@ fileModuleImpl:
{ let m = (rhs parseState 1)
(fun (mNamespaceOpt, isRec, path, xml) ->
match path, mNamespaceOpt with
- | [], None -> ParsedImplFileFragment.AnonModule($1, m)
+ | [], None ->
+ // Use last declaration's end to avoid the range being extended to the OBLOCKEND/EOF position
+ let m =
+ match List.tryLast $1 with
+ | Some lastDecl -> Range.withEnd lastDecl.Range.End m
+ | None -> m
+ ParsedImplFileFragment.AnonModule($1, m)
| _ ->
let lastDeclRange = List.tryLast $1 |> Option.map (fun decl -> decl.Range) |> Option.defaultValue (rhs parseState 1)
let m = withStart (lhs parseState).Start lastDeclRange
diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf
index 6896c33a6a4..5f142e3c0f3 100644
--- a/src/Compiler/xlf/FSComp.txt.cs.xlf
+++ b/src/Compiler/xlf/FSComp.txt.cs.xlf
@@ -8962,6 +8962,11 @@
This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments.
+
+ Scope 'let ... in' body to the same line in sequence expressions
+ Scope 'let ... in' body to the same line in sequence expressions
+
+