Skip to content

fix(lower): guard deeply-nested expression chains against stack overflow (#5259)#5264

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5259-nested-expr
Jun 16, 2026
Merged

fix(lower): guard deeply-nested expression chains against stack overflow (#5259)#5264
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5259-nested-expr

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #5259.

Problem

Expression lowering (lower_expr) is recursive — one frame per node. A left-nested chain like var x = 1+1+…+1, var x = o.a.a.…a, or var x = a||a||… (shapes bundlers/minifiers routinely emit) recurses thousands of frames deep and overflows the native stack, aborting the whole process:

thread 'perry-main' has overflowed its stack
fatal runtime error: stack overflow, aborting    # SIGABRT (exit 134)

No diagnostic is emitted — the process just dies. The crash is in the check-lower stage (parsing completes fine).

Fix

Add a recursion-depth guard at the lower_expr entry point, exactly as the issue suggests:

  • A per-module expr_lower_depth counter on LoweringContext, incremented on entry and decremented on every exit path (so a recoverable lowering error elsewhere never leaves it inflated).
  • Past MAX_EXPR_LOWER_DEPTH (2000), lowering bails via lower_bail! with a span-tagged diagnostic:
error[U006]: expression nested too deeply (exceeded 2000 levels); split the chain across statements or intermediate variables

The compiler runs its collect/lower walk on a 128 MB stack (perry-main), and the heaviest shape (member chains) uses ~16 KB of stack per level, so a ceiling of 2000 caps worst-case lowering depth at ~32 MB — comfortably under the limit and far above anything real code or a reasonable build emits. The only inputs it now rejects are the degenerate ones that previously crashed.

Validation

Verified against all three repro shapes from the issue with the built binary:

input before after
1+1+… ×20000 SIGABRT clean U006 diagnostic
o.a.a.…a ×8000 SIGABRT clean U006 diagnostic
a‖a‖… ×20000 SIGABRT clean U006 diagnostic
under-limit chains (≤1900) compiled still compile & run correctly (end-to-end, incl. codegen)

New unit tests (crates/perry-hir/src/lower/tests.rs) cover all three rejecting shapes plus an under-limit accepting case. They run on a 128 MB-stack thread mirroring perry-main, since the default ~2 MB cargo-test stack can't hold the multi-thousand-node parse/lower walk. Full perry-hir lib suite: 158 passed.

Per maintainer convention, this PR intentionally omits the version bump and CHANGELOG entry.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Added safeguard to prevent compiler crashes when expressions are nested too deeply; deeply nested expressions now produce a clear diagnostic instead of causing a stack overflow.
  • Tests

    • Added regression tests verifying proper handling of deeply nested expressions.

…low (#5259)

Expression lowering (`lower_expr`) recurses once per node, so a left-nested
`1+1+…+1`, `o.a.a.…a`, or `a||a||…` chain — shapes bundlers/minifiers emit —
recurses thousands of frames deep and overflows the native stack, aborting the
process with `fatal runtime error: stack overflow` (SIGABRT, exit 134) and no
diagnostic.

Add a recursion-depth guard at the `lower_expr` entry point: a per-module
counter on `LoweringContext`, incremented on entry and decremented on every
exit path. Past `MAX_EXPR_LOWER_DEPTH` (2000) lowering bails via `lower_bail!`
with a span-tagged "expression nested too deeply" diagnostic instead of
recursing further. The compiler lowers on a 128 MB stack and the heaviest
shape (member chains) uses ~16 KB/level, so 2000 caps worst-case lowering at
~32 MB — well under the limit and far above anything real code emits, so only
the degenerate inputs that would otherwise crash are rejected.

Tests cover all three shapes plus an under-limit chain that must still lower;
they run on a 128 MB-stack thread (mirroring `perry-main`) because the default
~2 MB cargo-test stack can't hold the multi-thousand-node parse/lower walk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug force-pushed the worktree-fix-5259-nested-expr branch from 6a97aba to 4b4b591 Compare June 16, 2026 11:56
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fda2ebb-fb9f-4718-8d3e-13a99f734195

📥 Commits

Reviewing files that changed from the base of the PR and between 0dafb62 and 4b4b591.

📒 Files selected for processing (4)
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/lower_expr.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/tests.rs

📝 Walkthrough

Walkthrough

Adds a recursion-depth guard to expression lowering in the perry HIR compiler. A new expr_lower_depth: u32 field is added to LoweringContext, initialized to 0. A constant MAX_EXPR_LOWER_DEPTH = 2000 is defined in lower_expr.rs, and lower_expr now increments, checks, and decrements this counter, emitting a "nested too deeply" diagnostic instead of overflowing the native stack.

Changes

Expression Lowering Depth Guard

Layer / File(s) Summary
LoweringContext depth field and initialization
crates/perry-hir/src/lower/lowering_context.rs, crates/perry-hir/src/lower/context.rs
Adds pub(crate) expr_lower_depth: u32 to LoweringContext with documentation, and initializes it to 0 in with_class_id_start.
MAX_EXPR_LOWER_DEPTH constant and lower_expr guard
crates/perry-hir/src/lower/lower_expr.rs
Defines MAX_EXPR_LOWER_DEPTH = 2000 with documentation and updates imports. Wraps lower_expr to increment the counter on entry, bail via lower_bail! with a "nested too deeply" diagnostic if the ceiling is exceeded, and decrement the counter on exit regardless of whether lower_expr_impl succeeds or fails.
Regression tests
crates/perry-hir/src/lower/tests.rs
Adds a run_with_large_stack 128MB-thread harness and assert_too_deep helper. Three tests assert binary, member, and logical chains above MAX_EXPR_LOWER_DEPTH are rejected; one test asserts a chain below the limit lowers successfully.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop, hop, hop — but not too deep,
A chain of 1+1+1 would make the stack weep!
Now expr_lower_depth counts every descent,
At two thousand levels the lowering is spent.
"Nested too deeply!" the rabbit declares,
No more fatal aborts — just a diagnostic that cares. 🌿

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-5259-nested-expr

Comment @coderabbitai help to get the list of available commands and usage tips.

@proggeramlug proggeramlug merged commit c82b6ec into main Jun 16, 2026
8 of 11 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-5259-nested-expr branch June 16, 2026 11:57
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.

lower: deeply-nested expressions (a+b+c+…, o.a.a.…) overflow the stack → fatal abort instead of a diagnostic

1 participant