diff --git a/changelog.d/10550-entry-block-allocas.md b/changelog.d/10550-entry-block-allocas.md new file mode 100644 index 0000000000..99d4e9ab8f --- /dev/null +++ b/changelog.d/10550-entry-block-allocas.md @@ -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). diff --git a/crates/perry-codegen/src/block.rs b/crates/perry-codegen/src/block.rs index dbb5a7ad51..65ace3da66 100644 --- a/crates/perry-codegen/src/block.rs +++ b/crates/perry-codegen/src/block.rs @@ -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 }); diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index aa7ff04a61..ec74af2786 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -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 diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index 109d708c81..fbdef0d15c 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -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() @@ -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() { @@ -683,7 +685,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // 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*. @@ -1161,7 +1164,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .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) { diff --git a/crates/perry-codegen/src/expr/entry_block_alloca_tests.rs b/crates/perry-codegen/src/expr/entry_block_alloca_tests.rs new file mode 100644 index 0000000000..b9d2015b17 --- /dev/null +++ b/crates/perry-codegen/src/expr/entry_block_alloca_tests.rs @@ -0,0 +1,303 @@ +//! #10463: the lowerings that used to place an argument buffer or an +//! out-parameter `alloca` in whatever block was current, each compiled inside +//! a counted loop. +//! +//! `LlFunction::for_each_final_item` now refuses any `alloca` outside the entry +//! block (`function/entry_allocas.rs`), so a regression at one of these sites +//! fails the compile below with that refusal. The IR is also read back here +//! with a scanner of its own, so the corpus does not rest on the refusal it is +//! meant to back up. Each case asserts its subject is live first: the runtime +//! entry the construct lowers to has to be called from a block after the entry +//! block, or the fixture never reached the site and a clean verdict would be +//! vacuous. + +use crate::compile_module; +use perry_hir::types::Type; +use perry_hir::{CompareOp, Expr, Function, Module, Param, Stmt, UpdateOp}; + +const N: u32 = 1; +const DATE: u32 = 2; +const ARR: u32 = 3; +const ARRAY_LIKE: u32 = 4; +const I: u32 = 10; + +fn param(id: u32, name: &str, ty: Type) -> Param { + Param { + id, + name: name.to_string(), + ty, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + } +} + +fn get(id: u32) -> Box { + Box::new(Expr::LocalGet(id)) +} + +fn num(v: f64) -> Box { + Box::new(Expr::Number(v)) +} + +/// `function probe(n, date, arr, arrayLike) { for (let i = 0; i < n; i++) { body } }` +fn looped(body: Vec) -> Module { + let mut module = Module::new("entry_block_alloca.ts"); + module.functions = vec![Function { + id: 90, + name: "probe".to_string(), + type_params: Vec::new(), + params: vec![ + param(N, "n", Type::Number), + param(DATE, "date", Type::Any), + param(ARR, "arr", Type::Array(Box::new(Type::Number))), + param(ARRAY_LIKE, "arrayLike", Type::Any), + ], + return_type: Type::Void, + body: vec![Stmt::For { + init: Some(Box::new(Stmt::Let { + id: I, + name: "i".to_string(), + ty: Type::Number, + mutable: true, + init: Some(Expr::Number(0.0)), + })), + condition: Some(Expr::Compare { + op: CompareOp::Lt, + left: get(I), + right: get(N), + }), + update: Some(Expr::Update { + id: I, + op: UpdateOp::Increment, + prefix: false, + }), + body, + }], + is_async: false, + is_generator: false, + is_strict: true, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }]; + module +} + +fn method_call(receiver: u32, method: &str, args: Vec) -> Stmt { + Stmt::Expr(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: get(receiver), + property: method.to_string(), + byte_offset: 0, + }), + args, + type_args: Vec::new(), + byte_offset: 0, + }) +} + +/// Every `alloca` outside its function's entry block, as `fn: line`. Labels are +/// flush-left `name:` lines; the first one after a `define` opens the entry +/// block and the next one closes it. +fn non_entry_allocas(ir: &str) -> Vec { + let mut found = Vec::new(); + let mut function = None; + let mut labels = 0usize; + for line in ir.lines() { + if line.starts_with("define ") { + function = Some(line.to_string()); + labels = 0; + } else if line.starts_with('}') { + function = None; + } else if let Some(define) = &function { + if line.ends_with(':') && !line.starts_with(|c: char| c.is_whitespace() || c == ';') { + labels += 1; + } else if labels > 1 && line.contains(" = alloca ") { + found.push(format!("{define}: {}", line.trim())); + } + } + } + found +} + +/// The number of calls to `@callee` in blocks after an entry block. +fn calls_outside_entry(ir: &str, callee: &str) -> usize { + let needle = format!("@{callee}("); + let mut in_function = false; + let mut labels = 0usize; + let mut count = 0; + for line in ir.lines() { + if line.starts_with("define ") { + in_function = true; + labels = 0; + } else if line.starts_with('}') { + in_function = false; + } else if in_function { + if line.ends_with(':') && !line.starts_with(|c: char| c.is_whitespace() || c == ';') { + labels += 1; + } else if labels > 1 && line.contains(&needle) { + count += 1; + } + } + } + count +} + +fn assert_entry_block_allocas_only(case: &str, callees: &[&str], body: Vec) { + let bytes = compile_module(&looped(body), crate::temp_root_coverage::entry_opts()) + .unwrap_or_else(|e| panic!("{case}: codegen failed: {e}")); + let ir = String::from_utf8(bytes).expect("LLVM IR should be UTF-8"); + for callee in callees { + assert!( + calls_outside_entry(&ir, callee) > 0, + "{case}: the fixture must lower to `@{callee}` inside the loop, or this \ + case checks nothing:\n{ir}" + ); + } + let stray = non_entry_allocas(&ir); + assert!( + stray.is_empty(), + "{case}: every alloca must be in its function's entry block (#10463); \ + found outside it:\n{}\n\n{ir}", + stray.join("\n") + ); +} + +#[test] +fn date_setters_keep_their_argument_buffer_in_the_entry_block() { + assert_entry_block_allocas_only( + "Date.prototype.set*", + &["js_date_apply_setter"], + vec![ + Stmt::Expr(Expr::DateSetTime { + date: get(DATE), + args: vec![Expr::LocalGet(I)], + }), + Stmt::Expr(Expr::DateSetUtcHours { + date: get(DATE), + args: vec![ + Expr::LocalGet(I), + Expr::Number(1.0), + Expr::Number(2.0), + Expr::Number(3.0), + ], + }), + ], + ); +} + +#[test] +fn date_utc_keeps_its_argument_buffer_in_the_entry_block() { + assert_entry_block_allocas_only( + "Date.UTC", + &["js_date_utc"], + vec![Stmt::Expr(Expr::DateUtc(vec![ + Expr::Number(2000.0), + Expr::LocalGet(I), + ]))], + ); +} + +#[test] +fn to_spliced_keeps_its_item_buffer_in_the_entry_block() { + assert_entry_block_allocas_only( + "Array.prototype.toSpliced", + &["js_array_to_spliced"], + vec![Stmt::Expr(Expr::ArrayToSpliced { + array: get(ARR), + start: num(1.0), + delete_count: num(1.0), + items: vec![Expr::LocalGet(I)], + })], + ); +} + +/// `Expr::ArraySplice`: the `i64` out-parameter AND the item buffer, with and +/// without items. +#[test] +fn local_splice_keeps_its_out_slot_and_item_buffer_in_the_entry_block() { + assert_entry_block_allocas_only( + "Expr::ArraySplice", + &["js_array_splice"], + vec![ + Stmt::Expr(Expr::ArraySplice { + array_id: ARR, + start: num(1.0), + delete_count: Some(num(1.0)), + items: vec![Expr::LocalGet(I)], + }), + Stmt::Expr(Expr::ArraySplice { + array_id: ARR, + start: num(1.0), + delete_count: Some(num(0.0)), + items: Vec::new(), + }), + ], + ); +} + +/// The generic array-method lowering (`lower_array_method`): `concat`, +/// `unshift`, and `splice` with its out-parameter. +#[test] +fn array_methods_keep_their_buffers_in_the_entry_block() { + assert_entry_block_allocas_only( + "arr.concat / arr.unshift / arr.splice", + &[ + "js_array_concat_variadic", + "js_array_unshift_variadic", + "js_array_splice", + ], + vec![ + method_call(ARR, "concat", vec![Expr::LocalGet(I)]), + method_call(ARR, "unshift", vec![Expr::LocalGet(I)]), + method_call( + ARR, + "splice", + vec![Expr::Number(0.0), Expr::Number(1.0), Expr::LocalGet(I)], + ), + ], + ); +} + +/// `Array.prototype.{push,unshift,splice,concat}.call(arrayLike, …)`. +#[test] +fn array_like_methods_keep_their_argument_buffer_in_the_entry_block() { + let call = |method: &str, args: Vec| { + Stmt::Expr(Expr::ArrayLikeMethod { + method: method.to_string(), + receiver: get(ARRAY_LIKE), + args, + }) + }; + assert_entry_block_allocas_only( + "Array.prototype.*.call", + &[ + "js_arraylike_push", + "js_arraylike_unshift", + "js_arraylike_splice", + "js_arraylike_concat", + ], + vec![ + call("push", vec![Expr::LocalGet(I)]), + call("unshift", vec![Expr::LocalGet(I)]), + call("splice", vec![Expr::Number(0.0), Expr::Number(2.0)]), + call("concat", vec![Expr::LocalGet(I)]), + ], + ); +} + +/// The scanner itself: it must report the pre-fix shape. +#[test] +fn the_scanner_reports_an_alloca_in_a_loop_body() { + let ir = "define double @f() {\nentry.0:\n %a = alloca double\n br label %for.body.1\n\ + \nfor.body.1:\n %b = alloca [1 x double]\n call double @g(ptr %b)\n}\n"; + assert_eq!( + non_entry_allocas(ir), + vec!["define double @f() {: %b = alloca [1 x double]".to_string()] + ); + assert_eq!(calls_outside_entry(ir, "g"), 1); +} diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 708e76133c..4c2690b505 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -69,7 +69,8 @@ use crate::types::{DOUBLE, I1, I32, I64, PTR}; use super::{ emit_root_nanbox_store_on_block, emit_shadow_slot_bind_for_local, emit_string_literal_global, emit_write_barrier, extract_array_of_object_shape, i32_bool_to_nanbox, lower_array_literal, - lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_str_handle, unbox_to_i64, FnCtx, + lower_expr, lower_js_args_array, nanbox_pointer_inline, nanbox_string_inline, unbox_str_handle, + unbox_to_i64, FnCtx, }; /// Reserved runtime class id for a built-in constructor usable as a class @@ -1425,10 +1426,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { }; let item_vals: Vec = vals[items_at..].to_vec(); + // Scratch out-parameter slot receiving the modified-array + // handle from js_array_splice. #10463: an entry-block alloca — + // `blk.alloca` in the current block grew the stack on every + // loop iteration, as did the item buffer below. + let out_slot = ctx.func.alloca_entry(I64); let blk = ctx.block(); - // Scratch out-parameter slot — used only in this block to - // receive the modified-array handle from js_array_splice. - let out_slot = blk.alloca(I64); blk.store(I64, "0", &out_slot); let arr_handle = unbox_to_i64(blk, &arr_box); // ToIntegerOrInfinity via the clamping helper: `fptosi` on @@ -1439,25 +1442,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]); - let (items_ptr, items_count_str) = if item_vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - // Allocate a stack buffer of [N x double] for the - // items, store each value, and pass the base pointer. - let n = item_vals.len(); - let items_count_str = format!("{}", n); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in item_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, items_count_str) - }; + // A stack buffer of [N x double] holding the items (null/0 + // when there are none). + let (items_ptr, items_count_str) = lower_js_args_array(ctx, &item_vals); // Note: js_array_splice's return value is the DELETED // array; the modified-in-place arr is written to *out_arr. - let deleted_handle = blk.call( + let deleted_handle = ctx.block().call( I64, "js_array_splice", &[ diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index d5777e3d26..b0c0de265c 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -52,8 +52,8 @@ use crate::type_analysis::{map_static_type_args, string_value_is_runtime_guarant use crate::types::{DOUBLE, I32, I64, PTR}; use super::{ - emit_string_literal_global, i32_bool_to_nanbox, lower_expr, nanbox_pointer_inline, - nanbox_string_inline, record_collection_number_key_fallback, + emit_string_literal_global, i32_bool_to_nanbox, lower_expr, lower_js_args_array, + nanbox_pointer_inline, nanbox_string_inline, record_collection_number_key_fallback, record_collection_number_key_selected, record_collection_string_key_fallback, record_collection_string_key_selected, unbox_str_handle, unbox_to_i64, FnCtx, }; @@ -845,24 +845,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // of raw NaN-boxed doubles + count (mirrors the dense // `js_array_concat_variadic` lowering). "splice" | "concat" => { - let n = arg_boxes.len(); - let (buf_reg, count_str) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_boxes.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; + // #10463: an entry-block buffer; allocated in the + // current block it grew the stack per loop iteration. + let (buf_reg, count_str) = lower_js_args_array(ctx, &arg_boxes); let fname = if method == "splice" { "js_arraylike_splice" } else { "js_arraylike_concat" }; - blk.call( + ctx.block().call( DOUBLE, fname, &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], @@ -880,24 +871,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // push(...) / unshift(...): variadic — pass an alloca buffer of // raw NaN-boxed doubles + count (mirrors splice/concat above). "push" | "unshift" => { - let n = arg_boxes.len(); - let (buf_reg, count_str) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_boxes.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; + let (buf_reg, count_str) = lower_js_args_array(ctx, &arg_boxes); let fname = if method == "push" { "js_arraylike_push" } else { "js_arraylike_unshift" }; - blk.call( + ctx.block().call( DOUBLE, fname, &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], diff --git a/crates/perry-codegen/src/expr/misc_methods.rs b/crates/perry-codegen/src/expr/misc_methods.rs index da90b4bee7..b1d349643c 100644 --- a/crates/perry-codegen/src/expr/misc_methods.rs +++ b/crates/perry-codegen/src/expr/misc_methods.rs @@ -14,9 +14,9 @@ use crate::type_analysis::{is_numeric_expr, is_provably_not_bigint}; use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR}; use super::{ - i32_bool_to_nanbox, lower_expr, lower_expr_native, lower_expr_value, lower_math_operand, - materialize_js_value, nanbox_pointer_inline, nanbox_string_inline, unbox_str_handle, - unbox_to_i64, FnCtx, + i32_bool_to_nanbox, lower_expr, lower_expr_native, lower_expr_value, lower_js_args_array, + lower_math_operand, materialize_js_value, nanbox_pointer_inline, nanbox_string_inline, + unbox_str_handle, unbox_to_i64, FnCtx, }; fn lowered_value_to_iter_result_f64( @@ -189,20 +189,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { for a in args.iter() { vals.push(lower_expr(ctx, a)?); } - let blk = ctx.block(); - let (args_ptr, argc) = if vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = vals.len(); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; - Ok(blk.call(DOUBLE, "js_date_utc", &[(PTR, &args_ptr), (I32, &argc)])) + // #10463: an entry-block buffer, not one per loop iteration. + let (args_ptr, argc) = lower_js_args_array(ctx, &vals); + Ok(ctx + .block() + .call(DOUBLE, "js_date_utc", &[(PTR, &args_ptr), (I32, &argc)])) } // -------- Object.defineProperty -------- diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 778f93e792..2d97e85232 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -165,6 +165,8 @@ mod class_field_barrier_tests; mod class_field_get_shape_tests; mod dispatch; #[cfg(test)] +mod entry_block_alloca_tests; +#[cfg(test)] mod hit_path_access_tests; #[cfg(test)] mod index_set_barrier_tests; diff --git a/crates/perry-codegen/src/expr/os_uri_dates.rs b/crates/perry-codegen/src/expr/os_uri_dates.rs index ced295966c..6d2b564064 100644 --- a/crates/perry-codegen/src/expr/os_uri_dates.rs +++ b/crates/perry-codegen/src/expr/os_uri_dates.rs @@ -10,7 +10,10 @@ use perry_hir::Expr; use crate::nanbox::double_literal; use crate::types::{DOUBLE, I1, I32, I64, PTR}; -use super::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use super::{ + lower_expr, lower_js_args_array, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, + FnCtx, +}; /// Field selector codes for `js_date_apply_setter`. Must match the runtime /// (`crates/perry-runtime/src/date.rs`): 0=FullYear 1=Month 2=Date 3=Hours @@ -42,22 +45,12 @@ pub(crate) fn lower_date_setter( for a in args { arg_vals.push(lower_expr(ctx, a)?); } - let blk = ctx.block(); - let (args_ptr, argc) = if arg_vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = arg_vals.len(); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; + // #10463: the buffer is an entry-block alloca. Emitted here, in whatever + // block is current, it grew the stack on every loop iteration. + let (args_ptr, argc) = lower_js_args_array(ctx, &arg_vals); let is_utc_str = if is_utc { "1" } else { "0" }; let field_str = format!("{}", field); - Ok(blk.call( + Ok(ctx.block().call( DOUBLE, "js_date_apply_setter", &[ @@ -350,23 +343,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { item_vals.push(lower_expr(ctx, it)?); } - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - - let (items_ptr, items_count_str) = if item_vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = item_vals.len(); - let items_count_str = format!("{}", n); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in item_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, items_count_str) - }; + let arr_handle = unbox_to_i64(ctx.block(), &arr_box); + // #10463: entry-block buffer (see `lower_date_setter`). + let (items_ptr, items_count_str) = lower_js_args_array(ctx, &item_vals); + let blk = ctx.block(); let result = blk.call( I64, "js_array_to_spliced", diff --git a/crates/perry-codegen/src/expr/v8_interop.rs b/crates/perry-codegen/src/expr/v8_interop.rs index 5e6fae9455..e994532e02 100644 --- a/crates/perry-codegen/src/expr/v8_interop.rs +++ b/crates/perry-codegen/src/expr/v8_interop.rs @@ -103,7 +103,6 @@ pub(crate) fn emit_v8_export_call( let argc = lowered_args.len(); let alloca_count = if argc == 0 { 1 } else { argc }; - let blk = ctx.block(); let argc_lit = format!("{}", argc); let spec_ptr = format!("@{}", spec_global); let name_ptr = format!("@{}", name_global); @@ -112,12 +111,10 @@ pub(crate) fn emit_v8_export_call( // Stack-allocate the args buffer (zero-len → still need a pointer; an // `alloca [1 x double]` is well-formed in LLVM and never dereferenced - // because argc=0 in that branch of the runtime). - let args_slot = blk.fresh_reg(); - blk.emit_raw(format!( - "{} = alloca [{} x double], align 8", - args_slot, alloca_count - )); + // because argc=0 in that branch of the runtime). #10463: in the entry + // block, so a call inside a loop does not grow the stack per iteration. + let args_slot = ctx.func.alloca_entry_array(DOUBLE, alloca_count); + let blk = ctx.block(); for (i, v) in lowered_args.iter().enumerate() { let slot = blk.fresh_reg(); blk.emit_raw(format!( @@ -207,7 +204,6 @@ pub(crate) fn emit_v8_member_method_call( let argc = lowered_args.len(); let alloca_count = if argc == 0 { 1 } else { argc }; - let blk = ctx.block(); let argc_lit = format!("{}", argc); let spec_ptr = format!("@{}", spec_global); let member_ptr = format!("@{}", member_global); @@ -216,11 +212,9 @@ pub(crate) fn emit_v8_member_method_call( let member_len_lit = format!("{}", member_bytes); let method_len_lit = format!("{}", method_bytes); - let args_slot = blk.fresh_reg(); - blk.emit_raw(format!( - "{} = alloca [{} x double], align 8", - args_slot, alloca_count - )); + // #10463: entry-block args buffer (see `emit_v8_export_call`). + let args_slot = ctx.func.alloca_entry_array(DOUBLE, alloca_count); + let blk = ctx.block(); for (i, v) in lowered_args.iter().enumerate() { let slot = blk.fresh_reg(); blk.emit_raw(format!( diff --git a/crates/perry-codegen/src/expr/worker_new.rs b/crates/perry-codegen/src/expr/worker_new.rs index e4e34b727d..12224f2c05 100644 --- a/crates/perry-codegen/src/expr/worker_new.rs +++ b/crates/perry-codegen/src/expr/worker_new.rs @@ -49,7 +49,9 @@ pub(super) fn lower_candidates( let bits = ctx.block().bitcast_double_to_i64(&file); let tag = ctx.block().lshr(I64, &bits, "48"); let is_url = ctx.block().icmp_eq(I64, &tag, POINTER_TAG_TOP16_I64); - let normalized = ctx.block().alloca(DOUBLE); + // #10463: both join slots are entry-block allocas, so a `new Worker` + // inside a loop does not grow the stack per iteration. + let normalized = ctx.func.alloca_entry(DOUBLE); let url_block = ctx.new_block("worker_url"); let string_block = ctx.new_block("worker_string"); let dispatch = ctx.new_block("worker_dispatch"); @@ -75,7 +77,7 @@ pub(super) fn lower_candidates( .call(I64, "js_get_string_pointer_unified", &[(DOUBLE, &file)]); // Comparisons below do not allocate. Only the selected spawn can // collect, after the last use of `spec`; options remain rooted. - let result = ctx.block().alloca(DOUBLE); + let result = ctx.func.alloca_entry(DOUBLE); let join = ctx.new_block("worker_join"); for (path, target) in &aliases { let key = ctx.strings.intern(path); diff --git a/crates/perry-codegen/src/function.rs b/crates/perry-codegen/src/function.rs index 541869cda4..dff3fb48e2 100644 --- a/crates/perry-codegen/src/function.rs +++ b/crates/perry-codegen/src/function.rs @@ -15,6 +15,9 @@ use crate::types::LlvmType; /// #7173 / #7174). A sibling file only because of the 2,000-line cap. mod precise_roots; +/// #10463: the entry-block `alloca` invariant, enforced on the final stream. +mod entry_allocas; + use precise_roots::{lower_precise_roots_to_native_stack, retype_landing_pads_for_statepoints}; pub struct LlFunction { @@ -1111,12 +1114,19 @@ impl LlFunction { usize::MAX }; let mut idx = 0usize; + let mut in_entry_block = is_entry; for inst in blk.insts() { if idx == boundary { for line in &self.entry_post_init_setup { self.text_item(line, rewrite_rets, &mut seq, sink)?; } } + entry_allocas::refuse_alloca_outside_entry_block( + &self.name, + &blk.label, + inst, + &mut in_entry_block, + ); self.inst_item(inst, rewrite_rets, &mut seq, sink)?; idx += 1; } diff --git a/crates/perry-codegen/src/function/entry_allocas.rs b/crates/perry-codegen/src/function/entry_allocas.rs new file mode 100644 index 0000000000..26bd6a9c4d --- /dev/null +++ b/crates/perry-codegen/src/function/entry_allocas.rs @@ -0,0 +1,210 @@ +//! #10463: every `alloca` a function emits lives in its LLVM entry block. +//! +//! LLVM lowers an `alloca` outside the entry block as a runtime stack-pointer +//! bump that is not undone until the function returns. Inside a loop every +//! iteration therefore consumes stack for good: `d.setTime(i)` took 16 B per +//! iteration and a two-million-iteration loop died with SIGSEGV at a point +//! that moved with `ulimit -s`. #167 added +//! [`LlFunction::alloca_entry_array`](super::LlFunction::alloca_entry_array) +//! for one family of call sites; eight sibling lowerings kept emitting +//! `alloca [N x double]` into whatever block was current, and the HIR-level +//! cross-module inliner copied them into callers' loops (date-fns +//! `addMinutes`). +//! +//! Fixing call sites one at a time is how the class survived #167, so the +//! invariant is enforced where every function body is finalized: +//! [`LlFunction::for_each_final_item`](super::LlFunction::for_each_final_item), +//! the single funnel both the textual and the native backends consume. An +//! entry-block slot comes from `LlFunction::alloca_entry*`, which splices it +//! ahead of block 0's instructions. The only other legal spelling is +//! `LlBlock::alloca` while block 0 is current (the parameter prologues). Any +//! `alloca` instruction the stream places after that point is refused. + +use crate::inst::LlInst; + +/// Refuse `inst` if it is an `alloca` outside the entry block. +/// +/// `in_entry_block` is true while the stream is still inside the LLVM entry +/// block: the caller starts it `true` for block 0 and `false` for every other +/// block, and a label inside an instruction stream (the invoke-EH +/// continuation `emit_inline_label` writes) ends the entry block part-way +/// through block 0, so it is cleared here. +pub(super) fn refuse_alloca_outside_entry_block( + function: &str, + block: &str, + inst: &LlInst, + in_entry_block: &mut bool, +) { + match inst { + LlInst::Alloca { .. } if !*in_entry_block => { + let mut line = String::new(); + inst.render_into(&mut line); + refuse(function, block, &line); + } + LlInst::Raw(text) => { + for line in text.split('\n') { + if is_label_line(line) { + *in_entry_block = false; + } else if !*in_entry_block && is_alloca_line(line) { + refuse(function, block, line); + } + } + } + _ => {} + } +} + +/// A flush-left `name:` line — the same column-0 rule the IR-reading scripts +/// anchor labels on (see `LlBlock::emit_inline_label`). Instructions carry a +/// two-space indent and never end in `:`. +fn is_label_line(line: &str) -> bool { + line.ends_with(':') + && line + .as_bytes() + .first() + .is_some_and(|b| !b.is_ascii_whitespace() && *b != b';') +} + +/// `%reg = alloca …`, whatever the allocated type. +fn is_alloca_line(line: &str) -> bool { + let line = line.trim_start(); + line.starts_with('%') + && line + .split_once(" = ") + .is_some_and(|(_, rhs)| rhs.starts_with("alloca ")) +} + +#[cold] +#[inline(never)] +fn refuse(function: &str, block: &str, line: &str) -> ! { + panic!( + "perry-codegen: `{}` is emitted in block `{block}` of @{function}, outside the \ + function's entry block. A non-entry alloca bumps the stack pointer at run time and \ + is not released until the function returns, so every loop iteration through it \ + consumes stack until the process dies with SIGSEGV (#167, #10463). Allocate the \ + slot with `LlFunction::alloca_entry` / `alloca_entry_array` instead.", + line.trim() + ) +} + +#[cfg(test)] +mod tests { + use super::super::LlFunction; + use crate::types::{DOUBLE, I64, PTR}; + + fn probe() -> LlFunction { + let mut f = LlFunction::new("perry_fn_alloca_probe", DOUBLE, Vec::new()); + let _ = f.create_block("entry"); + f + } + + fn entry_prologue_ends_at(ir: &str, first_non_entry_label: &str) -> usize { + ir.lines() + .position(|line| line == format!("{first_non_entry_label}:")) + .unwrap_or_else(|| panic!("no `{first_non_entry_label}:` label in:\n{ir}")) + } + + /// The control: the helpers put the slot in block 0 even when the block + /// being lowered is a loop body, and the body keeps only the uses. + #[test] + fn entry_helpers_hoist_the_slot_out_of_the_current_block() { + let mut f = probe(); + let body_label = f.create_block("for.body").label.clone(); + let buf = f.alloca_entry_array(DOUBLE, 2); + let out = f.alloca_entry(I64); + { + let blk = f.block_mut(1).unwrap(); + let slot = blk.gep(DOUBLE, &buf, &[(I64, "1")]); + blk.store(DOUBLE, "0.0", &slot); + blk.call(DOUBLE, "js_consume", &[(PTR, &buf), (PTR, &out)]); + blk.ret(DOUBLE, "0.0"); + } + f.block_mut(0).unwrap().br(&body_label); + let ir = f.to_ir(); + let body_starts = entry_prologue_ends_at(&ir, &body_label); + let alloca_lines: Vec = ir + .lines() + .enumerate() + .filter(|(_, line)| line.contains(" = alloca ")) + .map(|(i, _)| i) + .collect(); + assert_eq!(alloca_lines.len(), 2, "both slots rendered:\n{ir}"); + assert!( + alloca_lines.iter().all(|&i| i < body_starts), + "every alloca must precede the loop body's label:\n{ir}" + ); + } + + /// The bug shape the eight #10463 lowerings emitted: a raw `alloca` text + /// line in a loop body. + #[test] + #[should_panic(expected = "outside the function's entry block")] + fn a_raw_alloca_in_a_loop_body_is_refused() { + let mut f = probe(); + let _ = f.create_block("for.body"); + let blk = f.block_mut(1).unwrap(); + let buf = blk.next_reg(); + blk.emit_raw(format!("{buf} = alloca [1 x double]")); + blk.call(DOUBLE, "js_date_apply_setter", &[(PTR, &buf)]); + let _ = f.to_ir(); + } + + /// The typed spelling of the same mistake (`blk.alloca` in a non-entry + /// block — what `Expr::ArraySplice`'s out-parameter used). + #[test] + #[should_panic(expected = "outside the function's entry block")] + fn a_typed_alloca_in_a_non_entry_block_is_refused() { + let mut f = probe(); + let _ = f.create_block("splice"); + let _ = f.block_mut(1).unwrap().alloca(I64); + let _ = f.to_ir(); + } + + /// Multi-line raw payloads are split and checked line by line. + #[test] + #[should_panic(expected = "outside the function's entry block")] + fn an_alloca_inside_a_multi_line_raw_payload_is_refused() { + let mut f = probe(); + let _ = f.create_block("body"); + f.block_mut(1) + .unwrap() + .emit_raw("%a = add i64 1, 2\n %b = alloca double, align 8"); + let _ = f.to_ir(); + } + + /// An inline label ends the entry block part-way through block 0: an + /// `alloca` after it is in a different LLVM basic block. + #[test] + #[should_panic(expected = "outside the function's entry block")] + fn an_alloca_after_an_inline_label_in_block_zero_is_refused() { + let mut f = probe(); + let blk = f.block_mut(0).unwrap(); + blk.insts_mut() + .push(crate::inst::LlInst::Raw("eh.cont0:".to_string())); + let _ = blk.alloca(DOUBLE); + let _ = f.to_ir(); + } + + /// The native backend consumes the item stream, not the text: it is + /// refused there too. + #[test] + #[should_panic(expected = "outside the function's entry block")] + fn the_native_item_stream_refuses_it_too() { + let mut f = probe(); + let _ = f.create_block("body"); + let _ = f.block_mut(1).unwrap().alloca(DOUBLE); + let _ = f.for_each_final_item::<()>(&mut |_| Ok(())); + } + + /// The prologue spelling stays legal: `LlBlock::alloca` while block 0 is + /// current, before any inline label. + #[test] + fn a_typed_alloca_in_the_entry_prologue_is_accepted() { + let mut f = probe(); + let blk = f.block_mut(0).unwrap(); + let slot = blk.alloca(DOUBLE); + blk.store(DOUBLE, "0.0", &slot); + blk.ret(DOUBLE, "0.0"); + assert!(f.to_ir().contains(" = alloca double")); + } +} diff --git a/crates/perry-codegen/src/lower_array_method.rs b/crates/perry-codegen/src/lower_array_method.rs index b007c6f444..3b66f4cf99 100644 --- a/crates/perry-codegen/src/lower_array_method.rs +++ b/crates/perry-codegen/src/lower_array_method.rs @@ -49,8 +49,8 @@ use anyhow::{bail, Result}; use perry_hir::Expr; use crate::expr::{ - emit_root_nanbox_store_on_block, emit_write_barrier, nanbox_pointer_inline, - nanbox_string_inline, unbox_to_i64, FnCtx, + emit_root_nanbox_store_on_block, emit_write_barrier, lower_js_args_array, + nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx, }; use crate::nanbox::{double_literal, TAG_UNDEFINED}; use crate::rooting; @@ -302,21 +302,12 @@ pub(crate) fn lower_array_method( // // The buffer stores are pure, so the group's re-read above them // is the last thing that has to happen below a collection point. + let recv_handle = unbox_to_i64(ctx.block(), recv_box); + // No args: a null buffer + 0 count (concat() returns a copy). + // #10463: otherwise an entry-block buffer — allocated in the + // current block it grew the stack on every loop iteration. + let (buf_reg, count_str) = lower_js_args_array(ctx, &arg_vals); let blk = ctx.block(); - let recv_handle = unbox_to_i64(blk, recv_box); - let n = arg_vals.len(); - let (buf_reg, count_str) = if n == 0 { - // No args: pass a null buffer + 0 count (concat() returns a copy). - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; let result = blk.call( I64, "js_array_concat_variadic", @@ -901,19 +892,8 @@ pub(crate) fn lower_array_method( // items at the front in source order via the variadic helper. // The (possibly reallocated) array forwards from its old pointer, // so in-place mutation stays visible to the receiver slot. - let (buf_ptr, count_str) = if arg_vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = arg_vals.len(); - let blk = ctx.block(); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; + // #10463: an entry-block buffer (null/0 for no arguments). + let (buf_ptr, count_str) = lower_js_args_array(ctx, &arg_vals); let recv_handle = { let blk = ctx.block(); unbox_to_i64(blk, recv_box) @@ -968,8 +948,10 @@ pub(crate) fn lower_array_method( "2147483647.0".to_string() }; let item_vals: Vec = arg_vals.iter().skip(2).cloned().collect(); + // #10463: the out-parameter and the item buffer are entry-block + // allocas; `blk.alloca` here would grow the stack per iteration. + let out_slot = ctx.func.alloca_entry(I64); let blk = ctx.block(); - let out_slot = blk.alloca(I64); blk.store(I64, "0", &out_slot); let recv_handle = unbox_to_i64(blk, recv_box); // ToIntegerOrInfinity via the clamping helper: `fptosi` on @@ -981,19 +963,8 @@ pub(crate) fn lower_array_method( blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]); let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]); - let (items_ptr, items_count_str) = if item_vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = item_vals.len(); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in item_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; - let deleted_handle = blk.call( + let (items_ptr, items_count_str) = lower_js_args_array(ctx, &item_vals); + let deleted_handle = ctx.block().call( I64, "js_array_splice", &[ diff --git a/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs b/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs index 852532ffdd..4af99b144b 100644 --- a/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs +++ b/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs @@ -354,11 +354,13 @@ } } let arr_box = lower_expr(ctx, recv)?; + // #10463: the fused push's length out-parameter is an entry-block + // alloca; `blk.alloca` here grew the stack on every loop iteration. + let length_slot = u31_value.as_ref().map(|_| ctx.func.alloca_entry(I32)); let blk = ctx.block(); let mut arr_handle = unbox_to_i64(blk, &arr_box); let orig_handle = arr_handle.clone(); - let fused_length_slot = if let Some(value) = u31_value { - let length_slot = blk.alloca(I32); + let fused_length_slot = if let Some((value, length_slot)) = u31_value.zip(length_slot) { let fast_handle = blk.call( I64, "js_array_push_u31_with_length", diff --git a/test-files/_helpers/add_minutes_10463.ts b/test-files/_helpers/add_minutes_10463.ts new file mode 100644 index 0000000000..ff31942fa1 --- /dev/null +++ b/test-files/_helpers/add_minutes_10463.ts @@ -0,0 +1,9 @@ +// Helper for test_gap_10463_entry_block_allocas.ts: the date-fns 4.4.0 +// `addMinutes` shape (`addMinutes.js:31-34`). Imported, so that the +// cross-module inliner copies its `setTime` call into the caller's loop. +export function addMinutes(date: Date | number, amount: number): Date { + const _date = new Date(date instanceof Date ? date.getTime() : date); + if (isNaN(amount)) return new Date(NaN); + _date.setTime(_date.getTime() + amount * 60_000); + return _date; +} diff --git a/test-files/test_gap_10463_entry_block_allocas.ts b/test-files/test_gap_10463_entry_block_allocas.ts new file mode 100644 index 0000000000..5e0480a432 --- /dev/null +++ b/test-files/test_gap_10463_entry_block_allocas.ts @@ -0,0 +1,123 @@ +// #10463: several 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. Inside a loop that is a stack bump on every +// iteration, released only when the function returns, so these loops died +// with SIGSEGV once they had consumed the stack (~2^19 iterations for one +// 16-byte buffer at the default 8 MB). Each loop below would take at least +// 19 MB of stack that way on its own; with the buffers in the entry block it +// runs in constant stack. + +import { addMinutes } from "./_helpers/add_minutes_10463.ts"; + +function dateSetters(n: number): number { + const d = new Date(0); + let acc = 0; + for (let i = 0; i < n; i++) { + d.setTime(i * 1000); + acc = (acc + d.getUTCSeconds()) % 1_000_003; + d.setUTCMinutes(i % 60); + d.setUTCFullYear(2000 + (i % 30), i % 12, 1 + (i % 28)); + d.setUTCHours(i % 24, i % 60, i % 60, i % 1000); + acc = (acc + d.getUTCHours() + d.getUTCMonth() + d.getUTCMinutes()) % 1_000_003; + } + return acc + d.getTime(); +} + +function dateUtc(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + acc = (acc + (Date.UTC(2000 + (i % 50), i % 12) % 7919)) % 1_000_003; + acc = (acc + (Date.UTC(1970, 0, 1 + (i % 28), i % 24) % 7907)) % 1_000_003; + } + return acc; +} + +function toSpliced(n: number): number { + const a = [1, 2, 3]; + let acc = 0; + for (let i = 0; i < n; i++) { + const b = a.toSpliced(1, 1, i); + const c = b.toSpliced(0, 2, i, i + 1, i + 2); + acc = (acc + b[1] + c[2] + b.length + c.length) % 1_000_003; + } + return acc; +} + +function concat(n: number): number { + const a = [1, 2, 3]; + let acc = 0; + for (let i = 0; i < n; i++) { + const b = a.concat(i); + const c = b.concat([i + 1], i + 2); + acc = (acc + b[3] + c[5] + b.length + c.length) % 1_000_003; + } + return acc; +} + +function spliceLocal(n: number): number { + const a = [1, 2, 3]; + let acc = 0; + for (let i = 0; i < n; i++) { + const removed = a.splice(1, 1, i); + const none = a.splice(1, 0); + acc = (acc + removed[0] + none.length + a[1] + a.length) % 1_000_003; + } + return acc + a[0] + a[2]; +} + +class Holder { + arr: number[] = [1, 2, 3]; +} + +function unshiftSpliceField(n: number): number { + const h = new Holder(); + let acc = 0; + for (let i = 0; i < n; i++) { + h.arr.unshift(i); + h.arr.shift(); + const removed = h.arr.splice(0, 1, i); + acc = (acc + removed[0] + h.arr[0] + h.arr.length) % 1_000_003; + } + return acc; +} + +function arrayPrototypeCall(n: number): number { + const a: number[] = []; + let acc = 0; + for (let i = 0; i < n; i++) { + Array.prototype.push.call(a, i, i + 1); + Array.prototype.unshift.call(a, i + 2); + const removed: any = Array.prototype.splice.call(a, 0, 3); + const joined: any = Array.prototype.concat.call(removed, i); + acc = (acc + removed[1] + joined.length + joined[3] + a.length) % 1_000_003; + } + // The same generic lowerings over a plain array-like object. + const o: any = { length: 0 }; + Array.prototype.push.call(o, 1, 2); + Array.prototype.unshift.call(o, 0); + const r: any = Array.prototype.splice.call(o, 1, 1); + const j: any = Array.prototype.concat.call([7], o); + return acc + o.length * 10 + r[0] + o[1] + j.length; +} + +// The date-fns 4.4.0 `addMinutes` shape, imported: the cross-module inliner +// copies the helper's `setTime` call into this loop. +function addMinutesLoop(n: number): number { + let d = new Date(Date.UTC(2020, 0, 1)); + let acc = 0; + for (let i = 0; i < n; i++) { + const next = addMinutes(d, 1); + d = next; + acc = (acc + d.getUTCMinutes()) % 1_000_003; + } + return acc + d.getTime(); +} + +console.log("date setters", dateSetters(250_000)); +console.log("Date.UTC", dateUtc(600_000)); +console.log("toSpliced", toSpliced(600_000)); +console.log("concat", concat(600_000)); +console.log("splice (local)", spliceLocal(400_000)); +console.log("unshift/splice (field)", unshiftSpliceField(400_000)); +console.log("Array.prototype.*.call", arrayPrototypeCall(300_000)); +console.log("addMinutes in a loop", addMinutesLoop(1_200_000));