Skip to content

fix: run the compiler pipeline on a worker thread with a larger stack#13

Open
Flechazoie wants to merge 1 commit into
tea-compiler:mainfrom
Flechazoie:fix/compiler-stack-overflow
Open

fix: run the compiler pipeline on a worker thread with a larger stack#13
Flechazoie wants to merge 1 commit into
tea-compiler:mainfrom
Flechazoie:fix/compiler-stack-overflow

Conversation

@Flechazoie

Copy link
Copy Markdown

Problem

teac is recursive-descent throughout: pest pair → AST conversion, AST → IR lowering, and type inference all recurse once per nesting level of the source expression. A sum of N terms parses into a left-leaning tree of depth ≈ N, so stack usage grows linearly with expression length. With the default 8 MiB main-thread stack the compiler crashes on long (but perfectly legal) inputs:

thread 'main' has overflowed its stack

Reproduction (current main, aarch64 macOS, debug build)

The in-tree stress test is already close to the cliff: tests/long_code2 (a ~4000-term sum) needs more than 5 MiB of the 8 MiB budget — lowering the limit makes it crash:

$ (ulimit -s 6144 && target/debug/teac tests/long_code2/long_code2.tea -o /dev/null)  # ok
$ (ulimit -s 5120 && target/debug/teac tests/long_code2/long_code2.tea -o /dev/null)  # SIGABRT (stack overflow)

Doubling the same pattern crosses it outright:

$ python3 -c "
terms = ' + '.join(['a']*8000)
print('use std;\nfn main() -> i32 {\n    let a:i32 = 1;\n    let s:i32 = %s;\n    std::putint(s);\n    std::putch(10);\n    return 0;\n}' % terms)
" > /tmp/long8000.tea
$ target/debug/teac /tmp/long8000.tea -o /dev/null
thread 'main' has overflowed its stack

Since the assignments (asmt-2/3/4) all add code to these recursive paths (extra match arms, type coercion calls, float lowering), student forks consume even more stack per level and are likely to hit this on the stock test suite.

Fix

Run the whole pipeline on a worker thread with a generous (256 MiB) stack — the same technique rustc itself uses to sidestep deep-recursion stack overflows. The reservation is virtual memory only; pages are committed on demand, so the cost is one extra thread at startup and no measurable change in compile time.

Alternatives considered

  • Rewriting the recursive walks as explicit work-list loops — touches every compiler stage and obscures the otherwise clean structural recursion; disproportionate for the problem.
  • Segmented stack growth (stacker::maybe_grow) — adds a dependency and requires guarding every recursion site; easy to miss one.
  • Worker thread with a larger stack — ~10 lines in main.rs, no behavioral change otherwise, and the constant is trivially adjustable.

Testing

  • cargo test: 30/30 pass on aarch64 macOS (no behavior change).
  • The 8000-term repro above compiles and runs correctly after the fix; a 40000-term variant also passes.

The compiler is recursive-descent throughout (parsing, AST->IR lowering,
type inference), so stack usage grows linearly with expression length.
tests/long_code2 (~4000 terms) already needs >5 MiB of the default
8 MiB main-thread stack; an 8000-term expression overflows it outright.

Run the pipeline on a worker thread with a 256 MiB stack instead -- the
same technique rustc uses to sidestep deep-recursion stack overflows.
The reservation is virtual memory only; pages are committed on demand.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant