From 027ea58e7b84c4b99d84adb89e187f40be1eb8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 05:46:10 +0200 Subject: [PATCH 1/3] refactor(codegen): migrate arrays_finds.rs + array_methods.rs onto the Layer 1 rooting API (#7615) Layer 1 campaign slice 1b: `expr/arrays_finds.rs` and `expr/array_methods.rs` migrated end to end onto `crate::rooting` and added to `MIGRATED_MODULES`. Adds one combinator, `rooting::with_operands_rooted_across`, with its two callers: the `u8[i]` / `buf[i]` tails root the receiver across an index lowering whose representation the caller picks (`lower_expr_as_i32` vs `fptosi`), which the fixed re-read point of `with_operands_rooted` cannot express. `with_operands_rooted` is now the empty-`across` case of it, so "root, re-derive or reuse?" stays answered in one place. --- .../perry-codegen/src/expr/array_methods.rs | 300 ++++++---- crates/perry-codegen/src/expr/arrays_finds.rs | 527 +++++++++++------- crates/perry-codegen/src/rooting.rs | 81 ++- 3 files changed, 590 insertions(+), 318 deletions(-) diff --git a/crates/perry-codegen/src/expr/array_methods.rs b/crates/perry-codegen/src/expr/array_methods.rs index 257e0760f5..490200a2eb 100644 --- a/crates/perry-codegen/src/expr/array_methods.rs +++ b/crates/perry-codegen/src/expr/array_methods.rs @@ -3,11 +3,51 @@ //! Extracted from `expr/mod.rs` to keep that file under the 2000-line cap. //! Pure mechanical move — match arm bodies are verbatim copies, called from //! `lower_expr`'s outer dispatch. +//! +//! # Layer 1 migrated module (#7615, slice 1b) +//! +//! Nothing in here names `expr::temp_root`; every operand that is live across +//! the lowering of a sibling operand goes through +//! [`crate::rooting::with_operands_rooted`], which lowers the group left to +//! right with each already-evaluated value rooted across the ones that follow, +//! re-reads them below the last collection point, and owns the release on every +//! path out including `?`. `crate::rooting::migration_ledger` fails the build if +//! this module reaches back into the raw API. +//! +//! A single-operand arm keeps its plain `lower_expr` call, as the template +//! module (`expr/url_main.rs`, #7617) does: with nothing lowered after it there +//! is no window, `operand_protection` would answer `Reuse`, and wrapping it +//! would emit the same IR through more machinery. +//! +//! ## What the migration found +//! +//! `Expr::BufferSlice` is the one arm here that held a **raw, already-unboxed** +//! pointer across user code: +//! +//! ```text +//! let buf_box = lower_expr(buffer) // NaN-boxed BufferHeader +//! let buf_handle = unbox_to_i64(buf_box) // RAW pointer, in a register +//! let start_box = lower_expr(start) // arbitrary user code -- allocates +//! let end_box = lower_expr(end) // ditto +//! js_buffer_slice(buf_handle, ...) // reads the PRE-MOVE address +//! ``` +//! +//! That is #7453's shape with the extra twist that the value in flight is not +//! even NaN-boxed any more, so #7280's `root_reload` post-pass cannot help: it +//! re-reads a shadow slot into a `double`, and the consuming call reads an `i64` +//! derived above the window. The unbox now happens below the group's re-read, +//! which is the only place it can be correct. +//! +//! The other windows closed here are ordinary operand-to-operand ones — +//! `AggregateErrorNew`, `BufferConcatWithLength`, `ObjectCreate`, both +//! `FinalizationRegistry` mutators and the two `ErrorNew*` forms — where an +//! earlier NaN-boxed operand sat in a register while a later one ran. use anyhow::Result; use perry_hir::Expr; use crate::nanbox::double_literal; +use crate::rooting; use crate::types::{DOUBLE, I32, I64}; use super::{ @@ -87,24 +127,32 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // (NOT an array pointer) so Sets / strings / generators / any // iterable can be consumed and non-iterables rejected with a // TypeError. #2836: apply the optional `{ cause }`. - let errors_box = lower_expr(ctx, errors)?; - let m = lower_expr(ctx, message)?; - let options_box = match options { - Some(o) => lower_expr(ctx, o)?, - None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), - }; - let blk = ctx.block(); - let msg_handle = unbox_to_i64(blk, &m); - let err_handle = blk.call( - I64, - "js_aggregateerror_new_full", - &[ - (DOUBLE, &errors_box), - (I64, &msg_handle), - (DOUBLE, &options_box), - ], - ); - Ok(nanbox_pointer_inline(blk, &err_handle)) + // + // `errors` was live in a register across `message`'s lowering and + // `message` across the options bag's — both arbitrary user code. + let mut operands: Vec<&Expr> = vec![errors, message]; + if let Some(o) = options { + operands.push(o); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let errors_box = vals[0].clone(); + let m = vals[1].clone(); + let options_box = vals.get(2).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let blk = ctx.block(); + let msg_handle = unbox_to_i64(blk, &m); + let err_handle = blk.call( + I64, + "js_aggregateerror_new_full", + &[ + (DOUBLE, &errors_box), + (I64, &msg_handle), + (DOUBLE, &options_box), + ], + ); + Ok(nanbox_pointer_inline(blk, &err_handle)) + }) } // -------- RegExpLastIndex — regex.lastIndex getter -------- @@ -131,17 +179,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Ok(nanbox_pointer_inline(blk, &buf_handle)) } Expr::BufferConcatWithLength { list, total_length } => { - let arr_box = lower_expr(ctx, list)?; - let total_box = lower_expr(ctx, total_length)?; - let blk = ctx.block(); - // #2013: validate `list` is an Array (see BufferConcat above). - let arr_handle = blk.call(I64, "js_buffer_validate_concat_list", &[(DOUBLE, &arr_box)]); - let buf_handle = blk.call( - I64, - "js_buffer_concat_with_length", - &[(I64, &arr_handle), (DOUBLE, &total_box)], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + // The list is live across `total_length`'s lowering. + rooting::with_operands_rooted(ctx, &[list, total_length], |ctx, vals| { + let arr_box = vals[0].clone(); + let total_box = vals[1].clone(); + let blk = ctx.block(); + // #2013: validate `list` is an Array (see BufferConcat above). + let arr_handle = + blk.call(I64, "js_buffer_validate_concat_list", &[(DOUBLE, &arr_box)]); + let buf_handle = blk.call( + I64, + "js_buffer_concat_with_length", + &[(I64, &arr_handle), (DOUBLE, &total_box)], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) + }) } // #1177: `buf.slice(start?, end?)` on a statically buffer-producing @@ -156,30 +208,50 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // allocated Buffer registered in BUFFER_REGISTRY) so the result has // its own backing storage independent of the parent's lifetime. Expr::BufferSlice { buffer, start, end } => { - let buf_box = lower_expr(ctx, buffer)?; - let blk = ctx.block(); - let buf_handle = unbox_to_i64(blk, &buf_box); - // Default start=0, end=buf.length. `js_buffer_slice` itself - // handles end-clamping via `.min(len)`, so we can pass i32::MAX - // when end is omitted to mean "to the end" — matches how the - // Node API treats `buf.slice(start)` (no end → to the end). - let start_box = match start { - Some(e) => lower_expr(ctx, e)?, - None => double_literal(0.0), - }; - let end_box = match end { - Some(e) => lower_expr(ctx, e)?, - None => double_literal(i32::MAX as f64), - }; - let blk = ctx.block(); - let start_i32 = blk.fptosi(DOUBLE, &start_box, I32); - let end_i32 = blk.fptosi(DOUBLE, &end_box, I32); - let result = blk.call( - I64, - "js_buffer_slice", - &[(I64, &buf_handle), (I32, &start_i32), (I32, &end_i32)], - ); - Ok(nanbox_pointer_inline(blk, &result)) + // The receiver used to be unboxed to a RAW BufferHeader pointer + // before `start` and `end` were lowered, so the pointer the call + // read was the pre-move address whenever either argument ran user + // code. See the module header: this is the one shape in this file + // that #7280's `root_reload` structurally cannot repair, because + // what is stale is an `i64` derived above the window rather than + // the `double` a slot re-read would produce. The unbox is now + // emitted below the group's re-read. + let mut operands: Vec<&Expr> = vec![buffer]; + if let Some(e) = start { + operands.push(e); + } + if let Some(e) = end { + operands.push(e); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let buf_box = vals[0].clone(); + // Default start=0, end=buf.length. `js_buffer_slice` itself + // handles end-clamping via `.min(len)`, so we can pass i32::MAX + // when end is omitted to mean "to the end" — matches how the + // Node API treats `buf.slice(start)` (no end → to the end). + let mut next = 1; + let start_box = if start.is_some() { + next += 1; + vals[next - 1].clone() + } else { + double_literal(0.0) + }; + let end_box = if end.is_some() { + vals[next].clone() + } else { + double_literal(i32::MAX as f64) + }; + let blk = ctx.block(); + let buf_handle = unbox_to_i64(blk, &buf_box); + let start_i32 = blk.fptosi(DOUBLE, &start_box, I32); + let end_i32 = blk.fptosi(DOUBLE, &end_box, I32); + let result = blk.call( + I64, + "js_buffer_slice", + &[(I64, &buf_handle), (I32, &start_i32), (I32, &end_i32)], + ); + Ok(nanbox_pointer_inline(blk, &result)) + }) } // -------- BufferIsBuffer -------- @@ -224,17 +296,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // #2816: route through `js_object_create_with_props` so prototype // validation + the optional descriptor bag are handled uniformly. // Pass `undefined` for the props arg when only one argument was - // supplied. - let v = lower_expr(ctx, p)?; - let props_val = match props { - Some(props_expr) => lower_expr(ctx, props_expr)?, - None => crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), - }; - Ok(ctx.block().call( - DOUBLE, - "js_object_create_with_props", - &[(DOUBLE, &v), (DOUBLE, &props_val)], - )) + // supplied. The prototype is live across the descriptor bag's + // lowering, which for the usual `Object.create(proto, {…})` shape + // is an object literal — i.e. an allocation. + let mut operands: Vec<&Expr> = vec![p]; + if let Some(props_expr) = props { + operands.push(props_expr); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let v = vals[0].clone(); + let props_val = vals.get(1).cloned().unwrap_or_else(|| { + crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + Ok(ctx.block().call( + DOUBLE, + "js_object_create_with_props", + &[(DOUBLE, &v), (DOUBLE, &props_val)], + )) + }) } Expr::MathClz32(o) => { let v = lower_math_operand(ctx, o)?; @@ -267,42 +346,52 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { token, } => { // `reg.register(target, held, token?)` — always returns undefined. - let reg = lower_expr(ctx, registry)?; - let tgt = lower_expr(ctx, target)?; - let h = lower_expr(ctx, held)?; - let tok = if let Some(token_expr) = token { - lower_expr(ctx, token_expr)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_finreg_register", - &[(DOUBLE, ®), (DOUBLE, &tgt), (DOUBLE, &h), (DOUBLE, &tok)], - )) + // Four operands, each live across every one that follows it. + let mut operands: Vec<&Expr> = vec![registry, target, held]; + if let Some(token_expr) = token { + operands.push(token_expr); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let reg = vals[0].clone(); + let tgt = vals[1].clone(); + let h = vals[2].clone(); + let tok = vals.get(3).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + Ok(ctx.block().call( + DOUBLE, + "js_finreg_register", + &[(DOUBLE, ®), (DOUBLE, &tgt), (DOUBLE, &h), (DOUBLE, &tok)], + )) + }) } Expr::FinalizationRegistryUnregister { registry, token } => { // `reg.unregister(token)` — returns NaN-boxed boolean. - let reg = lower_expr(ctx, registry)?; - let tok = lower_expr(ctx, token)?; - Ok(ctx.block().call( - DOUBLE, - "js_finreg_unregister", - &[(DOUBLE, ®), (DOUBLE, &tok)], - )) + rooting::with_operands_rooted(ctx, &[registry, token], |ctx, vals| { + let reg = vals[0].clone(); + let tok = vals[1].clone(); + Ok(ctx.block().call( + DOUBLE, + "js_finreg_unregister", + &[(DOUBLE, ®), (DOUBLE, &tok)], + )) + }) } Expr::ErrorNewWithCause { message, cause } => { // new Error(msg, { cause }). Runtime stores the cause - // on the ErrorHeader so `e.cause` returns it. - let msg = lower_expr(ctx, message)?; - let c = lower_expr(ctx, cause)?; - let blk = ctx.block(); - let err_handle = blk.call( - I64, - "js_error_new_with_cause_from_value", - &[(DOUBLE, &msg), (DOUBLE, &c)], - ); - Ok(nanbox_pointer_inline(blk, &err_handle)) + // on the ErrorHeader so `e.cause` returns it. The message string is + // live across the cause's evaluation. + rooting::with_operands_rooted(ctx, &[message, cause], |ctx, vals| { + let msg = vals[0].clone(); + let c = vals[1].clone(); + let blk = ctx.block(); + let err_handle = blk.call( + I64, + "js_error_new_with_cause_from_value", + &[(DOUBLE, &msg), (DOUBLE, &c)], + ); + Ok(nanbox_pointer_inline(blk, &err_handle)) + }) } Expr::ErrorNewWithOptions { kind, @@ -313,16 +402,19 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // runtime value (variable or dynamic object). The runtime reads // the `cause` property off `options` and stamps the right // ERROR_KIND_* so `instanceof TypeError`/etc. still hold. - let msg = lower_expr(ctx, message)?; - let opts = lower_expr(ctx, options)?; - let blk = ctx.block(); - let kind_lit = (*kind as i64).to_string(); - let err_handle = blk.call( - I64, - "js_error_new_kind_with_options_from_value", - &[(I32, &kind_lit), (DOUBLE, &msg), (DOUBLE, &opts)], - ); - Ok(nanbox_pointer_inline(blk, &err_handle)) + let kind = *kind; + rooting::with_operands_rooted(ctx, &[message, options], |ctx, vals| { + let msg = vals[0].clone(); + let opts = vals[1].clone(); + let blk = ctx.block(); + let kind_lit = (kind as i64).to_string(); + let err_handle = blk.call( + I64, + "js_error_new_kind_with_options_from_value", + &[(I32, &kind_lit), (DOUBLE, &msg), (DOUBLE, &opts)], + ); + Ok(nanbox_pointer_inline(blk, &err_handle)) + }) } Expr::EnvGet(name) => { // process.env.HOME -> js_getenv("HOME") -> string handle diff --git a/crates/perry-codegen/src/expr/arrays_finds.rs b/crates/perry-codegen/src/expr/arrays_finds.rs index 43a7dd572b..7f48d6482c 100644 --- a/crates/perry-codegen/src/expr/arrays_finds.rs +++ b/crates/perry-codegen/src/expr/arrays_finds.rs @@ -3,6 +3,49 @@ //! Extracted from `expr/mod.rs` to keep that file under the 2000-line cap. //! Pure mechanical move — match arm bodies are verbatim copies, called from //! `lower_expr`'s outer dispatch. +//! +//! # Layer 1 migrated module (#7615, slice 1b) +//! +//! Nothing in here names `expr::temp_root`; every operand that is live across +//! the lowering of a sibling goes through [`crate::rooting`], which lowers the +//! group left to right with each already-evaluated value rooted across the ones +//! that follow, re-reads them below the last collection point, and owns the +//! release on every path out including `?`. +//! `crate::rooting::migration_ledger` fails the build if this module reaches +//! back into the raw API. +//! +//! A single-operand arm keeps its plain `lower_expr` call, as the template +//! module (`expr/url_main.rs`, #7617) does: with nothing lowered after it there +//! is no window and `operand_protection` would answer `Reuse`. +//! +//! ## What the migration found +//! +//! The marquee window is `arr.find(cb)` and its three siblings +//! (`findIndex`/`findLast`/`findLastIndex`): the array was lowered first and +//! held in a register while the callback was lowered, and a callback literal is +//! a `js_closure_new` — an allocation, in the single most common shape this +//! family is written in. Same for `Object.is(a, b)`, `Object.hasOwn(o, k)`, +//! `path.matchesGlob(p, pat)`, the `Map`/`Set` positional readers, the +//! multi-argument `new Date(...)` (each component live across the next), the +//! three-operand `NativeArenaView` / `NativePodView` and the polymorphic +//! `u8[k] = v` store. +//! +//! ## The index arms need a re-read point this module controls +//! +//! `u8[i]` and `buf[i]` lower the receiver with `lower_expr` and the index with +//! [`lower_index_i32`], which picks between an `i32` fast path and a `double` +//! plus `fptosi`. The receiver is live across that choice, but handing the index +//! to `with_operands_rooted` would force every such read back onto the NaN-boxed +//! path. [`crate::rooting::with_operands_rooted_across`] exists for exactly this +//! shape and arrived with these callers: the group is rooted before the +//! caller-controlled lowering and re-read after it. +//! +//! ## Not claimed +//! +//! `lower_buffer_load` / `lower_buffer_store` / `lower_expr_as_i32` lower their +//! own operands, in modules this slice does not migrate; the arms here call them +//! with no GC-managed register live, which is why they are left alone rather +//! than wrapped. use anyhow::{anyhow, bail, Result}; use perry_hir::types::Type as HirType; @@ -13,6 +56,7 @@ use crate::native_value::{ layout_runtime_id, BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, PodLayoutManifest, SemanticKind, }; +use crate::rooting; use crate::type_analysis::is_numeric_expr; use crate::types::{DOUBLE, I32, I64, PTR}; @@ -71,15 +115,17 @@ pub(crate) fn lower_uint8array_get_i32( // numeric keys to the byte read and string keys to the property path); the // proven-numeric fast paths above are untouched. if !is_numeric_expr(ctx, index) { - let a = lower_expr(ctx, array)?; - let key = lower_expr(ctx, index)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &a); - let result = blk.call( - DOUBLE, - "js_object_get_index_polymorphic", - &[(I64, &handle), (DOUBLE, &key)], - ); + let result = rooting::with_operands_rooted(ctx, &[array, index], |ctx, vals| { + let a = vals[0].clone(); + let key = vals[1].clone(); + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &a); + Ok(blk.call( + DOUBLE, + "js_object_get_index_polymorphic", + &[(I64, &handle), (DOUBLE, &key)], + )) + })?; return Ok(LoweredValue::js_value(result)); } @@ -117,29 +163,33 @@ pub(crate) fn lower_native_pod_view_with_layout( count: &Expr, layout: &PodLayoutManifest, ) -> Result { - let owner_value = lower_expr(ctx, owner)?; - let byte_offset = lower_expr(ctx, byte_offset)?; - let count = lower_expr(ctx, count)?; - let blk = ctx.block(); - let owner_handle = unbox_to_i64(blk, &owner_value); - let byte_offset_i64 = blk.fptosi(DOUBLE, &byte_offset, I64); - let count_i64 = blk.fptosi(DOUBLE, &count, I64); - let stride_i64 = layout.size.to_string(); - let alignment_i64 = layout.alignment.to_string(); - let layout_id = (layout_runtime_id(&layout.layout_id) as i64).to_string(); - let view = blk.call( - I64, - "js_native_pod_view", - &[ - (I64, &owner_handle), - (I64, &byte_offset_i64), - (I64, &count_i64), - (I64, &stride_i64), - (I64, &alignment_i64), - (I64, &layout_id), - ], - ); - Ok(nanbox_pointer_inline(blk, &view)) + // The arena owner is live across both the offset's and the count's + // lowering, and the byte offset across the count's. + rooting::with_operands_rooted(ctx, &[owner, byte_offset, count], |ctx, vals| { + let owner_value = vals[0].clone(); + let byte_offset = vals[1].clone(); + let count = vals[2].clone(); + let blk = ctx.block(); + let owner_handle = unbox_to_i64(blk, &owner_value); + let byte_offset_i64 = blk.fptosi(DOUBLE, &byte_offset, I64); + let count_i64 = blk.fptosi(DOUBLE, &count, I64); + let stride_i64 = layout.size.to_string(); + let alignment_i64 = layout.alignment.to_string(); + let layout_id = (layout_runtime_id(&layout.layout_id) as i64).to_string(); + let view = blk.call( + I64, + "js_native_pod_view", + &[ + (I64, &owner_handle), + (I64, &byte_offset_i64), + (I64, &count_i64), + (I64, &stride_i64), + (I64, &alignment_i64), + (I64, &layout_id), + ], + ); + Ok(nanbox_pointer_inline(blk, &view)) + }) } pub(crate) fn lower_native_pod_view( @@ -217,6 +267,21 @@ pub(crate) fn lower_buffer_index_get_i32( Ok(slow) } +/// The `(array_handle, callback_handle)` pair the four `arr.find*` runtime +/// entry points take, derived from an already-re-read operand group. +/// +/// One helper rather than four copies, because the emission ORDER is the thing +/// under test: the receiver's unbox must be below the group's re-read, and +/// `js_validate_array_callback` (#4091, a non-callable callback must raise the +/// spec TypeError before any iteration) must still be emitted between the unbox +/// and the consuming call, exactly where it was. +fn unbox_array_and_callback(ctx: &mut FnCtx<'_>, vals: &[String]) -> (String, String) { + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &vals[1])]); + (arr_handle, cb_handle) +} + pub(crate) fn lower( ctx: &mut FnCtx<'_>, expr: &Expr, @@ -270,91 +335,92 @@ pub(crate) fn lower( // minute?, second?, ms?)` in local time. dayjs's parseDate // takes this branch with regex-captured string args — see // js_date_new_local_components for the coercion path. - let mut vals: Vec = Vec::with_capacity(7); - for a in args.iter().take(7) { - vals.push(lower_expr(ctx, a)?); - } - // Pad *absent* trailing components with their ECMA-262 default - // literal (slot 2 `day` → 1, time slots 3-6 → 0) rather than - // `undefined`, so the runtime can run a plain ToNumber on every - // forwarded slot: a *present* `undefined` then coerces to NaN - // (Invalid Date), while a truly-omitted arg uses its default. - // Slots: 0 year, 1 month, 2 day, 3 hour, 4 min, 5 sec, 6 ms. - while vals.len() < 7 { - let default = if vals.len() == 2 { 1.0 } else { 0.0 }; - vals.push(double_literal(default)); - } - let blk = ctx.block(); - let call_args: Vec<(crate::types::LlvmType, &str)> = - vals.iter().map(|v| (DOUBLE, v.as_str())).collect(); - Ok(blk.call(DOUBLE, "js_date_new_local_components", &call_args)) + // + // Those regex-captured components are heap strings, and each one + // sat in a register while the components after it were lowered. + let operands: Vec<&Expr> = args.iter().take(7).collect(); + rooting::with_operands_rooted(ctx, &operands, |ctx, rooted| { + let mut vals: Vec = rooted.to_vec(); + // Pad *absent* trailing components with their ECMA-262 + // default literal (slot 2 `day` → 1, time slots 3-6 → 0) + // rather than `undefined`, so the runtime can run a plain + // ToNumber on every forwarded slot: a *present* `undefined` + // then coerces to NaN (Invalid Date), while a truly-omitted + // arg uses its default. Slots: 0 year, 1 month, 2 day, + // 3 hour, 4 min, 5 sec, 6 ms. + while vals.len() < 7 { + let default = if vals.len() == 2 { 1.0 } else { 0.0 }; + vals.push(double_literal(default)); + } + let blk = ctx.block(); + let call_args: Vec<(crate::types::LlvmType, &str)> = + vals.iter().map(|v| (DOUBLE, v.as_str())).collect(); + Ok(blk.call(DOUBLE, "js_date_new_local_components", &call_args)) + }) } }, // -------- arr.find(cb) / findIndex(cb) / findLast(cb) / findLastIndex(cb) -------- + // + // All four have the same skeleton, and had the same window: the array + // was lowered first and left in an SSA register while the callback was + // lowered. A callback literal lowers to `js_closure_new`, so the window + // allocates in the shape these are almost always written in + // (`xs.find(x => x.id === id)`). The receiver is now rooted across it and + // the unbox happens below the re-read. Expr::ArrayFind { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - Ok(blk.call( - DOUBLE, - "js_array_find", - &[(I64, &arr_handle), (I64, &cb_handle)], - )) + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let (arr_handle, cb_handle) = unbox_array_and_callback(ctx, vals); + Ok(ctx.block().call( + DOUBLE, + "js_array_find", + &[(I64, &arr_handle), (I64, &cb_handle)], + )) + }) } Expr::ArrayFindIndex { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - let i32_v = blk.call( - I32, - "js_array_findIndex", - &[(I64, &arr_handle), (I64, &cb_handle)], - ); - Ok(blk.sitofp(I32, &i32_v, DOUBLE)) + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let (arr_handle, cb_handle) = unbox_array_and_callback(ctx, vals); + let blk = ctx.block(); + let i32_v = blk.call( + I32, + "js_array_findIndex", + &[(I64, &arr_handle), (I64, &cb_handle)], + ); + Ok(blk.sitofp(I32, &i32_v, DOUBLE)) + }) } Expr::ArrayFindLast { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - Ok(blk.call( - DOUBLE, - "js_array_find_last", - &[(I64, &arr_handle), (I64, &cb_handle)], - )) + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let (arr_handle, cb_handle) = unbox_array_and_callback(ctx, vals); + Ok(ctx.block().call( + DOUBLE, + "js_array_find_last", + &[(I64, &arr_handle), (I64, &cb_handle)], + )) + }) } Expr::ArrayFindLastIndex { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - let i32_v = blk.call( - I32, - "js_array_find_last_index", - &[(I64, &arr_handle), (I64, &cb_handle)], - ); - Ok(blk.sitofp(I32, &i32_v, DOUBLE)) + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let (arr_handle, cb_handle) = unbox_array_and_callback(ctx, vals); + let blk = ctx.block(); + let i32_v = blk.call( + I32, + "js_array_find_last_index", + &[(I64, &arr_handle), (I64, &cb_handle)], + ); + Ok(blk.sitofp(I32, &i32_v, DOUBLE)) + }) } // -------- Object.is, Number.isInteger, etc. -------- - Expr::ObjectIs(a, b) => { - let av = lower_expr(ctx, a)?; - let bv = lower_expr(ctx, b)?; + Expr::ObjectIs(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { + let av = vals[0].clone(); + let bv = vals[1].clone(); Ok(ctx .block() .call(DOUBLE, "js_object_is", &[(DOUBLE, &av), (DOUBLE, &bv)])) - } + }), Expr::NumberIsInteger(operand) => { // Runtime already returns NaN-tagged TAG_TRUE/TAG_FALSE. let v = lower_expr(ctx, operand)?; @@ -396,17 +462,19 @@ pub(crate) fn lower( // entries straight out of the Map's internal buffer instead of // calling `js_map_entries` (which materializes N+1 small Arrays). Expr::MapEntryKeyAt { map, idx } | Expr::MapEntryValueAt { map, idx } => { - let m_box = lower_expr(ctx, map)?; - let i_dbl = lower_expr(ctx, idx)?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let i_i32 = blk.fptosi(DOUBLE, &i_dbl, I32); let runtime_fn = match expr { Expr::MapEntryKeyAt { .. } => "js_map_entry_key_at", Expr::MapEntryValueAt { .. } => "js_map_entry_value_at", _ => unreachable!(), }; - Ok(blk.call(DOUBLE, runtime_fn, &[(I64, &m_handle), (I32, &i_i32)])) + rooting::with_operands_rooted(ctx, &[map, idx], |ctx, vals| { + let m_box = vals[0].clone(); + let i_dbl = vals[1].clone(); + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + let i_i32 = blk.fptosi(DOUBLE, &i_dbl, I32); + Ok(blk.call(DOUBLE, runtime_fn, &[(I64, &m_handle), (I32, &i_i32)])) + }) } // -------- Set direct-element fast path -------- @@ -414,16 +482,18 @@ pub(crate) fn lower( // without materializing the buffer into an Array. Used by the // `for (const x of setExpr)` HIR fast path. Expr::SetValueAt { set, idx } => { - let s_box = lower_expr(ctx, set)?; - let i_dbl = lower_expr(ctx, idx)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let i_i32 = blk.fptosi(DOUBLE, &i_dbl, I32); - Ok(blk.call( - DOUBLE, - "js_set_value_at", - &[(I64, &s_handle), (I32, &i_i32)], - )) + rooting::with_operands_rooted(ctx, &[set, idx], |ctx, vals| { + let s_box = vals[0].clone(); + let i_dbl = vals[1].clone(); + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + let i_i32 = blk.fptosi(DOUBLE, &i_dbl, I32); + Ok(blk.call( + DOUBLE, + "js_set_value_at", + &[(I64, &s_handle), (I32, &i_i32)], + )) + }) } // -------- Set.values (set → array conversion for iteration) -------- @@ -517,21 +587,23 @@ pub(crate) fn lower( )) } Expr::PathMatchesGlob(p, pat) => { - let p_box = lower_expr(ctx, p)?; - let pat_box = lower_expr(ctx, pat)?; - let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); - let pat_handle = unbox_to_i64(blk, &pat_box); - let i32_v = blk.call( - I32, - "js_path_matches_glob", - &[(I64, &p_handle), (I64, &pat_handle)], - ); - Ok(i32_bool_to_nanbox(blk, &i32_v)) + rooting::with_operands_rooted(ctx, &[p, pat], |ctx, vals| { + let p_box = vals[0].clone(); + let pat_box = vals[1].clone(); + let blk = ctx.block(); + let p_handle = unbox_to_i64(blk, &p_box); + let pat_handle = unbox_to_i64(blk, &pat_box); + let i32_v = blk.call( + I32, + "js_path_matches_glob", + &[(I64, &p_handle), (I64, &pat_handle)], + ); + Ok(i32_bool_to_nanbox(blk, &i32_v)) + }) } - Expr::PathResolveJoin(a, b) => { - let a_box = lower_expr(ctx, a)?; - let b_box = lower_expr(ctx, b)?; + Expr::PathResolveJoin(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { + let a_box = vals[0].clone(); + let b_box = vals[1].clone(); let blk = ctx.block(); let a_handle = unbox_to_i64(blk, &a_box); let b_handle = unbox_to_i64(blk, &b_box); @@ -541,20 +613,22 @@ pub(crate) fn lower( &[(I64, &a_handle), (I64, &b_handle)], ); Ok(nanbox_string_inline(blk, &result)) - } + }), Expr::ProcessVersion => { let blk = ctx.block(); let handle = blk.call(I64, "js_process_version", &[]); Ok(nanbox_string_inline(blk, &handle)) } Expr::ObjectHasOwn(obj, key) => { - let obj_box = lower_expr(ctx, obj)?; - let key_box = lower_expr(ctx, key)?; - Ok(ctx.block().call( - DOUBLE, - "js_object_has_own", - &[(DOUBLE, &obj_box), (DOUBLE, &key_box)], - )) + rooting::with_operands_rooted(ctx, &[obj, key], |ctx, vals| { + let obj_box = vals[0].clone(); + let key_box = vals[1].clone(); + Ok(ctx.block().call( + DOUBLE, + "js_object_has_own", + &[(DOUBLE, &obj_box), (DOUBLE, &key_box)], + )) + }) } Expr::NumberIsNaN(operand) => { // Number.isNaN is strict: only returns true for actual @@ -721,13 +795,17 @@ pub(crate) fn lower( // `@@iterator`). The dynamic-index helper below stringifies the key // and would miss them. if matches!(index.as_ref(), Expr::SymbolFor(_)) { - let a = lower_expr(ctx, array)?; - let key = lower_expr(ctx, index)?; - return Ok(ctx.block().call( - DOUBLE, - "js_object_get_symbol_property", - &[(DOUBLE, &a), (DOUBLE, &key)], - )); + // `Symbol.for(k)` interns — it allocates a SymbolHeader on + // first use — so the receiver was live across an allocation. + return rooting::with_operands_rooted(ctx, &[array, index], |ctx, vals| { + let a = vals[0].clone(); + let key = vals[1].clone(); + Ok(ctx.block().call( + DOUBLE, + "js_object_get_symbol_property", + &[(DOUBLE, &a), (DOUBLE, &key)], + )) + }); } if let Some(value) = lower_buffer_load(ctx, array, index, BufferAccessSpec::uint8array_get())? @@ -736,15 +814,17 @@ pub(crate) fn lower( return Ok(materialize_js_value(ctx, value, reason)); } if !numeric_index_has_integer_array_index_proof(ctx, index) { - let a = lower_expr(ctx, array)?; - let key = lower_expr(ctx, index)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &a); - return Ok(blk.call( - DOUBLE, - "js_typed_array_index_get_dynamic", - &[(I64, &handle), (DOUBLE, &key)], - )); + return rooting::with_operands_rooted(ctx, &[array, index], |ctx, vals| { + let a = vals[0].clone(); + let key = vals[1].clone(); + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &a); + Ok(blk.call( + DOUBLE, + "js_typed_array_index_get_dynamic", + &[(I64, &handle), (DOUBLE, &key)], + )) + }); } // #6088: a proven non-negative integer key whose value is NOT // proven in bounds (the inline load above bailed). The native i32 @@ -752,15 +832,26 @@ pub(crate) fn lower( // a JS-value `u8[i]` must instead read `undefined` (ECMAScript // IntegerIndexedExotic `[[Get]]`). In-range reads still return the // byte as a number. - let a = lower_expr(ctx, array)?; - let idx_i32 = lower_index_i32(ctx, index)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &a); - Ok(blk.call( - DOUBLE, - "js_uint8array_index_get_value", - &[(I64, &handle), (I32, &idx_i32)], - )) + // + // The receiver is lowered first (spec order: the MemberExpression's + // base before its key) and was live across `lower_index_i32`, which + // lowers arbitrary user code. See the module header for why the + // index cannot simply join the operand list. + rooting::with_operands_rooted_across( + ctx, + &[array], + &[index], + |ctx| lower_index_i32(ctx, index), + |ctx, vals, idx_i32| { + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &vals[0]); + Ok(blk.call( + DOUBLE, + "js_uint8array_index_get_value", + &[(I64, &handle), (I32, &idx_i32)], + )) + }, + ) } Expr::BufferIndexGet { buffer, index } => { // Proven-bounds inline load keeps the native fast path. @@ -772,15 +863,24 @@ pub(crate) fn lower( } // #6088: out-of-range → `undefined`, not the `0` byte-sentinel the // native `js_buffer_get` accessor is forced to return. - let a = lower_expr(ctx, buffer)?; - let idx_i32 = lower_index_i32(ctx, index)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &a); - Ok(blk.call( - DOUBLE, - "js_buffer_index_get_value", - &[(I64, &handle), (I32, &idx_i32)], - )) + // + // Same shape as `Uint8ArrayGet`'s tail above: receiver live across + // the caller-controlled index lowering. + rooting::with_operands_rooted_across( + ctx, + &[buffer], + &[index], + |ctx| lower_index_i32(ctx, index), + |ctx, vals, idx_i32| { + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &vals[0]); + Ok(blk.call( + DOUBLE, + "js_buffer_index_get_value", + &[(I64, &handle), (I32, &idx_i32)], + )) + }, + ) } Expr::Uint8ArraySet { array, @@ -808,24 +908,30 @@ pub(crate) fn lower( // store; the polymorphic setter dispatches numeric keys to the // byte write and string keys to the own-prop table. let key_maybe_string = !is_numeric_expr(ctx, index); - let a = lower_expr(ctx, array)?; - let key = lower_expr(ctx, index)?; - let val = lower_expr(ctx, value)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &a); - let result = if key_maybe_string { - blk.call_void( - "js_object_set_index_polymorphic", - &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], - ); - val.clone() - } else { - blk.call( - DOUBLE, - "js_typed_array_index_set_dynamic", - &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], - ) - }; + // Receiver live across BOTH the key's and the value's lowering, + // key live across the value's — the `m.set(k, v)` shape + // `RootedOperands::push` documents. + let result = + rooting::with_operands_rooted(ctx, &[array, index, value], |ctx, vals| { + let a = vals[0].clone(); + let key = vals[1].clone(); + let val = vals[2].clone(); + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &a); + Ok(if key_maybe_string { + blk.call_void( + "js_object_set_index_polymorphic", + &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], + ); + val.clone() + } else { + blk.call( + DOUBLE, + "js_typed_array_index_set_dynamic", + &[(I64, &handle), (DOUBLE, &key), (DOUBLE, &val)], + ) + }) + })?; if value_discarded { return Ok(double_literal(0.0)); } @@ -1073,25 +1179,28 @@ pub(crate) fn lower( byte_offset, length, } => { - let owner_value = lower_expr(ctx, owner)?; - let byte_offset = lower_expr(ctx, byte_offset)?; - let length = lower_expr(ctx, length)?; - let blk = ctx.block(); - let owner_handle = unbox_to_i64(blk, &owner_value); - let kind_i32 = (*kind as i32).to_string(); - let byte_offset_i64 = blk.fptosi(DOUBLE, &byte_offset, I64); - let length_i64 = blk.fptosi(DOUBLE, &length, I64); - let view = blk.call( - I64, - "js_native_arena_view", - &[ - (I64, &owner_handle), - (I32, &kind_i32), - (I64, &byte_offset_i64), - (I64, &length_i64), - ], - ); - Ok(nanbox_pointer_inline(blk, &view)) + let kind = *kind; + rooting::with_operands_rooted(ctx, &[owner, byte_offset, length], |ctx, vals| { + let owner_value = vals[0].clone(); + let byte_offset = vals[1].clone(); + let length = vals[2].clone(); + let blk = ctx.block(); + let owner_handle = unbox_to_i64(blk, &owner_value); + let kind_i32 = (kind as i32).to_string(); + let byte_offset_i64 = blk.fptosi(DOUBLE, &byte_offset, I64); + let length_i64 = blk.fptosi(DOUBLE, &length, I64); + let view = blk.call( + I64, + "js_native_arena_view", + &[ + (I64, &owner_handle), + (I32, &kind_i32), + (I64, &byte_offset_i64), + (I64, &length_i64), + ], + ); + Ok(nanbox_pointer_inline(blk, &view)) + }) } Expr::NativePodView { diff --git a/crates/perry-codegen/src/rooting.rs b/crates/perry-codegen/src/rooting.rs index b1d7697b80..4cce03df74 100644 --- a/crates/perry-codegen/src/rooting.rs +++ b/crates/perry-codegen/src/rooting.rs @@ -452,12 +452,64 @@ pub(crate) fn with_operands_rooted<'f, R>( exprs: &[&Expr], body: impl FnOnce(&mut FnCtx<'f>, &[String]) -> Result, ) -> Result { - let (values, guard) = crate::expr::temp_root::lower_exprs_rooted(ctx, exprs)?; - let out = body(ctx, &values); + with_operands_rooted_across(ctx, exprs, &[], |_| Ok(()), |ctx, vals, ()| body(ctx, vals)) +} + +/// [`with_operands_rooted`], but with a caller-controlled lowering step wedged +/// between the operand group and its re-read. +/// +/// `across` lowers `across_exprs` in a representation this API cannot produce — +/// today that is `expr::arrays_finds`'s index lowering, which picks between the +/// `i32` fast path (`lower_expr_as_i32`) and a `double` + `fptosi` from the +/// expression's proven integer range. Feeding those indexes to +/// [`with_operands_rooted`] instead would force every `u8[i]` back onto the +/// NaN-boxed path, which is a codegen-quality regression rather than a rooting +/// fix. +/// +/// **Why the plain form cannot serve.** Its re-read point is fixed at the end of +/// the operand list, so an operand lowered before caller-controlled work is +/// re-read *above* that work and is stale again by the time the call runs — the +/// exact half-measure #7114 is. Here the group is rooted before `across` runs +/// and re-read after it, so `body` sees post-collection values. +/// +/// `across_exprs` is used for one thing: deciding whether the window collects at +/// all. It is not lowered here — `across` owns that — so passing the +/// expressions rather than a `bool` keeps "does this window collect?" answered +/// by `operand_protection` like every other site, instead of by the caller. +/// When neither the later operands nor `across_exprs` can collect, nothing is +/// pushed and the emitted IR is unchanged. +/// +/// The release still happens on every path out, including `across`'s `?`. +pub(crate) fn with_operands_rooted_across<'f, T, R>( + ctx: &mut FnCtx<'f>, + exprs: &[&Expr], + across_exprs: &[&Expr], + across: impl FnOnce(&mut FnCtx<'f>) -> Result, + body: impl FnOnce(&mut FnCtx<'f>, &[String], T) -> Result, +) -> Result { + use crate::expr::temp_root::{any_may_trigger_gc, root_operands_begin}; + + let across_collects = any_may_trigger_gc(ctx, across_exprs.iter().copied()); + let mut group = root_operands_begin(exprs.len()); + let out = (|| { + // Incremental, one operand at a time: each is rooted BEFORE the next is + // lowered. Rooting a finished list afterwards is worse than doing + // nothing — it publishes an already-dangling pointer into a slot the + // collector scans (`root_operands_begin`'s doc, #6969). + for (i, expr) in exprs.iter().enumerate() { + let value = crate::expr::lower_expr(ctx, expr)?; + let collects = + across_collects || any_may_trigger_gc(ctx, exprs[i + 1..].iter().copied()); + group.push(ctx, expr, &value, collects); + } + let extra = across(ctx)?; + let values = group.reread(ctx, exprs)?; + body(ctx, &values, extra) + })(); // Released after `body`'s consuming call, which itself allocates -- and on - // the error path too, so a lowering that bails does not leave the group - // pushed. - crate::expr::temp_root::temp_root_release(ctx, guard); + // every error path too, including a bail from the operand lowering itself, + // so a lowering that fails does not leave the group pushed. + group.release(ctx); out } @@ -489,6 +541,17 @@ pub(crate) fn with_operands_rooted<'f, R>( /// from an unstarted one, and that is what let the half-migration hide. /// /// No boundary is outstanding today. +/// +/// **Listing a module that never used the escape hatch passes vacuously.** That +/// is true of every module migrated so far except `expr/url_main.rs`: they named +/// no `temp_root` symbol before the migration, so +/// `migrated_modules_do_not_reach_past_the_rooting_api` went green the instant +/// the line was added. The listing only means something if the slice ALSO ran +/// the sabotage arm — inject a real, compiling `temp_root_push_*` / +/// `temp_root_truncate` pair into the migrated module, confirm the ledger test +/// goes red and names the lines, then revert. Slices 1a and 1b both did, and +/// recorded it in their PRs; a slice that skips it is adding a line that asserts +/// nothing. #[cfg(test)] const MIGRATED_MODULES: &[(&str, &str)] = &[ ( @@ -499,6 +562,14 @@ const MIGRATED_MODULES: &[(&str, &str)] = &[ "crates/perry-codegen/src/lower_array_method.rs", include_str!("lower_array_method.rs"), ), + ( + "crates/perry-codegen/src/expr/arrays_finds.rs", + include_str!("expr/arrays_finds.rs"), + ), + ( + "crates/perry-codegen/src/expr/array_methods.rs", + include_str!("expr/array_methods.rs"), + ), ]; /// Lines in `src` that reach past [`crate::rooting`] into the raw rooting API. From bb23d2ac6fbbeb82464b112d5db0a355b2be7326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 06:02:00 +0200 Subject: [PATCH 2/3] docs(changelog): add slice 1b fragment (#7620) --- ...620-layer1-slice1b-arrays-finds-rooting.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 changelog.d/7620-layer1-slice1b-arrays-finds-rooting.md diff --git a/changelog.d/7620-layer1-slice1b-arrays-finds-rooting.md b/changelog.d/7620-layer1-slice1b-arrays-finds-rooting.md new file mode 100644 index 0000000000..2edc184fc8 --- /dev/null +++ b/changelog.d/7620-layer1-slice1b-arrays-finds-rooting.md @@ -0,0 +1,60 @@ +### Layer 1 rooting migration, slice 1b — `expr/arrays_finds.rs` + `expr/array_methods.rs` (#7615) + +Both modules now root every operand through `crate::rooting` and are listed in +`MIGRATED_MODULES`; neither names `expr::temp_root`. Follows the template +(#7617) and slice 1a (#7618). + +**A new combinator, with its callers.** `rooting::with_operands_rooted_across` +roots an operand group across a lowering step whose *representation* the caller +picks — here `u8[i]` / `buf[i]`, where the index goes through +`lower_expr_as_i32` or `fptosi` and the receiver is live across that choice. +`with_operands_rooted`'s re-read point is fixed at the end of its operand list, +so an operand lowered before caller-controlled work would be re-read above it +and stale again by the call, which is the #7114 half-measure. `across_exprs` is +passed as expressions rather than a `bool` so "does this window collect?" stays +inside `operand_protection`; `with_operands_rooted` is now the empty-`across` +case of it, keeping that decision in one place. + +**Windows closed.** `Expr::BufferSlice` unboxed the receiver to a **raw** +`BufferHeader*` *before* lowering `start` and `end`, so `buf.slice(f(), g())` +read a pre-move address — and because what was stale was an `i64` derived above +the window rather than a `double`, #7280's `root_reload` structurally could not +repair it. The four `arr.find*` arms held the array in a register while the +callback was lowered, and a callback literal is a `js_closure_new`. Plus the +ordinary operand-to-operand windows: `AggregateError`, `Buffer.concat(list, +total)`, `Object.create(proto, props)`, both `FinalizationRegistry` mutators, +both `ErrorNew*` forms, `Object.is`, `Object.hasOwn`, `path.matchesGlob`, +`path.resolve`'s pairwise joins, the `Map`/`Set` positional readers, the +multi-argument `new Date(…)`, `NativeArenaView` / `NativePodView`, and the +polymorphic `u8[k] = v` store. + +**Scoped honestly.** For a shadow-slotted local receiver #7280 had already +repaired the `find` window — visible in the baseline IR as an out-of-sequence +re-read, and accounting for all 16 removed `load double` instructions. What this +slice genuinely adds is the three shapes #7280 cannot cover: a receiver +reassigned by its own argument (where re-loading would observe the assignment, +so only a temp root gives both the call-time value and a rewritten address), a +raw already-unboxed pointer, and operands with no slot at all. + +**Verified locally** (CI backlog is deep, so this is the evidence): IR +byte-identical up to register/label renaming on 8 purpose-built probes (172 +functions, 164 identical, 8 differing — all `main`, net delta 100% root +plumbing, nothing deleted) and over the whole `gc-root-dominance` corpus (2452 +functions, 2443 identical; 6 root-plumbing diffs and 3 that are pre-existing +compiler nondeterminism, proven by compiling one source twice with the same +binary). `gc-root-dominance` green in both gated modes with `--seeded-violations +40` at 40/40 and root stores up 9810 → 9826 (the gate's subject was live); +`-p perry-codegen --lib` 691 pass and `--doc`'s two `compile_fail,E0499` arms +still reject; `-p perry-runtime --no-fail-fast` 1886 pass on the first run; 60 +gap tests over 17 family filters identical on both arms; probes byte-identical +again under `PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` with the protector +confirmed live (5 retired sets/run). Ledger sabotage run per module: a real +`temp_root_*` pair turns the ledger red naming the exact lines, in both files. + +**Reported, not fixed here.** `path.resolve(base, f())` throws +`ERR_INVALID_ARG_TYPE` where node returns the path: `Expr::PathResolveJoin` +unboxes with `unbox_to_i64`, so a short computed string's inline SSO bytes are +read as a `StringHeader*` (#214 class, bisected by string length, present on +both arms). The correct helper allocates, which opens the #7213 window in the +same arm — a rooting change with its own combinator question, not something to +hide inside a refactor. From acf2117a7cea9f058c449261fd35ea85ee338fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 06:16:00 +0200 Subject: [PATCH 3/3] chore(version): bump to 0.5.1355 --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a38887efd3..9375e4e3b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1354 +**Current Version:** 0.5.1355 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 7d7cd5213b..61bb49b197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1354" +version = "0.5.1355" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1354" +version = "0.5.1355" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1354" +version = "0.5.1355" [[package]] name = "perry-ui-tvos" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1354" +version = "0.5.1355" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 6989854881..8f91aab0c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1354" +version = "0.5.1355" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"