Skip to content
Closed
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: 8 additions & 2 deletions crates/perry-codegen/src/expr/in_presence_ic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,14 @@ pub(crate) fn lower_in_presence_ic(ctx: &mut FnCtx<'_>, obj_box: &str, key_box:
let shape_word = ctx.block().zext(I32, &shape_id, I64);
let cached_shape_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]);
let cached_shape = ctx.block().load(I64, &cached_shape_ptr);
// An unarmed cache reads 0, which no stamped shape word can equal, so the
// "is this site armed?" question needs no test of its own.
// The "is this site armed?" question has no test of its own: the runtime
// resolves this cache with word 0 already holding `IN_PRESENCE_UNARMED`
// (`1 << 32`, above every zero-extended `+4` word), so an unarmed site
// matches nothing. It is NOT enough that no stamped shape word is 0 — an
// UNSTAMPED receiver's `+4` is its `parent_class_id`, which is 0 for an
// anonymous object literal, and a site resolved by a prototype-chain
// `true` (resolved, budget counted, never armed) used to read 0 there and
// answer `"k" in {}` with `true`. Same flaw #10833 took off the read tower.
let shape_matches = ctx.block().icmp_eq(I64, &shape_word, &cached_shape);
let present = ctx.block().and(I1, &is_object, &not_forwarded);
let present = ctx.block().and(I1, &present, &ordinary);
Expand Down
372 changes: 223 additions & 149 deletions crates/perry-codegen/src/expr/property_get/generic_dispatch.rs

Large diffs are not rendered by default.

462 changes: 393 additions & 69 deletions crates/perry-codegen/src/expr/property_get/tests.rs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions crates/perry-codegen/src/gc_call_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ pub(crate) fn classify_direct_callee(name: &str) -> GcCallEffect {
// outside the GC heap (the `js_box_alloc_bits` precedent).
| "perry_transition_cache_base"
| "js_transition_ic_note_hit"
// `object/inherited_read_cache.rs` (#10834/#10842): a direct-mapped
// per-thread table probe — identity-word and ShapeId compares, a
// validity-word compare, then one load through the holder. It
// allocates nothing, never calls user code and never walks the
// chain (that is the miss handler's prime); every case it cannot
// serve answers TAG_HOLE and the emitted code takes its ordinary
// slow call. Listed so nothing is spilled or reloaded around it on
// the declined-guard edge of every generic property read.
| "js_inherited_read_cache_hit_f64"
| "js_transition_ic_spill_append"
| "js_write_barrier_slot"
| "js_write_barrier_slot_validated_parent"
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/root_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const NON_COLLECTING: &[&str] = &[
"js_write_barrier_root_nanbox",
"perry_transition_cache_base",
"js_transition_ic_note_hit",
"js_inherited_read_cache_hit_f64",
"js_transition_ic_spill_append",
"js_write_barrier_slot",
"js_write_barrier_slot_validated_parent",
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/runtime_decls/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@ pub fn declare_phase_b_objects(module: &mut LlModule) {
);
module.declare_function("perry_transition_cache_base", PTR, &[]);
module.declare_function("js_transition_ic_note_hit", VOID, &[]);
// #10834/#10842: the inherited-read cache hit, asked on the generic
// property read's declined-guard edge (`expr/property_get/
// generic_dispatch.rs`): masked receiver + interned key -> NaN-boxed
// value, or `TAG_HOLE` for a decline. A pure state read (see
// `gc_call_effects.rs`).
module.declare_function("js_inherited_read_cache_hit_f64", DOUBLE, &[PTR, PTR]);
module.declare_function(
"js_put_value_set_dyn_ic",
DOUBLE,
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/object/field_get_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,9 @@ pub(crate) use ic_miss::{primitive_proto_method_name_static, test_push_catch_pri
#[path = "field_get_set/ic_miss/ic_slow.rs"]
mod ic_slow;
pub(crate) use ic_slot::pic_slot_census;
pub use ic_slot::{pic_arena_bytes, pic_slot_peek, pic_slot_resolve, pic_slots_resolved};
pub use ic_slot::{
pic_arena_bytes, pic_slot_peek, pic_slot_resolve, pic_slot_resolve_init, pic_slots_resolved,
};
pub use ic_slow::{js_object_get_field_ic_nonptr, js_object_get_field_ic_slow};

#[cfg(test)]
Expand Down
73 changes: 72 additions & 1 deletion crates/perry-runtime/src/object/field_get_set/has_property_ic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ pub unsafe extern "C" fn js_in_operator_presence_ic(
// negative is not cacheable (see the module header).
return answer;
}
let cache = crate::object::pic_slot_resolve(slot);
// Resolved with word 0 already holding [`IN_PRESENCE_UNARMED`]: a `true`
// from the prototype chain resolves the slot (the attempt budget lives in
// the cache) but cannot arm it, and a zero word 0 is not "unarmed" to the
// emitted guard — see the constant.
let cache = crate::object::pic_slot_resolve_init(slot, |c| {
(*c).shape = IN_PRESENCE_UNARMED;
});
if cache.is_null() {
return answer;
}
Expand All @@ -106,6 +112,20 @@ pub unsafe extern "C" fn js_in_operator_presence_ic(
answer
}

/// Word 0 of a presence cache that has been resolved but not armed.
///
/// The emitted guard (`perry-codegen/src/expr/in_presence_ic.rs`) compares the
/// receiver's zero-extended `+4` word against word 0 and has no "is this site
/// armed?" test of its own, on the assumption that no stamped receiver's
/// ShapeId can be 0. That is true, and beside the point: a receiver that was
/// never shape-stamped carries `parent_class_id` at `+4`, which is 0 for an
/// anonymous object literal. So at a site whose cache had been resolved by a
/// prototype-chain `true` (which resolves, spends an attempt, and cannot arm),
/// `"k" in {}` read word 0 == 0 == the receiver's word and answered `true`.
/// The same zero-sentinel flaw was taken off the property-read tower in
/// #10833; `1 << 32` is above every zero-extended `u32`, so it matches nothing.
pub(crate) const IN_PRESENCE_UNARMED: u64 = 1 << 32;

/// The receiver's ShapeId, when a site may answer `true` for `key` from it
/// alone: an ordinary, shape-stamped, descriptor-free, tombstone-free heap
/// object whose keys array holds `key` as an own entry.
Expand Down Expand Up @@ -191,4 +211,55 @@ mod tests {
fn the_attempt_budget_is_small_and_nonzero() {
assert!(IN_PRESENCE_ATTEMPT_BUDGET > 0 && IN_PRESENCE_ATTEMPT_BUDGET <= 16);
}

/// A `true` the site cannot arm on (here: the key sits under a descriptor)
/// resolves the cache to count the attempt, and used to leave word 0 at
/// its zeroed birth value — which the emitted guard then matched against
/// any receiver whose `+4` word is 0, i.e. every unstamped anonymous
/// object literal: `"k" in {}` answered `true`. Word 0 must be resolved
/// holding a value no zero-extended `u32` can equal.
#[test]
fn a_resolved_but_unarmed_site_cannot_be_matched_by_an_unstamped_receiver() {
let _lock = crate::gc::global_side_table_test_lock();
let scope = crate::gc::RuntimeHandleScope::new();
let obj = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 4));
let key = scope.root_string_ptr(crate::string::js_string_from_bytes(b"k".as_ptr(), 1));
obj.with_mut_ptr(|o| {
key.with_const_ptr(|k| crate::object::js_object_set_field_by_name(o, k, 1.0))
});
// A descriptor makes the receiver unarmable while `in` still says true.
obj.with_mut_ptr(|o: *mut ObjectHeader| {
super::super::super::descriptor_state::set_property_attrs(
o as usize,
"k".to_string(),
crate::object::PropertyAttrs::new(false, true, false),
);
});
let mut slot: *mut InPresenceCache = std::ptr::null_mut();
let answer = obj.with_mut_ptr(|o: *mut ObjectHeader| {
key.with_const_ptr(|k: *const crate::StringHeader| unsafe {
js_in_operator_presence_ic(
f64::from_bits(crate::value::js_nanbox_pointer(o as i64).to_bits()),
f64::from_bits(crate::value::js_nanbox_string(k as i64).to_bits()),
&mut slot,
)
})
});
assert_eq!(
answer.to_bits(),
0x7FFC_0000_0000_0004,
"test premise: `in` is true"
);
assert!(
!slot.is_null(),
"test premise: the attempt resolved the cache"
);
let word0 = unsafe { (*slot).shape };
assert_eq!(word0, IN_PRESENCE_UNARMED);
assert!(
word0 > u64::from(u32::MAX),
"an unarmed word must be unmatchable by any zero-extended +4 word, got {word0:#x}"
);
assert_ne!(word0, 0, "0 is what an unstamped `{{}}` carries at +4");
}
}
Loading
Loading