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
7 changes: 7 additions & 0 deletions changelog.d/8076-class-field-runtime-layout-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Use canonical layout metadata in runtime class-field guards

Raw-number class-field guards now read the object's canonical typed-layout
header bit after proving its exact class, keys, and slot bounds. This removes
the remaining per-access thread-local descriptor-map lookup when the inline
guard is disabled, while `PERRY_VERIFY_TYPED_INTACT` retains the independent
descriptor check for GC-layout verification.
13 changes: 13 additions & 0 deletions crates/perry-runtime/src/gc/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub(super) struct TypedLayoutDescriptor {
thread_local! {
pub(super) static TRACE_SLOT_READS: Cell<usize> = const { Cell::new(0) };
static TYPED_SLOT_DESCRIPTOR_PROBES: Cell<usize> = const { Cell::new(0) };
static TYPED_RAW_F64_DESCRIPTOR_QUERIES: Cell<usize> = const { Cell::new(0) };
}

// #6893: SHAPE-keyed canonical typed layout. Replaces the per-OBJECT
Expand Down Expand Up @@ -1344,6 +1345,8 @@ pub(crate) fn layout_typed_intact_for_user(user_ptr: usize) -> bool {
}

pub(crate) fn layout_typed_raw_f64_slot_for_user(user_ptr: usize, slot_index: usize) -> bool {
#[cfg(test)]
TYPED_RAW_F64_DESCRIPTOR_QUERIES.with(|c| c.set(c.get() + 1));
with_typed_descriptor_for_query(user_ptr, |layout| {
slot_index < layout.slot_count && layout.raw_f64_mask.contains_slot(slot_index)
})
Expand Down Expand Up @@ -1840,3 +1843,13 @@ pub(super) fn test_reset_typed_slot_descriptor_probes() {
pub(super) fn test_typed_slot_descriptor_probes() -> usize {
TYPED_SLOT_DESCRIPTOR_PROBES.with(Cell::get)
}

#[cfg(test)]
pub(crate) fn test_reset_typed_raw_f64_descriptor_queries() {
TYPED_RAW_F64_DESCRIPTOR_QUERIES.with(|c| c.set(0));
}

#[cfg(test)]
pub(crate) fn test_typed_raw_f64_descriptor_queries() -> usize {
TYPED_RAW_F64_DESCRIPTOR_QUERIES.with(Cell::get)
}
58 changes: 40 additions & 18 deletions crates/perry-runtime/src/typed_feedback/guards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,30 @@ fn descriptor_blocks_class_field_get(obj_addr: usize, class_id: u32, key_name: &
false
}

/// Decide the raw-f64 half of a class-field guard after the caller has proven
/// the receiver's exact class/keys pair and that `field_index` is in bounds.
///
/// That shape proof ties the slot to the compile-time mask which made
/// `require_raw_f64` true. The per-object INTACT bit is therefore the complete
/// production answer: it is cleared before any representation downgrade and
/// is the same O(1) fact the codegen-inlined guard already trusts. Keep the
/// descriptor lookup only in `PERRY_VERIFY_TYPED_INTACT` mode, where doing the
/// expensive independent check is the feature's purpose.
#[inline]
fn class_field_raw_f64_layout_contract(
object_addr: usize,
field_index: u32,
require_raw_f64: bool,
) -> bool {
if !require_raw_f64 {
return true;
}
if verify_typed_intact_enabled() {
return crate::gc::layout_typed_raw_f64_slot_for_user(object_addr, field_index as usize);
}
crate::gc::layout_typed_intact_for_user(object_addr)
}

fn class_field_get_contract(
receiver: f64,
expected_class_id: u32,
Expand Down Expand Up @@ -272,11 +296,11 @@ fn class_field_get_contract(
&& expected_field_index < (*obj).field_count
&& plain_array_index_guard(expected_keys, expected_field_index, true)
&& object_key_matches_field(obj, key, expected_field_index)
&& (!require_raw_f64
|| crate::gc::layout_typed_raw_f64_slot_for_user(
object_addr,
expected_field_index as usize,
))
&& class_field_raw_f64_layout_contract(
object_addr,
expected_field_index,
require_raw_f64,
)
&& !class_getter_in_chain(class_id, &key_name)
&& !descriptor_blocks_class_field_get(object_addr, class_id, &key_name);
(shape_addr, class_id, gc_type, valid)
Expand Down Expand Up @@ -308,6 +332,12 @@ fn class_field_fast_contract(
&& (*obj).class_id == expected_class_id
&& std::ptr::eq((*obj).keys_array as *const ArrayHeader, expected_keys)
&& expected_field_index < (*obj).field_count;
let layout_ok = shape_ok
&& class_field_raw_f64_layout_contract(
object_addr,
expected_field_index,
require_raw_f64,
);
// #5093 self-check: the codegen-inlined fast path concludes "slot K is
// raw-f64" purely from the per-object intact bit (plus a class_id/keys
// match). Under PERRY_VERIFY_TYPED_INTACT=1, assert that whenever this
Expand All @@ -317,24 +347,15 @@ fn class_field_fast_contract(
// double. Any drift aborts loudly during the test sweep.
if require_raw_f64 && shape_ok && verify_typed_intact_enabled() {
let intact = crate::gc::layout_typed_intact_for_user(object_addr);
let raw = crate::gc::layout_typed_raw_f64_slot_for_user(
object_addr,
expected_field_index as usize,
);
if intact && !raw {
if intact && !layout_ok {
eprintln!(
"PERRY_VERIFY_TYPED_INTACT: intact bit set on class {} but slot {} is not raw-f64 in the side table (inline fast path would corrupt)",
expected_class_id, expected_field_index
);
std::process::abort();
}
}
shape_ok
&& (!require_raw_f64
|| crate::gc::layout_typed_raw_f64_slot_for_user(
object_addr,
expected_field_index as usize,
))
layout_ok
}
}

Expand Down Expand Up @@ -528,9 +549,10 @@ fn class_field_set_contract(
&& object_key_matches_field(obj, key, expected_field_index)
&& (!require_raw_f64
|| (is_plain_number_bits(value_bits)
&& crate::gc::layout_typed_raw_f64_slot_for_user(
&& class_field_raw_f64_layout_contract(
object_addr,
expected_field_index as usize,
expected_field_index,
true,
)))
&& !class_setter_in_chain(class_id, &key_name)
&& !descriptor_blocks_class_field_set(object_addr, class_id, &key_name);
Expand Down
22 changes: 22 additions & 0 deletions crates/perry-runtime/src/typed_feedback/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1663,17 +1663,28 @@ fn typed_feedback_class_field_get_guard_requires_raw_f64_layout_when_requested()
std::ptr::null(),
0,
);
crate::gc::test_reset_typed_raw_f64_descriptor_queries();

let first =
js_typed_feedback_class_field_get_guard(43, receiver, class_id, expected_keys, key_x, 0, 1);
assert_eq!(first, 1);
assert_eq!(
crate::gc::test_typed_raw_f64_descriptor_queries(),
0,
"the production guard must prove the raw slot from the canonical-layout header bit"
);

let payload = crate::string::js_string_from_bytes(b"boxed".as_ptr(), 5);
crate::object::js_object_set_field(obj, 0, crate::JSValue::string_ptr(payload));

let second =
js_typed_feedback_class_field_get_guard(43, receiver, class_id, expected_keys, key_x, 0, 1);
assert_eq!(second, 0);
assert_eq!(
crate::gc::test_typed_raw_f64_descriptor_queries(),
0,
"a cleared intact bit must reject without probing either descriptor map"
);

let site = &typed_feedback_snapshot().sites[0];
assert_eq!(site.guard_passes, 1);
Expand All @@ -1699,6 +1710,7 @@ fn typed_feedback_class_field_set_guard_requires_raw_f64_value_and_layout() {
std::ptr::null(),
0,
);
crate::gc::test_reset_typed_raw_f64_descriptor_queries();

let first = js_typed_feedback_class_field_set_guard(
44,
Expand All @@ -1711,6 +1723,11 @@ fn typed_feedback_class_field_set_guard_requires_raw_f64_value_and_layout() {
1,
);
assert_eq!(first, 1);
assert_eq!(
crate::gc::test_typed_raw_f64_descriptor_queries(),
0,
"the set guard must use the same O(1) canonical-layout proof as the get guard"
);

let payload = crate::string::js_string_from_bytes(b"boxed".as_ptr(), 5);
let payload_value = crate::value::js_nanbox_string(payload as i64);
Expand Down Expand Up @@ -1751,6 +1768,11 @@ fn typed_feedback_class_field_set_guard_requires_raw_f64_value_and_layout() {
1,
);
assert_eq!(fourth, 0);
assert_eq!(
crate::gc::test_typed_raw_f64_descriptor_queries(),
0,
"value rejections and the intact-bit proof must keep descriptor maps off the hot path"
);

let site = &typed_feedback_snapshot().sites[0];
assert_eq!(site.guard_passes, 1);
Expand Down
Loading