Parent: #934 Related: #515 (the same guard, applied to JSON), #935 (flat chains — a different fix)
Confirmed by execution
Each row is a crash I reproduced with the current test262 --eval binary, with the recursion taken from an lldb backtrace at the crash. node throws a catchable RangeError: Maximum call stack size exceeded for the nesting shapes it was run on.
| source | first crash | recursion |
|---|---|---|
((…1…)) parentheses | ~8,621 deep | parsePrimary > parseExpression > parseAssignment > parseConditional > parseBinary > parseUnary > parsePostfix |
[[…]] array literals | ~9,113 deep | parseArrayLiteral > parseSpreadable > parseAssignment > … > parsePrimary |
!!!…0 unary chain | 90,000 (90 KB) | parseUnary > parseUnary, one source byte per level |
nested try { } catch statements | ~15,019 deep | parseStatement > parseBlock > parseTry > parseStatementInner |
Chokepoints
From a mapping pass over src/parser.zig — these are the functions every recursive parsing cycle passes through:
parseAssignment(parser.zig:3138) — the expression chokepoint: assignment, conditional, arrow, yield, parentheses, array/object literals, call arguments, computed keys, default values. It recurses into itself directly, so the guard belongs at its entry.parseUnary(3519) — its self-loop (!,-,typeof,await, prefix++) never passes throughparseAssignment.parseBinary(3475) — the right-associative**self-loop bypasses both of the above.parseNew(3740) — thenew new new Xcycle;parsePrimaryis an equivalent alternative.parseStatement(1891) — every statement cycle: blocks,if/else, labels, loops,switch,try,with, function and class bodies.parseBindingTarget(2183) — nested destructuring patterns.litToPattern(2105) andexprToTarget(2165) — cover-grammar refinement recursing over an already-parsed expression.
Post-parse analysis walks — unbounded, not yet the weakest site
These recurse over finished-tree depth with no guard. None crashed at the depths tried, because the parser or compiler overflowed first; checkPrivateUsesInNode survived 36,000 levels. They still need guards, because shapes that the parser builds without recursion (see #935) can hand them a deeper tree than parsing did:
scanSuperAndArgs (4783), scanSuperAndArgsInPattern (4754), addPatternNames (1476), collectVarNames (1161), recurseScope (1282), checkPrivateUsesInNode (5034), checkPrivateUsesInPattern (5001); in context.zig, nodeHasTopLevelAwait (9809) and modulePatternBoundNames (9987).
Design constraints
- Report
RangeError: Maximum call stack size exceeded, notSyntaxError, matching node and #515. KeepParseErrorcompact. - Use
stack_scan.nearLimitrather than a fixed depth: stack cost per level ranges from 1 to 8 frames across these shapes, and differs again in Debug and TSan builds and on worker threads with smaller stacks. Fall back to a conservative depth only whereboundsKnown()is false. - Don't depend on the caller having run
stack_scan.enter: callregisterThreadBounds()when the parser is constructed, so syntax-only parses and parser unit tests are protected too. - Test in a subprocess — the
test-runtimesuite intools/focused_engine_tests.zigfrom #515 — since a crash kills the test runner. Include a moderate valid depth that must still parse.