From 277761bc1543b21149f106df880557ebb9eb3e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 1 Nov 2026 09:42:09 +0000 Subject: [PATCH 1/4] fix(runtime): a null [[Prototype]] is the shape's identity, not a header bit The link had two records: the shape's identity and OBJ_FLAG_NULL_PROTO, a header bit set at birth that the identity was derived from and ~20 readers tested directly. The bit was sticky across setPrototypeOf, a later null link never set it, and a declared-keys class family tagged null (require.cache) was registered with its class's identity while its header said null. So readers disagreed: a thenable on a relinked born-null object was not followed, util.inspect called a relinked object "null prototype" and missed setPrototypeOf(x, null), and require.cache inherited Object.prototype's methods. The shape is now the one record. A born-null birth publishes its first shape with the null identity passed in; a layout tagged null registers the null identity for class families too; a module namespace takes its null link through the prototype funnel. Every reader asks object_prototype_is_null / cell_prototype_is_null (the recorded prototype, else the shape), which answer TAG_NULL for any null link. The bit, its store-lane gates and its realm-image header word are gone; 0x40 is free. --- crates/perry-abi/src/lib.rs | 4 +- crates/perry-abi/src/realm_header.rs | 12 +-- .../src/codegen/realm_image/functions.rs | 2 +- crates/perry-codegen/src/layout_desc.rs | 4 +- .../perry-runtime/src/builtins/formatting.rs | 8 +- .../builtins/formatting/prototype_equality.rs | 4 +- .../src/fs/received_diagnostic_tests.rs | 32 ++++--- crates/perry-runtime/src/gc/types.rs | 3 - crates/perry-runtime/src/json/stringify.rs | 2 +- .../src/json/stringify_data_record.rs | 2 +- .../src/json/stringify_nested_records.rs | 2 +- .../src/json/stringify_record_output.rs | 2 +- .../src/json/stringify_shape_template.rs | 2 +- .../src/json/stringify_tojson_probe.rs | 6 +- .../perry-runtime/src/native_payload_birth.rs | 9 +- .../src/node_stream_event_emitter_shape.rs | 3 +- crates/perry-runtime/src/object/alloc.rs | 8 +- .../perry-runtime/src/object/alloc_basic.rs | 24 +++-- crates/perry-runtime/src/object/assign.rs | 5 +- .../src/object/descriptor_state.rs | 14 +-- .../src/object/field_get_set/for_in_stable.rs | 15 +-- .../src/object/field_set_by_name.rs | 5 +- .../src/object/field_set_by_name/tail.rs | 5 - .../src/object/iterator_prototypes.rs | 8 +- .../perry-runtime/src/object/layout_desc.rs | 28 +++--- .../src/object/layout_desc/registration.rs | 14 ++- crates/perry-runtime/src/object/mod.rs | 19 ++++ .../src/object/namespace_create.rs | 13 ++- .../object/native_call_method/object_proto.rs | 4 +- .../src/object/object_literal_ops.rs | 3 +- .../src/object/object_ops/prototype.rs | 12 ++- .../src/object/property_ops/set.rs | 21 ++-- .../src/object/proto_validity.rs | 5 +- .../src/object/prototype_chain.rs | 31 +++--- crates/perry-runtime/src/object/shapes.rs | 96 +++++++++++++------ .../src/object/shapes_class_role.rs | 4 +- .../src/object/shapes_store_kind.rs | 18 +--- .../perry-runtime/src/promise/then_probe.rs | 6 +- crates/perry-runtime/src/proxy.rs | 1 - .../perry-runtime/src/validators/received.rs | 9 +- .../src/value/to_string_primitive.rs | 9 +- .../test_gap_null_prototype_require_cache.cts | 8 ++ ...test_gap_null_prototype_shape_authority.ts | 34 +++++++ 43 files changed, 292 insertions(+), 224 deletions(-) create mode 100644 test-files/test_gap_null_prototype_require_cache.cts create mode 100644 test-files/test_gap_null_prototype_shape_authority.ts diff --git a/crates/perry-abi/src/lib.rs b/crates/perry-abi/src/lib.rs index 1241e03dfac..57a9cbdb2f9 100644 --- a/crates/perry-abi/src/lib.rs +++ b/crates/perry-abi/src/lib.rs @@ -328,8 +328,8 @@ pub const GC_OBJECT_CALLABLE: u16 = 0x0020; /// Heap-birth facts shared by runtime allocation and realm-cell emission. /// These are existing meanings in GcHeader::_reserved, never image markers. pub const OBJ_FLAG_PLAIN_ORDINARY: u16 = 0x0200; -/// An object has a null [[Prototype]]. -pub const OBJ_FLAG_NULL_PROTO: u16 = 0x40; +// 0x40 was `OBJ_FLAG_NULL_PROTO`: a null [[Prototype]] is the shape's +// identity (`PROTO_ID_NULL`), not a header bit. The bit is free. /// At least one own property carries non-default descriptor attributes. pub const OBJ_FLAG_HAS_DESCRIPTORS: u16 = 0x800; /// An array has one front-reserve pointer to its key-attribute array. diff --git a/crates/perry-abi/src/realm_header.rs b/crates/perry-abi/src/realm_header.rs index c24a5da3c24..5d1f58aa1ac 100644 --- a/crates/perry-abi/src/realm_header.rs +++ b/crates/perry-abi/src/realm_header.rs @@ -14,8 +14,8 @@ use crate::{ GC_CLOSURE_NON_CONSTRUCTABLE, GC_LAYOUT_ALL_POINTERS, GC_LAYOUT_POINTER_FREE, GC_LAYOUT_SIDE_MASK, GC_OBJECT_CALLABLE, NATIVE_MODULE_CLASS_ID, OBJ_FLAG_FROZEN, - OBJ_FLAG_HAS_DESCRIPTORS, OBJ_FLAG_NEVER_MOVE, OBJ_FLAG_NO_EXTEND, OBJ_FLAG_NULL_PROTO, - OBJ_FLAG_PLAIN_ORDINARY, OBJ_FLAG_SEALED, OBJ_FLAG_TYPED_ARRAY_PROTO, + OBJ_FLAG_HAS_DESCRIPTORS, OBJ_FLAG_NEVER_MOVE, OBJ_FLAG_NO_EXTEND, OBJ_FLAG_PLAIN_ORDINARY, + OBJ_FLAG_SEALED, OBJ_FLAG_TYPED_ARRAY_PROTO, }; /// The integrity level a birth leaves, as `Object.preventExtensions`, @@ -127,7 +127,8 @@ pub enum PrototypeMark { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ObjectBirth { pub exotic: ObjectExotic, - /// The `[[Prototype]]` is null. + /// The `[[Prototype]]` is null: the cell is born on a layout whose + /// identity is null. It sets no header bit (the shape is the authority). pub null_prototype: bool, pub layout: ObjectLayout, pub prototype_mark: PrototypeMark, @@ -189,11 +190,6 @@ pub const fn object_reserved(birth: ObjectBirth) -> u16 { ObjectExotic::Ordinary => { integrity | never_move - | if birth.null_prototype { - OBJ_FLAG_NULL_PROTO - } else { - 0 - } | if birth.typed_array_prototype { OBJ_FLAG_TYPED_ARRAY_PROTO } else { diff --git a/crates/perry-codegen/src/codegen/realm_image/functions.rs b/crates/perry-codegen/src/codegen/realm_image/functions.rs index b0e6efafb7d..ed3522aca83 100644 --- a/crates/perry-codegen/src/codegen/realm_image/functions.rs +++ b/crates/perry-codegen/src/codegen/realm_image/functions.rs @@ -65,7 +65,7 @@ pub(crate) fn emit_function( exported: false, class_id: 0, layout_word: word, - reserved: perry_abi::OBJ_FLAG_NULL_PROTO | perry_abi::OBJ_FLAG_PLAIN_ORDINARY, + reserved: perry_abi::OBJ_FLAG_PLAIN_ORDINARY, meta: None, slots: &[prototype], }, diff --git a/crates/perry-codegen/src/layout_desc.rs b/crates/perry-codegen/src/layout_desc.rs index df794216be4..5a4a860baec 100644 --- a/crates/perry-codegen/src/layout_desc.rs +++ b/crates/perry-codegen/src/layout_desc.rs @@ -810,7 +810,9 @@ fn emit_desc( let word = crate::layout_ordinals::descriptor_word( desc_name, format!("{keys:?}|{family}|{slot_count}|{class:?}"), - 0, + // A family the descriptor links to null (`require.cache`) carries + // the null identity, which only a null-kind word names. + if proto == 1 { 2 } else { 0 }, true, // Both nominal and admitted classless families carry their DPA word. true, diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index bffe9bdaa76..e78ee837683 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -1144,12 +1144,8 @@ unsafe fn format_object_as_json( }; // Node prefixes a null-prototype object with `[Object: null prototype]` // (e.g. `Object.create(null)`), since it has no constructor to name. - // The flag is set at allocation by `js_object_alloc_null_proto`. - let is_null_proto = { - let gc = - (obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 - }; + // The object's shape (or recorded prototype) says whether it has one. + let is_null_proto = crate::object::shapes::cell_prototype_is_null(obj_ptr as usize); // The display prefix before the `{ … }` body: a real class/constructor // name when present, otherwise `[Object: null prototype]` for a // null-proto plain object, otherwise nothing. (Distinct from diff --git a/crates/perry-runtime/src/builtins/formatting/prototype_equality.rs b/crates/perry-runtime/src/builtins/formatting/prototype_equality.rs index 987d87709bc..e549bcaa300 100644 --- a/crates/perry-runtime/src/builtins/formatting/prototype_equality.rs +++ b/crates/perry-runtime/src/builtins/formatting/prototype_equality.rs @@ -63,9 +63,7 @@ pub(super) fn prototype_token(value: f64) -> Option { } unsafe { - let gc_header = - (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + if crate::object::shapes::cell_prototype_is_null(addr) { return Some(TAG_NULL); } let obj = addr as *const crate::object::ObjectHeader; diff --git a/crates/perry-runtime/src/fs/received_diagnostic_tests.rs b/crates/perry-runtime/src/fs/received_diagnostic_tests.rs index 8e3f2a5759e..17b99039ee1 100644 --- a/crates/perry-runtime/src/fs/received_diagnostic_tests.rs +++ b/crates/perry-runtime/src/fs/received_diagnostic_tests.rs @@ -1165,25 +1165,29 @@ native_fallback_tests!(received_native_data_view_fallback, "DataView"); native_fallback_tests!(received_native_uint8_fallback, "Uint8Array"); #[test] -fn received_native_null_born_typed_array() { - with_received_intrinsic_constructor("Int16Array", |value, _| { +fn received_native_null_linked_typed_array() { + with_received_intrinsic_constructor("Int16Array", |value, prototype| { let scope = crate::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(value); let addr = crate::value::addr_class::object_ref_addr(value.get_nanbox_f64()); assert!(crate::object::prototype_chain::object_static_prototype(addr).is_none()); - let header = unsafe { crate::value::addr_class::try_read_tracked_gc_header(addr) } - .expect("fixture needs a tracked arena header"); - let original = unsafe { (*header.as_ptr())._reserved }; - unsafe { - (*header.as_ptr())._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; - } - let result = describe_received(value.get_nanbox_f64()); + // A typed array reaches a null [[Prototype]] only by a link, which + // records it (`object_static_prototype`). + crate::object::js_object_set_prototype_of( + value.get_nanbox_f64(), + f64::from_bits(crate::value::TAG_NULL), + ); let addr = crate::value::addr_class::object_ref_addr(value.get_nanbox_f64()); - let header = unsafe { crate::value::addr_class::try_read_tracked_gc_header(addr) }.unwrap(); - unsafe { - (*header.as_ptr())._reserved = original; - } - assert_eq!(result, "[Int16Array: null prototype]"); + assert_eq!( + crate::object::prototype_chain::object_static_prototype(addr), + Some(crate::value::TAG_NULL) + ); + assert_eq!( + describe_received(value.get_nanbox_f64()), + "[Int16Array: null prototype]" + ); + // The fixture checks the restored value afterwards. + crate::object::js_object_set_prototype_of(value.get_nanbox_f64(), prototype); }); } diff --git a/crates/perry-runtime/src/gc/types.rs b/crates/perry-runtime/src/gc/types.rs index 1ec984cadf4..a5f1f7d3d4c 100644 --- a/crates/perry-runtime/src/gc/types.rs +++ b/crates/perry-runtime/src/gc/types.rs @@ -1510,7 +1510,6 @@ mod validation; pub(crate) use validation::{validate_gc_type_info, validate_gc_type_metadata}; - // Flag constants pub const GC_FLAG_MARKED: u8 = 0x01; pub const GC_FLAG_ARENA: u8 = 0x02; @@ -1639,7 +1638,6 @@ pub use perry_abi::{OBJ_FLAG_FROZEN, OBJ_FLAG_NO_EXTEND, OBJ_FLAG_SEALED}; /// objects (`OBJ_FLAG_RESERVED_BIT_MAP_SEE_DOC`); arrays and byte owners give /// it other meanings, so every reader checks `obj_type` first. Set-only. pub(crate) use perry_abi::OBJ_FLAG_NEVER_MOVE; -pub use perry_abi::OBJ_FLAG_NULL_PROTO; /// I1 companion (`gc::never_move::note_memo_named`): a site memo stores /// this object's address. If a class relink displaces it from a class /// identity, the retirement pins it, so the memo keeps reading this object. @@ -1789,7 +1787,6 @@ pub(crate) const GC_OBJECT_CALLABLE_IS_DISJOINT: bool = GC_OBJECT_CALLABLE | OBJ_FLAG_FROZEN | OBJ_FLAG_SEALED | OBJ_FLAG_NO_EXTEND - | OBJ_FLAG_NULL_PROTO | OBJ_FLAG_NEVER_MOVE | OBJ_FLAG_TYPED_ARRAY_PROTO | OBJ_FLAG_PLAIN_ORDINARY diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index 1dc9942b5d4..20d029c002b 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -1079,7 +1079,7 @@ pub(crate) unsafe fn stringify_array_depth(ptr: *const u8, buf: &mut String, dep // indices or move the array. Raw slot reads (including template probing) // cannot preserve those effects. Keep this walk outside the dense path. if crate::array::array_iteration_is_exotic_resolved(arr) - || crate::object::prototype_chain::object_static_prototype(arr as usize).is_some() + || crate::object::prototype_chain::object_static_prototype_is_linked(arr as usize) { stringify_exotic_array(arr, len, buf, depth); STRINGIFY_STACK.with(|s| s.borrow_mut().pop()); diff --git a/crates/perry-runtime/src/json/stringify_data_record.rs b/crates/perry-runtime/src/json/stringify_data_record.rs index 5e4d693e2da..7fbf5a83a21 100644 --- a/crates/perry-runtime/src/json/stringify_data_record.rs +++ b/crates/perry-runtime/src/json/stringify_data_record.rs @@ -152,7 +152,7 @@ pub(super) unsafe fn try_emit_with_live( return false; }; if !template.own_keys_exclude_to_json - || crate::object::prototype_chain::object_static_prototype(obj as usize).is_some() + || crate::object::prototype_chain::object_static_prototype_is_linked(obj as usize) { return false; } diff --git a/crates/perry-runtime/src/json/stringify_nested_records.rs b/crates/perry-runtime/src/json/stringify_nested_records.rs index c677dfa31b4..4cd07e5b547 100644 --- a/crates/perry-runtime/src/json/stringify_nested_records.rs +++ b/crates/perry-runtime/src/json/stringify_nested_records.rs @@ -35,7 +35,7 @@ unsafe fn ordinary_object(bits: u64) -> Option<(*const crate::ObjectHeader, usiz || (header.size as usize) < crate::gc::GC_HEADER_SIZE + crate::object::header::meta_form_alloc_bytes(0) || !super::stringify_tojson_probe::object_is_plain_record(obj) - || crate::object::prototype_chain::object_static_prototype(obj as usize).is_some() + || crate::object::prototype_chain::object_static_prototype_is_linked(obj as usize) { return None; } diff --git a/crates/perry-runtime/src/json/stringify_record_output.rs b/crates/perry-runtime/src/json/stringify_record_output.rs index 6b90c7e47cf..f7577055fe3 100644 --- a/crates/perry-runtime/src/json/stringify_record_output.rs +++ b/crates/perry-runtime/src/json/stringify_record_output.rs @@ -249,7 +249,7 @@ unsafe fn dense_array(bits: u64) -> Option<(*const crate::ArrayHeader, usize)> { || len > (*arr).capacity as usize || (header.size as usize) < crate::gc::GC_HEADER_SIZE + ARRAY_BYTES + len * 8 || (crate::object::prototype_chain::array_static_proto_recorded() - && crate::object::prototype_chain::object_static_prototype(arr as usize).is_some()) + && crate::object::prototype_chain::object_static_prototype_is_linked(arr as usize)) { return None; } diff --git a/crates/perry-runtime/src/json/stringify_shape_template.rs b/crates/perry-runtime/src/json/stringify_shape_template.rs index 66c584b2ba0..82f8e64a8a7 100644 --- a/crates/perry-runtime/src/json/stringify_shape_template.rs +++ b/crates/perry-runtime/src/json/stringify_shape_template.rs @@ -396,7 +396,7 @@ pub(crate) unsafe fn try_emit_shape_element( *data_record_global_proof = false; if !global_to_json_absent || !super::stringify_tojson_probe::object_is_plain_record(obj) - || crate::object::prototype_chain::object_static_prototype(obj as usize).is_some() + || crate::object::prototype_chain::object_static_prototype_is_linked(obj as usize) { return false; } diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe.rs b/crates/perry-runtime/src/json/stringify_tojson_probe.rs index 74961fff6b7..d73d43b1119 100644 --- a/crates/perry-runtime/src/json/stringify_tojson_probe.rs +++ b/crates/perry-runtime/src/json/stringify_tojson_probe.rs @@ -235,7 +235,7 @@ unsafe fn compute_object_proto_tojson_state() -> u8 { // A class-linked or re-prototyped Object.prototype is out of the ordinary // enough to always defer to the slow path. if (*proto).class_id != 0 - || crate::object::prototype_chain::object_static_prototype(proto_ptr).is_some() + || crate::object::prototype_chain::object_static_prototype_is_linked(proto_ptr) { return PROTO_TOJSON_PRESENT; } @@ -609,7 +609,7 @@ pub(super) unsafe fn to_json_definitely_absent_after_own_keys(ptr: *const u8) -> if class_id != 0 && class_chain_may_have_to_json(class_id) { return false; } - if crate::object::prototype_chain::object_static_prototype(ptr as usize).is_some() { + if crate::object::prototype_chain::object_static_prototype_is_linked(ptr as usize) { return false; } if object_proto_may_have_to_json() { @@ -651,7 +651,7 @@ pub(super) unsafe fn inherited_to_json_possible_without_gc( class_plain_record: bool, ) -> bool { !class_plain_record - || crate::object::prototype_chain::object_static_prototype(ptr as usize).is_some() + || crate::object::prototype_chain::object_static_prototype_is_linked(ptr as usize) || (OBJECT_PROTO_TOJSON_STATE.with(|c| c.get()) == PROTO_TOJSON_DIRTY && CACHED_OBJECT_PROTO_BITS.with(|c| c.get()) == 0) || object_proto_may_have_to_json() diff --git a/crates/perry-runtime/src/native_payload_birth.rs b/crates/perry-runtime/src/native_payload_birth.rs index 0318cdad223..9b79d56fa8f 100644 --- a/crates/perry-runtime/src/native_payload_birth.rs +++ b/crates/perry-runtime/src/native_payload_birth.rs @@ -141,13 +141,10 @@ unsafe fn family_proto_id(proto: *mut ObjectHeader) -> Option { (!meta.is_null()).then(|| (*meta).proto_serial) } -/// An unpublished null-prototype object, as `js_object_alloc_null_proto` -/// initializes one before publishing its birth shape. +/// An unpublished state record. Its null [[Prototype]] is the identity of +/// the filled birth shape its caller stamps (`PROTO_ID_NULL`). unsafe fn unpublished_state(count: u32) -> *mut ObjectHeader { - let obj = crate::object::object_alloc_unpublished(0, count); - let gc = crate::gc::header_from_trusted_user_ptr(obj.cast()).cast_mut(); - (*gc)._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; - obj + crate::object::object_alloc_unpublished(0, count) } /// Stamp a fresh, unobserved object with `id`. diff --git a/crates/perry-runtime/src/node_stream_event_emitter_shape.rs b/crates/perry-runtime/src/node_stream_event_emitter_shape.rs index 0b2d82223da..ede0b75f10e 100644 --- a/crates/perry-runtime/src/node_stream_event_emitter_shape.rs +++ b/crates/perry-runtime/src/node_stream_event_emitter_shape.rs @@ -117,7 +117,8 @@ pub(super) fn shape_prototype(obj: *const crate::object::ObjectHeader) -> Option pub(super) fn null_prototype(obj: *const crate::object::ObjectHeader, flags: u16) -> bool { // SAFETY: a live ordinary object the caller validated. let word = unsafe { crate::object::shapes::object_prototype_word(obj) }; - word == crate::value::TAG_NULL || (word == 0 && flags & crate::gc::OBJ_FLAG_NULL_PROTO != 0) + let _ = flags; + word == crate::value::TAG_NULL } /// What `target`'s shape says about its own key `key` ([`own_slot_of`]); a diff --git a/crates/perry-runtime/src/object/alloc.rs b/crates/perry-runtime/src/object/alloc.rs index c22d6840c35..421ecfa085e 100644 --- a/crates/perry-runtime/src/object/alloc.rs +++ b/crates/perry-runtime/src/object/alloc.rs @@ -91,9 +91,8 @@ pub(crate) fn registered_class_keys_array( } /// #1175: allocate an object whose `[[Prototype]]` is null. Same layout as -/// `js_object_alloc`, but the `OBJ_FLAG_NULL_PROTO` bit is set on the GC -/// header so `Object.getPrototypeOf` returns null instead of the heap -/// pointer / synthesized proto. A runtime record born here is not marked +/// `js_object_alloc`; its first shape names the null identity, so +/// `Object.getPrototypeOf` (and every other reader of the link) answers null. A runtime record born here is not marked /// ordinary (a namespace, scope, resource or internal state record); a plain /// dictionary result uses [`js_object_alloc_null_proto_ordinary`]. #[no_mangle] @@ -188,7 +187,6 @@ pub(crate) unsafe fn object_alloc_null_proto_with_key_attrs_and_inline( let obj = super::alloc_basic::object_alloc_unpublished(0, inline); let with_attrs = attrs.iter().any(|&entry| entry != 0); let gc = (obj as *mut u8).sub(crate::gc::GC_HEADER_SIZE) as *mut crate::gc::GcHeader; - (*gc)._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; if with_attrs { // The descriptor bit is initialized with the final attributed keys; // no descriptor is installed in an external table or process gate. @@ -222,7 +220,7 @@ pub(crate) unsafe fn object_alloc_null_proto_with_key_attrs_and_inline( } canonical_keys::canonicalize(&proof, list, count) }; - set_object_keys_with_live(obj, canonical.view(), inline); + super::set_object_keys_with_live_born_null(obj, canonical.view(), inline); // The shape's keys are an external child edge. A previously traced // newborn bag can acquire this edge while incremental marking is active. crate::gc::runtime_shade_external_edge( diff --git a/crates/perry-runtime/src/object/alloc_basic.rs b/crates/perry-runtime/src/object/alloc_basic.rs index 12a57fc1c9b..35b69d317d3 100644 --- a/crates/perry-runtime/src/object/alloc_basic.rs +++ b/crates/perry-runtime/src/object/alloc_basic.rs @@ -283,12 +283,6 @@ fn object_alloc_with_parent_impl< if PREMARK_PLAIN { crate::object::shapes::store_kind::premark_plain_ordinary(ptr); } - if BORN_NULL { - // The allocator just returned this live cell; no safepoint has - // intervened, so its trusted header is still ours to initialize. - let gc = crate::gc::header_from_trusted_user_ptr(ptr.cast()).cast_mut(); - (*gc)._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; - } // A class-less newborn's birth shape is a function of its prototype // edge, slot count and kind: replay the one this site last published while its // record still names those facts (ShapeIds are never reused). @@ -298,7 +292,13 @@ fn object_alloc_with_parent_impl< if id != 0 && crate::object::shapes::shape_is_keyless_birth_of( LayoutWord(id), - crate::object::shapes::object_proto_id(ptr), + // A born-null newborn names the null identity in its + // first shape: where its [[Prototype]] lives from birth. + if BORN_NULL { + crate::object::shapes::PROTO_ID_NULL + } else { + crate::object::shapes::object_proto_id(ptr) + }, field_count, crate::object::shapes::store_kind::receiver_ordinary_kind(ptr), ) @@ -316,7 +316,15 @@ fn object_alloc_with_parent_impl< } } // #8113: the birth live-slot bound is published here and nowhere else. - let id = crate::object::shapes::birth_publish_object_shape(ptr, field_count); + let id = if BORN_NULL { + crate::object::shapes::birth_publish_object_shape_as( + ptr, + field_count, + crate::object::shapes::PROTO_ID_NULL, + ) + } else { + crate::object::shapes::birth_publish_object_shape(ptr, field_count) + }; if memo { KEYLESS_BIRTH.with(|birth| birth.set(id.0)); crate::object::shape_carriers::note_shape_id(id); diff --git a/crates/perry-runtime/src/object/assign.rs b/crates/perry-runtime/src/object/assign.rs index d3d1d660dbe..b9ced844693 100644 --- a/crates/perry-runtime/src/object/assign.rs +++ b/crates/perry-runtime/src/object/assign.rs @@ -341,10 +341,7 @@ unsafe fn positional_prototype_shape_keys( /// `getPrototypeOf` machinery: class-implied/per-object identities have no /// shape-owned pointer word and therefore refuse this fast path. unsafe fn positional_shape_prototype(obj: *const ObjectHeader) -> Option> { - let header = crate::value::addr_class::try_read_gc_header(obj as usize)?; - if header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { - return Some(None); - } + crate::value::addr_class::try_read_gc_header(obj as usize)?; let shape = super::shapes::object_shape_descriptor(obj)?; if shape.proto_id == super::shapes::PROTO_ID_NULL { return Some(None); diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index 016af5b09ff..3565a32d597 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -848,15 +848,11 @@ pub(crate) unsafe fn plain_data_write_may_intercept(addr: usize, class_id: u32, // Plain object. Its prototype is exactly `Object.prototype` unless a // `setPrototypeOf` target was recorded for it — #10287: a recorded // prototype is vetted per key instead of rejecting the receiver. - if super::prototype_chain::object_static_prototype(addr).is_some() { - plain_custom_prototype_may_intercept(addr, key) - } else if crate::value::addr_class::try_read_gc_header(addr) - .is_some_and(|header| header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0) - { - // Born with a null `[[Prototype]]` and never relinked: no chain. - false - } else { - object_proto_may_intercept_key(key) + match super::prototype_chain::object_static_prototype(addr) { + // A null `[[Prototype]]`: no chain. + Some(crate::value::TAG_NULL) => false, + Some(_) => plain_custom_prototype_may_intercept(addr, key), + None => object_proto_may_intercept_key(key), } } else { // Class instance: an inherited accessor / non-writable data property diff --git a/crates/perry-runtime/src/object/field_get_set/for_in_stable.rs b/crates/perry-runtime/src/object/field_get_set/for_in_stable.rs index e4c1710ea0d..ec31014efc6 100644 --- a/crates/perry-runtime/src/object/field_get_set/for_in_stable.rs +++ b/crates/perry-runtime/src/object/field_get_set/for_in_stable.rs @@ -68,7 +68,7 @@ fn canonical_prototype_has_no_enumerable_chain_keys(receiver_addr: usize) -> boo let prototype_addr = crate::array::object_prototype_addr(); if prototype_addr == 0 || prototype_addr == receiver_addr - || super::super::prototype_chain::object_static_prototype(prototype_addr).is_some() + || super::super::prototype_chain::object_static_prototype_is_linked(prototype_addr) { return false; } @@ -109,7 +109,7 @@ fn canonical_prototype_has_no_enumerable_chain_keys(receiver_addr: usize) -> boo let after_addr = crate::array::object_prototype_addr(); let after = unsafe { prototype_signature(after_addr) }; if after == Some(now) - && super::super::prototype_chain::object_static_prototype(after_addr).is_none() + && !super::super::prototype_chain::object_static_prototype_is_linked(after_addr) { PROTOTYPE_VERDICT.with(|cell| { cell.set(Some(PrototypeVerdict { @@ -142,13 +142,14 @@ fn stable_single_own_for_in_keys(value: f64) -> Option<*mut ArrayHeader> { { return stable_miss("receiver_kind"); } - // A born-null receiver qualifies when its shape proves it ordinary with - // no chain: its own key is then the whole enumeration. The bit alone is - // sticky across a relink, so a relinked one misses. - let null_chain = receiver_gc._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0; + // A receiver whose shape proves it ordinary with no chain: its own keys + // are then the whole enumeration. + let null_chain = unsafe { + super::super::shapes::object_prototype_is_null(receiver_addr as *const ObjectHeader) + }; if null_chain && !unsafe { - super::super::shapes::store_kind::born_null_shape_proven( + super::super::shapes::store_kind::null_shape_proven( receiver_addr as *const ObjectHeader, receiver_gc._reserved, ) 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 f8a67356db6..ef130d6ea06 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -212,11 +212,10 @@ pub extern "C" fn js_object_set_field_by_name( | crate::gc::OBJ_FLAG_NO_EXTEND | crate::gc::OBJ_FLAG_HAS_DESCRIPTORS | crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO; - // A born-null receiver takes the lane when its shape proves - // it ordinary with no chain (the plan is keyed on that shape). + // The plan is keyed on the shape, which names the receiver's + // [[Prototype]] (a null one too). if crate::gc_type_is_object!((*gc_hdr).obj_type) && (*gc_hdr)._reserved & LANE_BLOCKING == 0 - && super::shapes::store_kind::born_null_admits_lane(o, (*gc_hdr)._reserved) { let class_id = (*o).class_id; // Class objects must reach the static-write mirror. 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 5609bb98bdf..8ba079fc116 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 @@ -272,10 +272,6 @@ pub(crate) fn set_field_by_name_object_tail( let plan_eligible = !key.is_null() && obj_class_id != NATIVE_MODULE_CLASS_ID && crate::object::object_is_regular(obj) - && super::shapes::store_kind::born_null_admits_lane( - obj, - (*gc_header)._reserved, - ) // Per-key, not per-receiver: a descriptor on some OTHER key cannot // intercept this one, and a plan hit skips the own-accessor // short-circuit below, so the key must be provably uncovered. @@ -573,7 +569,6 @@ pub(crate) fn set_field_by_name_object_tail( // freshly read `obj_flags`, not the pre-vet read. let record_plan_eligible = obj_class_id != NATIVE_MODULE_CLASS_ID && crate::object::object_is_regular(obj) - && super::shapes::store_kind::born_null_admits_lane(obj, obj_flags) // `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; diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index a3ca86a99ba..2acdc3158e7 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -1918,12 +1918,10 @@ pub(crate) unsafe fn array_record_close_is_absent() -> bool { return true; } if parent == 0 { - let Some(header) = crate::value::addr_class::try_read_gc_header(obj as usize) else { + if crate::value::addr_class::try_read_gc_header(obj as usize).is_none() { return false; - }; - if header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 - || obj as usize == object_proto - { + } + if obj as usize == object_proto { return true; } // Class-zero ordinary owners inherit Object.prototype when their diff --git a/crates/perry-runtime/src/object/layout_desc.rs b/crates/perry-runtime/src/object/layout_desc.rs index ddddb30729f..3f29a8e2ea0 100644 --- a/crates/perry-runtime/src/object/layout_desc.rs +++ b/crates/perry-runtime/src/object/layout_desc.rs @@ -192,9 +192,9 @@ pub const LAYOUT_PROTO_OBJECT: *const PerryProtoFamily = std::ptr::null(); pub const LAYOUT_PROTO_NULL: *const PerryProtoFamily = std::ptr::without_provenance(1); /// `%Object.prototype%` for a class family whose instances do not inherit /// from the class (a compiler declared-keys record, `exports`); `OBJECT` (0) -/// there means the class's own prototype. A class family tagged `NULL` keeps -/// the class's shape identity and is born with the null-prototype flag -/// (`mark_layout_object_facts`). +/// there means the class's own prototype. A class family tagged `NULL` +/// (`require.cache`) is registered with the null identity, like any layout +/// tagged `NULL`: its shape names the link. pub const LAYOUT_PROTO_OBJECT_EXPLICIT: *const PerryProtoFamily = std::ptr::without_provenance(3); /// `PerryLayoutDesc.proto` addresses at or above this are a /// `*const PerryProtoFamily`. @@ -248,7 +248,6 @@ unsafe fn proto_family_serial_registered(fam: *const PerryProtoFamily) -> u64 { AtomicU64::from_ptr(std::ptr::addr_of!((*fam).serial).cast_mut()).load(Ordering::Acquire) } - /// One key of a layout's key sequence (enumeration order). #[repr(C)] pub struct PerryLayoutKey { @@ -526,7 +525,6 @@ section_sentinel!( ".perryatm$m" ); - #[cfg(target_os = "windows")] #[used(linker)] #[link_section = ".perrylay$a"] @@ -583,7 +581,6 @@ fn layout_bounds() -> (usize, usize) { } } - /// Every record of a section, in section order (the linker may pad between /// input sections; records are 8-aligned and a zero word never starts one). fn records(bounds: (usize, usize)) -> impl Iterator { @@ -593,8 +590,6 @@ fn records(bounds: (usize, usize)) -> impl Iterator { (0..count).map(move |i| (start + i * size) as *mut T) } - - /// The agent-independent well-known symbol a kind-1 atom is bound to. pub(crate) unsafe fn well_known_atom_symbol( atom: *const StaticAtom, @@ -641,14 +636,16 @@ unsafe fn image_atom_symbol(atom: *const StaticAtom) -> Option<*mut crate::symbo .then(|| (*atom).string.cast_mut().cast()) } - mod atoms; +#[cfg(any( + all(feature = "wasm-image-selftest", target_family = "wasm"), + all(test, target_os = "windows") +))] +use atoms::atom_bounds; #[cfg(test)] use atoms::image_atoms; -#[cfg(any(all(feature = "wasm-image-selftest", target_family = "wasm"), all(test, target_os = "windows")))] -use atoms::atom_bounds; -pub(crate) use atoms::{atom_bytes, coloured_atoms, seed_static_atoms, shadow_name_descriptor}; pub use atoms::StaticAtom; +pub(crate) use atoms::{atom_bytes, coloured_atoms, seed_static_atoms, shadow_name_descriptor}; #[path = "layout_desc/lazy_image.rs"] mod lazy_image; @@ -845,8 +842,8 @@ unsafe fn finish_layout_object( } /// The per-object record a layout-born object carries before its shape is -/// stamped: plain ordinary, null prototype, descriptors and integrity, all -/// from the descriptor. Every allocator that stamps a layout shape calls it, +/// stamped: plain ordinary, descriptors and integrity, all from the +/// descriptor (its [[Prototype]] is the layout's identity). Every allocator that stamps a layout shape calls it, /// so the shape kind and the record agree (R6). /// /// # Safety @@ -865,9 +862,6 @@ unsafe fn mark_layout_object_facts(obj: *mut ObjectHeader, d: &PerryLayoutDesc) } } } - if d.proto == LAYOUT_PROTO_NULL { - (*gc)._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; - } if d.integrity != 0 || (d.key_count != 0 && std::slice::from_raw_parts(d.keys, d.key_count as usize) diff --git a/crates/perry-runtime/src/object/layout_desc/registration.rs b/crates/perry-runtime/src/object/layout_desc/registration.rs index e57fe93775b..3463a6a7a6c 100644 --- a/crates/perry-runtime/src/object/layout_desc/registration.rs +++ b/crates/perry-runtime/src/object/layout_desc/registration.rs @@ -320,13 +320,16 @@ pub(super) unsafe fn register_bound( } } // Case 2c: a class family's instances have the class's prototype, and - // its id is its descriptor record's. + // its id is its descriptor record's, unless the descriptor names the + // instances' prototype itself: `%Object.prototype%` (3, `exports`) or + // null (1, `require.cache`). The shape's identity is the one record of + // that link. let class_family = d.class_id != 0; let proto_id = match d.proto.addr() { 3 => super::super::shapes::PROTO_ID_DEFAULT, + 1 => super::super::shapes::PROTO_ID_NULL, _ if class_family => super::super::shapes::class_proto_id(d.class_id), 0 => super::super::shapes::PROTO_ID_DEFAULT, - 1 => super::super::shapes::PROTO_ID_NULL, 2 if d.variant == crate::codegen_abi::LAYOUT_VARIANT_FUNCTION_CONSTRUCTOR => { match function_proto { Some(id) if id != 0 => id, @@ -457,7 +460,12 @@ pub(super) unsafe fn register_bound( } else if function { super::super::shapes::allocate_declared_layout_id(proto_id).0 } else if proto_id == super::super::shapes::PROTO_ID_DEFAULT - || class_family + // A class family keeps its section-ordered identity: a class + // identity is implied, which a plain id carries. A family linked to + // null (`require.cache`) names the null identity, which only a + // null-kind id carries: it takes an allocated one, beside the + // null-kind word the codegen gave its descriptor. + || (class_family && proto_id != super::super::shapes::PROTO_ID_NULL) || d.variant == crate::codegen_abi::LAYOUT_VARIANT_FUNCTION_CONSTRUCTOR { requested diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 996a82669b3..0b9bbe7a4c8 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1815,6 +1815,25 @@ unsafe fn set_object_keys_with_live( set_object_keys_with_live_rep(obj, keys, live_inline_slot_count, field_rep::REP_ANY); } +/// [`set_object_keys_with_live`] for an unstamped null-prototype newborn: its +/// first shape names `PROTO_ID_NULL` (`shapes::publish_object_shape_from_rep_born`). +#[cfg_attr(feature = "shape-mint-diag", track_caller)] +pub(crate) unsafe fn set_object_keys_with_live_born_null( + obj: *mut ObjectHeader, + keys: ObjectKeys, + live_inline_slot_count: u32, +) { + debug_assert!(shapes::object_shape_descriptor(obj).is_none()); + shapes::publish_object_shape_from_rep_born( + obj, + None, + keys, + live_inline_slot_count, + field_rep::REP_ANY, + Some(shapes::PROTO_ID_NULL), + ); +} + /// `set_object_keys_with_live` publishing the successor with field /// representation `rep` (charter step 5, T2: `field_rep_store::publish_key_add_edge`). #[cfg_attr(feature = "shape-mint-diag", track_caller)] diff --git a/crates/perry-runtime/src/object/namespace_create.rs b/crates/perry-runtime/src/object/namespace_create.rs index fa2b8a6bc02..a39348cbc59 100644 --- a/crates/perry-runtime/src/object/namespace_create.rs +++ b/crates/perry-runtime/src/object/namespace_create.rs @@ -44,8 +44,8 @@ pub extern "C" fn js_finalize_namespace(value: f64) -> f64 { unsafe { (*obj).class_id = MODULE_NAMESPACE_CLASS_ID; // A module namespace exotic object has a [[Prototype]] of `null` - // (ECMA-262 10.4.6), and `OBJ_FLAG_NULL_PROTO` is the proof - // `Object.getPrototypeOf` already reads to answer `null`. Without it a + // (ECMA-262 10.4.6): its shape names the null identity, which + // `Object.getPrototypeOf` reads to answer `null`. Without it a // namespace reached the self-prototype fallback in // `object_ops/prototype.rs`, which answers with a further namespace // object, so a prototype-chain walk over an `import * as ns` binding @@ -53,9 +53,12 @@ pub extern "C" fn js_finalize_namespace(value: f64) -> f64 { // `NATIVE_MODULE_CLASS_ID` arm already guards against for // `require("path")` namespaces; a static ESM namespace reaches this // function instead, and had no such guard. - let gc = crate::gc::header_from_trusted_user_ptr(obj.cast()).cast_mut(); - (*gc)._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; - crate::object::shapes::restamp_object_proto_id(obj); + crate::object::shapes::transition_object_shape_prototype( + obj, + crate::object::shapes::PROTO_ID_NULL, + crate::value::TAG_NULL, + ); + crate::object::shapes::store_kind::restamp_object_store_kind(obj); } crate::object::js_object_prevent_extensions(value) } diff --git a/crates/perry-runtime/src/object/native_call_method/object_proto.rs b/crates/perry-runtime/src/object/native_call_method/object_proto.rs index 1efb67fdc5b..b7ce02acac4 100644 --- a/crates/perry-runtime/src/object/native_call_method/object_proto.rs +++ b/crates/perry-runtime/src/object/native_call_method/object_proto.rs @@ -1,9 +1,7 @@ use super::*; pub(super) unsafe fn object_has_null_proto_flag(object: *const ObjectHeader) -> bool { - let gc_header = - (object as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - ((*gc_header)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO) != 0 + crate::object::shapes::object_prototype_is_null(object) } /// Resolve `method_name` through an ORDINARY object's prototype chain and call diff --git a/crates/perry-runtime/src/object/object_literal_ops.rs b/crates/perry-runtime/src/object/object_literal_ops.rs index fc7bfc88a3c..45c67b53f9b 100644 --- a/crates/perry-runtime/src/object/object_literal_ops.rs +++ b/crates/perry-runtime/src/object/object_literal_ops.rs @@ -43,8 +43,7 @@ pub(super) unsafe fn object_literal_key_to_string(key_value: f64) -> *mut crate: live_obj(&key_handle), value_of_key.get_raw_const_ptr::(), ); - let gc = gc_header_for(live_obj(&key_handle)); - if ((*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO) != 0 + if crate::object::shapes::object_prototype_is_null(live_obj(&key_handle)) && to_string.is_undefined() && value_of.is_undefined() { diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index e9ea7d433d3..ba5ccd0d932 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -685,9 +685,11 @@ pub(crate) fn get_prototype_of_resolved(obj_value: f64) -> f64 { unsafe { let obj = raw_addr as *const ObjectHeader; let gc = gc_header_for(obj); - // #1175: objects allocated with a null prototype - // (Object.create(null), querystring.parse) report null here. - if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + // #1175: objects with a null prototype (Object.create(null), + // querystring.parse, linked to null): their shape says so. + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && crate::object::shapes::object_prototype_is_null(obj) + { return f64::from_bits(TAG_NULL); } // A RegExp's internal prototype does not depend on its @@ -949,7 +951,9 @@ pub(crate) fn get_prototype_of_resolved(obj_value: f64) -> f64 { unsafe { let obj = bits as *const ObjectHeader; let gc = gc_header_for(obj); - if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && crate::object::shapes::object_prototype_is_null(obj) + { return f64::from_bits(TAG_NULL); } diff --git a/crates/perry-runtime/src/object/property_ops/set.rs b/crates/perry-runtime/src/object/property_ops/set.rs index 8e5adb44c37..718416ac796 100644 --- a/crates/perry-runtime/src/object/property_ops/set.rs +++ b/crates/perry-runtime/src/object/property_ops/set.rs @@ -808,8 +808,9 @@ pub(crate) fn create_data_property(receiver: f64, key: f64, value: f64) -> bool reflect_non_object_typeerror("defineProperty"); } let scope = crate::gc::RuntimeHandleScope::new(); - let receiver = - scope.root_nanbox_f64(crate::object::object_ops::normalize_descriptor_operand(receiver)); + let receiver = scope.root_nanbox_f64( + crate::object::object_ops::normalize_descriptor_operand(receiver), + ); let value = scope.root_nanbox_f64(value); let key = scope.root_nanbox_f64(crate::object::js_to_property_key(key)); let descriptor = @@ -1042,13 +1043,11 @@ pub(crate) fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, rece // builds O(n²)). #10287: a RECORDED prototype (a // function-constructed receiver, `Object.create`, // `setPrototypeOf`) is walked per key rather than rejected. - // A born-null receiver whose shape proves it ordinary - // with no chain has nothing to intercept the key. + // A receiver whose shape proves it ordinary with + // no chain has nothing to intercept the key. use crate::object::shapes::store_kind; let obj = cur_addr() as *const crate::ObjectHeader; - if header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 - && store_kind::born_null_shape_proven(obj, header._reserved) - { + if store_kind::null_shape_proven(obj, header._reserved) { true } else { match crate::object::prototype_chain::object_static_prototype( @@ -1057,6 +1056,8 @@ pub(crate) fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, rece None => { !crate::object::object_proto_may_intercept_key(cur_key()) } + // A null chain: nothing intercepts. + Some(crate::value::TAG_NULL) => true, Some(_) => { !crate::object::plain_custom_prototype_may_intercept( cur_addr(), @@ -1138,12 +1139,6 @@ pub(crate) fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, rece && crate::object::object_is_regular( addr as *const crate::ObjectHeader, ) - // A born-null receiver's plan is keyed on a - // shape that proves it ordinary with no chain. - && crate::object::shapes::store_kind::born_null_admits_lane( - addr as *const crate::ObjectHeader, - header._reserved, - ) && interned != 0; let verdict = if plan_eligible && crate::object::prop_plan::store_plan_check( diff --git a/crates/perry-runtime/src/object/proto_validity.rs b/crates/perry-runtime/src/object/proto_validity.rs index 18855138efc..5e62cbc4ce0 100644 --- a/crates/perry-runtime/src/object/proto_validity.rs +++ b/crates/perry-runtime/src/object/proto_validity.rs @@ -372,7 +372,10 @@ unsafe fn keep_prototype_in_record( return; } let word = crate::object::shapes::object_prototype_word(object); - if word != 0 { + // A null identity answers from the shape word inline: there is no word + // to keep. A property holder's record slot is its owner's [[Prototype]] + // (`prototype_chain::object_static_prototype`), never the bag's own null. + if word != 0 && word != crate::value::TAG_NULL { (*meta).prototype = word; // GC_STORE_AUDIT(BARRIERED): meta-record prototype slot store (parent // = the meta record), as in the prototype funnel. diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 32808599c7e..f5eef61cfae 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -589,9 +589,17 @@ fn object_set_static_prototype_impl_with( } } +/// Does `obj_ptr` have a non-null [[Prototype]] other than its default? +/// [`object_static_prototype`] less a null link: the question a root such as +/// `%Object.prototype%` (whose own link is null) asks of "was it re-linked". +pub(crate) fn object_static_prototype_is_linked(obj_ptr: usize) -> bool { + object_static_prototype(obj_ptr).is_some_and(|bits| bits != crate::value::TAG_NULL) +} + /// Look up the recorded prototype bits for an object, if any. Returns `None` /// when no explicit prototype has been recorded (the object still has its -/// default prototype); `Some(TAG_NULL)` when it was explicitly set to `null`. +/// default prototype); `Some(TAG_NULL)` when its [[Prototype]] is null, born +/// so (`Object.create(null)`) or linked so: its shape's one identity. pub fn object_static_prototype(obj_ptr: usize) -> Option { if crate::hot_diag::receiver_repr_on() { crate::hot_diag::receiver_repr_note_decoded_pointer(obj_ptr); @@ -658,14 +666,14 @@ pub fn object_static_prototype(obj_ptr: usize) -> Option { /// /// A chain ends explicitly when a hop carries a recorded `TAG_NULL` /// (`Object.setPrototypeOf(x, null)`, `__proto__ = null`) or is a cell born -/// with no prototype at all (`Object.create(null)`, `OBJ_FLAG_NULL_PROTO`). It +/// with no prototype at all (`Object.create(null)`: its shape's null identity). It /// does NOT end explicitly when a hop simply has no record: that is an /// ordinary object standing on the class default, which is exactly the case /// the fallback exists for. /// /// Cost is paid only on a MISS, and only walks hops that carry a record: an -/// ordinary receiver answers `false` from one absent meta record plus one -/// header bit. +/// ordinary receiver answers `false` from one absent meta record plus its +/// shape word. #[cfg(test)] pub(crate) fn prototype_chain_ends_in_explicit_null(obj_ptr: usize) -> bool { chain_ends_in_explicit_null_before::(obj_ptr) @@ -767,20 +775,15 @@ unsafe fn class_link_prototype(obj_ptr: usize) -> usize { super::class_registry::class_holder_prototype(class_id) as usize } -/// Was this cell allocated with no prototype (`Object.create(null)`, -/// `querystring.parse`)? That is `OBJ_FLAG_NULL_PROTO`, the header bit #1175 -/// added, and it is the born-null half of the question -/// [`chain_ends_in_explicit_null_before`] asks. +/// Does this cell's shape name a null prototype with nothing recorded over +/// it (`Object.create(null)`, `querystring.parse`)? The born-null half of the +/// question [`chain_ends_in_explicit_null_before`] asks (#1175). #[inline] unsafe fn cell_is_born_null_proto(obj_ptr: usize) -> bool { if obj_ptr == 0 || !crate::object::is_valid_obj_ptr(obj_ptr as *const u8) { return false; } - let Some(header) = crate::value::addr_class::try_read_gc_header(obj_ptr) else { - return false; - }; - crate::gc_type_is_object!(header.obj_type) - && header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 + super::shapes::cell_prototype_is_null(obj_ptr) } /// Whether ordinary property lookup must consult the receiver's own chain @@ -907,7 +910,7 @@ pub(crate) unsafe fn default_object_prototype_for_owner(obj_ptr: usize) -> Optio return None; } let gc = super::gc_header_for(obj); - if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT && super::shapes::object_prototype_is_null(obj) { return None; } if crate::gc_type_is_not_object!((*gc).obj_type) diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index efb8f3541cd..5e978ba7f4b 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -4203,6 +4203,23 @@ pub(crate) unsafe fn birth_publish_object_shape( synchronize_object_shape_descriptor_from(obj, None, live_inline_slot_count) } +/// [`birth_publish_object_shape`] of a keyless newborn whose allocator names +/// its [[Prototype]] identity (a born-null record: `PROTO_ID_NULL`). +pub(crate) unsafe fn birth_publish_object_shape_as( + obj: *mut crate::object::ObjectHeader, + live_inline_slot_count: u32, + proto_id: u64, +) -> LayoutWord { + publish_object_shape_from_rep_born( + obj, + None, + crate::object::ObjectKeys::NONE, + live_inline_slot_count, + super::field_rep::REP_ANY, + Some(proto_id), + ) +} + /// Publish a new live inline-slot bound for an ALREADY PUBLISHED object. /// /// This is the #8113 replacement for `(*obj).field_count = n`. The successor @@ -4329,6 +4346,29 @@ pub(crate) unsafe fn publish_object_shape_from_rep( keys_view: crate::object::ObjectKeys, live_inline_slot_count: u32, rep: u64, +) -> LayoutWord { + publish_object_shape_from_rep_born( + obj, + predecessor, + keys_view, + live_inline_slot_count, + rep, + None, + ) +} + +/// [`publish_object_shape_from_rep`] for an unstamped newborn whose allocator +/// knows its [[Prototype]] identity (`Some`): a born-null record names +/// `PROTO_ID_NULL` in its first shape, the one place that fact is kept. A +/// receiver with a lineage carries the lineage's identity either way. +#[cfg_attr(feature = "shape-mint-diag", track_caller)] +pub(crate) unsafe fn publish_object_shape_from_rep_born( + obj: *mut crate::object::ObjectHeader, + predecessor: Option, + keys_view: crate::object::ObjectKeys, + live_inline_slot_count: u32, + rep: u64, + birth_proto: Option, ) -> LayoutWord { if obj.is_null() || !shape_word_is_writable(obj) { return LayoutWord(0); @@ -4426,7 +4466,7 @@ pub(crate) unsafe fn publish_object_shape_from_rep( // receiver with no lineage (an unstamped newborn) reads its own. let proto_id = match lineage { Some(descriptor) => descriptor.proto_id, - None => object_proto_id(obj), + None => birth_proto.unwrap_or_else(|| object_proto_id(obj)), }; let id = publish_shape_result(shape_descriptor_ensure_with_rep( keys_view, @@ -5275,10 +5315,9 @@ pub(crate) const SYNTHETIC_CLASS_ID_BASE: u32 = /// `obj` is a live `ObjectHeader`. /// /// A null [[Prototype]] is the identity `PROTO_ID_NULL` and reads back as -/// `TAG_NULL` — except on a cell BORN null (`Object.create(null)`, -/// `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. +/// `TAG_NULL`, however the cell came to have it (born null, as +/// `Object.create(null)`, or linked to null later): the shape is the one +/// authority ([`object_prototype_is_null`]). #[inline(always)] pub(crate) unsafe fn object_prototype_word(obj: *const crate::object::ObjectHeader) -> u64 { // A1_UNPROVEN: record-first: shape word (kind-A/B question for S0f). @@ -5287,27 +5326,37 @@ pub(crate) unsafe fn object_prototype_word(obj: *const crate::object::ObjectHead return (*meta).prototype; } // A default, class or per-object identity answers 0 from the id alone, - // and a null link from the header. The link is the layout's (an overflow + // and a null identity `TAG_NULL`. The link is the layout's (an overflow // never holds [[Prototype]]), so the layout id answers under the bit. let word = super::layout_desc::layout_id_of_word((*obj).parent_class_id); match shape_word_kind(word) { SHAPE_ID_KIND_PLAIN => 0, - SHAPE_ID_KIND_NULL if is_layout_word(word) => null_linked_prototype_word(obj), + SHAPE_ID_KIND_NULL if is_layout_word(word) => crate::value::TAG_NULL, _ => 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. +/// Is `obj`'s [[Prototype]] null? Its recorded prototype, else its shape's +/// identity: the one authority, whatever made the link null. Allocation-free. /// /// # 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, - } +#[inline(always)] +pub(crate) unsafe fn object_prototype_is_null(obj: *const crate::object::ObjectHeader) -> bool { + object_prototype_word(obj) == crate::value::TAG_NULL +} + +/// [`object_prototype_is_null`] for an arbitrary heap address: false for +/// anything that is not a live, unforwarded ordinary object cell. +/// +/// # Safety +/// `addr` is any word; it is proven a GC cell before it is read. +pub(crate) unsafe fn cell_prototype_is_null(addr: usize) -> bool { + crate::value::addr_class::try_read_gc_header(addr).is_some_and(|header| { + header.obj_type == crate::gc::GC_TYPE_OBJECT + && header.gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 + && object_prototype_is_null(addr as *const crate::object::ObjectHeader) + }) } /// [`object_prototype_word`] of a meta-less receiver whose ShapeId may name @@ -5332,10 +5381,7 @@ unsafe fn linked_object_prototype_word(obj: *const crate::object::ObjectHeader) if proto_id != PROTO_ID_NULL { return record.prototype_word(); } - 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, - } + crate::value::TAG_NULL } /// The prototype identity `obj` has when its recorded [[Prototype]] is @@ -5366,15 +5412,9 @@ pub(crate) unsafe fn object_proto_id_for( { return PROTO_ID_PER_OBJECT; } - // A recorded prototype outranks the born-null header bit, which is - // sticky: `Object.setPrototypeOf(Object.create(null), p)` keeps the bit - // and its shape must still name `p`. - if recorded == 0 { - if let Some(header) = crate::value::addr_class::try_read_gc_header(obj as usize) { - if header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { - return PROTO_ID_NULL; - } - } + // A null link (born null or linked to null later) is the null identity. + if recorded == crate::value::TAG_NULL { + return PROTO_ID_NULL; } let class_id = (*obj).class_id; let class = shapes_class_role::receiver_vtable_class(obj); diff --git a/crates/perry-runtime/src/object/shapes_class_role.rs b/crates/perry-runtime/src/object/shapes_class_role.rs index 9ab5cc704d4..dec699927a9 100644 --- a/crates/perry-runtime/src/object/shapes_class_role.rs +++ b/crates/perry-runtime/src/object/shapes_class_role.rs @@ -92,9 +92,7 @@ pub(super) unsafe fn newborn_proto_matches( proto: u64, ) -> bool { // A1_UNPROVEN: record-first: newborn shape (kind-A/B question for S0f). - if !crate::meta_read!(crate::meta_obj_runtime_born!(obj as *mut _)).is_null() - || (*super::super::gc_header_for(obj))._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 - { + if !crate::meta_read!(crate::meta_obj_runtime_born!(obj as *mut _)).is_null() { return object_proto_id(obj) == proto; } if proto == PROTO_ID_DEFAULT { diff --git a/crates/perry-runtime/src/object/shapes_store_kind.rs b/crates/perry-runtime/src/object/shapes_store_kind.rs index f8b41ce6eb2..30268e420fd 100644 --- a/crates/perry-runtime/src/object/shapes_store_kind.rs +++ b/crates/perry-runtime/src/object/shapes_store_kind.rs @@ -323,21 +323,7 @@ pub(crate) fn shape_proves_ordinary_null_proto(shape_id: LayoutWord) -> bool { }) } -/// The born-null gate of the shape-keyed store lanes and plans: a receiver -/// whose header `reserved` word carries no born-null bit passes (its shape -/// already keys the plan), and a born-null one passes only when its shape -/// proves it ordinary with the null identity -/// ([`shape_proves_ordinary_null_proto`]). -/// -/// # Safety -/// `obj` is a live `GC_TYPE_OBJECT` `ObjectHeader` and `reserved` is its -/// header's `_reserved` word. -#[inline(always)] -pub(crate) unsafe fn born_null_admits_lane(obj: *const ObjectHeader, reserved: u16) -> bool { - reserved & crate::gc::OBJ_FLAG_NULL_PROTO == 0 || born_null_shape_proven(obj, reserved) -} - -/// A born-null receiver's shape proves it ordinary with the null identity +/// A receiver's shape proves it ordinary with the null identity /// ([`shape_proves_ordinary_null_proto`]). A class-less receiver reaches the /// `Ordinary` kind only through its plain-ordinary mark (R1, R6), so an /// unmarked runtime record (a scope, namespace or resource) is answered from @@ -347,7 +333,7 @@ pub(crate) unsafe fn born_null_admits_lane(obj: *const ObjectHeader, reserved: u /// `obj` is a live `GC_TYPE_OBJECT` `ObjectHeader` and `reserved` is its /// header's `_reserved` word. #[inline(always)] -pub(crate) unsafe fn born_null_shape_proven(obj: *const ObjectHeader, reserved: u16) -> bool { +pub(crate) unsafe fn null_shape_proven(obj: *const ObjectHeader, reserved: u16) -> bool { reserved & crate::gc::OBJ_FLAG_PLAIN_ORDINARY != 0 && shape_proves_ordinary_null_proto(object_shape_stamp(obj)) } diff --git a/crates/perry-runtime/src/promise/then_probe.rs b/crates/perry-runtime/src/promise/then_probe.rs index a7b7ae58954..ed4accee544 100644 --- a/crates/perry-runtime/src/promise/then_probe.rs +++ b/crates/perry-runtime/src/promise/then_probe.rs @@ -51,7 +51,7 @@ //! * no own `then` key (a direct dense scan of the keys array — this is the //! common REAL thenable, `{ then(res, rej) {…} }`, and it must always be //! found); -//! * `OBJ_FLAG_NULL_PROTO` ⇒ the own keys ARE the whole chain, answer proven; +//! * a null [[Prototype]] (the shape's identity) ⇒ the own keys ARE the whole chain, answer proven; //! * otherwise no per-instance `setPrototypeOf` / `__proto__` override //! (`object_static_prototype`) ⇒ the chain is exactly //! `obj → Object.prototype → null`. @@ -297,7 +297,7 @@ fn compute_object_prototype_then(proto_addr: usize) -> Option { } // Likewise if `Object.prototype` itself was given a prototype: the chain // above it is not modelled here, and an accessor could live on it. - if crate::object::prototype_chain::object_static_prototype(proto_addr).is_some() { + if crate::object::prototype_chain::object_static_prototype_is_linked(proto_addr) { return Some(false); } let proto_value = crate::value::js_nanbox_pointer(proto_addr as i64); @@ -530,7 +530,7 @@ unsafe fn prove_no_then(value: f64) -> Outcome { OwnScan::NoThen => {} } // A null-prototype object's own keys are its whole `[[Get]]` chain. - if flags & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + if crate::object::shapes::object_prototype_is_null(obj) { return Outcome::Proved; } // A per-instance `setPrototypeOf` / `__proto__` override puts an arbitrary diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 90775c4b1a6..4453e0fb1f7 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -935,7 +935,6 @@ mod tests { crate::gc::OBJ_FLAG_FROZEN, crate::gc::OBJ_FLAG_SEALED, crate::gc::OBJ_FLAG_NO_EXTEND, - crate::gc::OBJ_FLAG_NULL_PROTO, crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO, crate::gc::OBJ_FLAG_HAS_DESCRIPTORS, 0x0018, // GC_COPY_SURVIVAL_AGE_MASK diff --git a/crates/perry-runtime/src/validators/received.rs b/crates/perry-runtime/src/validators/received.rs index b95a18d159a..a7ec8f1ccc1 100644 --- a/crates/perry-runtime/src/validators/received.rs +++ b/crates/perry-runtime/src/validators/received.rs @@ -108,10 +108,7 @@ pub(crate) fn describe_received_js(value: f64) -> *mut StringHeader { } let addr = crate::value::addr_class::object_ref_addr(value.get_nanbox_f64()); let header = unsafe { crate::value::addr_class::try_read_tracked_gc_header(addr) }; - let null_proto = header.is_some_and(|h| unsafe { - (*h.as_ptr())._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 - }); - if null_proto + if received_has_null_prototype(addr) || crate::object::prototype_chain::object_static_prototype(addr) == Some(crate::value::TAG_NULL) { @@ -277,9 +274,7 @@ pub(crate) fn received_native_brand(value: f64) -> Option<&'static str> { } pub(crate) fn received_has_null_prototype(addr: usize) -> bool { - unsafe { crate::value::addr_class::try_read_tracked_gc_header(addr) }.is_some_and( - |header| unsafe { (*header.as_ptr())._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 }, - ) + unsafe { crate::object::shapes::cell_prototype_is_null(addr) } } /// Inspect link authority, not properties: no constructor Get, descriptor diff --git a/crates/perry-runtime/src/value/to_string_primitive.rs b/crates/perry-runtime/src/value/to_string_primitive.rs index bf0c5d5043d..f545cd39f02 100644 --- a/crates/perry-runtime/src/value/to_string_primitive.rs +++ b/crates/perry-runtime/src/value/to_string_primitive.rs @@ -118,9 +118,9 @@ unsafe fn ordinary_to_primitive_string_inner(value: f64) -> Option { } } -/// True iff `value` is a heap object stamped `OBJ_FLAG_NULL_PROTO` -/// (`Object.create(null)` and friends) — i.e. it has no `[[Prototype]]`, so it -/// does not inherit the default `Object.prototype.toString`/`valueOf`. +/// True iff `value` is an ordinary object whose `[[Prototype]]` is null +/// (`Object.create(null)` and friends, or one linked to null), so it does not +/// inherit the default `Object.prototype.toString`/`valueOf`. unsafe fn value_is_null_proto_object(value: f64) -> bool { let jsval = JSValue::from_bits(value.to_bits()); if !jsval.is_pointer() { @@ -136,8 +136,7 @@ unsafe fn value_is_null_proto_object(value: f64) -> bool { if (obj as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { return false; } - let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 + crate::object::shapes::cell_prototype_is_null(obj as usize) } #[cold] diff --git a/test-files/test_gap_null_prototype_require_cache.cts b/test-files/test_gap_null_prototype_require_cache.cts new file mode 100644 index 00000000000..25d745611fb --- /dev/null +++ b/test-files/test_gap_null_prototype_require_cache.cts @@ -0,0 +1,8 @@ +// require.cache is a null-prototype record: its shape says so, so reads find +// nothing on Object.prototype. +const self = require("./fixtures/awb2-static-proto/base.cjs"); +const c: any = require.cache; +console.log("1", Object.getPrototypeOf(c) === null, typeof c.toString, typeof c.hasOwnProperty, "toString" in c, typeof self.Base); +c.extra = 1; +console.log("2", Object.getPrototypeOf(c) === null, typeof c.toString, Object.keys(c).includes("extra")); +console.log("3", Object.getPrototypeOf(exports) === Object.prototype, typeof exports.hasOwnProperty, typeof module.exports.toString); diff --git a/test-files/test_gap_null_prototype_shape_authority.ts b/test-files/test_gap_null_prototype_shape_authority.ts new file mode 100644 index 00000000000..10a9862972e --- /dev/null +++ b/test-files/test_gap_null_prototype_shape_authority.ts @@ -0,0 +1,34 @@ +// A null [[Prototype]] is the object's shape identity: born null and relinked, +// or linked to null later, every reader agrees with the link. +import { inspect } from "node:util"; +import * as ns from "node:path"; +const o: any = Object.setPrototypeOf(Object.create(null), Object.prototype); +o.a = 1; +console.log("1", typeof o.hasOwnProperty, o.hasOwnProperty("a"), String(o), `${o}`, o + ""); +console.log("2", Object.getPrototypeOf(o) === Object.prototype, o instanceof Object, Object.prototype.isPrototypeOf(o)); +console.log("3", inspect(o), inspect(Object.create(null))); +const p = { inherited: "yes", toString() { return "P!"; } }; +const q: any = Object.setPrototypeOf(Object.create(null), p); +q.own = 2; +const keys: string[] = []; +for (const k in q) keys.push(k); +console.log("4", keys.join(","), q.inherited, String(q), JSON.stringify(Object.assign({}, q))); +const r: any = Object.setPrototypeOf({ x: 1 }, null); +console.log("5", Object.getPrototypeOf(r), typeof r.toString, inspect(r), "toString" in r); +let threw = false; +try { String(r); } catch (e) { threw = e instanceof TypeError; } +console.log("6", threw); +const n: any = Object.create(null); +n.k = 1; +console.log("7", Object.getPrototypeOf(n), typeof n.toString, inspect(n), n instanceof Object); +class K { k = 1; } +const kk: any = Object.setPrototypeOf(Object.create(null), K.prototype); +console.log("8", kk instanceof K, Object.getPrototypeOf(kk) === K.prototype, typeof kk.constructor); +console.log("9", Object.getPrototypeOf(ns), typeof (ns as any).toString); +const back: any = Object.setPrototypeOf(Object.setPrototypeOf(Object.create(null), Object.prototype), null); +console.log("10", Object.getPrototypeOf(back), typeof back.toString, inspect(back)); +const thenable: any = Object.setPrototypeOf(Object.create(null), { then(res: any) { res("thenable"); } }); +Promise.resolve(thenable).then((v) => console.log("11", v)); +const plainThen: any = Object.create(null); +plainThen.then = (res: any) => res("own then"); +Promise.resolve(plainThen).then((v) => console.log("12", v)); From 54a1ce8c8f02b055507d9439de2cbbb4589b16dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 1 Nov 2026 10:04:58 +0000 Subject: [PATCH 2/4] fix(runtime): a function's [[Prototype]] is its shape's identity Object.setPrototypeOf(fn, p) wrote the link as a "p" key in the function's internal state record and moved the function to the shared FunctionDictionary shape. Every read of a relinked function (ArkType's callable class instances) then searched that record by key bytes and lost every shape fast path, and the readers that never looked there answered from Function.prototype. A described function's shape already names its [[Prototype]] (its body kind's intrinsic, by reserved serial, now derived from the body kind without a record read). A link now moves it to the Function shape of its own keys with the linked identity, the class-less rule an ordinary receiver takes (the prototype gets its stable serial first); a key add carries the identity its current shape names. The record keeps the link only for a FunctionDictionary function, which answers nothing about it ("ask the object"), and a described link moves into the record when the function becomes a dictionary. The bag stays a null-chain slot store, so no own-property writer changes meaning. The readers follow the one link: closure_static_prototype and object_static_prototype (a function answers from its own link, never its property bag's record), the layout probe, instanceof (a linked function's chain is walked for `C` and for `Function`, and its @@hasInstance is read from that chain when a hop may own a symbol key), `constructor` and a null link's members (undefined, not Function.prototype's). Stores onto a bound function take the function store lane, which checks the chain. Also: a program's first inherited read of a function (`fn.toString`) answered undefined because %Function.prototype% was not built yet; the read now builds the realm global that holds it. --- .../src/closure/dynamic_props.rs | 60 +++- crates/perry-runtime/src/closure/mod.rs | 4 +- crates/perry-runtime/src/closure/shape.rs | 307 ++++++++++++++---- .../construct/compiled_function.rs | 7 +- .../field_get_set/get_field_by_name_tail.rs | 11 +- .../src/object/instanceof/dynamic_dispatch.rs | 110 ++++++- .../src/object/instanceof/static_dispatch.rs | 17 +- .../perry-runtime/src/object/layout_probe.rs | 6 +- .../src/object/prototype_chain.rs | 25 ++ .../perry-runtime/src/object/shape_store.rs | 9 +- .../src/object/shape_store_tests.rs | 8 +- crates/perry-runtime/src/object/shapes.rs | 7 + .../test_gap_function_first_inherited_read.ts | 7 + test-files/test_gap_function_link_on_shape.ts | 51 +++ .../test_gap_prototype_link_fast_paths.ts | 42 +++ 15 files changed, 567 insertions(+), 104 deletions(-) create mode 100644 test-files/test_gap_function_first_inherited_read.ts create mode 100644 test-files/test_gap_function_link_on_shape.ts create mode 100644 test-files/test_gap_prototype_link_fast_paths.ts diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 78b9941c796..357affdb200 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -63,8 +63,15 @@ pub fn closure_set_static_prototype(closure_ptr: usize, proto_bits: u64) { } return; } - unsafe { super::props::state_set_prototype(closure_ptr, proto_bits) }; + // A described function names the link on its shape; a dictionary one + // (or a link with no stable identity) keeps it in its own record. + if unsafe { super::shape::closure_link_on_shape(closure_ptr, proto_bits) } { + return; + } + // Into FunctionDictionary first: it moves a link the described shape + // named into the record, which this link then replaces. super::shape::note_function_own_state_changed(closure_ptr); + unsafe { super::props::state_set_prototype(closure_ptr, proto_bits) }; } /// Look up the static prototype object bits recorded for a closure, if any. @@ -72,12 +79,25 @@ pub fn closure_static_prototype(closure_ptr: usize) -> Option { if closure_ptr == 0 || !is_closure_ptr(closure_ptr) { return None; } + closure_static_prototype_proven(closure_ptr) +} + +/// [`closure_static_prototype`] of a proven, live closure cell. +pub(crate) fn closure_static_prototype_proven(closure_ptr: usize) -> Option { if unsafe { super::shape::is_class_info((*(closure_ptr as *const ClosureHeader)).info) } { return Some(unsafe { crate::object::class_value::constructor_parent_bits(closure_ptr as *mut ClosureHeader) }); } - unsafe { super::props::state_prototype(closure_ptr) } + // A described shape is the record of the link; FunctionDictionary keeps + // it in the function's own record ("ask the object"). + let closure = closure_ptr as *const ClosureHeader; + unsafe { + if (*closure).shape_id != super::shape::function_dictionary_shape() { + return super::shape::described_link_bits(closure); + } + super::props::state_prototype(closure_ptr) + } } /// Is `ptr` a live function object (`GC_TYPE_CLOSURE` cell)? Safe for an @@ -366,6 +386,11 @@ fn closure_proto_chain_get( if depth > 0 { proto_bits = closure_static_prototype(cur)?; } + // A null link ends the chain: the read's answer is `undefined`, not + // `%Function.prototype%`'s. + if proto_bits == crate::value::TAG_NULL { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } let proto_f64 = f64::from_bits(proto_bits); let proto_ptr = crate::value::js_nanbox_get_pointer(proto_f64) as usize; if proto_ptr == 0 || proto_ptr == cur { @@ -467,6 +492,25 @@ fn closure_proto_chain_get( None } +/// Build the realm global that holds %Function.prototype% (its first +/// inherited read), keeping the reading function and receiver alive across +/// the allocation. Returns both, re-read. +#[cold] +#[inline(never)] +fn cold_materialize_function_prototype(self_ptr: usize, receiver: f64) -> (usize, f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver_root = scope.root_nanbox_f64(receiver); + let self_root = (self_ptr != 0) + .then(|| scope.root_nanbox_f64(crate::value::js_nanbox_pointer(self_ptr as i64))); + let _ = super::shape::function_prototype_ptr_materialized(); + ( + self_root.map_or(0, |r| { + (r.get_nanbox_u64() & crate::value::POINTER_MASK) as usize + }), + receiver_root.get_nanbox_f64(), + ) +} + /// `[[Get]]` of `prop` on the real `%Function.prototype%` object, for a /// function-object receiver whose own/static lookup already missed — a /// user method or expando (`Function.prototype.myHelper = fn`) or a @@ -485,6 +529,18 @@ pub(crate) fn function_prototype_inherited_get( if crate::object::reified_function_method_name(prop).is_some() { return None; } + // A read needs %Function.prototype% itself: build the realm global that + // holds it when this is the program's first inherited read (the set + // side's descriptor probe must not, so the shared target answers 0). + let (self_ptr, receiver) = if crate::array::function_prototype_addr() == 0 + && !matches!( + prop, + "prototype" | "name" | "length" | "caller" | "arguments" + ) { + cold_materialize_function_prototype(self_ptr, receiver) + } else { + (self_ptr, receiver) + }; let proto_ptr = function_prototype_fallback_target(self_ptr, prop)?; // A defineProperty accessor on Function.prototype // (`{ get: () => 12 }`) is invoked with the reading diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index 2d7285af5ad..d1bfaa3a472 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -91,8 +91,8 @@ pub use unbox::{js_closure_unbox_callee_checked, js_closure_unbox_callee_checked pub(crate) use dynamic_props::{ clone_closure_rebind_this, closure_get_dynamic_prop_keyed, closure_reads_this_from_capture, - closure_set_via_function_prototype_descriptor, function_prototype_fallback_target, - function_prototype_inherited_get, + closure_set_via_function_prototype_descriptor, closure_static_prototype_proven, + function_prototype_fallback_target, function_prototype_inherited_get, }; pub(crate) use dynamic_props::{ closure_define_data_with_attrs, closure_define_data_with_attrs_bytes, diff --git a/crates/perry-runtime/src/closure/shape.rs b/crates/perry-runtime/src/closure/shape.rs index 0476996f891..8ada990c672 100644 --- a/crates/perry-runtime/src/closure/shape.rs +++ b/crates/perry-runtime/src/closure/shape.rs @@ -202,7 +202,8 @@ pub(crate) fn birth_shape_for_body(info: *const super::JsFunctionInfo) -> Layout /// Is `closure` on a DESCRIBED Function shape (base or keyed, any body /// kind) — i.e. not FunctionDictionary? Such a closure has no accessor, no -/// symbol key, no delete marker and no recorded prototype. +/// symbol key and no delete marker, and its shape names its [[Prototype]] +/// (its body kind's intrinsic, or a link: [`closure_on_base_identity`]). /// /// # Safety /// `closure` is a proven, live closure cell. @@ -224,11 +225,23 @@ pub(crate) unsafe fn closure_on_base_shape(closure: *const ClosureHeader) -> boo id != function_dictionary_shape() && !is_class_info((*closure).info) } +/// Is `closure` on a described Function shape that names its body kind's +/// intrinsic [[Prototype]] (no link moved it)? +/// +/// # Safety +/// `closure` is a proven, live closure cell. +#[inline] +pub(crate) unsafe fn closure_on_base_identity(closure: *const ClosureHeader) -> bool { + closure_on_base_shape(closure) + && shapes::shape_proto_id((*closure).shape_id) == Some(base_identity(closure)) +} + /// Is `closure` on a described Function shape of a plain function body: its /// own properties are its bag plus the synthesized intrinsic ones, and its -/// [[Prototype]] is `%Function.prototype%`? Not FunctionDictionary, a class -/// constructor, a bound function or method (whose reads have their own -/// dispatch), or an async or generator body (another intrinsic prototype). +/// [[Prototype]] is its shape's identity (`%Function.prototype%` until a +/// link)? Not FunctionDictionary, a class constructor, a bound function or +/// method (whose reads have their own dispatch), or an async or generator +/// body (another intrinsic prototype). /// /// # Safety /// `closure` is a proven, live closure cell. @@ -261,6 +274,13 @@ pub(crate) unsafe fn closure_become_dictionary(closure: *mut ClosureHeader) { let id = (*closure).shape_id; // A class function object keeps its (sticky, dictionary-kind) class shape. if id != dict && !is_class_info((*closure).info) { + // FunctionDictionary answers nothing about the function's + // [[Prototype]] ("ask the object"): a link its described shape named + // moves into the function's own record, as an ordinary dictionary + // receiver's lives in its own record. + if let Some(bits) = described_link_bits(closure) { + super::props::state_set_prototype(closure as usize, bits); + } // GC_STORE_AUDIT(POINTER_FREE): a ShapeId, never a heap reference. (*closure).shape_id = dict; } @@ -281,74 +301,214 @@ pub(crate) unsafe fn closure_become_dictionary(closure: *mut ClosureHeader) { pub(crate) fn refresh_closure_shape(ptr: usize) { unsafe { let closure = ptr as *mut ClosureHeader; - let dict = function_dictionary_shape(); - if (*closure).shape_id == dict || is_class_info((*closure).info) { + if (*closure).shape_id == function_dictionary_shape() || is_class_info((*closure).info) { return; } - if super::props::has_state(ptr) { - closure_become_dictionary(closure); - return; + // The link is carried: a described shape names the function's + // [[Prototype]] identity (its body kind's intrinsic until a link). + refresh_closure_shape_as(ptr, None); + } +} + +/// The [[Prototype]] identity of the base shape `closure` is born on: its +/// body kind's intrinsic (`%Function.prototype%`, ...), by reserved serial. +/// +/// # Safety +/// `closure` is a proven, live closure cell. +#[inline] +pub(crate) unsafe fn base_identity(closure: *const ClosureHeader) -> u64 { + base_identity_of(birth_shape_for_body((*closure).info)) +} + +/// [`base_identity`] of the base shape `base`: the four body-kind words +/// (`function_base_shape`) name the reserved serials in kind order, with no +/// record read; a bound body's word answers from its record. +#[inline] +fn base_identity_of(base: LayoutWord) -> u64 { + let kind = base + .0 + .wrapping_sub(function_base_shape(FunctionProtoKind::Function).0); + let id = if kind <= FunctionProtoKind::AsyncGenerator as u32 { + INTRINSIC_SERIAL_FUNCTION + kind as u64 + } else { + shapes::shape_proto_id(base).unwrap_or(INTRINSIC_SERIAL_FUNCTION) + }; + debug_assert_eq!(Some(id), shapes::shape_proto_id(base).or(Some(id))); + id +} + +/// The linked [[Prototype]] bits a DESCRIBED (non-dictionary, non-class) +/// function's shape names, when a link moved it off its base identity: +/// `TAG_NULL`, or the identity's prototype word. `None` on its base identity +/// (nothing linked), for a dictionary or class shape, or an identity with no +/// word. +/// +/// # Safety +/// `closure` is a proven, live closure cell. +pub(crate) unsafe fn described_link_bits(closure: *const ClosureHeader) -> Option { + let id = (*closure).shape_id; + if id == function_dictionary_shape() || is_class_info((*closure).info) { + return None; + } + // The base shape names the base identity: one compare, no record read. + let base = birth_shape_for_body((*closure).info); + if id == base { + return None; + } + let pid = shapes::shape_proto_id(id)?; + if pid == base_identity_of(base) { + return None; + } + if pid == shapes::PROTO_ID_NULL { + return Some(crate::value::TAG_NULL); + } + let word = shapes::identity_prototype_word(pid); + (word != 0).then_some(word) +} + +/// Link a DESCRIBED function to `proto_bits` (`Object.setPrototypeOf(fn, p)`, +/// a pointer or null) on its shape: the prototype gets its stable serial, as +/// the ordinary link funnel gives it, and the function moves to the Function +/// shape of its own keys with that identity. False, with nothing done, when +/// the function must keep the link in its own record instead: a dictionary +/// or class shape, internal state, or a prototype with no stable identity. +/// +/// # Safety +/// `ptr` is a proven, live closure cell; `proto_bits` is a pointer or +/// `TAG_NULL`. +pub(crate) unsafe fn closure_link_on_shape(ptr: usize, proto_bits: u64) -> bool { + let closure = ptr as *mut ClosureHeader; + if (*closure).shape_id == function_dictionary_shape() + || is_class_info((*closure).info) + || super::props::has_state(ptr) + { + return false; + } + let _no_move = crate::gc::GcSuppressScope::new(); + let value = crate::value::JSValue::from_bits(proto_bits); + if value.is_pointer() { + let addr = value.as_pointer::() as usize; + if crate::object::proto_validity::mark_object_as_prototype(addr).is_none() { + return false; + } + } else if proto_bits != crate::value::TAG_NULL { + return false; + } + let Some(proto_id) = shapes::stable_function_link_identity(proto_bits) else { + return false; + }; + shapes::write_identity_word(proto_id, proto_bits); + crate::object::builtin_proto::note_write((*closure).props, "function own state"); + refresh_closure_shape_as(ptr, Some(proto_id)); + true +} + +/// [`refresh_closure_shape`] naming `link` as the function's [[Prototype]] +/// identity, or (`None`) carrying the identity its current shape names: its +/// body kind's intrinsic on the base shape, else the current descriptor's, +/// read once with the canonical check below. +/// +/// # Safety +/// `ptr` is a proven, live, described closure cell. +unsafe fn refresh_closure_shape_as(ptr: usize, link: Option) { + let closure = ptr as *mut ClosureHeader; + if super::props::has_state(ptr) { + closure_become_dictionary(closure); + return; + } + let base = birth_shape_for_body((*closure).info); + let current = (*closure).shape_id; + let current_desc = if current != base { + shapes::shape_descriptor_of_receiver(current.0) + } else { + None + }; + let proto_id = link.unwrap_or_else(|| { + current_desc + .as_ref() + .map_or_else(|| base_identity_of(base), |f| f.proto_id) + }); + let base_kind = || shapes::shape_object_kind_for_word(base).unwrap(); + let mint = + |keys: crate::object::ObjectKeys, logical: u32, live: u32, facts: shapes::ReceiverFacts| { + let id = shapes::publish_shape_result(shapes::shape_descriptor_ensure_with_generation( + keys, + logical, + live, + 0, + base_kind(), + proto_id, + facts, + )); + shapes::note_external_carrier_of_word(id.0); + id + }; + // No own key beyond the intrinsic ones: the base shape on the base + // identity, else the keyless shape naming the link. + let keyless = || -> LayoutWord { + if base_identity_of(base) == proto_id { + return base; } - let base = birth_shape_for_body((*closure).info); - let bag = super::props::bag_of(ptr); - let next = if bag.is_null() { - base - } else { - match shapes::object_shape_descriptor(bag) { - Some(d) - if d.object_kind.is_ordinary_layout() - && d.hole_count == 0 - && d.semantic_generation == 0 => - { - if d.logical_key_count == 0 { - base - } else { - let proto_id = - shapes::shape_proto_id(base).unwrap_or(INTRINSIC_SERIAL_FUNCTION); - let base_kind = shapes::shape_object_kind_for_word(base).unwrap(); - // The keyed id is canonical in exactly these inputs: - // when the closure already carries the id they name - // (a value store, an attribute edit that changed - // nothing), there is nothing to mint. - let current = (*closure).shape_id; - if current != base - && shapes::shape_descriptor_of_receiver((current).0).is_some_and(|f| { - f.object_kind == base_kind - && f.keys == d.keys - && f.logical_key_count == d.logical_key_count - && f.live_inline_slot_count == d.live_inline_slot_count - && f.semantic_generation == 0 - && f.proto_id == proto_id - && f.brands() == d.brands() - }) - { - return; - } - let id = shapes::publish_shape_result( - shapes::shape_descriptor_ensure_with_generation( - d.keys_view(), - d.logical_key_count, - d.live_inline_slot_count, - 0, - base_kind, - proto_id, - // The bag's brands are the closure's (#11791). - shapes::ReceiverFacts::of_descriptor( - &d, - function_shape_summary(ShapeObjectKind::Function), - ), - ), - ); - shapes::note_external_carrier_of_word((id).0); - id + if current_desc.as_ref().is_some_and(|f| { + f.keys == 0 + && f.semantic_generation == 0 + && f.proto_id == proto_id + && f.object_kind == base_kind() + }) { + return current; + } + mint( + crate::object::ObjectKeys::NONE, + 0, + 0, + shapes::ReceiverFacts::summary(function_shape_summary(ShapeObjectKind::Function)), + ) + }; + let bag = super::props::bag_of(ptr); + let next = if bag.is_null() { + keyless() + } else { + match shapes::object_shape_descriptor(bag) { + Some(d) + if d.object_kind.is_ordinary_layout() + && d.hole_count == 0 + && d.semantic_generation == 0 => + { + if d.logical_key_count == 0 { + keyless() + } else { + // The keyed id is canonical in exactly these inputs: + // when the closure already carries the id they name + // (a value store, an attribute edit that changed + // nothing), there is nothing to mint. + if current_desc.as_ref().is_some_and(|f| { + f.keys == d.keys + && f.logical_key_count == d.logical_key_count + && f.live_inline_slot_count == d.live_inline_slot_count + && f.semantic_generation == 0 + && f.proto_id == proto_id + && f.brands() == d.brands() + && f.object_kind == base_kind() + }) { + return; } + mint( + d.keys_view(), + d.logical_key_count, + d.live_inline_slot_count, + // The bag's brands are the closure's (#11791). + shapes::ReceiverFacts::of_descriptor( + &d, + function_shape_summary(ShapeObjectKind::Function), + ), + ) } - _ => dict, } - }; - // GC_STORE_AUDIT(POINTER_FREE): a ShapeId, never a heap reference. - (*closure).shape_id = next; - } + _ => function_dictionary_shape(), + } + }; + // GC_STORE_AUDIT(POINTER_FREE): a ShapeId, never a heap reference. + (*closure).shape_id = next; } /// Does the Function ShapeId `id` describe a receiver that inherits `key` @@ -819,16 +979,21 @@ mod tests { } #[test] - fn a_recorded_prototype_a_delete_and_an_accessor_each_leave_the_base_shape() { + fn a_link_names_its_identity_and_a_delete_and_an_accessor_leave_the_base_shape() { let _lock = crate::gc::global_side_table_test_lock(); let _t = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); let a = fresh(&PLAIN_BODY); let proto = crate::object::js_object_alloc(0, 0); - closure_set_static_prototype( - a as usize, - crate::value::js_nanbox_pointer(proto as i64).to_bits(), + let bits = crate::value::js_nanbox_pointer(proto as i64).to_bits(); + closure_set_static_prototype(a as usize, bits); + // The link stays a shape fact: a described Function shape whose + // identity names it, off the base identity. + assert_eq!(kind_of(a), Some(ShapeObjectKind::Function)); + assert!(!unsafe { closure_on_base_identity(a) }); + assert_eq!( + crate::closure::closure_static_prototype(a as usize), + Some(bits) ); - assert_eq!(kind_of(a), Some(ShapeObjectKind::FunctionDictionary)); let b = fresh(&PLAIN_BODY); crate::closure::closure_mark_key_deleted(b as usize, "length"); diff --git a/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs b/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs index f43825fbe3e..3ce12c57b69 100644 --- a/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs +++ b/crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs @@ -395,9 +395,10 @@ pub(crate) fn ordinary_compiled_function_has_instance( unsafe { // InstanceofOperator step 2: an own `@@hasInstance` is a symbol key, // which only a FunctionDictionary function object can hold; the - // inherited one is Function.prototype's, which is non-writable and - // non-configurable, so OrdinaryHasInstance applies. - if !crate::closure::shape::closure_on_base_shape(closure as *const ClosureHeader) { + // inherited one is Function.prototype's (when no link moved the + // function off it), which is non-writable and non-configurable, so + // OrdinaryHasInstance applies. + if !crate::closure::shape::closure_on_base_identity(closure as *const ClosureHeader) { return None; } let proto = own_prototype_object(closure)?; diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index c8f9e31158d..a3372dbdcfd 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -212,7 +212,11 @@ pub(crate) fn get_field_by_name_object_tail( if val.to_bits() != crate::value::TAG_UNDEFINED { return JSValue::from_bits(val.to_bits()); } - if name_str == "constructor" { + // Only a function on its intrinsic [[Prototype]] inherits this: + // a linked one's read above already answered from its chain. + if name_str == "constructor" + && crate::closure::closure_static_prototype(obj as usize).is_none() + { if let Some(ctor) = crate::object::generator_function_constructor_of(obj as usize) { @@ -676,7 +680,10 @@ pub(crate) fn get_field_by_name_object_tail( // to `%GeneratorFunction%` / `%AsyncGeneratorFunction%`. // Other functions have no `constructor` own-prop in Perry's // model (they fall through to `undefined`, as before). - if name_str == "constructor" { + // A linked function's read above answered from its chain. + if name_str == "constructor" + && crate::closure::closure_static_prototype(obj as usize).is_none() + { if let Some(ctor) = crate::object::generator_function_constructor_of(obj as usize) { diff --git a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs index be9dcd55f81..377f445c13d 100644 --- a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs @@ -110,16 +110,24 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, mut type_ref: f64) -> f64 { // overrides even native constructor brand checks. The native generic hook // lives on Function.prototype, so the own-property gate distinguishes an // explicit override from that inherited default without recursion. - let symbols_absent = rhs.closure.is_some_and(|(closure, _)| unsafe { - let bag = (*closure).props; - bag.is_null() - || !crate::object::overflow::shadowed(bag) - && crate::object::shapes::object_shape_record(bag).is_some_and(|record| { - // A legacy dictionary can have an ordinary keyless shape; - // its private key list carries no immutable absence proof. - record.keys() != 0 && record.proves_no_symbols() - }) - }); + // That inherited default is the hook only while the function's chain is + // its intrinsic one: a linked function's `@@hasInstance` is GetMethod's + // full [[Get]] over its chain (the intrinsic thunk, reached, answers the + // same ordinary walk). + let linked = rhs + .closure + .is_some_and(|(closure, _)| unsafe { linked_chain_may_hook(closure) }); + let symbols_absent = !linked + && rhs.closure.is_some_and(|(closure, _)| unsafe { + let bag = (*closure).props; + bag.is_null() + || !crate::object::overflow::shadowed(bag) + && crate::object::shapes::object_shape_record(bag).is_some_and(|record| { + // A legacy dictionary can have an ordinary keyless shape; + // its private key list carries no immutable absence proof. + record.keys() != 0 && record.proves_no_symbols() + }) + }); if !symbols_absent { let hi_sym = crate::symbol::well_known_symbol("hasInstance"); if !hi_sym.is_null() { @@ -131,8 +139,11 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, mut type_ref: f64) -> f64 { // their ordinary symbol query. let owns_hook = if let Some((closure, _)) = rhs.closure { let bag = unsafe { (*closure).props }; - !bag.is_null() - && unsafe { super::shaped_symbols::position(bag, hi_sym as usize).is_some() } + linked + || !bag.is_null() + && unsafe { + super::shaped_symbols::position(bag, hi_sym as usize).is_some() + } } else { unsafe { crate::symbol::js_object_has_own_symbol(type_ref, hi_f64) } }; @@ -149,8 +160,21 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, mut type_ref: f64) -> f64 { value = lhs_h.get_nanbox_f64(); type_ref = rhs_h.get_nanbox_f64(); rhs.refresh_after_getter(type_ref); - if let HasInstanceOutcome::Result(result) = dispatch_own_has_instance(cb, value) { - return result; + // A linked function whose chain reaches the intrinsic default + // takes the ordinary dispatch below (its native brand checks + // are that default's answer). + let intrinsic = linked && { + let fp = crate::object::builtin_prototype_value("Function"); + crate::JSValue::from_bits(fp.to_bits()).is_pointer() + && unsafe { crate::symbol::js_object_get_symbol_property(fp, hi_f64) } + .to_bits() + == cb.to_bits() + }; + if !intrinsic { + if let HasInstanceOutcome::Result(result) = dispatch_own_has_instance(cb, value) + { + return result; + } } } } @@ -695,3 +719,61 @@ mod tests { ); } } + +/// Can the chain a linked function `closure` names supply an `@@hasInstance` +/// other than `%Function.prototype%`'s default? Walks the hops before that +/// default (or null): each must prove it owns no symbol key from its shape. +/// `false` for a function on its intrinsic chain, or one whose linked chain +/// proves absence hop by hop (a builtin constructor linked to its parent). +/// +/// # Safety +/// `closure` is a proven, live closure cell. +unsafe fn linked_chain_may_hook(closure: *const crate::closure::ClosureHeader) -> bool { + let Some(mut bits) = crate::closure::closure_static_prototype(closure as usize) else { + return false; + }; + let fp = crate::object::builtin_prototype_value("Function").to_bits(); + for _ in 0..32 { + let v = crate::JSValue::from_bits(bits); + if bits == fp || v.is_null() { + return false; + } + if !v.is_pointer() { + return true; + } + let addr = v.as_pointer::() as usize; + let proves = |bag: *mut ObjectHeader| { + bag.is_null() + || !crate::object::overflow::shadowed(bag) + && crate::object::shapes::object_shape_record(bag) + .is_some_and(|record| record.keys() != 0 && record.proves_no_symbols()) + }; + if crate::closure::is_closure_ptr(addr) { + let hop = addr as *const crate::closure::ClosureHeader; + if crate::closure::shape::is_class_info((*hop).info) || !proves((*hop).props) { + return true; + } + bits = match crate::closure::closure_static_prototype(addr) { + Some(next) => next, + None => return false, + }; + continue; + } + match crate::value::addr_class::try_read_gc_header(addr) { + Some(header) + if header.obj_type == crate::gc::GC_TYPE_OBJECT + && header.gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 => + { + if !proves(addr as *mut ObjectHeader) { + return true; + } + bits = match crate::object::prototype_chain::object_static_prototype(addr) { + Some(next) => next, + None => crate::object::builtin_prototype_value("Object").to_bits(), + }; + } + _ => return true, + } + } + true +} diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs index a461272c451..7889fec312f 100644 --- a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -282,15 +282,24 @@ fn native_instanceof( // has `Function.prototype` in its prototype chain. Keep `CLASS_ID_FUNCTION` // in sync with perry-codegen/src/expr/instance_misc1.rs. if class_id == CLASS_ID_FUNCTION { - if value_is_callable(value) { + // A function linked off its intrinsic chain (`Object.setPrototypeOf + // (fn, p)`) reaches `Function.prototype` only if `p`'s chain does. + let addr = value_addr(value); + let linked_function = addr != 0 + && crate::closure::is_closure_ptr(addr) + && !crate::closure::shape::is_class_info(unsafe { + (*(addr as *const crate::closure::ClosureHeader)).info + }) + && crate::closure::closure_static_prototype(addr).is_some(); + if value_is_callable(value) && !linked_function { return true_val; } // An ordinary object whose recorded chain reaches a function // (`Object.create(fn)`) has `Function.prototype` on it too. - let addr = value_addr(value); if addr != 0 - && unsafe { crate::object::prototype_chain::meta_capable_object(addr) }.is_some() - && crate::object::prototype_chain::object_static_prototype(addr).is_some() + && (linked_function + || unsafe { crate::object::prototype_chain::meta_capable_object(addr) }.is_some() + && crate::object::prototype_chain::object_static_prototype(addr).is_some()) { let function = constructor .as_ref() diff --git a/crates/perry-runtime/src/object/layout_probe.rs b/crates/perry-runtime/src/object/layout_probe.rs index 486e92e250f..5543b01b16c 100644 --- a/crates/perry-runtime/src/object/layout_probe.rs +++ b/crates/perry-runtime/src/object/layout_probe.rs @@ -1065,9 +1065,13 @@ unsafe fn exotic_get( crate::gc::GC_TYPE_CLOSURE => { let c = obj as *const crate::closure::ClosureHeader; let plain = crate::closure::shape::closure_on_plain_function_shape(c); + // A plain function's [[Prototype]] is its shape's identity + // (`%Function.prototype%` until a link moves it). ( (*c).props as *const ObjectHeader, - plain.then_some(crate::closure::shape::INTRINSIC_SERIAL_FUNCTION), + plain + .then(|| crate::object::shapes::shape_proto_id((*c).shape_id)) + .flatten(), plain, ) } diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index f5eef61cfae..a0e2a6b2ae6 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -626,6 +626,31 @@ pub fn object_static_prototype(obj_ptr: usize) -> Option { return crate::buffer::store::bag_get(obj_ptr, crate::buffer::store::PROTOTYPE_KEY) .map(f64::to_bits); } + // A function's link is its own (its shape's identity, or its + // record once a dictionary), never its property bag's: the bag's + // record slot below is an exotic owner's, and the bag itself is + // born null. A class constructor keeps its holder answer. + // `receiver_header_type` proved the cell and read its type. + Some(crate::gc::GC_TYPE_CLOSURE) + if (*crate::gc::header_from_trusted_user_ptr(obj_ptr as *const u8)).gc_flags + & crate::gc::GC_FLAG_FORWARDED + == 0 + && !crate::closure::shape::is_class_info( + (*(obj_ptr as *const crate::closure::ClosureHeader)).info, + ) => + { + if let Some(bits) = crate::closure::closure_static_prototype_proven(obj_ptr) { + return Some(bits); + } + // A legacy holder record on its bag (`object_set_static_prototype` + // of a function): read the bag's record directly. + let bag = (*(obj_ptr as *const crate::closure::ClosureHeader)).props; + if bag.is_null() { + return None; + } + let meta = crate::meta_read!(crate::meta_obj_runtime_born!(bag as *mut _)); + return (!meta.is_null() && (*meta).prototype != 0).then(|| (*meta).prototype); + } Some(kind) if !owned_prototype_receiver_type(kind) && crate::value::addr_class::try_read_tracked_gc_header(obj_ptr).is_some() => diff --git a/crates/perry-runtime/src/object/shape_store.rs b/crates/perry-runtime/src/object/shape_store.rs index 86ceaf643a7..dbabc1e861f 100644 --- a/crates/perry-runtime/src/object/shape_store.rs +++ b/crates/perry-runtime/src/object/shape_store.rs @@ -984,9 +984,16 @@ unsafe fn try_function_store( return super::try_existing_own_data_overwrite_by_content(bag, key, name.as_bytes(), value) .then_some(value); } + // A bound function keeps its own keys in the same bag as any function + // (its intrinsic `name`/`length` were declined above), and its shape + // names its [[Prototype]] as any described function's does. if !matches!( descriptor.object_kind, - shapes::ShapeObjectKind::Function | shapes::ShapeObjectKind::FunctionDictionary + shapes::ShapeObjectKind::Function + | shapes::ShapeObjectKind::FunctionDictionary + | shapes::ShapeObjectKind::FunctionBound + | shapes::ShapeObjectKind::FunctionBoundCall + | shapes::ShapeObjectKind::FunctionBoundApply ) || flags & (crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND) != 0 { return None; diff --git a/crates/perry-runtime/src/object/shape_store_tests.rs b/crates/perry-runtime/src/object/shape_store_tests.rs index 6306808660d..f39d02aad2e 100644 --- a/crates/perry-runtime/src/object/shape_store_tests.rs +++ b/crates/perry-runtime/src/object/shape_store_tests.rs @@ -686,7 +686,7 @@ fn function_dictionary_adds_reuse_the_named_bag_transition() { } #[test] -fn function_dictionary_asks_its_actual_prototype_own_attributes() { +fn a_linked_function_asks_its_actual_prototype_own_attributes() { let _lock = crate::gc::global_side_table_test_lock(); let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); let a = fresh_test_function(); @@ -715,10 +715,10 @@ fn function_dictionary_asks_its_actual_prototype_own_attributes() { (*((v.to_bits() & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader)) .shape_id }; - assert_eq!( + assert_ne!( id(a), id(b), - "FunctionDictionary is shared across these distinct prototype chains" + "each function's shape names its own prototype identity" ); } assert_eq!( @@ -730,7 +730,7 @@ fn function_dictionary_asks_its_actual_prototype_own_attributes() { assert_eq!( unsafe { try_store(b, key_value, 2.0) }, Some(2.0), - "the same FunctionDictionary id with a different prototype must remain writable" + "a different prototype's chain must remain writable" ); } diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 5e978ba7f4b..1f6882ccb3e 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -5153,6 +5153,13 @@ pub(crate) unsafe fn stable_declared_linked_proto_id(class_id: u32, bits: u64) - stable_linked_proto_id_for_class(class, bits) } +/// The [[Prototype]] identity a function's shape names for a link to `bits` +/// (null, or a prototype that already has its stable serial): the class-less +/// rule an ordinary receiver's link takes. +pub(crate) fn stable_function_link_identity(bits: u64) -> Option { + stable_linked_proto_id_for_class(0, bits) +} + fn stable_linked_proto_id_for_class(class: u32, bits: u64) -> Option { if bits == crate::value::TAG_NULL { return Some(PROTO_ID_NULL); diff --git a/test-files/test_gap_function_first_inherited_read.ts b/test-files/test_gap_function_first_inherited_read.ts new file mode 100644 index 00000000000..96a0f039c3e --- /dev/null +++ b/test-files/test_gap_function_first_inherited_read.ts @@ -0,0 +1,7 @@ +// The program's first inherited read of a function is %Function.prototype%'s +// member, before anything else has built the realm global. +const f: any = function f() {}; +const t = f.toString; +console.log("1", typeof t, t === Function.prototype.toString); +const g: any = (x: number) => x; +console.log("2", typeof g.hasOwnProperty, g.constructor === Function); diff --git a/test-files/test_gap_function_link_on_shape.ts b/test-files/test_gap_function_link_on_shape.ts new file mode 100644 index 00000000000..48a4ae61970 --- /dev/null +++ b/test-files/test_gap_function_link_on_shape.ts @@ -0,0 +1,51 @@ +// A function's [[Prototype]] is its shape's identity while its shape describes +// it, and its own record's once it is a dictionary; reads, getPrototypeOf and +// instanceof all follow the one link. +const P = Object.getPrototypeOf; +const proto = { inherited: "from-proto", get viaThis() { return typeof this === "function" ? "fn:" + (this as any).tag : "other"; } }; +function plain() { return 1; } +(plain as any).tag = "plain"; +Object.setPrototypeOf(plain, proto); +console.log("1", P(plain) === proto, (plain as any).inherited, (plain as any).viaThis, typeof (plain as any).call, plain()); +const bound: any = Object.assign(Object.setPrototypeOf(Function.prototype.bind.call(plain, null), proto), { tag: "bound" }); +console.log("2", P(bound) === proto, bound.inherited, bound.viaThis, bound.name, bound.length, typeof bound.apply); +async function af() { return 2; } +Object.setPrototypeOf(af, proto); +console.log("3", P(af) === proto, (af as any).inherited, typeof (af as any).then); +class Base { m() { return "Base.m"; } get g() { return "g:" + (this as any).tag; } } +class Derived extends Base {} +const callable: any = Object.assign(Object.setPrototypeOf(((x: number) => x * 2).bind(undefined), Derived.prototype), { tag: "callable" }); +console.log("4", P(callable) === Derived.prototype, callable.m(), callable.g, callable instanceof Derived, callable(21)); +const nul: any = Object.setPrototypeOf(function n() {}, null); +console.log("5", P(nul), typeof nul.call, typeof nul.toString, nul.name, "call" in nul); +// relink twice, then back to Function.prototype +const re: any = function re() {}; +Object.setPrototypeOf(re, proto); +Object.setPrototypeOf(re, { other: "second" }); +console.log("6", re.other, re.inherited, P(re) === proto); +Object.setPrototypeOf(re, Function.prototype); +console.log("7", P(re) === Function.prototype, re.other, typeof re.call); +// an accessor and a delete after a link: the link stays +const dict: any = function dict() {}; +Object.setPrototypeOf(dict, proto); +Object.defineProperty(dict, "acc", { get() { return "own-acc"; }, configurable: true }); +console.log("8", P(dict) === proto, dict.acc, dict.inherited); +delete dict.name; +console.log("9", P(dict) === proto, dict.name, dict.inherited); +Object.setPrototypeOf(dict, { fresh: "fresh" }); +console.log("10", dict.fresh, dict.inherited, dict.acc); +// own keys shadow the link +const sh: any = Object.setPrototypeOf(function sh() {}, proto); +sh.inherited = "own"; +console.log("11", sh.inherited, P(sh) === proto); +// a custom @@hasInstance on the linked chain +const hp = { [Symbol.hasInstance](v: any) { return v === 42; } }; +const hf: any = Object.setPrototypeOf(function hf() {}, hp); +console.log("12", (42 as any) instanceof hf, ({} as any) instanceof hf); +// Function.prototype is not on a relinked function's chain +(Function.prototype as any).fpx = "fp"; +console.log("13", (plain as any).fpx, (function () {} as any).fpx, bound.fpx); +delete (Function.prototype as any).fpx; +// a function linked off %Function.prototype% is an instance of Function only +// through its link, and inherits no `constructor` from a null link +console.log("14", callable instanceof Function, plain instanceof Function, nul instanceof Function, typeof nul.constructor, (function () {}) instanceof Function); diff --git a/test-files/test_gap_prototype_link_fast_paths.ts b/test-files/test_gap_prototype_link_fast_paths.ts new file mode 100644 index 00000000000..1f01fbebb8d --- /dev/null +++ b/test-files/test_gap_prototype_link_fast_paths.ts @@ -0,0 +1,42 @@ +// Each fast path the one-authority [[Prototype]] model takes must still +// answer what the full operation does. +const log: string[] = []; +// A store on an object whose chain holds a setter runs the setter; on a null +// chain it creates the own property. +const withSetter = { set zz(_v: number) { log.push("setter"); } }; +const o: any = Object.setPrototypeOf({}, withSetter); +o.zz = 1; +const n: any = Object.create(null); +n.zz = 1; +console.log("1", Object.prototype.hasOwnProperty.call(o, "zz"), log.join(","), n.zz, Object.keys(n).join(",")); +// A relinked chain's toJSON is found; a null chain has none. +const tj = Object.setPrototypeOf({ a: 1 }, { toJSON() { return "TJ"; } }); +const nj = Object.assign(Object.create(null), { a: 2 }); +console.log("2", JSON.stringify(tj), JSON.stringify(nj), JSON.stringify([tj, nj])); +// Every body kind stands on its own intrinsic until a link moves it. +async function af() {} +function* gf() {} +const AsyncFunction = Object.getPrototypeOf(af); +console.log("3", Object.getPrototypeOf(function () {}) === Function.prototype, Object.getPrototypeOf(af) === AsyncFunction, + Object.getPrototypeOf(gf) !== Function.prototype, typeof (function () {}).call, (() => 1) instanceof Function); +// A key added after a link keeps the link. +const p = { inherited: "p" }; +const fk: any = Object.setPrototypeOf(function fk() {}, p); +fk.k1 = 1; fk.k2 = 2; +console.log("4", Object.getPrototypeOf(fk) === p, fk.inherited, fk.k1 + fk.k2, Object.keys(fk).join(",")); +// A bound function linked to a chain with a setter runs it; a readonly key +// on the chain blocks the store. +const ro = Object.defineProperty({}, "fixed", { value: 1, writable: false }); +const b: any = Object.setPrototypeOf(function t() {}.bind(null), Object.setPrototypeOf({ set zz(_v: number) { log.push("bound-setter"); } }, ro)); +b.zz = 1; +const fixedSet = Reflect.set(b, "fixed", 2); +b.free = 3; +console.log("5", Object.prototype.hasOwnProperty.call(b, "zz"), log.join(","), fixedSet, b.fixed, b.free); +// A plain function stands on %Function.prototype%, not another body kind's +// intrinsic, with or without own keys. +const AF: any = Object.getPrototypeOf(af); +AF.onAsync = "A"; +const pk: any = function pk() {}; pk.own = 1; +const pn: any = function pn() {}; +console.log("6", pk.onAsync, pn.onAsync, (af as any).onAsync); +delete AF.onAsync; From ad67282e34324cf9acd92059b84941e7c8918872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 1 Nov 2026 14:00:40 +0000 Subject: [PATCH 3/4] changelog: fragment for the [[Prototype]] shape authority --- changelog.d/0-prototype-shape-authority.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 changelog.d/0-prototype-shape-authority.md diff --git a/changelog.d/0-prototype-shape-authority.md b/changelog.d/0-prototype-shape-authority.md new file mode 100644 index 00000000000..9534b25bb36 --- /dev/null +++ b/changelog.d/0-prototype-shape-authority.md @@ -0,0 +1,20 @@ +An object's and a function's [[Prototype]] now have one record: the shape. + +A null prototype was recorded twice, in the shape's identity and in a header +bit (`OBJ_FLAG_NULL_PROTO`) set at birth, and readers disagreed. Fixed: a +thenable on a relinked `Object.create(null)` object was not followed; +`util.inspect` called an object relinked to `Object.prototype` "null +prototype" and missed `Object.setPrototypeOf(x, null)`; `require.cache` +inherited `Object.prototype`'s methods. The bit is gone (0x40 is free). + +`Object.setPrototypeOf(fn, p)` moved a function to the shared +FunctionDictionary shape and kept the link as a key in its state record; the +function now stays on a described shape that names the link, as an ordinary +object does. Fixed along the way: `fn instanceof C` and `fn instanceof +Function` ignored a function's link; a function linked to `null` still read +`toString` and `constructor` from `Function.prototype`; an `@@hasInstance` on +a function's linked chain was ignored; a program's first inherited read of a +function (`fn.toString`) answered `undefined`. Stores onto a bound function +take the function store lane. An inherited read on a callable class instance +drops from 1.93k to 1.64k Ir, a call from 3.70k to 3.22k; startup Ir drops on +every witness (fastify inject -0.27%, commander -0.89%). From b19b243bf186e095dc67e8447bae302970569f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 1 Nov 2026 15:37:57 +0100 Subject: [PATCH 4/4] changelog: number the PR fragment --- ...type-shape-authority.md => 13666-prototype-shape-authority.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{0-prototype-shape-authority.md => 13666-prototype-shape-authority.md} (100%) diff --git a/changelog.d/0-prototype-shape-authority.md b/changelog.d/13666-prototype-shape-authority.md similarity index 100% rename from changelog.d/0-prototype-shape-authority.md rename to changelog.d/13666-prototype-shape-authority.md