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
19 changes: 19 additions & 0 deletions changelog.d/11411-attrs-with-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Property attributes live with the keys (charter step 3, V8's descriptor
arrays). A key list that carries any non-default attribute (writable /
enumerable / configurable, accessor-ness) owns a parallel attributes array,
attached through the named-properties reserve slot the collector already
traces; attribute-free key lists are unchanged. The canonical key trie's edge
is `(key, attributes)`, so equal keys with equal attributes share one list and
one ShapeId whatever the path, and a key that arrives with its attributes
(an `exports` getter per re-export, a builtin prototype method, an arguments
object's `length`/`callee`) is appended in place. Each shape record carries an
attribute summary byte that the prototype-chain store check reads first.

An ordinary object's attributes no longer live in the address-keyed
`property_descriptors` table, its owner index or its meta-record Bloom bits,
and the attribute generation hash is gone. The read inline cache declines
only an accessor KEY and the write caches only a key that is not plain
writable data, so one `Object.defineProperty` no longer takes an object's
other keys off the property caches (#10871). The default-off
`PERRY_ATTR_DIAG` census (`--features perry-runtime/attr-census`) counts
which attribute paths a program runs.
10 changes: 8 additions & 2 deletions crates/perry-codegen/src/expr/proxy_reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ use proxy_reflect_write_ic::StableTombstoneSlotCheck;
// unconditional layout note retires the proof even for pointer-free tagged
// values such as SSO strings and booleans. After that miss, the PIC is eligible
// again. This keeps proof retirement out of every ordinary-object hit.
const WRITE_PIC_BLOCKING_FLAGS: u16 = 0x1987;
//
// Charter step 3 (#10871): FROZEN/SEALED/NO_EXTEND (0x7) and HAS_DESCRIPTORS
// (0x800) are NOT here. The prime that publishes this cache's tokens checks,
// per KEY, that the receiver's keys record the key as a plain writable data
// property (`key_attrs::entry_is_plain_writable_data`), and every attribute or
// integrity change moves the ShapeId, so the token compare proves it.
const WRITE_PIC_BLOCKING_FLAGS: u16 = 0x1180;

/// #8098: `GcHeader::_reserved` bit 9 — the runtime birth-marked this
/// class-less receiver an ORDINARY plain object (`JSON.parse` output), so it is
Expand Down Expand Up @@ -457,7 +463,7 @@ fn guarded_declared_class_property_candidate(ctx: &FnCtx<'_>, target: &Expr) ->
/// Registers arrive in k → v → t evaluation order (see the call site); from
/// the target register onward the path is call-free until the store or the
/// outlined slow call. Guards are byte-for-byte the static write PIC's
/// (GcHeader -8/-7/-6 with BLOCKING 0x1987 incl. typed-intact and the packed
/// (GcHeader -8/-7/-6 with BLOCKING 0x1180 incl. typed-intact and the packed
/// numeric-proof authority, ObjectHeader
/// regular/class/token via the #6804 discriminated shape-token select).
/// The raw store fires only for non-reference VALUE tags (not pointer/
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ crate-type = ["rlib"]
# they look gets this backwards, so every call site is gated rather than
# argued about.
shape-mint-diag = []
# Charter step 3: default-off `PERRY_ATTR_DIAG` census of the attribute
# machinery (where attributes are written, read, and which key-list paths run).
attr-census = []

# `default` keeps the shipped prebuilt libperry_runtime.a and plain
# `cargo build`/test full-featured. The auto-optimize path builds with
Expand Down
34 changes: 34 additions & 0 deletions crates/perry-runtime/src/array/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,40 @@ pub(crate) fn js_array_alloc_key_list(capacity: u32, all_ptr: bool) -> *mut Arra
ptr
}

/// [`js_array_alloc_key_list`] for a key list that carries ATTRIBUTES
/// (`object/key_attrs.rs`): one physical slot in front of logical element 0
/// is reserved for the attributes pointer, under `GC_ARRAY_NAMED_PROPS`, so
/// `array_front_offset` is 1 from birth and every element reader (which goes
/// through `array_elements_ptr`) sees the same logical layout as an
/// attribute-free list. The reserve word starts as `TAG_HOLE`, a non-pointer,
/// so a collection before the attributes are attached traces nothing there.
pub(crate) fn js_array_alloc_key_list_reserved(capacity: u32, all_ptr: bool) -> *mut ArrayHeader {
let capacity = array_capacity_or_throw(capacity);
let reserve = crate::object::key_attrs::KEYS_ATTRS_FRONT_SLOTS;
let ptr = arena_alloc_gc(
array_byte_size(capacity as usize + reserve),
8,
crate::gc::GC_TYPE_ARRAY,
) as *mut ArrayHeader;
unsafe {
(*ptr).length = 0;
(*ptr).capacity = capacity;
// GC_STORE_AUDIT(INIT): the reserve word of a just-allocated array
// nothing references yet; a non-pointer, so no edge and no barrier.
std::ptr::write(ptr.add(1) as *mut u64, crate::value::TAG_HOLE);
clear_array_numeric_layout(ptr);
if all_ptr {
crate::gc::layout_init_all_pointer_slots(ptr as *mut u8);
} else {
crate::gc::layout_init_pointer_free(ptr as *mut u8);
}
let header = crate::gc::header_from_trusted_user_ptr(ptr.cast()).cast_mut();
(*header)._reserved |= crate::gc::GC_ARRAY_NAMED_PROPS;
debug_assert_eq!(crate::array::array_front_offset(ptr), reserve);
}
ptr
}

/// Create a new empty array (convenience alias for `js_array_alloc(0)`).
/// Used by perry-ui audio code.
#[no_mangle]
Expand Down
11 changes: 6 additions & 5 deletions crates/perry-runtime/src/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ mod tests;
mod typed_array_receiver_tests;

pub(crate) use self::alloc::{
array_length_range_error, js_array_alloc_key_list, js_array_alloc_pointer_elements,
js_array_alloc_with_length_exact,
array_length_range_error, js_array_alloc_key_list, js_array_alloc_key_list_reserved,
js_array_alloc_pointer_elements, js_array_alloc_with_length_exact,
};
pub use self::alloc::{
js_array_alloc, js_array_alloc_literal, js_array_alloc_with_length,
Expand Down Expand Up @@ -307,9 +307,10 @@ pub(crate) use self::named_props::{
array_has_named_properties_resolved, array_has_sparse_index_properties_resolved,
array_named_property_delete, array_named_property_delete_by_name, array_named_property_get,
array_named_property_get_by_name, array_named_property_has, array_named_property_names,
array_named_property_set, array_named_props_reserve, carry_named_props_reserve,
prune_dead_full_array_named_property_owners, transfer_full_array_named_props_owner,
visit_array_named_props_slots,
array_named_property_set, array_named_props_reserve, array_named_props_slot,
carry_named_props_reserve, ensure_named_props_slot,
prune_dead_full_array_named_property_owners, store_named_props_word,
transfer_full_array_named_props_owner, visit_array_named_props_slots,
};

// Sole caller is the regex-engine-gated `regex::perex_results`, so the helpers
Expand Down
15 changes: 13 additions & 2 deletions crates/perry-runtime/src/array/named_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,19 @@ unsafe fn lookup(
/// `arr` must be a live, forwarding-resolved head with a reserve, `pairs` a
/// live pairs array. Nothing may allocate between the write and the barrier.
unsafe fn store_pairs_pointer(arr: *mut ArrayHeader, pairs: *mut ArrayHeader) {
store_named_props_word(arr, crate::value::js_nanbox_pointer(pairs as i64).to_bits());
}

/// Store `bits` into the reserve header word of a flagged head, barriered.
/// The pairs pointer's store, shared with the keys-array attributes pointer
/// (`object/key_attrs.rs`), which reuses this reserve on internal key lists.
///
/// # Safety
/// `arr` must be a live, forwarding-resolved head carrying
/// `GC_ARRAY_NAMED_PROPS`.
#[inline]
pub(crate) unsafe fn store_named_props_word(arr: *mut ArrayHeader, bits: u64) {
let slot = array_named_props_slot(arr);
let bits = crate::value::js_nanbox_pointer(pairs as i64).to_bits();
// GC_STORE_AUDIT(BARRIERED): the reserved-slot edge is recorded by the
// slot barrier below; the collector enumerates this exact word as a fixed
// child slot of the array.
Expand Down Expand Up @@ -617,7 +628,7 @@ unsafe fn materialize_inline(arr: *mut ArrayHeader) -> *mut ArrayHeader {
/// # Safety
/// `arr` must be a live, forwarding-resolved `GC_TYPE_ARRAY` head that
/// `resolved_flags` reported as a real array. May allocate (via `js_array_grow`).
unsafe fn ensure_named_props_slot(arr: *mut ArrayHeader) -> *mut ArrayHeader {
pub(crate) unsafe fn ensure_named_props_slot(arr: *mut ArrayHeader) -> *mut ArrayHeader {
if array_named_props_flagged_resolved(arr) {
return arr;
}
Expand Down
5 changes: 1 addition & 4 deletions crates/perry-runtime/src/async_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,13 +1251,10 @@ pub extern "C" fn js_async_resource_subclass_init(
let current_this = this_handle.get_nanbox_f64();
let current_raw =
crate::value::js_nanbox_get_pointer(current_this) as *mut ObjectHeader;
crate::object::js_object_set_field_by_name(
crate::object::define_builtin_data_property(
current_raw,
method_key,
method_handle.get_nanbox_f64(),
);
crate::object::set_builtin_property_attrs(
current_raw as usize,
name.to_string(),
crate::object::PropertyAttrs::new(true, false, true),
);
Expand Down
12 changes: 9 additions & 3 deletions crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,12 @@ fn test_tenured_owner_descriptor_entries_survive_minor_gc() {
let _guard = GcTestIsolationGuard::new();
let (obj, _) = unsafe { alloc_old_test_object(0) };
let addr = obj as usize;
crate::object::set_property_attrs(
// An accessor: an ordinary object's DATA attributes live with its keys
// (charter step 3); its accessor closures are still owner-keyed.
crate::object::set_accessor_descriptor(
addr,
"oldKey".to_string(),
crate::object::PropertyAttrs::new(false, true, true),
crate::object::AccessorDescriptor::default(),
);

// MINOR traces never mark the old generation — an unmarked old-gen
Expand All @@ -296,7 +298,11 @@ fn test_tenured_owner_descriptor_entries_survive_minor_gc() {
let _ = gc_collect_minor();

assert!(
crate::object::get_property_attrs(addr, "oldKey").is_some(),
crate::state::state()
.descriptors
.accessor_descriptors
.borrow()
.contains_key(&(addr, "oldKey".to_string())),
"an old-gen owner's descriptor entry must survive a minor GC — \
minor-trace deadness is not trustworthy for tenured objects"
);
Expand Down
78 changes: 78 additions & 0 deletions crates/perry-runtime/src/gc/tests/keys_attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Charter step 3: a keys array's attributes array hangs off its
//! named-properties reserve word (`object/key_attrs.rs`). The collector already
//! emits that word as a fixed child slot of every flagged array; this pins
//! that a real copying minor moves BOTH the keys backing and its attributes
//! array and leaves the entries readable through the moved keys.
//!
//! Fault injection this catches: a keys-list allocator that forgets
//! `GC_ARRAY_NAMED_PROPS` (the reserve word is then never visited, the
//! attributes array dies in the nursery, and the entries read back as
//! whatever reuses its storage) — `attributes_survive_a_moving_minor`.

use super::super::*;
use super::support::{collect_minor_trace, CopyingNurseryTestGuard, GcTriggerThresholdTestGuard};
use crate::object::canonical_keys::{extend_key_with_entry, CanonicalKeys, SharedLayout};
use crate::object::key_attrs::{self, ENTRY_ACCESSOR, ENTRY_HAS_GET, ENTRY_NON_ENUMERABLE};

#[test]
fn attributes_survive_a_moving_minor() {
let _guard = CopyingNurseryTestGuard::new(0);
let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
super::support::register_runtime_handle_root_scanner_for_tests();
gc_register_mutable_root_scanner(crate::object::canonical_keys::scan_canonical_keys_roots_mut);
crate::object::canonical_keys::reset_for_test();
let proof = SharedLayout::shape_cache_entry();
let entries = [0u8, ENTRY_NON_ENUMERABLE, ENTRY_ACCESSOR | ENTRY_HAS_GET];
let scope = RuntimeHandleScope::new();
let list = scope.root_raw_mut_ptr::<crate::ArrayHeader>(std::ptr::null_mut());
let mut len = 0u32;
for (i, &e) in entries.iter().enumerate() {
let name = format!("ka_moving_{i}");
let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
// `extend_key_with_entry` roots its operands across its allocation.
let next = list.with_mut_ptr(|arr: *mut crate::ArrayHeader| unsafe {
extend_key_with_entry(&proof, CanonicalKeys::from_rooted(arr, len), k, e)
});
list.set_raw_mut_ptr(next.as_ptr());
len = next.len();
}
let (before_keys, before_attrs) = list.with_mut_ptr(|keys: *mut crate::ArrayHeader| {
(keys as usize, unsafe { key_attrs::keys_attrs(keys) }
as usize)
});
assert_ne!(
before_attrs, 0,
"premise: the list carries an attributes array"
);
assert!(
crate::arena::pointer_in_nursery(before_attrs),
"premise: the attributes array is young, so a copying minor moves it"
);
let trace = collect_minor_trace(GcTriggerKind::Direct);
assert!(
trace.copying_nursery.copied_objects > 0,
"premise: the minor copied"
);
list.with_mut_ptr(|after_keys: *mut crate::ArrayHeader| {
let after_attrs = unsafe { key_attrs::keys_attrs(after_keys) } as usize;
assert_ne!(
after_keys as usize, before_keys,
"premise: the keys backing moved"
);
assert_ne!(
after_attrs, before_attrs,
"the attributes array must move with it"
);
for (i, &e) in entries.iter().enumerate() {
assert_eq!(
unsafe { key_attrs::keys_entry(after_keys, i as u32) },
e,
"entry {i} after the move"
);
}
assert_eq!(
unsafe { key_attrs::keys_summary(after_keys, 3) },
key_attrs::SUMMARY_NON_ENUMERABLE | key_attrs::SUMMARY_ACCESSOR
);
});
}
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ mod inline_generation_gate_contract;
mod inline_pointer_bearing_contract;
mod json_parse_scalar;
mod json_stringify_output;
mod keys_attrs;
mod layout_inline_mask;
mod layout_pointer_free_hazard;
mod layout_residue_histogram;
Expand Down
10 changes: 9 additions & 1 deletion crates/perry-runtime/src/gc/tests/young_log_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,16 @@ fn old_descriptor_owners_are_skipped_by_a_minor() {

let _ = gc_collect_minor();

// The TABLE entry, read directly: the owner's attributes (which say that
// `g` is an accessor) live with its keys since charter step 3, and this
// harness registers only the descriptor scanner, not the shape table's.
assert_eq!(
crate::object::get_accessor_descriptor(owner, "g").map(|acc| acc.get),
crate::state::state()
.descriptors
.accessor_descriptors
.borrow()
.get(&(owner, "g".to_string()))
.map(|acc| acc.get),
Some(ptr_bits(getter))
);
let row = walk("object.descriptors");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ fn shape_template_declines_element_descriptors_before_output() {
let value = crate::json::test_json_parse_direct(source);
let obj = value.as_pointer::<crate::ObjectHeader>() as *mut crate::ObjectHeader;
let template = build_shape_prefix_template(value.bits()).unwrap();
// The descriptor bit travels with this receiver, independently of
// the shared keys array. Marking it must invalidate raw-slot emission
// even when a template was already built for the same shape.
// Charter step 3: the attribute lives with the keys, so the receiver
// moves to a different key list; raw-slot emission from the template
// built for the old one must decline.
crate::object::set_property_attrs(
obj as usize,
"id".into(),
crate::object::PropertyAttrs::new(true, false, true),
);
assert_eq!(
assert_ne!(
crate::object::object_keys(obj).arr(),
template.keys_arr.get()
);
Expand Down
Loading
Loading