Skip to content

fix(codegen): keep every alloca in the function entry block - #10550

Closed
proggeramlug wants to merge 2 commits into
mainfrom
fix/10463-entry-block-allocas
Closed

proggeramlug wants to merge 2 commits into
mainfrom
fix/10463-entry-block-allocas

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Several expression lowerings emitted their argument buffer (alloca [N x double]) or out-parameter (alloca i64) into whatever block was current instead of the function's entry block. LLVM lowers a non-entry alloca to a runtime stack-pointer bump that is released only when the function returns. So a loop calling d.setTime(i), Date.UTC(...), arr.concat(x), arr.splice(...), arr.toSpliced(...), arr.unshift(x) or Array.prototype.{push,unshift,splice,concat}.call(...) consumed stack on every iteration. It died with SIGSEGV after about 2^19 iterations at the default 8 MB stack, at a point that moved with ulimit -s. date-fns addMinutes in a loop crashed the same way, because the cross-module inliner copies its setTime into the caller's loop.

This PR moves every such site to the existing entry-block helpers and adds a check where function bodies are finalized, so the class cannot come back one call site at a time.

Root cause

#167 added LlFunction::alloca_entry_array for the native-method dispatch buffers, but sibling lowerings kept doing blk.emit_raw(format!("{} = alloca [{} x double]", …)) or blk.alloca(I64) in the current block. Line numbers are at 7661bc0.

Confirmed crashers (each one SIGSEGVs in a loop on the baseline):

  • crates/perry-codegen/src/expr/os_uri_dates.rs:51: lower_date_setter, used by every Date.prototype.set*
  • crates/perry-codegen/src/expr/misc_methods.rs:198: Date.UTC
  • crates/perry-codegen/src/expr/os_uri_dates.rs:362: toSpliced
  • crates/perry-codegen/src/lower_array_method.rs:313 (concat), :910 (unshift), :972 + :989 (splice out-slot + items). The issue listed :910/:972/:989 as unreached; holder.arr.unshift(x) / holder.arr.splice(0, 1, x) on a class field reach them and crash.
  • crates/perry-codegen/src/expr/instance_misc1.rs:1431 + :1450: Expr::ArraySplice out-slot + items
  • crates/perry-codegen/src/expr/logical_collections.rs:853 + :888: Array.prototype.*.call

Same pattern, also converted:

  • expr/dyn_extern_i18n.rs:106, :384, :686, :1164: multi-target dynamic require / import() result slots, i18n row and plural join slots
  • expr/worker_new.rs:52, :78: new Worker
  • expr/v8_interop.rs:118, :221: V8-interop argument buffers
  • lower_call/native/native_instance_branch.rs:361: fused push length slot
  • codegen/helpers.rs:1395-1401: the namespace populator's four buffers (runs once per module init, converted for uniformity)

Fix

  • Every site above now allocates through LlFunction::alloca_entry / alloca_entry_array, or through lower_js_args_array, which already hoists its buffer and returns null/0 for no arguments. Each buffer is still filled completely immediately before its call, as before, so no stale value survives across iterations. None of these slots is a GC root; their store/load positions are unchanged.
  • Mechanical check (crates/perry-codegen/src/function/entry_allocas.rs): LlFunction::for_each_final_item is the one funnel both the textual and the native backends consume, and it now refuses any alloca that lands outside the entry block. That covers typed LlInst::Alloca, raw text, allocas inside multi-line raw payloads, and allocas after an inline invoke-EH label in block 0. The panic names the function, block and instruction and points at alloca_entry. LlBlock::alloca stays legal only while block 0 is current, which the parameter prologues rely on; its doc comment now says so.

Tests

  • Gap test test-files/test_gap_10463_entry_block_allocas.ts (+ helper test-files/_helpers/add_minutes_10463.ts). It runs each construct in its own loop, each needing at least 19 MB of stack under the old lowering: Date setters, Date.UTC, toSpliced, concat, local splice, field unshift/splice, Array.prototype.*.call, and an imported date-fns-shaped addMinutes.
    • baseline 7661bc0: CRASHED in the harness (PERRY_SKIP_BUILD=1 ./run_parity_tests.sh --filter test_gap_10463). Run separately, each of the 8 sections SIGSEGVs on its own at the default 8 MB stack.
    • this branch: PASS, byte-identical to Node 26.5.1. Node 1.09 s, Perry 1.32 s.
  • expr::entry_block_alloca_tests (cargo-test visible): a HIR corpus of every confirmed construct inside a counted loop, compiled with compile_module. Each case first asserts its runtime entry (js_date_apply_setter, js_date_utc, js_array_to_spliced, js_array_splice, js_array_concat_variadic, js_array_unshift_variadic, js_arraylike_{push,unshift,splice,concat}) is called from a non-entry block, so the case is not vacuous. It then reads the emitted IR back with its own scanner and asserts no alloca is outside any function's entry block. A scanner self-test covers the pre-fix shape.
  • function::entry_allocas::tests: five refusal cases (raw alloca in a loop body, typed alloca in a non-entry block, alloca in a multi-line raw payload, alloca after an inline label in block 0, refusal on the native item stream) and two controls (the helpers hoist out of a loop block; a prologue alloca in block 0 is accepted).
  • Sabotage check: with only lower_date_setter reverted, date_setters_keep_their_argument_buffer_in_the_entry_block fails through the refusal. With the refusal also disabled, it fails through the independent scanner, and the five should_panic tests fail.
  • Corpus sweep: perry compile --no-link --trace llvm over all 1659 test-files/*.ts with both compilers.
    • baseline: 61 files have allocas outside an entry block (dynamic import, namespace re-export, Date, splice/concat tests, …).
    • this branch: 0 such files, 0 refusals, identical compile exit status for every file. test_wasm_add timed out once on this branch's arm while building the wasm host for the first time; a rerun gives rc=0.
  • python3 scripts/check_test_registration.py: OK.

Validation

check result
cargo test --release -p perry-codegen --tests 36/36 test binaries ok (lib: 1585 passed, 1 ignored; 35 integration suites)
./scripts/run_lint_gates.sh (full, incl. compile tier) 82/83 passed, 2 CI-only skipped. Pre-existing red: Public benchmark evidence freshness (public artifact benchmark inputs changed); the same gate prints the same error in the baseline 7661bc0 checkout
gap suite (PERRY_SKIP_BUILD=1 ./scripts/run_gap_tests.sh) GAP_EXIT=0, snapshot OK: 814 pass / 6 output mismatches / 0 compile fail / 0 crashed (820 tests incl. the new one). The 6 mismatches are exactly the baseline run's 6 (2159_defineproperty_class_prototype, 2514_settracesigint, json_lazy_defineproperty_index, perfhooks_3088_3008_3010_3011, prop_plan_cache_invalidation, v8_2_3680plus). No new failures. An earlier run was discarded: the lint gate scripts/regen_api_docs.sh runs cargo build --release -p perry and replaced the binary mid-run with a build using different feature unification. 3 ext-wrapper tests then hit compile failures; all 3 pass individually on the canonical build and in the clean run

Performance (perf stat -e instructions:u,task-clock, 3 runs each, PERRY_NO_AUTO_OPTIMIZE=1, both arms under ulimit -s unlimited so the baseline can finish)

workload baseline instructions fix instructions Δ task-clock base → fix Node (in-script / wall)
date setters (setTime + setUTCMinutes), 2M iterations 2,214.10M 2,206.09M −0.36% 300–322 → 225–233 ms 190–199 ms / 0.31–0.39 s
a.concat(i), 2M 4,013.06M 4,009.05M −0.10% 436–448 → 414–432 ms 430–435 ms / 0.53–0.55 s
Array.prototype.push.call + splice.call on an array, 2M 5,024.15M 5,016.14M −0.16% 515–620 → 540–544 ms 160–168 ms / 0.26–0.32 s
local splice middle insert + head replace, n=60000 587.61M 587.13M −0.08% 153–186 → 147–156 ms 219–302 ms / 0.32–0.41 s

Instruction counts are identical across runs within ±0.01%. Removing the per-iteration stack growth saves the page faults it caused: the date-setter loop touched 64 MB of stack on the baseline, hence the task-clock drop.

Package check (informational)

date-fns 4.4.0 addMinutes / addDays / startOfDay / differenceInMinutes in a 2M-iteration loop compiles and matches Node on this branch. In the two shapes I tried, the real date-fns addMinutes was not cross-module-inlined on the baseline either, so that program did not crash there. The date-fns-shaped helper in the gap test is inlined and does crash on the baseline.

Not verified

  • The dynamic import()/require, i18n, new Worker, V8-interop, fused-push and namespace-populator sites were not crash-tested in a loop. Their conversion is checked by the refusal and by the corpus sweep: the dynamic-import and namespace tests had non-entry allocas on the baseline and have none now.
  • Only Linux x64 was run. The refusal is target-independent codegen, but macOS/Windows/iOS builds were not exercised.

Fixes #10463

Summary by CodeRabbit

  • Bug Fixes

    • Fixed excessive stack growth in long-running loops using date and array operations, preventing crashes after many iterations.
    • Improved stability for array-like method calls, including concat, splice, push, unshift, and toSpliced.
    • Added safeguards to prevent invalid stack allocations during code generation.
  • Tests

    • Added regression coverage for affected date, array, and worker scenarios.

Date setters, Date.UTC, concat/splice/toSpliced/unshift and
Array.prototype.{push,unshift,splice,concat}.call emitted their argument
buffer (or splice's i64 out-parameter) into whatever block was current. An
alloca outside the entry block is a runtime stack bump released only on
return, so each loop iteration consumed stack until the process died with
SIGSEGV (~2^19 iterations at 8 MB). The same pattern in the dynamic
import/require and i18n join slots, new Worker, the V8 interop argument
buffers, the fused push length slot and the namespace populator is fixed too:
all of them now allocate through alloca_entry / alloca_entry_array /
lower_js_args_array.

LlFunction::for_each_final_item, which both backends consume, now refuses any
alloca outside the entry block, so a new call site cannot reintroduce the
class.
@proggeramlug proggeramlug added the package-audit Found by the 2026 package audit: compiling real npm packages from source instead of native bindings label Sep 17, 2026
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a6c13080-f1f1-4215-a4b9-66b3c52fce5f

📥 Commits

Reviewing files that changed from the base of the PR and between 193889d and 1326dab.

📒 Files selected for processing (18)
  • changelog.d/10550-entry-block-allocas.md
  • crates/perry-codegen/src/block.rs
  • crates/perry-codegen/src/codegen/helpers.rs
  • crates/perry-codegen/src/expr/dyn_extern_i18n.rs
  • crates/perry-codegen/src/expr/entry_block_alloca_tests.rs
  • crates/perry-codegen/src/expr/instance_misc1.rs
  • crates/perry-codegen/src/expr/logical_collections.rs
  • crates/perry-codegen/src/expr/misc_methods.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/os_uri_dates.rs
  • crates/perry-codegen/src/expr/v8_interop.rs
  • crates/perry-codegen/src/expr/worker_new.rs
  • crates/perry-codegen/src/function.rs
  • crates/perry-codegen/src/function/entry_allocas.rs
  • crates/perry-codegen/src/lower_array_method.rs
  • crates/perry-codegen/src/lower_call/native/native_instance_branch.rs
  • test-files/_helpers/add_minutes_10463.ts
  • test-files/test_gap_10463_entry_block_allocas.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The codegen now routes affected allocations to function entry blocks, rejects non-entry allocas during final instruction streaming, and adds compiler and runtime regression coverage for long-running loops.

Changes

Entry-block alloca fix

Layer / File(s) Summary
Alloca invariant enforcement
crates/perry-codegen/src/block.rs, crates/perry-codegen/src/function.rs, crates/perry-codegen/src/function/entry_allocas.rs
LlFunction::for_each_final_item now rejects typed and raw alloca instructions outside the entry block. Documentation and unit tests define the accepted entry-prologue placement.
Argument-buffer allocation migration
crates/perry-codegen/src/codegen/helpers.rs, crates/perry-codegen/src/expr/instance_misc1.rs, crates/perry-codegen/src/expr/logical_collections.rs, crates/perry-codegen/src/expr/misc_methods.rs, crates/perry-codegen/src/expr/os_uri_dates.rs, crates/perry-codegen/src/expr/v8_interop.rs, crates/perry-codegen/src/lower_array_method.rs
Date, array, namespace, and V8 lowering paths now use alloca_entry, alloca_entry_array, or lower_js_args_array for argument buffers. Empty argument lists retain null and zero-count call arguments.
Result and out-parameter allocation migration
crates/perry-codegen/src/expr/dyn_extern_i18n.rs, crates/perry-codegen/src/expr/worker_new.rs, crates/perry-codegen/src/lower_call/native/native_instance_branch.rs
Result slots and array length out-parameters now use entry-block allocations instead of current-block allocations.
Codegen and runtime regression coverage
crates/perry-codegen/src/expr/entry_block_alloca_tests.rs, crates/perry-codegen/src/expr/mod.rs, test-files/_helpers/add_minutes_10463.ts, test-files/test_gap_10463_entry_block_allocas.ts, changelog.d/10550-entry-block-allocas.md
New tests scan generated IR, verify covered runtime calls occur in loop bodies, and exercise date and array lowerings with large iteration counts. The changelog records the fix and validation results.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 1326d

The allocation migration and invariant enforcement have no identified merge-blocking regression.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.49% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 17 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: keeping alloca instructions in the function entry block.
Description check ✅ Passed The description is detailed and covers the summary, root cause, concrete changes, related issue, tests, validation results, performance, and limitations. It does not reproduce every template heading o…
Linked Issues check ✅ Passed The PR meets the coding requirements in #10463. It routes affected Date, array, dynamic import/require, i18n, worker, V8 interop, fused push, and namespace-population allocations through entry-block h…
Out of Scope Changes check ✅ Passed The changes stay within #10463. The changelog entry documents the fix, and the helper and test changes support the allocation invariant and regression coverage. No unrelated product behavior or unrela…
Full details: Docstring Coverage

Explanation

Docstring coverage is 58.49% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 17 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed via merge train #10578 (v0.5.1593). All source commits preserve authorship; merged main matches the validated train exactly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package-audit Found by the 2026 package audit: compiling real npm packages from source instead of native bindings

Projects

None yet

1 participant