Skip to content

fix(codegen): hoist loop-body allocas to the function entry block - #39

Merged
petlukk merged 1 commit into
mainfrom
fix/loop-body-alloca-stack-overflow
Jun 11, 2026
Merged

fix(codegen): hoist loop-body allocas to the function entry block#39
petlukk merged 1 commit into
mainfrom
fix/loop-body-alloca-stack-overflow

Conversation

@petlukk

@petlukk petlukk commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Stmt::Let emitted build_alloca at the builder's current insertion point, so a let binding inside a loop body placed its alloca inside the loop block (e.g. while_body). An LLVM alloca in a non-entry block re-executes every iteration — a fresh dynamic stack allocation never reclaimed until the function returns — so the stack grows per iteration. mem2reg/SROA only seed promotion from the entry block, so loop-body allocas also survive optimization.

This overflowed the 8 MB main-thread stack in Olorin's log_level_scan.ea on ≥1 MB inputs (~8 bytes of stack per input byte) once its SIMD body carried ~50+ u8x16 let bindings. At -O0 the kernel emitted 68 allocas inside while_body71; only the 7 params lived in the entry block.

Fix

Add CodeGenerator::entry_block_alloca, which builds the alloca with a throwaway builder positioned before the entry block's first instruction (the caller's insertion point is left untouched). Apply it to Stmt::Let and the struct-literal struct_tmp.

Stmt::ForEach already implemented this inline ("Alloca in function entry block ... to avoid stack growth at O0 where mem2reg does not run") — the loop-var path is consolidated onto the new helper so the two can't drift. The helper is pub(crate), so no docs/public-api.txt change is needed.

Verification

  • Regression test tests/loop_alloca_hoisting.rs asserts every alloca in the unoptimized frontend IR (compile_to_ir, which skips optimization) lives in the entry block — deterministic and opt-level-independent.
  • Real kernel: after the fix, all 112 log_level_scan allocas are in entry, zero in any loop.
  • Runtime A/B (-O0, ulimit -s 2048): pre-fix SIGSEGV (exit 139) on a 4 MB input; post-fix exit 0 on 4 MB and 20 MB with correct results.
  • Full test suite green, clippy clean, edited files fmt-clean.

Once merged, Olorin can restore the WARNING/CRITICAL keywords to log_level_scan.ea (reverted in v2.8.3).

🤖 Generated with Claude Code

`Stmt::Let` emitted `build_alloca` at the builder's current insertion
point, so a `let` binding inside a loop body placed its alloca inside
the loop block (e.g. `while_body`). An LLVM alloca in a non-entry block
re-executes every iteration — a fresh dynamic stack allocation that is
never reclaimed until the function returns — so the stack grows per
iteration. mem2reg/SROA only seed promotion from the entry block, so
loop-body allocas also survive optimization.

This overflowed the 8 MB main-thread stack in Olorin's
`log_level_scan.ea` on >=1 MB inputs (~8 bytes of stack per input byte)
once its SIMD body carried ~50+ u8x16 `let` bindings. At -O0 the kernel
emitted 68 allocas inside `while_body71`; only the 7 params were in the
entry block.

Add `CodeGenerator::entry_block_alloca`, which builds the alloca with a
throwaway builder positioned before the entry block's first instruction
(the caller's insertion point is left untouched). Apply it to `Stmt::Let`
and the struct-literal `struct_tmp`. `Stmt::ForEach` already implemented
this inline ("Alloca in function entry block ... to avoid stack growth at
O0 where mem2reg does not run") and is consolidated onto the helper.

Regression test `tests/loop_alloca_hoisting.rs` asserts every alloca in
the unoptimized frontend IR lives in the entry block. Verified by a -O0 +
`ulimit -s 2048` runtime A/B on `log_level_scan.ea`: pre-fix SIGSEGV on a
4 MB input, post-fix clean exit on 4 MB and 20 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@petlukk
petlukk merged commit 711e182 into main Jun 11, 2026
4 checks passed
petlukk added a commit that referenced this pull request Jun 11, 2026
Patch release over v1.15.0 carrying the single codegen fix from #39
(hoist loop-body allocas to the function entry block). Generated -O3
code for existing kernels is unchanged — verified byte-identical asm
across the x86_64 and aarch64 benchmark kernels, so bench baselines are
unaffected and need no refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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