Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changelog.d/11834-prototype-link-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
The per-object prototype-divergence flag is gone (Refs #10507). Since an
object's prototype is a fact of its shape, the caches that consulted the flag
(store plans, array-subclass dense layouts, the defineProperty key-add path,
`instanceof` against a class object) already key on the shape or the
prototype, so a re-parented object simply meets a different cache entry. A
runtime-wired function-constructor instance no longer allocates a metadata
record.

The other prototype-link flags and the metadata allocation for them are gone
too (Refs #10507): `instanceof`, JSON.stringify's plain-record check, symbol
reads and the static shapes read an object's recorded prototype from its
shape, `instanceof` walks the live chain for a receiver on a foreign
prototype (so the `util.inherits` escape hatch and its latch are deleted),
and linking a prototype no longer allocates a metadata record.
28 changes: 6 additions & 22 deletions crates/perry-runtime/src/array/subclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,6 @@ pub(super) struct ValidatedObjectReceiver {
pub(super) object_flags: u16,
}

/// Read the per-instance prototype-divergence bit after the caller has already
/// proved a live, non-forwarded `GC_TYPE_OBJECT` receiver.
///
/// The public prototype-chain predicate accepts arbitrary addresses and must
/// re-run buffer/heap/header classification before touching `ObjectHeader`.
/// Dense Array-subclass paths have just completed that proof, so repeating it
/// ahead of every receiver-local layout-cache hit is both redundant and hot.
#[inline(always)]
unsafe fn validated_object_has_prototype_divergence(obj: *const ObjectHeader) -> bool {
let meta = (*obj).meta;
!meta.is_null() && (*meta).flags & crate::object::OBJECT_META_FLAG_PROTO_DIVERGED != 0
}

#[inline(always)]
fn dense_cache_key(class_id: u32, shape_id: u32) -> u64 {
((class_id as u64) << 32) | shape_id as u64
Expand Down Expand Up @@ -251,10 +238,9 @@ fn decimal_u32<'a>(mut value: u32, buf: &'a mut [u8; 10]) -> &'a [u8] {
/// allocates nothing and keeps no address into the moving heap.
unsafe fn build_dense_layout(obj: *const ObjectHeader) -> Option<DenseSubclassLayout> {
let class_id = (*obj).class_id;
if class_id == 0
|| !is_array_subclass_class_id(class_id)
|| validated_object_has_prototype_divergence(obj)
{
// A receiver moved to another prototype is on another ShapeId (the
// prototype identity is a shape fact), so it cannot meet this layout.
if class_id == 0 || !is_array_subclass_class_id(class_id) {
return None;
}
let shape = crate::object::shapes::object_shape_descriptor(obj)?;
Expand Down Expand Up @@ -366,11 +352,9 @@ fn validated_object_receiver_for_value(value: f64) -> Option<ValidatedObjectRece
/// descriptor, hole, or prototype case returns `None`.
#[inline]
fn dense_layout_for_validated_object(obj: *const ObjectHeader) -> Option<DenseSubclassLayout> {
// This is per receiver, not per ShapeId. A cached layout built before
// Object.setPrototypeOf must not let this object borrow the old proof.
if unsafe { validated_object_has_prototype_divergence(obj) } {
return None;
}
// `Object.setPrototypeOf` moves the receiver to another ShapeId, so a
// layout cached for the old one (owner word or `(class, ShapeId)` key)
// cannot be borrowed afterwards.
if let Some(layout) = unsafe { owner_cached_dense_layout(obj) } {
return Some(layout);
}
Expand Down
11 changes: 2 additions & 9 deletions crates/perry-runtime/src/array/subclass_loop_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,9 +479,8 @@ pub extern "C" fn js_packed_arraylike_loop_revalidate_live(
}
let object = raw.cast::<ObjectHeader>();
let current_receiver_word = unsafe { ptr::read_unaligned(raw.cast::<u64>()) };
if current_receiver_word != receiver_word
|| crate::object::prototype_chain::object_has_prototype_divergence(raw as usize)
{
// The receiver word carries the ShapeId, which pins the prototype.
if current_receiver_word != receiver_word {
return 0;
}
let dense_prefix_len = packed_bounds as u32;
Expand Down Expand Up @@ -625,12 +624,6 @@ fn revalidate_admitted_subclass_live(
{
return 0;
}
let meta = unsafe { (*object).meta };
if !meta.is_null()
&& unsafe { (*meta).flags } & crate::object::OBJECT_META_FLAG_PROTO_DIVERGED != 0
{
return 0;
}
let dense_prefix_len = packed_bounds as u32;
let live_inline_slots = (packed_bounds >> 32) as u32;
if admitted_bound > dense_prefix_len {
Expand Down
16 changes: 13 additions & 3 deletions crates/perry-runtime/src/array/subclass_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ fn dense_array_subclass_reads_slots_until_its_shape_changes() {
}

#[test]
fn dense_array_subclass_cache_declines_a_per_instance_prototype_override() {
fn dense_array_subclass_layout_follows_the_shape_across_a_prototype_override() {
// Pins the shape-carried representation: the elements store is the
// default, and this test is about the property-shape machinery.
let _representation =
Expand All @@ -265,11 +265,21 @@ fn dense_array_subclass_cache_declines_a_per_instance_prototype_override() {
crate::object::js_object_set_index_polymorphic(obj as i64, 0.0, 11.0);

assert_eq!(array_subclass_fast_index_get(receiver, 0), Some(11.0));
let before = unsafe { (*obj).parent_class_id };
crate::object::prototype_chain::object_set_user_prototype(obj as usize, crate::value::TAG_NULL);
// The prototype is a fact of the shape: the receiver moved to another
// ShapeId, so no layout cached for the old one (owner word or
// `(class, ShapeId)` key) can answer for it. What the new shape's layout
// answers is the receiver's OWN element, which no prototype affects.
assert_ne!(
unsafe { (*obj).parent_class_id },
before,
"a prototype change must move the receiver to another ShapeId"
);
assert_eq!(
array_subclass_fast_index_get(receiver, 0),
None,
"a receiver-local dense-layout record must not survive prototype divergence"
Some(11.0),
"an own dense element reads the same on any prototype"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,6 @@ fn test_object_meta_prototype_survives_copied_minor_move() {
Some(ptr_bits(old_proto)),
"test premise: the meta-resident prototype reads back before the GC"
);
assert!(
crate::object::prototype_chain::object_has_user_prototype_override(old_owner),
"test premise: the per-instance override bit lives in the meta record"
);
js_shadow_slot_set(0, ptr_bits(old_owner));
js_shadow_slot_set(1, ptr_bits(old_proto));

Expand All @@ -159,10 +155,6 @@ fn test_object_meta_prototype_survives_copied_minor_move() {
new_proto,
"the meta record's prototype slot must be rewritten to the moved proto"
);
assert!(
crate::object::prototype_chain::object_has_user_prototype_override(new_owner),
"the non-pointer meta flags must travel with the copied record"
);

js_shadow_slot_set(0, 0);
js_shadow_slot_set(1, 0);
Expand Down Expand Up @@ -739,10 +731,6 @@ fn test_object_meta_null_prototype_survives_full_gc_on_live_owner() {
"a live (rooted) owner's explicit-null prototype (a fact of its \
shape) must survive a full collection"
);
assert!(
!unsafe { (*(live as *const crate::object::ObjectHeader)).meta }.is_null(),
"the runtime-wiring link's divergence flag keeps its meta record alive"
);
js_shadow_slot_set(0, 0);
js_shadow_frame_pop(frame);
}
7 changes: 6 additions & 1 deletion crates/perry-runtime/src/json/stringify_tojson_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,12 @@ pub(super) unsafe fn plain_object_member(
return None;
}
let obj = addr as *const crate::ObjectHeader;
if !(*obj).meta.is_null() || !class_is_plain_record((*obj).class_id) {
// A recorded prototype (a fact of the shape, or a meta record's word)
// can carry a `toJSON` of its own: only a default-chained record is plain.
if !(*obj).meta.is_null()
|| !class_is_plain_record((*obj).class_id)
|| crate::object::shapes::object_prototype_word(obj) != 0
{
return None;
}
let (keys, live_slots) = crate::object::object_keys_and_live_slot_count(obj);
Expand Down
7 changes: 4 additions & 3 deletions crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,15 +676,16 @@ fn plain_object_member_admits_only_what_the_member_dispatch_would_walk() {
assert!(super::plain_object_member(declined, &mut proof).is_none());
}

// A recorded prototype lives in the meta record: declined.
// A recorded prototype (a fact of the receiver's shape): declined.
let inherits = parsed(&scope, r#"{"a":1}"#);
let proto = crate::object::js_object_alloc(0, 0);
crate::object::prototype_chain::object_set_user_prototype(
obj(&inherits) as usize,
crate::value::js_nanbox_pointer(proto as i64).to_bits(),
);
assert!(
!(*obj(&inherits)).meta.is_null(),
assert_eq!(
crate::object::prototype_chain::object_static_prototype(obj(&inherits) as usize),
Some(crate::value::js_nanbox_pointer(proto as i64).to_bits()),
"fixture must record a prototype"
);
assert!(
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ pub unsafe extern "C" fn js_super_method_call_dynamic(
// the declared-chain paths below read the refreshed copies.
let refreshed_args: Vec<f64>;
let (this_value, args_ptr) = if static_entry.is_none()
|| super::prototype_chain::any_user_prototype_override()
|| super::prototype_chain::any_class_chain_relinked()
{
let live_scope = crate::gc::RuntimeHandleScope::new();
let this_handle = live_scope.root_nanbox_f64(this_value);
Expand Down
11 changes: 6 additions & 5 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ pub use state::{

// ── prototype_objects.rs ────────────────────────────────────────────────────
pub(crate) use prototype_objects::{
class_decl_prototype_relinked, class_prototype_object, ensure_function_prototype_object,
function_class_id, function_value_for_class_id, instance_class_prototype_object,
object_proto_chain_symbol_slot, relinked_class_prototype_read, resolve_proto_chain_field,
resolve_proto_chain_field_noting_miss, resolve_proto_chain_field_with_receiver,
resolve_proto_chain_symbol, synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE,
class_decl_prototype_relinked, class_prototype_object, decl_prototype_relinked,
ensure_function_prototype_object, function_class_id, function_value_for_class_id,
instance_class_prototype_object, object_proto_chain_symbol_slot, relinked_class_prototype_read,
resolve_proto_chain_field, resolve_proto_chain_field_noting_miss,
resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol,
synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE,
};
pub use prototype_objects::{
js_set_function_prototype, js_set_prototype_property, NEXT_SYNTHETIC_CLASS_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ pub(crate) unsafe fn class_prototype_relinked(proto: *mut crate::object::ObjectH
if cid == 0 || super::class_decl_prototype_object(cid) != proto {
return;
}
super::super::prototype_chain::note_class_chain_relinked();
let mut names: Vec<String> = Vec::new();
if let Ok(registry) = super::CLASS_VTABLE_REGISTRY.read() {
if let Some(reg) = registry.as_ref() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,9 +636,36 @@ unsafe fn evaluated_parent_instance_field(
/// names the next hop of an instance chain, and walks over the class
/// registry must stop at `cid` (the recorded link continues the chain).
pub(crate) fn class_decl_prototype_relinked(cid: u32) -> bool {
if !super::super::prototype_chain::any_class_chain_relinked() {
return false;
}
let decl_proto = class_decl_prototype_object(cid);
!decl_proto.is_null()
&& super::super::prototype_chain::object_has_user_prototype_override(decl_proto as usize)
!decl_proto.is_null() && unsafe { decl_prototype_relinked(cid, decl_proto) }
}

/// Is `decl_proto` (class `cid`'s declaration prototype) standing on
/// anything but what its declaration links it to — the parent class's
/// declaration prototype, or `Object.prototype` for a base class? Read from
/// its recorded `[[Prototype]]` (a fact of its shape), not from history: a
/// relink back to the declared parent is not a relink. A declaration this
/// cannot name (a runtime-valued or native parent, `extends null`) answers
/// `true`, which only sends the caller down the exact prototype walk.
///
/// # Safety
/// `decl_proto` is class `cid`'s live declaration prototype.
pub(crate) unsafe fn decl_prototype_relinked(cid: u32, decl_proto: *mut ObjectHeader) -> bool {
let recorded = super::super::prototype_chain::object_static_prototype(decl_proto as usize);
let declared = match super::get_parent_class_id(cid) {
Some(parent) if parent != 0 && parent != cid => {
let parent_proto = class_decl_prototype_object(parent);
if parent_proto.is_null() {
return true;
}
Some(crate::value::js_nanbox_pointer(parent_proto as i64).to_bits())
}
_ => super::global_object_prototype_bits(),
};
recorded != declared
}

/// `key` read on the rest of class `cid`'s instance chain past its declared
Expand All @@ -660,7 +687,7 @@ pub(crate) unsafe fn relinked_class_prototype_read(
if decl_proto.is_null() {
return None;
}
match relinked_decl_prototype_field(decl_proto, key, receiver) {
match relinked_decl_prototype_field(cid, decl_proto, key, receiver) {
RelinkedRead::NotRelinked => None,
RelinkedRead::Answered(value) => Some(Some(value)),
RelinkedRead::Missed => Some(None),
Expand Down Expand Up @@ -689,12 +716,14 @@ enum RelinkedRead {
/// facts a read site validates decline on their own; this is the generic read
/// those sites fall back to and confirm their prime against.
unsafe fn relinked_decl_prototype_field(
cid: u32,
decl_proto: *mut ObjectHeader,
key: *const crate::StringHeader,
receiver: f64,
) -> RelinkedRead {
if key.is_null()
|| !super::super::prototype_chain::object_has_user_prototype_override(decl_proto as usize)
|| !super::super::prototype_chain::any_class_chain_relinked()
|| !decl_prototype_relinked(cid, decl_proto)
{
return RelinkedRead::NotRelinked;
}
Expand Down Expand Up @@ -935,7 +964,7 @@ unsafe fn resolve_proto_chain_field_inner(
// prototype. The registered parent class id no longer names the next
// hop: the recorded link does, and the rest of the chain is X's.
if let (false, Some(receiver)) = (decl_proto.is_null(), receiver) {
match relinked_decl_prototype_field(decl_proto, key, receiver) {
match relinked_decl_prototype_field(cid, decl_proto, key, receiver) {
RelinkedRead::NotRelinked => {}
RelinkedRead::Answered(value) => return Some(value),
RelinkedRead::Missed => return None,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/object/class_registry/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@ pub(crate) fn class_static_prototype_root_store(class_id: u32, proto_ptr: *mut O
if class_id == 0 || proto_ptr.is_null() {
return;
}
super::super::prototype_chain::note_class_chain_relinked();
let bits = crate::value::js_nanbox_pointer(proto_ptr as i64).to_bits();
crate::closure::closure_set_static_prototype(
crate::object::class_value::class_value_ptr(class_id) as usize,
Expand All @@ -814,6 +815,7 @@ pub(crate) fn class_static_prototype_root_clear(class_id: u32) {
if class_id == 0 {
return;
}
super::super::prototype_chain::note_class_chain_relinked();
crate::closure::closure_set_static_prototype(
crate::object::class_value::class_value_ptr(class_id) as usize,
crate::value::TAG_NULL,
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/object/class_super_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub(crate) unsafe fn class_super_base(
/// static lookup no longer describes it? One latch load answers `false` in a
/// process that never set a user prototype.
pub(super) fn static_chain_relinked(home_cid: u32, owner_cid: u32) -> bool {
if !super::prototype_chain::any_user_prototype_override() {
if !super::prototype_chain::any_class_chain_relinked() {
return false;
}
let mut cur = home_cid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,6 @@ pub(crate) unsafe fn prototype_from_template(
owner.to_bits(),
);
(*meta).prototype = parent_bits;
(*meta).flags |= crate::object::OBJECT_META_FLAG_PROTO_DIVERGED
| crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO;
crate::gc::runtime_write_barrier_slot(
meta as usize,
&(*meta).prototype as *const u64 as usize,
Expand Down Expand Up @@ -785,10 +783,7 @@ pub(crate) unsafe fn record_prototype_template(
|| meta.is_null()
|| (*meta).prototype != parent_proto
|| (*meta).private_evaluation_brand != owner
|| (*meta).flags
& !(crate::object::OBJECT_META_FLAG_PROTO_DIVERGED
| crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO)
!= 0
|| (*meta).flags != 0
|| (*meta).spill != 0
|| u32::try_from(count).is_err()
|| cell.proto_fills_base() + 2 * count > cell.len()
Expand Down Expand Up @@ -866,9 +861,9 @@ pub(crate) unsafe fn evaluation_chain_lost_method(
obj: *const ObjectHeader,
key: *const crate::string::StringHeader,
) -> bool {
let meta = (*obj).meta;
if meta.is_null() || (*meta).flags & crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO == 0
{
// Linked through a class evaluation: its shape names a prototype other
// than the one its template class implies.
if !crate::object::prototype_chain::object_has_individual_class_prototype(obj as usize) {
return false;
}
let class_id = (*obj).class_id;
Expand All @@ -888,7 +883,7 @@ pub(crate) unsafe fn evaluation_chain_lost_method(
if intact(obj) {
return false;
}
let proto = JSValue::from_bits((*meta).prototype);
let proto = JSValue::from_bits(crate::object::shapes::object_prototype_word(obj));
if proto.is_pointer() {
let proto = proto.as_pointer::<ObjectHeader>();
if crate::value::addr_class::try_read_gc_header(proto as usize)
Expand Down
1 change: 0 additions & 1 deletion crates/perry-runtime/src/object/field_set_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ pub extern "C" fn js_object_set_field_by_name(
// a cached edge here could hand it a foreign slot
// index below the floor.
&& crate::object::reserved_slot_floor_for_class_id(class_id) == 0
&& !super::prototype_chain::object_has_prototype_divergence(raw)
&& super::prop_plan::store_plan_check(
class_id,
key as usize,
Expand Down
9 changes: 5 additions & 4 deletions crates/perry-runtime/src/object/field_set_by_name/tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,10 @@ pub(crate) fn set_field_by_name_object_tail(
|| crate::object::own_descriptors_skip_key(
obj as usize,
f64::from_bits(JSValue::string_ptr(key as *mut _).bits()),
))
&& !super::prototype_chain::object_has_prototype_divergence(obj as usize);
));
// No per-receiver prototype gate: the plan key carries the receiver's
// prototype (`receiver_proto_bits`, a fact of its shape), so a
// receiver on another chain can only meet another plan.
let plan_fast = plan_eligible
&& super::prop_plan::store_plan_check(
obj_class_id,
Expand Down Expand Up @@ -560,8 +562,7 @@ pub(crate) fn set_field_by_name_object_tail(
&& obj_flags & PLAN_BLOCKING_FLAGS == 0
// `desc_gate_ok` above already proved this key is uncovered on
// this receiver, which is the per-key half of the old flag.
&& desc_gate_ok
&& !super::prototype_chain::object_has_prototype_divergence(obj as usize);
&& desc_gate_ok;
if !plan_fast && record_plan_eligible {
super::prop_plan::store_plan_record(
obj_class_id,
Expand Down
Loading
Loading