diff --git a/changelog.d/8076-class-field-runtime-layout-guard.md b/changelog.d/8076-class-field-runtime-layout-guard.md new file mode 100644 index 0000000000..6cb4507824 --- /dev/null +++ b/changelog.d/8076-class-field-runtime-layout-guard.md @@ -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. diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 3cf6200228..85196f368f 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -131,6 +131,7 @@ pub(super) struct TypedLayoutDescriptor { thread_local! { pub(super) static TRACE_SLOT_READS: Cell = const { Cell::new(0) }; static TYPED_SLOT_DESCRIPTOR_PROBES: Cell = const { Cell::new(0) }; + static TYPED_RAW_F64_DESCRIPTOR_QUERIES: Cell = const { Cell::new(0) }; } // #6893: SHAPE-keyed canonical typed layout. Replaces the per-OBJECT @@ -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) }) @@ -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) +} diff --git a/crates/perry-runtime/src/typed_feedback/guards.rs b/crates/perry-runtime/src/typed_feedback/guards.rs index 621ac1be8a..cb2476caa3 100644 --- a/crates/perry-runtime/src/typed_feedback/guards.rs +++ b/crates/perry-runtime/src/typed_feedback/guards.rs @@ -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, @@ -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) @@ -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 @@ -317,11 +347,7 @@ 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 @@ -329,12 +355,7 @@ fn class_field_fast_contract( 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 } } @@ -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); diff --git a/crates/perry-runtime/src/typed_feedback/tests.rs b/crates/perry-runtime/src/typed_feedback/tests.rs index 7465c9d4a1..4db726e112 100644 --- a/crates/perry-runtime/src/typed_feedback/tests.rs +++ b/crates/perry-runtime/src/typed_feedback/tests.rs @@ -1663,10 +1663,16 @@ 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)); @@ -1674,6 +1680,11 @@ fn typed_feedback_class_field_get_guard_requires_raw_f64_layout_when_requested() 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); @@ -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, @@ -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); @@ -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);