Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions changelog.d/10550-entry-block-allocas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
### Fixed

- Loops no longer consume stack on every iteration when they call a
`Date.prototype.set*` setter, `Date.UTC`, `arr.concat`, `arr.splice`,
`arr.toSpliced`, `arr.unshift` or `Array.prototype.{push,unshift,splice,concat}.call`
(#10463). Such a loop died with SIGSEGV after about 2^19 iterations at the
default 8 MB stack (`d.setTime(i)` used 16 B per iteration, and the crash point
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.

These lowerings emitted their argument buffer (`alloca [N x double]`) or
out-parameter (`alloca i64`) into whatever block was current. LLVM lowers an
`alloca` outside the entry block to a runtime stack-pointer bump that is only
released when the function returns. #167 added
`LlFunction::alloca_entry_array` for one family of call sites; these sibling
sites were never converted: `lower_date_setter` and `ArrayToSpliced`
(`expr/os_uri_dates.rs`), `Date.UTC` (`expr/misc_methods.rs`), the
`concat`/`unshift`/`splice` arms of `lower_array_method.rs`,
`Expr::ArraySplice` (`expr/instance_misc1.rs`) and the array-like `.call`
arms (`expr/logical_collections.rs`). Other sites had the same pattern: the
multi-target dynamic `import()`/`require` and i18n join slots
(`expr/dyn_extern_i18n.rs`), `new Worker` (`expr/worker_new.rs`), the V8
interop argument buffers (`expr/v8_interop.rs`), the fused `push` length slot
(`lower_call/native/native_instance_branch.rs`) and the module namespace
populator (`codegen/helpers.rs`). All of them now allocate through
`alloca_entry` / `alloca_entry_array` / `lower_js_args_array`. Each buffer is
still filled completely right before its call.

So the class cannot come back one call site at a time, the invariant is now
enforced where every function body is finalized:
`LlFunction::for_each_final_item` (read by both the textual and the native
backend) refuses any `alloca` outside the entry block, whether typed or raw
text, inside a multi-line raw payload, or after an inline invoke-EH label in
block 0. The panic message names the function, block and instruction
(`function/entry_allocas.rs`).

Validation: the gap test `test_gap_10463_entry_block_allocas` crashes on the
baseline (every section crashes on its own at 8 MB) and matches Node with the
fix. `expr::entry_block_alloca_tests` compiles each construct inside a counted
loop and reads the IR back with its own scanner. `function::entry_allocas::tests`
sabotage-test the refusal. A `--no-link --trace llvm` sweep over all 1659
`test-files/*.ts` found non-entry allocas in 61 files before the fix and 0
after. Instruction counts are neutral to slightly lower (−0.08% to −0.36% on
date-setter, `concat`, `Array.prototype.*.call` and `splice` loops).
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,12 @@ impl LlBlock {

// -------- Memory --------

/// An `alloca` in THIS block. Legal only while this is the entry block
/// (the parameter prologues): anywhere else the slot is a per-execution
/// stack bump, and `LlFunction::for_each_final_item` refuses it (#10463).
/// Lowering code allocates with `LlFunction::alloca_entry` /
/// `alloca_entry_array`, which place the slot in the entry block whatever
/// block is current.
pub fn alloca(&mut self, ty: LlvmType) -> String {
let r = self.reg();
self.push_inst(crate::inst::LlInst::Alloca { dst: r.clone(), ty });
Expand Down
16 changes: 6 additions & 10 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1388,17 +1388,13 @@ pub(super) fn emit_namespace_populator(
// per-entry loop simply doesn't execute.
let n = entries.len();
let buf_len = n.max(1);
let blk = ctx.block();

// Alloca the four parallel buffers.
let keys_buf = blk.next_reg();
blk.emit_raw(format!("{} = alloca [{} x ptr]", keys_buf, buf_len));
let lens_buf = blk.next_reg();
blk.emit_raw(format!("{} = alloca [{} x i32]", lens_buf, buf_len));
let vals_buf = blk.next_reg();
blk.emit_raw(format!("{} = alloca [{} x double]", vals_buf, buf_len));
let live_buf = blk.next_reg();
blk.emit_raw(format!("{} = alloca [{} x i8]", live_buf, buf_len));
// Alloca the four parallel buffers — in the entry block, like every
// alloca (#10463).
let keys_buf = ctx.func.alloca_entry_array(PTR, buf_len);
let lens_buf = ctx.func.alloca_entry_array(I32, buf_len);
let vals_buf = ctx.func.alloca_entry_array(DOUBLE, buf_len);
let live_buf = ctx.func.alloca_entry_array(I8, buf_len);

// #7210 (2): `vals_buf` is a plain stack alloca, not a shadow slot the
// collector scans. Each entry's value is a NaN-boxed JSValue that can be
Expand Down
11 changes: 7 additions & 4 deletions crates/perry-codegen/src/expr/dyn_extern_i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ fn lower_dynamic_require(ctx: &mut FnCtx<'_>, paths: &[String], arg: &Expr) -> R
// The no-match fallthrough resolves via the ambient require (builtin-or-throw)
// rather than rejecting.
let spec_val = lower_expr(ctx, arg)?;
let result_slot = ctx.block().alloca(DOUBLE);
// #10463: an entry-block slot; in the current block it grew the stack on
// every loop iteration.
let result_slot = ctx.func.alloca_entry(DOUBLE);
let join_block_idx = ctx.new_block("dynamic_require_join");
let path_handle =
ctx.block()
Expand Down Expand Up @@ -381,7 +383,7 @@ fn emit_i18n_row_value(
_ => return emit_i18n_template(ctx, &templates[default_idx], lowered_params),
};

let result_slot = ctx.block().alloca(DOUBLE);
let result_slot = ctx.func.alloca_entry(DOUBLE);
let join_block_idx = ctx.new_block("i18n_locale_join");

for (li, template) in templates.iter().enumerate() {
Expand Down Expand Up @@ -683,7 +685,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// promise (NaN-boxed POINTER_TAG f64) here, then jumps to
// a join block which loads and returns. Using an alloca
// keeps the IR straightforward without proper phi nodes.
let result_slot = ctx.block().alloca(DOUBLE);
// #10463: in the entry block, like every alloca.
let result_slot = ctx.func.alloca_entry(DOUBLE);
let join_block_idx = ctx.new_block("dynamic_import_join");

// Unbox the path argument once into an i64 StringHeader*.
Expand Down Expand Up @@ -1161,7 +1164,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
.map(|(_, idx)| *idx)
.unwrap_or(*string_idx);

let result_slot = ctx.block().alloca(DOUBLE);
let result_slot = ctx.func.alloca_entry(DOUBLE);
let join_block_idx = ctx.new_block("i18n_plural_join");

for (cat, form_idx) in plural_forms.iter().filter(|(cat, _)| *cat != 5) {
Expand Down
Loading
Loading