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
13 changes: 13 additions & 0 deletions changelog.d/11815-prototype-in-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
An ordinary object's prototype is now a fact of its shape (Refs #10507). A
ShapeId already names its receivers' prototype identity, and that identity now
leads back to the prototype through one word per prototype. So `new F()`,
`Object.getPrototypeOf`, inherited reads, `instanceof` and the method and
accessor sites read a function-constructor instance's prototype from its
ShapeId, and the instance carries no per-instance metadata record. `new F()`
drops from ~1,900 to ~1,165 instructions and 152 bytes per instance. The word
is traced through the receivers that carry the identity, like a shape's key
list, so a prototype stays alive exactly as long as something reaches it.
Also fixed: `Object.create(null) instanceof Object` was true; a class instance
re-parented with `Object.setPrototypeOf` was still `instanceof` its class;
`Object.create(fn) instanceof Function` was false; an `Object.create(null)`
object re-parented with `Object.setPrototypeOf` kept a null-prototype shape.
12 changes: 12 additions & 0 deletions crates/perry-runtime/src/gc/barrier_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,18 @@ pub(crate) fn runtime_store_jsvalue_slot(
runtime_write_barrier_slot(parent_user, slot_addr, value_bits);
}

/// Shade `value_bits` for an in-progress incremental mark: an edge that now
/// lives outside the heap on behalf of an owner the mark may already have
/// traced (a shape record's [[Prototype]] word, `object::shapes_prototype`).
/// The minor-collection half of such an edge is the shape table's
/// old-carrier gate, so no remembered-set entry is recorded.
pub(crate) fn runtime_shade_external_edge(value_bits: u64) {
if barrier_scalar_child_skips(value_bits) {
return;
}
let _ = incremental_mark_barrier_value(value_bits);
}

pub(crate) fn runtime_write_barrier_external_slot(
parent_addr: usize,
slot_addr: usize,
Expand Down
17 changes: 10 additions & 7 deletions crates/perry-runtime/src/gc/copying_object_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ use super::copying_parent_facts::{weak_holder_fact, ParentRemembering};
use super::*;

/// The slots, in visit order, the generic walk's layout arm hands the drain for
/// this object: the keys edge and the meta record (null when absent), then the
/// payload slots its selection names. Iterated, not stored.
/// this object: the keys edge, the shape's prototype edge and the meta record
/// (null when absent), then the payload slots its selection names. Iterated,
/// not stored.
#[derive(Clone)]
struct PlainObjectPlan {
prefix: [*mut u64; 2],
prefix: [*mut u64; 3],
next_prefix: usize,
payload: HeapSlotRange,
walk: PayloadWalk,
Expand All @@ -59,7 +60,7 @@ enum PayloadWalk {
impl PlainObjectPlan {
#[inline(always)]
unsafe fn next_slot(&mut self) -> Option<*mut u64> {
while self.next_prefix < 2 {
while self.next_prefix < 3 {
let slot = self.prefix[self.next_prefix];
self.next_prefix += 1;
if !slot.is_null() {
Expand Down Expand Up @@ -131,8 +132,8 @@ unsafe fn plain_object_plan(header: *mut GcHeader) -> PlainObjectPlan {
// `gc_child_slots` returns the EMPTY iterator: no shape, so no keys
// edge and no carrier note, no meta edge, no payload.
return PlainObjectPlan {
prefix: [std::ptr::null_mut(); 2],
next_prefix: 2,
prefix: [std::ptr::null_mut(); 3],
next_prefix: 3,
payload: HeapSlotRange::new(std::ptr::null_mut(), 0),
walk: PayloadWalk::Word(0),
};
Expand Down Expand Up @@ -182,11 +183,13 @@ unsafe fn plain_object_plan(header: *mut GcHeader) -> PlainObjectPlan {
crate::object::shapes::note_old_generation_carrier(shape);
}
let keys_edge = crate::object::gc_shape_keys_edge_slot(shape);
let prototype_edge = crate::object::gc_shape_prototype_edge_slot(shape, false);
// Visit order of the generic walk: prefix (none for objects), keys edge,
// meta, meta2 (none), payload.
// prototype edge, meta, meta2 (none), payload.
PlainObjectPlan {
prefix: [
keys_edge.unwrap_or(std::ptr::null_mut()),
prototype_edge.unwrap_or(std::ptr::null_mut()),
meta.unwrap_or(std::ptr::null_mut()),
],
next_prefix: 0,
Expand Down
10 changes: 10 additions & 0 deletions crates/perry-runtime/src/gc/dead_owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,16 @@ pub(super) const DEAD_KEY_PRUNES: &[DeadKeyPrune] = &[
prune: crate::object::shapes::prune_dead_shape_keys,
young_prune: Some(crate::object::shapes::prune_dead_shape_keys_young),
},
// A prototype identity's word is traced through its carriers; a word whose
// prototype died has none left, so it is cleared.
DeadKeyPrune {
table: "state().shapes prototype words + identity index",
owner: DeadKeyOwner::Any,
prune: crate::object::shapes::prune_dead_shape_prototypes,
// A minor roots every young word (`scan_shape_prototype_words_mut`),
// so only a full trace can find a word's prototype dead.
young_prune: None,
},
// #10868 step 2.5 stage 1b: the canonical keys trie holds its arrays
// WEAKLY, so a node whose array did not survive has to be reaped here or
// the next probe dereferences freed memory. Runs after the shape prune
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/gc/full_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub(crate) fn begin_full_trace() {
FULL_TRACE_ACTIVE.with(|active| {
assert!(!active.replace(true), "full trace already active");
});
// The prototype identity words emit their carrier edge once per trace.
crate::object::shapes::note_full_trace_begin();
crate::proxy::gc_begin_full_trace();
if let Some(hook) = FETCH_TRACE.with(Cell::get) {
FETCH_TRACE_ARMED.with(|armed| armed.set(true));
Expand Down
10 changes: 10 additions & 0 deletions crates/perry-runtime/src/gc/layout_slot_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ pub(super) unsafe fn visit_gc_layout_slot_descriptors_inline<F>(
// `PERRY_GC_VERIFY_EVACUATION` is what established the second half is
// needed: without it the verifier aborts on a `slot_page_ever_dirty=false`
// old→young edge through this word.
let shape_prototype_edge = if (*header).obj_type == GC_TYPE_OBJECT {
crate::object::gc_shape_prototype_edge_slot(child_slots.object_shape, full_trace_active())
} else {
None
};
let shape_keys_edge = if (*header).obj_type == GC_TYPE_OBJECT {
// #9726: unlike the minor-rooting gate below, full-trace descriptor
// liveness is generation-blind. Every reachable shaped receiver must
Expand Down Expand Up @@ -144,6 +149,11 @@ pub(super) unsafe fn visit_gc_layout_slot_descriptors_inline<F>(
if let Some(slot) = shape_keys_edge {
visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix));
}
// The receiver's [[Prototype]] when its shape names it: the identity's
// shared word, marked through like the keys word.
if let Some(slot) = shape_prototype_edge {
visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix));
}
if let Some(slot) = child_slots.take_meta_child_slot() {
visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix));
}
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,9 @@ pub fn gc_init() {
// ordered-keys slot; this scanner only follows existing forwarding records
// for descriptors and the pointer-keyed slot accelerator after evacuation.
reg_scanner!(crate::object::shapes::scan_shape_table_rekey_mut);
// The shape records' [[Prototype]] words and their identity index are
// strong roots (object::shapes_prototype).
reg_scanner!(crate::object::shapes::scan_shape_prototype_words_mut);
reg_scanner!(crate::proxy::scan_proxy_roots_mut);
// Object/string-valued `err.<prop> = v` user props live as raw bits in
reg_scanner!(exception_mutable_root_scanner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,64 @@ fn test_object_meta_prototype_survives_copied_minor_move() {
js_shadow_slot_set(1, 0);
}

/// The shape names the prototype (`object::shapes_prototype`): a receiver
/// linked by a class-default link (`new F()`) has no meta record, and its
/// prototype is reachable ONLY through its shape record's `prototype` word.
/// A copied minor must keep that prototype alive through the carrier's edge
/// and rewrite the shared word to the moved prototype.
#[test]
fn test_shape_prototype_word_survives_copied_minor_move_through_its_carrier() {
let _guard = CopyingNurseryTestGuard::new(2);

let (owner, _) = unsafe { alloc_nursery_test_object(0) };
// A function constructor's instance (synthetic class id): the one
// receiver a class-default link leaves without a meta record.
unsafe {
(*owner).class_id = crate::object::shapes::SYNTHETIC_CLASS_ID_BASE + 0x52;
}
let (proto, proto_fields) = unsafe { alloc_nursery_test_object(1) };
unsafe { *proto_fields = 42.0f64.to_bits() };
let old_owner = owner as usize;
let old_proto = proto as usize;
crate::object::prototype_chain::object_link_class_default_prototype(
old_owner,
ptr_bits(old_proto),
);
assert!(
unsafe { (*owner).meta }.is_null(),
"test premise: a class-default link allocates no meta record"
);
let stamp = unsafe { crate::object::shapes::object_shape_stamp(owner) };
assert_eq!(
crate::object::shapes::shape_prototype_word(stamp),
ptr_bits(old_proto),
"test premise: the shape record holds the prototype"
);
// Only the owner is rooted: the prototype lives through the shape edge.
js_shadow_slot_set(0, ptr_bits(old_owner));

let _ = gc_collect_minor();

let new_owner = (js_shadow_slot_get(0) & POINTER_MASK) as usize;
assert_ne!(new_owner, old_owner, "test premise: the owner must move");
let recorded = crate::object::prototype_chain::object_static_prototype(new_owner)
.expect("the moved owner must still resolve its prototype through its shape");
let new_proto = (recorded & POINTER_MASK) as usize;
assert_ne!(
new_proto, old_proto,
"the prototype must be evacuated and the shape's word rewritten"
);
assert_eq!(
unsafe {
*((new_proto + std::mem::size_of::<crate::object::ObjectHeader>()) as *const u64)
},
42.0f64.to_bits(),
"the rewritten word names the live, moved prototype"
);

js_shadow_slot_set(0, 0);
}

/// A class-evaluation object may be reachable only through an instance's
/// hidden ObjectMeta brand. The edge must retain and rewrite that class object
/// when a copied minor moves the owner, its metadata, and the brand together.
Expand Down Expand Up @@ -661,6 +719,10 @@ fn test_deferred_shape_slot_enumeration_survives_descriptor_table_reallocation()
#[test]
fn test_object_meta_null_prototype_survives_full_gc_on_live_owner() {
let _guard = GcTestIsolationGuard::new();
// The owner is rooted through a shadow frame of its own: without one the
// slot store roots nothing, the owner dies, and the read below would
// examine freed memory and pass for the wrong reason.
let frame = js_shadow_frame_push(1);

let (owner, _) = unsafe { alloc_nursery_test_object(0) };
let addr = owner as usize;
Expand All @@ -669,11 +731,18 @@ fn test_object_meta_null_prototype_survives_full_gc_on_live_owner() {

full_gc();

let live = (js_shadow_slot_get(0) & POINTER_MASK) as usize;
assert_eq!(live, addr, "test premise: a full mark-sweep does not move");
assert_eq!(
crate::object::prototype_chain::object_static_prototype(addr),
crate::object::prototype_chain::object_static_prototype(live),
Some(crate::value::TAG_NULL),
"a live (rooted) owner's meta record — and its explicit-null \
prototype — must survive a full collection"
"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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
//! class id and birth ShapeId are the prototype object's birth record
//! (`ObjectMeta::instance_birth`), minted on the first construction by the
//! ordinary allocate-then-link sequence and replayed afterwards — the same
//! class id, the same ShapeId (its `proto_id` is the prototype's serial) and
//! the same `meta.prototype` that sequence produces, with no hash lookup. A
//! reassigned `F.prototype` is read on the next construction; objects already
//! created keep the prototype their meta records.
//! class id and the same ShapeId (its `proto_id` is the prototype's serial,
//! its record's `prototype` word the prototype itself) that sequence
//! produces, with no hash lookup and no per-instance record. A reassigned
//! `F.prototype` is read on the next construction; objects already created
//! keep the prototype their shapes name.
use super::*;

use crate::closure::ClosureHeader;
Expand Down Expand Up @@ -101,15 +102,21 @@ unsafe fn birth_record(proto: *const ObjectHeader) -> Option<(u32, u32, u32)> {
// size the class has learned; a class that has since learned a larger
// size gets a new record.
let slots = crate::object::learned_inline_field_count(class_id);
(crate::object::shapes::shape_live_inline_slot_count_by_id(shape_id) == Some(slots))
// The id is not pinned: the descriptor table may have retired it and
// handed the id to another shape. Replay it only while it still names
// the birth facts — keyless, generation 0, no holes, this prototype's
// identity (so its record's word IS `proto`) and the learned slots.
crate::object::shapes::shape_is_keyless_birth(shape_id, (*meta).proto_serial, slots)
.then_some((class_id, shape_id, slots))
}

/// Mint `proto`'s birth record from the first construction, which takes the
/// ordinary sequence: an object of `F`'s synthetic class, linked to `proto`
/// as its class-default prototype. The record is that class id and the
/// ShapeId the link left, pinned for the agent's life so the record can never
/// name a retired id. Returns the constructed object.
/// ShapeId the link left; [`birth_record`] re-validates the id's facts on
/// every replay, so a retired id is never replayed and a dead function's
/// prototype is not kept alive by a pinned shape. Returns the constructed
/// object.
///
/// The class is the minting function's; a class whose registered prototype is
/// later moved off `proto` clears the record
Expand All @@ -134,9 +141,6 @@ unsafe fn mint_birth_record(func_value: f64, proto: *mut ObjectHeader) -> *mut O
});
let shape_id =
obj.with_mut_ptr::<ObjectHeader, _>(|obj| crate::object::shapes::object_shape_stamp(obj));
crate::object::shapes::note_external_shape_carrier(
crate::object::shapes::shape_descriptor_by_id(shape_id),
);
let meta = proto_handle
.with_mut_ptr::<ObjectHeader, _>(|proto| crate::object::object_meta_ensure(proto));
// GC_STORE_AUDIT(POINTER_FREE): a class id and a ShapeId, never a heap
Expand All @@ -162,34 +166,16 @@ pub(crate) unsafe fn forget_birth_record_of_class(old: *mut ObjectHeader, class_
}
}

/// An object born from `proto`'s record: class id and birth ShapeId stamped,
/// `meta.prototype` = `proto` (what the class-default link records).
/// An object born from `proto`'s record: class id and birth ShapeId stamped.
/// The ShapeId names `proto` (its record's `prototype` word), which is all
/// the class-default link records; no per-instance record is allocated.
///
/// # Safety
/// `proto` is a live `ObjectHeader` marked as a prototype.
unsafe fn born_from_record(
proto: *mut ObjectHeader,
class_id: u32,
shape_id: u32,
slots: u32,
) -> *mut ObjectHeader {
let scope = crate::gc::RuntimeHandleScope::new();
let proto_handle = scope.root_raw_mut_ptr(proto);
let obj = scope.root_raw_mut_ptr(crate::object::object_alloc_born(class_id, slots, shape_id));
let meta = obj.with_mut_ptr::<ObjectHeader, _>(|obj| crate::object::object_meta_ensure(obj));
let proto_bits = proto_handle
.with_mut_ptr::<ObjectHeader, _>(|proto| crate::value::js_nanbox_pointer(proto as i64))
.to_bits();
(*meta).prototype = proto_bits;
// GC_STORE_AUDIT(BARRIERED): meta-record prototype slot store — the
// record is an arena allocation, so the ordinary object-slot barrier
// applies (parent = the meta record), as in the class-default link.
crate::gc::runtime_write_barrier_slot(
meta as usize,
&(*meta).prototype as *const u64 as usize,
proto_bits,
);
obj.get_raw_mut_ptr::<ObjectHeader>()
/// `proto` is a live `ObjectHeader` marked as a prototype, and `shape_id`
/// passed [`birth_record`] for it.
#[inline]
unsafe fn born_from_record(class_id: u32, shape_id: u32, slots: u32) -> *mut ObjectHeader {
crate::object::object_alloc_born(class_id, slots, shape_id)
}

/// `new F(...args)` for an ordinary compiled function `F` (`closure`), or
Expand Down Expand Up @@ -220,7 +206,7 @@ pub(super) unsafe fn construct_ordinary_compiled_function(
None => return None,
};
let obj = match birth_record(proto) {
Some((class_id, shape_id, slots)) => born_from_record(proto, class_id, shape_id, slots),
Some((class_id, shape_id, slots)) => born_from_record(class_id, shape_id, slots),
None => mint_birth_record(func_handle.get_nanbox_f64(), proto),
};
let instance = crate::value::js_nanbox_pointer(obj as i64);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ fn construction_is_born_from_the_prototype_birth_record() {
synthetic_class_id_for_function(func),
"a construction carries its function's class"
);
let second_meta = (*second).meta;
// The birth shape names the prototype; a replayed construction
// carries no per-instance record.
assert!(
!second_meta.is_null(),
"the class-default link's meta record"
(*second).meta.is_null(),
"a replayed construction allocates no meta record"
);
assert_eq!(
(*second_meta).prototype,
crate::object::shapes::object_prototype_word(second),
crate::value::js_nanbox_pointer(proto as i64).to_bits()
);
assert_eq!(
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-runtime/src/object/gc_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ pub(crate) fn gc_shape_keys_edge_slot(record: Option<shapes::ShapeRecordRef>) ->
Some(record.keys_slot())
}

/// The AUTHORITATIVE [[Prototype]] edge of a traced receiver whose shape names
/// its prototype object (`shapes_prototype`): the shape record's own
/// `prototype` word, shared by every sibling exactly like the keys edge above
/// (a young carrier emits it; a minor also roots every word naming a young
/// object, which covers the old carriers it never traces). In a full trace
/// only the first carrier of each identity emits it (`identity_edge_slot`).
#[inline]
pub(crate) fn gc_shape_prototype_edge_slot(
record: Option<shapes::ShapeRecordRef>,
full_trace: bool,
) -> Option<*mut u64> {
record?.prototype_slot(full_trace)
}

/// The object's inline field-slot range, given the receiver's shape record
/// resolved once by the collector.
pub(crate) unsafe fn gc_field_slot_range(
Expand Down
Loading
Loading