Describe the bug
parse_statement recurses once per elif arm, so C-stack usage grows linearly with the length of an if/elif chain. Past roughly 12,800 arms the process segfaults while parsing.
I want to be straightforward about the severity, because the interesting part isn't the crash: below that threshold the behaviour is already correct. COMPILE_MAX_DEPTH (src/compiler.c:237, 128) rejects a long chain cleanly with expression nesting too deep to compile (limit 128). So no valid program is affected, and a chain long enough to crash is one the compiler would have refused anyway — it just never gets the chance, because the parser exhausts the stack first. What seems worth reporting is the two invariants underneath:
PARSE_MAX_DEPTH does not cover this path. Its comment at src/parser.c:636-640 scopes it to nested expressions and nested blocks, and an elif chain is syntactically flat, so nothing on this path ever charges g_parse_depth. The guard isn't defeated — it has nothing to compare.
- The comment at
src/parser.c:1390-1391 doesn't hold here. It reads Nesting depth is preserved by parse_block's own guard, but the elif desugaring manufactures nesting by recursing at src/parser.c:1616 (else_body[0] = parse_statement(p);) after that guard has already unwound. The recursion is non-tail — the result is stored and more work follows — so it cannot be flattened away.
Measured cost is ~656 bytes of C stack per arm at -O2. The exact threshold is therefore ulimit -s-dependent: ulimit -s 65536 turns every crash below back into normal behaviour.
Surfaced while working on #870.
To reproduce
The chain has to be generated. The shape is an ordinary top-level chain, one statement per arm:
x is 0
if x == 0:
print of 0
elif x == 1:
print of 1
else:
print of -1
Generate a long one and run it:
python3 -c '
n = 14000
print("x is 0")
print("if x == 0:")
print(" print of 0")
for i in range(1, n):
print(f"elif x == {i}:")
print(f" print of {i}")
print("else:")
print(" print of -1")
' > deep_elif.eigs
./src/eigenscript deep_elif.eigs ; echo "rc=$?"
./src/eigenscript --lint deep_elif.eigs ; echo "rc=$?"
Expected behavior
A chain this long is refused, not fatal. Either the existing compile-depth error, or a parse-time depth error in the same style as the other bounds in the tree — the point is a diagnostic and a non-zero exit rather than a signal.
Actual behavior
At n = 14000, both invocations die with rc=139 (128 + 11, SIGSEGV) during parsing, with no diagnostic.
The two modes differ below the crash threshold, which is worth knowing when reproducing:
./src/eigenscript deep_elif.eigs at n = 200 and at n = 12000 → rc=1, expression nesting too deep to compile (limit 128). Correct behaviour.
./src/eigenscript --lint deep_elif.eigs at n = 12000 → rc=0. Linting doesn't run the compiler, so the depth limit that saves the plain path isn't in play at all.
So if you reproduce with a few hundred arms you'll get the clean error and see nothing wrong — the crash needs roughly 12,800+ at the default stack size.
Environment
- OS: Debian GNU/Linux 13 (trixie)
- GCC version: 14.2.0 (Debian 14.2.0-19)
- EigenScript version: 0.39.0 (built from
main at 4e398aa1e47eeb0dc7182478ab4bf3240a7f8476)
Describe the bug
parse_statementrecurses once perelifarm, so C-stack usage grows linearly with the length of anif/elifchain. Past roughly 12,800 arms the process segfaults while parsing.I want to be straightforward about the severity, because the interesting part isn't the crash: below that threshold the behaviour is already correct.
COMPILE_MAX_DEPTH(src/compiler.c:237, 128) rejects a long chain cleanly withexpression nesting too deep to compile (limit 128). So no valid program is affected, and a chain long enough to crash is one the compiler would have refused anyway — it just never gets the chance, because the parser exhausts the stack first. What seems worth reporting is the two invariants underneath:PARSE_MAX_DEPTHdoes not cover this path. Its comment atsrc/parser.c:636-640scopes it to nested expressions and nested blocks, and anelifchain is syntactically flat, so nothing on this path ever chargesg_parse_depth. The guard isn't defeated — it has nothing to compare.src/parser.c:1390-1391doesn't hold here. It readsNesting depth is preserved by parse_block's own guard, but theelifdesugaring manufactures nesting by recursing atsrc/parser.c:1616(else_body[0] = parse_statement(p);) after that guard has already unwound. The recursion is non-tail — the result is stored and more work follows — so it cannot be flattened away.Measured cost is ~656 bytes of C stack per arm at
-O2. The exact threshold is thereforeulimit -s-dependent:ulimit -s 65536turns every crash below back into normal behaviour.Surfaced while working on #870.
To reproduce
The chain has to be generated. The shape is an ordinary top-level chain, one statement per arm:
Generate a long one and run it:
Expected behavior
A chain this long is refused, not fatal. Either the existing compile-depth error, or a parse-time depth error in the same style as the other bounds in the tree — the point is a diagnostic and a non-zero exit rather than a signal.
Actual behavior
At
n = 14000, both invocations die withrc=139(128 + 11, SIGSEGV) during parsing, with no diagnostic.The two modes differ below the crash threshold, which is worth knowing when reproducing:
./src/eigenscript deep_elif.eigsatn = 200and atn = 12000→rc=1,expression nesting too deep to compile (limit 128). Correct behaviour../src/eigenscript --lint deep_elif.eigsatn = 12000→rc=0. Linting doesn't run the compiler, so the depth limit that saves the plain path isn't in play at all.So if you reproduce with a few hundred arms you'll get the clean error and see nothing wrong — the crash needs roughly 12,800+ at the default stack size.
Environment
mainat4e398aa1e47eeb0dc7182478ab4bf3240a7f8476)