Guard the stack when checking long sequence expressions - #20480
Guard the stack when checking long sequence expressions#20480xperiandri wants to merge 3 commits into
Conversation
`TcSequenceExpression` recursed once per `expr; rest` node of a `seq { }`
body through `tcSequenceExprBody`, `tcSequenceExprBodyAsSequenceOrStatement`
and `tryTcSequenceExprBody` with no stack guard on that spine; only the
leaves were guarded, via `TcExpr`. On a 1 MB thread-pool thread a few
hundred (Debug) to a few thousand (Release) implicit-yield elements ran
the thread out of stack a few frames past a leaf.
With the spine guarded, `CheckNoReraise` then walked the right-nested
`Seq.append`/`Seq.delay` tree with `freeInExpr CollectLocals`, whose
`stackGuard` is `None`, and overflowed in `accFreeInExprNonLinearImpl`.
Run `tcSequenceExprBodyAsSequenceOrStatement` under `cenv.stackGuard` and
give `CheckNoReraise` the guarded free-variable options `CheckEscapes`
already uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Two related things found while verifying this, both pre-existing and deliberately not addressed here: 1. The same input still overflows in codegen ( There are ~25 more 2. Type checking a long Happy to open issues for both if maintainers prefer tracking them that way. |
Co-authored-by: Tomas Grosup <tomasgrosup@microsoft.com>
Head branch was pushed to by a user without write access
T-Gro
left a comment
There was a problem hiding this comment.
🤖🕵️ LGTM ✅
Dimensions covered (expand)
- Checking, stack guards and cancellation context
- Test and diagnostic fidelity
Description
Checking a long
seq { … }body overflows the stack.TcSequenceExpressionwalks theexpr; restspine withtcSequenceExprBody → tcSequenceExprBodyAsSequenceOrStatement → tryTcSequenceExprBody, one recursion per element, and none of those three functions is stack-guarded — only the leaves are, viaTcExpr. Once the spine has consumed a thread down to theStackGuardmargin every leaf hops to a fresh thread and returns, while the spine keeps growing on the original thread until it hits the guard page. The crash therefore lands a few frames past a leaf, inside the constraint solver:Found with a 183-element data file (ISO language codes as record literals in a
seq) that crashed a Debug VSIX in Visual Studio. Measured on a thread-pool thread withFSharpChecker.ParseAndCheckFileInProjectoverseq { { Name = "n1"; Id = 1 }; … }:The fix runs
tcSequenceExprBodyAsSequenceOrStatement— the funnel every spine step goes through — undercenv.stackGuard.Guard, the same shape asTranslateComputationExpressioninCheckComputationExpressions.fs.With the spine guarded the next wall is post-inference checking: the checked
seqis a right-nestedSeq.append (Seq.singleton e1) (Seq.delay (fun () -> …))tree, andCheckNoReraisewalked it withfreeInExpr CollectLocals, whosestackGuardisNone—accFreeInExprNonLinearImplthen recursed once per element (676 frames in Debug):CheckNoReraisenow usesCollectLocalsWithStackGuard(), asCheckEscapesin the same file already does. The otherfreeInExpr CollectLocalscall sites are in the optimizer and IlxGen, outside the IDE checking path, and are left alone here.Checklist
Long sequence of implicit yields does not overflow the stackinLanguage/SequenceExpressions/SequenceExpressionTests.fs(5000 elements; crashes the test host without the fix in both Debug and Release)docs/release-notes/.FSharp.Compiler.Service/11.0.100.md