From 5ae70f1a1a1489d981c34d735761a991d595b0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 22:15:10 +0200 Subject: [PATCH 1/4] perf(runtime): drop the prototype-divergence flag; the shape pins the prototype With the prototype a fact of the shape (#11815), every reader of OBJECT_META_FLAG_PROTO_DIVERGED was a cache gate whose key already carries the prototype, so the per-instance flag only duplicated the ShapeId: - store plans (field_set_by_name, its tail, the proxy set path) are keyed by (class id, key, receiver prototype bits); - array-subclass dense layouts (array/subclass.rs, subclass_loop_guard.rs) are keyed by (class id, ShapeId) and validated by the receiver word, which carries the ShapeId; - the defineProperty key-add tail (keys_array.rs) replays ShapeId-keyed transition edges; - `x instanceof ` (instanceof/dynamic_dispatch.rs) now asks whether the receiver has a recorded prototype instead. The flag, its funnel write, the class-object-template set/clear and its tests are deleted; bit 0 of ObjectMeta.flags is free. A runtime-wiring link on a function-constructor instance no longer allocates a meta record. Tests: `new F()` and `Object.create(F.prototype)` receivers share one shape; the dense array-subclass test now asserts the receiver moves to another ShapeId across a prototype override and its own element still reads the same (it asserted the old per-instance decline). Refs #10507 --- changelog.d/PENDING-prototype-link-flags.md | 7 ++++ crates/perry-runtime/src/array/subclass.rs | 28 ++++----------- .../src/array/subclass_loop_guard.rs | 11 ++---- .../perry-runtime/src/array/subclass_tests.rs | 16 +++++++-- .../field_get_set/class_object_template.rs | 8 ++--- .../src/object/field_set_by_name.rs | 1 - .../src/object/field_set_by_name/tail.rs | 9 ++--- .../src/object/instanceof/dynamic_dispatch.rs | 5 +-- crates/perry-runtime/src/object/meta_flags.rs | 4 +-- crates/perry-runtime/src/object/mod.rs | 3 +- .../src/object/object_ops/keys_array.rs | 5 --- .../src/object/prototype_chain.rs | 35 ++++--------------- .../src/object/shapes_prototype_tests.rs | 31 ++++++++++++++++ crates/perry-runtime/src/proxy.rs | 3 -- 14 files changed, 79 insertions(+), 87 deletions(-) create mode 100644 changelog.d/PENDING-prototype-link-flags.md diff --git a/changelog.d/PENDING-prototype-link-flags.md b/changelog.d/PENDING-prototype-link-flags.md new file mode 100644 index 0000000000..72eea54b20 --- /dev/null +++ b/changelog.d/PENDING-prototype-link-flags.md @@ -0,0 +1,7 @@ +The per-object prototype-divergence flag is gone (Refs #10507). Since an +object's prototype is a fact of its shape, the caches that consulted the flag +(store plans, array-subclass dense layouts, the defineProperty key-add path, +`instanceof` against a class object) already key on the shape or the +prototype, so a re-parented object simply meets a different cache entry. A +runtime-wired function-constructor instance no longer allocates a metadata +record. diff --git a/crates/perry-runtime/src/array/subclass.rs b/crates/perry-runtime/src/array/subclass.rs index 5817f46717..1bbf13254c 100644 --- a/crates/perry-runtime/src/array/subclass.rs +++ b/crates/perry-runtime/src/array/subclass.rs @@ -110,19 +110,6 @@ pub(super) struct ValidatedObjectReceiver { pub(super) object_flags: u16, } -/// Read the per-instance prototype-divergence bit after the caller has already -/// proved a live, non-forwarded `GC_TYPE_OBJECT` receiver. -/// -/// The public prototype-chain predicate accepts arbitrary addresses and must -/// re-run buffer/heap/header classification before touching `ObjectHeader`. -/// Dense Array-subclass paths have just completed that proof, so repeating it -/// ahead of every receiver-local layout-cache hit is both redundant and hot. -#[inline(always)] -unsafe fn validated_object_has_prototype_divergence(obj: *const ObjectHeader) -> bool { - let meta = (*obj).meta; - !meta.is_null() && (*meta).flags & crate::object::OBJECT_META_FLAG_PROTO_DIVERGED != 0 -} - #[inline(always)] fn dense_cache_key(class_id: u32, shape_id: u32) -> u64 { ((class_id as u64) << 32) | shape_id as u64 @@ -251,10 +238,9 @@ fn decimal_u32<'a>(mut value: u32, buf: &'a mut [u8; 10]) -> &'a [u8] { /// allocates nothing and keeps no address into the moving heap. unsafe fn build_dense_layout(obj: *const ObjectHeader) -> Option { let class_id = (*obj).class_id; - if class_id == 0 - || !is_array_subclass_class_id(class_id) - || validated_object_has_prototype_divergence(obj) - { + // A receiver moved to another prototype is on another ShapeId (the + // prototype identity is a shape fact), so it cannot meet this layout. + if class_id == 0 || !is_array_subclass_class_id(class_id) { return None; } let shape = crate::object::shapes::object_shape_descriptor(obj)?; @@ -366,11 +352,9 @@ fn validated_object_receiver_for_value(value: f64) -> Option Option { - // This is per receiver, not per ShapeId. A cached layout built before - // Object.setPrototypeOf must not let this object borrow the old proof. - if unsafe { validated_object_has_prototype_divergence(obj) } { - return None; - } + // `Object.setPrototypeOf` moves the receiver to another ShapeId, so a + // layout cached for the old one (owner word or `(class, ShapeId)` key) + // cannot be borrowed afterwards. if let Some(layout) = unsafe { owner_cached_dense_layout(obj) } { return Some(layout); } diff --git a/crates/perry-runtime/src/array/subclass_loop_guard.rs b/crates/perry-runtime/src/array/subclass_loop_guard.rs index 650226efb4..dcf3308bc7 100644 --- a/crates/perry-runtime/src/array/subclass_loop_guard.rs +++ b/crates/perry-runtime/src/array/subclass_loop_guard.rs @@ -479,9 +479,8 @@ pub extern "C" fn js_packed_arraylike_loop_revalidate_live( } let object = raw.cast::(); let current_receiver_word = unsafe { ptr::read_unaligned(raw.cast::()) }; - if current_receiver_word != receiver_word - || crate::object::prototype_chain::object_has_prototype_divergence(raw as usize) - { + // The receiver word carries the ShapeId, which pins the prototype. + if current_receiver_word != receiver_word { return 0; } let dense_prefix_len = packed_bounds as u32; @@ -625,12 +624,6 @@ fn revalidate_admitted_subclass_live( { return 0; } - let meta = unsafe { (*object).meta }; - if !meta.is_null() - && unsafe { (*meta).flags } & crate::object::OBJECT_META_FLAG_PROTO_DIVERGED != 0 - { - return 0; - } let dense_prefix_len = packed_bounds as u32; let live_inline_slots = (packed_bounds >> 32) as u32; if admitted_bound > dense_prefix_len { diff --git a/crates/perry-runtime/src/array/subclass_tests.rs b/crates/perry-runtime/src/array/subclass_tests.rs index c79a7f8d7f..23faa2c431 100644 --- a/crates/perry-runtime/src/array/subclass_tests.rs +++ b/crates/perry-runtime/src/array/subclass_tests.rs @@ -251,7 +251,7 @@ fn dense_array_subclass_reads_slots_until_its_shape_changes() { } #[test] -fn dense_array_subclass_cache_declines_a_per_instance_prototype_override() { +fn dense_array_subclass_layout_follows_the_shape_across_a_prototype_override() { // Pins the shape-carried representation: the elements store is the // default, and this test is about the property-shape machinery. let _representation = @@ -265,11 +265,21 @@ fn dense_array_subclass_cache_declines_a_per_instance_prototype_override() { crate::object::js_object_set_index_polymorphic(obj as i64, 0.0, 11.0); assert_eq!(array_subclass_fast_index_get(receiver, 0), Some(11.0)); + let before = unsafe { (*obj).parent_class_id }; crate::object::prototype_chain::object_set_user_prototype(obj as usize, crate::value::TAG_NULL); + // The prototype is a fact of the shape: the receiver moved to another + // ShapeId, so no layout cached for the old one (owner word or + // `(class, ShapeId)` key) can answer for it. What the new shape's layout + // answers is the receiver's OWN element, which no prototype affects. + assert_ne!( + unsafe { (*obj).parent_class_id }, + before, + "a prototype change must move the receiver to another ShapeId" + ); assert_eq!( array_subclass_fast_index_get(receiver, 0), - None, - "a receiver-local dense-layout record must not survive prototype divergence" + Some(11.0), + "an own dense element reads the same on any prototype" ); } diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs index 8dacc0d911..9202c437de 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs @@ -715,8 +715,7 @@ pub(crate) unsafe fn prototype_from_template( owner.to_bits(), ); (*meta).prototype = parent_bits; - (*meta).flags |= crate::object::OBJECT_META_FLAG_PROTO_DIVERGED - | crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO; + (*meta).flags |= crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO; crate::gc::runtime_write_barrier_slot( meta as usize, &(*meta).prototype as *const u64 as usize, @@ -785,10 +784,7 @@ pub(crate) unsafe fn record_prototype_template( || meta.is_null() || (*meta).prototype != parent_proto || (*meta).private_evaluation_brand != owner - || (*meta).flags - & !(crate::object::OBJECT_META_FLAG_PROTO_DIVERGED - | crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO) - != 0 + || (*meta).flags & !crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO != 0 || (*meta).spill != 0 || u32::try_from(count).is_err() || cell.proto_fills_base() + 2 * count > cell.len() 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 e15a773e73..2bc8867bdc 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -204,7 +204,6 @@ pub extern "C" fn js_object_set_field_by_name( // a cached edge here could hand it a foreign slot // index below the floor. && crate::object::reserved_slot_floor_for_class_id(class_id) == 0 - && !super::prototype_chain::object_has_prototype_divergence(raw) && super::prop_plan::store_plan_check( class_id, key as usize, 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 c79b566f87..efc96f573e 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 @@ -290,8 +290,10 @@ pub(crate) fn set_field_by_name_object_tail( || crate::object::own_descriptors_skip_key( obj as usize, f64::from_bits(JSValue::string_ptr(key as *mut _).bits()), - )) - && !super::prototype_chain::object_has_prototype_divergence(obj as usize); + )); + // No per-receiver prototype gate: the plan key carries the receiver's + // prototype (`receiver_proto_bits`, a fact of its shape), so a + // receiver on another chain can only meet another plan. let plan_fast = plan_eligible && super::prop_plan::store_plan_check( obj_class_id, @@ -560,8 +562,7 @@ pub(crate) fn set_field_by_name_object_tail( && obj_flags & PLAN_BLOCKING_FLAGS == 0 // `desc_gate_ok` above already proved this key is uncovered on // this receiver, which is the per-key half of the old flag. - && desc_gate_ok - && !super::prototype_chain::object_has_prototype_divergence(obj as usize); + && desc_gate_ok; if !plan_fast && record_plan_eligible { super::prop_plan::store_plan_record( obj_class_id, diff --git a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs index a0bd9d39ce..eaa553fbee 100644 --- a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs @@ -147,8 +147,9 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { if is_class_object_value(type_ref) { // Static/forward `new C()` sites can still construct by template id // without attaching an evaluated prototype. Retain that representation's - // class-id check; recorded individual chains are authoritative. - if !super::prototype_chain::object_has_prototype_divergence(value_addr(value)) { + // class-id check; a recorded prototype (a fact of the receiver's + // shape) is authoritative. + if super::prototype_chain::object_static_prototype(value_addr(value)).is_none() { let obj = crate::JSValue::from_bits(bits).as_pointer::(); return js_instanceof(value, js_object_get_class_id(obj)); } diff --git a/crates/perry-runtime/src/object/meta_flags.rs b/crates/perry-runtime/src/object/meta_flags.rs index 79cd40776f..5c468dd22b 100644 --- a/crates/perry-runtime/src/object/meta_flags.rs +++ b/crates/perry-runtime/src/object/meta_flags.rs @@ -5,7 +5,6 @@ //! bit, and the "last free bit" warning below is only useful next to the //! constants it constrains. -pub(crate) const OBJECT_META_FLAG_PROTO_DIVERGED: u64 = 1; pub(crate) const OBJECT_META_FLAG_USER_PROTO_OVERRIDE: u64 = 1 << 3; pub(crate) const OBJECT_META_FLAG_CLASS_EVALUATION_PROTO: u64 = 1 << 4; /// This object is used as somebody's `[[Prototype]]`, so a STRUCTURAL mutation @@ -35,7 +34,8 @@ pub(crate) const OBJECT_META_FLAG_EXOTIC_READ_RECEIVER: u64 = 1 << 6; // // `ObjectMeta::flags` bit map (u64), verified against #8690's comment in // `array/subclass.rs` and every reader in the tree: -// bit 0 prototype-semantic divergence (OBJECT_META_FLAG_PROTO_DIVERGED) +// bit 0 free (was the prototype-divergence bit: a diverged prototype +// is a different ShapeId now, `shapes::object_prototype_word`) // bit 1 packed-numeric payload valid (#8690) // bit 2 packed-numeric u32 entity proof (#8690) // bit 3 user-origin prototype signal (OBJECT_META_FLAG_USER_PROTO_OVERRIDE) diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index def5ed3be6..3d8d3c1110 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1790,8 +1790,7 @@ pub(crate) unsafe fn object_keys_and_live_slots( pub(crate) mod meta_flags; pub(crate) use meta_flags::{ OBJECT_META_FLAG_CLASS_EVALUATION_PROTO, OBJECT_META_FLAG_EXOTIC_READ_RECEIVER, - OBJECT_META_FLAG_IS_PROTOTYPE, OBJECT_META_FLAG_PROTO_DIVERGED, - OBJECT_META_FLAG_USER_PROTO_OVERRIDE, + OBJECT_META_FLAG_IS_PROTOTYPE, OBJECT_META_FLAG_USER_PROTO_OVERRIDE, }; pub(crate) mod meta_record; 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 1218798e08..484064e86a 100644 --- a/crates/perry-runtime/src/object/object_ops/keys_array.rs +++ b/crates/perry-runtime/src/object/object_ops/keys_array.rs @@ -542,11 +542,6 @@ unsafe fn define_append_transition_eligible( crate::object::shape_mint_census::note_define_outcome("ineligible: exotic expando"); return false; } - if crate::object::prototype_chain::object_has_prototype_divergence(obj as usize) { - #[cfg(feature = "shape-mint-diag")] - crate::object::shape_mint_census::note_define_outcome("ineligible: prototype divergence"); - return false; - } let verdict = match super::super::shapes::object_shape_descriptor(obj) { // A keyless receiver has no descriptor yet on some paths; the tail // learns its keyless→one-key edge from the same stamp. diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 22eca907a6..78bee93800 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -442,7 +442,6 @@ pub(crate) fn object_link_created_prototype(obj_ptr: usize, proto_bits: u64) { } fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: PrototypeLinkKind) { - let prototype_diverged = link_kind != PrototypeLinkKind::ClassDefault; let user_override = matches!( link_kind, PrototypeLinkKind::UserOverride | PrototypeLinkKind::FreshObject @@ -560,9 +559,6 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: let per_object = proto_id == crate::object::shapes::PROTO_ID_PER_OBJECT || !crate::object::shapes::shape_word_is_writable(obj); let mut link_flags = 0u64; - if prototype_diverged { - link_flags |= crate::object::OBJECT_META_FLAG_PROTO_DIVERGED; - } if user_override { link_flags |= crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE; } @@ -612,7 +608,7 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: } let proto_bits = proto_handle.get_heap_word_u64(); #[cfg(feature = "shape-mint-diag")] - if prototype_diverged { + if link_kind != PrototypeLinkKind::ClassDefault { crate::object::shape_mint_census::note_proto_divergence( crate::object::shapes::object_shape_stamp(obj), proto_bits, @@ -866,10 +862,6 @@ unsafe fn cell_is_born_null_proto(obj_ptr: usize) -> bool { && header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 } -pub(crate) fn object_has_prototype_divergence(obj_ptr: usize) -> bool { - object_has_prototype_flag(obj_ptr, crate::object::OBJECT_META_FLAG_PROTO_DIVERGED) -} - /// True only when a user-facing operation selected this receiver's prototype. /// Runtime wiring can use the same metadata record and loud invalidations, but /// it deliberately leaves this distinct bit clear. @@ -1253,14 +1245,11 @@ mod tests { let runtime_wired = crate::object::js_object_alloc(0, 0); object_set_static_prototype(runtime_wired as usize, crate::value::TAG_NULL); - let runtime_meta = unsafe { (*runtime_wired).meta }; - assert!(!runtime_meta.is_null()); - assert_ne!( - unsafe { (*runtime_meta).flags } & crate::object::OBJECT_META_FLAG_PROTO_DIVERGED, - 0, - "the loud runtime setter must retain its conservative divergence signal" + // A diverged prototype is a different shape, not a flag. + assert_eq!( + object_static_prototype(runtime_wired as usize), + Some(crate::value::TAG_NULL) ); - assert!(object_has_prototype_divergence(runtime_wired as usize)); assert!( !object_has_user_prototype_override(runtime_wired as usize), "runtime prototype wiring must not masquerade as a user override" @@ -1274,20 +1263,17 @@ mod tests { assert!( class_default_meta.is_null() || unsafe { (*class_default_meta).flags } - & (crate::object::OBJECT_META_FLAG_PROTO_DIVERGED - | crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE) + & crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE == 0, - "class-default links must publish neither divergence signal" + "class-default links must not publish the user-override signal" ); assert_eq!( object_static_prototype(class_default as usize), Some(crate::value::TAG_NULL) ); - assert!(!object_has_prototype_divergence(class_default as usize)); let evaluated = crate::object::js_object_alloc(0, 0); object_link_class_evaluation_prototype(evaluated as usize, crate::value::TAG_NULL); - assert!(object_has_prototype_divergence(evaluated as usize)); assert!(object_has_individual_class_prototype(evaluated as usize)); assert!(!object_has_user_prototype_override(evaluated as usize)); assert!(!object_has_individual_class_prototype( @@ -1299,13 +1285,6 @@ mod tests { let user_overridden = crate::object::js_object_alloc(0, 0); object_set_user_prototype(user_overridden as usize, crate::value::TAG_NULL); - let user_meta = unsafe { (*user_overridden).meta }; - assert!(!user_meta.is_null()); - assert_ne!( - unsafe { (*user_meta).flags } & crate::object::OBJECT_META_FLAG_PROTO_DIVERGED, - 0 - ); - assert!(object_has_prototype_divergence(user_overridden as usize)); assert!(object_has_user_prototype_override(user_overridden as usize)); } diff --git a/crates/perry-runtime/src/object/shapes_prototype_tests.rs b/crates/perry-runtime/src/object/shapes_prototype_tests.rs index 13dde655a3..2a921e45ba 100644 --- a/crates/perry-runtime/src/object/shapes_prototype_tests.rs +++ b/crates/perry-runtime/src/object/shapes_prototype_tests.rs @@ -84,3 +84,34 @@ fn a_null_prototype_is_a_shape_fact() { // one keeps the same word there (the cheaper read). assert_eq!(unsafe { (*(*obj).meta).prototype }, crate::value::TAG_NULL); } + +/// How the prototype was linked (`new F()` vs `Object.create(F.prototype)`) +/// is not observable in JS, so the two receivers must share one shape: the +/// identity names the same prototype, and nothing else about the link may +/// split them. +#[test] +fn new_f_and_object_create_of_its_prototype_share_a_shape() { + let _no_move = crate::gc::GcSuppressScope::new(); + let proto = js_object_alloc(0, 0); + let created = crate::object::js_object_create(f64::from_bits(bits(proto))); + let created = crate::value::JSValue::from_bits(created.to_bits()).as_pointer::() + as *mut ObjectHeader; + let width = crate::object::shapes::shape_live_inline_slot_count_by_id(unsafe { + super::super::object_shape_stamp(created) + }) + .unwrap(); + let constructed = js_object_alloc(crate::object::shapes::SYNTHETIC_CLASS_ID_BASE + 0x53, width); + object_link_class_default_prototype(constructed as usize, bits(proto)); + let (a, b) = unsafe { + ( + super::super::object_shape_stamp(created), + super::super::object_shape_stamp(constructed), + ) + }; + assert_eq!(object_static_prototype(created as usize), Some(bits(proto))); + assert_eq!( + object_static_prototype(constructed as usize), + Some(bits(proto)) + ); + assert_eq!(a, b, "one prototype, one empty layout: one ShapeId"); +} diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 9545cae9b4..b5b0e5f155 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -2120,9 +2120,6 @@ fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, receiver: f64) // neither record nor honor store plans. let plan_eligible = header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO == 0 - && !crate::object::prototype_chain::object_has_prototype_divergence( - addr, - ) && class_id != crate::object::NATIVE_MODULE_CLASS_ID // #8113: this asks for ORDINARY specifically — // it must stay FALSE for a class object or From 061b345c885446b7aaa6bbf70269024b32391af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 22:15:11 +0200 Subject: [PATCH 2/4] perf(runtime): read prototype-link facts from the shape; delete the link flags The user-override and class-evaluation flags (and the process-wide USER_PROTO_OVERRIDE_EVER latch) recorded HOW an object's prototype was set. With the prototype a fact of the shape, every reader now asks the shape what the prototype IS: - "does this instance stand on a prototype other than its class's?" is the shape identity versus the class's implied identity (`object_has_individual_class_prototype`: prototype_override field reads, native_call_method, symbol gets, evaluation-template misses). A compiled class instance linked to its own declaration prototype (runtime wiring of a native-base subclass) takes the class identity, so it keeps the class surface and its class's shapes. - "was a class declaration prototype re-pointed?" compares its recorded [[Prototype]] with its declaration's parent prototype (`decl_prototype_relinked`), behind a latch armed only by user relinks of class-chain links (a declaration prototype, a class object or constructor, a function object); re-pointing an ordinary instance arms nothing. - `instanceof` against a class walks the live chain from the shape when the receiver's identity is not its class's or a class-chain link was relinked; the util.inherits escape hatch is gone (the relink is a shape fact of the prototype it moved). The prototype funnel records no flag and so allocates no meta record: Object.create, `__proto__` and setPrototypeOf receivers carry their prototype in their shape alone. A receiver that already has a meta record keeps the same bits there. ObjectMeta.flags bits 0, 3 and 4 are free. Refs #10507 --- changelog.d/PENDING-prototype-link-flags.md | 7 + .../meta_and_shape_records.rs | 12 - .../src/json/stringify_tojson_probe.rs | 7 +- .../src/json/stringify_tojson_probe_tests.rs | 7 +- .../src/object/class_constructors.rs | 2 +- .../src/object/class_registry.rs | 11 +- .../class_registry/prototype_methods.rs | 1 + .../class_registry/prototype_objects.rs | 39 +++- .../src/object/class_registry/state.rs | 2 + .../src/object/class_super_chain.rs | 2 +- .../field_get_set/class_object_template.rs | 11 +- crates/perry-runtime/src/object/instanceof.rs | 97 ++++---- .../src/object/instanceof/static_dispatch.rs | 7 +- crates/perry-runtime/src/object/meta_flags.rs | 6 +- crates/perry-runtime/src/object/mod.rs | 5 +- .../perry-runtime/src/object/property_key.rs | 2 +- .../src/object/prototype_chain.rs | 215 ++++++++++-------- crates/perry-runtime/src/object/shapes.rs | 37 +-- .../src/object/shapes_prototype_tests.rs | 5 +- .../perry-runtime/src/object/static_shapes.rs | 8 +- crates/perry-runtime/src/symbol/get.rs | 22 +- 21 files changed, 285 insertions(+), 220 deletions(-) diff --git a/changelog.d/PENDING-prototype-link-flags.md b/changelog.d/PENDING-prototype-link-flags.md index 72eea54b20..611d9456f8 100644 --- a/changelog.d/PENDING-prototype-link-flags.md +++ b/changelog.d/PENDING-prototype-link-flags.md @@ -5,3 +5,10 @@ object's prototype is a fact of its shape, the caches that consulted the flag prototype, so a re-parented object simply meets a different cache entry. A runtime-wired function-constructor instance no longer allocates a metadata record. + +The other prototype-link flags and the metadata allocation for them are gone +too (Refs #10507): `instanceof`, JSON.stringify's plain-record check, symbol +reads and the static shapes read an object's recorded prototype from its +shape, `instanceof` walks the live chain for a receiver on a foreign +prototype (so the `util.inherits` escape hatch and its latch are deleted), +and linking a prototype no longer allocates a metadata record. diff --git a/crates/perry-runtime/src/gc/tests/dead_owner_side_tables/meta_and_shape_records.rs b/crates/perry-runtime/src/gc/tests/dead_owner_side_tables/meta_and_shape_records.rs index a59d9ddf42..e1877cabbe 100644 --- a/crates/perry-runtime/src/gc/tests/dead_owner_side_tables/meta_and_shape_records.rs +++ b/crates/perry-runtime/src/gc/tests/dead_owner_side_tables/meta_and_shape_records.rs @@ -139,10 +139,6 @@ fn test_object_meta_prototype_survives_copied_minor_move() { Some(ptr_bits(old_proto)), "test premise: the meta-resident prototype reads back before the GC" ); - assert!( - crate::object::prototype_chain::object_has_user_prototype_override(old_owner), - "test premise: the per-instance override bit lives in the meta record" - ); js_shadow_slot_set(0, ptr_bits(old_owner)); js_shadow_slot_set(1, ptr_bits(old_proto)); @@ -159,10 +155,6 @@ fn test_object_meta_prototype_survives_copied_minor_move() { new_proto, "the meta record's prototype slot must be rewritten to the moved proto" ); - assert!( - crate::object::prototype_chain::object_has_user_prototype_override(new_owner), - "the non-pointer meta flags must travel with the copied record" - ); js_shadow_slot_set(0, 0); js_shadow_slot_set(1, 0); @@ -739,10 +731,6 @@ fn test_object_meta_null_prototype_survives_full_gc_on_live_owner() { "a live (rooted) owner's explicit-null prototype (a fact of its \ shape) must survive a full collection" ); - assert!( - !unsafe { (*(live as *const crate::object::ObjectHeader)).meta }.is_null(), - "the runtime-wiring link's divergence flag keeps its meta record alive" - ); js_shadow_slot_set(0, 0); js_shadow_frame_pop(frame); } diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe.rs b/crates/perry-runtime/src/json/stringify_tojson_probe.rs index 66de736954..1ea0eddd34 100644 --- a/crates/perry-runtime/src/json/stringify_tojson_probe.rs +++ b/crates/perry-runtime/src/json/stringify_tojson_probe.rs @@ -810,7 +810,12 @@ pub(super) unsafe fn plain_object_member( return None; } let obj = addr as *const crate::ObjectHeader; - if !(*obj).meta.is_null() || !class_is_plain_record((*obj).class_id) { + // A recorded prototype (a fact of the shape, or a meta record's word) + // can carry a `toJSON` of its own: only a default-chained record is plain. + if !(*obj).meta.is_null() + || !class_is_plain_record((*obj).class_id) + || crate::object::shapes::object_prototype_word(obj) != 0 + { return None; } let (keys, live_slots) = crate::object::object_keys_and_live_slot_count(obj); diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs b/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs index 226db1628b..aa31b0702c 100644 --- a/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs +++ b/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs @@ -676,15 +676,16 @@ fn plain_object_member_admits_only_what_the_member_dispatch_would_walk() { assert!(super::plain_object_member(declined, &mut proof).is_none()); } - // A recorded prototype lives in the meta record: declined. + // A recorded prototype (a fact of the receiver's shape): declined. let inherits = parsed(&scope, r#"{"a":1}"#); let proto = crate::object::js_object_alloc(0, 0); crate::object::prototype_chain::object_set_user_prototype( obj(&inherits) as usize, crate::value::js_nanbox_pointer(proto as i64).to_bits(), ); - assert!( - !(*obj(&inherits)).meta.is_null(), + assert_eq!( + crate::object::prototype_chain::object_static_prototype(obj(&inherits) as usize), + Some(crate::value::js_nanbox_pointer(proto as i64).to_bits()), "fixture must record a prototype" ); assert!( diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 1b171bb798..bae04ab9dc 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -932,7 +932,7 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( // the declared-chain paths below read the refreshed copies. let refreshed_args: Vec; let (this_value, args_ptr) = if static_entry.is_none() - || super::prototype_chain::any_user_prototype_override() + || super::prototype_chain::any_class_chain_relinked() { let live_scope = crate::gc::RuntimeHandleScope::new(); let this_handle = live_scope.root_nanbox_f64(this_value); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index a674f85707..4ebe645b67 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -117,11 +117,12 @@ pub use state::{ // ── prototype_objects.rs ──────────────────────────────────────────────────── pub(crate) use prototype_objects::{ - class_decl_prototype_relinked, class_prototype_object, ensure_function_prototype_object, - function_class_id, function_value_for_class_id, instance_class_prototype_object, - object_proto_chain_symbol_slot, relinked_class_prototype_read, resolve_proto_chain_field, - resolve_proto_chain_field_noting_miss, resolve_proto_chain_field_with_receiver, - resolve_proto_chain_symbol, synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE, + class_decl_prototype_relinked, class_prototype_object, decl_prototype_relinked, + ensure_function_prototype_object, function_class_id, function_value_for_class_id, + instance_class_prototype_object, object_proto_chain_symbol_slot, relinked_class_prototype_read, + resolve_proto_chain_field, resolve_proto_chain_field_noting_miss, + resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol, + synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE, }; pub use prototype_objects::{ js_set_function_prototype, js_set_prototype_property, NEXT_SYNTHETIC_CLASS_ID, diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index 30499f2612..b4c48a75dd 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -230,6 +230,7 @@ pub(crate) unsafe fn class_prototype_relinked(proto: *mut crate::object::ObjectH if cid == 0 || super::class_decl_prototype_object(cid) != proto { return; } + super::super::prototype_chain::note_class_chain_relinked(); let mut names: Vec = Vec::new(); if let Ok(registry) = super::CLASS_VTABLE_REGISTRY.read() { if let Some(reg) = registry.as_ref() { diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 6cbbe01fe5..9505420d07 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -636,9 +636,36 @@ unsafe fn evaluated_parent_instance_field( /// names the next hop of an instance chain, and walks over the class /// registry must stop at `cid` (the recorded link continues the chain). pub(crate) fn class_decl_prototype_relinked(cid: u32) -> bool { + if !super::super::prototype_chain::any_class_chain_relinked() { + return false; + } let decl_proto = class_decl_prototype_object(cid); - !decl_proto.is_null() - && super::super::prototype_chain::object_has_user_prototype_override(decl_proto as usize) + !decl_proto.is_null() && unsafe { decl_prototype_relinked(cid, decl_proto) } +} + +/// Is `decl_proto` (class `cid`'s declaration prototype) standing on +/// anything but what its declaration links it to — the parent class's +/// declaration prototype, or `Object.prototype` for a base class? Read from +/// its recorded `[[Prototype]]` (a fact of its shape), not from history: a +/// relink back to the declared parent is not a relink. A declaration this +/// cannot name (a runtime-valued or native parent, `extends null`) answers +/// `true`, which only sends the caller down the exact prototype walk. +/// +/// # Safety +/// `decl_proto` is class `cid`'s live declaration prototype. +pub(crate) unsafe fn decl_prototype_relinked(cid: u32, decl_proto: *mut ObjectHeader) -> bool { + let recorded = super::super::prototype_chain::object_static_prototype(decl_proto as usize); + let declared = match super::get_parent_class_id(cid) { + Some(parent) if parent != 0 && parent != cid => { + let parent_proto = class_decl_prototype_object(parent); + if parent_proto.is_null() { + return true; + } + Some(crate::value::js_nanbox_pointer(parent_proto as i64).to_bits()) + } + _ => super::global_object_prototype_bits(), + }; + recorded != declared } /// `key` read on the rest of class `cid`'s instance chain past its declared @@ -660,7 +687,7 @@ pub(crate) unsafe fn relinked_class_prototype_read( if decl_proto.is_null() { return None; } - match relinked_decl_prototype_field(decl_proto, key, receiver) { + match relinked_decl_prototype_field(cid, decl_proto, key, receiver) { RelinkedRead::NotRelinked => None, RelinkedRead::Answered(value) => Some(Some(value)), RelinkedRead::Missed => Some(None), @@ -689,12 +716,14 @@ enum RelinkedRead { /// facts a read site validates decline on their own; this is the generic read /// those sites fall back to and confirm their prime against. unsafe fn relinked_decl_prototype_field( + cid: u32, decl_proto: *mut ObjectHeader, key: *const crate::StringHeader, receiver: f64, ) -> RelinkedRead { if key.is_null() - || !super::super::prototype_chain::object_has_user_prototype_override(decl_proto as usize) + || !super::super::prototype_chain::any_class_chain_relinked() + || !decl_prototype_relinked(cid, decl_proto) { return RelinkedRead::NotRelinked; } @@ -935,7 +964,7 @@ unsafe fn resolve_proto_chain_field_inner( // prototype. The registered parent class id no longer names the next // hop: the recorded link does, and the rest of the chain is X's. if let (false, Some(receiver)) = (decl_proto.is_null(), receiver) { - match relinked_decl_prototype_field(decl_proto, key, receiver) { + match relinked_decl_prototype_field(cid, decl_proto, key, receiver) { RelinkedRead::NotRelinked => {} RelinkedRead::Answered(value) => return Some(value), RelinkedRead::Missed => return None, diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index 25f93fa56f..554c06e60b 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -802,6 +802,7 @@ pub(crate) fn class_static_prototype_root_store(class_id: u32, proto_ptr: *mut O if class_id == 0 || proto_ptr.is_null() { return; } + super::super::prototype_chain::note_class_chain_relinked(); let bits = crate::value::js_nanbox_pointer(proto_ptr as i64).to_bits(); crate::closure::closure_set_static_prototype( crate::object::class_value::class_value_ptr(class_id) as usize, @@ -814,6 +815,7 @@ pub(crate) fn class_static_prototype_root_clear(class_id: u32) { if class_id == 0 { return; } + super::super::prototype_chain::note_class_chain_relinked(); crate::closure::closure_set_static_prototype( crate::object::class_value::class_value_ptr(class_id) as usize, crate::value::TAG_NULL, diff --git a/crates/perry-runtime/src/object/class_super_chain.rs b/crates/perry-runtime/src/object/class_super_chain.rs index 07b932b8d6..3134b72549 100644 --- a/crates/perry-runtime/src/object/class_super_chain.rs +++ b/crates/perry-runtime/src/object/class_super_chain.rs @@ -77,7 +77,7 @@ pub(crate) unsafe fn class_super_base( /// static lookup no longer describes it? One latch load answers `false` in a /// process that never set a user prototype. pub(super) fn static_chain_relinked(home_cid: u32, owner_cid: u32) -> bool { - if !super::prototype_chain::any_user_prototype_override() { + if !super::prototype_chain::any_class_chain_relinked() { return false; } let mut cur = home_cid; diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs index 9202c437de..4ec5646c77 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs @@ -715,7 +715,6 @@ pub(crate) unsafe fn prototype_from_template( owner.to_bits(), ); (*meta).prototype = parent_bits; - (*meta).flags |= crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO; crate::gc::runtime_write_barrier_slot( meta as usize, &(*meta).prototype as *const u64 as usize, @@ -784,7 +783,7 @@ pub(crate) unsafe fn record_prototype_template( || meta.is_null() || (*meta).prototype != parent_proto || (*meta).private_evaluation_brand != owner - || (*meta).flags & !crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO != 0 + || (*meta).flags != 0 || (*meta).spill != 0 || u32::try_from(count).is_err() || cell.proto_fills_base() + 2 * count > cell.len() @@ -862,9 +861,9 @@ pub(crate) unsafe fn evaluation_chain_lost_method( obj: *const ObjectHeader, key: *const crate::string::StringHeader, ) -> bool { - let meta = (*obj).meta; - if meta.is_null() || (*meta).flags & crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO == 0 - { + // Linked through a class evaluation: its shape names a prototype other + // than the one its template class implies. + if !crate::object::prototype_chain::object_has_individual_class_prototype(obj as usize) { return false; } let class_id = (*obj).class_id; @@ -884,7 +883,7 @@ pub(crate) unsafe fn evaluation_chain_lost_method( if intact(obj) { return false; } - let proto = JSValue::from_bits((*meta).prototype); + let proto = JSValue::from_bits(crate::object::shapes::object_prototype_word(obj)); if proto.is_pointer() { let proto = proto.as_pointer::(); if crate::value::addr_class::try_read_gc_header(proto as usize) diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index f3d7de9cde..9f935b7ac8 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -683,43 +683,63 @@ pub(crate) fn class_chain_reaches(start: u32, want: u32) -> bool { } } -/// `value instanceof ` for an instance of class `start` when a -/// user `[[Prototype]]` change sits on its way: the instance's own prototype -/// was replaced, or a class prototype on the declared chain from `start` was -/// relinked before that chain reaches `want`. Then the declared class ids no -/// longer describe the chain, and `OrdinaryHasInstance` walks the live one. -/// `None` means the declared walk answers: no such change was ever made (one -/// latch load), or `want` is reached first. +/// `value instanceof ` (`value` an ordinary object of class +/// `start`) when the declared class ids may not describe its chain: its shape +/// names a prototype other than the one class `start` implies (a user +/// `setPrototypeOf`/`__proto__`, `Object.create`, a function constructor's +/// instance, an evaluated class), or a class declaration prototype on the way +/// from `start` was re-pointed. Then `OrdinaryHasInstance` walks the live +/// chain, reading each hop's prototype from its shape. `None` means the +/// declared walk answers: the receiver's shape names its class's prototype +/// and no declaration prototype was ever relinked (one latch load), or +/// `want` is reached first. /// `want` is a compiled class, or `Object` (its reserved id), whose /// constructor is the global one. #[inline(always)] pub(crate) fn relinked_instance_chain_answer(value: f64, start: u32, want: u32) -> Option { - if !super::prototype_chain::any_user_prototype_override() { + let obj = value_addr(value) as *const ObjectHeader; + if obj.is_null() { return None; } - relinked_instance_chain_answer_armed(value, start, want) + // SAFETY: every caller has proved a live `GC_TYPE_OBJECT` receiver. + let identity = unsafe { crate::object::shapes::object_shape_identity(obj) }; + // The common receiver stands on its own class's prototype: one compare + // decides, before any registry is asked what `start` implies. + if identity != (crate::object::shapes::PROTO_ID_CLASS | u64::from(start)) + && identity != crate::object::shapes::class_proto_id(start) + && identity != crate::object::shapes::PROTO_ID_PER_OBJECT + && super::prototype_chain::object_prototype_is_foreign(obj as usize) + { + return live_chain_answer(value, want); + } + if !super::prototype_chain::any_class_chain_relinked() { + return None; + } + relinked_declared_chain_answer(value, start, want) } -/// [`relinked_instance_chain_answer`] once a user prototype override exists. -/// Out of line so the callers' common path stays one latch load. +/// The receiver's own (shape-recorded) chain decides. #[cold] #[inline(never)] -fn relinked_instance_chain_answer_armed(value: f64, start: u32, want: u32) -> Option { +fn live_chain_answer(value: f64, want: u32) -> Option { const CLASS_ID_OBJECT: u32 = 0xFFFF0050; if want == 0 || (want != CLASS_ID_OBJECT && !super::is_class_id_registered(want)) { return None; } - let live = || { - let constructor = if want == CLASS_ID_OBJECT { - js_get_global_this_builtin_value(b"Object".as_ptr(), 6) - } else { - super::class_constructor_ref_value(want) - }; - ordinary_has_instance_prototype_walk(value, constructor) + let constructor = if want == CLASS_ID_OBJECT { + js_get_global_this_builtin_value(b"Object".as_ptr(), 6) + } else { + super::class_constructor_ref_value(want) }; - if super::prototype_chain::object_has_user_prototype_override(value_addr(value)) { - return Some(live()); - } + Some(ordinary_has_instance_prototype_walk(value, constructor)) +} + +/// [`relinked_instance_chain_answer`] once a class declaration prototype was +/// ever re-pointed: the live chain decides from the first relinked +/// declaration prototype on the declared walk. +#[cold] +#[inline(never)] +fn relinked_declared_chain_answer(value: f64, start: u32, want: u32) -> Option { if start == 0 { return None; } @@ -729,7 +749,7 @@ fn relinked_instance_chain_answer_armed(value: f64, start: u32, want: u32) -> Op return None; } if super::class_registry::class_decl_prototype_relinked(cur) { - return Some(live()); + return live_chain_answer(value, want); } match get_parent_class_id(cur) { Some(pid) if pid != 0 && pid != cur => cur = pid, @@ -860,33 +880,10 @@ fn subclass_of_builtin_reaches(value: f64, class_id: u32) -> bool { } // #9362: util.inherits(DerivedClass, BaseClass) links DerivedClass.prototype - // to BaseClass.prototype at runtime; it does not (and must not) create an - // extends edge between the constructor objects. The class-id fast path - // above therefore misses even though the observable prototype chain - // contains BaseClass.prototype. Only pay for the spec prototype walk when - // the candidate class's declaration prototype has a user-selected parent. - // The two `class_decl_prototype_object` probes are class registry reads - // (TLS + RwLock + map, ~130 instructions each) and they ran EAGERLY on - // every call that got this far — which is every MISS, the path this whole - // ladder exists to answer `false` on. They exist only to ask a question - // whose answer is `false` for every receiver in a process that never - // re-points an object's prototype, and the latch answers that for the - // whole process in one load. Set, never cleared, and published before the - // flag it guards, so it can only ever be conservatively true. - if super::prototype_chain::any_user_prototype_override() { - let candidate_proto = super::class_registry::class_decl_prototype_object(cur); - let target_proto = super::class_registry::class_decl_prototype_object(class_id); - if !candidate_proto.is_null() - && !target_proto.is_null() - && super::prototype_chain::object_has_user_prototype_override(candidate_proto as usize) - && ordinary_has_instance_prototype_walk( - value, - super::class_constructor_ref_value(class_id), - ) - { - return true; - } - } + // to BaseClass.prototype at runtime without an extends edge between the + // constructors. That relink is a shape fact of the prototype it moved + // (`relinked_instance_chain_answer` above walks the live chain from it), + // so no further escape hatch is needed here. false } diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs index e78fa66690..777325fbf7 100644 --- a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -591,9 +591,8 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { } // A relinked class prototype (or a replaced instance prototype) // can end the chain before `Object.prototype`. - if let Some(header) = super::super::prototype_chain::any_user_prototype_override() - .then(|| unsafe { crate::value::addr_class::try_read_gc_header(value_addr(value)) }) - .flatten() + if let Some(header) = + unsafe { crate::value::addr_class::try_read_gc_header(value_addr(value)) } { if header.obj_type == crate::gc::GC_TYPE_OBJECT { let obj_class_id = @@ -616,7 +615,7 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { unsafe { crate::value::addr_class::try_read_gc_header(obj as usize) } .is_some_and(|h| h._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0); if (born_null - || crate::object::prototype_chain::any_user_prototype_override() + || crate::object::prototype_chain::any_class_chain_relinked() || crate::object::prototype_chain::object_static_prototype(addr).is_some()) && crate::object::prototype_chain::prototype_chain_ends_in_null_before_object_prototype(addr) { diff --git a/crates/perry-runtime/src/object/meta_flags.rs b/crates/perry-runtime/src/object/meta_flags.rs index 5c468dd22b..289fd21246 100644 --- a/crates/perry-runtime/src/object/meta_flags.rs +++ b/crates/perry-runtime/src/object/meta_flags.rs @@ -5,8 +5,6 @@ //! bit, and the "last free bit" warning below is only useful next to the //! constants it constrains. -pub(crate) const OBJECT_META_FLAG_USER_PROTO_OVERRIDE: u64 = 1 << 3; -pub(crate) const OBJECT_META_FLAG_CLASS_EVALUATION_PROTO: u64 = 1 << 4; /// This object is used as somebody's `[[Prototype]]`, so a STRUCTURAL mutation /// of it (key add, delete, descriptor install, attribute change, /// `setPrototypeOf`) is invisible to everything that inherits from it and must @@ -38,8 +36,8 @@ pub(crate) const OBJECT_META_FLAG_EXOTIC_READ_RECEIVER: u64 = 1 << 6; // is a different ShapeId now, `shapes::object_prototype_word`) // bit 1 packed-numeric payload valid (#8690) // bit 2 packed-numeric u32 entity proof (#8690) -// bit 3 user-origin prototype signal (OBJECT_META_FLAG_USER_PROTO_OVERRIDE) -// bit 4 class-evaluation prototype (post-dates #8690's comment) +// bit 3 free (was the user-override bit) \ how a prototype was linked +// bit 4 free (was the class-evaluation bit) / is not a fact: its shape is // bit 5 this object is a prototype (here) // bit 6 exotic read receiver (here) // bit 7 *** THE LAST FREE BIT IN THIS WORD *** diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 3d8d3c1110..4f35b49d9f 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1788,10 +1788,7 @@ pub(crate) unsafe fn object_keys_and_live_slots( } pub(crate) mod meta_flags; -pub(crate) use meta_flags::{ - OBJECT_META_FLAG_CLASS_EVALUATION_PROTO, OBJECT_META_FLAG_EXOTIC_READ_RECEIVER, - OBJECT_META_FLAG_IS_PROTOTYPE, OBJECT_META_FLAG_USER_PROTO_OVERRIDE, -}; +pub(crate) use meta_flags::{OBJECT_META_FLAG_EXOTIC_READ_RECEIVER, OBJECT_META_FLAG_IS_PROTOTYPE}; pub(crate) mod meta_record; pub use meta_record::ObjectMeta; diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index dbf436125e..e8cfb54db8 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -365,7 +365,7 @@ pub unsafe extern "C" fn js_super_accessor_get(home_class_id: u32, key: f64, rec .ok() .map(|s| s.to_string()) }; - let base = if super::prototype_chain::any_user_prototype_override() { + let base = if super::prototype_chain::any_class_chain_relinked() { super::class_super_chain::super_get_live_base(home_class_id, parent_class_id, receiver) } else { None diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 78bee93800..04395018ee 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -211,32 +211,29 @@ pub(crate) fn test_resolution_stack_enter_and_forget(owner: usize) -> bool { /// an object — the overwhelmingly common case. static OBJECT_PROTOTYPES_NONEMPTY: AtomicBool = AtomicBool::new(false); -/// Latched true by the first `OBJECT_META_FLAG_USER_PROTO_OVERRIDE` a receiver -/// is ever given — i.e. the first `Object.setPrototypeOf` / `util.inherits` -/// that re-points a live object's `[[Prototype]]` away from its class default. -/// -/// The flag lives on the receiver's meta record, so asking "does this object -/// have one?" costs two dependent loads — but only after the caller has -/// already found the object. `instanceof`'s `util.inherits` escape hatch has -/// to look up TWO class declaration prototypes through the class registry -/// before it can ask, and that pair of registry probes was the single largest -/// cost of a `o instanceof C` MISS (~130 instructions each, on a path whose -/// whole budget was 669). This latch answers for the entire process in one -/// relaxed-acquire load. -/// -/// Conservative by construction: it is set, never cleared, and it is stored -/// BEFORE the flag it guards (same discipline as [`OBJECT_PROTOTYPES_NONEMPTY`] -/// above), so any reader that could observe the flag already observes the -/// latch. A false positive costs a probe pair; a false negative is impossible. -static USER_PROTO_OVERRIDE_EVER: AtomicBool = AtomicBool::new(false); - -/// Has any object in this process ever been given a user `[[Prototype]]` -/// override? A `false` proves `object_has_user_prototype_override` would -/// answer `false` for every receiver, so a caller may skip whatever work it -/// would need to do to ask. +/// Latched true by the first user relink of a CLASS CHAIN link: a class +/// declaration prototype (`Object.setPrototypeOf(C.prototype, X)`, +/// `util.inherits`), a class constructor, or a function object. Those are the +/// events that make a class's registered parent edges stop describing the +/// live chain, which the class-id shortcuts (vtable, decl-proto and static +/// walks) assume. Ordinary receivers re-pointed by `setPrototypeOf`, +/// `__proto__` or `Object.create` do not set it: their prototype is a fact of +/// their own shape. While it is clear, `class_decl_prototype_relinked` and the +/// super/static relink probes answer `false` in one load; once set, they read +/// the recorded `[[Prototype]]` of the link in question. +static CLASS_CHAIN_RELINKED_EVER: AtomicBool = AtomicBool::new(false); + +/// Has any class chain link (see [`CLASS_CHAIN_RELINKED_EVER`]) ever been +/// re-pointed by a user operation? `false` proves every class's registered +/// parent edges still describe its live chain. #[inline] -pub(crate) fn any_user_prototype_override() -> bool { - USER_PROTO_OVERRIDE_EVER.load(Ordering::Acquire) +pub(crate) fn any_class_chain_relinked() -> bool { + CLASS_CHAIN_RELINKED_EVER.load(Ordering::Acquire) +} + +/// Arm [`any_class_chain_relinked`] (`class_prototype_relinked`). +pub(crate) fn note_class_chain_relinked() { + CLASS_CHAIN_RELINKED_EVER.store(true, Ordering::Release); } /// #10362: mark `obj_ptr`'s own header as an owner in the residual registry. @@ -558,28 +555,13 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: // authority, as for a per-object identity. let per_object = proto_id == crate::object::shapes::PROTO_ID_PER_OBJECT || !crate::object::shapes::shape_word_is_writable(obj); - let mut link_flags = 0u64; - if user_override { - link_flags |= crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE; - } - if link_kind == PrototypeLinkKind::ClassEvaluation { - link_flags |= crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO; - } let mut obj = obj; - // A class-default link on a meta-less instance of a function - // constructor (`new F()`, a synthetic class) needs no meta record - // at all: `shapes::object_prototype_word` reads such a receiver's - // prototype from its shape's identity word. Every other receiver - // has a record anyway — for the link's flags, a PER_OBJECT - // identity, or as the one-compare "nothing recorded" answer for a - // meta-less non-synthetic receiver — and that record's word is - // the cheaper read of the same prototype, written here beside the - // identity's word and never anywhere else. - if per_object - || link_flags != 0 - || !(*obj).meta.is_null() - || !crate::object::shapes::is_synthetic_class_id((*obj).class_id) - { + // The prototype is a fact of the receiver's shape + // (`shapes::object_prototype_word`), so a link allocates no meta + // record. A receiver that has one anyway, or whose PER_OBJECT + // identity answers nothing from its shape, keeps the same bits in + // it: the cheaper read, written here and nowhere else. + if per_object || !(*obj).meta.is_null() { // `object_meta_ensure` allocates and may evacuate the owner. let (meta, moved) = obj_handle.across_mut::(|| { crate::object::object_meta_ensure(obj) @@ -587,16 +569,6 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: obj = moved; let word = proto_handle.get_heap_word_u64(); (*meta).prototype = word; - if user_override { - // Latch BEFORE the flag: a reader that observes the flag - // must already observe the latch (see - // `USER_PROTO_OVERRIDE_EVER`). - USER_PROTO_OVERRIDE_EVER.store(true, Ordering::Release); - } - (*meta).flags |= link_flags; - if user_override { - crate::object::class_registry::class_prototype_relinked(obj); - } // GC_STORE_AUDIT(BARRIERED): meta-record prototype slot store — // the record is an arena allocation, so the ordinary // object-slot barrier applies (parent = the meta record). @@ -606,6 +578,15 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: word, ); } + if user_override { + // A user relink of a class declaration prototype retires the + // class's prototype-method fast guards and arms + // `any_class_chain_relinked`; so does one of a class object. + if crate::object::class_registry::is_class_object_ptr(obj.cast()) { + note_class_chain_relinked(); + } + crate::object::class_registry::class_prototype_relinked(obj); + } let proto_bits = proto_handle.get_heap_word_u64(); #[cfg(feature = "shape-mint-diag")] if link_kind != PrototypeLinkKind::ClassDefault { @@ -621,6 +602,11 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: return; } } + if user_override { + // A function object (a class constructor or a plain function whose + // `.prototype` other chains name) re-pointed by a user operation. + note_class_chain_relinked(); + } let mut slot_addr = 0usize; if let Ok(mut map) = get_object_prototypes().lock() { // Latch BEFORE the insert, and UNDER THE LOCK (#7737). @@ -710,17 +696,6 @@ pub(crate) fn object_static_prototype_known_non_meta(obj_ptr: usize) -> Option bool { - unsafe { - let Some(obj) = meta_capable_object(obj_ptr) else { - return false; - }; - let meta = (*obj).meta; - !meta.is_null() && (*meta).flags & flag != 0 - } -} - /// True when this receiver's recorded prototype diverges from its class /// default, regardless of whether runtime wiring or a user-facing operation /// selected it. Cache guards use this conservative signal. @@ -862,24 +837,63 @@ unsafe fn cell_is_born_null_proto(obj_ptr: usize) -> bool { && header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 } -/// True only when a user-facing operation selected this receiver's prototype. -/// Runtime wiring can use the same metadata record and loud invalidations, but -/// it deliberately leaves this distinct bit clear. -#[inline] -pub(crate) fn object_has_user_prototype_override(obj_ptr: usize) -> bool { - object_has_prototype_flag(obj_ptr, crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE) -} - /// Whether ordinary property lookup must consult the receiver's own chain -/// before the shared class vtable: user overrides and evaluated classes both -/// have this requirement; unrelated runtime prototype wiring does not. +/// before its class's shared surface (vtable, declaration prototype): its +/// shape's prototype identity is not the one its class implies — a user +/// `setPrototypeOf`/`__proto__`, an evaluated class's prototype, runtime +/// wiring to anything but the class's own declaration prototype. A shape +/// read (`shapes::object_shape_identity`), not a flag. A receiver with no +/// class surface (class id 0, a synthetic class) never needs this. #[inline] pub(crate) fn object_has_individual_class_prototype(obj_ptr: usize) -> bool { - object_has_prototype_flag( - obj_ptr, - crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE - | crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO, - ) + unsafe { + let Some(obj) = meta_capable_object(obj_ptr) else { + return false; + }; + let implied = crate::object::shapes::class_proto_id((*obj).class_id); + if implied == crate::object::shapes::PROTO_ID_DEFAULT { + return false; + } + let pid = crate::object::shapes::object_shape_identity(obj); + pid != implied && pid != crate::object::shapes::PROTO_ID_PER_OBJECT + } +} + +/// Does this receiver stand on a prototype from OUTSIDE its own class — not +/// its class's declaration prototype, nor an evaluation prototype of its own +/// (template) class? A user `setPrototypeOf`/`__proto__`, `Object.create`, +/// or a `null` link. A fact of what the receiver's shape names (its recorded +/// prototype's identity), not of how it was linked: re-pointing it back to +/// its class's prototype makes it false again. Then the declared class ids +/// do not describe the receiver's chain at all. +pub(crate) fn object_prototype_is_foreign(obj_ptr: usize) -> bool { + unsafe { + let Some(obj) = meta_capable_object(obj_ptr) else { + return false; + }; + // A class object's [[Prototype]] is its heritage (the parent + // constructor): its declared static chain. A user relink of one arms + // `any_class_chain_relinked`, which the static walks consult. + if crate::object::class_registry::is_class_object_ptr(obj.cast()) { + return false; + } + let recorded = crate::object::shapes::object_prototype_word(obj); + if recorded == 0 { + return false; + } + let value = crate::value::JSValue::from_bits(recorded); + if !value.is_pointer() { + return true; + } + let proto = value.as_pointer::(); + let Some(header) = crate::value::addr_class::try_read_gc_header(proto as usize) else { + return true; + }; + let class_id = (*obj).class_id; + !(header.obj_type == crate::gc::GC_TYPE_OBJECT + && class_id != 0 + && (*proto).class_id == class_id) + } } /// #11391: `new F()` records F's `.prototype` of that moment as the instance's @@ -1250,32 +1264,27 @@ mod tests { object_static_prototype(runtime_wired as usize), Some(crate::value::TAG_NULL) ); - assert!( - !object_has_user_prototype_override(runtime_wired as usize), - "runtime prototype wiring must not masquerade as a user override" - ); let class_default = crate::object::js_object_alloc(0, 0); object_link_class_default_prototype(class_default as usize, crate::value::TAG_NULL); - // The prototype is a shape fact; whatever record the receiver keeps - // carries neither divergence signal. - let class_default_meta = unsafe { (*class_default).meta }; - assert!( - class_default_meta.is_null() - || unsafe { (*class_default_meta).flags } - & crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE - == 0, - "class-default links must not publish the user-override signal" - ); + // The prototype is a shape fact, and a link records no flag: the + // receiver needs no meta record at all. + assert!(unsafe { (*class_default).meta }.is_null()); assert_eq!( object_static_prototype(class_default as usize), Some(crate::value::TAG_NULL) ); - let evaluated = crate::object::js_object_alloc(0, 0); + // "Individual" is read from the shape: a compiled class's instance + // whose identity names anything but its class's prototype. Receivers + // with no class surface (class id 0) never are. + const CLASS: u32 = 0x7A; + let fresh = crate::object::js_object_alloc(CLASS, 0); + assert!(!object_has_individual_class_prototype(fresh as usize)); + let evaluated = crate::object::js_object_alloc(CLASS, 0); object_link_class_evaluation_prototype(evaluated as usize, crate::value::TAG_NULL); assert!(object_has_individual_class_prototype(evaluated as usize)); - assert!(!object_has_user_prototype_override(evaluated as usize)); + assert!(unsafe { (*evaluated).meta }.is_null(), "no flag, no record"); assert!(!object_has_individual_class_prototype( class_default as usize )); @@ -1283,9 +1292,15 @@ mod tests { runtime_wired as usize )); - let user_overridden = crate::object::js_object_alloc(0, 0); + let user_overridden = crate::object::js_object_alloc(CLASS, 0); object_set_user_prototype(user_overridden as usize, crate::value::TAG_NULL); - assert!(object_has_user_prototype_override(user_overridden as usize)); + assert!(object_has_individual_class_prototype( + user_overridden as usize + )); + assert!( + unsafe { (*user_overridden).meta }.is_null(), + "no flag, no record" + ); } /// #10827. Every one of these is a case where the READ used to disagree diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 39a6adbba5..9c06bcbd67 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -4027,18 +4027,18 @@ pub(crate) fn shape_is_keyless_birth(id: u32, proto_id: u64, slots: u32) -> bool } } -/// A synthetic class id: a plain function constructor's (or -/// `Object.create`'s historical) class, the only receivers the prototype -/// funnel links without a meta record. +/// The prototype identity `obj`'s ShapeId names, read through the agent +/// directory (an unstamped or unknown id reads the default identity). +/// +/// # Safety +/// `obj` is a live `ObjectHeader`. #[inline] -pub(crate) fn is_synthetic_class_id(class_id: u32) -> bool { - (SYNTHETIC_CLASS_ID_BASE - ..crate::object::class_registry::prototype_objects::SYNTHETIC_CLASS_ID_END) - .contains(&class_id) +pub(crate) unsafe fn object_shape_identity(obj: *const crate::object::ObjectHeader) -> u64 { + (*ShapeSlab::agent_record(object_shape_stamp(obj))).proto_id } -/// The first synthetic class id (`class_registry::prototype_objects`), for -/// the receivers [`is_synthetic_class_id`] admits. +/// The first synthetic class id (`class_registry::prototype_objects`): a +/// plain function constructor's instances. pub(crate) const SYNTHETIC_CLASS_ID_BASE: u32 = crate::object::class_registry::prototype_objects::SYNTHETIC_CLASS_ID_BASE; @@ -4063,14 +4063,6 @@ pub(crate) unsafe fn object_prototype_word(obj: *const crate::object::ObjectHead if !meta.is_null() && (*meta).prototype != 0 { return (*meta).prototype; } - // Only a function constructor's instance (a synthetic class) is linked - // without writing a meta record's word - // (`prototype_chain::object_set_static_prototype_impl`); it may have - // gained a record for something else since. Any other receiver without a - // recorded word recorded nothing, which one compare says. - if !is_synthetic_class_id((*obj).class_id) { - return 0; - } // The agent directory read: never null, an absent id reads the empty // record (identity 0, the default). let proto_id = (*ShapeSlab::agent_record(object_shape_stamp(obj))).proto_id; @@ -4121,6 +4113,17 @@ pub(crate) unsafe fn object_proto_id_for( let class_id = (*obj).class_id; let class = vtable_class(class_id); if recorded != 0 { + // A compiled class instance linked to its own class's declaration + // prototype (runtime wiring of a native-base subclass instance) has + // exactly the prototype its class implies: the class identity, so it + // shares its class's shapes and its class surface stays exact. + if class != 0 { + let decl = crate::object::class_registry::class_decl_prototype_object(class); + if !decl.is_null() && crate::value::js_nanbox_pointer(decl as i64).to_bits() == recorded + { + return PROTO_ID_CLASS | u64::from(class); + } + } if let Some(id) = stable_linked_proto_id(class_id, recorded) { return id; } diff --git a/crates/perry-runtime/src/object/shapes_prototype_tests.rs b/crates/perry-runtime/src/object/shapes_prototype_tests.rs index 2a921e45ba..691167c682 100644 --- a/crates/perry-runtime/src/object/shapes_prototype_tests.rs +++ b/crates/perry-runtime/src/object/shapes_prototype_tests.rs @@ -80,9 +80,8 @@ fn a_null_prototype_is_a_shape_fact() { object_static_prototype(obj as usize), Some(crate::value::TAG_NULL) ); - // The link's user-override FLAG needs a meta record, and a receiver with - // one keeps the same word there (the cheaper read). - assert_eq!(unsafe { (*(*obj).meta).prototype }, crate::value::TAG_NULL); + // A link records no flag, so it allocates no meta record. + assert!(unsafe { (*obj).meta }.is_null()); } /// How the prototype was linked (`new F()` vs `Object.create(F.prototype)`) diff --git a/crates/perry-runtime/src/object/static_shapes.rs b/crates/perry-runtime/src/object/static_shapes.rs index c3bca6e3d7..759fb0216a 100644 --- a/crates/perry-runtime/src/object/static_shapes.rs +++ b/crates/perry-runtime/src/object/static_shapes.rs @@ -348,7 +348,13 @@ unsafe fn finalized_constfn_facts( infos: &[shapes::ConstFnSlotInfo], rebuilt: bool, ) -> Option { - if obj.is_null() || !shapes::shape_word_is_writable(obj) || !(*obj).meta.is_null() { + if obj.is_null() + || !shapes::shape_word_is_writable(obj) + || !(*obj).meta.is_null() + // A receiver whose shape names a recorded prototype keeps it: a + // static class shape names the class's. + || shapes::object_prototype_word(obj) != 0 + { return None; } let current = shapes::object_shape_descriptor(obj)?; diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 3cb8beadbe..87a7a95305 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -1385,6 +1385,21 @@ pub(crate) unsafe fn js_object_get_symbol_property_with_receiver( { return slot.read(receiver_f64); } + // A class OBJECT (a pointer, e.g. a class declared in a function + // body) extending a built-in constructor inherits that + // constructor's symbol statics, as the class-ref form does. + if crate::object::is_class_object_ptr(obj_ptr as *const u8) { + if let Some(parent_ctor) = crate::object::builtin_parent_ctor_in_chain(cid) { + let v = js_object_get_symbol_property_with_receiver( + parent_ctor, + sym_f64, + receiver_f64, + ); + if v.to_bits() != TAG_UNDEFINED { + return v; + } + } + } // A symbol-keyed property added to a DECLARED class's // `.prototype` after the declaration — `C.prototype[S] = f`, // `Object.defineProperty(C.prototype, S, ...)`, or @@ -1455,7 +1470,9 @@ unsafe fn declared_prototype_symbol_holder( mut class_id: u32, ) -> Option { let receiver_addr = (receiver.to_bits() & crate::value::POINTER_MASK) as usize; - if crate::object::prototype_chain::object_has_user_prototype_override(receiver_addr) { + // A receiver whose shape names a prototype other than its class's is + // read along its own chain, not the declared one. + if crate::object::prototype_chain::object_prototype_is_foreign(receiver_addr) { return None; } for _ in 0..32 { @@ -1465,7 +1482,8 @@ unsafe fn declared_prototype_symbol_holder( if has_own_symbol_property(proto_value, sym) { return Some(proto_value); } - if crate::object::prototype_chain::object_has_user_prototype_override(declared as usize) + if crate::object::prototype_chain::any_class_chain_relinked() + && crate::object::decl_prototype_relinked(class_id, declared) { return None; } From d26852711b0fa62ccb637e860096ee2c63917ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 22:15:11 +0200 Subject: [PATCH 3/4] perf(runtime): the ShapeId names its prototype identity kind; read the record only for a word Phase 2 made every meta-less receiver read its [[Prototype]] from its shape record and identity word (~50 instructions), where main answered a meta-less non-synthetic receiver with one compare of its class id. qs (+0.75%), commander (+0.29%), g6 (+7.4%), `instanceof` against a class (+21%) and decimal_class paid for it. - A ShapeId carries its prototype identity KIND in bits 20-21: plain (the realm default, a class's declaration prototype, a per-object identity), word (a serial, MIXED or UNIQUE link) or null. Each band mints every kind from its own counter, in alternating 2^20-id granules (page-aligned, so no record memory is spent on the interleave); the compiler's static band is one plain granule. `ShapeSlab::insert` asserts that a plain or null id names exactly that identity. - `object_prototype_word` (now inlined): the meta word, else 0 for a plain id, the born-null header test for a null id, and the record plus identity word (out of line) only for a word id. - `object_has_individual_class_prototype` and the `instanceof` relink check test the kind before any record or registry read; `instanceof` hands the check the receiver it already holds. - `object_proto_id_for` asks the class registry about a declaration prototype only when the recorded prototype is an object of that class. - The inherited-read walks (`read_holder::walk`, `class_link`, `class_read::admitted_next`) read each hop's word once for both the identity check and the hop, not two or three times. - An object marked as a prototype keeps its own [[Prototype]] in its meta record (`keep_prototype_in_record`): a class declaration prototype is linked to its parent's before it has a record. Refs #10507 --- crates/perry-runtime/src/object/instanceof.rs | 36 ++- .../src/object/instanceof/static_dispatch.rs | 4 +- .../src/object/method_site/read_holder.rs | 86 ++++-- .../method_site/read_holder/class_read.rs | 5 +- .../src/object/proto_validity.rs | 40 ++- .../src/object/prototype_chain.rs | 15 +- crates/perry-runtime/src/object/shapes.rs | 246 +++++++++++++++--- .../src/object/shapes_slot_list.rs | 6 +- .../perry-runtime/src/object/shapes_store.rs | 11 + .../src/object/shapes_store_tests.rs | 6 +- .../perry-runtime/src/object/shapes_tests.rs | 60 ++++- 11 files changed, 443 insertions(+), 72 deletions(-) diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 9f935b7ac8..e00dfcc9bc 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -701,16 +701,34 @@ pub(crate) fn relinked_instance_chain_answer(value: f64, start: u32, want: u32) if obj.is_null() { return None; } + relinked_object_chain_answer(obj, value, start, want) +} + +/// [`relinked_instance_chain_answer`] for a caller that already holds the +/// receiver `obj` (`value`'s live `GC_TYPE_OBJECT`). +#[inline(always)] +pub(crate) fn relinked_object_chain_answer( + obj: *const ObjectHeader, + value: f64, + start: u32, + want: u32, +) -> Option { // SAFETY: every caller has proved a live `GC_TYPE_OBJECT` receiver. - let identity = unsafe { crate::object::shapes::object_shape_identity(obj) }; - // The common receiver stands on its own class's prototype: one compare - // decides, before any registry is asked what `start` implies. - if identity != (crate::object::shapes::PROTO_ID_CLASS | u64::from(start)) - && identity != crate::object::shapes::class_proto_id(start) - && identity != crate::object::shapes::PROTO_ID_PER_OBJECT - && super::prototype_chain::object_prototype_is_foreign(obj as usize) - { - return live_chain_answer(value, want); + // The common receiver's ShapeId names an unlinked identity: the default, + // its own class's or a per-object one, none of them foreign (a class + // instance linked anywhere but its class's declaration prototype has a + // linked identity). One compare of the header word decides, before the + // shape record or any registry is read. + if unsafe { crate::object::shapes::shape_word_may_be_linked((*obj).parent_class_id) } { + // SAFETY: as above. + let identity = unsafe { crate::object::shapes::object_shape_identity(obj) }; + if identity != (crate::object::shapes::PROTO_ID_CLASS | u64::from(start)) + && identity != crate::object::shapes::class_proto_id(start) + && identity != crate::object::shapes::PROTO_ID_PER_OBJECT + && super::prototype_chain::object_prototype_is_foreign(obj as usize) + { + return live_chain_answer(value, want); + } } if !super::prototype_chain::any_class_chain_relinked() { return None; diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs index 777325fbf7..62a2842b93 100644 --- a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -848,7 +848,9 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { // walk also follows the generic-origin edge, so a dynamic RHS holding a // generic class (`const C = Gen; x instanceof C`) matches an instance of // one of its specializations. - if let Some(answer) = relinked_instance_chain_answer(value, obj_class_id, class_id) { + if let Some(answer) = + super::relinked_object_chain_answer(obj_ptr, value, obj_class_id, class_id) + { return if answer { true_val } else { false_val }; } if class_chain_reaches(obj_class_id, class_id) { diff --git a/crates/perry-runtime/src/object/method_site/read_holder.rs b/crates/perry-runtime/src/object/method_site/read_holder.rs index 50628f22fa..b939a01787 100644 --- a/crates/perry-runtime/src/object/method_site/read_holder.rs +++ b/crates/perry-runtime/src/object/method_site/read_holder.rs @@ -61,8 +61,8 @@ use super::{next_prototype, ordinary_receiver, WORKER_AGENTS_EXIST}; use crate::object::shapes::{ - object_proto_id, object_shape_descriptor, object_shape_stamp, shape_proto_id, PIC_ID_TOKEN_BIT, - PROTO_ID_CLASS, PROTO_ID_DEFAULT, PROTO_ID_MIXED, PROTO_ID_NULL, PROTO_ID_UNIQUE, + object_shape_descriptor, object_shape_stamp, shape_proto_id, PIC_ID_TOKEN_BIT, PROTO_ID_CLASS, + PROTO_ID_DEFAULT, PROTO_ID_MIXED, PROTO_ID_NULL, PROTO_ID_UNIQUE, }; use crate::object::{ObjectHeader, PicCache, PicCacheSlot}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -568,15 +568,46 @@ unsafe fn hop_admitted(addr: usize, name: &[u8]) -> bool { !key_may_be_accessor(obj, name) } -/// The prototype identity `obj`'s shape records, if it admits: a serial, the -/// default link or null — and equal to what the object says it is. -pub(super) unsafe fn admitted_proto_id(obj: *const ObjectHeader) -> Option { +/// What `obj` says its prototype identity is +/// ([`crate::object::shapes::object_proto_id`]), and the +/// recorded [[Prototype]] word that says it: one read of the word serves +/// both the identity check and the hop ([`next_from_word`]). +#[inline] +pub(super) unsafe fn stated_link(obj: *const ObjectHeader) -> (u64, u64) { + let word = crate::object::shapes::object_prototype_word(obj); + (crate::object::shapes::object_proto_id_for(obj, word), word) +} + +/// [`next_prototype`] of `obj`, given its recorded word as [`stated_link`] +/// read it. +#[inline] +pub(super) unsafe fn next_from_word(obj: *const ObjectHeader, word: u64) -> *const ObjectHeader { + if word == 0 { + return next_prototype(obj); + } + let p = crate::value::JSValue::from_bits(word); + if p.is_pointer() { + p.as_pointer() + } else { + std::ptr::null() + } +} + +/// [`admitted_proto_id`], with `obj`'s recorded word. +unsafe fn admitted_link(obj: *const ObjectHeader) -> Option<(u64, u64)> { let pid = shape_proto_id(object_shape_stamp(obj))?; let serial = pid != PROTO_ID_DEFAULT && pid < crate::object::shapes::PROTO_ID_CLASS; if !(serial || pid == PROTO_ID_DEFAULT || pid == PROTO_ID_NULL) { return None; } - (object_proto_id(obj) == pid).then_some(pid) + let (stated, word) = stated_link(obj); + (stated == pid).then_some((pid, word)) +} + +/// The prototype identity `obj`'s shape records, if it admits: a serial, the +/// default link or null — and equal to what the object says it is. +pub(super) unsafe fn admitted_proto_id(obj: *const ObjectHeader) -> Option { + admitted_link(obj).map(|(pid, _)| pid) } /// A MIXED identity records an explicit serial link. A bare CLASS identity @@ -585,11 +616,12 @@ pub(super) unsafe fn admitted_proto_id(obj: *const ObjectHeader) -> Option /// ShapeId (see the module docs), which the hit's holder compare sees. unsafe fn class_link(recv: *const ObjectHeader) -> Option<*const ObjectHeader> { let pid = shape_proto_id(object_shape_stamp(recv))?; - if object_proto_id(recv) != pid { + let (stated, word) = stated_link(recv); + if stated != pid { return None; } let holder = if (PROTO_ID_MIXED..PROTO_ID_UNIQUE).contains(&pid) { - next_prototype(recv) + next_from_word(recv, word) } else if (PROTO_ID_CLASS..PROTO_ID_MIXED).contains(&pid) { crate::object::class_decl_prototype_object((*recv).class_id) } else { @@ -734,6 +766,8 @@ unsafe fn walk(recv: *const ObjectHeader, name: &[u8], class_first: bool) -> Opt // [[Prototype]] is null for its whole life, whatever its shape's // identity word says, so reaching it ends the chain. let terminal = depth > 1 && current as usize == object_prototype; + // `current`'s recorded word, once read for its identity check. + let mut word = None; let pid = if terminal { PROTO_ID_NULL } else if depth == 1 && class_first { @@ -741,15 +775,27 @@ unsafe fn walk(recv: *const ObjectHeader, name: &[u8], class_first: bool) -> Opt // C.prototype. The collecting class-read hit compares that // pointer on every use; this walk records it as the first hop. crate::object::shapes::PROTO_ID_CLASS - } else if class_first { - let pid = shape_proto_id(object_shape_stamp(current))?; - if (PROTO_ID_MIXED..PROTO_ID_UNIQUE).contains(&pid) && object_proto_id(current) == pid { - pid + } else { + let mixed = if class_first { + let pid = shape_proto_id(object_shape_stamp(current))?; + if (PROTO_ID_MIXED..PROTO_ID_UNIQUE).contains(&pid) { + let (stated, w) = stated_link(current); + word = Some(w); + (stated == pid).then_some(pid) + } else { + None + } } else { - admitted_proto_id(current)? + None + }; + match mixed { + Some(pid) => pid, + None => { + let (pid, w) = admitted_link(current)?; + word = Some(w); + pid + } } - } else { - admitted_proto_id(current)? }; if pid == PROTO_ID_NULL { // `current` is the terminal object, and it lacks `name`. @@ -768,7 +814,10 @@ unsafe fn walk(recv: *const ObjectHeader, name: &[u8], class_first: bool) -> Opt } else if pid == PROTO_ID_DEFAULT { object_prototype as *const ObjectHeader } else { - next_prototype(current) + match word { + Some(w) => next_from_word(current, w), + None => next_prototype(current), + } }; if next.is_null() || next == current || next == recv || !hop_admitted(next as usize, name) { return None; @@ -1606,7 +1655,10 @@ mod tests { crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 1, keys, shape_id, 0); let claimed = shape_proto_id(shape_id).expect("class shape must be stamped"); assert_eq!(claimed, crate::object::shapes::class_proto_id(CID)); - assert_eq!(unsafe { object_proto_id(obj) }, claimed); + assert_eq!( + unsafe { crate::object::shapes::object_proto_id(obj) }, + claimed + ); assert!(claimed >= crate::object::shapes::PROTO_ID_CLASS); assert_eq!(unsafe { admitted_proto_id(obj) }, None); } diff --git a/crates/perry-runtime/src/object/method_site/read_holder/class_read.rs b/crates/perry-runtime/src/object/method_site/read_holder/class_read.rs index cf98ffab90..8f30980ac1 100644 --- a/crates/perry-runtime/src/object/method_site/read_holder/class_read.rs +++ b/crates/perry-runtime/src/object/method_site/read_holder/class_read.rs @@ -99,7 +99,8 @@ fn hop_identity_pins_link(pid: u64) -> bool { /// shape/prototype rule the prime walk used. A changed or exotic link declines. unsafe fn admitted_next(hop: *const ObjectHeader) -> Option { let pid = shape_proto_id(object_shape_stamp(hop))?; - if object_proto_id(hop) != pid { + let (stated, word) = super::stated_link(hop); + if stated != pid { return None; } if !(pid == PROTO_ID_DEFAULT @@ -114,7 +115,7 @@ unsafe fn admitted_next(hop: *const ObjectHeader) -> Option { } else if pid == PROTO_ID_NULL { 0 } else { - next_prototype(hop) as usize + super::next_from_word(hop, word) as usize }; (next != 0).then_some(next) } diff --git a/crates/perry-runtime/src/object/proto_validity.rs b/crates/perry-runtime/src/object/proto_validity.rs index 2477875db4..0ad962af95 100644 --- a/crates/perry-runtime/src/object/proto_validity.rs +++ b/crates/perry-runtime/src/object/proto_validity.rs @@ -264,11 +264,49 @@ unsafe fn ensure_meta_for_mark(obj: usize, flag: u64) -> Option<*mut crate::obje } }); let meta = (*object).meta; - return (!meta.is_null()).then_some(meta); + if meta.is_null() { + return None; + } + if flag == crate::object::OBJECT_META_FLAG_IS_PROTOTYPE { + keep_prototype_in_record(object, meta); + } + return Some(meta); } Some(meta) } +/// A prototype is read on every inherited walk through it, so it keeps its +/// own [[Prototype]] bits in its meta record, the cheaper read +/// (`shapes::object_prototype_word`). The prototype funnel writes both while +/// a record exists; an object linked before it had one (a class declaration +/// prototype, linked to its parent's when it is created) starts its record +/// from the shape's word when it becomes a prototype. Only a linked +/// identity has a word. +/// +/// # Safety +/// `object` is a live `ObjectHeader` and `meta` its record. +unsafe fn keep_prototype_in_record( + object: *mut crate::object::ObjectHeader, + meta: *mut crate::object::ObjectMeta, +) { + if (*meta).prototype != 0 + || !crate::object::shapes::shape_word_may_be_linked((*object).parent_class_id) + { + return; + } + let word = crate::object::shapes::object_prototype_word(object); + if word != 0 { + (*meta).prototype = word; + // GC_STORE_AUDIT(BARRIERED): meta-record prototype slot store (parent + // = the meta record), as in the prototype funnel. + crate::gc::runtime_write_barrier_slot( + meta as usize, + &(*meta).prototype as *const u64 as usize, + word, + ); + } +} + /// # Safety /// `obj` is a live heap address, or 0. #[inline] diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 04395018ee..41c39a8124 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -850,6 +850,13 @@ pub(crate) fn object_has_individual_class_prototype(obj_ptr: usize) -> bool { let Some(obj) = meta_capable_object(obj_ptr) else { return false; }; + // An unlinked identity (the ShapeId says so) is the default, a + // class's or a per-object one: never another class's link, since a + // link of a class instance to anything but its own class's + // declaration prototype is a linked identity. + if !crate::object::shapes::shape_word_may_be_linked((*obj).parent_class_id) { + return false; + } let implied = crate::object::shapes::class_proto_id((*obj).class_id); if implied == crate::object::shapes::PROTO_ID_DEFAULT { return false; @@ -871,16 +878,16 @@ pub(crate) fn object_prototype_is_foreign(obj_ptr: usize) -> bool { let Some(obj) = meta_capable_object(obj_ptr) else { return false; }; + let recorded = crate::object::shapes::object_prototype_word(obj); + if recorded == 0 { + return false; + } // A class object's [[Prototype]] is its heritage (the parent // constructor): its declared static chain. A user relink of one arms // `any_class_chain_relinked`, which the static walks consult. if crate::object::class_registry::is_class_object_ptr(obj.cast()) { return false; } - let recorded = crate::object::shapes::object_prototype_word(obj); - if recorded == 0 { - return false; - } let value = crate::value::JSValue::from_bits(recorded); if !value.is_pointer() { return true; diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 9c06bcbd67..f44614b541 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -1141,17 +1141,92 @@ const _: () = assert!(DICTIONARY_SHAPE_ID_BASE < SHAPE_ID_END); /// stamp arriving on another thread must never alias an id that thread /// allocated for a different shape. Monotonic — ids are NEVER reused, so /// a stale stamp or cache entry can only miss, not falsely hit. -static SHAPE_ID_NEXT: std::sync::atomic::AtomicU32 = - std::sync::atomic::AtomicU32::new(STATIC_SHAPE_ID_END); +static SHAPE_ID_NEXT: [std::sync::atomic::AtomicU32; 3] = [ + std::sync::atomic::AtomicU32::new(STATIC_SHAPE_ID_END), + std::sync::atomic::AtomicU32::new(STATIC_SHAPE_ID_END), + std::sync::atomic::AtomicU32::new(STATIC_SHAPE_ID_END), +]; + +/// The dictionary band's own monotonic counters (see +/// [`DICTIONARY_SHAPE_ID_BASE`]), one per identity kind +/// ([`SHAPE_ID_KIND_SHIFT`]); never reused, each parks at the band's end. +static DICTIONARY_SHAPE_ID_NEXT: [std::sync::atomic::AtomicU32; 3] = [ + std::sync::atomic::AtomicU32::new(DICTIONARY_SHAPE_ID_BASE), + std::sync::atomic::AtomicU32::new(DICTIONARY_SHAPE_ID_BASE), + std::sync::atomic::AtomicU32::new(DICTIONARY_SHAPE_ID_BASE), +]; + +/// The exotic band's own monotonic counters ([`EXOTIC_SHAPE_ID_BASE`]). +static EXOTIC_SHAPE_ID_NEXT: [std::sync::atomic::AtomicU32; 3] = [ + std::sync::atomic::AtomicU32::new(EXOTIC_SHAPE_ID_BASE), + std::sync::atomic::AtomicU32::new(EXOTIC_SHAPE_ID_BASE), + std::sync::atomic::AtomicU32::new(EXOTIC_SHAPE_ID_BASE), +]; + +/// # The identity kind: a ShapeId says what kind of prototype identity it names +/// +/// Bits 20-21 of a ShapeId are its identity KIND ([`proto_id_kind`]): +/// * [`SHAPE_ID_KIND_PLAIN`] (0): an identity that answers by itself and +/// records no prototype: the realm default, a compiled class's declaration +/// prototype, a per-object identity; +/// * [`SHAPE_ID_KIND_WORD`] (1): a LINKED identity with a word naming its +/// prototype (a recorded prototype's serial, `MIXED`, a `UNIQUE` link); +/// * [`SHAPE_ID_KIND_NULL`] (2): a null [[Prototype]]. +/// +/// Every band draws its ids from one counter per kind, and the counters hand +/// out 2^20-id granules in turn (kind 3 is never minted), so the kind is a +/// fact of the id's VALUE, fixed at the mint (`ShapeSlab::insert` asserts +/// it). The static band (`[SHAPE_ID_BASE, STATIC_SHAPE_ID_END)`, 2^20 ids) is +/// one plain granule: the compiler names only class and literal shapes. +/// +/// It is what lets `object_prototype_word` answer the common receiver (a +/// literal, a class instance on its class's prototype) in one compare of the +/// header word, as the receiver's class id did before the prototype moved +/// into the shape, answer a null link from the header, and read the shape +/// record only for a word identity. Granules are page-aligned +/// (`shapes_store` pages hold 2^15 records), so the kinds cost no record +/// memory: a kind's untouched pages are never allocated. +pub(crate) const SHAPE_ID_KIND_SHIFT: u32 = 20; +pub(crate) const SHAPE_ID_KIND_MASK: u32 = 3 << SHAPE_ID_KIND_SHIFT; +pub(crate) const SHAPE_ID_KIND_PLAIN: u32 = 0; +pub(crate) const SHAPE_ID_KIND_WORD: u32 = 1; +pub(crate) const SHAPE_ID_KIND_NULL: u32 = 2; +/// One granule of each kind (and the never-minted fourth). +const SHAPE_ID_KIND_GROUP: u32 = 4 << SHAPE_ID_KIND_SHIFT; +const _: () = assert!(STATIC_SHAPE_ID_END - SHAPE_ID_BASE <= 1 << SHAPE_ID_KIND_SHIFT); +const _: () = assert!(SHAPE_ID_BASE % SHAPE_ID_KIND_GROUP == 0); +const _: () = assert!(DICTIONARY_SHAPE_ID_BASE % SHAPE_ID_KIND_GROUP == 0); +const _: () = assert!(EXOTIC_SHAPE_ID_BASE % SHAPE_ID_KIND_GROUP == 0); +const _: () = assert!(SHAPE_ID_END % SHAPE_ID_KIND_GROUP == 0); + +/// The identity kind `header_word` (an `ObjectHeader`'s ShapeId word, or +/// whatever else it holds) carries in its kind bits. +#[inline(always)] +pub(crate) fn shape_word_kind(header_word: u32) -> u32 { + (header_word & SHAPE_ID_KIND_MASK) >> SHAPE_ID_KIND_SHIFT +} -/// The dictionary band's own monotonic counter (see -/// [`DICTIONARY_SHAPE_ID_BASE`]); never reused, parks at `SHAPE_ID_END`. -static DICTIONARY_SHAPE_ID_NEXT: std::sync::atomic::AtomicU32 = - std::sync::atomic::AtomicU32::new(DICTIONARY_SHAPE_ID_BASE); +/// Does `header_word` possibly name a linked prototype identity (a word or +/// null)? `false` proves the receiver's shape identity is plain: +/// `object_prototype_word` answers from its meta record or 0, without +/// reading the shape record. +#[inline(always)] +pub(crate) fn shape_word_may_be_linked(header_word: u32) -> bool { + header_word & SHAPE_ID_KIND_MASK != 0 +} -/// The exotic band's own monotonic counter ([`EXOTIC_SHAPE_ID_BASE`]). -static EXOTIC_SHAPE_ID_NEXT: std::sync::atomic::AtomicU32 = - std::sync::atomic::AtomicU32::new(EXOTIC_SHAPE_ID_BASE); +/// The ShapeId kind ([`SHAPE_ID_KIND_SHIFT`]) of prototype identity +/// `proto_id`. +#[inline] +pub(crate) fn proto_id_kind(proto_id: u64) -> u32 { + if proto_id == PROTO_ID_NULL { + SHAPE_ID_KIND_NULL + } else if shapes_prototype::proto_id_carries_word(proto_id) { + SHAPE_ID_KIND_WORD + } else { + SHAPE_ID_KIND_PLAIN + } +} static SHAPE_SEMANTIC_NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); @@ -1216,6 +1291,7 @@ pub(crate) enum ShapeDescriptorError { InvalidFacts, } +#[cfg(test)] fn alloc_shape_id_from( next: &std::sync::atomic::AtomicU32, end: u32, @@ -1240,44 +1316,94 @@ fn alloc_shape_id_from( } } -/// An ORDINARY-band ShapeId: every mint except a dictionary shape's. -fn alloc_shape_id() -> Result { - alloc_shape_id_from(&SHAPE_ID_NEXT, DICTIONARY_SHAPE_ID_BASE) +/// [`alloc_shape_id_from`] for one identity kind ([`SHAPE_ID_KIND_SHIFT`]): +/// an id whose kind bits are `kind`. A counter that reaches another kind's +/// granule skips to its own next one, so a band's counters interleave +/// granule by granule. +fn alloc_shape_id_of_kind( + next: &std::sync::atomic::AtomicU32, + end: u32, + kind: u32, +) -> Result { + use std::sync::atomic::Ordering; + loop { + let current = next.load(Ordering::Relaxed); + let mut id = current; + if shape_word_kind(id) != kind { + // This kind's granule in the current group, or the next group's. + id = (id & !(SHAPE_ID_KIND_GROUP - 1)) | (kind << SHAPE_ID_KIND_SHIFT); + if id < current { + id = id.saturating_add(SHAPE_ID_KIND_GROUP); + } + } + if id >= end { + next.store(end, Ordering::Relaxed); + return Err(ShapeIdExhausted); + } + if next + .compare_exchange_weak(current, id + 1, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + return Ok(id); + } + } +} + +/// An ORDINARY-band ShapeId for a shape whose identity is `proto_id`: every +/// mint except a dictionary or exotic shape's. +fn alloc_shape_id(proto_id: u64) -> Result { + let kind = proto_id_kind(proto_id); + alloc_shape_id_of_kind( + &SHAPE_ID_NEXT[kind as usize], + DICTIONARY_SHAPE_ID_BASE, + kind, + ) } /// A dictionary-band ShapeId ([`DICTIONARY_SHAPE_ID_BASE`]). -fn alloc_dictionary_shape_id() -> Result { - alloc_shape_id_from(&DICTIONARY_SHAPE_ID_NEXT, EXOTIC_SHAPE_ID_BASE) +fn alloc_dictionary_shape_id(proto_id: u64) -> Result { + let kind = proto_id_kind(proto_id); + alloc_shape_id_of_kind( + &DICTIONARY_SHAPE_ID_NEXT[kind as usize], + EXOTIC_SHAPE_ID_BASE, + kind, + ) } /// An exotic-band ShapeId ([`EXOTIC_SHAPE_ID_BASE`]). -fn alloc_exotic_shape_id() -> Result { - alloc_shape_id_from(&EXOTIC_SHAPE_ID_NEXT, SHAPE_ID_END) +fn alloc_exotic_shape_id(proto_id: u64) -> Result { + let kind = proto_id_kind(proto_id); + alloc_shape_id_of_kind(&EXOTIC_SHAPE_ID_NEXT[kind as usize], SHAPE_ID_END, kind) } /// The band a new shape's id is drawn from is decided by its generation /// namespace: a dictionary generation (bit 62 set, bit 63 clear — /// `dictionary::next_generation`) mints in the dictionary band, everything -/// else in the ordinary band. -fn alloc_shape_id_for_generation(semantic_generation: u64) -> Result { +/// else in the ordinary band. Its identity decides the kind. +fn alloc_shape_id_for_generation( + semantic_generation: u64, + proto_id: u64, +) -> Result { const DETERMINISTIC_BIT: u64 = 1 << 63; let tag = crate::object::dictionary::DICTIONARY_GENERATION_TAG; if semantic_generation & (DETERMINISTIC_BIT | tag) == tag { - alloc_dictionary_shape_id() + alloc_dictionary_shape_id(proto_id) } else { - alloc_shape_id() + alloc_shape_id(proto_id) } } -/// The next ShapeId this process would hand out. +/// The ordinary-band ids this process has handed out, as a counter. /// /// Tests assert the DELTA across a workload, because ids come from a 2^30 -/// counter that is never reused and parks (fail-stop) at the end: a path that +/// range that is never reused and parks (fail-stop) at the end: a path that /// mints one id per operation is a process-LIFETIME bug, not merely a memory /// cost, and nothing in the program's output ever reveals it. #[cfg(test)] pub(crate) fn test_shape_id_counter() -> u32 { - SHAPE_ID_NEXT.load(std::sync::atomic::Ordering::Relaxed) + SHAPE_ID_NEXT.iter().fold(0u32, |sum, next| { + sum.wrapping_add(next.load(std::sync::atomic::Ordering::Relaxed)) + }) } /// Get or create the exact structural descriptor. The public allocation and @@ -1708,9 +1834,9 @@ fn shape_descriptor_intern_with_special_mode( let id = match adopted { Some(id) => id, None => if object_kind.is_exotic() { - alloc_exotic_shape_id() + alloc_exotic_shape_id(proto_id) } else { - alloc_shape_id_for_generation(semantic_generation) + alloc_shape_id_for_generation(semantic_generation, proto_id) } .map_err(|_| ShapeDescriptorError::IdExhausted)?, }; @@ -4039,6 +4165,7 @@ pub(crate) unsafe fn object_shape_identity(obj: *const crate::object::ObjectHead /// The first synthetic class id (`class_registry::prototype_objects`): a /// plain function constructor's instances. +#[cfg(test)] pub(crate) const SYNTHETIC_CLASS_ID_BASE: u32 = crate::object::class_registry::prototype_objects::SYNTHETIC_CLASS_ID_BASE; @@ -4046,8 +4173,10 @@ pub(crate) const SYNTHETIC_CLASS_ID_BASE: u32 = /// prototype is the default or the class's). A receiver that has a meta /// record has it there (the prototype funnel writes both, and a /// `PROTO_ID_PER_OBJECT` receiver's shape answers nothing); a meta-less -/// function-constructor instance reads it from its shape's identity word -/// (`shapes_prototype`). Allocation-free. +/// receiver reads it from its shape's identity word (`shapes_prototype`). +/// Only a word identity has one, and the ShapeId says which those are +/// ([`SHAPE_ID_KIND_SHIFT`]): every other receiver answers 0 (or null) from +/// its header word, with no shape-record read. Allocation-free. /// /// # Safety /// `obj` is a live `ObjectHeader`. @@ -4057,12 +4186,43 @@ pub(crate) const SYNTHETIC_CLASS_ID_BASE: u32 = /// `OBJ_FLAG_NULL_PROTO`), which answers 0 as it always has: every reader /// tests that header bit for the born-null case, and a recorded null would /// send it down the re-prototyped-receiver paths instead. -#[inline] +#[inline(always)] pub(crate) unsafe fn object_prototype_word(obj: *const crate::object::ObjectHeader) -> u64 { let meta = (*obj).meta; if !meta.is_null() && (*meta).prototype != 0 { return (*meta).prototype; } + // A default, class or per-object identity answers 0 from the id alone, + // and a null link from the header. + let word = (*obj).parent_class_id; + match shape_word_kind(word) { + SHAPE_ID_KIND_PLAIN => 0, + SHAPE_ID_KIND_NULL if is_shape_id(word) => null_linked_prototype_word(obj), + _ => linked_object_prototype_word(obj), + } +} + +/// [`object_prototype_word`] of a meta-less receiver whose ShapeId names the +/// null identity: `TAG_NULL`, or 0 on a cell born null. +/// +/// # Safety +/// `obj` is a live `ObjectHeader`. +#[inline(never)] +unsafe fn null_linked_prototype_word(obj: *const crate::object::ObjectHeader) -> u64 { + match crate::value::addr_class::try_read_gc_header(obj as usize) { + Some(header) if header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 => 0, + _ => crate::value::TAG_NULL, + } +} + +/// [`object_prototype_word`] of a meta-less receiver whose ShapeId may name +/// a linked identity: the shape record's identity, and that identity's word. +/// Out of line, so every caller's common answer stays a few inlined compares. +/// +/// # Safety +/// `obj` is a live `ObjectHeader`. +#[inline(never)] +unsafe fn linked_object_prototype_word(obj: *const crate::object::ObjectHeader) -> u64 { // The agent directory read: never null, an absent id reads the empty // record (identity 0, the default). let proto_id = (*ShapeSlab::agent_record(object_shape_stamp(obj))).proto_id; @@ -4116,8 +4276,11 @@ pub(crate) unsafe fn object_proto_id_for( // A compiled class instance linked to its own class's declaration // prototype (runtime wiring of a native-base subclass instance) has // exactly the prototype its class implies: the class identity, so it - // shares its class's shapes and its class surface stays exact. - if class != 0 { + // shares its class's shapes and its class surface stays exact. A + // declaration prototype is an object of its own class + // (`class_decl_prototype_value`), so only such a prototype can be + // it: the registry is asked only then. + if class != 0 && bits_name_object_of_class(recorded, class) { let decl = crate::object::class_registry::class_decl_prototype_object(class); if !decl.is_null() && crate::value::js_nanbox_pointer(decl as i64).to_bits() == recorded { @@ -4159,6 +4322,24 @@ pub(crate) unsafe fn object_proto_id_for( PROTO_ID_DEFAULT } +/// Do `bits` name a live ordinary object whose class id is `class`? +/// +/// # Safety +/// `bits` are a recorded [[Prototype]] word. +#[inline] +unsafe fn bits_name_object_of_class(bits: u64, class: u32) -> bool { + let value = crate::value::JSValue::from_bits(bits); + if !value.is_pointer() { + return false; + } + let addr = value.as_pointer::() as usize; + matches!( + crate::value::addr_class::try_read_gc_header(addr), + Some(header) if header.obj_type == crate::gc::GC_TYPE_OBJECT + && (*(addr as *const crate::object::ObjectHeader)).class_id == class + ) +} + /// The prototype identity recorded in shape `id`, or `None` for an id with no /// descriptor. One slab read; no descriptor copy. #[inline] @@ -5176,7 +5357,10 @@ pub(crate) fn shape_table_census() -> Vec { )); // Ids ever minted by this process: the slab is indexed by id, so the gap // between this and `shapes.descriptors` is what chunk release reclaims. - let minted = SHAPE_ID_NEXT.load(std::sync::atomic::Ordering::Relaxed) - STATIC_SHAPE_ID_END; + let minted = SHAPE_ID_NEXT + .iter() + .map(|next| next.load(std::sync::atomic::Ordering::Relaxed) - STATIC_SHAPE_ID_END) + .sum::(); rows.push(("shapes.ids_minted(process)", minted as usize, 0)); // How the descriptor population splits by [[Prototype]] identity kind, // and how many distinct prototype identities it names: what the diff --git a/crates/perry-runtime/src/object/shapes_slot_list.rs b/crates/perry-runtime/src/object/shapes_slot_list.rs index 4ad2e18a03..0eea73c872 100644 --- a/crates/perry-runtime/src/object/shapes_slot_list.rs +++ b/crates/perry-runtime/src/object/shapes_slot_list.rs @@ -617,7 +617,7 @@ pub(crate) unsafe fn rekey_stable_tombstone_shape_after_squeeze( if !super::is_shape_id(old_id) { return None; } - let new_id = super::alloc_shape_id().ok()?; + let new_id = super::alloc_shape_id(current.proto_id).ok()?; let generation = super::SHAPE_SEMANTIC_NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if generation == 0 { super::shape_id_exhausted_abort(); @@ -1029,7 +1029,7 @@ fn rekey_predecessor_for_delete( { return 0; } - let Ok(id) = super::alloc_shape_id() else { + let Ok(id) = super::alloc_shape_id(live.proto_id) else { return 0; }; let mut inner = table.inner.borrow_mut(); @@ -1088,7 +1088,7 @@ fn mint_detached_delete_successor( hole_count: u32, proto_id: u64, ) -> u32 { - let Ok(id) = super::alloc_shape_id() else { + let Ok(id) = super::alloc_shape_id(proto_id) else { return 0; }; let mut record = ShapeRecord::new( diff --git a/crates/perry-runtime/src/object/shapes_store.rs b/crates/perry-runtime/src/object/shapes_store.rs index a001960693..9686230368 100644 --- a/crates/perry-runtime/src/object/shapes_store.rs +++ b/crates/perry-runtime/src/object/shapes_store.rs @@ -1179,6 +1179,17 @@ impl ShapeSlab { pub(super) fn insert(&mut self, id: u32, mut record: ShapeRecord) -> Option { let (band, index) = locate(id); assert!(band < 3, "ShapeSlab::insert: id outside the ShapeId range"); + // The id's identity kind is a fact the prototype readers trust + // without reading this record (`shapes::SHAPE_ID_KIND_SHIFT`): a + // plain or null kind names exactly that identity. (The other kinds + // send a reader to this record.) + let kind = super::shape_word_kind(id); + assert!( + !matches!(kind, super::SHAPE_ID_KIND_PLAIN | super::SHAPE_ID_KIND_NULL) + || kind == super::proto_id_kind(record.proto_id), + "ShapeSlab::insert: identity {:#x} under ShapeId {id:#x} of kind {kind}", + record.proto_id + ); let band = band as u8; record.set(RECORD_FLAG_PRESENT, true); let (page, chunk, slot) = Self::split(index); diff --git a/crates/perry-runtime/src/object/shapes_store_tests.rs b/crates/perry-runtime/src/object/shapes_store_tests.rs index ad77bf8e28..1f8595d319 100644 --- a/crates/perry-runtime/src/object/shapes_store_tests.rs +++ b/crates/perry-runtime/src/object/shapes_store_tests.rs @@ -96,9 +96,9 @@ fn the_agent_directory_is_the_agent_slab_and_an_absent_id_reads_empty() { } let table = &crate::state::state().shapes; let ids = [ - alloc_shape_id().unwrap(), - alloc_dictionary_shape_id().unwrap(), - alloc_exotic_shape_id().unwrap(), + alloc_shape_id(super::super::PROTO_ID_DEFAULT).unwrap(), + alloc_dictionary_shape_id(super::super::PROTO_ID_DEFAULT).unwrap(), + alloc_exotic_shape_id(super::super::PROTO_ID_DEFAULT).unwrap(), ]; let reps = [0b01u64, 0b01 << 2, 0b10 << 4]; for (&id, &rep) in ids.iter().zip(&reps) { diff --git a/crates/perry-runtime/src/object/shapes_tests.rs b/crates/perry-runtime/src/object/shapes_tests.rs index f6c83b5194..6f1b90096b 100644 --- a/crates/perry-runtime/src/object/shapes_tests.rs +++ b/crates/perry-runtime/src/object/shapes_tests.rs @@ -530,6 +530,63 @@ mod descriptor_tests_8067 { } } + /// The identity kind is a fact of the id's value: every band mints each + /// kind of identity under its own kind bits, which is what lets + /// `object_prototype_word` skip the record read. + #[test] + fn a_shape_id_says_what_kind_of_prototype_identity_it_names() { + let cases = [ + (PROTO_ID_DEFAULT, SHAPE_ID_KIND_PLAIN), + (PROTO_ID_CLASS | 7, SHAPE_ID_KIND_PLAIN), + (PROTO_ID_PER_OBJECT, SHAPE_ID_KIND_PLAIN), + (42, SHAPE_ID_KIND_WORD), + ( + PROTO_ID_MIXED | (7 << PROTO_ID_MIXED_SERIAL_BITS) | 42, + SHAPE_ID_KIND_WORD, + ), + (PROTO_ID_UNIQUE | 5, SHAPE_ID_KIND_WORD), + (PROTO_ID_NULL, SHAPE_ID_KIND_NULL), + ]; + for (proto_id, kind) in cases { + assert_eq!(proto_id_kind(proto_id), kind, "{proto_id:#x}"); + for _ in 0..3 { + for id in [ + alloc_shape_id(proto_id).unwrap(), + alloc_dictionary_shape_id(proto_id).unwrap(), + alloc_exotic_shape_id(proto_id).unwrap(), + ] { + assert_eq!(shape_word_kind(id), kind, "{id:#x}"); + assert_eq!(shape_word_may_be_linked(id), kind != 0, "{id:#x}"); + } + } + } + // A counter that reaches another kind's granule skips to its own next + // one, and one that would skip past the band's end parks there. + let g = 1u32 << SHAPE_ID_KIND_SHIFT; + let next = std::sync::atomic::AtomicU32::new(SHAPE_ID_BASE + g - 1); + let plain = |next: &std::sync::atomic::AtomicU32| { + alloc_shape_id_of_kind(next, SHAPE_ID_END, SHAPE_ID_KIND_PLAIN) + }; + assert_eq!(plain(&next), Ok(SHAPE_ID_BASE + g - 1)); + assert_eq!(plain(&next), Ok(SHAPE_ID_BASE + 4 * g)); + let next = std::sync::atomic::AtomicU32::new(SHAPE_ID_BASE + g); + assert_eq!( + alloc_shape_id_of_kind(&next, SHAPE_ID_END, SHAPE_ID_KIND_NULL), + Ok(SHAPE_ID_BASE + 2 * g) + ); + let next = std::sync::atomic::AtomicU32::new(SHAPE_ID_BASE + 3 * g); + assert_eq!( + alloc_shape_id_of_kind(&next, SHAPE_ID_END, SHAPE_ID_KIND_WORD), + Ok(SHAPE_ID_BASE + 5 * g) + ); + let next = std::sync::atomic::AtomicU32::new(SHAPE_ID_END - 1); + assert_eq!(plain(&next), Err(ShapeIdExhausted)); + assert_eq!( + next.load(std::sync::atomic::Ordering::Relaxed), + SHAPE_ID_END + ); + } + #[test] fn exhaustion_parks_without_reuse_or_alias() { let next = std::sync::atomic::AtomicU32::new(SHAPE_ID_END - 1); @@ -566,7 +623,8 @@ mod descriptor_tests_8067 { let keys = 0x8067_0000_0000_1700usize; let local = shape_descriptor_ensure(keys as *const ArrayHeader, 1, 1) .expect("shape range unexpectedly exhausted"); - let external = alloc_shape_id().expect("shape range unexpectedly exhausted"); + let external = + alloc_shape_id(PROTO_ID_DEFAULT).expect("shape range unexpectedly exhausted"); assert!(shapes_slot_list::install_external_shape_id( external, keys as *const ArrayHeader, From 46f35ea94f9dd105bf190cff9773ba9374ae1f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 22:15:35 +0200 Subject: [PATCH 4/4] changelog: key the prototype-link-flags fragment to PR 11834 --- ...DING-prototype-link-flags.md => 11834-prototype-link-flags.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{PENDING-prototype-link-flags.md => 11834-prototype-link-flags.md} (100%) diff --git a/changelog.d/PENDING-prototype-link-flags.md b/changelog.d/11834-prototype-link-flags.md similarity index 100% rename from changelog.d/PENDING-prototype-link-flags.md rename to changelog.d/11834-prototype-link-flags.md