From 2a400e008c075ad46a30a29dde13009c9b46dabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 14:27:49 +0200 Subject: [PATCH 1/2] fix(codegen): root the Set receiver across the value in SetHas/SetDelete; return js_map_set's receiver (#9523) --- crates/perry-codegen/src/expr/bigint_set.rs | 787 ++++++++++-------- .../src/lower_call/property_get/map_set.rs | 18 +- crates/perry-codegen/src/rooting/mod.rs | 4 + .../src/temp_root_coverage/mod.rs | 1 + .../src/temp_root_coverage/set_receiver.rs | 204 +++++ ...gap_9523_map_set_chain_returns_receiver.ts | 89 ++ ...ap_9523_set_receiver_roots_across_value.ts | 134 +++ 7 files changed, 878 insertions(+), 359 deletions(-) create mode 100644 crates/perry-codegen/src/temp_root_coverage/set_receiver.rs create mode 100644 test-files/test_gap_9523_map_set_chain_returns_receiver.ts create mode 100644 test-files/test_gap_9523_set_receiver_roots_across_value.ts diff --git a/crates/perry-codegen/src/expr/bigint_set.rs b/crates/perry-codegen/src/expr/bigint_set.rs index 7683f57fc9..bc6f2460fa 100644 --- a/crates/perry-codegen/src/expr/bigint_set.rs +++ b/crates/perry-codegen/src/expr/bigint_set.rs @@ -9,6 +9,7 @@ use perry_hir::types::Type as HirType; use perry_hir::{BinaryOp, Expr}; use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::rooting::{operand_may_collect, with_rooted_group, RootedGroup}; use crate::type_analysis::{ is_bigint_expr, set_static_type_args, string_value_is_runtime_guaranteed, }; @@ -133,6 +134,48 @@ fn guarded_set_number_add(ctx: &mut FnCtx<'_>, set_handle: &str, value_box: &str ) } +/// The `Set` receiver's raw handle on the UNPROTECTED path of a `SetHas` / +/// `SetDelete` lowering (#9523), or `None` when the receiver is rooted. +/// +/// Both arms lower the receiver, then the value, then consume the receiver as +/// a raw `i64` — the #6970 shape their Map twins (`MapGet` / `MapHas` / +/// `MapDelete`) were fixed for. The receiver is now a [`RootedGroup`] operand, +/// pushed before `value` is lowered. Unboxing eagerly is only sound when the +/// group pushed nothing: then `value` cannot collect, `reread` hands the +/// original register back, and the emitted IR — register numbering included — +/// is exactly what it was before this change. On the protected path the +/// handle has to come from the *re-read* box, below the value's lowering, so +/// it is derived in [`reread_set_receiver`] instead. +fn eager_set_handle(ctx: &mut FnCtx<'_>, group: &RootedGroup<'_>) -> Result> { + if group.is_rooted() { + return Ok(None); + } + let s_box = group.reread(ctx, 0)?; + let blk = ctx.block(); + Ok(Some(unbox_to_i64(blk, &s_box))) +} + +/// Re-derive the `Set` receiver handle AFTER `value` has been lowered (#9523). +/// +/// Mirrors `math_simple.rs`'s `reread_map_set_receiver_and_key`: on the +/// protected path the box is read back out of its temp-root slot — mandatory, +/// since an evacuating minor inside the value's lowering rewrites the slot and +/// the register pushed beforehand names from-space — and the handle is +/// unboxed from that. On the unprotected path this is the eagerly computed +/// handle and nothing is emitted. +fn reread_set_receiver( + ctx: &mut FnCtx<'_>, + group: &RootedGroup<'_>, + s_handle_unrooted: &Option, +) -> Result { + if let Some(handle) = s_handle_unrooted { + return Ok(handle.clone()); + } + let s_box = group.reread(ctx, 0)?; + let blk = ctx.block(); + Ok(unbox_to_i64(blk, &s_box)) +} + fn guarded_set_number_has(ctx: &mut FnCtx<'_>, set_handle: &str, value_box: &str) -> String { let guard_raw = ctx .block() @@ -870,214 +913,230 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ); let use_string_set = is_static_string_set(ctx, set) && string_value_is_runtime_guaranteed(ctx, value); - let s_box = lower_expr(ctx, set)?; - let s_handle = { - let blk = ctx.block(); - unbox_to_i64(blk, &s_box) - }; - let i32_v = if use_i32_set { - let value_i32 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; - let i32_v = { - let blk = ctx.block(); - blk.call( - I32, - "js_set_has_i32", - &[(I64, &s_handle), (I32, &value_i32.value)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetHas", - "collection_typed_value.set_has_i32", - &value_i32, - "set", - "int32_value_helper", - "js_set_has_i32", - "set_slot", - ); - i32_v - } else if use_u32_set { - let value_u32 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; - let i32_v = { - let blk = ctx.block(); - blk.call( - I32, - "js_set_has_u32", - &[(I64, &s_handle), (I32, &value_u32.value)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetHas", - "collection_typed_value.set_has_u32", - &value_u32, - "set", - "uint32_value_helper", - "js_set_has_u32", - "set_slot", - ); - i32_v - } else if use_f32_set { - let value_f32 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; - let i32_v = { - let blk = ctx.block(); - blk.call( - I32, - "js_set_has_f32", - &[(I64, &s_handle), (F32, &value_f32.value)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetHas", - "collection_typed_value.set_has_f32", - &value_f32, - "set", - "float32_value_helper", - "js_set_has_f32", - "set_slot", - ); - i32_v - } else if use_boolean_set { - let value_i1 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; - let i32_v = { - let blk = ctx.block(); - let value_i32 = blk.zext(I1, &value_i1.value, I32); - blk.call( - I32, - "js_set_has_bool", - &[(I64, &s_handle), (I32, &value_i32)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetHas", - "collection_typed_value.set_has_bool", - &value_i1, - "set", - "boolean_value_helper", - "js_set_has_bool", - "set_slot", - ); - i32_v - } else if use_number_set { - let v_box = lower_expr(ctx, value)?; - guarded_set_number_has(ctx, &s_handle, &v_box) - } else { - if use_string_set { - let value_ref = lower_expr_native( + // #9523: the receiver used to be unboxed to a raw `i64` HERE, before + // `value` was lowered, and consumed after — a bare SSA register across + // an arbitrary allocation, the #6970 shape `MapGet` / `MapHas` / + // `MapDelete` were fixed for. `s.has(makeKey())` with an evacuating + // minor inside `makeKey` handed `js_set_has` a from-space header. Root the + // receiver across the value's lowering and derive the handle from the + // re-read box; when the value cannot collect nothing is pushed and the + // eager unbox keeps the IR byte for byte. + let value_collects = operand_may_collect(ctx, value); + let i32_v = with_rooted_group(ctx, 1, |ctx, group| { + group.lower(ctx, set, value_collects)?; + let s_handle_unrooted = eager_set_handle(ctx, group)?; + let i32_v = if use_i32_set { + let value_i32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_has_i32", + &[(I64, &s_handle), (I32, &value_i32.value)], + ) + }; + record_collection_typed_value_selected( ctx, - value, - crate::native_value::ExpectedNativeRep::StringRef, - )?; + "SetHas", + "collection_typed_value.set_has_i32", + &value_i32, + "set", + "int32_value_helper", + "js_set_has_i32", + "set_slot", + ); + i32_v + } else if use_u32_set { + let value_u32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; let i32_v = { let blk = ctx.block(); - let i32_v = blk.call( + blk.call( I32, - "js_set_has_string", - &[(I64, &s_handle), (I64, &value_ref.value)], - ); - i32_v + "js_set_has_u32", + &[(I64, &s_handle), (I32, &value_u32.value)], + ) }; - record_collection_string_key_selected( + record_collection_typed_value_selected( ctx, "SetHas", - "collection_string_key.set_has", - &value_ref.value, + "collection_typed_value.set_has_u32", + &value_u32, "set", - "js_set_has_string", + "uint32_value_helper", + "js_set_has_u32", + "set_slot", ); + i32_v + } else if use_f32_set { + let value_f32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_has_f32", + &[(I64, &s_handle), (F32, &value_f32.value)], + ) + }; record_collection_typed_value_selected( ctx, "SetHas", - "collection_typed_value.set_has_string", - &value_ref, + "collection_typed_value.set_has_f32", + &value_f32, "set", - "string_value_helper", - "js_set_has_string", + "float32_value_helper", + "js_set_has_f32", "set_slot", ); i32_v - } else { - let v_box = lower_expr(ctx, value)?; + } else if use_boolean_set { + let value_i1 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; let i32_v = { let blk = ctx.block(); - blk.call(I32, "js_set_has", &[(I64, &s_handle), (DOUBLE, &v_box)]) + let value_i32 = blk.zext(I1, &value_i1.value, I32); + blk.call( + I32, + "js_set_has_bool", + &[(I64, &s_handle), (I32, &value_i32)], + ) }; - if receiver_i32_set { - record_collection_typed_value_fallback( - ctx, - "SetHas", - "collection_typed_value.set_has_generic", - &v_box, - "set", - "int32_value_helper", - "js_set_has", - "value_expr_not_native_i32", - ); - } else if receiver_u32_set { - record_collection_typed_value_fallback( - ctx, - "SetHas", - "collection_typed_value.set_has_generic", - &v_box, - "set", - "uint32_value_helper", - "js_set_has", - "value_expr_not_native_u32", - ); - } else if receiver_f32_set { - record_collection_typed_value_fallback( + record_collection_typed_value_selected( + ctx, + "SetHas", + "collection_typed_value.set_has_bool", + &value_i1, + "set", + "boolean_value_helper", + "js_set_has_bool", + "set_slot", + ); + i32_v + } else if use_number_set { + let v_box = lower_expr(ctx, value)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + guarded_set_number_has(ctx, &s_handle, &v_box) + } else { + if use_string_set { + let value_ref = lower_expr_native( ctx, - "SetHas", - "collection_typed_value.set_has_generic", - &v_box, - "set", - "float32_value_helper", - "js_set_has", - "value_expr_not_native_f32", - ); - } else if receiver_boolean_set { - record_collection_typed_value_fallback( + value, + crate::native_value::ExpectedNativeRep::StringRef, + )?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + let i32_v = blk.call( + I32, + "js_set_has_string", + &[(I64, &s_handle), (I64, &value_ref.value)], + ); + i32_v + }; + record_collection_string_key_selected( ctx, "SetHas", - "collection_typed_value.set_has_generic", - &v_box, + "collection_string_key.set_has", + &value_ref.value, "set", - "boolean_value_helper", - "js_set_has", - "value_expr_not_native_i1", + "js_set_has_string", ); - } else if receiver_number_set { - record_collection_number_key_fallback( + record_collection_typed_value_selected( ctx, "SetHas", - "collection_number_value.set_has_generic", - &v_box, + "collection_typed_value.set_has_string", + &value_ref, "set", - "number_value_helper", - "js_set_has", - "value_expr_not_numeric", - "value", + "string_value_helper", + "js_set_has_string", + "set_slot", ); + i32_v } else { - record_collection_string_key_fallback( - ctx, - "SetHas", - "collection_string_key.set_has_generic", - &v_box, - "set", - "js_set_has", - "receiver_or_value_not_static_string", - ); + let v_box = lower_expr(ctx, value)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_set_has", &[(I64, &s_handle), (DOUBLE, &v_box)]) + }; + if receiver_i32_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "int32_value_helper", + "js_set_has", + "value_expr_not_native_i32", + ); + } else if receiver_u32_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "uint32_value_helper", + "js_set_has", + "value_expr_not_native_u32", + ); + } else if receiver_f32_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "float32_value_helper", + "js_set_has", + "value_expr_not_native_f32", + ); + } else if receiver_boolean_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "boolean_value_helper", + "js_set_has", + "value_expr_not_native_i1", + ); + } else if receiver_number_set { + record_collection_number_key_fallback( + ctx, + "SetHas", + "collection_number_value.set_has_generic", + &v_box, + "set", + "number_value_helper", + "js_set_has", + "value_expr_not_numeric", + "value", + ); + } else { + record_collection_string_key_fallback( + ctx, + "SetHas", + "collection_string_key.set_has_generic", + &v_box, + "set", + "js_set_has", + "receiver_or_value_not_static_string", + ); + } + i32_v } - i32_v - } - }; + }; + Ok(i32_v) + })?; let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_v, "0"); let tagged = blk.select( @@ -1110,214 +1169,230 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ); let use_string_set = is_static_string_set(ctx, set) && string_value_is_runtime_guaranteed(ctx, value); - let s_box = lower_expr(ctx, set)?; - let s_handle = { - let blk = ctx.block(); - unbox_to_i64(blk, &s_box) - }; - let i32_v = if use_i32_set { - let value_i32 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; - let i32_v = { - let blk = ctx.block(); - blk.call( - I32, - "js_set_delete_i32", - &[(I64, &s_handle), (I32, &value_i32.value)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetDelete", - "collection_typed_value.set_delete_i32", - &value_i32, - "set", - "int32_value_helper", - "js_set_delete_i32", - "set_slot", - ); - i32_v - } else if use_u32_set { - let value_u32 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; - let i32_v = { - let blk = ctx.block(); - blk.call( - I32, - "js_set_delete_u32", - &[(I64, &s_handle), (I32, &value_u32.value)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetDelete", - "collection_typed_value.set_delete_u32", - &value_u32, - "set", - "uint32_value_helper", - "js_set_delete_u32", - "set_slot", - ); - i32_v - } else if use_f32_set { - let value_f32 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; - let i32_v = { - let blk = ctx.block(); - blk.call( - I32, - "js_set_delete_f32", - &[(I64, &s_handle), (F32, &value_f32.value)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetDelete", - "collection_typed_value.set_delete_f32", - &value_f32, - "set", - "float32_value_helper", - "js_set_delete_f32", - "set_slot", - ); - i32_v - } else if use_boolean_set { - let value_i1 = - lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; - let i32_v = { - let blk = ctx.block(); - let value_i32 = blk.zext(I1, &value_i1.value, I32); - blk.call( - I32, - "js_set_delete_bool", - &[(I64, &s_handle), (I32, &value_i32)], - ) - }; - record_collection_typed_value_selected( - ctx, - "SetDelete", - "collection_typed_value.set_delete_bool", - &value_i1, - "set", - "boolean_value_helper", - "js_set_delete_bool", - "set_slot", - ); - i32_v - } else if use_number_set { - let v_box = lower_expr(ctx, value)?; - guarded_set_number_delete(ctx, &s_handle, &v_box) - } else { - if use_string_set { - let value_ref = lower_expr_native( + // #9523: the receiver used to be unboxed to a raw `i64` HERE, before + // `value` was lowered, and consumed after — a bare SSA register across + // an arbitrary allocation, the #6970 shape `MapGet` / `MapHas` / + // `MapDelete` were fixed for. `s.has(makeKey())` with an evacuating + // minor inside `makeKey` handed `js_set_delete` a from-space header. Root the + // receiver across the value's lowering and derive the handle from the + // re-read box; when the value cannot collect nothing is pushed and the + // eager unbox keeps the IR byte for byte. + let value_collects = operand_may_collect(ctx, value); + let i32_v = with_rooted_group(ctx, 1, |ctx, group| { + group.lower(ctx, set, value_collects)?; + let s_handle_unrooted = eager_set_handle(ctx, group)?; + let i32_v = if use_i32_set { + let value_i32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_delete_i32", + &[(I64, &s_handle), (I32, &value_i32.value)], + ) + }; + record_collection_typed_value_selected( ctx, - value, - crate::native_value::ExpectedNativeRep::StringRef, - )?; + "SetDelete", + "collection_typed_value.set_delete_i32", + &value_i32, + "set", + "int32_value_helper", + "js_set_delete_i32", + "set_slot", + ); + i32_v + } else if use_u32_set { + let value_u32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; let i32_v = { let blk = ctx.block(); - let i32_v = blk.call( + blk.call( I32, - "js_set_delete_string", - &[(I64, &s_handle), (I64, &value_ref.value)], - ); - i32_v + "js_set_delete_u32", + &[(I64, &s_handle), (I32, &value_u32.value)], + ) }; - record_collection_string_key_selected( + record_collection_typed_value_selected( ctx, "SetDelete", - "collection_string_key.set_delete", - &value_ref.value, + "collection_typed_value.set_delete_u32", + &value_u32, "set", - "js_set_delete_string", + "uint32_value_helper", + "js_set_delete_u32", + "set_slot", ); + i32_v + } else if use_f32_set { + let value_f32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_delete_f32", + &[(I64, &s_handle), (F32, &value_f32.value)], + ) + }; record_collection_typed_value_selected( ctx, "SetDelete", - "collection_typed_value.set_delete_string", - &value_ref, + "collection_typed_value.set_delete_f32", + &value_f32, "set", - "string_value_helper", - "js_set_delete_string", + "float32_value_helper", + "js_set_delete_f32", "set_slot", ); i32_v - } else { - let v_box = lower_expr(ctx, value)?; + } else if use_boolean_set { + let value_i1 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; let i32_v = { let blk = ctx.block(); - blk.call(I32, "js_set_delete", &[(I64, &s_handle), (DOUBLE, &v_box)]) + let value_i32 = blk.zext(I1, &value_i1.value, I32); + blk.call( + I32, + "js_set_delete_bool", + &[(I64, &s_handle), (I32, &value_i32)], + ) }; - if receiver_i32_set { - record_collection_typed_value_fallback( - ctx, - "SetDelete", - "collection_typed_value.set_delete_generic", - &v_box, - "set", - "int32_value_helper", - "js_set_delete", - "value_expr_not_native_i32", - ); - } else if receiver_u32_set { - record_collection_typed_value_fallback( - ctx, - "SetDelete", - "collection_typed_value.set_delete_generic", - &v_box, - "set", - "uint32_value_helper", - "js_set_delete", - "value_expr_not_native_u32", - ); - } else if receiver_f32_set { - record_collection_typed_value_fallback( + record_collection_typed_value_selected( + ctx, + "SetDelete", + "collection_typed_value.set_delete_bool", + &value_i1, + "set", + "boolean_value_helper", + "js_set_delete_bool", + "set_slot", + ); + i32_v + } else if use_number_set { + let v_box = lower_expr(ctx, value)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + guarded_set_number_delete(ctx, &s_handle, &v_box) + } else { + if use_string_set { + let value_ref = lower_expr_native( ctx, - "SetDelete", - "collection_typed_value.set_delete_generic", - &v_box, - "set", - "float32_value_helper", - "js_set_delete", - "value_expr_not_native_f32", - ); - } else if receiver_boolean_set { - record_collection_typed_value_fallback( + value, + crate::native_value::ExpectedNativeRep::StringRef, + )?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + let i32_v = blk.call( + I32, + "js_set_delete_string", + &[(I64, &s_handle), (I64, &value_ref.value)], + ); + i32_v + }; + record_collection_string_key_selected( ctx, "SetDelete", - "collection_typed_value.set_delete_generic", - &v_box, + "collection_string_key.set_delete", + &value_ref.value, "set", - "boolean_value_helper", - "js_set_delete", - "value_expr_not_native_i1", + "js_set_delete_string", ); - } else if receiver_number_set { - record_collection_number_key_fallback( + record_collection_typed_value_selected( ctx, "SetDelete", - "collection_number_value.set_delete_generic", - &v_box, + "collection_typed_value.set_delete_string", + &value_ref, "set", - "number_value_helper", - "js_set_delete", - "value_expr_not_numeric", - "value", + "string_value_helper", + "js_set_delete_string", + "set_slot", ); + i32_v } else { - record_collection_string_key_fallback( - ctx, - "SetDelete", - "collection_string_key.set_delete_generic", - &v_box, - "set", - "js_set_delete", - "receiver_or_value_not_static_string", - ); + let v_box = lower_expr(ctx, value)?; + let s_handle = reread_set_receiver(ctx, group, &s_handle_unrooted)?; + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_set_delete", &[(I64, &s_handle), (DOUBLE, &v_box)]) + }; + if receiver_i32_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "int32_value_helper", + "js_set_delete", + "value_expr_not_native_i32", + ); + } else if receiver_u32_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "uint32_value_helper", + "js_set_delete", + "value_expr_not_native_u32", + ); + } else if receiver_f32_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "float32_value_helper", + "js_set_delete", + "value_expr_not_native_f32", + ); + } else if receiver_boolean_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "boolean_value_helper", + "js_set_delete", + "value_expr_not_native_i1", + ); + } else if receiver_number_set { + record_collection_number_key_fallback( + ctx, + "SetDelete", + "collection_number_value.set_delete_generic", + &v_box, + "set", + "number_value_helper", + "js_set_delete", + "value_expr_not_numeric", + "value", + ); + } else { + record_collection_string_key_fallback( + ctx, + "SetDelete", + "collection_string_key.set_delete_generic", + &v_box, + "set", + "js_set_delete", + "receiver_or_value_not_static_string", + ); + } + i32_v } - i32_v - } - }; + }; + Ok(i32_v) + })?; let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_v, "0"); let tagged = blk.select( diff --git a/crates/perry-codegen/src/lower_call/property_get/map_set.rs b/crates/perry-codegen/src/lower_call/property_get/map_set.rs index 68076e8e34..d3304ca3e1 100644 --- a/crates/perry-codegen/src/lower_call/property_get/map_set.rs +++ b/crates/perry-codegen/src/lower_call/property_get/map_set.rs @@ -26,7 +26,7 @@ use anyhow::Result; use perry_hir::Expr; -use crate::expr::{lower_expr, unbox_to_i64, FnCtx}; +use crate::expr::{lower_expr, nanbox_pointer_inline, unbox_to_i64, FnCtx}; use crate::nanbox::double_literal; use crate::rooting; use crate::type_analysis::{ @@ -88,11 +88,23 @@ pub(crate) fn try_lower_map_set_methods( (vals[0].clone(), vals[1].clone(), vals[2].clone()); let blk = ctx.block(); let m_handle = unbox_to_i64(blk, &m_box); - blk.call_void( + // #9523: `js_map_set` returns the RECEIVER as it stands + // after the insert. For a `class X extends Map` instance + // that receiver is a movable `ObjectHeader` the runtime + // roots across the grow (`map_op_returning_receiver`), so + // a moving minor inside `ensure_capacity` hands back a + // different address — and `m_box`, read from its slot + // BEFORE the call, is then a from-space pointer. The + // chained `.set(a, 1).set(b, 2)` consumes exactly that + // return value. `Expr::MapSet` already re-boxes the + // returned pointer; this arm called the helper as `void` + // and returned the pre-call box. + let receiver = blk.call( + I64, "js_map_set", &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], ); - Ok(Some(m_box)) + Ok(Some(nanbox_pointer_inline(blk, &receiver))) }, ); } diff --git a/crates/perry-codegen/src/rooting/mod.rs b/crates/perry-codegen/src/rooting/mod.rs index 1a16070e08..e791b8215b 100644 --- a/crates/perry-codegen/src/rooting/mod.rs +++ b/crates/perry-codegen/src/rooting/mod.rs @@ -1670,6 +1670,10 @@ const MIGRATED_MODULES: &[(&str, &str)] = &[ "crates/perry-codegen/src/expr/logical_collections.rs", include_str!("../expr/logical_collections.rs"), ), + ( + "crates/perry-codegen/src/expr/bigint_set.rs", + include_str!("../expr/bigint_set.rs"), + ), ( "crates/perry-codegen/src/lower_call/property_get/map_set.rs", include_str!("../lower_call/property_get/map_set.rs"), diff --git a/crates/perry-codegen/src/temp_root_coverage/mod.rs b/crates/perry-codegen/src/temp_root_coverage/mod.rs index 502a6f693b..d46ad2ce0a 100644 --- a/crates/perry-codegen/src/temp_root_coverage/mod.rs +++ b/crates/perry-codegen/src/temp_root_coverage/mod.rs @@ -47,6 +47,7 @@ mod builtin_ctor; mod call_callee; mod dispatch_receiver; mod operands; +mod set_receiver; pub(crate) fn entry_opts() -> CompileOptions { CompileOptions { diff --git a/crates/perry-codegen/src/temp_root_coverage/set_receiver.rs b/crates/perry-codegen/src/temp_root_coverage/set_receiver.rs new file mode 100644 index 0000000000..e8b6ae2560 --- /dev/null +++ b/crates/perry-codegen/src/temp_root_coverage/set_receiver.rs @@ -0,0 +1,204 @@ +//! #9523: the `Expr::SetHas` / `Expr::SetDelete` RECEIVER is a rooted +//! temporary, not an SSA register. +//! +//! `expr/bigint_set.rs` lowered the receiver, unboxed it to a raw `i64` handle, +//! THEN lowered the value expression — arbitrary user code that can allocate — +//! and consumed the handle after. That is the #6970 shape `MapGet` / `MapHas` +//! / `MapDelete` were fixed for (`math_simple.rs`, `logical_collections.rs`), +//! found live in these two twins by #9522's audit. An evacuating minor inside +//! the value's lowering moves the Set; the handle keeps the pre-move address, +//! and `js_set_has` reads a from-space header. `test-files/test_gap_9523_set_ +//! receiver_roots_across_value.ts` is the end-to-end half. +//! +//! # Non-vacuity +//! +//! The positive assertions name the VALUE — the register `js_set_alloc` +//! produced went into a rooted slot, and the consuming helper read its operand +//! back OUT of that slot — so a compiler that roots nothing cannot satisfy +//! them by emitting nothing. The receiver is deliberately a fresh `SetNew` +//! rather than a local read: a load out of a shadow slot is a re-readable +//! location that `root_reload` already re-derives below the collection point, +//! so a `LocalGet` receiver would pass against the unfixed compiler. (The +//! typed-arm test below uses a local on purpose and pins the *temp* slot the +//! fix adds, which `root_reload` never emits.) +//! +//! The negative controls hold the other side: a value that cannot collect +//! leaves no window, so the lowering must stay on its pre-#9523 IR with no +//! temp slot at all. +//! +//! Sabotage: reverting `bigint_set.rs`'s two arms to the eager +//! `unbox_to_i64(lower_expr(set))` fails the positive tests with "is never +//! stored into a rooted slot — it lives its whole life in an SSA register" and +//! the typed-arm test with a temp-slot count of 0. + +use super::{allocating, main_ir_for, under_both_lowerings}; +use crate::testing::temp_slots::{ + assert_no_temp_rooting, assert_rooted_across, first_call_result, temp_root_slots, +}; +use perry_hir::types::Type; +use perry_hir::{Expr, Stmt}; + +const STRING_SET_LOCAL: u32 = 910; + +fn set_has(set: Expr, value: Expr) -> Stmt { + Stmt::Expr(Expr::SetHas { + set: Box::new(set), + value: Box::new(value), + }) +} + +fn set_delete(set: Expr, value: Expr) -> Stmt { + Stmt::Expr(Expr::SetDelete { + set: Box::new(set), + value: Box::new(value), + }) +} + +/// `const s: Set = new Set()` — the receiver shape the frontend +/// produces (`Expr::SetHas { set: LocalGet(..) }`), typed so the string arm +/// (`js_set_has_string`) is selected. +fn string_set_local() -> Stmt { + Stmt::Let { + id: STRING_SET_LOCAL, + name: "s".to_string(), + ty: Type::Generic { + base: "Set".to_string(), + type_args: vec![Type::String], + }, + mutable: false, + init: Some(Expr::SetNew), + } +} + +/// `String({})` — runtime-guaranteed to be a string, so the typed string arm +/// is selected, and a collection point: `ToPrimitive` on an object can run user +/// code, and the object literal itself allocates. +fn allocating_string() -> Expr { + Expr::StringCoerce(Box::new(allocating())) +} + +/// THE GAP (#9523), `has`: the receiver is produced before the value and +/// consumed after it, so it must live in a rooted slot and `js_set_has` must +/// read it back out of that slot. +#[test] +fn a_set_has_receiver_is_rooted_across_an_allocating_value() { + under_both_lowerings(|lowering| { + let ir = main_ir_for( + "set_has_receiver_rooted.ts", + vec![set_has(Expr::SetNew, allocating())], + ); + let set = first_call_result(&ir, "js_set_alloc").unwrap_or_else(|| { + panic!( + "{lowering}: no call to `js_set_alloc` in `main` — this test has no subject:\n{ir}" + ) + }); + assert_rooted_across( + &ir, + &set, + "js_set_has", + &format!( + "{lowering}: #9523 — the Set receiver is live across the value, which allocates" + ), + ); + }); +} + +/// THE GAP (#9523), `delete`: same window, same contract, the other twin. +#[test] +fn a_set_delete_receiver_is_rooted_across_an_allocating_value() { + under_both_lowerings(|lowering| { + let ir = main_ir_for( + "set_delete_receiver_rooted.ts", + vec![set_delete(Expr::SetNew, allocating())], + ); + let set = first_call_result(&ir, "js_set_alloc").unwrap_or_else(|| { + panic!( + "{lowering}: no call to `js_set_alloc` in `main` — this test has no subject:\n{ir}" + ) + }); + assert_rooted_across( + &ir, + &set, + "js_set_delete", + &format!( + "{lowering}: #9523 — the Set receiver is live across the value, which allocates" + ), + ); + }); +} + +/// The control on the other side: a value that cannot collect leaves no +/// window, so the receiver must not pay a temp slot and the IR is exactly what +/// it was before #9523. Without this half, a lowering that roots every receiver +/// unconditionally would pass the tests above and pay for it on every `has`. +#[test] +fn a_set_has_with_a_non_allocating_value_pays_no_temp_slot() { + under_both_lowerings(|lowering| { + for (name, stmt) in [ + ("set_has_no_gc.ts", set_has(Expr::SetNew, Expr::Integer(7))), + ( + "set_delete_no_gc.ts", + set_delete(Expr::SetNew, Expr::Integer(7)), + ), + ] { + let ir = main_ir_for(name, vec![stmt]); + assert!( + ir.contains("@js_set_has(") || ir.contains("@js_set_delete("), + "{lowering}: {name} must reach the generic Set helper, or this proves nothing:\n{ir}" + ); + assert_no_temp_rooting( + &ir, + &format!("{lowering}: {name} — #9523 gate: nothing after the receiver can collect"), + ); + } + }); +} + +/// The typed arms take the same window. A `Set` local with a +/// string-guaranteed, allocating value selects `js_set_has_string`, and the +/// receiver — a plain local read — must be pushed into a TEMP slot for the +/// value's duration. `root_reload` would re-derive the shadow-slot load on its +/// own, but it never emits a temp slot, so the count below is the fix's own +/// signature: exactly one temp slot with the value, none without it. +#[test] +fn a_typed_string_set_receiver_is_temp_rooted_only_when_the_value_collects() { + under_both_lowerings(|lowering| { + let rooted = main_ir_for( + "set_has_string_arm_rooted.ts", + vec![ + string_set_local(), + set_has(Expr::LocalGet(STRING_SET_LOCAL), allocating_string()), + ], + ); + assert!( + rooted.contains("@js_set_has_string("), + "{lowering}: the fixture must select the string arm, or this proves nothing:\n{rooted}" + ); + let rooted_slots = temp_root_slots(&rooted); + assert_eq!( + rooted_slots.len(), + 1, + "{lowering}: #9523 — the string-arm receiver must be the ONE temp root across \ + the allocating value; got {rooted_slots:?}:\n{rooted}" + ); + + let unrooted = main_ir_for( + "set_has_string_arm_no_gc.ts", + vec![ + string_set_local(), + set_has( + Expr::LocalGet(STRING_SET_LOCAL), + Expr::String("k".to_string()), + ), + ], + ); + assert!( + unrooted.contains("@js_set_has_string("), + "{lowering}: the control must select the same arm:\n{unrooted}" + ); + assert_no_temp_rooting( + &unrooted, + &format!("{lowering}: a literal value cannot collect, so the receiver pays no slot"), + ); + }); +} diff --git a/test-files/test_gap_9523_map_set_chain_returns_receiver.ts b/test-files/test_gap_9523_map_set_chain_returns_receiver.ts new file mode 100644 index 0000000000..b2dab61399 --- /dev/null +++ b/test-files/test_gap_9523_map_set_chain_returns_receiver.ts @@ -0,0 +1,89 @@ +// #9523 (second item): `lower_call/property_get/map_set.rs`'s `"set"` arm — the +// path `this.field.set(k, v)` takes when the field is declared `Map` — +// called `js_map_set` as `void` and returned the receiver box it had read from +// its root slot BEFORE the call. `Expr::MapSet` (the `m.set(k, v)` shape on a +// plain local) re-boxes the pointer the helper RETURNS instead. +// +// `js_map_set` returns the receiver as it stands after the insert. When the +// receiver is a `class X extends Map` instance, the runtime resolves it to the +// hidden backing `MapHeader`, roots the receiver (a movable `ObjectHeader`) and +// runs the insert under that root (`map_op_returning_receiver`, #7570). A +// moving minor inside the grow (`ensure_capacity` notes an external side +// allocation, which can trigger one) relocates the receiver, and the helper +// hands back the NEW address. The pre-call box is then a from-space pointer — +// and a chained `.set(a, 1).set(b, 2)` is exactly the consumer of that value: +// the second call dispatches on whatever the first one returned. +// +// The fault needs the minor to fire INSIDE the first `set`'s grow, which is a +// pressure question rather than a deterministic one, so this fixture sweeps +// the nursery fill across rounds and reports how many rounds disagree with +// the specification. Node prints `bad=0` for every round. + +class Registry extends Map {} + +class Holder { + m: Map; + constructor() { + this.m = new Registry(); + } + // The chained shape. The FIRST `.set` is the `"set"` arm; the second one + // consumes its return value. + put(a: string, b: string): number { + this.m.set(a, 1).set(b, 2); + return this.m.size; + } + // The identity contract on its own: `Map.prototype.set` returns `this`. + same(a: string): boolean { + return this.m.set(a, 3) === this.m; + } +} + +// Allocates `n` escaping cells (kept alive in a bounded window) so the nursery +// is filled to a controlled level before the chained set runs. +function fill(n: number): any[] { + let keep: any[] = []; + for (let i = 0; i < n; i++) { + keep.push({ a: i, b: i + 1, c: i + 2, d: i + 3 }); + if (keep.length >= 2048) { + keep = []; + } + } + return keep; +} + +function main(): void { + let bad = 0; + let sameOk = 0; + const rounds = 24; + for (let r = 0; r < rounds; r++) { + const holder = new Holder(); + const reg = new Registry(); + // Fill the initial capacity so the chained set's first insert grows. + for (let i = 0; i < 8; i++) { + reg.set("p" + r + "_" + i, i); + } + holder.m = reg; + // Sweep the fill so successive rounds land the grow at different points of + // the nursery budget. + const keep = fill(120000 + r * 20000); + const size = holder.put("a" + r, "b" + r); + const ok = + size === 10 && + holder.m.get("a" + r) === 1 && + holder.m.get("b" + r) === 2 && + holder.m.get("p" + r + "_7") === 7; + if (!ok) { + bad++; + } + if (holder.same("s" + r)) { + sameOk++; + } + if (keep.length < 0) { + console.log("unreachable"); + } + } + console.log("map-set-chain bad=" + bad); + console.log("set returns receiver=" + sameOk + "/" + rounds); +} + +main(); diff --git a/test-files/test_gap_9523_set_receiver_roots_across_value.ts b/test-files/test_gap_9523_set_receiver_roots_across_value.ts new file mode 100644 index 0000000000..baf1ef4cdb --- /dev/null +++ b/test-files/test_gap_9523_set_receiver_roots_across_value.ts @@ -0,0 +1,134 @@ +// #9523: `Expr::SetHas` and `Expr::SetDelete` unboxed the receiver to a raw +// `i64` handle BEFORE lowering the value expression and consumed that handle +// after it — the #6970 shape their Map twins (`MapGet` / `MapHas` / +// `MapDelete`) were fixed for. +// +// `crates/perry-codegen/src/expr/bigint_set.rs` lowered `set`, masked the +// pointer out of the NaN-box into an SSA register, THEN lowered `value` — +// arbitrary user code that allocates — and only then called `js_set_has` / +// `js_set_delete` with the register. An evacuating young-gen minor inside the +// value's evaluation moves the Set; the register keeps the pre-move address, +// and the runtime helper reads a from-space header. Nothing faults at the +// move: the answer is simply wrong (`has` false for a member, `delete` false +// and the member still present) or the process dies on a recycled cell. +// +// TWO THINGS THIS FIXTURE NEEDS, AND BOTH ARE LOAD-BEARING: +// +// 1. The receiver must be a MODULE-LEVEL binding read from inside a function. +// A function-local Set lives in a shadow slot, and `root_reload.rs` already +// re-materialises a slot load — together with the `bitcast`/`and` unmask +// derived from it — below every collection point that can reach a use, so +// a plain local receiver passes on the unfixed compiler. A module-level +// binding is a `@perry_global_*` load, which that pass deliberately does +// NOT reload ("that population needs rooting, not reloading"), so the raw +// handle is the only copy the consuming call ever sees. +// 2. The value must allocate past the 16 MiB nursery cap +// (`SCAVENGE_NURSERY_CAP_DEFAULT_MB`) with cells that ESCAPE, the +// `test_gap_9417_dispatch_receiver_roots.ts` recipe. A scalar-replaced +// object literal never reaches the arena, and a churn that allocates +// nothing is a test that cannot fail. +// +// Each probe allocates a FRESH Set immediately before the call, so the Set is +// a nursery object when the value's churn runs and the minor that fires there +// is the one that evacuates it. + +let stringSet: Set = new Set(); +let numberSet: Set = new Set(); +let anySet: Set = new Set(); + +function churn(n: number): void { + let keep: any[] = []; + for (let i = 0; i < n; i++) { + const cell = { a: i, b: i + 1, c: i + 2, d: i + 3 }; + keep.push(cell); + if (keep.length >= 1024) { + keep = []; + } + } +} + +// The value expressions. Each one collects (churn) and then builds the key it +// returns, so the key itself is a post-collection object. +function stringKey(k: number): string { + churn(400000); + return "k" + k; +} +function numberKey(k: number): number { + churn(400000); + return k * 3 + 0.5; +} +function anyKey(k: number): any { + churn(400000); + return "a" + k; +} + +// THE GAP, string-typed receiver (`js_set_has_string` / `js_set_delete_string`). +function probeStringSet(k: number): string { + stringSet = new Set(); + stringSet.add("k" + k); + const has = stringSet.has(stringKey(k)); + stringSet = new Set(); + stringSet.add("k" + k); + const del = stringSet.delete(stringKey(k)); + return has + "/" + del + "/" + stringSet.size; +} + +// THE GAP, number-typed receiver (the guarded number arm). +function probeNumberSet(k: number): string { + numberSet = new Set(); + numberSet.add(k * 3 + 0.5); + const has = numberSet.has(numberKey(k)); + numberSet = new Set(); + numberSet.add(k * 3 + 0.5); + const del = numberSet.delete(numberKey(k)); + return has + "/" + del + "/" + numberSet.size; +} + +// THE GAP, untyped value (the generic `js_set_has` / `js_set_delete` arm). +function probeAnySet(k: number): string { + anySet = new Set(); + anySet.add("a" + k); + const has = anySet.has(anyKey(k)); + anySet = new Set(); + anySet.add("a" + k); + const del = anySet.delete(anyKey(k)); + return has + "/" + del + "/" + anySet.size; +} + +// CONTRACT, NOT A GAP: a value that cannot collect leaves no window, so the +// lowering must stay on its unprotected path and still answer correctly. Here +// so a fix that over-roots or mis-orders the group is caught. +function probeControl(k: number): string { + stringSet = new Set(); + const key = "k" + k; + stringSet.add(key); + const has = stringSet.has(key); + const del = stringSet.delete(key); + return has + "/" + del + "/" + stringSet.size; +} + +function main(): void { + const rounds = 6; + let bad = 0; + let first = ""; + for (let k = 0; k < rounds; k++) { + const results = [ + "string " + probeStringSet(k), + "number " + probeNumberSet(k), + "any " + probeAnySet(k), + ]; + for (const r of results) { + if (r.indexOf(" true/true/0") < 0) { + bad++; + if (first === "") { + first = r; + } + } + } + } + console.log("set-receiver bad=" + bad); + console.log("first bad=" + first); + console.log("control=" + probeControl(0)); +} + +main(); From 7c3bd4f9932d776a42e5e142f399fd4e4b037ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 14:53:26 +0200 Subject: [PATCH 2/2] changelog: #9532 Set receiver rooting fragment; bump 0.5.1521 --- CLAUDE.md | 2 +- Cargo.lock | 156 +++++++++--------- Cargo.toml | 2 +- .../9532-set-receiver-root-across-value.md | 37 +++++ 4 files changed, 117 insertions(+), 80 deletions(-) create mode 100644 changelog.d/9532-set-receiver-root-across-value.md diff --git a/CLAUDE.md b/CLAUDE.md index 94d8b53bf4..fc684662e0 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.1520 +**Current Version:** 0.5.1521 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index d6c4beedb8..15f106c66d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5632,7 +5632,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "base64 0.22.1", @@ -5694,7 +5694,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-dispatch", "serde", @@ -5702,7 +5702,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "cc", "libc", @@ -5711,7 +5711,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "inkwell", @@ -5728,7 +5728,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-hir", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-hir", @@ -5744,7 +5744,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-dispatch", @@ -5753,7 +5753,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-hir", @@ -5761,7 +5761,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "base64 0.22.1", @@ -5773,7 +5773,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-hir", @@ -5781,7 +5781,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "async-trait", @@ -5810,14 +5810,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "serde", "serde_json", @@ -5825,7 +5825,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1520" +version = "0.5.1521" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5836,7 +5836,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "clap", @@ -5851,7 +5851,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "block2", "objc2", @@ -5861,7 +5861,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "argon2", "perry-ffi", @@ -5870,7 +5870,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "reqwest", @@ -5879,7 +5879,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "bcrypt", "perry-ffi", @@ -5887,7 +5887,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "rusqlite", @@ -5895,7 +5895,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "scraper", @@ -5903,7 +5903,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "perry-runtime", @@ -5911,7 +5911,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "chrono", "cron", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "chrono", "perry-ffi", @@ -5929,7 +5929,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "rust_decimal", @@ -5937,7 +5937,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "serde_json", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5953,7 +5953,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "perry-runtime", @@ -5961,14 +5961,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "bytes", "http-body-util", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "bytes", "lazy_static", @@ -5999,7 +5999,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "bytes", @@ -6031,7 +6031,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "lazy_static", "perry-ffi", @@ -6041,7 +6041,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "lru", "perry-ffi", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "chrono", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "bson", "futures-util", @@ -6081,7 +6081,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "chrono", "perry-ffi", @@ -6093,7 +6093,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "nanoid", "perry-ffi", @@ -6102,7 +6102,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "bytes", "perry-ffi", @@ -6117,7 +6117,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6136,7 +6136,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "lettre", "perry-ffi", @@ -6146,7 +6146,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "fancy-regex", "notify", @@ -6158,7 +6158,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "printpdf", @@ -6166,7 +6166,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "sqlx", @@ -6175,7 +6175,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "perry-runtime", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "governor", "perry-ffi", @@ -6192,7 +6192,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "fast_image_resize", "image", @@ -6203,7 +6203,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "lazy_static", "perry-ffi", @@ -6212,7 +6212,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "serde", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "perry-runtime", @@ -6237,7 +6237,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "uuid", @@ -6245,7 +6245,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "regex", @@ -6255,7 +6255,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "futures-util", "lazy_static", @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "brotli", "flate2", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "dashmap", "once_cell", @@ -6287,7 +6287,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-api-manifest", @@ -6306,7 +6306,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-diagnostics", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "base64 0.22.1", @@ -6369,14 +6369,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6471,14 +6471,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "perry-hir", @@ -6487,7 +6487,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6495,7 +6495,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "itoa", @@ -6513,7 +6513,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "rand 0.10.2", "serde", @@ -6523,7 +6523,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.0", @@ -6546,7 +6546,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "block2", @@ -6563,7 +6563,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "block2", @@ -6579,7 +6579,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1520" +version = "0.5.1521" [[package]] name = "perry-ui-test" @@ -6590,11 +6590,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1520" +version = "0.5.1521" [[package]] name = "perry-ui-tvos" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "block2", @@ -6611,7 +6611,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "block2", @@ -6628,7 +6628,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "block2", "libc", @@ -6642,7 +6642,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "libc", @@ -6661,7 +6661,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "base64 0.22.1", "libc", @@ -6674,7 +6674,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "anyhow", "base64 0.22.1", @@ -6690,7 +6690,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1520" +version = "0.5.1521" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 7652038146..85cf4032c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -335,7 +335,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1520" +version = "0.5.1521" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/9532-set-receiver-root-across-value.md b/changelog.d/9532-set-receiver-root-across-value.md new file mode 100644 index 0000000000..fda555d99d --- /dev/null +++ b/changelog.d/9532-set-receiver-root-across-value.md @@ -0,0 +1,37 @@ +### Fixed + +- **`s.has(makeKey())` / `s.delete(makeKey())` on a module-level `Set` no + longer read a moved receiver.** `Expr::SetHas` and `Expr::SetDelete` + (`expr/bigint_set.rs`) lowered the receiver, masked it to a raw `i64` + handle, *then* lowered the value expression, and consumed the handle after + — the #6970 shape their Map twins (`MapGet` / `MapHas` / `MapDelete`) were + fixed for, found live by #9522's audit of every Map/Set/WeakMap lowering + (#9523). A function-local Set was already covered — `root_reload` re-derives + a shadow-slot load and its unmask below every collection point — but a + module-level Set is a `@perry_global_*` load, which that pass deliberately + does not reload, so an evacuating minor inside the value's evaluation left + `js_set_has` a from-space header. Measured: the new gap fixture SIGSEGVs on + unfixed main where node prints `bad=0`; the from-space quarantine reports the + fault address as retired by the first minor with a last-known object of + `GC_TYPE_SET`. + + Both arms now root the receiver in a `RootedGroup` before the value is + lowered and re-read it from the slot afterwards, exactly as the Map twins + do; when the value cannot collect nothing is pushed and the IR is unchanged. + `bigint_set.rs` joins the rooting migration ledger. + +- **`this.field.set(a, 1).set(b, 2)` consumes the receiver `js_map_set` + returns.** `lower_call/property_get/map_set.rs`'s `"set"` arm called the + helper as `void` and returned the receiver box it had read *before* the + call. `js_map_set` returns the receiver as it stands after the insert — for + a `class X extends Map` instance the runtime roots the movable + `ObjectHeader` across the grow and hands back the relocated address — so the + chained call could dispatch on a from-space pointer. The arm now re-boxes + the returned pointer, as `Expr::MapSet` already did. Latent (the minor must + fire inside the first insert's grow); pinned by a chained-set fixture. + + Fixtures: `test-files/test_gap_9523_set_receiver_roots_across_value.ts` + (fails on unfixed main, byte-identical to node fixed) and + `test_gap_9523_map_set_chain_returns_receiver.ts`; codegen contract in + `temp_root_coverage/set_receiver.rs`, sabotage-verified under both root + lowerings.