From 9180b7f40c372fd05bff330367cfef16dec5a189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 27 Jul 2026 05:01:30 +0200 Subject: [PATCH 1/5] =?UTF-8?q?perf(codegen/runtime):=20#6812=20w12=20?= =?UTF-8?q?=E2=80=94=203-way=20dynamic-key=20write=20IC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same-receiver dynamic-key stores route through an outlined per-site IC (same perry_ic_N [8 x i64] global family as the static write PIC): [shape_token, (key_bits, slot) x 3]. A way's key is the NaN-boxed key VALUE — SSO keys (<= 5 bytes) compare by CONTENT (move-immune, hits across distinct string instances), heap keys by identity (a moved or re-built key misses; identity can never false-hit). The hit path validates everything against the receiver's LIVE state (ordinary/unblocked/class-tagged object, current shape token equality, slot within the inline region) — cached token/slot are compared, never dereferenced, so stale entries self-heal by missing. No rooting or safepoint bookkeeping on the hot path: that is the structural difference from the discarded safepointing-RHS approach, whose per-write rooting measured 2x slower. Miss = full [[Set]] + MRU re-prime (shape change resets the way set); every call is at worst the generic path. Distinct-receiver stores keep the receiver-aware generic helper. Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG --- .../perry-codegen/src/expr/proxy_reflect.rs | 32 ++- .../src/runtime_decls/objects.rs | 6 + crates/perry-runtime/src/proxy/put_value.rs | 243 ++++++++++++++++++ 3 files changed, 276 insertions(+), 5 deletions(-) diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index bf22b72809..123dd12374 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -1047,12 +1047,34 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let t = lower_expr(ctx, target)?; let k = lower_expr(ctx, key)?; let v = lower_expr(ctx, value)?; - let r = if same_put_value_receiver_expr(target, receiver) { - t.clone() - } else { - lower_expr(ctx, receiver)? - }; let strict_i32 = if *strict { "1" } else { "0" }; + // #6812 (w12): same-receiver dynamic-key stores route through the + // outlined 3-way dynamic-key IC — per-site cache in the same + // `perry_ic_N` global family as the static write PIC. The helper + // validates everything against the receiver's LIVE state (cached + // token/slot are compared, never dereferenced) and falls through + // to full `[[Set]]` + re-prime on any mismatch, so every call is + // at worst the generic path. Distinct-receiver stores keep the + // receiver-aware generic helper. + if same_put_value_receiver_expr(target, receiver) { + let site_id = ctx.ic_site_counter; + ctx.ic_site_counter += 1; + let cache_name = format!("perry_ic_{}", site_id); + ctx.ic_globals.push(cache_name.clone()); + let cache_ref = format!("@{}", cache_name); + return Ok(ctx.block().call( + DOUBLE, + "js_put_value_set_dyn_ic", + &[ + (crate::types::PTR, &cache_ref), + (DOUBLE, &t), + (DOUBLE, &k), + (DOUBLE, &v), + (I32, strict_i32), + ], + )); + } + let r = lower_expr(ctx, receiver)?; Ok(ctx.block().call( DOUBLE, "js_put_value_set", diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 9e61684cd4..70cd18c90d 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -453,6 +453,12 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { DOUBLE, &[DOUBLE, DOUBLE, DOUBLE, DOUBLE, I32], ); + // #6812 (w12): outlined 3-way dynamic-key write IC (per-site cache ptr). + module.declare_function( + "js_put_value_set_dyn_ic", + DOUBLE, + &[PTR, DOUBLE, DOUBLE, DOUBLE, I32], + ); module.declare_function( "js_put_value_set_ic_miss", DOUBLE, diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 6be163e033..40375f8972 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -381,6 +381,249 @@ pub extern "C" fn js_put_value_set_ic_miss( result } +// --------------------------------------------------------------------------- +// #6812 (w12): 3-way dynamic-key write IC. +// +// Layout of the per-site `[8 x i64]` cache (same global family as the +// static write PIC): `[shape_token, k0, s0, k1, s1, k2, s2, _spare]`. +// A way's key is the NaN-boxed key VALUE's bits: SSO keys (<= 5 bytes) +// compare by CONTENT — move-immune and hitting across distinct string +// instances — while heap keys compare by identity (a moved or re-built key +// simply misses; identity can never false-hit). The shape token mirrors the +// static PIC (stamped ShapeId lifted with the ID bit, else the shared keys +// pointer) and, like every Perry IC, is only COMPARED, never dereferenced: +// stale entries self-heal by missing. That property is what the discarded +// safepointing-RHS approach lacked — nothing here roots or revalidates +// across safepoints, so the hot path is pure compares before a store. + +const DYN_IC_WAYS: usize = 3; + +/// Outlined dynamic-key PutValue with per-site cache. Fast path: shape token +/// + key-bits match -> validated own-slot overwrite. Everything else falls +/// through to the full `[[Set]]` semantics and re-primes. +#[no_mangle] +pub extern "C" fn js_put_value_set_dyn_ic( + cache: *mut [i64; 8], + target: f64, + key: f64, + value: f64, + strict: i32, +) -> f64 { + if !cache.is_null() { + let hit = unsafe { + let c = &*cache; + let token = c[0] as u64; + if token != 0 { + let key_bits = key.to_bits() as i64; + let mut found = None; + for way in 0..DYN_IC_WAYS { + if c[1 + way * 2] == key_bits { + found = Some(c[2 + way * 2] as u32); + break; + } + } + found.and_then(|slot| dyn_ic_try_store(target, token, slot, value)) + } else { + None + } + }; + if let Some(ret) = hit { + return ret; + } + } + js_put_value_set_dyn_ic_miss(cache, target, key, value, strict) +} + +/// Validated fast store: the receiver must still be an ordinary, +/// non-forwarded, unblocked, class-tagged heap object whose CURRENT shape +/// token equals the cached one and whose inline region covers the slot. +/// Every check reads the receiver's live state — the cached token/slot are +/// never dereferenced — so a GC between prime and hit at worst causes a +/// miss, never a wrong store. +#[inline] +unsafe fn dyn_ic_try_store(target: f64, token: u64, slot: u32, value: f64) -> Option { + let target_bits = target.to_bits(); + if (target_bits & !POINTER_MASK) != POINTER_TAG { + return None; + } + let obj_addr = (target_bits & POINTER_MASK) as usize; + let gc_header = crate::value::addr_class::try_read_gc_header(obj_addr)?; + const BLOCKING_FLAGS: u16 = crate::gc::OBJ_FLAG_FROZEN + | crate::gc::OBJ_FLAG_SEALED + | crate::gc::OBJ_FLAG_NO_EXTEND + | crate::gc::OBJ_FLAG_HAS_DESCRIPTORS + | crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO + | crate::gc::GC_OBJ_TYPED_LAYOUT_INTACT; + if gc_header.obj_type != crate::gc::GC_TYPE_OBJECT + || gc_header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + || gc_header._reserved & BLOCKING_FLAGS != 0 + { + return None; + } + let obj = obj_addr as *mut crate::ObjectHeader; + let class_id = (*obj).class_id; + if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR + || class_id == 0 + || class_id == crate::object::NATIVE_MODULE_CLASS_ID + { + return None; + } + let current_token = { + let parent_class_id = (*obj).parent_class_id; + if crate::object::shapes::is_shape_id(parent_class_id) { + crate::object::shapes::PIC_ID_TOKEN_BIT | parent_class_id as u64 + } else { + (*obj).keys_array as u64 + } + }; + if current_token != token { + return None; + } + let alloc_limit = std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32); + if slot >= alloc_limit { + return None; + } + crate::object::store_object_field_slot(obj, slot as usize, value.to_bits()); + Some(value) +} + +// #6088-style keep: codegen emits the only call; a whole-program bitcode +// link would otherwise dead-strip the IC entry. +#[used] +static KEEP_JS_PUT_VALUE_SET_DYN_IC: extern "C" fn(*mut [i64; 8], f64, f64, f64, i32) -> f64 = + js_put_value_set_dyn_ic; + +/// Full `[[Set]]` semantics + prime. Mirrors the static PIC's prime policy +/// (only a successful ordinary own-data overwrite on a class-tagged, +/// unblocked, shape-shared receiver may prime), with the key resolved from +/// its VALUE (SSO or heap) instead of requiring an interned pointer. +#[no_mangle] +pub extern "C" fn js_put_value_set_dyn_ic_miss( + cache: *mut [i64; 8], + target: f64, + key: f64, + value: f64, + strict: i32, +) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let target_handle = scope.root_nanbox_f64(target); + let key_handle = scope.root_nanbox_f64(key); + let value_handle = scope.root_nanbox_f64(value); + let result = js_put_value_set( + target_handle.get_nanbox_f64(), + key_handle.get_nanbox_f64(), + value_handle.get_nanbox_f64(), + target_handle.get_nanbox_f64(), + strict, + ); + if cache.is_null() { + return result; + } + unsafe { + let target = target_handle.get_nanbox_f64(); + let key = key_handle.get_nanbox_f64(); + let target_bits = target.to_bits(); + if (target_bits & !POINTER_MASK) != POINTER_TAG { + return result; + } + let obj_addr = (target_bits & POINTER_MASK) as usize; + let Some(gc_header) = crate::value::addr_class::try_read_gc_header(obj_addr) else { + return result; + }; + const BLOCKING_FLAGS: u16 = crate::gc::OBJ_FLAG_FROZEN + | crate::gc::OBJ_FLAG_SEALED + | crate::gc::OBJ_FLAG_NO_EXTEND + | crate::gc::OBJ_FLAG_HAS_DESCRIPTORS + | crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO + | crate::gc::GC_OBJ_TYPED_LAYOUT_INTACT; + if gc_header.obj_type != crate::gc::GC_TYPE_OBJECT + || gc_header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + || gc_header._reserved & BLOCKING_FLAGS != 0 + { + return result; + } + let obj = obj_addr as *mut crate::ObjectHeader; + let class_id = (*obj).class_id; + if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR + || class_id == 0 + || class_id == crate::object::NATIVE_MODULE_CLASS_ID + || crate::array::object_prototype_addr_matches(obj_addr) + { + return result; + } + let keys = (*obj).keys_array; + if keys.is_null() || (keys as u64) >> 48 != 0 { + return result; + } + let Some(keys_gc) = crate::value::addr_class::try_read_gc_header(keys as usize) else { + return result; + }; + if keys_gc.obj_type != crate::gc::GC_TYPE_ARRAY + || keys_gc.gc_flags & (crate::gc::GC_FLAG_FORWARDED | crate::gc::GC_FLAG_SHAPE_SHARED) + != crate::gc::GC_FLAG_SHAPE_SHARED + { + return result; + } + // Resolve the key's own-field index by VALUE (SSO or heap bytes). + let mut key_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let key_jsval = crate::value::JSValue::from_bits(key.to_bits()); + let Some(key_bytes) = crate::string::js_string_key_bytes(key_jsval, &mut key_buf) else { + return result; + }; + let key_count = crate::array::keys_array_len_capped_to_capacity(keys); + if key_count > 4096 { + return result; + } + let mut own_idx = None; + let mut cand_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for i in 0..key_count { + let candidate = crate::array::js_array_get(keys, i as u32); + if let Some(cand_bytes) = crate::string::js_string_key_bytes(candidate, &mut cand_buf) { + if cand_bytes == key_bytes { + own_idx = Some(i as u32); + break; + } + } + } + let Some(idx) = own_idx else { + return result; + }; + let alloc_limit = + std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32); + if idx >= alloc_limit { + return result; + } + let parent_class_id = (*obj).parent_class_id; + let shape_token = if crate::object::shapes::is_shape_id(parent_class_id) { + crate::object::shapes::PIC_ID_TOKEN_BIT | parent_class_id as u64 + } else { + keys as u64 + }; + let c = &mut *cache; + let key_bits = key.to_bits() as i64; + if c[0] as u64 != shape_token { + // New shape at this site: restart the way set. + *c = [0; 8]; + } + // MRU insert: shift ways down, newest first. A duplicate key way is + // moved to the front rather than duplicated. + let mut ways: Vec<(i64, i64)> = (0..DYN_IC_WAYS) + .map(|w| (c[1 + w * 2], c[2 + w * 2])) + .filter(|(k, _)| *k != 0 && *k != key_bits) + .collect(); + ways.insert(0, (key_bits, idx as i64)); + ways.truncate(DYN_IC_WAYS); + for (w, (k, sl)) in ways.iter().enumerate() { + c[1 + w * 2] = *k; + c[2 + w * 2] = *sl; + } + // Token last: a zero or stale token cannot hit until it matches the + // receiver's current discriminated shape. + c[0] = shape_token as i64; + } + result +} + #[cold] fn trace_object_array_numeric_write_rejection(reason: &'static str) { if std::env::var_os("PERRY_TRACE_OBJECT_ARRAY_WRITE_GUARD").is_some() { From e9f8235313bae50c4f373b9c31692873f9a7cf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 27 Jul 2026 05:31:24 +0200 Subject: [PATCH 2/5] =?UTF-8?q?perf(codegen):=20#6812=20w12=20v2=20?= =?UTF-8?q?=E2=80=94=20inline=20hit=20path=20for=20the=20dynamic-key=20wri?= =?UTF-8?q?te=20IC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the receiver is a plain local (≡ target) and the value is compile-proven numeric, the 3-way IC hit is emitted inline: the static write PIC's exact guard sequence (heap-candidate branch, GcHeader -8/-7/-6 checks with BLOCKING 0x1907, ObjectHeader regular/class/token checks with the #6804 discriminated shape-token select), then a 3-way key-bits compare chain, a floor-4 bounds check on the phi'd slot, and a raw double store — no barrier and no layout note, sound because numeric bits never create references and hardware NaNs sit below the 0x7FFA tag space. Header offset is target-layout derived (ILP32-safe). All misses and every site failing the inline gate take the outlined helper, which bottoms out at the generic path. Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG --- .../perry-codegen/src/expr/proxy_reflect.rs | 181 +++++++++++++++++- 1 file changed, 172 insertions(+), 9 deletions(-) diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index 123dd12374..3559c69cc2 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -1049,20 +1049,176 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let v = lower_expr(ctx, value)?; let strict_i32 = if *strict { "1" } else { "0" }; // #6812 (w12): same-receiver dynamic-key stores route through the - // outlined 3-way dynamic-key IC — per-site cache in the same - // `perry_ic_N` global family as the static write PIC. The helper - // validates everything against the receiver's LIVE state (cached - // token/slot are compared, never dereferenced) and falls through - // to full `[[Set]]` + re-prime on any mismatch, so every call is - // at worst the generic path. Distinct-receiver stores keep the - // receiver-aware generic helper. + // 3-way dynamic-key IC — per-site cache in the same `perry_ic_N` + // global family as the static write PIC, layout + // `[token, k0, s0, k1, s1, k2, s2, _]`. When the receiver is a + // plain local and the VALUE is compile-proven numeric, the hit + // path is emitted INLINE (guards copied from the static PIC; + // a numeric raw store needs no barrier or layout note — numeric + // bits never create references, and hardware NaNs sit below the + // 0x7FFA tag space). Every miss — and every site that fails the + // inline gate — takes the outlined helper, which is itself at + // worst the generic path. if same_put_value_receiver_expr(target, receiver) { let site_id = ctx.ic_site_counter; ctx.ic_site_counter += 1; let cache_name = format!("perry_ic_{}", site_id); ctx.ic_globals.push(cache_name.clone()); let cache_ref = format!("@{}", cache_name); - return Ok(ctx.block().call( + let inline_ok = matches!(target.as_ref(), Expr::LocalGet(_) | Expr::This) + && is_numeric_expr(ctx, value); + if !inline_ok { + return Ok(ctx.block().call( + DOUBLE, + "js_put_value_set_dyn_ic", + &[ + (crate::types::PTR, &cache_ref), + (DOUBLE, &t), + (DOUBLE, &k), + (DOUBLE, &v), + (I32, strict_i32), + ], + )); + } + let k_bits = ctx.block().bitcast_double_to_i64(&k); + let t_bits = ctx.block().bitcast_double_to_i64(&t); + let t_handle = ctx.block().and(I64, &t_bits, POINTER_MASK_I64); + let t_tag = ctx.block().lshr(I64, &t_bits, "48"); + let is_ptr = ctx.block().icmp_eq(I64, &t_tag, "32765"); + let above = ctx.block().icmp_ugt(I64, &t_handle, "1048575"); + let heap_candidate = ctx.block().and(I1, &is_ptr, &above); + + let guard_idx = ctx.new_block("put.dynic.guard"); + let ways_idx = ctx.new_block("put.dynic.ways"); + let way1_idx = ctx.new_block("put.dynic.way1"); + let way2_idx = ctx.new_block("put.dynic.way2"); + let bounds_idx = ctx.new_block("put.dynic.bounds"); + let store_idx = ctx.new_block("put.dynic.store"); + let slow_idx = ctx.new_block("put.dynic.slow"); + let merge_idx = ctx.new_block("put.dynic.merge"); + let guard_label = ctx.block_label(guard_idx); + let ways_label = ctx.block_label(ways_idx); + let way1_label = ctx.block_label(way1_idx); + let way2_label = ctx.block_label(way2_idx); + let bounds_label = ctx.block_label(bounds_idx); + let store_label = ctx.block_label(store_idx); + let slow_label = ctx.block_label(slow_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block() + .cond_br(&heap_candidate, &guard_label, &slow_label); + + // Header + object guards: byte-for-byte the static write + // PIC's emitted checks (offsets -8/-7/-6 GcHeader, +0/+4/+8/ + // +12/+16 ObjectHeader; BLOCKING 0x1907 incl. typed-intact). + ctx.current_block = guard_idx; + let gc_type_addr = ctx.block().sub(I64, &t_handle, "8"); + let gc_type_ptr = ctx.block().inttoptr(I64, &gc_type_addr); + let gc_type = ctx.block().load(I8, &gc_type_ptr); + let gc_object = ctx.block().icmp_eq(I8, &gc_type, "2"); + let gc_flags_addr = ctx.block().sub(I64, &t_handle, "7"); + let gc_flags_ptr = ctx.block().inttoptr(I64, &gc_flags_addr); + let gc_flags = ctx.block().load(I8, &gc_flags_ptr); + let forwarded = ctx.block().and(I8, &gc_flags, "128"); + let not_forwarded = ctx.block().icmp_eq(I8, &forwarded, "0"); + let reserved_addr = ctx.block().sub(I64, &t_handle, "6"); + let reserved_ptr = ctx.block().inttoptr(I64, &reserved_addr); + let reserved = ctx.block().load(I16, &reserved_ptr); + let blocked = ctx.block().and(I16, &reserved, "6407"); // 0x1907 + let flags_clear = ctx.block().icmp_eq(I16, &blocked, "0"); + let object_type_ptr = ctx.block().inttoptr(I64, &t_handle); + let object_type = ctx.block().load(I32, &object_type_ptr); + let regular = ctx.block().icmp_eq(I32, &object_type, "1"); + let class_addr = ctx.block().add(I64, &t_handle, "4"); + let class_ptr = ctx.block().inttoptr(I64, &class_addr); + let class_id = ctx.block().load(I32, &class_ptr); + let class_nonzero = ctx.block().icmp_ne(I32, &class_id, "0"); + let not_native_module = ctx.block().icmp_ne(I32, &class_id, "-2"); + let keys_addr = ctx.block().add(I64, &t_handle, "16"); + let keys_ptr = ctx.block().inttoptr(I64, &keys_addr); + let keys = ctx.block().load(I64, &keys_ptr); + let parent_class_addr = ctx.block().add(I64, &t_handle, "8"); + let parent_class_ptr = ctx.block().inttoptr(I64, &parent_class_addr); + let parent_class_id = ctx.block().load(I32, &parent_class_ptr); + let shape_id_rel = ctx.block().add(I32, &parent_class_id, "-2147483648"); + let has_shape_id = ctx.block().icmp_ult(I32, &shape_id_rel, "1073741824"); + let shape_id64 = ctx.block().zext(I32, &parent_class_id, I64); + let shape_id_token = ctx.block().or(I64, &shape_id64, "4611686018427387904"); + let shape_token = + ctx.block() + .select(I1, &has_shape_id, I64, &shape_id_token, &keys); + let cached_token_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]); + let cached_token = ctx.block().load(I64, &cached_token_ptr); + let token_match = ctx.block().icmp_eq(I64, &shape_token, &cached_token); + let token_nonzero = ctx.block().icmp_ne(I64, &shape_token, "0"); + let mut ok = ctx.block().and(I1, &gc_object, ¬_forwarded); + ok = ctx.block().and(I1, &ok, &flags_clear); + ok = ctx.block().and(I1, &ok, ®ular); + ok = ctx.block().and(I1, &ok, &class_nonzero); + ok = ctx.block().and(I1, &ok, ¬_native_module); + ok = ctx.block().and(I1, &ok, &token_match); + ok = ctx.block().and(I1, &ok, &token_nonzero); + ctx.block().cond_br(&ok, &ways_label, &slow_label); + + // 3-way key compare on the NaN-boxed key bits. + ctx.current_block = ways_idx; + let k0_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]); + let k0 = ctx.block().load(I64, &k0_ptr); + let s0_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "2")]); + let s0 = ctx.block().load(I64, &s0_ptr); + let hit0 = ctx.block().icmp_eq(I64, &k_bits, &k0); + ctx.block().cond_br(&hit0, &bounds_label, &way1_label); + ctx.current_block = way1_idx; + let k1_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "3")]); + let k1 = ctx.block().load(I64, &k1_ptr); + let s1_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "4")]); + let s1 = ctx.block().load(I64, &s1_ptr); + let hit1 = ctx.block().icmp_eq(I64, &k_bits, &k1); + ctx.block().cond_br(&hit1, &bounds_label, &way2_label); + ctx.current_block = way2_idx; + let k2_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "5")]); + let k2 = ctx.block().load(I64, &k2_ptr); + let s2_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "6")]); + let s2 = ctx.block().load(I64, &s2_ptr); + let hit2 = ctx.block().icmp_eq(I64, &k_bits, &k2); + ctx.block().cond_br(&hit2, &bounds_label, &slow_label); + + // Bounds: slot < max(field_count, floor 4), slot phi'd by way. + ctx.current_block = bounds_idx; + let slot = ctx.block().phi( + I64, + &[(&s0, &ways_label), (&s1, &way1_label), (&s2, &way2_label)], + ); + let field_count_addr = ctx.block().add(I64, &t_handle, "12"); + let field_count_ptr = ctx.block().inttoptr(I64, &field_count_addr); + let field_count = ctx.block().load(I32, &field_count_ptr); + let field_count64 = ctx.block().zext(I32, &field_count, I64); + let below_floor = ctx.block().icmp_ult(I64, &field_count64, "4"); + let inline_limit = ctx + .block() + .select(I1, &below_floor, I64, "4", &field_count64); + let slot_in_bounds = ctx.block().icmp_ult(I64, &slot, &inline_limit); + ctx.block() + .cond_br(&slot_in_bounds, &store_label, &slow_label); + + // Numeric raw store: no barrier, no layout note (numeric bits + // never create references; a stale pointer bit in the mask is + // a conservative visit, never a missed one). + ctx.current_block = store_idx; + // Field slots start at the header's end on every target + // (24-byte ILP32 and 32-byte LP64 are both 8-byte multiples). + let header_words = + (crate::target_layout::object_header_size_bytes(ctx.target_triple) / 8) + .to_string(); + let slot_word = ctx.block().add(I64, &slot, &header_words); + let obj_ptr = ctx.block().inttoptr(I64, &t_handle); + let slot_ptr = ctx + .block() + .gep_inbounds(I64, &obj_ptr, &[(I64, &slot_word)]); + ctx.block().store(DOUBLE, &v, &slot_ptr); + ctx.block().br(&merge_label); + + ctx.current_block = slow_idx; + let slow_result = ctx.block().call( DOUBLE, "js_put_value_set_dyn_ic", &[ @@ -1072,7 +1228,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (DOUBLE, &v), (I32, strict_i32), ], - )); + ); + ctx.block().br(&merge_label); + + ctx.current_block = merge_idx; + let result = ctx + .block() + .phi(DOUBLE, &[(&v, &store_label), (&slow_result, &slow_label)]); + return Ok(result); } let r = lower_expr(ctx, receiver)?; Ok(ctx.block().call( From 273b146a93e1edf93fa9c6a887102d6b6ab2f70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 27 Jul 2026 05:46:40 +0200 Subject: [PATCH 3/5] =?UTF-8?q?perf(codegen):=20#6812=20w12=20v2b=20?= =?UTF-8?q?=E2=80=94=20k=E2=86=92v=E2=86=92t=20evaluation=20order=20remove?= =?UTF-8?q?s=20the=20value=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline hit previously required a compile-proven-numeric value, which Any-typed counter arithmetic ('r + i') fails — the whole w12 shape stayed outlined. Reorder evaluation to key → value → target (the target is a pure local read, so hoisting is unobservable): a GC during key/value evaluation happens before the target pointer is materialized; a moved key merely misses by stale bits. The store re-checks the VALUE's tag at runtime and routes reference-creating values (pointer/string/bigint) to the outlined helper, so the raw store stays barrier-free with no compile-time gate. Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG --- .../perry-codegen/src/expr/proxy_reflect.rs | 378 ++++++++++-------- 1 file changed, 204 insertions(+), 174 deletions(-) diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index 3559c69cc2..21d70e7601 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -590,6 +590,187 @@ fn lower_put_value_static_write_ic( Ok(Some(result)) } +/// #6812 (w12): inline hit path for the 3-way dynamic-key write IC. +/// Registers arrive in k → v → t evaluation order (see the call site); from +/// the target register onward the path is call-free until the store or the +/// outlined slow call. Guards are byte-for-byte the static write PIC's +/// (GcHeader -8/-7/-6 with BLOCKING 0x1907 incl. typed-intact, ObjectHeader +/// regular/class/token via the #6804 discriminated shape-token select). +/// The raw store fires only for non-reference VALUE tags (not pointer/ +/// string/bigint), so it needs no barrier and no layout note; every other +/// case — and every miss — takes the outlined helper, which bottoms out at +/// full `[[Set]]` + re-prime. +fn lower_put_value_dyn_ic_inline( + ctx: &mut FnCtx<'_>, + t: &str, + k: &str, + v: &str, + strict_i32: &str, +) -> Result { + let site_id = ctx.ic_site_counter; + ctx.ic_site_counter += 1; + let cache_name = format!("perry_ic_{}", site_id); + ctx.ic_globals.push(cache_name.clone()); + let cache_ref = format!("@{}", cache_name); + + let k_bits = ctx.block().bitcast_double_to_i64(k); + let v_bits = ctx.block().bitcast_double_to_i64(v); + let t_bits = ctx.block().bitcast_double_to_i64(t); + let t_handle = ctx.block().and(I64, &t_bits, POINTER_MASK_I64); + let t_tag = ctx.block().lshr(I64, &t_bits, "48"); + let is_ptr = ctx.block().icmp_eq(I64, &t_tag, "32765"); + let above = ctx.block().icmp_ugt(I64, &t_handle, "1048575"); + // Value tag: reference-creating stores (pointer 0x7FFD, string 0x7FFF, + // bigint 0x7FFA) leave the inline path before any store. + let v_tag = ctx.block().lshr(I64, &v_bits, "48"); + let v_not_obj = ctx.block().icmp_ne(I64, &v_tag, "32765"); + let v_not_str = ctx.block().icmp_ne(I64, &v_tag, "32767"); + let v_not_big = ctx.block().icmp_ne(I64, &v_tag, "32762"); + let mut entry_ok = ctx.block().and(I1, &is_ptr, &above); + entry_ok = ctx.block().and(I1, &entry_ok, &v_not_obj); + entry_ok = ctx.block().and(I1, &entry_ok, &v_not_str); + entry_ok = ctx.block().and(I1, &entry_ok, &v_not_big); + + let guard_idx = ctx.new_block("put.dynic.guard"); + let ways_idx = ctx.new_block("put.dynic.ways"); + let way1_idx = ctx.new_block("put.dynic.way1"); + let way2_idx = ctx.new_block("put.dynic.way2"); + let bounds_idx = ctx.new_block("put.dynic.bounds"); + let store_idx = ctx.new_block("put.dynic.store"); + let slow_idx = ctx.new_block("put.dynic.slow"); + let merge_idx = ctx.new_block("put.dynic.merge"); + let guard_label = ctx.block_label(guard_idx); + let ways_label = ctx.block_label(ways_idx); + let way1_label = ctx.block_label(way1_idx); + let way2_label = ctx.block_label(way2_idx); + let bounds_label = ctx.block_label(bounds_idx); + let store_label = ctx.block_label(store_idx); + let slow_label = ctx.block_label(slow_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&entry_ok, &guard_label, &slow_label); + + ctx.current_block = guard_idx; + let gc_type_addr = ctx.block().sub(I64, &t_handle, "8"); + let gc_type_ptr = ctx.block().inttoptr(I64, &gc_type_addr); + let gc_type = ctx.block().load(I8, &gc_type_ptr); + let gc_object = ctx.block().icmp_eq(I8, &gc_type, "2"); + let gc_flags_addr = ctx.block().sub(I64, &t_handle, "7"); + let gc_flags_ptr = ctx.block().inttoptr(I64, &gc_flags_addr); + let gc_flags = ctx.block().load(I8, &gc_flags_ptr); + let forwarded = ctx.block().and(I8, &gc_flags, "128"); + let not_forwarded = ctx.block().icmp_eq(I8, &forwarded, "0"); + let reserved_addr = ctx.block().sub(I64, &t_handle, "6"); + let reserved_ptr = ctx.block().inttoptr(I64, &reserved_addr); + let reserved = ctx.block().load(I16, &reserved_ptr); + let blocked = ctx.block().and(I16, &reserved, "6407"); // 0x1907 + let flags_clear = ctx.block().icmp_eq(I16, &blocked, "0"); + let object_type_ptr = ctx.block().inttoptr(I64, &t_handle); + let object_type = ctx.block().load(I32, &object_type_ptr); + let regular = ctx.block().icmp_eq(I32, &object_type, "1"); + let class_addr = ctx.block().add(I64, &t_handle, "4"); + let class_ptr = ctx.block().inttoptr(I64, &class_addr); + let class_id = ctx.block().load(I32, &class_ptr); + let class_nonzero = ctx.block().icmp_ne(I32, &class_id, "0"); + let not_native_module = ctx.block().icmp_ne(I32, &class_id, "-2"); + let keys_addr = ctx.block().add(I64, &t_handle, "16"); + let keys_ptr = ctx.block().inttoptr(I64, &keys_addr); + let keys = ctx.block().load(I64, &keys_ptr); + let parent_class_addr = ctx.block().add(I64, &t_handle, "8"); + let parent_class_ptr = ctx.block().inttoptr(I64, &parent_class_addr); + let parent_class_id = ctx.block().load(I32, &parent_class_ptr); + let shape_id_rel = ctx.block().add(I32, &parent_class_id, "-2147483648"); + let has_shape_id = ctx.block().icmp_ult(I32, &shape_id_rel, "1073741824"); + let shape_id64 = ctx.block().zext(I32, &parent_class_id, I64); + let shape_id_token = ctx.block().or(I64, &shape_id64, "4611686018427387904"); + let shape_token = ctx + .block() + .select(I1, &has_shape_id, I64, &shape_id_token, &keys); + let cached_token_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]); + let cached_token = ctx.block().load(I64, &cached_token_ptr); + let token_match = ctx.block().icmp_eq(I64, &shape_token, &cached_token); + let token_nonzero = ctx.block().icmp_ne(I64, &shape_token, "0"); + let mut ok = ctx.block().and(I1, &gc_object, ¬_forwarded); + ok = ctx.block().and(I1, &ok, &flags_clear); + ok = ctx.block().and(I1, &ok, ®ular); + ok = ctx.block().and(I1, &ok, &class_nonzero); + ok = ctx.block().and(I1, &ok, ¬_native_module); + ok = ctx.block().and(I1, &ok, &token_match); + ok = ctx.block().and(I1, &ok, &token_nonzero); + ctx.block().cond_br(&ok, &ways_label, &slow_label); + + ctx.current_block = ways_idx; + let k0_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]); + let k0 = ctx.block().load(I64, &k0_ptr); + let s0_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "2")]); + let s0 = ctx.block().load(I64, &s0_ptr); + let hit0 = ctx.block().icmp_eq(I64, &k_bits, &k0); + ctx.block().cond_br(&hit0, &bounds_label, &way1_label); + ctx.current_block = way1_idx; + let k1_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "3")]); + let k1 = ctx.block().load(I64, &k1_ptr); + let s1_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "4")]); + let s1 = ctx.block().load(I64, &s1_ptr); + let hit1 = ctx.block().icmp_eq(I64, &k_bits, &k1); + ctx.block().cond_br(&hit1, &bounds_label, &way2_label); + ctx.current_block = way2_idx; + let k2_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "5")]); + let k2 = ctx.block().load(I64, &k2_ptr); + let s2_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "6")]); + let s2 = ctx.block().load(I64, &s2_ptr); + let hit2 = ctx.block().icmp_eq(I64, &k_bits, &k2); + ctx.block().cond_br(&hit2, &bounds_label, &slow_label); + + ctx.current_block = bounds_idx; + let slot = ctx.block().phi( + I64, + &[(&s0, &ways_label), (&s1, &way1_label), (&s2, &way2_label)], + ); + let field_count_addr = ctx.block().add(I64, &t_handle, "12"); + let field_count_ptr = ctx.block().inttoptr(I64, &field_count_addr); + let field_count = ctx.block().load(I32, &field_count_ptr); + let field_count64 = ctx.block().zext(I32, &field_count, I64); + let below_floor = ctx.block().icmp_ult(I64, &field_count64, "4"); + let inline_limit = ctx + .block() + .select(I1, &below_floor, I64, "4", &field_count64); + let slot_in_bounds = ctx.block().icmp_ult(I64, &slot, &inline_limit); + ctx.block() + .cond_br(&slot_in_bounds, &store_label, &slow_label); + + ctx.current_block = store_idx; + let header_words = + (crate::target_layout::object_header_size_bytes(ctx.target_triple) / 8).to_string(); + let slot_word = ctx.block().add(I64, &slot, &header_words); + let obj_ptr = ctx.block().inttoptr(I64, &t_handle); + let slot_ptr = ctx + .block() + .gep_inbounds(I64, &obj_ptr, &[(I64, &slot_word)]); + // GC_STORE_AUDIT(POINTER_FREE): the entry tag test proved the value is + // not pointer/string/bigint — non-reference bits need no barrier. + ctx.block().store(DOUBLE, v, &slot_ptr); + ctx.block().br(&merge_label); + + ctx.current_block = slow_idx; + let slow_result = ctx.block().call( + DOUBLE, + "js_put_value_set_dyn_ic", + &[ + (crate::types::PTR, &cache_ref), + (DOUBLE, t), + (DOUBLE, k), + (DOUBLE, v), + (I32, strict_i32), + ], + ); + ctx.block().br(&merge_label); + + ctx.current_block = merge_idx; + let result = ctx + .block() + .phi(DOUBLE, &[(v, &store_label), (&slow_result, &slow_label)]); + Ok(result) +} + fn static_write_key(ctx: &FnCtx<'_>, key: &Expr) -> Option { match key { Expr::String(property) => Some(property.clone()), @@ -1044,181 +1225,37 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { downgrade_unknown_call_expr(ctx, key); downgrade_unknown_call_expr(ctx, value); downgrade_unknown_call_expr(ctx, receiver); + let strict_i32 = if *strict { "1" } else { "0" }; + // #6812 (w12) inline path: evaluation order k → v → t. The + // target is a pure local read, so hoisting key/value evaluation + // above its REGISTER materialization is unobservable — and it + // makes the path GC-clean with NO compile-time value gate: a GC + // during key/value evaluation happens before the target pointer + // exists; a moved key merely misses by stale bits (identity + // compare — false negatives only); the store re-checks the + // VALUE's tag at runtime and routes reference-creating values + // (pointer/string/bigint) to the outlined path. + let dyn_inline = same_put_value_receiver_expr(target, receiver) + && matches!(target.as_ref(), Expr::LocalGet(_) | Expr::This); + if dyn_inline { + let k = lower_expr(ctx, key)?; + let v = lower_expr(ctx, value)?; + let t = lower_expr(ctx, target)?; + return lower_put_value_dyn_ic_inline(ctx, &t, &k, &v, strict_i32); + } let t = lower_expr(ctx, target)?; let k = lower_expr(ctx, key)?; let v = lower_expr(ctx, value)?; - let strict_i32 = if *strict { "1" } else { "0" }; - // #6812 (w12): same-receiver dynamic-key stores route through the - // 3-way dynamic-key IC — per-site cache in the same `perry_ic_N` - // global family as the static write PIC, layout - // `[token, k0, s0, k1, s1, k2, s2, _]`. When the receiver is a - // plain local and the VALUE is compile-proven numeric, the hit - // path is emitted INLINE (guards copied from the static PIC; - // a numeric raw store needs no barrier or layout note — numeric - // bits never create references, and hardware NaNs sit below the - // 0x7FFA tag space). Every miss — and every site that fails the - // inline gate — takes the outlined helper, which is itself at - // worst the generic path. + // #6812 (w12): same-receiver dynamic-key stores that failed the + // inline gate (computed target expressions) still take the + // outlined 3-way IC helper. if same_put_value_receiver_expr(target, receiver) { let site_id = ctx.ic_site_counter; ctx.ic_site_counter += 1; let cache_name = format!("perry_ic_{}", site_id); ctx.ic_globals.push(cache_name.clone()); let cache_ref = format!("@{}", cache_name); - let inline_ok = matches!(target.as_ref(), Expr::LocalGet(_) | Expr::This) - && is_numeric_expr(ctx, value); - if !inline_ok { - return Ok(ctx.block().call( - DOUBLE, - "js_put_value_set_dyn_ic", - &[ - (crate::types::PTR, &cache_ref), - (DOUBLE, &t), - (DOUBLE, &k), - (DOUBLE, &v), - (I32, strict_i32), - ], - )); - } - let k_bits = ctx.block().bitcast_double_to_i64(&k); - let t_bits = ctx.block().bitcast_double_to_i64(&t); - let t_handle = ctx.block().and(I64, &t_bits, POINTER_MASK_I64); - let t_tag = ctx.block().lshr(I64, &t_bits, "48"); - let is_ptr = ctx.block().icmp_eq(I64, &t_tag, "32765"); - let above = ctx.block().icmp_ugt(I64, &t_handle, "1048575"); - let heap_candidate = ctx.block().and(I1, &is_ptr, &above); - - let guard_idx = ctx.new_block("put.dynic.guard"); - let ways_idx = ctx.new_block("put.dynic.ways"); - let way1_idx = ctx.new_block("put.dynic.way1"); - let way2_idx = ctx.new_block("put.dynic.way2"); - let bounds_idx = ctx.new_block("put.dynic.bounds"); - let store_idx = ctx.new_block("put.dynic.store"); - let slow_idx = ctx.new_block("put.dynic.slow"); - let merge_idx = ctx.new_block("put.dynic.merge"); - let guard_label = ctx.block_label(guard_idx); - let ways_label = ctx.block_label(ways_idx); - let way1_label = ctx.block_label(way1_idx); - let way2_label = ctx.block_label(way2_idx); - let bounds_label = ctx.block_label(bounds_idx); - let store_label = ctx.block_label(store_idx); - let slow_label = ctx.block_label(slow_idx); - let merge_label = ctx.block_label(merge_idx); - ctx.block() - .cond_br(&heap_candidate, &guard_label, &slow_label); - - // Header + object guards: byte-for-byte the static write - // PIC's emitted checks (offsets -8/-7/-6 GcHeader, +0/+4/+8/ - // +12/+16 ObjectHeader; BLOCKING 0x1907 incl. typed-intact). - ctx.current_block = guard_idx; - let gc_type_addr = ctx.block().sub(I64, &t_handle, "8"); - let gc_type_ptr = ctx.block().inttoptr(I64, &gc_type_addr); - let gc_type = ctx.block().load(I8, &gc_type_ptr); - let gc_object = ctx.block().icmp_eq(I8, &gc_type, "2"); - let gc_flags_addr = ctx.block().sub(I64, &t_handle, "7"); - let gc_flags_ptr = ctx.block().inttoptr(I64, &gc_flags_addr); - let gc_flags = ctx.block().load(I8, &gc_flags_ptr); - let forwarded = ctx.block().and(I8, &gc_flags, "128"); - let not_forwarded = ctx.block().icmp_eq(I8, &forwarded, "0"); - let reserved_addr = ctx.block().sub(I64, &t_handle, "6"); - let reserved_ptr = ctx.block().inttoptr(I64, &reserved_addr); - let reserved = ctx.block().load(I16, &reserved_ptr); - let blocked = ctx.block().and(I16, &reserved, "6407"); // 0x1907 - let flags_clear = ctx.block().icmp_eq(I16, &blocked, "0"); - let object_type_ptr = ctx.block().inttoptr(I64, &t_handle); - let object_type = ctx.block().load(I32, &object_type_ptr); - let regular = ctx.block().icmp_eq(I32, &object_type, "1"); - let class_addr = ctx.block().add(I64, &t_handle, "4"); - let class_ptr = ctx.block().inttoptr(I64, &class_addr); - let class_id = ctx.block().load(I32, &class_ptr); - let class_nonzero = ctx.block().icmp_ne(I32, &class_id, "0"); - let not_native_module = ctx.block().icmp_ne(I32, &class_id, "-2"); - let keys_addr = ctx.block().add(I64, &t_handle, "16"); - let keys_ptr = ctx.block().inttoptr(I64, &keys_addr); - let keys = ctx.block().load(I64, &keys_ptr); - let parent_class_addr = ctx.block().add(I64, &t_handle, "8"); - let parent_class_ptr = ctx.block().inttoptr(I64, &parent_class_addr); - let parent_class_id = ctx.block().load(I32, &parent_class_ptr); - let shape_id_rel = ctx.block().add(I32, &parent_class_id, "-2147483648"); - let has_shape_id = ctx.block().icmp_ult(I32, &shape_id_rel, "1073741824"); - let shape_id64 = ctx.block().zext(I32, &parent_class_id, I64); - let shape_id_token = ctx.block().or(I64, &shape_id64, "4611686018427387904"); - let shape_token = - ctx.block() - .select(I1, &has_shape_id, I64, &shape_id_token, &keys); - let cached_token_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]); - let cached_token = ctx.block().load(I64, &cached_token_ptr); - let token_match = ctx.block().icmp_eq(I64, &shape_token, &cached_token); - let token_nonzero = ctx.block().icmp_ne(I64, &shape_token, "0"); - let mut ok = ctx.block().and(I1, &gc_object, ¬_forwarded); - ok = ctx.block().and(I1, &ok, &flags_clear); - ok = ctx.block().and(I1, &ok, ®ular); - ok = ctx.block().and(I1, &ok, &class_nonzero); - ok = ctx.block().and(I1, &ok, ¬_native_module); - ok = ctx.block().and(I1, &ok, &token_match); - ok = ctx.block().and(I1, &ok, &token_nonzero); - ctx.block().cond_br(&ok, &ways_label, &slow_label); - - // 3-way key compare on the NaN-boxed key bits. - ctx.current_block = ways_idx; - let k0_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]); - let k0 = ctx.block().load(I64, &k0_ptr); - let s0_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "2")]); - let s0 = ctx.block().load(I64, &s0_ptr); - let hit0 = ctx.block().icmp_eq(I64, &k_bits, &k0); - ctx.block().cond_br(&hit0, &bounds_label, &way1_label); - ctx.current_block = way1_idx; - let k1_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "3")]); - let k1 = ctx.block().load(I64, &k1_ptr); - let s1_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "4")]); - let s1 = ctx.block().load(I64, &s1_ptr); - let hit1 = ctx.block().icmp_eq(I64, &k_bits, &k1); - ctx.block().cond_br(&hit1, &bounds_label, &way2_label); - ctx.current_block = way2_idx; - let k2_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "5")]); - let k2 = ctx.block().load(I64, &k2_ptr); - let s2_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "6")]); - let s2 = ctx.block().load(I64, &s2_ptr); - let hit2 = ctx.block().icmp_eq(I64, &k_bits, &k2); - ctx.block().cond_br(&hit2, &bounds_label, &slow_label); - - // Bounds: slot < max(field_count, floor 4), slot phi'd by way. - ctx.current_block = bounds_idx; - let slot = ctx.block().phi( - I64, - &[(&s0, &ways_label), (&s1, &way1_label), (&s2, &way2_label)], - ); - let field_count_addr = ctx.block().add(I64, &t_handle, "12"); - let field_count_ptr = ctx.block().inttoptr(I64, &field_count_addr); - let field_count = ctx.block().load(I32, &field_count_ptr); - let field_count64 = ctx.block().zext(I32, &field_count, I64); - let below_floor = ctx.block().icmp_ult(I64, &field_count64, "4"); - let inline_limit = ctx - .block() - .select(I1, &below_floor, I64, "4", &field_count64); - let slot_in_bounds = ctx.block().icmp_ult(I64, &slot, &inline_limit); - ctx.block() - .cond_br(&slot_in_bounds, &store_label, &slow_label); - - // Numeric raw store: no barrier, no layout note (numeric bits - // never create references; a stale pointer bit in the mask is - // a conservative visit, never a missed one). - ctx.current_block = store_idx; - // Field slots start at the header's end on every target - // (24-byte ILP32 and 32-byte LP64 are both 8-byte multiples). - let header_words = - (crate::target_layout::object_header_size_bytes(ctx.target_triple) / 8) - .to_string(); - let slot_word = ctx.block().add(I64, &slot, &header_words); - let obj_ptr = ctx.block().inttoptr(I64, &t_handle); - let slot_ptr = ctx - .block() - .gep_inbounds(I64, &obj_ptr, &[(I64, &slot_word)]); - ctx.block().store(DOUBLE, &v, &slot_ptr); - ctx.block().br(&merge_label); - - ctx.current_block = slow_idx; - let slow_result = ctx.block().call( + return Ok(ctx.block().call( DOUBLE, "js_put_value_set_dyn_ic", &[ @@ -1228,14 +1265,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (DOUBLE, &v), (I32, strict_i32), ], - ); - ctx.block().br(&merge_label); - - ctx.current_block = merge_idx; - let result = ctx - .block() - .phi(DOUBLE, &[(&v, &store_label), (&slow_result, &slow_label)]); - return Ok(result); + )); } let r = lower_expr(ctx, receiver)?; Ok(ctx.block().call( From bedab8f45697c841cc96c60ea54396ea04d57fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 27 Jul 2026 05:52:42 +0200 Subject: [PATCH 4/5] style: cargo fmt drift from main (compile.rs comment reflow) Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG --- crates/perry/src/commands/compile/collect_modules.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index 0c4dd6b2d3..7a6df18792 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -760,8 +760,8 @@ fn collect_module_one( } }; *next_class_id = new_next_class_id; // Update the global class_id counter - // Preserve native result types before async lowering splits awaited values - // across synthetic locals. The later global fixup remains for inlined code. + // Preserve native result types before async lowering splits awaited values + // across synthetic locals. The later global fixup remains for inlined code. perry_hir::fix_local_native_instances(&mut hir_module); // #2309 Stage 2: fold build-time `process.env` branches BEFORE dynamic From 2552588636f63448359a9d42a8a94d8994388b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 27 Jul 2026 08:13:10 +0200 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20CodeRabbit=20round=201=20on=20#6895?= =?UTF-8?q?=20=E2=80=94=20zero-key=20sentinel=20hole;=20shared=20inline-fl?= =?UTF-8?q?oor=20constant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NaN-box bits of the JS number 0 are 0x0 — identical to the empty-way sentinel — so a dynamic numeric-0 key at a primed site could false-hit an unfilled way and store into slot 0. Guard k_bits != 0 in the inline entry condition and the outlined probe, and never prime zero bits (numeric keys cannot prime anyway; the byte resolver accepts only strings). Regression covered by dynkey sanity s11. Bounds selects now share INLINE_SLOT_FLOOR_LIT with a MUST-match comment against the runtime floor. Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG --- .../perry-codegen/src/expr/proxy_reflect.rs | 26 ++++++++++++++----- crates/perry-runtime/src/proxy/put_value.rs | 13 ++++++++-- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index 21d70e7601..7aa7b4ec34 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -376,10 +376,12 @@ fn lower_put_value_static_write_ic( let field_count_ptr = ctx.block().inttoptr(I64, &field_count_addr); let field_count = ctx.block().load(I32, &field_count_ptr); let field_count64 = ctx.block().zext(I32, &field_count, I64); - let below_floor = ctx.block().icmp_ult(I64, &field_count64, "4"); - let inline_limit = ctx + let below_floor = ctx .block() - .select(I1, &below_floor, I64, "4", &field_count64); + .icmp_ult(I64, &field_count64, INLINE_SLOT_FLOOR_LIT); + let inline_limit = + ctx.block() + .select(I1, &below_floor, I64, INLINE_SLOT_FLOOR_LIT, &field_count64); let slot_in_bounds = ctx.block().icmp_ult(I64, &slot, &inline_limit); let mut hit = ctx.block().and(I1, &heap_candidate, &gc_object); @@ -626,10 +628,14 @@ fn lower_put_value_dyn_ic_inline( let v_not_obj = ctx.block().icmp_ne(I64, &v_tag, "32765"); let v_not_str = ctx.block().icmp_ne(I64, &v_tag, "32767"); let v_not_big = ctx.block().icmp_ne(I64, &v_tag, "32762"); + // Zero key bits are the empty-way sentinel (and the JS number 0): + // they must never reach the way compares. + let k_nonzero = ctx.block().icmp_ne(I64, &k_bits, "0"); let mut entry_ok = ctx.block().and(I1, &is_ptr, &above); entry_ok = ctx.block().and(I1, &entry_ok, &v_not_obj); entry_ok = ctx.block().and(I1, &entry_ok, &v_not_str); entry_ok = ctx.block().and(I1, &entry_ok, &v_not_big); + entry_ok = ctx.block().and(I1, &entry_ok, &k_nonzero); let guard_idx = ctx.new_block("put.dynic.guard"); let ways_idx = ctx.new_block("put.dynic.ways"); @@ -729,10 +735,12 @@ fn lower_put_value_dyn_ic_inline( let field_count_ptr = ctx.block().inttoptr(I64, &field_count_addr); let field_count = ctx.block().load(I32, &field_count_ptr); let field_count64 = ctx.block().zext(I32, &field_count, I64); - let below_floor = ctx.block().icmp_ult(I64, &field_count64, "4"); - let inline_limit = ctx + let below_floor = ctx .block() - .select(I1, &below_floor, I64, "4", &field_count64); + .icmp_ult(I64, &field_count64, INLINE_SLOT_FLOOR_LIT); + let inline_limit = + ctx.block() + .select(I1, &below_floor, I64, INLINE_SLOT_FLOOR_LIT, &field_count64); let slot_in_bounds = ctx.block().icmp_ult(I64, &slot, &inline_limit); ctx.block() .cond_br(&slot_in_bounds, &store_label, &slow_label); @@ -771,6 +779,12 @@ fn lower_put_value_dyn_ic_inline( Ok(result) } +/// Inline-slot floor for emitted bounds checks — MUST match +/// perry-runtime `object::INLINE_SLOT_FLOOR` (the runtime pads every object +/// to at least this many physical slots; a codegen value larger than the +/// runtime's would widen inline stores into unallocated memory). +const INLINE_SLOT_FLOOR_LIT: &str = "4"; + fn static_write_key(ctx: &FnCtx<'_>, key: &Expr) -> Option { match key { Expr::String(property) => Some(property.clone()), diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 40375f8972..ec0a3e1b9d 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -413,8 +413,11 @@ pub extern "C" fn js_put_value_set_dyn_ic( let hit = unsafe { let c = &*cache; let token = c[0] as u64; - if token != 0 { - let key_bits = key.to_bits() as i64; + let key_bits = key.to_bits() as i64; + // `0` is the empty-way sentinel — and also the NaN-box bits of + // the JS number 0, so a dynamic numeric-0 key must never reach + // the way compares (it would false-hit an unfilled way). + if token != 0 && key_bits != 0 { let mut found = None; for way in 0..DYN_IC_WAYS { if c[1 + way * 2] == key_bits { @@ -601,6 +604,12 @@ pub extern "C" fn js_put_value_set_dyn_ic_miss( }; let c = &mut *cache; let key_bits = key.to_bits() as i64; + // Preserve the empty-way sentinel invariant: never prime bits 0 + // (only the JS number 0 has them, and numeric keys cannot prime + // anyway — the byte resolver above only accepts strings). + if key_bits == 0 { + return result; + } if c[0] as u64 != shape_token { // New shape at this site: restart the way set. *c = [0; 8];