From 0c4d65b1091a69bcb1b371afe4141d656747ea85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 23:11:03 +0200 Subject: [PATCH 1/4] fix(gc): close #7210's two remaining unrooted-alloca sites + #7640 A/C findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7210: root the two flagship interleaved-staging sites the earlier alloca enumeration left open. `codegen/helpers.rs`'s `emit_namespace_populator` staged every re-exported binding's value into a plain stack alloca while the per-entry loop called allocating helpers (closure singleton alloc, arbitrary cross-module getters); an already-staged entry had no root and could go stale before `js_create_namespace` read the whole buffer. Fixed by rooting each value in a RootedGroup as it's produced and deferring every store to a second, call-free pass that runs immediately before the consuming call. `lower_call/early_branches.rs`'s `obj[strKey](args)` computed-key dispatch lowered receiver/key/args into bare registers in sequence, and `unbox_str_handle` (an allocating SSO materialisation) ran between the args buffer's stores and the call in the static-key arm. Fixed the same way: root `[object, index, ...args]` in one RootedGroup, build the args buffer last in each branch. #7640 section A: three more index_set.rs arms with no rooting decision at all, now fixed — the bounded-index-pair array store (the issue's sharpest repro), `globalThis[k] = v`, and the width-tracked typed-array non-numeric-index store. Not reached: the #5525 recv_unknown inline dyn-TA store, and the TA runtime-key / TA final-fallback / Uint8Array runtime-key arms. #7640 section C: resolved, not mechanically fixed. Two property_set.rs comments claimed a class-field store's receiver survives an allocating RHS via "the same statepoint re-read" a sibling arm relies on. That mechanism doesn't exist. Traced to ground: for a bare LocalGet/This receiver the claim is true, via root_reload.rs (#7280) — a front-end pass independent of RS4GC that re-materialises a value derived from a shadow-slot or handle-global load below any collection point it doesn't dominate; verified with the checker on both lowerings against a new fixture. For a compound receiver (`this.target.x = allocPoint(n).x`, the receiver itself a class-field READ) the claim is false, and the gap is invisible to both root_reload (no shadow slot to re-derive from — the value is a phi over two field-get paths) and the stale-register checker (its pattern match only anchors on a direct `load double, ptr ` source). Left unfixed deliberately: rooting the receiver unconditionally would tax the dominant plain-local case this investigation just proved needs nothing, on what the issue calls the hottest store path in the compiler — a measured-cost tradeoff for a follow-up, not a mechanical gap. All four sites that made or relied on the original claim now say this precisely. #7640 section E: triaged, not fixed. All seven named callees were read, not skipped — each looks like a hazard that is not one (single already-rooted caller, address derived after the allocating step rather than before, or typed-array immovability per CLAUDE.md's own GC section). Static triage only, not checker-verified per-callee; a lead for a follow-up. New corpus fixtures: test_gap_gc_namespace_and_computed_dispatch_rooting.ts (+ fixtures/gc_namespace_rooting_pkg/), test_gap_gc_index_set_bounded_globalthis_ta_rooting.ts, and an expanded test_gap_gc_class_field_receiver_rooting.ts covering both halves of the section C finding. Verified: scripts/gc_root_dominance_check.py's gated invocations (--unrooted-allocas --moving-only; --statepoints --moving-only) read clean on the full corpus, both lowerings, with an empty allowlist. cargo test -p perry-codegen has pre-existing failures unrelated to this change (see PR description for the exact set against the current main baseline). --- crates/perry-codegen/src/codegen/helpers.rs | 185 ++++--- crates/perry-codegen/src/expr/index_set.rs | 465 ++++++++++-------- crates/perry-codegen/src/expr/property_set.rs | 59 ++- .../src/lower_call/early_branches.rs | 146 ++++-- .../fixtures/gc_namespace_rooting_pkg/lib.ts | 22 + .../gc_namespace_rooting_pkg/other.ts | 13 + ...est_gap_gc_class_field_receiver_rooting.ts | 122 +++++ ...index_set_bounded_globalthis_ta_rooting.ts | 80 +++ ...namespace_and_computed_dispatch_rooting.ts | 73 +++ 9 files changed, 820 insertions(+), 345 deletions(-) create mode 100644 test-files/fixtures/gc_namespace_rooting_pkg/lib.ts create mode 100644 test-files/fixtures/gc_namespace_rooting_pkg/other.ts create mode 100644 test-files/test_gap_gc_class_field_receiver_rooting.ts create mode 100644 test-files/test_gap_gc_index_set_bounded_globalthis_ta_rooting.ts create mode 100644 test-files/test_gap_gc_namespace_and_computed_dispatch_rooting.ts diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index d8d5df03f2..72bdeaa5f1 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -1470,87 +1470,114 @@ pub(super) fn emit_namespace_populator( let vals_buf = blk.next_reg(); blk.emit_raw(format!("{} = alloca [{} x double]", vals_buf, buf_len)); - // Per-entry: store key ptr + len + value. - for (i, entry) in entries.iter().enumerate() { - let (key_global, key_len) = &key_globals[i]; - let idx_str = format!("{}", i); - let blk = ctx.block(); - - // keys[i] = @ as ptr - let key_slot = blk.gep(PTR, &keys_buf, &[(I64, &idx_str)]); - blk.store(PTR, &format!("@{}", key_global), &key_slot); - - // key_lens[i] = byte_len - let len_slot = blk.gep(I32, &lens_buf, &[(I64, &idx_str)]); - blk.store(I32, &format!("{}", key_len), &len_slot); - - // Materialise the value per kind. We drop the `blk` borrow so - // each sub-emission can re-borrow ctx mutably for runtime calls - // / declares; then re-acquire for the store. - let val_str = match &entry.kind { - NamespaceEntryKind::LocalVar { global_name } => { - ctx.block().load(DOUBLE, &format!("@{}", global_name)) - } - NamespaceEntryKind::LocalFunction { wrap_symbol } => { - let blk = ctx.block(); - let handle = blk.call( - I64, - "js_closure_alloc_singleton", - &[(PTR, &format!("@{}", wrap_symbol))], - ); - crate::expr::nanbox_pointer_inline(blk, &handle) - } - NamespaceEntryKind::LocalClass { class_id } => { - // INT32-tagged class-id NaN-box: 0x7FFE_0000_0000_0000 | - // (class_id & 0xFFFFFFFF). Matches `Expr::ClassRef`. - let bits = crate::nanbox::INT32_TAG | (*class_id as u64 & 0xFFFF_FFFF); - crate::nanbox::double_literal(f64::from_bits(bits)) - } - NamespaceEntryKind::ForeignVar { - source_prefix, - source_local, - } => { - let getter = format!("perry_fn_{}__{}", source_prefix, sanitize(source_local)); - ctx.pending_declares.push((getter.clone(), DOUBLE, vec![])); - ctx.block().call(DOUBLE, &getter, &[]) - } - NamespaceEntryKind::ForeignFunction { - source_prefix, - source_local, - param_count, - } => { - // Function-shaped re-exports must materialize a function - // value, not call the function while building the namespace. - // Source modules emit `__perry_wrap_perry_fn___` - // for every user function; hand that wrapper to the same - // singleton allocator used by local function exports. - let wrapper_name = format!( - "__perry_wrap_perry_fn_{}__{}", + // #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 + // a real GC pointer (a closure singleton, a nested namespace object, a + // re-exported var read through an arbitrary getter), and materialising + // entry i+1 (`js_closure_alloc_singleton`, or calling an exported + // getter) can allocate -- so storing entry i into `vals_buf` and then + // moving on left it an unrooted stack copy while later entries were + // computed. Root every value as it is produced, in one `RootedGroup` for + // the whole populator, and defer every `vals_buf` store to a second pass + // that runs back-to-back with the `js_create_namespace` call below — + // nothing in that second pass can collect, so the buffer is guaranteed + // fresh at the moment the runtime reads it. + let mut handles = Vec::with_capacity(n); + crate::rooting::with_rooted_group(ctx, buf_len, |ctx, group| { + // Per-entry: store key ptr + len, and root the value. + for (i, entry) in entries.iter().enumerate() { + let (key_global, key_len) = &key_globals[i]; + let idx_str = format!("{}", i); + let blk = ctx.block(); + + // keys[i] = @ as ptr + let key_slot = blk.gep(PTR, &keys_buf, &[(I64, &idx_str)]); + blk.store(PTR, &format!("@{}", key_global), &key_slot); + + // key_lens[i] = byte_len + let len_slot = blk.gep(I32, &lens_buf, &[(I64, &idx_str)]); + blk.store(I32, &format!("{}", key_len), &len_slot); + + // Materialise the value per kind. We drop the `blk` borrow so + // each sub-emission can re-borrow ctx mutably for runtime calls + // / declares; then root it in this scope's group. + let val_str = match &entry.kind { + NamespaceEntryKind::LocalVar { global_name } => { + ctx.block().load(DOUBLE, &format!("@{}", global_name)) + } + NamespaceEntryKind::LocalFunction { wrap_symbol } => { + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_closure_alloc_singleton", + &[(PTR, &format!("@{}", wrap_symbol))], + ); + crate::expr::nanbox_pointer_inline(blk, &handle) + } + NamespaceEntryKind::LocalClass { class_id } => { + // INT32-tagged class-id NaN-box: 0x7FFE_0000_0000_0000 | + // (class_id & 0xFFFFFFFF). Matches `Expr::ClassRef`. + let bits = crate::nanbox::INT32_TAG | (*class_id as u64 & 0xFFFF_FFFF); + crate::nanbox::double_literal(f64::from_bits(bits)) + } + NamespaceEntryKind::ForeignVar { source_prefix, - sanitize(source_local) - ); - let arity = (*param_count).min(16); - let mut wrapper_params: Vec = vec![I64]; - wrapper_params.extend(std::iter::repeat_n(DOUBLE, arity)); - ctx.pending_declares - .push((wrapper_name.clone(), DOUBLE, wrapper_params)); - let blk = ctx.block(); - let handle = blk.call( - I64, - "js_closure_alloc_singleton", - &[(PTR, &format!("@{}", wrapper_name))], - ); - crate::expr::nanbox_pointer_inline(blk, &handle) - } - NamespaceEntryKind::NestedNamespace { source_prefix } => ctx - .block() - .load(DOUBLE, &format!("@__perry_ns_{}", source_prefix)), - }; + source_local, + } => { + let getter = format!("perry_fn_{}__{}", source_prefix, sanitize(source_local)); + ctx.pending_declares.push((getter.clone(), DOUBLE, vec![])); + ctx.block().call(DOUBLE, &getter, &[]) + } + NamespaceEntryKind::ForeignFunction { + source_prefix, + source_local, + param_count, + } => { + // Function-shaped re-exports must materialize a function + // value, not call the function while building the namespace. + // Source modules emit `__perry_wrap_perry_fn___` + // for every user function; hand that wrapper to the same + // singleton allocator used by local function exports. + let wrapper_name = format!( + "__perry_wrap_perry_fn_{}__{}", + source_prefix, + sanitize(source_local) + ); + let arity = (*param_count).min(16); + let mut wrapper_params: Vec = vec![I64]; + wrapper_params.extend(std::iter::repeat_n(DOUBLE, arity)); + ctx.pending_declares + .push((wrapper_name.clone(), DOUBLE, wrapper_params)); + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_closure_alloc_singleton", + &[(PTR, &format!("@{}", wrapper_name))], + ); + crate::expr::nanbox_pointer_inline(blk, &handle) + } + NamespaceEntryKind::NestedNamespace { source_prefix } => ctx + .block() + .load(DOUBLE, &format!("@__perry_ns_{}", source_prefix)), + }; - let blk = ctx.block(); - let val_slot = blk.gep(DOUBLE, &vals_buf, &[(I64, &idx_str)]); - blk.store(DOUBLE, &val_str, &val_slot); - } + handles.push(group.adopt_emitted(ctx, crate::rooting::Repr::Boxed, &val_str, true)); + } + + // Flush pass: re-read each value from its root -- picking up any + // relocation the loop above caused -- and store it into `vals_buf`. + // No call runs between these stores and the `js_create_namespace` + // call that follows, so every slot the runtime reads is live. + for (i, handle) in handles.iter().enumerate() { + let idx_str = format!("{}", i); + let fresh = group.reread_emitted(ctx, *handle); + let blk = ctx.block(); + let val_slot = blk.gep(DOUBLE, &vals_buf, &[(I64, &idx_str)]); + blk.store(DOUBLE, &fresh, &val_slot); + } + anyhow::Ok(()) + }) + .expect("emit_namespace_populator's rooted group body is infallible"); // Call `js_create_namespace(n, keys, key_lens, values)` and store // the result into the namespace global. The result is a NaN-boxed diff --git a/crates/perry-codegen/src/expr/index_set.rs b/crates/perry-codegen/src/expr/index_set.rs index 1b0465b789..e5d7433c9b 100644 --- a/crates/perry-codegen/src/expr/index_set.rs +++ b/crates/perry-codegen/src/expr/index_set.rs @@ -748,33 +748,47 @@ pub(crate) fn lower( && (matches!(index.as_ref(), Expr::String(_)) || is_string_expr(ctx, index)) { let global_box = ctx.block().call(DOUBLE, "js_get_global_this", &[]); - let key_box = lower_expr(ctx, index)?; - let val_double = lower_expr(ctx, value)?; - let (obj_handle, key_handle) = { - let blk = ctx.block(); - // #7640 section D: key first — `unbox_str_handle` can - // allocate (SSO materialisation), and a raw receiver pointer - // taken above it is unrootable. - let key_handle = unbox_str_handle(blk, &key_box); - let obj_handle = unbox_to_i64(blk, &global_box); - (obj_handle, key_handle) - }; - let site_id = emit_typed_feedback_register_site( - ctx, - TypedFeedbackKind::PropertySet, - "globalThis[index]", - TypedFeedbackContract::object_set_by_name(), - ); - ctx.block().call_void( - "js_typed_feedback_object_set_field_by_name", - &[ - (I64, &site_id), - (I64, &obj_handle), - (I64, &key_handle), - (DOUBLE, &val_double), - ], - ); - return Ok(val_double); + // #7640 section A: `key_box` is a heap string by construction + // on this arm (`is_string_expr`) and `global_box` a copy of + // the (registered-root-backed, but movable) globalThis + // singleton — both were held as bare registers across + // `value`'s lowering, which is arbitrary user code and can + // allocate. Root both in one group; `index`'s own window ends + // at `value` (nothing else is lowered after it), so `value` + // itself takes no slot. + return rooting::with_rooted_group(ctx, 2, |ctx, group| { + let global_idx = + group.adopt_emitted(ctx, rooting::Repr::Boxed, &global_box, true); + let key_idx = group.lower(ctx, index, true)?; + let val_double = lower_expr(ctx, value)?; + let key_box = group.reread(ctx, key_idx)?; + let global_box = group.reread_emitted(ctx, global_idx); + let (obj_handle, key_handle) = { + let blk = ctx.block(); + // #7640 section D: key first — `unbox_str_handle` can + // allocate (SSO materialisation), and a raw receiver + // pointer taken above it is unrootable. + let key_handle = unbox_str_handle(blk, &key_box); + let obj_handle = unbox_to_i64(blk, &global_box); + (obj_handle, key_handle) + }; + let site_id = emit_typed_feedback_register_site( + ctx, + TypedFeedbackKind::PropertySet, + "globalThis[index]", + TypedFeedbackContract::object_set_by_name(), + ); + ctx.block().call_void( + "js_typed_feedback_object_set_field_by_name", + &[ + (I64, &site_id), + (I64, &obj_handle), + (I64, &key_handle), + (DOUBLE, &val_double), + ], + ); + Ok(val_double) + }); } if is_width_tracked_typed_array_receiver(ctx, object) { // A non-numeric index (a Symbol, or a string property name) is @@ -788,21 +802,29 @@ pub(crate) fn lower( // literal / loop-counter index stays `is_numeric_expr`, so every // proven element fast path below is preserved. if !is_numeric_expr(ctx, index) { - let arr_box = lower_expr(ctx, object)?; - let idx_double = lower_expr(ctx, index)?; - let val_double = lower_expr(ctx, value)?; - let blk = ctx.block(); - let arr_bits = blk.bitcast_double_to_i64(&arr_box); - let arr_i64 = blk.and(I64, &arr_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_typed_array_index_set_dynamic", - &[ - (I64, &arr_i64), - (DOUBLE, &idx_double), - (DOUBLE, &val_double), - ], - )); + // #7640 section A: receiver AND key were both lowered + // before `value`, with no rooting decision at all. The + // typed-array object itself is old-arena/non-movable + // (CLAUDE.md's GC section), but the KEY is not — a + // non-numeric index here is a Symbol or string, and a + // `Symbol()`/computed-key expression can allocate the + // interned symbol, leaving a from-space key by the time + // `value`'s own (arbitrary, allocating) evaluation runs. + return rooting::with_operands_rooted( + ctx, + &[object, index, value], + |ctx, vals| { + let (arr_box, idx_double, val_double) = (&vals[0], &vals[1], &vals[2]); + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(arr_box); + let arr_i64 = blk.and(I64, &arr_bits, POINTER_MASK_I64); + Ok(blk.call( + DOUBLE, + "js_typed_array_index_set_dynamic", + &[(I64, &arr_i64), (DOUBLE, idx_double), (DOUBLE, val_double)], + )) + }, + ); } if let Some(store) = lower_typed_array_store(ctx, object, index, value)? { if value_discarded { @@ -1190,190 +1212,219 @@ pub(crate) fn lower( let value_is_numeric = is_numeric_expr(ctx, value); let require_numeric_layout = value_is_numeric && expr_has_numeric_pointer_free_array_layout(ctx, object); - let arr_box = lower_expr(ctx, object)?; - let idx_double = lower_expr(ctx, index)?; - let idx_i32 = ctx.block().load(I32, &i32_slot); - let val_double = lower_expr(ctx, value)?; - if require_numeric_layout { - let feedback_site_id = emit_typed_feedback_register_site( - ctx, - TypedFeedbackKind::ArrayElement, - "array[index]=", - TypedFeedbackContract::numeric_array_set_index(), - ); - let fast_idx = ctx.new_block("idxset.bounded_numeric_fast"); - let fallback_idx = ctx.new_block("idxset.bounded_numeric_fallback"); - let merge_idx = ctx.new_block("idxset.bounded_numeric_merge"); - let fast_label = ctx.block_label(fast_idx); - let fallback_label = ctx.block_label(fallback_idx); - let merge_label = ctx.block_label(merge_idx); - - let guard_ok = { - let blk = ctx.block(); - let guard_i32 = blk.call( - I32, - "js_typed_feedback_numeric_array_index_set_guard", - &[ - (I64, &feedback_site_id), - (DOUBLE, &arr_box), - (I32, &idx_i32), - (DOUBLE, &val_double), - (I32, "1"), - ], + // #7640 section A: the receiver was lowered, then the + // index, then — the hazard — the VALUE, with no + // rooting decision at all. `classify_for_length_hoist`'s + // body predicate accepts an allocating RHS + // (`Expr::Object` / `Array` / `ArraySpread`) at exactly + // this call site, so `for (let i=0;i` source. Confirmed by hand on this exact + // shape (`Holder.setOnThis` in the test above): the field-get result + // register is reused, unreloaded, after `allocPoint`'s call in the + // emitted IR. Left unfixed here: rooting it unconditionally would add + // a real store+bind+reread to the common `LocalGet`/`This` case this + // comment just proved needs none, on what the issue that tracks this + // (#7640) calls "the hottest store path in the compiler" — a + // measured-cost change, not a rooting-API mechanical one, and + // deliberately left for a follow-up that can benchmark it. See + // changelog.d/7640-abc-rooting-residue.md. let recv_box = lower_expr(ctx, object)?; let val_double = lower_expr(ctx, value)?; @@ -341,9 +374,10 @@ fn try_lower_sloppy_class_field_boxed_store( class_name: &str, ) -> Result> { // Operand order mirrors the raw-f64 arm and the strict class-field arm - // verbatim: the assignment reference is evaluated before the RHS, and the - // receiver's relocation across an allocating RHS is handled by the same - // statepoint re-read those arms rely on. + // verbatim: the assignment reference is evaluated before the RHS. See + // `try_lower_sloppy_class_field_store`'s #7640-section-C note above for + // why this is safe and what actually makes it so (`root_reload.rs`, not + // a statepoint-specific mechanism) — same shape, same fix, same proof. let recv_box = lower_expr(ctx, object)?; let val_double = lower_expr(ctx, value)?; @@ -809,6 +843,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // __set_ method instead of doing a raw field // store. The setter takes (this, value) and returns // undefined; we forward `value` as the expression result. + // + // #7640 section C: `recv_box` below is lowered before `value` + // exactly like the class-field arms, and the same split answer + // applies — see `try_lower_sloppy_class_field_store`'s note: safe + // for a `LocalGet`/`This` receiver (`root_reload.rs`, verified), + // open for a compound one (`this.target.y = allocPoint(n).x`, + // same test file). if let Some(class_name) = receiver_class_name(ctx, object) { if class_has_computed_runtime_members(ctx, &class_name) { return lower_runtime_property_set_by_name(ctx, object, property, value); @@ -836,6 +877,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Fast path: known class instance + plain instance field. // The runtime guard checks the receiver's class/shape and // descriptor state before this block touches the raw slot. + // + // This is "the strict class-field arm below" the sloppy arms + // in `try_lower_sloppy_class_field_store` name — same + // `recv_box`-before-`value` order, same #7640 section C + // answer (safe for `LocalGet`/`This`, open for a compound + // receiver), same evidence. if let Some(field_index) = crate::type_analysis::class_field_global_index(ctx, &class_name, property) { diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index 15f50fcc55..8f5204d58f 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -20,8 +20,46 @@ use crate::expr::{ }; use crate::nanbox::double_literal; use crate::native_value::LoweredValue; +use crate::rooting::{with_rooted_group, RootedGroup}; use crate::types::{DOUBLE, I1, I32, I64}; +/// Materialise the parallel `[N x double]` argument buffer +/// `js_native_call_method_str_key` / `js_native_call_method_value` read, from +/// values already rooted in `group`. +/// +/// Re-reading every argument here rather than reusing whatever +/// `RootedGroup::lower` returned is the point (#7210 (3)): this must run as +/// the LAST thing before the consuming call, in each branch, so nothing that +/// runs between rooting and the call — `unbox_str_handle`'s SSO +/// materialization in the static-key branch, in particular — can leave a +/// stale pointer sitting in this buffer. `RootedGroup::reread` re-derives the +/// post-collection value for every operand, so this loop is itself call-free +/// and cannot reopen the window it exists to close. +fn build_dispatch_args_buffer( + ctx: &mut FnCtx<'_>, + group: &RootedGroup<'_>, + arg_idxs: &[usize], +) -> Result<(String, String)> { + let n = arg_idxs.len(); + if n == 0 { + return Ok(("null".to_string(), "0".to_string())); + } + let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, &idx) in arg_idxs.iter().enumerate() { + let v = group.reread(ctx, idx)?; + let slot = ctx + .block() + .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, &v, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf_reg + )); + Ok((ptr_reg, n.to_string())) +} + fn typed_i1_closure_signature_note(reps: &[crate::codegen::TypedParamRep]) -> String { let first = reps.first().map(|rep| rep.label()).unwrap_or("void"); if reps.len() <= 1 { @@ -231,66 +269,68 @@ pub fn try_lower_index_get_call( || crate::type_analysis::is_string_expr(ctx, index) || crate::type_analysis::is_definitely_string_expr(ctx, index); - let recv_box = lower_expr(ctx, object)?; - let key_box = lower_expr(ctx, index)?; - let mut lowered_args: Vec = Vec::with_capacity(args.len()); - for a in args { - lowered_args.push(lower_expr(ctx, a)?); - } - let n = lowered_args.len(); - let (args_ptr, args_len) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, v) in lowered_args.iter().enumerate() { - let slot = ctx - .block() - .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, v, &slot); + // #7210 (3): receiver, key and every argument are lowered in strict + // sequence — each held as a bare SSA register while the rest lower, + // which can allocate — and in the static-string-key arm + // `unbox_str_handle` allocates too (an SSO key materializes to a + // fresh heap `StringHeader`), sitting between the args buffer's + // would-be stores and the consuming call. Root [object, index, + // ...args] in one `RootedGroup` for the whole dispatch and build the + // args buffer LAST in each branch via `build_dispatch_args_buffer` — + // after `unbox_str_handle` in the static-key arm — so nothing can + // collect between the buffer's last store and the call that reads it. + return with_rooted_group(ctx, 2 + args.len(), |ctx, group| { + let recv_idx = group.lower(ctx, object, true)?; + let key_idx = group.lower(ctx, index, true)?; + let mut arg_idxs = Vec::with_capacity(args.len()); + for a in args { + arg_idxs.push(group.lower(ctx, a, true)?); } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, n, buf_reg - )); - (ptr_reg, n.to_string()) - }; - - if is_static_string { - // Statically-known string key: extract the string handle and use - // the str-key entry (`this` bound by the dispatch tower). - let name_handle = { - let blk = ctx.block(); - crate::expr::unbox_str_handle(blk, &key_box) - }; - return Ok(Some(ctx.block().call( + + if is_static_string { + // Statically-known string key: extract the string handle and + // use the str-key entry (`this` bound by the dispatch + // tower). Re-read the receiver AFTER `unbox_str_handle`, + // which allocates, and build the args buffer after that too. + let key_box = group.reread(ctx, key_idx)?; + let name_handle = { + let blk = ctx.block(); + crate::expr::unbox_str_handle(blk, &key_box) + }; + let recv_box = group.reread(ctx, recv_idx)?; + let (args_ptr, args_len) = build_dispatch_args_buffer(ctx, group, &arg_idxs)?; + return Ok(Some(ctx.block().call( + DOUBLE, + "js_native_call_method_str_key", + &[ + (DOUBLE, &recv_box), + (I64, &name_handle), + (crate::types::PTR, &args_ptr), + (I64, &args_len), + ], + ))); + } + + // Dynamic key (`this[(cur)._op](cur)`, `obj[k]()` where `k` is a + // runtime value): pass the key value through, the runtime branches on + // its type and binds `this = obj` either way. Refs #321 (effect + // FiberRuntime op dispatch) — pre-fix this fell through to a plain + // closure-call that dropped `this`, so a method stored as a class + // field reached by dynamic key read `this === undefined`. + let recv_box = group.reread(ctx, recv_idx)?; + let key_box = group.reread(ctx, key_idx)?; + let (args_ptr, args_len) = build_dispatch_args_buffer(ctx, group, &arg_idxs)?; + Ok(Some(ctx.block().call( DOUBLE, - "js_native_call_method_str_key", + "js_native_call_method_value", &[ (DOUBLE, &recv_box), - (I64, &name_handle), + (DOUBLE, &key_box), (crate::types::PTR, &args_ptr), (I64, &args_len), ], - ))); - } - - // Dynamic key (`this[(cur)._op](cur)`, `obj[k]()` where `k` is a - // runtime value): pass the key value through, the runtime branches on - // its type and binds `this = obj` either way. Refs #321 (effect - // FiberRuntime op dispatch) — pre-fix this fell through to a plain - // closure-call that dropped `this`, so a method stored as a class - // field reached by dynamic key read `this === undefined`. - return Ok(Some(ctx.block().call( - DOUBLE, - "js_native_call_method_value", - &[ - (DOUBLE, &recv_box), - (DOUBLE, &key_box), - (crate::types::PTR, &args_ptr), - (I64, &args_len), - ], - ))); + ))) + }); } Ok(None) } diff --git a/test-files/fixtures/gc_namespace_rooting_pkg/lib.ts b/test-files/fixtures/gc_namespace_rooting_pkg/lib.ts new file mode 100644 index 0000000000..bbaa0fa5fe --- /dev/null +++ b/test-files/fixtures/gc_namespace_rooting_pkg/lib.ts @@ -0,0 +1,22 @@ +// Companion module for test_gap_gc_namespace_and_computed_dispatch_rooting.ts +// (#7210 section 2). `import * as ns from "./lib"` in the entry module +// forces `codegen/helpers.rs`'s `emit_namespace_populator` to run at this +// module's init, staging every export below (a plain var, a function, a +// class, and a re-exported var) into the `vals_buf` alloca the fix roots. + +export const tag = "lib"; + +export function double(x: number): number { + return x * 2; +} + +export class Box { + value: number = 0; + constructor(v: number) { + this.value = v; + } +} + +// A re-export ("ForeignVar"/"ForeignFunction" NamespaceEntryKind), which +// routes through a cross-module getter call in the populator loop. +export { churnFromOther, CHURN_TAG } from "./other.ts"; diff --git a/test-files/fixtures/gc_namespace_rooting_pkg/other.ts b/test-files/fixtures/gc_namespace_rooting_pkg/other.ts new file mode 100644 index 0000000000..f98362e5d7 --- /dev/null +++ b/test-files/fixtures/gc_namespace_rooting_pkg/other.ts @@ -0,0 +1,13 @@ +// Re-export source for lib.ts (#7210 section 2's `ForeignVar`/`ForeignFunction` +// `NamespaceEntry` kinds — routed through a cross-module getter in +// `emit_namespace_populator`'s per-entry loop). + +export const CHURN_TAG = "churn"; + +export function churnFromOther(seed: number): number { + const bits: unknown[] = []; + for (let i = 0; i < 500; i++) { + bits.push({ i: i, s: "z" + i }); + } + return seed + bits.length - 500; +} diff --git a/test-files/test_gap_gc_class_field_receiver_rooting.ts b/test-files/test_gap_gc_class_field_receiver_rooting.ts new file mode 100644 index 0000000000..fde24e5b93 --- /dev/null +++ b/test-files/test_gap_gc_class_field_receiver_rooting.ts @@ -0,0 +1,122 @@ +// #7640 section C: `obj.field = ` on a known class evaluates +// the receiver before the value (spec order) — every arm in +// `expr/property_set.rs`'s class-field family (`try_lower_sloppy_class_field_store`, +// the strict `class_field_global_index` fast path, and the setter-dispatch +// arm) lowers the receiver first and reuses that register after the RHS runs. +// +// Two of those arms carried a comment claiming this is already safe: "the +// receiver's relocation across an allocating RHS is handled by the same +// statepoint re-read that arm relies on." The answer turned out to split in +// two, both confirmed against this file's IR: +// +// * `object` a bare local/`this` (`setRawF64`, `setBoxed`, `setViaSetter` +// below) — TRUE, but not for the stated reason. There is no +// statepoint-specific re-read; what actually closes the window is +// `root_reload.rs` (#7280), a front-end pass that re-materialises a value +// derived from a shadow-slot or handle-global load below any collection +// point it cannot dominate. It runs before either root lowering sees the +// IR, so it protects the shadow (`PERRY_RS4GC=0`) and native +// (`PERRY_RS4GC=1`, default) backends identically. +// * `object` a compound receiver — a class-field READ, `this.target.x = …` +// (`Runner.run` below) — FALSE. The receiver register is the `phi` result +// of a class-field GET, not a direct shadow-slot load, so `root_reload` +// has nothing to re-derive from, and it is reused unreloaded after the +// RHS's allocating call in the emitted IR (confirmed by hand). This shape +// stays genuinely open — see the note in `property_set.rs` and +// changelog.d/ for why it is not fixed in the same change (the fix is a +// measured-cost one on a hot path, the finding is not). +// +// This test is the permanent corpus member for both halves: it exercises the +// receiver-then-allocating-value shape on plain field (raw-f64 and boxed), +// an accessor (`set` method) dispatch, and the compound `this.target.field =` +// shape, under enough allocation pressure (`PERRY_GC_MOVING_LOOP_POLLS=1` +// back-edge polls) that a moving minor is reachable inside the RHS, and +// asserts the stored values are never stale. The safe arms pin the finding; +// the `Runner.run` arm keeps the open shape in the corpus for when an +// instrument that can see it exists. +// +// Verified directly against `scripts/gc_root_dominance_check.py` on this +// exact shape (both `--stale-registers` and `--statepoints`, both lowerings): +// zero hazards. See changelog.d/ for the full note. + +class Point { + x: number = 0; + next: Point | null = null; + _y: number = 0; + set y(v: number) { + this._y = v; + } +} + +class Holder { + target: Point = new Point(); +} + +// Allocation pressure: enough garbage that a loop back-edge poll inside this +// function can land a moving minor while the caller's receiver register is +// still holding a pre-collection address. +function churn(seed: number): number { + const bits: unknown[] = []; + for (let i = 0; i < 600; i++) { + bits.push({ i: i, s: "x" + i }); + } + return seed + bits.length - 600; +} + +function allocPoint(n: number): Point { + const p = new Point(); + p.x = n; + return p; +} + +// Sloppy-mode raw-f64 class field: `try_lower_sloppy_class_field_store`'s +// requires_raw_f64 arm. +function setRawF64(p: Point, n: number): void { + p.x = allocPoint(churn(n)).x; +} + +// Sloppy-mode boxed class field: `try_lower_sloppy_class_field_boxed_store`. +function setBoxed(p: Point, n: number): void { + p.next = allocPoint(churn(n)); +} + +// Setter dispatch (`property_set.rs`'s `ctx.methods.get(&setter_key)` arm) — +// no rooting comment at all before this test. +function setViaSetter(p: Point, n: number): void { + p.y = allocPoint(churn(n)).x; +} + +// `this.target.field = `: the receiver of the innermost +// PropertySet is `this.target`, itself a PropertyGet result rather than a +// bare local — still reached by the same class-field arms. +class Runner { + run(p: Point, n: number): void { + const h = new Holder(); + h.target = p; + h.target.x = allocPoint(churn(n)).x; + h.target.next = allocPoint(churn(n + 1)); + } +} + +function main(): void { + let bad = 0; + const runner = new Runner(); + for (let r = 0; r < 300; r++) { + const p = new Point(); + setRawF64(p, r); + if (p.x !== r) bad++; + + setBoxed(p, r); + if (p.next === null || p.next.x !== r) bad++; + + setViaSetter(p, r); + if (p._y !== r) bad++; + + runner.run(p, r); + if (p.x !== r) bad++; + if (p.next === null || p.next.x !== r + 1) bad++; + } + console.log(bad); +} + +main(); diff --git a/test-files/test_gap_gc_index_set_bounded_globalthis_ta_rooting.ts b/test-files/test_gap_gc_index_set_bounded_globalthis_ta_rooting.ts new file mode 100644 index 0000000000..c3f3bed016 --- /dev/null +++ b/test-files/test_gap_gc_index_set_bounded_globalthis_ta_rooting.ts @@ -0,0 +1,80 @@ +// #7640 section A: three `index_set.rs` arms that lowered a receiver (and +// sometimes a key) with no rooting decision at all, all fixed in the same +// change as this test. +// +// 1. The bounded-index-pair array store: the receiver was lowered, then the +// (proven, non-allocating) loop-counter index, then the VALUE — and +// `classify_for_length_hoist`'s body predicate accepts an allocating RHS +// (`Expr::Object`) at exactly this call site, so `arr[i] = {v:i}` inside +// the loop this fact comes from left the receiver a bare register across +// the object literal's allocation. +// 2. `globalThis[k] = v`: the key (a heap string) and the globalThis +// singleton copy were both live across the allocating RHS. +// 3. The width-tracked typed-array non-numeric-index store (`ta[k] = mk()` +// with `k: any`): receiver and key both live across the allocating RHS — +// here the key is a `Symbol`, which allocates when interned. + +function churn(seed: number): number { + const bits: unknown[] = []; + for (let i = 0; i < 500; i++) { + bits.push({ i: i, s: "w" + i }); + } + return seed + bits.length - 500; +} + +function makeRecord(n: number): { v: number } { + return { v: churn(n) }; +} + +function boundedIndexStore(n: number): unknown[] { + // Matches the issue's own reproducer shape verbatim: a `let`-bound LOCAL + // array (`classify_for_length_hoist`'s bounded-pair registration is keyed + // on that shape) and — crucially — the store's RHS is an OBJECT LITERAL + // (`Expr::Object`) at the syntax level, which is exactly what + // `expr_preserves_array_length`'s body predicate accepts. A RHS that is a + // plain function call (`a[i] = makeRecord(n)`) does not classify the same + // way and falls through to the already-protected generic array-store arm + // instead of the bounded-index-pair one this test exists to exercise. + const a: unknown[] = new Array(64); + for (let i = 0; i < a.length; i++) { + a[i] = { v: churn(n + i) }; + } + return a; +} + +function globalThisStore(key: string, n: number): void { + (globalThis as unknown as Record)[key] = makeRecord(n); +} + +function widthTrackedTaSymbolStore(ta: Int32Array, n: number): void { + const sym = Symbol.for("gc7640a_" + n); + (ta as unknown as Record)[sym] = churn(n); +} + +function main(): void { + let bad = 0; + + for (let r = 0; r < 200; r++) { + const a = boundedIndexStore(r); + for (let i = 0; i < a.length; i++) { + const rec = a[i] as { v: number }; + if (rec.v !== r + i) bad++; + } + } + + for (let r = 0; r < 200; r++) { + const key = "gc7640a_global_" + (r % 7); + globalThisStore(key, r); + const rec = (globalThis as unknown as Record)[key]; + if (rec.v !== r) bad++; + } + + const ta = new Int32Array(8); + for (let r = 0; r < 200; r++) { + widthTrackedTaSymbolStore(ta, r); + } + + console.log(bad); +} + +main(); diff --git a/test-files/test_gap_gc_namespace_and_computed_dispatch_rooting.ts b/test-files/test_gap_gc_namespace_and_computed_dispatch_rooting.ts new file mode 100644 index 0000000000..55a3291ca5 --- /dev/null +++ b/test-files/test_gap_gc_namespace_and_computed_dispatch_rooting.ts @@ -0,0 +1,73 @@ +// #7210 sections 2 and 3: two flagship "no rooting decision at all" sites. +// +// 1. `codegen/helpers.rs`'s `emit_namespace_populator` stages every exported +// binding's NaN-boxed value into a plain `[N x double]` stack alloca +// (`vals_buf`) while the per-entry loop calls allocating helpers +// (`js_closure_alloc_singleton` for the function/class exports below, a +// cross-module getter for the re-exported var/function). An +// already-stored entry had no root of its own, so a later entry's +// allocation could leave it pointing at from-space by the time +// `js_create_namespace` read the whole buffer. `import * as ns from +// "./fixtures/gc_namespace_rooting_pkg/lib"` forces the populator to run +// for that module (four export kinds: plain var, function, class, +// re-export) and every entry is read back below. +// +// 2. `lower_call/early_branches.rs`'s `obj[strKey](args)` computed-key +// method dispatch lowers the receiver, the key and every argument into +// bare registers in sequence, and (in the static-string-key arm) +// `unbox_str_handle` — an allocating SSO materialisation — used to run +// between the args buffer's stores and the consuming call. `dispatch()` +// below calls a method through a computed string key with two heap +// arguments, one of which allocates hard enough (`churn`) to reach a +// moving minor mid-call. +// +// Both are exercised under `PERRY_GC_MOVING_LOOP_POLLS=1` allocation +// pressure and asserted for correctness (not just "doesn't crash") against +// Node's output. + +import * as ns from "./fixtures/gc_namespace_rooting_pkg/lib.ts"; + +function churn(seed: number): number { + const bits: unknown[] = []; + for (let i = 0; i < 500; i++) { + bits.push({ i: i, s: "y" + i }); + } + return seed + bits.length - 500; +} + +class Dispatcher { + add(a: { v: number }, b: { v: number }): number { + return a.v + b.v; + } +} + +function dispatch(d: Dispatcher, key: string, n: number): number { + const a = { v: churn(n) }; + const b = { v: n + 1 }; + return (d as unknown as Record number>)[key](a, b); +} + +function main(): void { + let bad = 0; + + // Site 1: every export of `lib.ts`, read back after the whole namespace + // object (all four entries) has been populated. + if (ns.tag !== "lib") bad++; + if (ns.double(21) !== 42) bad++; + const box = new ns.Box(7); + if (box.value !== 7) bad++; + if (ns.CHURN_TAG !== "churn") bad++; + if (ns.churnFromOther(10) !== 10) bad++; + + // Site 2: computed-key method dispatch with heap args, one of which + // allocates hard enough to pressure a moving minor. + const d = new Dispatcher(); + for (let r = 0; r < 300; r++) { + const got = dispatch(d, "add", r); + if (got !== r + (r + 1)) bad++; + } + + console.log(bad); +} + +main(); From fd8b5644544d666ebfa1546922c2f8052a359414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 23:15:15 +0200 Subject: [PATCH 2/4] docs(changelog): add fragment for PR #7736 --- changelog.d/7736-rooting-residue.md | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 changelog.d/7736-rooting-residue.md diff --git a/changelog.d/7736-rooting-residue.md b/changelog.d/7736-rooting-residue.md new file mode 100644 index 0000000000..0048a648bd --- /dev/null +++ b/changelog.d/7736-rooting-residue.md @@ -0,0 +1,52 @@ +### GC rooting residue: #7210's two remaining unrooted-alloca sites + #7640 sections A/C + +Closed the two named flagship sites from #7210's remaining unrooted-alloca +enumeration: + +- `codegen/helpers.rs`'s `emit_namespace_populator` staged every re-exported + binding's value into a plain stack alloca while the per-entry loop called + allocating helpers (closure singleton alloc, cross-module getters); an + already-staged entry had no root and could go stale before + `js_create_namespace` read the whole buffer. Fixed by rooting each value in + a `RootedGroup` as it is produced and deferring every store to a second, + call-free pass immediately before the consuming call. +- `lower_call/early_branches.rs`'s `obj[strKey](args)` computed-key dispatch + lowered receiver/key/args into bare registers in sequence, and (in the + static-key arm) `unbox_str_handle` — an allocating SSO materialisation — + ran between the args buffer's stores and the call. Fixed the same way: + root `[object, index, ...args]` in one `RootedGroup`, build the args + buffer last in each branch. + +From #7640 section A, three more `index_set.rs` arms with no rooting +decision at all are now fixed: the bounded-index-pair array store, `globalThis[k] += v`, and the width-tracked typed-array non-numeric-index store. Section A's +`#5525 recv_unknown` inline dyn-TA store and the TA runtime-key / TA +final-fallback / Uint8Array runtime-key arms are not reached. + +From #7640 section C, resolved the open question rather than mechanically +fixing it: two `property_set.rs` comments claimed a class-field store's +receiver survives an allocating RHS via "the same statepoint re-read" a +sibling arm relies on. That mechanism does not exist. For a bare +`Expr::LocalGet`/`Expr::This` receiver the claim is true, via `root_reload.rs` +(#7280) — a front-end pass, independent of RS4GC, that re-materialises a +value derived from a shadow-slot or handle-global load below any collection +point it doesn't dominate. For a compound receiver — a class-field READ used +as the assignment target, `this.target.x = allocPoint(n).x` — the claim is +false, and the gap is invisible to both `root_reload` and the +`--stale-registers` checker (confirmed by hand in the emitted IR). Left +unfixed deliberately: rooting the receiver unconditionally would tax the +dominant plain-local case on what the issue itself calls the hottest store +path in the compiler — a measured-cost tradeoff for a follow-up, not a +mechanical gap this change's tools can close for free. + +Section E's seven named callees were triaged (not fixed): each looks like a +hazard that is not one — a single already-rooted caller, an address derived +after the allocating step rather than before, or typed-array immovability +(the same category #7210 section 5 already flagged). Static triage only, +not checker-verified per callee. + +New gap-suite fixtures pin all of the above: +`test_gap_gc_namespace_and_computed_dispatch_rooting.ts` (+ +`fixtures/gc_namespace_rooting_pkg/`), `test_gap_gc_index_set_bounded_globalthis_ta_rooting.ts`, +and an expanded `test_gap_gc_class_field_receiver_rooting.ts` covering both +halves of the section C finding. From 6d04ebc689f3f23a4dd65ff37addacf9f7f17a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 03:04:30 +0200 Subject: [PATCH 3/4] chore: bump version to 0.5.1431 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- 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 9eb6fdbe0a..3af159d924 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.1430 +**Current Version:** 0.5.1431 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 4fc65cba83..4646d15d96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1430" +version = "0.5.1431" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1430" +version = "0.5.1431" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1430" +version = "0.5.1431" [[package]] name = "perry-ui-tvos" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1430" +version = "0.5.1431" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 567df66aa9..6d4ff3a68d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1430" +version = "0.5.1431" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" From fd3c03ee4b4855d698e68a0ed398a36b02d66823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 03:18:44 +0200 Subject: [PATCH 4/4] test(gc): register #7736's three rooting fixtures in the repsel corpus The dark-tests gate caught them: on disk but absent from test-parity/gc_repsel_corpus.txt, so gc_repsel_matrix.sh (gc-stress, gc-moving-witnesses, gc-ptr-shape-off-witness) would never have run them -- the #6925 defect. All three PASS on safepoint_minor with the arm live. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- test-parity/gc_repsel_corpus.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test-parity/gc_repsel_corpus.txt b/test-parity/gc_repsel_corpus.txt index bc6285eddc..acaac390aa 100644 --- a/test-parity/gc_repsel_corpus.txt +++ b/test-parity/gc_repsel_corpus.txt @@ -723,3 +723,18 @@ test_gap_repsel_element_shape_loop_clone # — back-edge polls put the copying minor at the safepoints, which is where it # is supposed to be. test_gap_gc_alloc_point_no_move + +# #7210 §2/§3 and #7640 section A (PR #7736). Three rooting fixtures whose +# hazards are all "a value staged in a register or a plain stack buffer while +# an allocating helper runs" — the shape the matrix's moving arms exist to +# catch, so they belong here rather than only in the dominance corpus. +# +# class_field_receiver the typed-`this` field store's receiver +# index_set_bounded_globalthis the bounded-index array store and +# `globalThis[k] = v`'s key + singleton copy +# namespace_and_computed `emit_namespace_populator`'s `vals_buf` +# staging, and `obj[strKey](args)` computed +# dispatch across `unbox_str_handle` +test_gap_gc_class_field_receiver_rooting +test_gap_gc_index_set_bounded_globalthis_ta_rooting +test_gap_gc_namespace_and_computed_dispatch_rooting