diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index bf22b72809..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); @@ -590,6 +592,199 @@ 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"); + // 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"); + 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, 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); + + 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) +} + +/// 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()), @@ -1044,15 +1239,49 @@ 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 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 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); + 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..ec0a3e1b9d 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -381,6 +381,258 @@ 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; + 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 { + 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; + // 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]; + } + // 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() { 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