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
10 changes: 10 additions & 0 deletions changelog.d/8074-authoritative-shape-descriptor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
**`ShapeId` now resolves to exact agent-local object layout facts.**

Each published shape descriptor records the ordered keys array, logical key
count, and live inline-slot count for the current agent. Object allocation and
mutation publish a complete descriptor before exposing its id, and moving GC
keeps descriptors synchronized with live object keys while reclaiming dead
shape metadata. Shape ids are never reused; exhausted callers continue safely
through the existing unstamped-object path. `ObjectHeader.keys_array` and
`.field_count` remain the source of truth, and the runtime and FFI ABIs are
unchanged.
44 changes: 38 additions & 6 deletions crates/perry-codegen/src/lower_call/new_alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ fn new_site_is_in_loop(ctx: &FnCtx<'_>) -> bool {
ctx.func.alloc_hot
}

/// Whether the raw inline allocator can publish the class's pre-minted
/// descriptor without asking the runtime to repair its live-slot facts.
fn inline_shape_descriptor_facts_exact(
canonical_key_count: Option<u32>,
allocation_field_count: u32,
) -> bool {
canonical_key_count.is_some_and(|key_count| key_count == allocation_field_count)
}

/// Emit the instance allocation for `new <class_name>(...)` and return the raw
/// object handle (an `i64` user pointer, NOT NaN-boxed).
///
Expand Down Expand Up @@ -344,13 +353,24 @@ fn emit_instance_alloc_inner(
// So the choice is per site, not global: a `new` inside a loop takes
// the inline bump (it runs many times, and the size cost is bounded to
// loop bodies); everything else keeps the outlined call and
// contributes nothing to binary growth. `PERRY_INLINE_NEW=1` still
// forces the inline form everywhere, for A/B measurement.
// contributes nothing to binary growth. `PERRY_INLINE_NEW=1` forces
// the inline form for A/B measurement only when the exact descriptor
// facts below admit raw inline allocation; missing or mismatched facts
// still use the outlined entry point.
//
// NOTE the env test is `is_none()`: `PERRY_INLINE_NEW=""` *enables*
// the inline path, because an empty string is `Some("")`.
let force_inline_new = std::env::var_os("PERRY_INLINE_NEW").is_some();
if !force_inline_new && !new_site_is_in_loop(ctx) {
// #8067: the raw inline allocator cannot ask the runtime to validate
// descriptor facts after writing the ShapeId. Admit it only when the
// allocation's live-slot bound exactly equals the module-init keys
// count used to mint that id. Width-hinted/mismatched allocations use
// the outlined entry point, which installs an exact local descriptor.
let descriptor_facts_exact = inline_shape_descriptor_facts_exact(
ctx.class_field_counts.get(class_name).copied(),
field_count,
);
if !descriptor_facts_exact || (!force_inline_new && !new_site_is_in_loop(ctx)) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let keys_slot = if let Some(s) = ctx.class_keys_slots.get(class_name).cloned() {
s
} else {
Expand Down Expand Up @@ -569,9 +589,9 @@ fn emit_instance_alloc_inner(

// Second 8 bytes: ShapeId (u32, low) | field_count (u32, high).
// Rung 0 removed the last inheritance consumer of this word; the
// parent edge was registered during module init. Keep the old
// parent value only if the process-global ShapeId range was
// exhausted and init returned 0, preserving the lazy fallback.
// parent edge was registered during module init. A zero id is the
// recoverable exhaustion path: retain the old parent word and let
// the still-authoritative pointer/count guards handle the object.
let oh_addr_2 = blk.gep(I8, &raw, &[(I64, "16")]);
let has_shape_id = blk.icmp_ne(I32, &shape_id, "0");
let shape_word = blk.select(I1, &has_shape_id, I32, &shape_id, &parent_cid.to_string());
Expand Down Expand Up @@ -681,3 +701,15 @@ fn emit_instance_alloc_inner(
)
}
}

#[cfg(test)]
mod tests {
use super::inline_shape_descriptor_facts_exact;

#[test]
fn raw_inline_shape_stamp_requires_exact_descriptor_facts() {
assert!(inline_shape_descriptor_facts_exact(Some(5), 5));
assert!(!inline_shape_descriptor_facts_exact(Some(5), 8));
assert!(!inline_shape_descriptor_facts_exact(None, 5));
}
}
16 changes: 10 additions & 6 deletions crates/perry-runtime/src/array/indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,12 +428,16 @@ pub(crate) unsafe fn keys_array_len_capped_to_capacity(arr: *const ArrayHeader)
return (*arr).length as usize;
}
}
let raw = js_array_length(arr) as usize;
if arr.is_null() {
raw
} else {
raw.min((*arr).capacity as usize)
}
// A forwarding stub overwrites the old payload's `(length, capacity)`
// words with the target address. Resolve once, then read BOTH facts from
// the live header; mixing a resolved length with the stale from-space
// capacity can truncate an otherwise exact shape count.
let live = clean_arr_ptr(arr);
if live.is_null() {
return js_array_length(arr) as usize;
}
let raw = js_array_length(live) as usize;
raw.min((*live).capacity as usize)
}

/// Read slot `index` of a dense internal keys/property array.
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-runtime/src/gc/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,21 @@ pub(crate) fn test_gc_rewrite_slot_count(user_ptr: usize) -> Option<usize> {
Some(count)
}

#[cfg(test)]
pub(crate) fn test_gc_rewrite_slot_addresses(user_ptr: usize) -> Option<Vec<usize>> {
if user_ptr < GC_HEADER_SIZE + 0x1000 {
return None;
}
let header = unsafe { header_from_user_ptr(user_ptr as *const u8) };
let mut slots = Vec::new();
unsafe {
visit_gc_rewrite_slot_descriptors(header, |descriptor| {
descriptor.visit_slots(&mut |slot| slots.push(slot.slot as usize));
});
}
Some(slots)
}

#[inline(always)]
pub(super) fn record_trace_slot_read() {
#[cfg(test)]
Expand Down
60 changes: 60 additions & 0 deletions crates/perry-runtime/src/gc/layout_slot_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,69 @@ pub(super) unsafe fn visit_gc_layout_slot_descriptors(
visit: &mut dyn FnMut(GcMutableSlotDescriptor),
) {
let mut child_slots = gc_child_slots(header);
// Capture the authoritative pre-visit facts. A copying visit can rewrite
// `keys_array`, and a sibling may already have rewritten the shared
// descriptor, so the descriptor helper accepts exactly the old OR new
// pointer — never an unrelated pointer that merely shares an id.
let object_shape_facts = if (*header).obj_type == GC_TYPE_OBJECT {
let obj = (header as *mut u8).add(GC_HEADER_SIZE) as *mut crate::object::ObjectHeader;
if crate::regex::regex_header_has_magic(obj as *const crate::regex::RegExpHeader) {
None
} else {
let old_keys = (*obj).keys_array;
let live_inline_slot_count = (*obj).field_count;
if old_keys.is_null() {
Some((obj, 0, 0, live_inline_slot_count))
} else if crate::value::addr_class::try_read_tracked_gc_header(old_keys as usize)
.is_some_and(|keys_header| (*keys_header.as_ptr()).obj_type == GC_TYPE_ARRAY)
{
// A forwarded tracked array still carries GC_TYPE_ARRAY in
// its from-space header. The length helper follows that stub,
// so a sibling whose shared keys edge was already rewritten
// can still validate against the descriptor's new pointer.
Some((
obj,
old_keys as u64,
crate::array::keys_array_len_capped_to_capacity(old_keys) as u32,
live_inline_slot_count,
))
} else {
// Do not dereference corrupt/unmapped header words merely
// because their sibling word happens to look like a ShapeId.
// The authoritative header edge below is still enumerated;
// only redundant descriptor synchronization is skipped.
None
}
}
} else {
None
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if let Some(slot) = child_slots.take_prefix_child_slot() {
visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix));
}
// #8067: the header keys slot above is the sole strong edge. Once its
// visitor callback has run, mirror an immediate rewrite into the weak
// descriptor. Never enumerate the HashMap bucket as a GC slot: dirty-page
// work may retain enumerated slot addresses across budgeted resumptions,
// during which descriptor insertion can reallocate the table. A deferred
// visitor leaves old==new here; the metadata forwarding pass repairs it
// after copying. RegExp aliases GC_TYPE_OBJECT with a different native
// header and was excluded while capturing the facts above.
if let Some((obj, old_keys, logical_key_count, live_inline_slot_count)) = object_shape_facts {
let new_keys = (*obj).keys_array as u64;
// Mark, verify, and deferred dirty scans leave the header edge
// unchanged. Only a copying rewrite needs to borrow and update the
// weak descriptor table.
if new_keys != old_keys {
crate::object::shapes::synchronize_live_object_shape_descriptor_after_header_visit(
obj,
old_keys,
new_keys,
logical_key_count,
live_inline_slot_count,
);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if let Some(slot) = child_slots.take_meta_child_slot() {
visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix));
}
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,9 +830,9 @@ pub fn gc_init() {
// reflect-metadata store were invisible to GC — values swept/moved under
// live references, owner keys stale after evacuation.
reg_scanner!(crate::object::descriptor_state::scan_descriptor_roots_mut);
// #6759 Phase C3a: shape records follow their keys array across
// evacuation (metadata-rewrite rekey only; the records hold no heap
// references and mark nothing).
// #8067: the descriptor table is weak. Live-object layout scans trace its
// 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);
reg_scanner!(crate::proxy::scan_proxy_roots_mut);
// Object/string-valued `err.<prop> = v` user props live as raw bits in
Expand Down
Loading
Loading