From 7125b99c8c614440281e202ed28f9834dcb93fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 30 Aug 2026 02:55:55 +0200 Subject: [PATCH] fix(codegen): use live receiver in stable packed loops (#9117) --- .../9270-stable-packed-forwarded-receiver.md | 3 ++ .../src/stmt/stable_packed_loop.rs | 46 ++++++++----------- .../issue_8655_array_subclass_indexing.rs | 4 +- .../issue_8690_loop_versioned_arraylike.rs | 4 +- 4 files changed, 26 insertions(+), 31 deletions(-) create mode 100644 changelog.d/9270-stable-packed-forwarded-receiver.md diff --git a/changelog.d/9270-stable-packed-forwarded-receiver.md b/changelog.d/9270-stable-packed-forwarded-receiver.md new file mode 100644 index 0000000000..0e896bcac2 --- /dev/null +++ b/changelog.d/9270-stable-packed-forwarded-receiver.md @@ -0,0 +1,3 @@ +### Fixed + +- Make stable packed loops consume the guard's live Array address after cross-call growth instead of reading a stale forwarding stub. diff --git a/crates/perry-codegen/src/stmt/stable_packed_loop.rs b/crates/perry-codegen/src/stmt/stable_packed_loop.rs index 87bb58a80e..fdd1e9cc3c 100644 --- a/crates/perry-codegen/src/stmt/stable_packed_loop.rs +++ b/crates/perry-codegen/src/stmt/stable_packed_loop.rs @@ -1592,21 +1592,21 @@ pub(super) fn lower( ), (PTR, descriptor.as_str()), ]; - if candidate.capture_index.is_some() { - let live_raw = - ctx.block() - .call(I64, "js_packed_arraylike_loop_guard_live", &guard_args); - ( - ctx.block().icmp_ne(I64, &live_raw, "0"), - Some(live_raw), - None, - ) - } else { - let guard = ctx - .block() - .call(I32, "js_packed_arraylike_loop_guard", &guard_args); - (ctx.block().icmp_ne(I32, &guard, "0"), None, None) - } + // An ordinary local can already name an Array growth forwarding + // stub when the array was grown across a call boundary. The guard + // follows that edge and validates the live target, so codegen must + // consume the live address it returns instead of reloading and + // de-tagging the stale local for direct fast-clone reads (#9117). + // Captured receivers need the same result because their reload is + // itself a runtime call that can move the target. + let live_raw = + ctx.block() + .call(I64, "js_packed_arraylike_loop_guard_live", &guard_args); + ( + ctx.block().icmp_ne(I64, &live_raw, "0"), + Some(live_raw), + None, + ) }; // The conservative column matcher proves the cloned body call-free, and // `fast_raw` below reloads the rooted derived receiver after every @@ -1633,18 +1633,10 @@ pub(super) fn lower( descriptor_word(ctx, &descriptor, 6) }; let bound_i32 = ctx.block().trunc(I64, &bound64, I32); - // A capture reload is itself a runtime call, so its admission returns the - // post-call live address. Ordinary addressable bindings retain the old - // guard/reload sequence; their reload is a plain load and their clone must - // still pass the call-free scan unless it has explicit access revalidation. - let fast_raw = if let Some(live_raw) = admitted_live_raw { - live_raw - } else { - let fast_receiver = crate::expr::lower_expr(ctx, &Expr::LocalGet(candidate.array_id))?; - let fast_bits = ctx.block().bitcast_double_to_i64(&fast_receiver); - ctx.block() - .and(I64, &fast_bits, crate::nanbox::POINTER_MASK_I64) - }; + // Every admission returns the post-guard LIVE receiver. Besides captured + // reloads, this is required for ordinary locals that still hold an Array + // growth forwarding stub (#9117). + let fast_raw = admitted_live_raw.expect("every packed-loop admission returns a live receiver"); let fast_scan_start = ctx.func.num_blocks(); let installed_typed_array_views = typed_array_admission .as_ref() diff --git a/crates/perry/tests/issue_8655_array_subclass_indexing.rs b/crates/perry/tests/issue_8655_array_subclass_indexing.rs index 7349b03c92..81e5661774 100644 --- a/crates/perry/tests/issue_8655_array_subclass_indexing.rs +++ b/crates/perry/tests/issue_8655_array_subclass_indexing.rs @@ -111,8 +111,8 @@ fn wolf_ecs_loop_has_a_fallback_free_versioned_fast_copy() { let system = function_ir(&ir, "__system(double"); assert!( - system.contains("call i32 @js_packed_arraylike_loop_guard("), - "the original #8655 fixture must enter through the loop preheader proof" + system.contains("call i64 @js_packed_arraylike_loop_guard_live("), + "the original #8655 fixture must enter through the live-receiver loop preheader proof" ); assert!(system.contains("stable_packed.loop.fast.preheader")); assert!( diff --git a/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs b/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs index b1eda124b3..b52082b70a 100644 --- a/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs +++ b/crates/perry/tests/issue_8690_loop_versioned_arraylike.rs @@ -133,10 +133,10 @@ console.log(checksum); let read_only = function_ir(&ir, "__scan("); assert_eq!( read_only - .matches("call i32 @js_packed_arraylike_loop_guard(") + .matches("call i64 @js_packed_arraylike_loop_guard_live(") .count(), 3, - "the outer fast/slow copies each own a preheader-versioned inner loop" + "the outer fast/slow copies each own a live-receiver preheader-versioned inner loop" ); assert!(read_only.contains("stable_packed.loop.fast.preheader")); assert!(read_only.contains("stable_packed.loop.slow.preheader"));