fix(checker): general non-overflowing AST teardown + measured MAX_EXPR_DEPTH (#37)#43
Conversation
…R_DEPTH (#37) Closes the Linux-side of #37 (subtleties 1 & 3). 1. General iterative AST teardown (subtlety 1) The recursive-`Drop` stack overflow is shape-independent; the old test-only `drop_program_iteratively` only flattened 2-arg `Call` chains. Replaced with `ast::drop_program_iteratively`: pops owned nodes from an explicit heap worklist and destructures them by value (legal because `Expr` has no `Drop` impl), moving every recursive child into the worklist before the shallow remainder drops. O(n) time, O(n) heap, O(1) stack, zero shape assumptions. Covers every recursive edge (Box<Expr>, Vec<Expr>, Block/Stmt, Match/Record/ Lambda/Ai). Measured: tears down a 1,000,000-deep non-Call chain on a 512 KiB stack (recursive Drop cliffs at ~5k). `impl Drop for Expr` was deliberately rejected: it would forbid by-value destructuring of `Expr` across the parser/HIR/MIR crates ("cannot move out of a type which implements Drop"). Documented. 2. MAX_EXPR_DEPTH re-derived from a measured budget (subtlety 3) `examples/measure_depth.rs` probes one `check_expr` walk on a fixed-size thread stack: ~4.4 KiB/level. The checker is never dispatched onto a large-stack thread, so the binding constraint is the OS main thread = 1 MiB on Windows. The old 256 needs ~1.14 MiB -- it could overflow *before* the guard fires on Windows (not just "unjustified" as the issue said, but unsafe there). Re-derived to 128 (~569 KiB, ~54% of a 1 MiB Windows stack). The parser's independent MAX_PARSE_EXPR_DEPTH=64 is the real operational ceiling, so no parseable program reaches the guard and lowering it rejects zero real code. Stale "quarter of 256" parser docs corrected. 3. Regression test (DoD item 4) `test_deep_non_call_ast_teardown_does_not_overflow`: a 50,000-deep `Unary` chain (no `Call`) -- the exact shape the old helper could not handle. `checker_alloc_scaling` depth ladder made constant- relative so it can't silently exceed a re-tuned guard. Still open in #37: the Windows-CI half of the stack measurement (needs CI access; the Linux measurement + harness land here). Full `cargo test -p my-lang` green (137 unit + 3 alloc-scaling + rest). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Drive-by review from a parallel session that independently re-derived this exact fix (same iterative heap-worklist teardown, same by-value destructuring + explicit Closing the open "Windows-CI leg" item without needing CI access. The measurement is shipped as Sketch that worked in the parallel session:
Note for integration tests: Optional: a companion Everything else here LGTM as-is. |
🔍 Hypatia Security Scan — findings in this PR's changed files2 finding(s) in files this PR modifies (of 32 repo-wide; 0 baselined, not shown).
View findings (this PR's files only)[
{
"reason": "expect() in hot path (157 occurrences, CWE-754)",
"type": "expect_in_hot_path",
"file": "/home/runner/work/my-lang/my-lang/crates/my-lang/src/parser.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "medium"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/my-lang/my-lang/crates/my-lang/examples/measure_depth.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "high"
}
]Pre-existing repo-wide findings are triaged via the Hypatia backlog issue and suppressed here by Powered by Hypatia Neurosymbolic CI/CD Intelligence |
) * fix(checker): self-driving stack-depth measurement + CI guard (#37) Closes the one open DoD item on #37: the Windows-CI leg of the stack-budget measurement confirming MAX_EXPR_DEPTH=128 is safe on the 1 MiB msvc main-thread stack. Subtleties 1 & 3 were fixed in #43; the value was left open until that datapoint, and nothing yet ran the probe. - examples/measure_depth.rs: rewrite the single-shot probe into a self-driving measurement. It re-execs itself as worker subprocesses, binary-searches the overflow cliff for the recursive Drop, the guarded checker walk and the iterative teardown, prints per-platform bytes/level, and exits non-zero if MAX_EXPR_DEPTH no longer fits the 1 MiB floor with headroom. One command now produces and asserts the datapoint on any platform (was a probe needing a manual exit-code wrapper). - .github/workflows/stack-depth.yml: run that measurement and the regression test on ubuntu-latest + windows-latest (same pinned actions as checker-scaling.yml), so the Windows datapoint is produced and locked in on every change instead of relying on a manual run. - tests/stack_depth_37.rs: assert the depth-128 guarded walk and a 1e6-deep iterative teardown both survive a 1 MiB stack. - checker.rs: document that the budget is now reconfirmed automatically on both OSes rather than by a one-off manual measurement. https://claude.ai/code/session_013JnrUmkCpMHsABRmhLVyd2 * fix(measure_depth): harden survives() against false PASS (#37 review) Review of the self-driving measurement flagged that survives() collapsed both subprocess spawn-failure and any non-zero exit into false ("cliffed"), with no positive proof the walk ran — an infra failure (or a future path that exits 0 without doing the work) could skew a measured cliff or risk a false PASS. - survives() now distinguishes three outcomes: spawn-failure / exit-0-without- running-the-walk are fatal (exit 3, never a datapoint); a survived run requires exit 0 AND the worker's "OK ..." completion line; a non-zero exit is the genuine overflow cliff signal. - Document that overflow-aborts-the-process is the cliff mechanism and that join().is_err() only covers the unwinding-panic path (default panic=unwind; abort would also exit non-zero, so either strategy is safe). Driver still PASSes locally (MAX_EXPR_DEPTH=128 safe within 819 KiB of the 1 MiB floor); the binary searches were independently verified correct. https://claude.ai/code/session_013JnrUmkCpMHsABRmhLVyd2 --------- Co-authored-by: Claude <noreply@anthropic.com>
Closes the Linux-side of #37 (subtleties 1 & 3). Windows-CI stack measurement still open (needs CI access).
1. General iterative AST teardown (subtlety 1)
The recursive-
Dropoverflow is shape-independent; the old test-onlydrop_program_iterativelyonly handled 2-argCallchains. Replaced withast::drop_program_iteratively: owned nodes popped from an explicit heap worklist, destructured by value (legal becauseExprhas noDropimpl), every recursive child moved into the worklist before the shallow remainder drops. O(n) time, O(n) heap, O(1) stack, zero shape assumptions; covers every recursive edge (Box<Expr>,Vec<Expr>,Block/Stmt,Match/Record/Lambda/Ai). Measured: tears down a 1,000,000-deep non-Callchain on a 512 KiB stack (recursiveDropcliffs at ~5k).impl Drop for Exprwas deliberately rejected — it forbids by-value destructuring ofExpracross the parser/HIR/MIR crates. Documented inline (the issue's "or justify an alternative" clause).2.
MAX_EXPR_DEPTHre-derived from a measured budget (subtlety 3)New
examples/measure_depth.rsprobes onecheck_exprwalk on a fixed thread stack: ≈4.4 KiB/level. The checker is never put on a large-stack thread, so the binding constraint is the OS main thread = 1 MiB on Windows. Old256 × 4.4 KiB ≈ 1.14 MiB— it can overflow before the guard fires on Windows (not merely "unjustified" as the issue states, but unsafe there). Re-derived 256 → 128 (~569 KiB, ~54% of 1 MiB; ~46% headroom). Parser's independentMAX_PARSE_EXPR_DEPTH = 64is the real operational ceiling, so no parseable program reaches the guard — lowering it rejects zero real code. Stale "quarter of 256" parser docs corrected.3. Regression test (DoD item 4)
test_deep_non_call_ast_teardown_does_not_overflow— a 50,000-deepUnarychain (noCall), the exact shape the old helper could not handle.checker_alloc_scalingdepth ladder made constant-relative so it can't silently exceed a re-tuned guard.Verification
cargo test -p my-langfully green: 137 unit + 3 alloc-scaling + 15 + 1, 0 failed. Minimal diff (235+/49−, 4 files + 1 new example) — no crate-wide reformat.DoD status
MAX_EXPR_DEPTHfrom the measured budget (256 → 128) + update docs(checker): reframe MAX_EXPR_DEPTH as a stack-recursion bound (closes #14) #36-reframed docsCall-shaped AST🤖 Generated with Claude Code