diff --git a/changelog.d/8074-authoritative-shape-descriptor.md b/changelog.d/8074-authoritative-shape-descriptor.md new file mode 100644 index 0000000000..06cea94871 --- /dev/null +++ b/changelog.d/8074-authoritative-shape-descriptor.md @@ -0,0 +1,10 @@ +**`ShapeId` now resolves to exact agent-local object layout facts.** + +Each published shape descriptor records the ordered keys array, logical key +count, and live inline-slot count for the current agent. Object allocation and +mutation publish a complete descriptor before exposing its id, and moving GC +keeps descriptors synchronized with live object keys while reclaiming dead +shape metadata. Shape ids are never reused; exhausted callers continue safely +through the existing unstamped-object path. `ObjectHeader.keys_array` and +`.field_count` remain the source of truth, and the runtime and FFI ABIs are +unchanged. diff --git a/crates/perry-codegen/src/lower_call/new_alloc.rs b/crates/perry-codegen/src/lower_call/new_alloc.rs index c06407176d..ced4293840 100644 --- a/crates/perry-codegen/src/lower_call/new_alloc.rs +++ b/crates/perry-codegen/src/lower_call/new_alloc.rs @@ -125,6 +125,15 @@ fn new_site_is_in_loop(ctx: &FnCtx<'_>) -> bool { ctx.func.alloc_hot } +/// Whether the raw inline allocator can publish the class's pre-minted +/// descriptor without asking the runtime to repair its live-slot facts. +fn inline_shape_descriptor_facts_exact( + canonical_key_count: Option, + allocation_field_count: u32, +) -> bool { + canonical_key_count.is_some_and(|key_count| key_count == allocation_field_count) +} + /// Emit the instance allocation for `new (...)` and return the raw /// object handle (an `i64` user pointer, NOT NaN-boxed). /// @@ -344,13 +353,24 @@ fn emit_instance_alloc_inner( // So the choice is per site, not global: a `new` inside a loop takes // the inline bump (it runs many times, and the size cost is bounded to // loop bodies); everything else keeps the outlined call and - // contributes nothing to binary growth. `PERRY_INLINE_NEW=1` still - // forces the inline form everywhere, for A/B measurement. + // contributes nothing to binary growth. `PERRY_INLINE_NEW=1` forces + // the inline form for A/B measurement only when the exact descriptor + // facts below admit raw inline allocation; missing or mismatched facts + // still use the outlined entry point. // // NOTE the env test is `is_none()`: `PERRY_INLINE_NEW=""` *enables* // the inline path, because an empty string is `Some("")`. let force_inline_new = std::env::var_os("PERRY_INLINE_NEW").is_some(); - if !force_inline_new && !new_site_is_in_loop(ctx) { + // #8067: the raw inline allocator cannot ask the runtime to validate + // descriptor facts after writing the ShapeId. Admit it only when the + // allocation's live-slot bound exactly equals the module-init keys + // count used to mint that id. Width-hinted/mismatched allocations use + // the outlined entry point, which installs an exact local descriptor. + let descriptor_facts_exact = inline_shape_descriptor_facts_exact( + ctx.class_field_counts.get(class_name).copied(), + field_count, + ); + if !descriptor_facts_exact || (!force_inline_new && !new_site_is_in_loop(ctx)) { let keys_slot = if let Some(s) = ctx.class_keys_slots.get(class_name).cloned() { s } else { @@ -569,9 +589,9 @@ fn emit_instance_alloc_inner( // Second 8 bytes: ShapeId (u32, low) | field_count (u32, high). // Rung 0 removed the last inheritance consumer of this word; the - // parent edge was registered during module init. Keep the old - // parent value only if the process-global ShapeId range was - // exhausted and init returned 0, preserving the lazy fallback. + // parent edge was registered during module init. A zero id is the + // recoverable exhaustion path: retain the old parent word and let + // the still-authoritative pointer/count guards handle the object. let oh_addr_2 = blk.gep(I8, &raw, &[(I64, "16")]); let has_shape_id = blk.icmp_ne(I32, &shape_id, "0"); let shape_word = blk.select(I1, &has_shape_id, I32, &shape_id, &parent_cid.to_string()); @@ -681,3 +701,15 @@ fn emit_instance_alloc_inner( ) } } + +#[cfg(test)] +mod tests { + use super::inline_shape_descriptor_facts_exact; + + #[test] + fn raw_inline_shape_stamp_requires_exact_descriptor_facts() { + assert!(inline_shape_descriptor_facts_exact(Some(5), 5)); + assert!(!inline_shape_descriptor_facts_exact(Some(5), 8)); + assert!(!inline_shape_descriptor_facts_exact(None, 5)); + } +} diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index f2e4e52ae1..dfcaf1dad9 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -428,12 +428,16 @@ pub(crate) unsafe fn keys_array_len_capped_to_capacity(arr: *const ArrayHeader) return (*arr).length as usize; } } - let raw = js_array_length(arr) as usize; - if arr.is_null() { - raw - } else { - raw.min((*arr).capacity as usize) - } + // A forwarding stub overwrites the old payload's `(length, capacity)` + // words with the target address. Resolve once, then read BOTH facts from + // the live header; mixing a resolved length with the stale from-space + // capacity can truncate an otherwise exact shape count. + let live = clean_arr_ptr(arr); + if live.is_null() { + return js_array_length(arr) as usize; + } + let raw = js_array_length(live) as usize; + raw.min((*live).capacity as usize) } /// Read slot `index` of a dense internal keys/property array. diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 3cf6200228..aa3cff779e 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -1815,6 +1815,21 @@ pub(crate) fn test_gc_rewrite_slot_count(user_ptr: usize) -> Option { Some(count) } +#[cfg(test)] +pub(crate) fn test_gc_rewrite_slot_addresses(user_ptr: usize) -> Option> { + if user_ptr < GC_HEADER_SIZE + 0x1000 { + return None; + } + let header = unsafe { header_from_user_ptr(user_ptr as *const u8) }; + let mut slots = Vec::new(); + unsafe { + visit_gc_rewrite_slot_descriptors(header, |descriptor| { + descriptor.visit_slots(&mut |slot| slots.push(slot.slot as usize)); + }); + } + Some(slots) +} + #[inline(always)] pub(super) fn record_trace_slot_read() { #[cfg(test)] diff --git a/crates/perry-runtime/src/gc/layout_slot_visit.rs b/crates/perry-runtime/src/gc/layout_slot_visit.rs index adc134a61c..388b802de3 100644 --- a/crates/perry-runtime/src/gc/layout_slot_visit.rs +++ b/crates/perry-runtime/src/gc/layout_slot_visit.rs @@ -15,9 +15,69 @@ pub(super) unsafe fn visit_gc_layout_slot_descriptors( visit: &mut dyn FnMut(GcMutableSlotDescriptor), ) { let mut child_slots = gc_child_slots(header); + // Capture the authoritative pre-visit facts. A copying visit can rewrite + // `keys_array`, and a sibling may already have rewritten the shared + // descriptor, so the descriptor helper accepts exactly the old OR new + // pointer — never an unrelated pointer that merely shares an id. + let object_shape_facts = if (*header).obj_type == GC_TYPE_OBJECT { + let obj = (header as *mut u8).add(GC_HEADER_SIZE) as *mut crate::object::ObjectHeader; + if crate::regex::regex_header_has_magic(obj as *const crate::regex::RegExpHeader) { + None + } else { + let old_keys = (*obj).keys_array; + let live_inline_slot_count = (*obj).field_count; + if old_keys.is_null() { + Some((obj, 0, 0, live_inline_slot_count)) + } else if crate::value::addr_class::try_read_tracked_gc_header(old_keys as usize) + .is_some_and(|keys_header| (*keys_header.as_ptr()).obj_type == GC_TYPE_ARRAY) + { + // A forwarded tracked array still carries GC_TYPE_ARRAY in + // its from-space header. The length helper follows that stub, + // so a sibling whose shared keys edge was already rewritten + // can still validate against the descriptor's new pointer. + Some(( + obj, + old_keys as u64, + crate::array::keys_array_len_capped_to_capacity(old_keys) as u32, + live_inline_slot_count, + )) + } else { + // Do not dereference corrupt/unmapped header words merely + // because their sibling word happens to look like a ShapeId. + // The authoritative header edge below is still enumerated; + // only redundant descriptor synchronization is skipped. + None + } + } + } else { + None + }; if let Some(slot) = child_slots.take_prefix_child_slot() { visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix)); } + // #8067: the header keys slot above is the sole strong edge. Once its + // visitor callback has run, mirror an immediate rewrite into the weak + // descriptor. Never enumerate the HashMap bucket as a GC slot: dirty-page + // work may retain enumerated slot addresses across budgeted resumptions, + // during which descriptor insertion can reallocate the table. A deferred + // visitor leaves old==new here; the metadata forwarding pass repairs it + // after copying. RegExp aliases GC_TYPE_OBJECT with a different native + // header and was excluded while capturing the facts above. + if let Some((obj, old_keys, logical_key_count, live_inline_slot_count)) = object_shape_facts { + let new_keys = (*obj).keys_array as u64; + // Mark, verify, and deferred dirty scans leave the header edge + // unchanged. Only a copying rewrite needs to borrow and update the + // weak descriptor table. + if new_keys != old_keys { + crate::object::shapes::synchronize_live_object_shape_descriptor_after_header_visit( + obj, + old_keys, + new_keys, + logical_key_count, + live_inline_slot_count, + ); + } + } if let Some(slot) = child_slots.take_meta_child_slot() { visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix)); } diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index b7a3bf2e77..38efa8489e 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -830,9 +830,9 @@ pub fn gc_init() { // reflect-metadata store were invisible to GC — values swept/moved under // live references, owner keys stale after evacuation. reg_scanner!(crate::object::descriptor_state::scan_descriptor_roots_mut); - // #6759 Phase C3a: shape records follow their keys array across - // evacuation (metadata-rewrite rekey only; the records hold no heap - // references and mark nothing). + // #8067: the descriptor table is weak. Live-object layout scans trace its + // ordered-keys slot; this scanner only follows existing forwarding records + // for descriptors and the pointer-keyed slot accelerator after evacuation. reg_scanner!(crate::object::shapes::scan_shape_table_rekey_mut); reg_scanner!(crate::proxy::scan_proxy_roots_mut); // Object/string-valued `err. = v` user props live as raw bits in diff --git a/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs b/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs index 0edf805647..bdee7964f8 100644 --- a/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs +++ b/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs @@ -576,6 +576,7 @@ fn test_object_dead_payload_arm_clears_keys_index() { "a live keys_array's shape record must survive the prune" ); crate::object::shapes::shape_drop(live_keys as *const crate::array::ArrayHeader); + crate::object::shapes::test_drop_shape_descriptors(live_keys); } #[test] @@ -903,9 +904,10 @@ fn test_descriptor_meta_summary_survives_copied_minor_move() { js_shadow_slot_set(0, 0); } -/// #6759 Phase C3a: an owned keys array's grow-realloc migrates the shape -/// record (slot map + stable shape_id) to the new address instead of -/// orphaning it. +/// #8067: an owned keys array's append-reallocation may migrate its validated +/// slot-index accelerator, but must neither repoint nor eagerly delete the old +/// immutable descriptor. A sibling naming it remains valid; otherwise weak +/// post-trace pruning eventually retires it. #[test] fn test_shape_record_migrates_on_owned_grow() { let _global = global_side_table_test_lock(); @@ -924,11 +926,19 @@ fn test_shape_record_migrates_on_owned_grow() { ); assert_eq!( crate::object::shapes::test_shape_id_for_keys(new_addr), - Some(id), - "the record — including its stable shape_id — must move to the new address" + None, + "an append-reallocation must not repoint the old immutable descriptor" + ); + assert_eq!( + crate::object::shapes::shape_descriptor_by_id(id) + .expect("grow must not delete a potentially sibling-owned descriptor") + .keys, + old_addr as u64, + "an append-reallocation repointed the old immutable descriptor" ); // Cleanup so the seeded address can't leak into later tests. crate::object::shapes::shape_drop(new_addr as *const crate::array::ArrayHeader); + crate::object::shapes::test_drop_shape_descriptors(old_addr); } /// #6759 Phase C3a: GC evacuation MOVES a live keys array — the shape @@ -963,7 +973,260 @@ fn test_shape_record_rekeys_on_copied_minor_move() { ); crate::object::shapes::shape_drop(new_addr as *const crate::array::ArrayHeader); + crate::object::shapes::test_drop_shape_descriptors(new_addr); + js_shadow_slot_set(0, 0); +} + +/// #8067: the by-id table is weak. Churning shapes without any live object +/// owners must return the descriptor census to baseline after a full trace; +/// otherwise the descriptor's keys copy would make historical arrays immortal. +#[test] +fn test_dead_shape_descriptor_churn_returns_to_baseline_after_full_gc() { + let _guard = GcTestIsolationGuard::new(); + crate::object::shapes::test_clear_shape_table(); + let baseline = crate::object::shapes::test_shape_descriptor_count(); + let mut ids = Vec::new(); + for _ in 0..32 { + let keys = unsafe { alloc_nursery_test_array() }; + ids.push( + crate::object::shapes::shape_descriptor_ensure(keys, 0, 0) + .expect("shape range unexpectedly exhausted"), + ); + } + assert_eq!( + crate::object::shapes::test_shape_descriptor_count(), + baseline + ids.len(), + "test premise: churn must publish one descriptor per distinct keys array" + ); + + full_gc_with_no_block_persistence(); + + assert_eq!( + crate::object::shapes::test_shape_descriptor_count(), + baseline, + "weak descriptor table retained dead shape keys" + ); + assert!( + ids.into_iter() + .all(|id| crate::object::shapes::shape_descriptor_by_id(id).is_none()), + "a dead shape id still resolved after its keys array was reclaimed" + ); +} + +/// #8067: the header is the sole strong edge; a live stamped object's scan +/// synchronizes its weak descriptor mirror. Two siblings share one descriptor; +/// after copied-minor evacuation both headers and that descriptor must agree. +#[test] +fn test_shared_live_shape_descriptor_survives_and_rekeys_once() { + let _guard = CopyingNurseryTestGuard::new(2); + crate::object::shapes::test_clear_shape_table(); + gc_register_mutable_root_scanner(crate::object::shapes::scan_shape_table_rekey_mut); + + let keys = unsafe { alloc_nursery_test_array() }; + let old_keys = keys as usize; + let id = crate::object::shapes::shape_descriptor_ensure(keys, 0, 0) + .expect("shape range unexpectedly exhausted"); + let (a, _) = unsafe { alloc_nursery_test_object(0) }; + let (b, _) = unsafe { alloc_nursery_test_object(0) }; + unsafe { + (*a).keys_array = keys; + (*a).parent_class_id = id; + (*b).keys_array = keys; + (*b).parent_class_id = id; + } + assert_eq!( + crate::gc::test_gc_rewrite_slot_count(a as usize), + Some(1), + "a stamped object must enumerate only its authoritative header keys slot" + ); + js_shadow_slot_set(0, ptr_bits(a as usize)); + js_shadow_slot_set(1, ptr_bits(b as usize)); + + let _ = gc_collect_minor(); + + let a_after = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::object::ObjectHeader; + let b_after = (js_shadow_slot_get(1) & POINTER_MASK) as *mut crate::object::ObjectHeader; + let descriptor = crate::object::shapes::shape_descriptor_by_id(id) + .expect("live shared descriptor disappeared during evacuation"); + unsafe { + assert_eq!((*a_after).parent_class_id, id); + assert_eq!((*b_after).parent_class_id, id); + assert_eq!((*a_after).keys_array, (*b_after).keys_array); + assert_ne!((*a_after).keys_array as usize, old_keys); + assert_eq!(descriptor.keys, (*a_after).keys_array as u64); + } + assert_eq!( + crate::object::shapes::test_shape_descriptor_count(), + 1, + "two live siblings must retain exactly their one shared descriptor" + ); + crate::object::shapes::shape_drop(descriptor.keys as usize as *const crate::array::ArrayHeader); + crate::object::shapes::test_drop_shape_descriptors(descriptor.keys as usize); + js_shadow_slot_set(0, 0); + js_shadow_slot_set(1, 0); +} + +/// #8074 review: a forwarded array's from-space payload contains its forwarding +/// address, not a usable `(length, capacity)` pair. Descriptor fact capture +/// must resolve both values from the live array or the stale capacity word can +/// truncate the logical count and make an immediate header rewrite fail closed. +#[test] +fn test_forwarded_keys_capacity_preserves_immediate_descriptor_sync() { + let _guard = GcTestIsolationGuard::new(); + crate::object::shapes::test_clear_shape_table(); + + let old_keys = unsafe { alloc_nursery_test_array() }; + let live_keys = unsafe { alloc_nursery_test_array() }; + // `set_forwarding_address` stores this pointer over the old length/capacity + // pair. Pick a logical count one above the resulting stale capacity word, + // so the pre-fix mixed old/new read deterministically truncates it. + let stale_capacity = ((live_keys as u64) >> 32) as u32; + let logical_key_count = stale_capacity + .checked_add(1) + .expect("tracked array address unexpectedly fills the high u32"); + unsafe { + (*old_keys).length = logical_key_count; + (*old_keys).capacity = logical_key_count; + (*live_keys).length = logical_key_count; + (*live_keys).capacity = logical_key_count; + } + let id = crate::object::shapes::shape_descriptor_ensure(old_keys, logical_key_count, 0) + .expect("shape range unexpectedly exhausted"); + let (owner, _) = unsafe { alloc_nursery_test_object(0) }; + unsafe { + (*owner).keys_array = old_keys; + (*owner).parent_class_id = id; + } + + let old_header = unsafe { header_from_user_ptr(old_keys.cast()) } as *mut GcHeader; + let old_flags = unsafe { (*old_header).gc_flags }; + let old_payload = unsafe { *(old_keys as *const u64) }; + unsafe { + set_forwarding_address(old_header, live_keys.cast()); + assert_eq!((*old_keys).capacity, stale_capacity); + assert!( + (*old_keys).capacity < logical_key_count, + "test premise: the stale capacity must truncate the live count" + ); + } + + let owner_header = unsafe { header_from_user_ptr(owner.cast()) } as *mut GcHeader; + let header_keys_slot = unsafe { std::ptr::addr_of_mut!((*owner).keys_array) as *mut u64 }; + let mut rewritten = 0usize; + unsafe { + visit_gc_layout_slot_descriptors(owner_header, &mut |descriptor| { + descriptor.visit_slots(&mut |slot| { + if slot.slot == header_keys_slot { + *slot.slot = live_keys as u64; + rewritten += 1; + } + }); + }); + } + assert_eq!( + rewritten, 1, + "the authoritative header edge must be rewritten once" + ); + let descriptor = crate::object::shapes::shape_descriptor_by_id(id) + .expect("rewritten live descriptor disappeared"); + assert_eq!(descriptor.keys, live_keys as u64); + assert_eq!(descriptor.logical_key_count, logical_key_count); + assert_eq!(descriptor.live_inline_slot_count, 0); + + unsafe { + *(old_keys as *mut u64) = old_payload; + (*old_header).gc_flags = old_flags; + } + crate::object::shapes::test_clear_shape_table(); +} + +/// #8067 release fail-closed guard: descriptor fact capture must classify the +/// keys word before reading ArrayHeader fields. A live GC_TYPE_OBJECT can +/// carry a corrupt header edge and a real ShapeId at the same time; the +/// authoritative header edge remains visible, but descriptor synchronization +/// must skip without dereferencing that word. +#[test] +fn test_shape_descriptor_skips_a_plausible_misaligned_corrupt_keys_word() { + let _guard = GcTestIsolationGuard::new(); + crate::object::shapes::test_clear_shape_table(); + + let valid_keys = unsafe { alloc_nursery_test_array() }; + let id = crate::object::shapes::shape_descriptor_ensure(valid_keys, 0, 0) + .expect("shape range unexpectedly exhausted"); + let (owner, _) = unsafe { alloc_nursery_test_object(0) }; + let corrupt_keys = 0x2800_0203usize; + assert!( + unsafe { crate::value::addr_class::try_read_tracked_gc_header(corrupt_keys) }.is_none(), + "test premise: the plausible misaligned word is not an exact tracked allocation" + ); + unsafe { + (*owner).keys_array = corrupt_keys as *mut crate::array::ArrayHeader; + (*owner).parent_class_id = id; + } + js_shadow_slot_set(0, ptr_bits(owner as usize)); + + assert_eq!( + crate::gc::test_gc_rewrite_slot_count(owner as usize), + Some(1), + "only the authoritative corrupt header slot may be enumerated" + ); + let descriptor = crate::object::shapes::shape_descriptor_by_id(id) + .expect("invalid header facts must not retire the unrelated descriptor"); + assert_eq!(descriptor.keys, valid_keys as u64); + assert_eq!(descriptor.logical_key_count, 0); + assert_eq!(descriptor.live_inline_slot_count, 0); + + js_shadow_slot_set(0, 0); + crate::object::shapes::test_drop_shape_descriptors(valid_keys as usize); +} + +/// #8067: DirtyHeaderSlotScan retains enumerated raw slot pointers between +/// budgeted work units. Descriptor-table growth in that mutator window must +/// not invalidate any saved pointer, so a stamped object's enumeration may +/// contain its stable ObjectHeader slot but never a HashMap bucket address. +#[test] +fn test_deferred_shape_slot_enumeration_survives_descriptor_table_reallocation() { + let _guard = GcTestIsolationGuard::new(); + crate::object::shapes::test_clear_shape_table(); + let frame = js_shadow_frame_push(1); + + let keys_before = unsafe { alloc_nursery_test_array() }; + js_shadow_slot_set(0, ptr_bits(keys_before as usize)); + let id = crate::object::shapes::shape_descriptor_ensure(keys_before, 0, 0) + .expect("shape range unexpectedly exhausted"); + let (owner, _) = unsafe { alloc_nursery_test_object(0) }; + let keys = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::array::ArrayHeader; + unsafe { + (*owner).keys_array = keys; + (*owner).parent_class_id = id; + } + js_shadow_slot_set(0, ptr_bits(owner as usize)); + let saved_slots = crate::gc::test_gc_rewrite_slot_addresses(owner as usize) + .expect("tracked object must have a rewrite descriptor"); + let header_keys_slot = unsafe { std::ptr::addr_of_mut!((*owner).keys_array) as *mut u64 }; + assert_eq!( + saved_slots, + vec![header_keys_slot as usize], + "deferred work retained a descriptor-table bucket address" + ); + + for i in 0..1024usize { + let fake_keys = 0x8067_1000_0000_0000usize + i * 0x1000; + crate::object::shapes::shape_descriptor_ensure(fake_keys as *const _, 0, 0) + .expect("shape range unexpectedly exhausted during reallocation fixture"); + } + let owner_after = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::object::ObjectHeader; + let slots_after = crate::gc::test_gc_rewrite_slot_addresses(owner_after as usize) + .expect("rooted object must remain enumerable after table growth"); + assert_eq!( + slots_after, + vec![header_keys_slot as usize], + "descriptor-table growth changed the stable authoritative slot address" + ); + js_shadow_slot_set(0, 0); + js_shadow_frame_pop(frame); + crate::object::shapes::test_clear_shape_table(); } /// #6759 Phase B: the meta record is kept alive by its owner (the header diff --git a/crates/perry-runtime/src/object/alloc.rs b/crates/perry-runtime/src/object/alloc.rs index 519767a43e..9045d2228b 100644 --- a/crates/perry-runtime/src/object/alloc.rs +++ b/crates/perry-runtime/src/object/alloc.rs @@ -332,8 +332,8 @@ pub extern "C" fn js_object_alloc_class_inline_keys( /// initialization. Installing it after the existing allocator returns keeps /// every allocation/rooting/layout invariant above in one implementation, /// while making a fresh class instance immediately usable by ShapeId guards. -/// A zero/exhausted id preserves the allocation-time parent word and therefore -/// falls back to the pre-rung-2 lazy-stamping behavior. +/// A zero/exhausted id preserves the allocation-time parent word; the retained +/// pointer/count guards remain the fail-closed source of truth. #[no_mangle] pub extern "C" fn js_object_alloc_class_inline_keys_stamped( class_id: u32, @@ -344,10 +344,8 @@ pub extern "C" fn js_object_alloc_class_inline_keys_stamped( ) -> *mut ObjectHeader { let ptr = object_alloc_class_inline_keys_impl(class_id, parent_class_id, field_count, keys_array); - if crate::object::shapes::is_shape_id(shape_id) { - unsafe { - (*ptr).parent_class_id = shape_id; - } + unsafe { + crate::object::shapes::birth_stamp_object_shape(ptr, shape_id); } ptr } @@ -755,9 +753,7 @@ pub extern "C" fn js_object_alloc_with_shape( // newborn literals carry their stable identity immediately, so // typed_feedback tokens and the id-keyed FIELD_CACHE never see a // pre-stamp window for shape-cached objects. - if runtime_shape_id != 0 { - (*obj_ptr).parent_class_id = runtime_shape_id; - } + crate::object::shapes::birth_stamp_object_shape(obj_ptr, runtime_shape_id); } obj_handle.get_raw_mut_ptr::() diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index d0328606bc..d214f8c88c 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -377,18 +377,19 @@ pub extern "C" fn js_object_delete_field( // After the rebuild above, the survivors occupy slots `0..new_count`, // inline up to the allocation's capacity. That is exactly // `min(new_count, alloc_limit)`. - (*obj).field_count = std::cmp::min(new_count, alloc_limit) as u32; + set_object_live_slot_count(obj, std::cmp::min(new_count, alloc_limit) as u32); - // 4) Drop the (post-compaction) keys array's shape record — slots - // past `i` have shifted, so any map is stale. The shrink check - // in `shape_slot_lookup` would also catch this lazily; dropping - // eagerly keeps the record from serving hash misses meanwhile. + // 4) Drop the (post-compaction) keys array's slot-index accelerator — + // slots past `i` have shifted, so any map is stale. Descriptors are + // not eagerly deleted because a sibling may still name one; exact + // new facts are published below and weak post-trace pruning retires + // dead historical descriptors. crate::object::shapes::shape_drop((*obj).keys_array); // #6759 C3c: the compaction changed the layout under the SAME keys // address, so the stamped shape id no longer describes this - // object. Ids are never reused, so clearing here (plus the - // record drop above) makes every stale id-keyed cache entry a - // permanent miss; the next resolve stamps a fresh id. + // object. Ids are never reused, so clearing here makes every stale + // id-keyed cache entry a permanent miss for this receiver; the + // synchronization below stamps exact post-delete facts. // // #6759 C3 rung 1: this now fires for CLASS INSTANCES too — the // whole point of the rung. A delete on a class instance is exactly @@ -407,6 +408,7 @@ pub extern "C" fn js_object_delete_field( // `shape_slot_lookup`'s shrink check already anticipates), so // deleting it would silently make that path wrong. crate::object::shapes::clear_object_shape_stamp(obj); + crate::object::shapes::synchronize_object_shape_descriptor(obj); 1 } @@ -650,11 +652,10 @@ mod shape_transition_tests_6759 { crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) } - /// A plain object's `delete` DOES mint a new ShapeId — lazily. The record - /// for the compacted keys array is dropped (`shape_drop`) and the stamp - /// cleared, so the next resolve allocates a genuinely fresh id rather than - /// reviving the old one. Ids are never reused, so "different" is the whole - /// property. + /// A plain object's `delete` mints a new authoritative ShapeId eagerly. The + /// stale slot index is dropped, the stamp is cleared, then the compacted + /// facts install a genuinely fresh id rather than reviving the old one. + /// Ids are never reused, so "different" is the whole property. #[test] fn delete_mints_a_fresh_shape_id_for_a_plain_object() { let _lock = crate::gc::global_side_table_test_lock(); @@ -672,14 +673,7 @@ mod shape_transition_tests_6759 { assert_eq!(js_object_delete_field(obj, key("del6759_a")), 1); - // The stamp is cleared eagerly … - assert_eq!( - (*obj).parent_class_id, - 0, - "the stamp still describes the PRE-delete key list" - ); - // … and re-minted, distinctly, on the next resolve. - let _ = crate::object::js_object_get_field_by_name(obj, key("del6759_c")); + // The compacted descriptor is installed before delete returns. let after = (*obj).parent_class_id; assert!( is_shape_id(after), @@ -690,6 +684,11 @@ mod shape_transition_tests_6759 { "the delete re-used the pre-delete ShapeId — a shape-id compare \ would accept a compacted object as its own class's shape" ); + let descriptor = crate::object::shapes::shape_descriptor_by_id(after) + .expect("delete must publish a by-id descriptor"); + assert_eq!(descriptor.keys, (*obj).keys_array as u64); + assert_eq!(descriptor.logical_key_count, 2); + assert_eq!(descriptor.live_inline_slot_count, (*obj).field_count); } } @@ -698,8 +697,8 @@ mod shape_transition_tests_6759 { /// This is the assertion `delete_leaves_a_class_instance_with_no_shape_word_to_transition` /// asked to be replaced by. A class instance now HAS a shape word, so a /// `delete` on it transitions the same way a plain object's does: the - /// compaction clears the stamp, and the next resolve mints a genuinely - /// fresh id (ids are never reused) rather than reviving the class's + /// compaction clears the stamp, then installs a genuinely fresh id before + /// returning (ids are never reused) rather than reviving the class's /// canonical one. /// /// The old test's other half — that `class_id` is preserved and the keys @@ -751,14 +750,7 @@ mod shape_transition_tests_6759 { 30.0, "test premise: the delete did not compact the slots" ); - // The stamp is cleared eagerly … - assert_eq!( - (*obj).parent_class_id, - 0, - "the stamp still describes the PRE-delete key list" - ); - // … and re-minted, distinctly, on the next resolve. - let _ = crate::object::js_object_get_field_by_name(obj, key("del6759_z")); + // The compacted descriptor is installed before delete returns. let after = (*obj).parent_class_id; assert!( is_shape_id(after), @@ -769,6 +761,11 @@ mod shape_transition_tests_6759 { "the delete re-used the pre-delete ShapeId — a shape-id compare \ would accept a compacted class instance as its own class's shape" ); + let descriptor = crate::object::shapes::shape_descriptor_by_id(after) + .expect("class delete must publish a by-id descriptor"); + assert_eq!(descriptor.keys, (*obj).keys_array as u64); + assert_eq!(descriptor.logical_key_count, 2); + assert_eq!(descriptor.live_inline_slot_count, (*obj).field_count); // Still true, and still what the guard compares until rung 3. assert_ne!( diff --git a/crates/perry-runtime/src/object/field_get_set/field_ops.rs b/crates/perry-runtime/src/object/field_get_set/field_ops.rs index b96f03684e..c11fc9a012 100644 --- a/crates/perry-runtime/src/object/field_get_set/field_ops.rs +++ b/crates/perry-runtime/src/object/field_get_set/field_ops.rs @@ -163,7 +163,7 @@ pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, // widening here can only ever expose non-pointer sentinels ahead of // the store that is about to fill this one in. if field_index >= (*obj).field_count { - (*obj).field_count = field_index + 1; + set_object_live_slot_count(obj, field_index + 1); } crate::gc::runtime_store_jsvalue_slot( obj as usize, diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 812798cffe..5d7c263187 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -169,15 +169,15 @@ pub extern "C" fn js_object_set_field_by_name( .add(std::mem::size_of::()) as *mut JSValue; let slot = fields_ptr.add(slot_idx as usize); + if slot_idx >= (*o).field_count { + set_object_live_slot_count(o, slot_idx + 1); + } crate::gc::runtime_store_jsvalue_slot( o as usize, slot as usize, slot_idx as usize, vbits, ); - if slot_idx >= (*o).field_count { - (*o).field_count = slot_idx + 1; - } } else { overflow_set(o as usize, slot_idx as usize, vbits); } diff --git a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs index 7902f6f5f9..15b69d8763 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs @@ -102,10 +102,10 @@ pub(crate) unsafe fn try_existing_own_data_overwrite( let alloc_limit = std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize; if (idx as usize) < alloc_limit { - store_object_field_slot(obj, idx as usize, vbits); if idx >= (*obj).field_count { - (*obj).field_count = idx + 1; + set_object_live_slot_count(obj, idx + 1); } + store_object_field_slot(obj, idx as usize, vbits); } else { overflow_set(obj_addr, idx as usize, vbits); } @@ -271,10 +271,10 @@ pub extern "C" fn js_object_set_field_by_name_transition_fast( }; if slot_usize < alloc_limit { - store_object_field_slot(obj, slot_usize, vbits); if slot_idx >= (*obj).field_count { - (*obj).field_count = slot_idx + 1; + set_object_live_slot_count(obj, slot_idx + 1); } + store_object_field_slot(obj, slot_usize, vbits); } else { overflow_set(obj as usize, slot_usize, vbits); } diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index e12a0adbbd..3a09f15a69 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -476,22 +476,17 @@ pub(super) fn set_field_by_name_object_tail( let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; let slot = fields_ptr.add(slot_idx as usize); + // Publish the expanded traced range and its exact + // descriptor before the pointer-bearing slot value. + if slot_idx >= (*obj).field_count { + set_object_live_slot_count(obj, slot_idx + 1); + } crate::gc::runtime_store_jsvalue_slot( obj as usize, slot as usize, slot_idx as usize, vbits, ); - // Bump field_count only for inline slots — leaving - // it at the physical capacity is what steers - // `js_object_get_field_by_name`'s reads to the - // overflow map for slots ≥ alloc_limit. Bumping it - // past capacity would make reads dereference past - // the object's inline field array into adjacent - // arena data. - if slot_idx >= (*obj).field_count { - (*obj).field_count = slot_idx + 1; - } } else { // Cached slot is past the object's inline capacity — // store in the overflow map (same as the slow path's @@ -538,7 +533,7 @@ pub(super) fn set_field_by_name_object_tail( // can only expose non-pointer sentinels — then publish the value. // Bump field_count so Object.keys()/values()/entries() see the new property. if (*obj).field_count == 0 { - (*obj).field_count = 1; + set_object_live_slot_count(obj, 1); } js_object_set_field(obj, 0, JSValue::from_bits(value.to_bits())); refresh_roots_after_alloc!(); @@ -710,10 +705,10 @@ pub(super) fn set_field_by_name_object_tail( refresh_roots_after_alloc!(); set_object_keys_array(obj, new_keys); super::mark_object_dynamic_shape_unknown(obj); - // #6759 Phase C3a: an owned grow keeps its shape identity — - // migrate the record instead of orphaning it (a shared - // fork must NOT migrate: the old address still describes - // the siblings' live shape). + // #8067: migrate only the owned array's validated slot index; + // the immutable descriptor is versioned to the new facts. A + // shared fork must NOT migrate: the old address still serves + // the siblings' live shape. if !keys_shared { super::shapes::shape_keys_grown(prev_keys_usize, new_keys); } @@ -748,8 +743,8 @@ pub(super) fn set_field_by_name_object_tail( refresh_roots_after_alloc!(); set_object_keys_array(obj, new_keys); super::mark_object_dynamic_shape_unknown(obj); - // #6759 Phase C3a: owned grow keeps its shape identity (see the - // overflow branch above). + // #8067: owned grow keeps the slot index, while the immutable + // descriptor is versioned (see the overflow branch above). if !keys_shared { super::shapes::shape_keys_grown(prev_keys_usize, new_keys); } @@ -760,7 +755,7 @@ pub(super) fn set_field_by_name_object_tail( // slot is undefined-initialized at allocation, so the widened range // can only expose non-pointer sentinels — then publish the value. if new_index as u32 >= (*obj).field_count { - (*obj).field_count = new_index as u32 + 1; + set_object_live_slot_count(obj, new_index as u32 + 1); } js_object_set_field(obj, new_index as u32, JSValue::from_bits(value.to_bits())); refresh_roots_after_alloc!(); @@ -911,7 +906,7 @@ pub(super) fn set_field_by_name_object_tail( refresh_roots_after_alloc!(); set_object_keys_array(obj, new_keys); super::mark_object_dynamic_shape_unknown(obj); - // #6759 Phase C3a: owned grow keeps its shape identity. + // #8067: migrate the owned slot index, not the descriptor id. if !keys_shared { super::shapes::shape_keys_grown(prev_keys_usize, new_keys); } @@ -963,7 +958,7 @@ pub(super) fn set_field_by_name_object_tail( // Update the object's keys_array pointer in case js_array_push reallocated set_object_keys_array(obj, new_keys); super::mark_object_dynamic_shape_unknown(obj); - // #6759 Phase C3a: owned grow keeps its shape identity. + // #8067: migrate the owned slot index, not the descriptor id. if !keys_shared { super::shapes::shape_keys_grown(prev_keys_usize, new_keys); } @@ -977,7 +972,7 @@ pub(super) fn set_field_by_name_object_tail( // can only expose non-pointer sentinels — then publish the value. // Bump field_count to reflect the newly added property if new_index as u32 >= (*obj).field_count { - (*obj).field_count = new_index as u32 + 1; + set_object_live_slot_count(obj, new_index as u32 + 1); } js_object_set_field(obj, new_index as u32, JSValue::from_bits(value.to_bits())); refresh_roots_after_alloc!(); diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index b74e7613c2..0b14d04640 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1681,7 +1681,9 @@ pub struct ObjectHeader { pub object_type: u32, /// Class ID for this object (used for instanceof, vtable lookup) pub class_id: u32, - /// Parent class ID for inheritance chain (0 if no parent) + /// Compatibility word: the parent class ID during allocation, then the + /// runtime `ShapeId` after shape stamping. Parent lookup must use the class + /// registry; direct reads of this word are not authoritative parent data. pub parent_class_id: u32, /// Number of fields in this object pub field_count: u32, @@ -1830,13 +1832,10 @@ pub(crate) unsafe fn gc_object_meta_slot(user_ptr: usize) -> Option<*mut u64> { #[inline] unsafe fn set_object_keys_array(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader) { // #6759 C3c: a stamped shape id (carried in the `parent_class_id` word) - // described the OLD keys array — clear it on a pointer CHANGE so the stamp - // invariant (`stamp != 0 ⟹ stamp == id of current keys`) holds; the resolve - // paths re-stamp on their next successful lookup. A same-pointer update - // (in-place append) keeps the stamp: slots are append-only, existing - // mappings stay valid — and the C3a grow-migration keeps the id itself - // alive across reallocs, so the fresh stamp after a grow resolves to the - // SAME id. + // described the OLD keys array — clear it on a pointer CHANGE so no stale + // id is visible while the authoritative header changes. A same-pointer + // append is versioned by `synchronize_object_shape_descriptor` below; an + // immutable old descriptor is never silently changed in place. // // #6759 C3 rung 1: no `class_id == 0` gate. The word is a ShapeId iff // `is_shape_id` says so, for class instances too — and `clear_object_shape_stamp` @@ -1865,6 +1864,27 @@ unsafe fn set_object_keys_array(obj: *mut ObjectHeader, keys_array: *mut ArrayHe &(*obj).keys_array as *const _ as usize, keys_array as u64, ); + // #8067: the old header edge remains authoritative, but every visible + // ShapeId must now resolve to the exact rooted ordered-keys/live-slot + // descriptor. Same-pointer appends are versioned inside the helper. + shapes::synchronize_object_shape_descriptor(obj); +} + +/// Publish a new authoritative live-inline-slot bound without ever exposing a +/// ShapeId whose descriptor disagrees with `ObjectHeader.field_count`. +/// +/// Callers growing the traced range must invoke this before publishing the +/// pointer-bearing field value (#7154): old stamp clear → header count write → +/// complete descriptor install → new stamp → value-slot store. +#[inline] +pub(super) unsafe fn set_object_live_slot_count(obj: *mut ObjectHeader, field_count: u32) { + if (*obj).field_count != field_count { + shapes::clear_object_shape_stamp(obj); + (*obj).field_count = field_count; + shapes::synchronize_object_shape_descriptor(obj); + } else { + shapes::debug_assert_object_shape_parity(obj); + } } #[inline] diff --git a/crates/perry-runtime/src/object/object_ops/keys_array.rs b/crates/perry-runtime/src/object/object_ops/keys_array.rs index 84a55d9157..1a8c416e07 100644 --- a/crates/perry-runtime/src/object/object_ops/keys_array.rs +++ b/crates/perry-runtime/src/object/object_ops/keys_array.rs @@ -34,7 +34,7 @@ pub(crate) unsafe fn ensure_key_in_keys_array( refresh_define_property_roots!(); set_object_keys_array(obj, new_keys); if (*obj).field_count == 0 { - (*obj).field_count = 1; + set_object_live_slot_count(obj, 1); } return; } @@ -150,7 +150,7 @@ pub(crate) unsafe fn ensure_key_in_keys_array( let inline_capacity = std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32); if new_index < inline_capacity && new_index >= (*obj).field_count { - (*obj).field_count = new_index + 1; + set_object_live_slot_count(obj, new_index + 1); } } @@ -160,6 +160,7 @@ mod tests { #[test] fn define_property_key_growth_does_not_mutate_a_shared_shape_sibling() { + let _lock = crate::gc::global_side_table_test_lock(); unsafe { let packed = b""; let first = @@ -171,13 +172,25 @@ mod tests { // A logical-field/key-count mismatch is not evidence that the // keys array is privately owned. This was the false assumption in // the old clone condition. - (*first).field_count = 1; + set_object_live_slot_count(first, 1); + let sibling_shape = (*sibling).parent_class_id; let key = crate::string::js_string_from_bytes(b"ALIAS_KEYS".as_ptr(), 10); ensure_key_in_keys_array(first, key); assert!(own_key_present(first, key)); assert!(!own_key_present(sibling, key)); assert_ne!((*first).keys_array, (*sibling).keys_array); + assert_ne!((*first).parent_class_id, sibling_shape); + let sibling_descriptor = crate::object::shapes::shape_descriptor_by_id(sibling_shape) + .expect("sibling descriptor must remain installed"); + assert_eq!(sibling_descriptor.keys, (*sibling).keys_array as u64); + assert_eq!(sibling_descriptor.logical_key_count, 0); + let first_descriptor = + crate::object::shapes::shape_descriptor_by_id((*first).parent_class_id) + .expect("defineProperty growth must install an exact descriptor"); + assert_eq!(first_descriptor.keys, (*first).keys_array as u64); + assert_eq!(first_descriptor.logical_key_count, 1); + assert_eq!(first_descriptor.live_inline_slot_count, 1); } } } diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 6a7a368b2f..b9ba97945a 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -1,4 +1,4 @@ -//! #6759 Phase C1: first-class Shape records, keyed on keys_array identity. +//! Agent-local authoritative object-shape descriptors (#8067). //! //! A shared `keys_array` already IS a shape (same pointer ⟹ same ordered //! key list, because mutation always forks a private clone). This module @@ -10,46 +10,111 @@ //! * `WIDE_KEY_INDEX` — keys-keyed but capped at a 4-entry LRU, so any //! working set past 4 wide shapes thrashed. //! -//! Trust model (inherited from both): entries are accelerators, never -//! authoritative. Every hit re-validates the stored key bytes at the -//! returned slot; a recycled keys_array address or an in-place mutation -//! fails validation, drops the entry, and the caller falls back to the -//! linear scan. Staleness is therefore harmless; the dead-owner prune -//! exists for memory, not correctness. See docs/shape-tree-plan.md. +//! The pointer-keyed key→slot index remains an accelerator: every hit still +//! re-validates the key bytes. Separately, every published `ShapeId` resolves +//! in this agent's `RuntimeState` to an immutable descriptor containing the +//! ordered-keys edge plus the exact logical-key and live-inline-slot bounds. +//! The descriptor table is weak: a live object's authoritative header edge +//! keeps keys alive and synchronizes the descriptor mirror, while dead-key +//! entries are pruned after tracing. This avoids +//! turning historical shapes into permanent roots. `ObjectHeader::{keys_array, +//! field_count}` remain authoritative in this first slice; publication helpers +//! assert descriptor parity and every old guard stays in place as redundant +//! evidence. use crate::array::ArrayHeader; use std::cell::RefCell; use std::collections::HashMap; -pub(crate) struct Shape { +pub(crate) struct ShapeIndex { /// Key count covered by `slots`. Longer live array ⟹ catch up /// incrementally (append-only while shared); shorter ⟹ a delete /// compacted it — drop and rebuild on next lookup. indexed_len: u32, - /// #6759 Phase C3a/C3c: stable shape identity, allocated once at - /// record birth and preserved by [`shape_keys_grown`] when an owned - /// keys array reallocates. Stamped into a plain object's - /// `parent_class_id` header word (dead weight for `class_id == 0`) - /// and used as the FIELD_CACHE key, so lookups stop churning on - /// capacity doublings and GC moves. 0 is never allocated ("no id"). - shape_id: u32, /// FNV-1a content hash of key bytes → candidate slots (collisions /// resolved by the per-hit content validation). slots: HashMap>, } +/// Immutable facts named by one ShapeId. The raw keys pointer is a weak mirror +/// of the authoritative `ObjectHeader::keys_array` edge: live-object scans +/// rekey it immediately after visiting that header slot, and the metadata pass +/// repairs deferred forwarding. The table itself never exposes a GC slot. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ShapeDescriptor { + /// Raw ArrayHeader address in Perry's fixed-width heap-word ABI. Keeping + /// this weak mirror u64 preserves identical representation on ILP32/LP64. + pub(crate) keys: u64, + pub(crate) logical_key_count: u32, + pub(crate) live_inline_slot_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct ShapeFacts { + keys: u64, + logical_key_count: u32, + live_inline_slot_count: u32, +} + +struct ShapeTableInner { + indices: crate::fast_hash::PtrHashMap, + descriptors: HashMap, + ids_by_facts: HashMap, + /// Keys-array address -> every descriptor id that currently names it. + /// Same-address key-count retirement uses this index instead of scanning + /// every shape ever observed by the agent. + ids_by_keys: HashMap>, +} + pub(crate) struct ShapeTable { - entries: RefCell>, + inner: RefCell, } impl ShapeTable { pub(crate) fn new() -> Self { ShapeTable { - entries: RefCell::new(crate::fast_hash::new_ptr_hash_map()), + inner: RefCell::new(ShapeTableInner { + indices: crate::fast_hash::new_ptr_hash_map(), + descriptors: HashMap::new(), + ids_by_facts: HashMap::new(), + ids_by_keys: HashMap::new(), + }), } } } +#[inline] +fn descriptor_facts(descriptor: ShapeDescriptor) -> ShapeFacts { + ShapeFacts { + keys: descriptor.keys, + logical_key_count: descriptor.logical_key_count, + live_inline_slot_count: descriptor.live_inline_slot_count, + } +} + +fn remove_id_from_keys_index(inner: &mut ShapeTableInner, keys: u64, id: u32) { + let remove_entry = if let Some(ids) = inner.ids_by_keys.get_mut(&keys) { + ids.retain(|&candidate| candidate != id); + ids.is_empty() + } else { + false + }; + if remove_entry { + inner.ids_by_keys.remove(&keys); + } +} + +fn rebuild_descriptor_reverse_indices(inner: &mut ShapeTableInner) { + let mut ids_by_facts = HashMap::with_capacity(inner.descriptors.len()); + let mut ids_by_keys: HashMap> = HashMap::new(); + for (&id, &descriptor) in &inner.descriptors { + ids_by_facts.insert(descriptor_facts(descriptor), id); + ids_by_keys.entry(descriptor.keys).or_default().push(id); + } + inner.ids_by_facts = ids_by_facts; + inner.ids_by_keys = ids_by_keys; +} + /// #6759 C3c: ShapeIds live in their own u32 range, disjoint from every /// real class id (user counter tops out far below; builtin reserved /// ranges sit at `0x7FFF_FF00..=0x7FFF_FFFF` and `0xFFFF_0000..`), so a @@ -90,43 +155,90 @@ pub(crate) fn is_shape_id_token(v: usize) -> bool { /// (4611686018427387904 = 1 << 62). pub(crate) const PIC_ID_TOKEN_BIT: u64 = 1 << 62; -fn alloc_shape_id() -> u32 { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ShapeIdExhausted; + +fn alloc_shape_id_from(next: &std::sync::atomic::AtomicU32) -> Result { use std::sync::atomic::Ordering; - let id = SHAPE_ID_NEXT.fetch_add(1, Ordering::Relaxed); - if id >= SHAPE_ID_END { - // Range exhausted: park the counter (every subsequent call lands - // here again, so it can never wrap back into the valid range) and - // stop handing out ids — 0 disables the acceleration, never - // correctness. - SHAPE_ID_NEXT.store(SHAPE_ID_END, Ordering::Relaxed); - return 0; + loop { + let id = next.load(Ordering::Relaxed); + if id >= SHAPE_ID_END { + // Park at the exclusive end. In particular, never fetch_add at + // END: wrapping to zero could eventually alias a live ShapeId. + next.store(SHAPE_ID_END, Ordering::Relaxed); + return Err(ShapeIdExhausted); + } + if next + .compare_exchange_weak(id, id + 1, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + return Ok(id); + } } - id } -/// #6759 C3c: get-or-create the shape record for `keys` and return its -/// stable id (0 only if the id range is exhausted). Used by the resolve -/// paths to stamp a plain object's header after a successful lookup. -pub(crate) fn shape_id_for_keys_ensure(keys: *const ArrayHeader, key_count: u32) -> u32 { +fn alloc_shape_id() -> Result { + alloc_shape_id_from(&SHAPE_ID_NEXT) +} + +/// Get or create the exact descriptor. Exhaustion is recoverable and +/// fail-closed: callers leave the object unstamped and continue through the +/// retained authoritative header pointer/count guards. No id is reused and no +/// descriptor lookup can alias. +pub(crate) fn shape_descriptor_ensure( + keys: *const ArrayHeader, + logical_key_count: u32, + live_inline_slot_count: u32, +) -> Result { let keys_id = keys as usize; if keys_id == 0 { - return 0; + return Err(ShapeIdExhausted); } - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - entries - .entry(keys_id) - .or_insert_with(|| Shape { - indexed_len: 0, - shape_id: alloc_shape_id(), - // `key_count` is a capacity HINT only — some callers pass an - // unvalidated header length, so cap it before it sizes an - // allocation. - slots: HashMap::with_capacity(key_count.min(4096) as usize), - }) - .shape_id + let facts = ShapeFacts { + keys: keys_id as u64, + logical_key_count, + live_inline_slot_count, + }; + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + if let Some(&id) = inner.ids_by_facts.get(&facts) { + return Ok(id); + } + let id = alloc_shape_id()?; + let descriptor = ShapeDescriptor { + keys: keys_id as u64, + logical_key_count, + live_inline_slot_count, + }; + // Publish by-id first, then the reverse accelerator. An ObjectHeader is + // stamped only after this function returns, so a visible id always has a + // complete descriptor. + inner.descriptors.insert(id, descriptor); + inner.ids_by_facts.insert(facts, id); + inner.ids_by_keys.entry(facts.keys).or_default().push(id); + Ok(id) +} + +/// Compatibility mint for canonical shapes whose key and live-slot counts are +/// identical. New object-aware paths use [`shape_descriptor_ensure`] directly. +pub(crate) fn shape_id_for_keys_ensure(keys: *const ArrayHeader, key_count: u32) -> u32 { + shape_descriptor_ensure(keys, key_count, key_count).unwrap_or(0) +} + +pub(crate) fn shape_descriptor_by_id(shape_id: u32) -> Option { + if !is_shape_id(shape_id) { + return None; + } + crate::state::state() + .shapes + .inner + .borrow() + .descriptors + .get(&shape_id) + .copied() } -/// Mint (or retrieve) the stable ShapeId paired with a canonical keys array. +/// Mint (or retrieve) the ShapeId paired with canonical keys and equal +/// key/live-slot counts. /// /// Codegen calls this once per class during module initialization and stores /// the result beside `@perry_class_keys_*`. It deliberately takes a raw u64 @@ -195,10 +307,10 @@ pub(crate) unsafe fn object_shape_stamp(obj: *const crate::object::ObjectHeader) } } -/// Stamp `obj` with the stable ShapeId of `keys`, minting the shape record on +/// Stamp `obj` with the exact ShapeId of `keys`, minting the descriptor on /// first touch. Returns the id, or 0 when the receiver is not stampable (a -/// RegExp alias) or the id range is exhausted — in which case the caller keeps -/// its keys-address fallback, which is never a correctness difference. +/// RegExp alias) or the id range is exhausted. Exhaustion leaves the object +/// unstamped so the retained header pointer/count checks remain authoritative. #[inline] pub(crate) unsafe fn stamp_object_shape( obj: *mut crate::object::ObjectHeader, @@ -208,16 +320,19 @@ pub(crate) unsafe fn stamp_object_shape( if !shape_word_is_writable(obj) { return 0; } - let id = shape_id_for_keys_ensure(keys, key_count); - if id != 0 { - (*obj).parent_class_id = id; - } + let Ok(id) = shape_descriptor_ensure(keys, key_count, (*obj).field_count) else { + clear_object_shape_stamp(obj); + return 0; + }; + (*obj).parent_class_id = id; + debug_assert_object_shape_parity(obj); id } -/// Birth-stamp a NEWBORN receiver with an already-minted ShapeId. A zero id — -/// no shape-cache record yet, or the id range exhausted — leaves the word -/// alone, which preserves the pre-stamp fallback rather than inventing one. +/// Birth-stamp a NEWBORN receiver with an already-minted ShapeId after checking +/// its descriptor against the completed header. A missing, foreign, or +/// count-mismatched id is replaced with an exact local descriptor when ids +/// remain available; exhaustion leaves the receiver explicitly unstamped. /// /// ★ **A shape's population must be UNIFORMLY stamped or uniformly not.** The /// emitted read PIC derives its ENTIRE cache token from this word @@ -255,11 +370,181 @@ pub(crate) unsafe fn birth_stamp_object_shape( obj: *mut crate::object::ObjectHeader, runtime_shape_id: u32, ) { - if is_shape_id(runtime_shape_id) { + if is_shape_id(runtime_shape_id) && descriptor_matches_object(runtime_shape_id, obj) { (*obj).parent_class_id = runtime_shape_id; + debug_assert_object_shape_parity(obj); + } else { + synchronize_object_shape_descriptor(obj); } } +/// Install the exact descriptor for the object's current authoritative header +/// facts. This is the only shape publication operation used by mutations. +/// Exhaustion (or no keys) clears the stamp, retaining the old exact guards. +pub(crate) unsafe fn synchronize_object_shape_descriptor( + obj: *mut crate::object::ObjectHeader, +) -> u32 { + if obj.is_null() || !shape_word_is_writable(obj) { + return 0; + } + let keys = (*obj).keys_array; + if keys.is_null() { + clear_object_shape_stamp(obj); + return 0; + } + let key_count = crate::array::keys_array_len_capped_to_capacity(keys) as u32; + + // A same-address length change is legal only for an owned keys array. A + // shared array must have cloned before push; otherwise siblings already + // observe mutated bytes and no descriptor can make that state sound. + let old_id = object_shape_stamp(obj); + if let Some(old) = shape_descriptor_by_id(old_id) { + if old.keys == keys as u64 && old.logical_key_count != key_count { + let Some(gc) = crate::value::addr_class::try_read_tracked_gc_header(keys as usize) + else { + clear_object_shape_stamp(obj); + return 0; + }; + if (*gc.as_ptr()).obj_type != crate::gc::GC_TYPE_ARRAY { + clear_object_shape_stamp(obj); + return 0; + } + let shared = (*gc.as_ptr()).gc_flags & crate::gc::GC_FLAG_SHAPE_SHARED != 0; + debug_assert!( + !shared, + "shared keys array mutated in place under an immutable ShapeId" + ); + if shared { + clear_object_shape_stamp(obj); + return 0; + } + retire_key_count_versions(keys as u64, key_count); + } + } + + let Ok(id) = shape_descriptor_ensure(keys, key_count, (*obj).field_count) else { + clear_object_shape_stamp(obj); + return 0; + }; + (*obj).parent_class_id = id; + debug_assert_object_shape_parity(obj); + id +} + +fn retire_key_count_versions(keys: u64, current_key_count: u32) { + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + let Some(ids) = inner.ids_by_keys.remove(&keys) else { + return; + }; + let mut current_ids = Vec::with_capacity(ids.len()); + for id in ids { + let Some(descriptor) = inner.descriptors.get(&id).copied() else { + continue; + }; + debug_assert_eq!( + descriptor.keys, keys, + "keys index contains a foreign descriptor" + ); + if descriptor.keys != keys { + let correct_ids = inner.ids_by_keys.entry(descriptor.keys).or_default(); + if !correct_ids.contains(&id) { + correct_ids.push(id); + } + } else if descriptor.logical_key_count != current_key_count { + inner.descriptors.remove(&id); + inner.ids_by_facts.remove(&descriptor_facts(descriptor)); + } else { + current_ids.push(id); + } + } + if !current_ids.is_empty() { + inner.ids_by_keys.insert(keys, current_ids); + } +} + +fn descriptor_matches_object(shape_id: u32, obj: *const crate::object::ObjectHeader) -> bool { + let Some(d) = shape_descriptor_by_id(shape_id) else { + return false; + }; + unsafe { + let keys = (*obj).keys_array; + !keys.is_null() + && d.keys == keys as u64 + && d.logical_key_count == crate::array::keys_array_len_capped_to_capacity(keys) as u32 + && d.live_inline_slot_count == (*obj).field_count + } +} + +#[inline] +pub(crate) unsafe fn debug_assert_object_shape_parity(obj: *const crate::object::ObjectHeader) { + let id = object_shape_stamp(obj); + if id != 0 { + debug_assert!( + descriptor_matches_object(id, obj), + "published ShapeId disagrees with authoritative ObjectHeader facts" + ); + } +} + +/// Validate and immediately mirror a live object's authoritative keys edge +/// after that header slot has been visited. No address inside the descriptor +/// HashMap is ever handed to a generic visitor: remembered-set enumeration can +/// save slot pointers across budgeted mutator resumptions, while descriptor +/// insertion/pruning may reallocate the table in between. +/// +/// An immediate copying visitor has already rewritten `new_header_keys`; a +/// deferred dirty-work visitor leaves it equal to `old_header_keys`, and the +/// registered metadata forwarding pass repairs the weak mirror after copying. +/// Exact release-mode facts prevent a stale or foreign id from rekeying an +/// unrelated descriptor. Returns whether the descriptor facts validated. +pub(crate) unsafe fn synchronize_live_object_shape_descriptor_after_header_visit( + obj: *const crate::object::ObjectHeader, + old_header_keys: u64, + new_header_keys: u64, + logical_key_count: u32, + live_inline_slot_count: u32, +) -> bool { + let shape_id = object_shape_stamp(obj); + if shape_id == 0 { + return false; + } + + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + let (old_facts, new_facts) = { + let Some(descriptor) = inner.descriptors.get_mut(&shape_id) else { + // A foreign-agent/stale id fails closed; the authoritative header + // edge is still traced by the caller. + return false; + }; + // Release-mode fail-closed gate. An id hit is insufficient: a foreign + // or stale id must never cause an unrelated descriptor pointer to be + // rekeyed. `new_header_keys` may differ after evacuation; a sibling + // may also have rewritten the shared descriptor before this object. + if descriptor.logical_key_count != logical_key_count + || descriptor.live_inline_slot_count != live_inline_slot_count + || (descriptor.keys != old_header_keys && descriptor.keys != new_header_keys) + { + return false; + } + let old_facts = descriptor_facts(*descriptor); + if descriptor.keys == old_header_keys && new_header_keys != old_header_keys { + descriptor.keys = new_header_keys; + } + (old_facts, descriptor_facts(*descriptor)) + }; + if new_facts != old_facts { + inner.ids_by_facts.remove(&old_facts); + inner.ids_by_facts.insert(new_facts, shape_id); + remove_id_from_keys_index(&mut inner, old_facts.keys, shape_id); + inner + .ids_by_keys + .entry(new_facts.keys) + .or_default() + .push(shape_id); + } + true +} + /// Drop the stamp iff the word currently holds one, leaving a real /// `parent_class_id` untouched. Returns true when a stamp was cleared. /// @@ -277,7 +562,7 @@ pub(crate) unsafe fn clear_object_shape_stamp(obj: *mut crate::object::ObjectHea } /// Build (or extend) the slot map for `keys` covering `key_count` keys. -unsafe fn index_range(shape: &mut Shape, keys: *const ArrayHeader, key_count: u32) { +unsafe fn index_range(shape: &mut ShapeIndex, keys: *const ArrayHeader, key_count: u32) { let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let (slots, slot_len) = super::keys_array_dense_slots(keys); for i in shape.indexed_len..key_count.min(slot_len as u32) { @@ -306,12 +591,12 @@ pub(crate) unsafe fn shape_slot_lookup( build: bool, ) -> Option { let keys_id = keys as usize; - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - let shape = match entries.get_mut(&keys_id) { + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + let shape = match inner.indices.get_mut(&keys_id) { Some(s) => { if s.indexed_len > key_count { // Shrink (delete/compaction): slots are untrustworthy. - entries.remove(&keys_id); + inner.indices.remove(&keys_id); return None; } s @@ -320,9 +605,8 @@ pub(crate) unsafe fn shape_slot_lookup( if !build { return None; } - entries.entry(keys_id).or_insert(Shape { + inner.indices.entry(keys_id).or_insert(ShapeIndex { indexed_len: 0, - shape_id: alloc_shape_id(), slots: HashMap::with_capacity(key_count as usize), }) } @@ -356,8 +640,8 @@ pub(crate) fn shape_note_append( key_hash: u64, slot: u32, ) { - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - if let Some(shape) = entries.get_mut(&(keys as usize)) { + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + if let Some(shape) = inner.indices.get_mut(&(keys as usize)) { if shape.indexed_len + 1 == new_count { shape.indexed_len = new_count; shape.slots.entry(key_hash).or_default().push(slot); @@ -368,18 +652,18 @@ pub(crate) fn shape_note_append( /// Back-fill a linear-scan hit (no-op when the shape has no entry — the /// next lookup builds it wholesale at the caller's threshold). pub(crate) fn shape_note_hit(keys: *const ArrayHeader, key_hash: u64, slot: u32) { - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - if let Some(shape) = entries.get_mut(&(keys as usize)) { + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + if let Some(shape) = inner.indices.get_mut(&(keys as usize)) { shape.slots.entry(key_hash).or_default().push(slot); } } -/// #6759 Phase C3a: an OWNED (non-`GC_FLAG_SHAPE_SHARED`) keys array was -/// reallocated by `js_array_push` — the SAME logical shape now lives at a -/// new address. Migrate the record (slot map, indexed_len, shape_id) so it -/// survives the capacity doubling; pre-C3a the record was orphaned at the -/// old address and the next lookup rebuilt it O(key_count), making every -/// doubling of a wide object's build pay a full re-index. +/// An OWNED (non-`GC_FLAG_SHAPE_SHARED`) keys array was reallocated while +/// `js_array_push` appended a key. Migrate only the validated slot-index +/// accelerator so it survives capacity growth. The weak old descriptor is not +/// eagerly deleted: even if a release-only invariant regression left a sibling +/// naming it, that sibling must continue to resolve. Post-trace dead-key +/// pruning retires it once no live owner reaches the old array. /// /// Callers must pass the OWNED-grow pair only: a shared array's fork is a /// genuine transition (the clone starts a NEW identity and the old address @@ -392,48 +676,75 @@ pub(crate) fn shape_keys_grown(old_keys: usize, new_keys: *const ArrayHeader) { if old_keys == 0 || new_id == 0 || old_keys == new_id { return; } - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - if let Some(shape) = entries.remove(&old_keys) { - entries.insert(new_id, shape); + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + if let Some(shape) = inner.indices.remove(&old_keys) { + inner.indices.insert(new_id, shape); } } -/// Drop the shape for a keys_array that was compacted/retired in place -/// (delete path). Address-recycled arrays need no eager drop — validation -/// rejects them — but the delete path knows the map is stale NOW. +/// Drop only the validated slot-index accelerator for a keys array that was +/// compacted/retired (delete path). Descriptors are weak and exact-fact gated, +/// but are not eagerly removed: another live sibling may still name one. The +/// post-trace dead-key fan-out retires them when the array is actually dead. pub(crate) fn shape_drop(keys: *const ArrayHeader) { - crate::state::state() - .shapes - .entries - .borrow_mut() - .remove(&(keys as usize)); + let keys = keys as usize; + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + inner.indices.remove(&keys); } -/// Memory prune for the dead-owner fan-out: drop shapes whose keys_array -/// is dead. Correctness never depends on this (validation-on-hit). +/// Post-trace weak-table prune: drop slot indices and by-id descriptors whose +/// keys array is dead. A live object has already traced its authoritative +/// header edge and synchronized the descriptor named by its ShapeId, so a +/// descriptor removed here cannot be named by a live object. Correctness fails +/// closed on a missing lookup, independently of pruning. pub(crate) fn prune_dead_shape_keys(is_dead_owner: &dyn Fn(usize) -> bool) { - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - if !entries.is_empty() { - entries.retain(|keys_id, _| !is_dead_owner(*keys_id)); + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + if !inner.indices.is_empty() { + inner.indices.retain(|keys_id, _| !is_dead_owner(*keys_id)); + } + let stale: Vec = inner + .descriptors + .iter() + .filter_map(|(&id, descriptor)| is_dead_owner(descriptor.keys as usize).then_some(id)) + .collect(); + if !stale.is_empty() { + for id in stale { + inner.descriptors.remove(&id); + } + // Rebuild rather than removing by current facts one-at-a-time. A + // deferred live-object rewrite may have changed the by-id pointer + // before the metadata scanner repaired the reverse accelerators; a + // rebuild cannot retain either an old-facts or old-keys entry. + rebuild_descriptor_reverse_indices(&mut inner); } } -/// #6759 Phase C3a: rekey shape records when GC evacuation MOVES their -/// keys array, so a wide object's slot map (and its stable `shape_id`) -/// survives a copied minor instead of being orphaned at the from-space -/// address and rebuilt O(key_count) on the next lookup. Metadata-rewrite -/// only — the records hold no heap references (slot indexes + an address -/// used as identity), so outside that phase this scanner is a no-op and -/// marks nothing. Same pattern as the descriptor-table owner rekey. +/// Metadata-only forwarding repair for the weak descriptor table and +/// pointer-keyed slot indices. Mark/copy mode does not root anything; live +/// object scans provide descriptor reachability, and post-copy rewrite follows +/// only forwarding records those live edges already created. pub(crate) fn scan_shape_table_rekey_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - if !visitor.is_metadata_rewrite_phase() { - return; + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + let mut descriptor_moved = false; + for descriptor in inner.descriptors.values_mut() { + let mut addr = descriptor.keys as usize; + if visitor.visit_metadata_usize_slot(&mut addr) { + descriptor.keys = addr as u64; + descriptor_moved = true; + } } - let mut entries = crate::state::state().shapes.entries.borrow_mut(); - if entries.is_empty() { + // Rebuild throughout rewrite phase even when this pass itself observed no + // move: an immediate live-object header callback may already have rekeyed + // a shared descriptor, while a deferred callback relies on this pass. + if descriptor_moved || visitor.is_metadata_rewrite_phase() { + rebuild_descriptor_reverse_indices(&mut inner); + } + + if !visitor.is_metadata_rewrite_phase() || inner.indices.is_empty() { return; } - let moved: Vec<(usize, usize)> = entries + let moved: Vec<(usize, usize)> = inner + .indices .keys() .filter_map(|&keys_id| { let mut addr = keys_id; @@ -442,8 +753,8 @@ pub(crate) fn scan_shape_table_rekey_mut(visitor: &mut crate::gc::RuntimeRootVis }) .collect(); for (old, new) in moved { - if let Some(shape) = entries.remove(&old) { - entries.insert(new, shape); + if let Some(shape) = inner.indices.remove(&old) { + inner.indices.insert(new, shape); } } } @@ -452,32 +763,71 @@ pub(crate) fn scan_shape_table_rekey_mut(visitor: &mut crate::gc::RuntimeRootVis pub(crate) fn test_shape_entry_exists(keys_id: usize) -> bool { crate::state::state() .shapes - .entries + .inner .borrow() + .indices .get(&keys_id) .is_some() } #[cfg(test)] -pub(crate) fn test_seed_shape_entry(keys_id: usize) { - crate::state::state().shapes.entries.borrow_mut().insert( - keys_id, - Shape { - indexed_len: 0, - shape_id: alloc_shape_id(), - slots: HashMap::new(), - }, - ); +pub(crate) fn test_shape_descriptor_count() -> usize { + crate::state::state() + .shapes + .inner + .borrow() + .descriptors + .len() } #[cfg(test)] -pub(crate) fn test_shape_id_for_keys(keys_id: usize) -> Option { +pub(crate) fn test_clear_shape_table() { + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + inner.indices.clear(); + inner.descriptors.clear(); + inner.ids_by_facts.clear(); + inner.ids_by_keys.clear(); +} + +#[cfg(test)] +pub(crate) fn test_drop_shape_descriptors(keys_id: usize) { + let mut inner = crate::state::state().shapes.inner.borrow_mut(); + let stale = inner + .ids_by_keys + .remove(&(keys_id as u64)) + .unwrap_or_default(); + for id in stale { + if let Some(descriptor) = inner.descriptors.remove(&id) { + inner.ids_by_facts.remove(&descriptor_facts(descriptor)); + } + } +} + +#[cfg(test)] +pub(crate) fn test_seed_shape_entry(keys_id: usize) { crate::state::state() .shapes - .entries - .borrow() - .get(&keys_id) - .map(|s| s.shape_id) + .inner + .borrow_mut() + .indices + .insert( + keys_id, + ShapeIndex { + indexed_len: 0, + slots: HashMap::new(), + }, + ); + let _ = shape_descriptor_ensure(keys_id as *const ArrayHeader, 0, 0) + .expect("test shape id range unexpectedly exhausted"); +} + +#[cfg(test)] +pub(crate) fn test_shape_id_for_keys(keys_id: usize) -> Option { + let inner = crate::state::state().shapes.inner.borrow(); + inner + .ids_by_keys + .get(&(keys_id as u64)) + .and_then(|ids| ids.first().copied()) } #[cfg(test)] @@ -489,7 +839,7 @@ mod c3c_tests { } /// #6759 C3c: ids come from the dedicated range (disjoint from real and - /// builtin class ids), are stable per keys identity, and distinct + /// builtin class ids), are stable per exact descriptor facts, and distinct /// across identities. #[test] fn shape_ids_are_range_disjoint_and_stable() { @@ -508,6 +858,8 @@ mod c3c_tests { assert!(!is_shape_id(0xFFFF_0005)); shape_drop(a as *const ArrayHeader); shape_drop(b as *const ArrayHeader); + test_drop_shape_descriptors(a); + test_drop_shape_descriptors(b); } /// #6759 C3 rung 2: the codegen-facing allocator receives the id minted @@ -544,9 +896,10 @@ mod c3c_tests { /// #6759 C3c stamp invariant on a REAL object through the real /// write/read paths: a read resolution stamps a shape id into the - /// plain object's `parent_class_id`; after further appends the stamp - /// is either cleared (keys pointer changed) or still equal to the - /// current keys' id (in-place append / migrated grow). + /// plain object's `parent_class_id`; after further appends any surviving + /// stamp resolves to exact current pointer/logical/live facts. This fixture + /// deliberately reserves eight live inline slots while owning fewer keys, + /// so the old key-count-only compatibility mint is not the expected id. #[test] fn plain_object_stamp_lifecycle() { let _lock = crate::gc::global_side_table_test_lock(); @@ -568,14 +921,15 @@ mod c3c_tests { let stamp2 = (*obj).parent_class_id; if stamp2 != 0 { assert!(is_shape_id(stamp2)); - let cur = shape_id_for_keys_ensure( - (*obj).keys_array, - crate::array::js_array_length((*obj).keys_array), - ); + let descriptor = shape_descriptor_by_id(stamp2) + .expect("a surviving stamp must resolve in this agent"); + assert_eq!(descriptor.keys, (*obj).keys_array as u64); assert_eq!( - stamp2, cur, - "a surviving stamp must equal the CURRENT keys' id" + descriptor.logical_key_count, + crate::array::js_array_length((*obj).keys_array) ); + assert_eq!(descriptor.live_inline_slot_count, (*obj).field_count); + debug_assert_object_shape_parity(obj); } // Reads still resolve correctly through the id-keyed cache. @@ -678,3 +1032,224 @@ mod c6804_tests { } } } + +#[cfg(test)] +mod descriptor_tests_8067 { + use super::*; + + fn key(name: &str) -> *mut crate::StringHeader { + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) + } + + #[test] + fn exhaustion_parks_without_reuse_or_alias() { + let next = std::sync::atomic::AtomicU32::new(SHAPE_ID_END - 1); + assert_eq!(alloc_shape_id_from(&next), Ok(SHAPE_ID_END - 1)); + assert_eq!(alloc_shape_id_from(&next), Err(ShapeIdExhausted)); + assert_eq!(alloc_shape_id_from(&next), Err(ShapeIdExhausted)); + assert_eq!( + next.load(std::sync::atomic::Ordering::Relaxed), + SHAPE_ID_END, + "exhaustion must park instead of wrapping into an alias" + ); + } + + #[test] + fn a_foreign_agent_id_misses_instead_of_aliasing_same_address() { + let _lock = crate::gc::global_side_table_test_lock(); + let fake_keys = 0x8067_0000_0000_1000usize; + let local = shape_descriptor_ensure(fake_keys as *const ArrayHeader, 2, 2) + .expect("shape range unexpectedly exhausted"); + let foreign = std::thread::spawn(move || { + assert_eq!( + shape_descriptor_by_id(local), + None, + "another RuntimeState resolved a foreign agent's ShapeId" + ); + shape_descriptor_ensure(fake_keys as *const ArrayHeader, 2, 2) + .expect("shape range unexpectedly exhausted") + }) + .join() + .expect("agent-isolation thread panicked"); + assert_ne!( + local, foreign, + "process-global ids must not alias by address" + ); + shape_drop(fake_keys as *const ArrayHeader); + test_drop_shape_descriptors(fake_keys); + } + + #[test] + fn gc_descriptor_mirror_requires_exact_release_facts() { + let _lock = crate::gc::global_side_table_test_lock(); + let keys = 0x8067_0000_0000_2000usize; + let id = shape_descriptor_ensure(keys as *const ArrayHeader, 3, 2) + .expect("shape range unexpectedly exhausted"); + let obj = crate::object::ObjectHeader { + object_type: 1, + class_id: 0, + parent_class_id: id, + field_count: 2, + keys_array: keys as *mut ArrayHeader, + meta: std::ptr::null_mut(), + }; + + unsafe { + assert!( + !synchronize_live_object_shape_descriptor_after_header_visit( + &obj, + keys as u64 + 0x1000, + keys as u64 + 0x2000, + 3, + 2, + ) + ); + assert!( + !synchronize_live_object_shape_descriptor_after_header_visit( + &obj, + keys as u64, + keys as u64, + 4, + 2, + ) + ); + } + assert_eq!(shape_descriptor_by_id(id).unwrap().keys, keys as u64); + + let moved_keys = keys as u64 + 0x3000; + assert!(unsafe { + synchronize_live_object_shape_descriptor_after_header_visit( + &obj, + keys as u64, + moved_keys, + 3, + 2, + ) + }); + assert_eq!(shape_descriptor_by_id(id).unwrap().keys, moved_keys); + test_drop_shape_descriptors(moved_keys as usize); + assert_eq!( + shape_descriptor_by_id(id), + None, + "descriptor rekey did not update the keys-address index" + ); + } + + #[test] + fn key_count_retirement_is_scoped_to_one_keys_identity() { + let _lock = crate::gc::global_side_table_test_lock(); + let keys = 0x8067_0000_0000_2100usize; + let unrelated_keys = 0x8067_0000_0000_2200usize; + let stale_a = shape_descriptor_ensure(keys as *const ArrayHeader, 1, 1) + .expect("shape range unexpectedly exhausted"); + let stale_b = shape_descriptor_ensure(keys as *const ArrayHeader, 1, 2) + .expect("shape range unexpectedly exhausted"); + let current = shape_descriptor_ensure(keys as *const ArrayHeader, 2, 2) + .expect("shape range unexpectedly exhausted"); + let unrelated = shape_descriptor_ensure(unrelated_keys as *const ArrayHeader, 1, 1) + .expect("shape range unexpectedly exhausted"); + + retire_key_count_versions(keys as u64, 2); + + assert_eq!(shape_descriptor_by_id(stale_a), None); + assert_eq!(shape_descriptor_by_id(stale_b), None); + assert!(shape_descriptor_by_id(current).is_some()); + assert!(shape_descriptor_by_id(unrelated).is_some()); + let inner = crate::state::state().shapes.inner.borrow(); + let current_ids = inner + .ids_by_keys + .get(&(keys as u64)) + .expect("current keys identity disappeared from retirement index"); + assert_eq!(current_ids.as_slice(), &[current]); + drop(inner); + + test_drop_shape_descriptors(keys); + test_drop_shape_descriptors(unrelated_keys); + } + + #[test] + fn shape_drop_does_not_delete_a_potential_siblings_descriptor() { + let _lock = crate::gc::global_side_table_test_lock(); + let keys = 0x8067_0000_0000_3000usize; + let id = shape_descriptor_ensure(keys as *const ArrayHeader, 1, 1) + .expect("shape range unexpectedly exhausted"); + + shape_drop(keys as *const ArrayHeader); + + assert_eq!( + shape_descriptor_by_id(id).map(|descriptor| descriptor.keys), + Some(keys as u64), + "shape_drop eagerly invalidated a descriptor a sibling may still name" + ); + test_drop_shape_descriptors(keys); + } + + #[test] + fn live_slot_growth_versions_descriptor_before_value_publication() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let packed = b"slot8067_a"; + let obj = crate::object::js_object_alloc_with_shape( + 0x8067_1001, + 1, + packed.as_ptr(), + packed.len() as u32, + ); + let keys = (*obj).keys_array as usize; + let before = (*obj).parent_class_id; + let before_descriptor = shape_descriptor_by_id(before).expect("birth descriptor"); + assert_eq!(before_descriptor.live_inline_slot_count, 1); + + crate::object::js_object_set_field(obj, 1, crate::JSValue::string_ptr(key("value"))); + let after = (*obj).parent_class_id; + assert_ne!(before, after); + let after_descriptor = shape_descriptor_by_id(after).expect("grown descriptor"); + assert_eq!(after_descriptor.keys, keys as u64); + assert_eq!(after_descriptor.logical_key_count, 1); + assert_eq!(after_descriptor.live_inline_slot_count, 2); + debug_assert_object_shape_parity(obj); + } + } + + #[test] + fn shared_sibling_append_clones_before_descriptor_version_changes() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let packed = b"sib8067_a"; + let a = crate::object::js_object_alloc_with_shape( + 0x8067_1002, + 1, + packed.as_ptr(), + packed.len() as u32, + ); + let b = crate::object::js_object_alloc_with_shape( + 0x8067_1002, + 1, + packed.as_ptr(), + packed.len() as u32, + ); + let shared_keys = (*a).keys_array; + let shared_id = (*a).parent_class_id; + assert_eq!(shared_keys, (*b).keys_array); + assert_eq!(shared_id, (*b).parent_class_id); + + crate::object::js_object_set_field_by_name(a, key("sib8067_b"), 2.0); + + assert_ne!((*a).keys_array, shared_keys); + assert_eq!((*b).keys_array, shared_keys); + assert_eq!((*b).parent_class_id, shared_id); + assert_ne!((*a).parent_class_id, shared_id); + assert_eq!( + shape_descriptor_by_id(shared_id) + .expect("untouched sibling descriptor") + .logical_key_count, + 1 + ); + let transitioned = + shape_descriptor_by_id((*a).parent_class_id).expect("transitioned descriptor"); + assert_eq!(transitioned.keys, (*a).keys_array as u64); + assert_eq!(transitioned.logical_key_count, 2); + assert_eq!(transitioned.live_inline_slot_count, 2); + } + } +} diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py new file mode 100644 index 0000000000..47a386258e --- /dev/null +++ b/scripts/shape_descriptor_census.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python3 +"""#8067 exact shape-header census plus authority-order sabotage tests.""" + +from __future__ import annotations + +import argparse +import json +import re +from collections import Counter +from collections.abc import Callable +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +BASELINE_PATH = ROOT / "scripts" / "shape_descriptor_census_baseline.json" +FIELDS = ("object_type", "field_count", "keys_array") +RAW_STRING_START = re.compile(r'(?:br|r)(?P#{0,255})"') +RUST_SPECIAL = re.compile( + r"//|/\*|(?:b)?'(?:\\(?:x[0-9A-Fa-f]{2}|u\{[0-9A-Fa-f_]+\}|.)|[^'\\\n])'|(?:br|r)#{0,255}\"|(?:b|c)?\"" +) +BLOCK_COMMENT_MARK = re.compile(r'/\*|\*/') +QUOTED_STRING_TAIL = re.compile(r'(?:\\.|[^"\\])*"', re.DOTALL) + + +class CensusError(RuntimeError): + pass + + +def rust_sources() -> dict[str, str]: + return { + path.relative_to(ROOT).as_posix(): path.read_text(encoding="utf-8") + for path in sorted((ROOT / "crates").rglob("*.rs")) + } + + +def strip_rust_comments_and_literals(source: str) -> str: + """Blank comments/string literals while preserving code and newlines.""" + + chunks: list[str] = [] + pos = 0 + while match := RUST_SPECIAL.search(source, pos): + chunks.append(source[pos : match.start()]) + lexeme = match.group() + end = match.end() + if lexeme == "//": + newline = source.find("\n", end) + if newline < 0: + chunks.append(" ") + pos = len(source) + break + chunks.append("\n") + pos = newline + 1 + continue + if lexeme == "/*": + depth = 1 + cursor = end + while depth and (mark := BLOCK_COMMENT_MARK.search(source, cursor)): + depth += 1 if mark.group() == "/*" else -1 + cursor = mark.end() + end = cursor if depth == 0 else len(source) + else: + raw = RAW_STRING_START.fullmatch(lexeme) + if lexeme.endswith("'"): + end = match.end() + elif raw: + terminator = '"' + raw.group("hashes") + close = source.find(terminator, end) + end = len(source) if close < 0 else close + len(terminator) + else: + tail = QUOTED_STRING_TAIL.match(source, end) + end = len(source) if tail is None else tail.end() + chunks.append(" " + "\n" * source[match.start() : end].count("\n")) + pos = end + chunks.append(source[pos:]) + return "".join(chunks) + + +def stripped_sources(sources: dict[str, str]) -> dict[str, str]: + return {path: strip_rust_comments_and_literals(text) for path, text in sources.items()} + + +def run_literal_lexer_selftest() -> None: + fixture = """unsafe fn quote_fixture(o: *mut ObjectHeader) { + let byte_quote = b'"'; + (*o).keys_array = core::ptr::null_mut(); + let char_quote = '"'; + let dead = "(*o).keys_array = core::ptr::null_mut();"; + } + """ + clean = strip_rust_comments_and_literals(fixture) + if len(re.findall(r"\.\s*keys_array\b", clean)) != 1: + raise CensusError("literal lexer swallowed a real member between quote-char literals") + + +def normalize_line(line: str) -> str: + return re.sub(r"\s+", " ", line.strip()) + + +def callsite_multiset(clean: dict[str, str]) -> Counter[str]: + sites: Counter[str] = Counter() + for path, source in clean.items(): + for line in source.splitlines(): + normalized = normalize_line(line) + if not normalized: + continue + for field in FIELDS: + access_count = len(re.findall(rf"\.\s*{field}\b", line)) + declaration_count = len(re.findall(rf"\b{field}\s*:", line)) + if access_count: + sites[f"{path}|{field}|access|{normalized}"] += access_count + if declaration_count: + sites[f"{path}|{field}|declaration|{normalized}"] += declaration_count + return sites + + +def codegen_header_size_multiset(clean: dict[str, str]) -> Counter[str]: + sites: Counter[str] = Counter() + prefix = "crates/perry-codegen/src/" + for path, source in clean.items(): + if not path.startswith(prefix): + continue + for line in source.splitlines(): + count = len(re.findall(r"\bobject_header_size_bytes\b", line)) + if count: + sites[f"{path}|{normalize_line(line)}"] += count + return sites + + +def observed_census(sources: dict[str, str]) -> dict[str, object]: + candidates = { + path: text + for path, text in sources.items() + if any(field in text for field in FIELDS) + or "object_header_size_bytes" in text + } + clean = stripped_sources(candidates) + raw_sites = callsite_multiset(clean) + codegen_sites = codegen_header_size_multiset(clean) + totals = {field: 0 for field in FIELDS} + files: set[str] = set() + for identity, count in raw_sites.items(): + path, field, _, _ = identity.split("|", 3) + totals[field] += count + files.add(path) + return { + "raw_member_callsite_multiset": dict(sorted(raw_sites.items())), + "codegen_object_header_size_callsite_multiset": dict(sorted(codegen_sites.items())), + "summary": { + "raw_member_sites": totals, + "raw_member_files": len(files), + "codegen_object_header_size_sites": sum(codegen_sites.values()), + }, + } + + +def function_body(source: str, name: str) -> str: + match = re.search(rf"\bfn\s+{re.escape(name)}\b", source) + if not match: + raise CensusError(f"missing function body: {name}") + start = source.find("{", match.end()) + if start < 0: + raise CensusError(f"missing opening brace: {name}") + depth = 0 + for i in range(start, len(source)): + if source[i] == "{": + depth += 1 + elif source[i] == "}": + depth -= 1 + if depth == 0: + return source[start + 1 : i] + raise CensusError(f"missing closing brace: {name}") + + +def require_code(source: str, pattern: str, label: str) -> None: + if not re.search(pattern, source, re.MULTILINE | re.DOTALL): + raise CensusError(f"shape descriptor authority surface missing: {label}") + + +def assert_before(body: str, first: str, second: str, label: str) -> None: + first_at = body.find(first) + second_at = body.find(second) + if first_at < 0 or second_at < 0 or first_at >= second_at: + raise CensusError(f"shape descriptor authority ordering failed: {label}") + + +def assert_authority_surfaces(sources: dict[str, str]) -> None: + authority_paths = ( + "crates/perry-runtime/src/object/shapes.rs", + "crates/perry-runtime/src/object/mod.rs", + "crates/perry-codegen/src/lower_call/new_alloc.rs", + "crates/perry-runtime/src/gc/layout_slot_visit.rs", + "crates/perry-runtime/src/object/field_set_by_name/tail.rs", + ) + missing = [path for path in authority_paths if path not in sources] + if missing: + raise CensusError( + "shape descriptor authority source missing: " + ", ".join(missing) + ) + clean = stripped_sources({path: sources[path] for path in authority_paths}) + shapes = clean["crates/perry-runtime/src/object/shapes.rs"] + object_mod = clean["crates/perry-runtime/src/object/mod.rs"] + codegen_alloc = clean["crates/perry-codegen/src/lower_call/new_alloc.rs"] + layout_visit = clean["crates/perry-runtime/src/gc/layout_slot_visit.rs"] + transition_tail = clean[ + "crates/perry-runtime/src/object/field_set_by_name/tail.rs" + ] + + for pattern, label in ( + (r"descriptors\s*:\s*HashMap\s*<\s*u32\s*,\s*ShapeDescriptor", "by-id descriptor table"), + (r"logical_key_count\s*:\s*u32", "exact logical-key fact"), + (r"live_inline_slot_count\s*:\s*u32", "exact live-slot fact"), + (r"\bfn\s+shape_descriptor_by_id\b", "by-id lookup"), + (r"\bfn\s+debug_assert_object_shape_parity\b", "parity assertion"), + (r"\bfn\s+synchronize_live_object_shape_descriptor_after_header_visit\b", "live-object descriptor mirror"), + (r"is_dead_owner\s*\(\s*descriptor\.keys\s+as\s+usize\s*\)", "dead descriptor pruning"), + ): + require_code(shapes, pattern, label) + + allocator = function_body(shapes, "alloc_shape_id_from") + require_code(allocator, r"\bcompare_exchange_weak\s*\(", "exhaustion park") + if re.search(r"\bfetch_add\s*\(|\bprocess\s*::\s*(?:abort|exit)\s*\(", allocator): + raise CensusError("ShapeId exhaustion is wrapping or unrecoverable") + require_code(shapes, r"\.unwrap_or\s*\(\s*0\s*\)", "recoverable exhaustion fallback") + + scanner = function_body(shapes, "scan_shape_table_rekey_mut") + require_code(scanner, r"\bvisit_metadata_usize_slot\s*\(", "weak metadata rewrite") + scanner_slot_apis = set(re.findall(r"\b(visit_[A-Za-z0-9_]*slot)\s*\(", scanner)) + if scanner_slot_apis != {"visit_metadata_usize_slot"}: + raise CensusError( + "descriptor scanner slot API allowlist failed: " + + ", ".join(sorted(scanner_slot_apis)) + ) + + layout_body = function_body(layout_visit, "visit_gc_layout_slot_descriptors") + assert_before( + layout_body, + "child_slots.take_prefix_child_slot()", + "synchronize_live_object_shape_descriptor_after_header_visit(", + "authoritative header visit before descriptor mirror", + ) + assert_before( + layout_body, + "try_read_tracked_gc_header(old_keys as usize)", + "keys_array_len_capped_to_capacity(old_keys)", + "array-header validation before descriptor fact read", + ) + require_code( + layout_body, + r"\(\s*\*\s*keys_header\.as_ptr\s*\(\s*\)\s*\)\.obj_type\s*==\s*GC_TYPE_ARRAY", + "descriptor fact capture exact array type", + ) + + ensure = function_body(shapes, "shape_descriptor_ensure") + assert_before( + ensure, + "inner.descriptors.insert", + "inner.ids_by_facts.insert", + "by-id descriptor before reverse accelerator", + ) + sync = function_body(shapes, "synchronize_object_shape_descriptor") + assert_before( + sync, + "shape_descriptor_ensure", + "(*obj).parent_class_id = id", + "descriptor before ObjectHeader ShapeId", + ) + retirement = function_body(shapes, "retire_key_count_versions") + require_code( + retirement, + r"ids_by_keys\s*\.\s*remove\s*\(\s*&keys\s*\)", + "keys-scoped descriptor retirement index", + ) + if re.search(r"descriptors\s*\.\s*(?:iter|values|keys)\s*\(", retirement): + raise CensusError("shape descriptor retirement scans the global descriptor table") + for name in ("shape_keys_grown", "shape_drop"): + if "descriptors.remove" in function_body(shapes, name): + raise CensusError(f"{name} eagerly deletes a sibling descriptor") + + require_code(object_mod, r"\bfn\s+set_object_live_slot_count\b", "central live-slot publication helper") + alloc_body = function_body(codegen_alloc, "emit_instance_alloc_inner") + require_code(alloc_body, r"\bdescriptor_facts_exact\b", "raw-inline exact-facts admission gate") + + transition = function_body(transition_tail, "set_field_by_name_object_tail") + cache_arm_at = transition.find("transition_cache_lookup") + overflow_at = transition.find("overflow_set", cache_arm_at) + if cache_arm_at < 0 or overflow_at < 0: + raise CensusError("missing transition-cache publication arm") + cache_arm = transition[cache_arm_at:overflow_at] + assert_before( + cache_arm, + "set_object_live_slot_count", + "runtime_store_jsvalue_slot", + "transition-cache count before value", + ) + + +def swap_once(source: str, left: str, right: str) -> str: + left_at = source.find(left) + right_at = source.find(right) + if left_at < 0 or right_at < 0: + raise CensusError(f"sabotage fixture missing: {left!r} / {right!r}") + marker_left = "__CENSUS_SWAP_LEFT__" + marker_right = "__CENSUS_SWAP_RIGHT__" + return source.replace(left, marker_left, 1).replace(right, marker_right, 1).replace( + marker_left, right, 1 + ).replace(marker_right, left, 1) + + +def expect_rejected(label: str, check: Callable[[], None]) -> None: + try: + check() + except CensusError: + return + raise CensusError(f"sabotage self-test was not rejected: {label}") + + +def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object]) -> None: + missing_authority = dict(sources) + missing_authority.pop("crates/perry-runtime/src/object/shapes.rs") + expect_rejected( + "missing authority source", + lambda: assert_authority_surfaces(missing_authority), + ) + + raw_mutation = dict(sources) + path = "crates/perry-runtime/src/object/mod.rs" + raw_mutation[path] += "\nunsafe fn census_sabotage(o: *mut ObjectHeader) { (*o).keys_array = core::ptr::null_mut(); }\n" + expect_rejected( + "raw ObjectHeader mutation", + lambda: compare_exact_census(observed_census(raw_mutation), baseline), + ) + + strong_root = dict(sources) + path = "crates/perry-runtime/src/object/shapes.rs" + strong_root[path] = strong_root[path].replace( + "visitor.visit_metadata_usize_slot(&mut addr)", + "visitor.visit_usize_slot(&mut addr)", + 1, + ) + expect_rejected( + "strong descriptor-table root", + lambda: assert_authority_surfaces(strong_root), + ) + + alternate_strong_root = dict(sources) + path = "crates/perry-runtime/src/object/shapes.rs" + alternate_strong_root[path] = alternate_strong_root[path].replace( + "visitor.visit_metadata_usize_slot(&mut addr)", + "{ let moved = unsafe { visitor.visit_usize_raw_slot(&mut addr) }; " + "visitor.visit_metadata_usize_slot(&mut addr); moved }", + 1, + ) + expect_rejected( + "alternate strong raw-slot API plus dead metadata call", + lambda: assert_authority_surfaces(alternate_strong_root), + ) + + inverted_gc = dict(sources) + path = "crates/perry-runtime/src/gc/layout_slot_visit.rs" + inverted_gc[path] = swap_once( + inverted_gc[path], + "child_slots.take_prefix_child_slot()", + "synchronize_live_object_shape_descriptor_after_header_visit(", + ) + expect_rejected("descriptor before header visit", lambda: assert_authority_surfaces(inverted_gc)) + + inverted_publication = dict(sources) + path = "crates/perry-runtime/src/object/shapes.rs" + publication_body = function_body( + inverted_publication[path], "synchronize_object_shape_descriptor" + ) + inverted_body = swap_once( + publication_body, + "shape_descriptor_ensure(keys, key_count, (*obj).field_count)", + "(*obj).parent_class_id = id", + ) + inverted_publication[path] = inverted_publication[path].replace( + publication_body, inverted_body, 1 + ) + expect_rejected( + "ObjectHeader id before descriptor publication", + lambda: assert_authority_surfaces(inverted_publication), + ) + + unscoped_retirement = dict(sources) + path = "crates/perry-runtime/src/object/shapes.rs" + retirement_body = function_body( + unscoped_retirement[path], "retire_key_count_versions" + ) + unscoped_body, substitutions = re.subn( + r"ids_by_keys\s*\.\s*remove\s*\(\s*&keys\s*\)", + "ids_by_keys.get(&keys).cloned()", + retirement_body, + count=1, + ) + if substitutions != 1: + raise CensusError("descriptor retirement sabotage fixture missing") + unscoped_retirement[path] = unscoped_retirement[path].replace( + retirement_body, unscoped_body, 1 + ) + expect_rejected( + "descriptor retirement without keys index", + lambda: assert_authority_surfaces(unscoped_retirement), + ) + + stale_summary = json.loads(json.dumps(baseline)) + stale_summary["summary"]["raw_member_files"] += 1 + expect_rejected( + "stale baseline summary", + lambda: compare_exact_census(observed_census(sources), stale_summary), + ) + + +def compare_exact_census(observed: dict[str, object], baseline: dict[str, object]) -> None: + for key in ( + "raw_member_callsite_multiset", + "codegen_object_header_size_callsite_multiset", + "summary", + ): + actual = observed.get(key) + expected = baseline.get(key) + if actual != expected: + if key == "summary": + raise CensusError( + f"exact shape census summary changed; actual={actual}, expected={expected}" + ) + actual_counter = Counter(actual or {}) + expected_counter = Counter(expected or {}) + added = list((actual_counter - expected_counter).items())[:8] + removed = list((expected_counter - actual_counter).items())[:8] + raise CensusError( + f"exact callsite census changed for {key}; added={added}, removed={removed}" + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--emit-baseline", + action="store_true", + help="print the current exact multiset for reviewed baseline refresh", + ) + args = parser.parse_args() + run_literal_lexer_selftest() + sources = rust_sources() + observed = observed_census(sources) + if args.emit_baseline: + print(json.dumps(observed, indent=2, sort_keys=True)) + return + baseline = json.loads(BASELINE_PATH.read_text(encoding="utf-8")) + compare_exact_census(observed, baseline) + assert_authority_surfaces(sources) + run_sabotage_selftests(sources, baseline) + print(json.dumps(observed["summary"], indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/scripts/shape_descriptor_census_baseline.json b/scripts/shape_descriptor_census_baseline.json new file mode 100644 index 0000000000..4e44c1e72d --- /dev/null +++ b/scripts/shape_descriptor_census_baseline.json @@ -0,0 +1,368 @@ +{ + "codegen_object_header_size_callsite_multiset": { + "crates/perry-codegen/src/codegen/artifacts.rs|crate::target_layout::object_header_size_bytes(target_triple),": 1, + "crates/perry-codegen/src/expr/element_shape_guard.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/expr/member_update.rs|let header_skip = crate::target_layout::object_header_size_bytes(": 1, + "crates/perry-codegen/src/expr/property_get.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 3, + "crates/perry-codegen/src/expr/property_get/generic_dispatch.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/expr/property_get/helpers.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 3, + "crates/perry-codegen/src/expr/property_get/helpers.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(": 3, + "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 1, + "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 2, + "crates/perry-codegen/src/expr/proxy_reflect.rs|(crate::target_layout::object_header_size_bytes(ctx.target_triple) / 8).to_string();": 1, + "crates/perry-codegen/src/expr/proxy_reflect.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/lower_call/new.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/lower_call/new_alloc.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple);": 1, + "crates/perry-codegen/src/lower_call/scalar_method.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs|8 + crate::target_layout::object_header_size_bytes( ) + 8 * slots;": 1, + "crates/perry-codegen/src/stmt/loops.rs|let object_header_size = crate::target_layout::object_header_size_bytes(ctx.target_triple);": 1, + "crates/perry-codegen/src/target_layout.rs|assert_eq!(object_header_size_bytes( ), 24);": 2, + "crates/perry-codegen/src/target_layout.rs|assert_eq!(object_header_size_bytes( ), 32);": 4, + "crates/perry-codegen/src/target_layout.rs|let total = 8 + object_header_size_bytes(triple) + 8 * INLINE_SLOT_FLOOR;": 1, + "crates/perry-codegen/src/target_layout.rs|pub fn object_header_size_bytes(target_triple: &str) -> u64 {": 1 + }, + "raw_member_callsite_multiset": { + "crates/perry-codegen/src/expr/property_get/generic_dispatch.rs|field_count|declaration|fn emit_slot_in_bounds(ctx: &mut FnCtx<'_>, slot: &str, field_count: &str) -> String {": 1, + "crates/perry-codegen/src/lower_call/typed_shape_init.rs|field_count|declaration|field_count: u32,": 1, + "crates/perry-codegen/tests/native_proof_regressions.rs|field_count|declaration|let loop_body = |field_count: usize| {": 1, + "crates/perry-ext-events/src/lib.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader;": 1, + "crates/perry-ext-ws/src/lib.rs|field_count|access|let n = (*ptr).field_count;": 1, + "crates/perry-ffi/src/jsvalue.rs|field_count|declaration|field_count: u32,": 1, + "crates/perry-ffi/src/jsvalue.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader;": 1, + "crates/perry-ffi/src/jsvalue.rs|field_count|declaration|fn js_object_alloc_null_proto(class_id: u32, field_count: u32) -> *mut ObjectHeader;": 1, + "crates/perry-ffi/src/jsvalue.rs|keys_array|declaration|fn js_object_set_keys(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader);": 1, + "crates/perry-ffi/src/types.rs|field_count|declaration|pub field_count: u32,": 1, + "crates/perry-ffi/src/types.rs|keys_array|declaration|pub keys_array: *mut ArrayHeader,": 1, + "crates/perry-ffi/src/types.rs|object_type|declaration|pub object_type: u32,": 1, + "crates/perry-runtime/src/array/element_shape.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR {": 1, + "crates/perry-runtime/src/builtins/console.rs|keys_array|access|let keys_array = (*obj_ptr).keys_array;": 2, + "crates/perry-runtime/src/builtins/formatting.rs|keys_array|access|let _keys_array = (*obj_ptr).keys_array;": 1, + "crates/perry-runtime/src/builtins/formatting.rs|keys_array|access|let keys_array = (*obj_ptr).keys_array;": 2, + "crates/perry-runtime/src/builtins/formatting/util_format.rs|field_count|access|let num_fields = (*obj).field_count;": 1, + "crates/perry-runtime/src/builtins/formatting/util_format.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 1, + "crates/perry-runtime/src/builtins/globals.rs|field_count|access|for i in 0..field_count as usize {": 1, + "crates/perry-runtime/src/builtins/globals.rs|field_count|access|if key_count > (*src_obj).field_count as usize {": 1, + "crates/perry-runtime/src/builtins/globals.rs|field_count|access|let field_count = (*cloned_obj).field_count;": 1, + "crates/perry-runtime/src/builtins/globals.rs|keys_array|access|let keys_now = (*src_now).keys_array;": 1, + "crates/perry-runtime/src/builtins/globals.rs|keys_array|access|let src_keys = (*src_obj).keys_array;": 1, + "crates/perry-runtime/src/builtins/table.rs|keys_array|access|let keys_array = (*obj_ptr).keys_array;": 1, + "crates/perry-runtime/src/child_process/v8_serde.rs|field_count|access|let num_fields = (*obj).field_count;": 1, + "crates/perry-runtime/src/child_process/v8_serde.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 1, + "crates/perry-runtime/src/cluster.rs|field_count|declaration|fn alloc_object_value(field_count: u32) -> f64 {": 1, + "crates/perry-runtime/src/dyn_eval/env.rs|field_count|access|let alloc_limit = std::cmp::max((*o).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, + "crates/perry-runtime/src/dyn_eval/env.rs|keys_array|access|let keys = (*o).keys_array;": 1, + "crates/perry-runtime/src/error.rs|object_type|access|(*ptr).object_type = OBJECT_TYPE_ERROR;": 1, + "crates/perry-runtime/src/error.rs|object_type|declaration|pub object_type: u32,": 1, + "crates/perry-runtime/src/fs/dirent.rs|keys_array|access|let keys = (*obj_ptr).keys_array;": 1, + "crates/perry-runtime/src/gc/heap_snapshot.rs|field_count|access|let fc = unsafe { (*obj).field_count } as usize;": 1, + "crates/perry-runtime/src/gc/heap_snapshot.rs|keys_array|access|let keys_bits = (*obj).keys_array as u64;": 1, + "crates/perry-runtime/src/gc/layout.rs|field_count|access|if slot_index < (*object).field_count as usize {": 1, + "crates/perry-runtime/src/gc/layout.rs|field_count|access|let field_count = (*(user_ptr as *const crate::object::ObjectHeader)).field_count as usize;": 1, + "crates/perry-runtime/src/gc/layout.rs|field_count|access|let object_slot_count = (*obj_header).field_count as usize;": 1, + "crates/perry-runtime/src/gc/layout.rs|keys_array|access|(*(user_ptr as *const crate::object::ObjectHeader)).keys_array as usize": 1, + "crates/perry-runtime/src/gc/layout.rs|keys_array|access|let keys = (*obj_header).keys_array as usize;": 1, + "crates/perry-runtime/src/gc/layout_slot_visit.rs|field_count|access|let live_inline_slot_count = (*obj).field_count;": 1, + "crates/perry-runtime/src/gc/layout_slot_visit.rs|keys_array|access|let new_keys = (*obj).keys_array as u64;": 1, + "crates/perry-runtime/src/gc/layout_slot_visit.rs|keys_array|access|let old_keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/gc/tests/alloc.rs|object_type|declaration|object_type: crate::error::OBJECT_TYPE_ERROR,": 1, + "crates/perry-runtime/src/gc/tests/barrier.rs|field_count|access|for i in 0..field_count as usize {": 2, + "crates/perry-runtime/src/gc/tests/barrier.rs|field_count|declaration|unsafe fn field_index_not_on_last_page(fields: *mut u64, field_count: u32) -> usize {": 1, + "crates/perry-runtime/src/gc/tests/barrier.rs|field_count|declaration|unsafe fn field_indices_on_distinct_pages(fields: *mut u64, field_count: u32) -> (usize, usize) {": 1, + "crates/perry-runtime/src/gc/tests/copying.rs|keys_array|access|let keys = (*obj_after).keys_array;": 1, + "crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs|field_count|access|unsafe { (*obj).field_count },": 2, + "crates/perry-runtime/src/gc/tests/cycle_state.rs|field_count|access|(*child).field_count = 0;": 1, + "crates/perry-runtime/src/gc/tests/cycle_state.rs|keys_array|access|(*child).keys_array = std::ptr::null_mut();": 1, + "crates/perry-runtime/src/gc/tests/cycle_state.rs|object_type|access|(*child).object_type = crate::error::OBJECT_TYPE_REGULAR;": 1, + "crates/perry-runtime/src/gc/tests/cycle_state.rs|object_type|access|(*obj).object_type,": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|field_count|access|(*obj).field_count = 0;": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|(*a).keys_array = keys;": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|(*b).keys_array = keys;": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|(*obj).keys_array = std::ptr::null_mut();": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|(*owner).keys_array = corrupt_keys as *mut crate::array::ArrayHeader;": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|(*owner).keys_array = keys;": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|(*owner).keys_array = old_keys;": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|assert_eq!((*a_after).keys_array, (*b_after).keys_array);": 2, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|assert_eq!(descriptor.keys, (*a_after).keys_array as u64);": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|assert_ne!((*a_after).keys_array as usize, old_keys);": 1, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|keys_array|access|let header_keys_slot = unsafe { std::ptr::addr_of_mut!((*owner).keys_array) as *mut u64 };": 2, + "crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs|object_type|access|(*obj).object_type = 1;": 1, + "crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs|field_count|access|assert_eq!((*obj).field_count, 2);": 1, + "crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs|keys_array|access|(*first).keys_array,": 1, + "crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs|keys_array|access|(*second).keys_array,": 1, + "crates/perry-runtime/src/gc/tests/runtime_roots/json_shape_template.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs|keys_array|access|assert!(!(*obj_after).keys_array.is_null());": 1, + "crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs|keys_array|access|let key_value = crate::array::js_array_get((*obj_after).keys_array, 0).bits();": 1, + "crates/perry-runtime/src/gc/tests/support.rs|field_count|access|(*obj).field_count = field_count;": 2, + "crates/perry-runtime/src/gc/tests/support.rs|field_count|access|for i in 0..field_count as usize {": 2, + "crates/perry-runtime/src/gc/tests/support.rs|field_count|declaration|field_count: u32,": 2, + "crates/perry-runtime/src/gc/tests/support.rs|keys_array|access|(*obj).keys_array = std::ptr::null_mut();": 2, + "crates/perry-runtime/src/gc/tests/support.rs|object_type|access|(*obj).object_type = 1;": 2, + "crates/perry-runtime/src/gc/tests/support.rs|object_type|declaration|object_type: crate::error::OBJECT_TYPE_ERROR,": 1, + "crates/perry-runtime/src/json/mod.rs|field_count|access|(value, (*obj).field_count, (*(*obj).keys_array).length)": 1, + "crates/perry-runtime/src/json/mod.rs|field_count|access|assert!((*obj).field_count >= (*(*obj).keys_array).length);": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|(value, (*obj).field_count, (*(*obj).keys_array).length)": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|assert!((*obj).field_count >= (*(*obj).keys_array).length);": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|assert!(unsafe { (*empty).keys_array.is_null() });": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|assert_eq!((*(*nested).keys_array).length, 1);": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|assert_eq!((*(*obj).keys_array).length, 2);": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|return entry.keys_array;": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|access|visitor.visit_raw_mut_ptr_slot(&mut entry.keys_array);": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|declaration|keys_array: arr,": 1, + "crates/perry-runtime/src/json/mod.rs|keys_array|declaration|pub(crate) keys_array: *mut crate::ArrayHeader,": 1, + "crates/perry-runtime/src/json/parse_api.rs|field_count|declaration|field_count: u32,": 2, + "crates/perry-runtime/src/json/parser.rs|field_count|access|shape.field_count,": 1, + "crates/perry-runtime/src/json/parser.rs|field_count|access|std::cmp::max(shape.field_count as usize, crate::object::INLINE_SLOT_FLOOR);": 1, + "crates/perry-runtime/src/json/parser.rs|field_count|declaration|pub(crate) field_count: u32,": 1, + "crates/perry-runtime/src/json/parser.rs|keys_array|access|shape.keys_array,": 1, + "crates/perry-runtime/src/json/parser.rs|keys_array|declaration|pub(crate) keys_array: *mut crate::array::ArrayHeader,": 1, + "crates/perry-runtime/src/json/replacer.rs|field_count|access|let num_fields = (*obj).field_count;": 3, + "crates/perry-runtime/src/json/stringify.rs|field_count|access|let field_count = (*obj).field_count;": 1, + "crates/perry-runtime/src/json/stringify.rs|field_count|access|let fields = (*obj).field_count as usize;": 1, + "crates/perry-runtime/src/json/stringify.rs|field_count|access|let num_fields = (*obj).field_count;": 1, + "crates/perry-runtime/src/json/stringify.rs|keys_array|access|if (*(ptr as *const crate::ObjectHeader)).keys_array.is_null() {": 1, + "crates/perry-runtime/src/json/stringify.rs|keys_array|access|let keys = (*(ptr as *const crate::ObjectHeader)).keys_array;": 1, + "crates/perry-runtime/src/json/stringify.rs|keys_array|access|let keys = (*obj).keys_array as *const crate::ArrayHeader;": 1, + "crates/perry-runtime/src/json/stringify.rs|keys_array|access|let keys_arr = (*cur_obj()).keys_array;": 1, + "crates/perry-runtime/src/json/stringify.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 2, + "crates/perry-runtime/src/json/stringify.rs|keys_array|access|let potential_keys_ptr = (*obj).keys_array as u64;": 1, + "crates/perry-runtime/src/json/stringify_shape_template.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32)": 1, + "crates/perry-runtime/src/json/stringify_shape_template.rs|keys_array|access|if (*obj).keys_array != template.keys_arr.get() {": 1, + "crates/perry-runtime/src/json/stringify_shape_template.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 2, + "crates/perry-runtime/src/json/stringify_tojson_probe.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/json/stringify_tojson_probe.rs|keys_array|access|let keys = (*proto).keys_array;": 1, + "crates/perry-runtime/src/json_tape_tests.rs|field_count|access|(*nested).field_count,": 1, + "crates/perry-runtime/src/json_tape_tests.rs|field_count|access|(*object).field_count,": 2, + "crates/perry-runtime/src/native_abi.rs|object_type|access|let is_regular = (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR;": 1, + "crates/perry-runtime/src/navigator.rs|field_count|declaration|let field_count: u32 = 6;": 1, + "crates/perry-runtime/src/node_stream_json.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/node_stream_readwrite.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|(*new_ptr).field_count = 0;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|(*new_ptr).field_count = src_field_count;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|(*obj_ptr).field_count = field_count;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|(*ptr).field_count = field_count;": 5, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|(*ptr).field_count = logical_field_count as u32;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|let src_field_count = (*src).field_count as usize;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|access|let src_field_count = (*src_ptr).field_count;": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|declaration|field_count: u32,": 8, + "crates/perry-runtime/src/object/alloc.rs|field_count|declaration|fn remember_class_keys_array(class_id: u32, field_count: u32, keys_array: *mut ArrayHeader) {": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|declaration|pub extern fn js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader {": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|declaration|pub extern fn js_object_alloc_fast(class_id: u32, field_count: u32) -> *mut ObjectHeader {": 1, + "crates/perry-runtime/src/object/alloc.rs|field_count|declaration|pub extern fn js_object_alloc_null_proto(class_id: u32, field_count: u32) -> *mut ObjectHeader {": 1, + "crates/perry-runtime/src/object/alloc.rs|keys_array|access|(*new_ptr).keys_array = ptr::null_mut();": 2, + "crates/perry-runtime/src/object/alloc.rs|keys_array|access|(*ptr).keys_array = ptr::null_mut();": 3, + "crates/perry-runtime/src/object/alloc.rs|keys_array|access|let src_keys = (*src).keys_array;": 2, + "crates/perry-runtime/src/object/alloc.rs|keys_array|access|let src_keys_arr = (*src_ptr).keys_array;": 1, + "crates/perry-runtime/src/object/alloc.rs|keys_array|declaration|fn remember_class_keys_array(class_id: u32, field_count: u32, keys_array: *mut ArrayHeader) {": 1, + "crates/perry-runtime/src/object/alloc.rs|keys_array|declaration|keys_array: *mut ArrayHeader,": 3, + "crates/perry-runtime/src/object/alloc.rs|object_type|access|(*new_ptr).object_type = crate::error::OBJECT_TYPE_REGULAR;": 2, + "crates/perry-runtime/src/object/alloc.rs|object_type|access|(*obj_ptr).object_type = crate::error::OBJECT_TYPE_REGULAR;": 1, + "crates/perry-runtime/src/object/alloc.rs|object_type|access|(*ptr).object_type = crate::error::OBJECT_TYPE_REGULAR;": 6, + "crates/perry-runtime/src/object/arguments.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 2, + "crates/perry-runtime/src/object/arguments.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/object/class_registry/parent_static.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/class_registry/parent_static.rs|object_type|access|&& (*(ptr as *const ObjectHeader)).object_type == crate::error::OBJECT_TYPE_CLASS": 1, + "crates/perry-runtime/src/object/class_registry/parent_static.rs|object_type|access|(*(obj as *mut ObjectHeader)).object_type = crate::error::OBJECT_TYPE_CLASS;": 1, + "crates/perry-runtime/src/object/delete_rest.rs|field_count|access|assert_eq!(descriptor.live_inline_slot_count, (*obj).field_count);": 2, + "crates/perry-runtime/src/object/delete_rest.rs|field_count|access|let field_count = (*obj).field_count;": 1, + "crates/perry-runtime/src/object/delete_rest.rs|keys_array|access|(*obj).keys_array,": 1, + "crates/perry-runtime/src/object/delete_rest.rs|keys_array|access|assert_eq!(descriptor.keys, (*obj).keys_array as u64);": 2, + "crates/perry-runtime/src/object/delete_rest.rs|keys_array|access|crate::object::shapes::shape_drop((*obj).keys_array);": 1, + "crates/perry-runtime/src/object/delete_rest.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/delete_rest.rs|keys_array|access|let keys = (*src).keys_array;": 1, + "crates/perry-runtime/src/object/delete_rest.rs|keys_array|access|let keys_before = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/descriptor_state.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/descriptors.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/object/field_get_set/accessors.rs|field_count|access|(*obj).field_count": 1, + "crates/perry-runtime/src/object/field_get_set/accessors.rs|field_count|access|let fc = (*obj).field_count;": 1, + "crates/perry-runtime/src/object/field_get_set/accessors.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 1, + "crates/perry-runtime/src/object/field_get_set/accessors.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/field_get_set/enumeration.rs|field_count|access|(*obj).field_count as usize": 2, + "crates/perry-runtime/src/object/field_get_set/enumeration.rs|keys_array|access|let keys = (*obj).keys_array;": 3, + "crates/perry-runtime/src/object/field_get_set/field_ops.rs|field_count|access|if field_index >= (*obj).field_count {": 1, + "crates/perry-runtime/src/object/field_get_set/field_ops.rs|field_count|access|let stored_field_count = (*obj).field_count;": 1, + "crates/perry-runtime/src/object/field_get_set/field_ops.rs|keys_array|declaration|pub extern fn js_object_set_keys(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader) {": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs|field_count|access|(*o).field_count,": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs|keys_array|access|let keys = (*o).keys_array;": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs|field_count|access|let _field_count = (*obj).field_count as usize;": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs|object_type|access|if (*obj).object_type == crate::error::OBJECT_TYPE_CLASS && (*obj).class_id != 0 {": 1, + "crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs|object_type|access|let object_type = (*obj).object_type;": 1, + "crates/perry-runtime/src/object/field_get_set/has_property.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/field_get_set/ic_miss.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 1, + "crates/perry-runtime/src/object/field_get_set/ic_miss.rs|keys_array|access|(*obj).keys_array as u64": 1, + "crates/perry-runtime/src/object/field_get_set/ic_miss.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/object/field_get_set/ic_miss.rs|object_type|access|let is_regular = is_object && (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR;": 1, + "crates/perry-runtime/src/object/field_set_by_name.rs|field_count|access|(*o).field_count,": 1, + "crates/perry-runtime/src/object/field_set_by_name.rs|field_count|access|if slot_idx >= (*o).field_count {": 1, + "crates/perry-runtime/src/object/field_set_by_name.rs|keys_array|access|let keys = (*o).keys_array;": 1, + "crates/perry-runtime/src/object/field_set_by_name.rs|object_type|access|if (*o).object_type == crate::error::OBJECT_TYPE_REGULAR": 1, + "crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs|field_count|access|if idx >= (*obj).field_count {": 1, + "crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs|field_count|access|if slot_idx >= (*obj).field_count {": 1, + "crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 2, + "crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR": 1, + "crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs|object_type|access||| (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR": 1, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|field_count|access|if (*obj).field_count == 0 {": 1, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|field_count|access|if new_index as u32 >= (*obj).field_count {": 2, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|field_count|access|if slot_idx >= (*obj).field_count {": 1, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32)": 1, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 1, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|keys_array|access|(*obj).keys_array,": 2, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|keys_array|access|let keys = (*obj).keys_array;": 3, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|object_type|access|&& (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR": 2, + "crates/perry-runtime/src/object/field_set_by_name/tail.rs|object_type|access|let object_type = (*obj).object_type;": 1, + "crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_CLASS": 1, + "crates/perry-runtime/src/object/map_set_subclass.rs|field_count|access|assert_eq!(unsafe { (*obj).field_count }, 3);": 1, + "crates/perry-runtime/src/object/map_set_subclass.rs|object_type|access|assert_eq!(unsafe { (*obj).object_type }, OBJECT_TYPE_REGULAR);": 5, + "crates/perry-runtime/src/object/mod.rs|field_count|access|(*obj).field_count = field_count;": 1, + "crates/perry-runtime/src/object/mod.rs|field_count|access|if (*obj).field_count != field_count {": 1, + "crates/perry-runtime/src/object/mod.rs|field_count|access|let field_count = (*obj).field_count as usize;": 1, + "crates/perry-runtime/src/object/mod.rs|field_count|declaration|field_count: 0,": 1, + "crates/perry-runtime/src/object/mod.rs|field_count|declaration|field_count: u32,": 1, + "crates/perry-runtime/src/object/mod.rs|field_count|declaration|pub field_count: u32,": 1, + "crates/perry-runtime/src/object/mod.rs|field_count|declaration|pub(super) unsafe fn set_object_live_slot_count(obj: *mut ObjectHeader, field_count: u32) {": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|&(*obj).keys_array as *const _ as usize,": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|(*obj).keys_array = keys_array;": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|Some(&mut (*obj).keys_array as *mut _ as *mut u64)": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|crate::gc::runtime_store_root_raw_mut_ptr_slot(&mut entry.keys_array, keys_array);": 2, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|if (*obj).keys_array != keys_array {": 2, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|if obj.is_null() || (*obj).keys_array.is_null() {": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|let inline = unsafe { (*st.object_hot.shape_inline_cache.get())[slot].keys_array as usize };": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|return (entry.keys_array, entry.runtime_shape_id);": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|access|visitor.visit_raw_mut_ptr_slot(&mut entry.keys_array);": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|fn shape_cache_insert(shape_id: u32, keys_array: *mut ArrayHeader) {": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|keys_array: *mut ArrayHeader,": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|keys_array: 0,": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|keys_array: std::ptr::null_mut(),": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|keys_array: u64,": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|pub keys_array: *mut ArrayHeader,": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|pub(crate) fn test_seed_shape_cache_root(shape_id: u32, keys_array: *mut ArrayHeader) {": 1, + "crates/perry-runtime/src/object/mod.rs|keys_array|declaration|unsafe fn set_object_keys_array(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader) {": 1, + "crates/perry-runtime/src/object/mod.rs|object_type|declaration|object_type: 1,": 1, + "crates/perry-runtime/src/object/mod.rs|object_type|declaration|object_type: u32,": 1, + "crates/perry-runtime/src/object/mod.rs|object_type|declaration|pub object_type: u32,": 1, + "crates/perry-runtime/src/object/namespace_create.rs|keys_array|access|(*obj).keys_array = 0x2800_0203usize as *mut _;": 1, + "crates/perry-runtime/src/object/native_call_method.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/object/native_call_method.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR {": 1, + "crates/perry-runtime/src/object/native_call_method.rs|object_type|access|let object_type = (*obj).object_type;": 1, + "crates/perry-runtime/src/object/native_call_method/collection_methods.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/native_call_method/handle_methods.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/object_ops.rs|keys_array|declaration|pub(crate) use keys_array::{": 1, + "crates/perry-runtime/src/object/object_ops/accessors.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 1, + "crates/perry-runtime/src/object/object_ops/accessors.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs|keys_array|access|let keys = (*(ptr as *const ObjectHeader)).keys_array;": 1, + "crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|field_count|access|if (*obj).field_count == 0 {": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|field_count|access|if new_index < inline_capacity && new_index >= (*obj).field_count {": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|keys_array|access|(*obj).keys_array,": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|keys_array|access|assert_eq!((*first).keys_array, (*sibling).keys_array);": 2, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|keys_array|access|assert_eq!(first_descriptor.keys, (*first).keys_array as u64);": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|keys_array|access|assert_eq!(sibling_descriptor.keys, (*sibling).keys_array as u64);": 1, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|keys_array|access|assert_ne!((*first).keys_array, (*sibling).keys_array);": 2, + "crates/perry-runtime/src/object/object_ops/keys_array.rs|keys_array|access|let keys = (*obj).keys_array;": 4, + "crates/perry-runtime/src/object/object_ops_frozen.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/object/reflect_support.rs|keys_array|access|let keys_handle = scope.root_raw_mut_ptr((*obj).keys_array);": 1, + "crates/perry-runtime/src/object/shapes.rs|field_count|access|&& d.live_inline_slot_count == (*obj).field_count": 1, + "crates/perry-runtime/src/object/shapes.rs|field_count|access|assert_eq!(descriptor.live_inline_slot_count, (*obj).field_count);": 1, + "crates/perry-runtime/src/object/shapes.rs|field_count|access|let Ok(id) = shape_descriptor_ensure(keys, key_count, (*obj).field_count) else {": 2, + "crates/perry-runtime/src/object/shapes.rs|field_count|declaration|field_count: 2,": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|(*a).keys_array,": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|(*b).keys_array,": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|assert_eq!((*b).keys_array, shared_keys);": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|assert_eq!(descriptor.keys, (*obj).keys_array as u64);": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|assert_eq!(shared_keys, (*b).keys_array);": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|assert_eq!(transitioned.keys, (*a).keys_array as u64);": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|assert_ne!((*a).keys_array, shared_keys);": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|crate::array::js_array_length((*obj).keys_array)": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|let keys = (*obj).keys_array as usize;": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|let shared_keys = (*a).keys_array;": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|access|unsafe { (*obj).keys_array },": 1, + "crates/perry-runtime/src/object/shapes.rs|keys_array|declaration|keys_array: keys as *mut ArrayHeader,": 1, + "crates/perry-runtime/src/object/shapes.rs|object_type|declaration|object_type: 1,": 1, + "crates/perry-runtime/src/object/spill.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, + "crates/perry-runtime/src/object/spill.rs|field_count|declaration|pub(crate) fn reserve_object_spill(obj_ptr: usize, field_count: u32) {": 1, + "crates/perry-runtime/src/os.rs|field_count|declaration|let field_count: u32 = 14;": 1, + "crates/perry-runtime/src/perf_hooks.rs|keys_array|access|let keys_ptr = (*obj).keys_array as usize;": 1, + "crates/perry-runtime/src/perf_hooks.rs|keys_array|access|recorded != 0 && (*obj).keys_array as usize == recorded": 1, + "crates/perry-runtime/src/pointer_event.rs|field_count|declaration|let field_count: u32 = 4;": 1, + "crates/perry-runtime/src/promise/then_probe.rs|keys_array|access|let keys = (*obj).keys_array;": 2, + "crates/perry-runtime/src/promise/then_probe.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR {": 1, + "crates/perry-runtime/src/proxy.rs|object_type|access|&& (*(addr as *const crate::ObjectHeader)).object_type": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|(*first).field_count,": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|let alloc_limit = std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|object_array_numeric_write_slots(array, &keys[..field_count as usize], receiver_count)": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|slots[..field_count as usize]": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) as usize;": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|unsafe { std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32) };": 1, + "crates/perry-runtime/src/proxy/put_value.rs|field_count|declaration|field_count: u32,": 1, + "crates/perry-runtime/src/proxy/put_value.rs|keys_array|access|(*obj).keys_array as u64": 1, + "crates/perry-runtime/src/proxy/put_value.rs|keys_array|access|let keys = (*obj).keys_array;": 3, + "crates/perry-runtime/src/proxy/put_value.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR": 4, + "crates/perry-runtime/src/safe_area.rs|field_count|declaration|let field_count: u32 = 4;": 1, + "crates/perry-runtime/src/thread.rs|field_count|access|for i in 0..field_count {": 1, + "crates/perry-runtime/src/thread.rs|field_count|access|let field_count = (*obj).field_count as usize;": 1, + "crates/perry-runtime/src/thread.rs|keys_array|access|let keys = if !(*obj).keys_array.is_null() {": 1, + "crates/perry-runtime/src/thread.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 1, + "crates/perry-runtime/src/typed_feedback.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, + "crates/perry-runtime/src/typed_feedback.rs|keys_array|access|(*ptr).keys_array as usize": 2, + "crates/perry-runtime/src/typed_feedback.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/typed_feedback.rs|keys_array|access|let keys = (*ptr).keys_array;": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|field_count|access|&& expected_field_index < (*obj).field_count": 2, + "crates/perry-runtime/src/typed_feedback/guards.rs|field_count|access|&& expected_field_index < (*obj).field_count;": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|keys_array|access|&& std::ptr::eq((*obj).keys_array as *const ArrayHeader, expected_keys)": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|keys_array|access|*out_keys = (*obj).keys_array as u64;": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|keys_array|access|let keys = (*obj).keys_array;": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|keys_array|access|let shape_addr = (*obj).keys_array as usize;": 2, + "crates/perry-runtime/src/typed_feedback/guards.rs|keys_array|access|return ((*obj).keys_array as usize, (*obj).class_id, gc_type, false);": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|keys_array|access||| !std::ptr::eq((*obj).keys_array, expected_keys)": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|object_type|access|if (*obj).object_type != crate::error::OBJECT_TYPE_REGULAR {": 2, + "crates/perry-runtime/src/typed_feedback/guards.rs|object_type|access|let shape_ok = (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR": 1, + "crates/perry-runtime/src/typed_feedback/guards.rs|object_type|access|let valid = (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR": 1, + "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|assert_ne!(unsafe { (*obj).keys_array }, expected_keys);": 1, + "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|let keys = unsafe { (*obj).keys_array };": 1, + "crates/perry-runtime/src/url/search_params.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 2, + "crates/perry-runtime/src/url/search_params.rs|keys_array|access|let keys_arr = (*params).keys_array;": 1, + "crates/perry-runtime/src/url/url_class.rs|field_count|access|if !is_gc_object_header(url) || (*url).class_id != 0 || (*url).field_count < URL_FIELD_COUNT": 1, + "crates/perry-runtime/src/weakref.rs|field_count|access|((*obj).field_count > 0 && slot == object_field_slot(obj, 0))": 1, + "crates/perry-runtime/src/weakref.rs|field_count|access|(*obj).field_count > 0 && slot == object_field_slot(obj, 0)": 1, + "crates/perry-runtime/src/weakref.rs|field_count|access||| ((*obj).field_count > 1 && slot == object_field_slot(obj, 1))": 1, + "crates/perry-stdlib/src/streams.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader {": 1, + "crates/perry-stdlib/src/streams.rs|field_count|declaration|fn provider_js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader;": 1, + "crates/perry-stdlib/src/worker_threads.rs|field_count|access|(*object).field_count": 1, + "crates/perry-stdlib/src/worker_threads.rs|field_count|access|(0..field_count).any(|index| {": 1, + "crates/perry-stdlib/src/worker_threads.rs|keys_array|access|if (*object).keys_array.is_null() {": 1, + "crates/perry-stdlib/src/worker_threads.rs|keys_array|access|perry_runtime::array::js_array_length((*object).keys_array)": 1, + "crates/perry-ui-android/src/drag_drop.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-android/src/json.rs|field_count|access|let field_count = (*obj).field_count;": 1, + "crates/perry-ui-android/src/json.rs|field_count|access|let fields = (*obj).field_count as usize;": 1, + "crates/perry-ui-android/src/json.rs|field_count|access|let num_fields = (*obj).field_count;": 1, + "crates/perry-ui-android/src/json.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 2, + "crates/perry-ui-android/src/json.rs|keys_array|access|let potential_keys_ptr = (*obj).keys_array as u64;": 1, + "crates/perry-ui-android/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-gtk4/src/drag_drop.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-gtk4/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-ios/src/drag_drop.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-ios/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-macos/src/drag_drop.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-macos/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-tvos/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-visionos/src/drag_drop.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-visionos/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1, + "crates/perry-ui-windows/src/drag_drop.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32)": 1, + "crates/perry-ui-windows/src/widgets/canvas.rs|field_count|declaration|fn js_object_alloc(class_id: u32, field_count: u32) -> *mut c_void;": 1 + }, + "summary": { + "codegen_object_header_size_sites": 32, + "raw_member_files": 104, + "raw_member_sites": { + "field_count": 165, + "keys_array": 195, + "object_type": 54 + } + } +}