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..fdec281a01a 100644
--- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
+++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
@@ -22,3 +22,4 @@
* Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289))
* Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332))
+* Added warning FS3885 when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, which causes all subsequent lines to become part of the `let` body due to the parser's greedy behavior. ([Issue #7741](https://github.com/dotnet/fsharp/issues/7741), [PR #19501](https://github.com/dotnet/fsharp/pull/19501))
diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md
index d97ef294125..074af45dc5d 100644
--- a/docs/release-notes/.Language/preview.md
+++ b/docs/release-notes/.Language/preview.md
@@ -2,6 +2,7 @@
* Warn (FS3884) when a function or delegate value is used as an interpolated string argument, since it will be formatted via `ToString` rather than being applied. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289))
* Added `MethodOverloadsCache` language feature (preview) that caches overload resolution results for repeated method calls, significantly improving compilation performance. ([PR #19072](https://github.com/dotnet/fsharp/pull/19072))
+* Warn (FS3885) when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, causing unexpected scoping. ([Issue #7741](https://github.com/dotnet/fsharp/issues/7741), [PR #19501](https://github.com/dotnet/fsharp/pull/19501))
### Fixed
diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt
index a4007147b9a..8835f841443 100644
--- a/src/Compiler/FSComp.txt
+++ b/src/Compiler/FSComp.txt
@@ -1809,8 +1809,10 @@ 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"
+featureWarnOnLetInSequenceExpression,"Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines"
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"
3883,lexHashElifMustHaveIdent,"#elif directive should be immediately followed by an identifier"
3884,tcFunctionValueUsedAsInterpolatedStringArg,"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."
+3885,tcLetExpressionWithInHasMultiLineBody,"The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope."
diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs
index 1bc31837052..0cc80ce4541 100644
--- a/src/Compiler/Facilities/LanguageFeatures.fs
+++ b/src/Compiler/Facilities/LanguageFeatures.fs
@@ -108,6 +108,7 @@ type LanguageFeature =
| MethodOverloadsCache
| ImplicitDIMCoverage
| PreprocessorElif
+ | WarnOnLetInSequenceExpression
/// 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.WarnOnLetInSequenceExpression, 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.WarnOnLetInSequenceExpression -> FSComp.SR.featureWarnOnLetInSequenceExpression ()
/// 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..1a247cda15e 100644
--- a/src/Compiler/Facilities/LanguageFeatures.fsi
+++ b/src/Compiler/Facilities/LanguageFeatures.fsi
@@ -99,6 +99,7 @@ type LanguageFeature =
| MethodOverloadsCache
| ImplicitDIMCoverage
| PreprocessorElif
+ | WarnOnLetInSequenceExpression
/// 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..ffa3072e21c 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.WarnOnLetInSequenceExpression 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/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf
index 6896c33a6a4..de788ad9d5b 100644
--- a/src/Compiler/xlf/FSComp.txt.cs.xlf
+++ b/src/Compiler/xlf/FSComp.txt.cs.xlf
@@ -8962,6 +8962,16 @@
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.
+
+ Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines
+ Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines
+
+
+
+ The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope.
+ The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope.
+
+