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
47 changes: 47 additions & 0 deletions changelog.d/8890-header-unification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Gave every exotic cell type a metadata edge, and moved `Error`'s own properties
onto it — deleting a side table and all four of its GC hooks.

Cell types declare their fields independently; there is no shared header prefix.
So *"does this cell own an `ObjectMeta`?"* had no single answer, and only an
`ObjectHeader` could be asked. That is why per-object state for the exotic types
accumulated in tables keyed by the owner's **address** — there was nowhere on
the cell to put it. Errors alone carried seven such tables plus four GC hooks.

Every exotic cell now has a `meta` edge, reachable through one accessor
(`cell_meta_slot`): Object, Error, Map, Set, RegExp, Promise and Date. It
answers `None` for anything unmapped, so callers degrade to their existing
storage rather than mis-reading another layout as a pointer.

Each edge is **traced**, not merely rewritten. Where a type's rewrite arm is
also its mark path the slot goes there; RegExp delegates to the layout visitor,
so its edge goes in `gc_child_slots` instead. #6812 is exactly the bug of
choosing wrong — an edge visited only on the rewrite path is invisible to
marking, and the record is swept out from under a live owner.

`Date` needed more than a field: it was `pointer_free` with a `Leaf` (no-op)
descriptor, holding one raw `f64`. A cell with a pointer must be scanned, so it
moved to a new `MetaOnly` descriptor with `pointer_free = false`.
`validate_gc_type_info` caught the flag when an edit missed it.

The arena reuses free-list memory **without zeroing**, so an uninitialised meta
edge would be a garbage pointer the collector follows. Every allocation path
initialises it explicitly; `Promise` routes through `Promise::new`, so the
constructor covers its several sites.

`ObjectMeta` gains `expando`, a named-property bag for cells with no inline slot
layout. It is appended last because the struct's offsets are a contract with
codegen (`offset_of!` asserts at 32/48/56 — inserting mid-struct failed them).
`ERROR_USER_PROPS` is deleted along with all four of its GC hooks:
rekey-on-evacuation, finalize, dead-sweep and the root scanner. Error properties
are now an ordinary traced child edge that moves with its owner, dies with its
owner, and cannot be inherited by a later tenant of a recycled address.

The tracing tests assert slot **enumeration** directly rather than survival
across a collection. A survival test is vacuous here: arena block reset is
all-or-nothing, so `gc::trace` force-marks every object in a block that still
holds one reachable object (#7975), which keeps an untraced record alive anyway.
Verified by sabotage — deleting the visit line left the survival version passing
and fails the enumeration version.

No user-visible change on its own. This is the gate that lets the shape and
descriptor payloads move off address-keyed tables.
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const NANBOX_PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
#[repr(C)]
pub struct DateCell {
pub ts: f64,
/// #6759 phase 1 (header unification): per-object metadata record, or null.
///
/// Adding this made `DateCell` non-pointer-free, so its GC type entry moved
/// from `GcRewriteDescriptorKind::Leaf` (a no-op arm) to `MetaOnly` and its
/// `pointer_free` flag to `false` — a cell with a pointer must be scanned.
pub meta: *mut crate::object::ObjectMeta,
}

/// Allocate a fresh Date cell holding `ts` and return it as a NaN-boxed
Expand All @@ -41,6 +47,11 @@ pub fn alloc_date_cell(ts: f64) -> f64 {
crate::gc::GC_TYPE_DATE_CELL,
) as *mut DateCell;
(*ptr).ts = ts;
// MUST be explicit: the arena reuses free-list memory without zeroing,
// and since #6759 phase 1 this cell is no longer pointer-free — the
// collector now scans this slot, so leftover bytes would be followed
// as a pointer.
(*ptr).meta = std::ptr::null_mut();
// A previous (collected) Date at this address may have left expando
// properties in the side table; a fresh Date must start clean.
crate::object::exotic_expando::expando_clear_on_alloc(ptr as usize);
Expand Down
65 changes: 65 additions & 0 deletions crates/perry-runtime/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ pub struct ErrorHeader {
pub cause: f64,
/// Errors array for AggregateError (raw ArrayHeader pointer or null)
pub errors: *mut crate::array::ArrayHeader,
/// Per-object metadata record, or null. The same `ObjectMeta` cell an
/// `ObjectHeader` hangs off its own `meta` field.
///
/// #6759 phase 1 (header unification). An `ErrorHeader` is not an
/// `ObjectHeader`, so before this field the only place to put anything
/// per-error was a side table keyed by the error's ADDRESS — and errors
/// accumulated seven of them, each needing its own GC rekey-on-evacuation,
/// finalize, dead-sweep and root-scanner hook. Giving the cell a metadata
/// edge is what lets those payloads move onto the object itself.
///
/// Appended LAST on purpose: every preceding field keeps its offset, so
/// codegen and the `errors`-at-+48 assumption in this file's tests are
/// undisturbed.
///
/// Traced and rewritten by the `GcRewriteDescriptorKind::Error` arm in
/// `gc/layout_slot_visit.rs`. That arm is reached by
/// `trace_heap_rewrite_slots`, so it is the MARK path as well as the
/// evacuation-rewrite path — unlike `GcLayoutSlotKind::ObjectFields`,
/// which delegates to the layout visitor and needed its meta edge added
/// there separately (#6812: a meta edge enumerated only on the rewrite
/// path is invisible to marking).
pub meta: *mut crate::object::ObjectMeta,
}

thread_local! {
Expand Down Expand Up @@ -247,6 +269,9 @@ unsafe fn alloc_error(
(*ptr).stack = stack_handle.get_raw_const_ptr::<StringHeader>() as *mut StringHeader;
(*ptr).cause = f64::from_bits(TAG_UNDEFINED);
(*ptr).errors = std::ptr::null_mut();
// No metadata record until something needs one; the GC treats a null meta
// edge as absent.
(*ptr).meta = std::ptr::null_mut();

ptr
}
Expand Down Expand Up @@ -1930,3 +1955,43 @@ mod tostring_tests {
);
}
}

#[cfg(test)]
mod header_unification_tests {
use super::*;

/// #6759 phase 1: an `ErrorHeader` owns a metadata edge, and it is
/// reachable through the SAME accessor an `ObjectHeader` is.
///
/// This is the gate the rest of the migration stands on: while "does this
/// cell own an ObjectMeta?" had no uniform answer, per-error state had
/// nowhere to live but a side table keyed by the error's address.
#[test]
fn error_cell_exposes_a_meta_edge_like_an_object() {
let _lock = crate::gc::global_side_table_test_lock();
unsafe {
let msg = crate::string::js_string_from_bytes(b"boom".as_ptr(), 4);
let err = js_error_new_with_message(msg);
assert!(
(*err).meta.is_null(),
"a fresh error must start with no metadata record"
);
assert!(
crate::object::cell_has_meta_edge(err as usize),
"an error cell must be reachable through the uniform meta accessor"
);
let obj = crate::object::js_object_alloc(0, 0);
assert!(
crate::object::cell_has_meta_edge(obj as usize),
"an object cell must answer the same accessor"
);
// A cell type that has NOT been unified yet must answer `None`
// rather than mis-reading its own layout as a meta pointer.
let arr = crate::array::js_array_alloc(0);
assert!(
!crate::object::cell_has_meta_edge(arr as usize),
"a cell without a meta edge must report absence, not garbage"
);
}
}
}
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/gc/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1731,6 +1731,10 @@ pub(super) unsafe fn gc_child_slots(header: *mut GcHeader) -> HeapChildSlotItera
Some(last_index_slot),
HeapSlotRange::new(pattern_slot, slot_count),
)
// #6759 phase 1: the metadata edge. RegExp reaches marking through
// THIS iterator (its rewrite arm delegates here), so the edge has
// to be enumerated at this point, not in the rewrite match.
.with_meta_slot(crate::object::cell_meta_slot(user_ptr as usize).map(|s| s as *mut u64))
}
GcLayoutSlotKind::ObjectMeta => {
// Prototype and the private-evaluation brand are explicit prefix
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-runtime/src/gc/layout_slot_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors(
&mut (*promise).on_rejected as *mut _ as *mut u64,
));
visit(fixed_slot(&mut (*promise).next as *mut _ as *mut u64));
// #6759 phase 1: the metadata edge (MARK path as well as rewrite).
visit(fixed_slot(&mut (*promise).meta as *mut _ as *mut u64));
}
GcRewriteDescriptorKind::Error => {
let error = user_ptr as *mut crate::error::ErrorHeader;
Expand All @@ -201,6 +203,11 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors(
visit(fixed_slot(&mut (*error).stack as *mut _ as *mut u64));
visit(fixed_slot(&mut (*error).cause as *mut f64 as *mut u64));
visit(fixed_slot(&mut (*error).errors as *mut _ as *mut u64));
// #6759 phase 1: the metadata edge. This arm is reached by
// `trace_heap_rewrite_slots`, so visiting the slot here both MARKS
// the meta record (keeping it, and anything reachable only through
// it, alive) and rewrites the edge when evacuation moves it.
visit(fixed_slot(&mut (*error).meta as *mut _ as *mut u64));
}
GcRewriteDescriptorKind::Map => {
let map = user_ptr as *mut crate::map::MapHeader;
Expand Down Expand Up @@ -241,6 +248,11 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors(
range: HeapSlotRange::new((*map).entries as *mut u64, size as usize * 2),
layout_kind: None,
});
// #6759 phase 1: the metadata edge. This arm is the MARK path as
// well as the rewrite path (`trace_heap_rewrite_slots` drives it),
// so visiting here keeps the record — and anything reachable only
// through it — alive.
visit(fixed_slot(&mut (*map).meta as *mut _ as *mut u64));
}
GcRewriteDescriptorKind::Set => {
let set = user_ptr as *mut crate::set::SetHeader;
Expand All @@ -250,6 +262,8 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors(
layout_kind: None,
});
}
// #6759 phase 1: the metadata edge (MARK path as well as rewrite).
visit(fixed_slot(&mut (*set).meta as *mut _ as *mut u64));
}
GcRewriteDescriptorKind::LazyArray => {
let lazy = user_ptr as *mut crate::json_tape::LazyArrayHeader;
Expand Down Expand Up @@ -307,12 +321,25 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors(
// #6812: the object-owned overflow buffer is a raw-pointer child
// edge (0 = none), traced and rewritten exactly like `prototype`.
visit(fixed_slot(&mut (*meta).spill as *mut u64));
// #6759 phase 1: the named-property bag for a cell with no inline
// slot layout (an Error, say). Reachable ONLY through this record,
// so an unvisited edge here collects a live object's own
// properties — the same shape as the spill hazard above (#6812).
visit(fixed_slot(&mut (*meta).expando as *mut u64));
// A fresh class object stored as an instance's private evaluation
// brand is a NaN-boxed child edge and moves with the meta record.
visit(fixed_slot(
&mut (*meta).private_evaluation_brand as *mut u64,
));
}
GcRewriteDescriptorKind::MetaOnly => {
// #6759 phase 1: the cell's only traced edge is its metadata
// record. Reached by `trace_heap_rewrite_slots`, so this is the
// MARK path as well as the rewrite path.
if let Some(slot) = crate::object::cell_meta_slot(user_ptr as usize) {
visit(fixed_slot(slot as *mut u64));
}
}
GcRewriteDescriptorKind::Leaf => {}
}
}
Expand Down
4 changes: 0 additions & 4 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,10 +875,6 @@ pub fn gc_init() {
reg_scanner!(crate::object::shapes::scan_shape_table_rekey_mut);
reg_scanner!(crate::proxy::scan_proxy_roots_mut);
// Object/string-valued `err.<prop> = v` user props live as raw bits in
// ERROR_USER_PROPS — invisible to GC without this scanner (collectable
// while reachable; stale addresses after a move). The address KEYS are
// maintained by the ErrorSideTables move/finalize hooks.
reg_scanner!(crate::node_submodules::diagnostics_gc::scan_error_user_props_roots_mut,);
reg_scanner!(exception_mutable_root_scanner);
reg_scanner!(async_context_mutable_root_scanner);
reg_scanner!(async_hooks_mutable_root_scanner);
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/gc/tests/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ fn alloc_malloc_kind_test_object(obj_type: u8) -> *mut u8 {
next: std::ptr::null_mut(),
async_id: 0,
trigger_async_id: 0,
meta: std::ptr::null_mut(),
},
);
}
Expand All @@ -670,6 +671,7 @@ fn alloc_malloc_kind_test_object(obj_type: u8) -> *mut u8 {
stack: std::ptr::null_mut(),
cause: 0.0,
errors: std::ptr::null_mut(),
meta: std::ptr::null_mut(),
},
);
}
Expand Down
Loading
Loading