Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2e5cd30
perf: validate inherited method sites by holder shape and loaded slot
Sep 30, 2026
7c2fb81
perf: retire inherited read side table and marked value invalidation
Sep 30, 2026
8ba8606
fix: gate process-global read sites when workers start
Sep 30, 2026
cec5347
test: refuse class prototype identities at read-holder sites
Sep 30, 2026
19e8a24
test: reconcile A2 root-holder inventory after P4
Sep 30, 2026
7f4bd97
docs: note inherited-read single-path change
Sep 30, 2026
44f20d6
test: require relocated inherited method holder root
Sep 30, 2026
79cfd60
fix: acquire method-site slot publication before worker gate
Sep 30, 2026
b411d73
test: adapt class-prototype admission fixture to birth rep
Sep 30, 2026
5921301
Cache direct class prototype getters at read sites
Sep 30, 2026
6c64679
Admit live-linked declared class accessors
Sep 30, 2026
1aedbb6
Cache multiple receiver shapes for one absent read holder
Sep 30, 2026
0dda83e
test: cover polymorphic absent reads across prototype and GC changes
Sep 30, 2026
398133b
Warm lazy class getter site without latching and root read key
Sep 30, 2026
8d4d48b
Add bounded collecting class read memo for absent and inherited data
Sep 30, 2026
0d1ab04
Add class-instance optional-read parity fixture
Sep 30, 2026
fd227ae
Test class read memo worker gate on collecting hit
Sep 30, 2026
fd7d638
Add real-worker class read gate parity fixture
Sep 30, 2026
fc84b40
Keep multi-absent lookup off ordinary holder hits
Sep 30, 2026
fc74ad4
Scope read-holder key and test pointers to noncollecting use
Sep 30, 2026
b7eea0a
Scope class read key confirmation after generic getter
Sep 30, 2026
5beae69
perf: answer depth-one data holder before rare read kinds
Sep 30, 2026
43582ee
perf: keep rare holder reads out of inline class-field hit
Sep 30, 2026
2323621
perf: reuse holder shape proof on class getter hit
Sep 30, 2026
6de5642
Memoize direct class setters at packed PutValue sites
Sep 30, 2026
9e4c103
Test inherited setter site across evacuation and real worker gate
Sep 30, 2026
93afad3
Restrict setter memo to store-admitted receiver shapes
Sep 30, 2026
4b5d947
Give bundled setter worker a distinct class name
Sep 30, 2026
84da0a7
Probe direct setter before chain-store miss route
Sep 30, 2026
1429cf3
Guard class accessor sites with registry generation
Sep 30, 2026
d6d18cc
Decode only raw class accessor entries on read hits
Sep 30, 2026
b7105ce
Use validity epoch for direct class setter link
Sep 30, 2026
e483324
test: align A2 runtime gates with worker and value-store invariants
Sep 30, 2026
a0db1b4
test: preserve sticky worker gate across A2 unit tests
Sep 30, 2026
026cb9a
test: isolate A2 worker-gate units in fresh processes
Sep 30, 2026
ecfd2a9
ci: reconcile A2 root inventory with scope-context main
Sep 30, 2026
431b205
changelog: key inherited-read one-shape note to PR 11713
Sep 30, 2026
9498b64
Fix A2 test lint and rooted setter unit custody
Sep 30, 2026
25f58d9
test: make one-shape multi-absent witness nonvacuous
Sep 30, 2026
e985048
test(map): root ordered-delete fixture across string allocations
Sep 30, 2026
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
1 change: 1 addition & 0 deletions changelog.d/11713-inherited-read-one-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Objects:** Inherited reads and method calls now use receiver and holder shape facts; the old inherited-read cache and its GC roots are removed. Class getter and setter sites cache direct accessors with live shape and prototype checks. Worker startup gates holder-backed sites so each agent uses ordinary dispatch safely.
98 changes: 65 additions & 33 deletions crates/perry-codegen/src/expr/method_site.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The method-call site: `recv.m(args)` as ONE path — the property read's
//! receiver test and shape compare, one slot load (or the memoized inherited
//! closure), then a direct call of the method body with `recv` as `this`.
//! receiver test and shape compare, a slot load (from the receiver or a
//! shape-guarded direct holder), then a direct call with `recv` as `this`.
//!
//! The site's memo is a runtime `MethodSite`
//! (`perry-runtime/src/object/method_site.rs`, which states what an entry
Expand All @@ -12,8 +12,8 @@
//! w = load [recv] ; (class_id | ShapeId)
//! w == site.word else MISS
//! s = site.slot
//! s < 0 (inherited): PERRY_PROTO_VALIDITY == site.gen else MISS
//! h = site.closure ; f = site.code
//! s < 0 (inherited): [site.holder] == site.holder_word else MISS
//! v = load [site.holder + HDR + 8*i]
//! own: v = load [recv + HDR + 8*s] ; v is a heap pointer else MISS
//! fn: v = load [[recv + PROPS] + HDR + 8*s] (bit 61: a function's
//! own-property object; same checks as own)
Expand All @@ -30,7 +30,7 @@
//! exactly the behaviour it had.

use super::FnCtx;
use crate::types::{DOUBLE, I1, I32, I64, PTR};
use crate::types::{DOUBLE, I1, I32, I64, I8, PTR};

/// Is the One Path method site available for this call?
/// `PERRY_METHOD_SITE=0` at compile time keeps the old dispatcher (A/B).
Expand All @@ -42,7 +42,11 @@ pub(crate) fn method_site_enabled(ctx: &FnCtx<'_>, property: &str, argc: usize)
// 64-bit targets only: the site addresses 8-byte slots behind a 16-byte
// header and compares an 8-byte receiver word.
let triple = ctx.target_triple;
if !(triple.starts_with("x86_64") || triple.starts_with("aarch64")) || triple.contains("32") {
if !(triple.starts_with("x86_64")
|| triple.starts_with("aarch64")
|| triple.starts_with("arm64"))
|| triple.contains("32")
{
return false;
}
// A typed-feedback (profiling) build records every method call in the
Expand Down Expand Up @@ -110,6 +114,7 @@ pub(crate) fn emit_method_site(
let own_fn_idx = ctx.new_block("msite.own_fn");
let value_idx = ctx.new_block("msite.value");
let inh_idx = ctx.new_block("msite.inherited");
let inh_value_idx = ctx.new_block("msite.inherited_value");
let call_idx = ctx.new_block("msite.call");
let miss_idx = ctx.new_block("msite.miss");
let merge_idx = ctx.new_block("msite.merge");
Expand All @@ -119,6 +124,7 @@ pub(crate) fn emit_method_site(
let own_fn_l = ctx.block_label(own_fn_idx);
let value_l = ctx.block_label(value_idx);
let inh_l = ctx.block_label(inh_idx);
let inh_value_l = ctx.block_label(inh_value_idx);
let call_l = ctx.block_label(call_idx);
let miss_l = ctx.block_label(miss_idx);
let merge_l = ctx.block_label(merge_idx);
Expand All @@ -128,7 +134,18 @@ pub(crate) fn emit_method_site(
// takes the universal dispatcher directly, with its string and primitive
// arms, exactly as without a site. A heap object takes the site: its memo
// if the site has one, else the miss, which primes it.
let ic = crate::expr::emit_inline_cache_slot(ctx, &cache_name);
// The runtime publishes this process-global slot with an AtomicPtr CAS.
// A worker may enter the site just as the primary agent first publishes
// it, before the sticky worker gate below is loaded. Pair the load with
// that publication even though the worker will then take the miss path.
let slot_ref = format!("@{cache_name}");
let cache = ctx.block().load_atomic_acquire(PTR, &slot_ref, 8);
let present = ctx.block().icmp_ne(PTR, &cache, "null");
let ic = crate::expr::InlineCacheSlot {
slot_ref,
cache,
present,
};
let prim_idx = ctx.new_block("msite.primitive");
let object_idx = ctx.new_block("msite.object");
let prim_l = ctx.block_label(prim_idx);
Expand All @@ -141,7 +158,16 @@ pub(crate) fn emit_method_site(
fused.biased
};
ctx.current_block = object_idx;
ctx.block().cond_br(&ic.present, &deref_l, &miss_l);
// Site records are process-global, and inherited holders belong to the
// primary heap. A worker's first startup publishes this sticky gate
// before executing user code; afterward every agent takes the generic
// path. No worker reads a primary holder or races a primary site update.
let workers = ctx
.block()
.load_atomic_seq_cst(I8, "@PERRY_METHOD_SITE_WORKERS_PRESENT", 1);
let no_workers = ctx.block().icmp_eq(I8, &workers, "0");
let site_enabled = ctx.block().and(I1, &no_workers, &ic.present);
ctx.block().cond_br(&site_enabled, &deref_l, &miss_l);

// deref: the receiver word against each entry's word, in order.
ctx.current_block = deref_idx;
Expand Down Expand Up @@ -318,6 +344,33 @@ pub(crate) fn emit_method_site(
blk.br(&value_l);
(v, end)
};
// The receiver's shape pins the direct holder. Its word pins the slot;
// the slot value itself is loaded on every hit, just like an own method.
ctx.current_block = inh_idx;
let holder = {
let blk = ctx.block();
let hp = blk.gep(crate::types::I8, &entry, &[(I64, &abi_closure)]);
let holder = blk.load(I64, &hp);
let holder_ptr = blk.inttoptr(I64, &holder);
let word = blk.load(I64, &holder_ptr);
let wp = blk.gep(crate::types::I8, &entry, &[(I64, &abi_gen)]);
let saved = blk.load(I64, &wp);
let valid = blk.icmp_eq(I64, &word, &saved);
blk.cond_br(&valid, &inh_value_l, &miss_l);
holder
};
ctx.current_block = inh_value_idx;
let (inh_v, inh_end) = {
let blk = ctx.block();
let holder_ptr = blk.inttoptr(I64, &holder);
let base = blk.gep(crate::types::I8, &holder_ptr, &[(I64, &header.to_string())]);
let idx = blk.and(I64, &slot, &index_mask);
let vp = blk.gep(I64, &base, &[(I64, &idx)]);
let v = blk.load(I64, &vp);
let end = blk.label.clone();
blk.br(&value_l);
(v, end)
};
// value: it must hold a closure running the memoized body.
ctx.current_block = value_idx;
let own_ub = {
Expand All @@ -328,6 +381,7 @@ pub(crate) fn emit_method_site(
(&inline_v, &inline_end),
(&spill_v, &spill_end),
(&bag_v, &bag_end),
(&inh_v, &inh_end),
],
);
let u = blk.sub(I64, &v, &(RECEIVER_BIAS as i64).to_string());
Expand All @@ -336,7 +390,7 @@ pub(crate) fn emit_method_site(
u
};
ctx.current_block = own_fn_idx;
let (own_handle, own_func, own_end) = {
let (own_handle, own_func, _own_end) = {
let blk = ctx.block();
let kp = emit_field_ptr(blk, &own_ub, kind_offset);
let kind = blk.load(crate::types::I16, &kp);
Expand Down Expand Up @@ -387,31 +441,9 @@ pub(crate) fn emit_method_site(
}
(h, mf, end)
};
// inherited: the memoized closure, valid while the validity word holds.
ctx.current_block = inh_idx;
let (inh_handle, inh_func, inh_end) = {
let blk = ctx.block();
let g = blk.load(I64, "@PERRY_PROTO_VALIDITY");
let mg_p = blk.gep(crate::types::I8, &entry, &[(I64, &abi_gen)]);
let mg = blk.load(I64, &mg_p);
let valid = blk.icmp_eq(I64, &g, &mg);
let hp = blk.gep(crate::types::I8, &entry, &[(I64, &abi_closure)]);
let h = blk.load(I64, &hp);
let fp_p = blk.gep(crate::types::I8, &entry, &[(I64, &abi_code)]);
let f = blk.load(I64, &fp_p);
let end = blk.label.clone();
blk.cond_br(&valid, &call_l, &miss_l);
(h, f, end)
};
// call: the body directly, with the receiver as its `this` parameter.
ctx.current_block = call_idx;
let handle = ctx
.block()
.phi(I64, &[(&own_handle, &own_end), (&inh_handle, &inh_end)]);
let func = ctx
.block()
.phi(I64, &[(&own_func, &own_end), (&inh_func, &inh_end)]);
let fptr = ctx.block().inttoptr(I64, &func);
let fptr = ctx.block().inttoptr(I64, &own_func);
let mut call_args: Vec<String> = lowered_args.to_vec();
// Pad with `undefined` up to the arity the prime admits, so a body that
// declares a few more parameters than this call passes is entered
Expand All @@ -425,7 +457,7 @@ pub(crate) fn emit_method_site(
let hit_value = crate::expr::body_call::emit_js_body_call(
ctx.block(),
crate::expr::body_call::JsBody::Pointer(&fptr),
&handle,
&own_handle,
&recv_bits,
&call_args,
);
Expand Down
50 changes: 4 additions & 46 deletions crates/perry-codegen/src/expr/property_get/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,15 +1087,6 @@ fn generic_property_get_slot_load_is_reached_only_through_every_guard() {
"the overflow-bit test must not gate the inline slot load — a spill \
entry is refused by the ShapeId compare itself:\n{chain}"
);
// The inherited-read hook (#10834/#10842) lives on the DECLINED edge. Its
// answer must never be a condition on the way to the own slot load: if it
// were, an own read would pay a call, and this walk would have collected
// the call's result in the chain.
assert!(
!chain.contains("js_inherited_read_cache_hit_f64"),
"the inherited-read hook must not gate the inline slot load:\n{chain}"
);

// The GC header is not read on the way to the slot load at all: neither
// the kind byte (#10828 closed rule 3 — a `+4` word equal to a live
// ShapeId proves `GC_TYPE_OBJECT`) nor the descriptor flag (#10824 closed
Expand Down Expand Up @@ -1621,7 +1612,6 @@ fn the_generic_tower_is_one_leaf_call_two_exits_and_a_bounded_number_of_blocks()
.filter(|c| {
c.starts_with("js_object_get_field")
|| c.starts_with("js_typed_feedback_object_get_field")
|| c.starts_with("js_inherited_read_cache")
|| *c == "js_throw_type_error_property_access"
})
.collect();
Expand Down Expand Up @@ -1748,45 +1738,13 @@ fn a_spill_entry_is_recognised_by_the_front_and_nowhere_at_the_site() {
);
}

/// The inherited-read cache (#10834/#10842) is asked on the NEVER-PRIMED edge
/// and nowhere else. A read whose key lives on the prototype chain is never an
/// own slot on the receiver's shape, so a site that only reads such a key never
/// resolves its per-site cache. The first placement asked on EVERY path into
/// the exit and charged each own-key miss a declining probe (+88 on a
/// megamorphic site, +89 on a spill read, measured).
///
/// First-read D3: the probe moved into the slow entry
/// (`js_object_get_field_ic_slow`, which asks it only when the site's cache
/// slot is unresolved), behind the leaf front — so an own-key way, spill or
/// latched read never reaches it, and the site expands none of it. Pinned
/// here, each of which would otherwise fail silently (the program still
/// computes the right value through the slow entry):
///
/// 1. no block of the site calls the hook — in particular none on a path to
/// the inline slot load (the CFG-walk test asserts the same from the other
/// side);
/// 2. the slow entry is called from `pic.miss.call` only, with the same four
/// operands (the never-primed test reads the cache slot);
/// 3. `pic.miss.call` is reached from the front only on its `TAG_HOLE`
/// decline, so a front-served read never pays the probe;
/// 4. the merge takes the slow entry's value from `pic.miss.call`.
/// The generic read's collecting slow entry belongs on the miss-front decline.
/// Own-word and holder-shape hits bypass it. The call keeps all four operands,
/// and the merge uses the value returned by that one miss entry.
#[test]
fn the_inherited_read_cache_is_asked_on_the_never_primed_edge_only() {
fn the_generic_slow_read_is_called_only_after_the_front_declines() {
let ir = emit(false, None);
let blocks = tower_blocks(&ir);
// 1.
let holders: Vec<&str> = blocks
.iter()
.filter(|(_, body)| {
body.iter()
.any(|l| l.contains("@js_inherited_read_cache_hit_f64("))
})
.map(|(l, _)| l.as_str())
.collect();
assert!(
holders.is_empty(),
"the inherited hook belongs to the slow entry, not the site: {holders:?}"
);
// 2.
let slow_callers: Vec<(&str, &String)> = blocks
.iter()
Expand Down
2 changes: 0 additions & 2 deletions crates/perry-codegen/src/gc_effects/linux-x86_64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1455,8 +1455,6 @@ js_import_meta_resolve Reenters
js_import_meta_resolve_value Reenters
js_in_operator Reenters
js_in_operator_presence_ic Reenters
js_inherited_read_cache_hit_f64 Leaf
js_inherited_read_cache_stats Leaf
js_inline_arena_slow_alloc AllocOnly
js_inline_arena_state Leaf
js_install_global_value_surfaces Leaf
Expand Down
2 changes: 0 additions & 2 deletions crates/perry-codegen/src/gc_effects/macos-aarch64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1455,8 +1455,6 @@ js_import_meta_resolve Reenters
js_import_meta_resolve_value Reenters
js_in_operator Reenters
js_in_operator_presence_ic Reenters
js_inherited_read_cache_hit_f64 Leaf
js_inherited_read_cache_stats Leaf
js_inline_arena_slow_alloc AllocOnly
js_inline_arena_state Leaf
js_install_global_value_surfaces Leaf
Expand Down
2 changes: 0 additions & 2 deletions crates/perry-codegen/src/gc_effects/windows-x86_64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1455,8 +1455,6 @@ js_import_meta_resolve Reenters
js_import_meta_resolve_value Reenters
js_in_operator Reenters
js_in_operator_presence_ic Reenters
js_inherited_read_cache_hit_f64 Leaf
js_inherited_read_cache_stats Leaf
js_inline_arena_slow_alloc AllocOnly
js_inline_arena_state Leaf
js_install_global_value_surfaces Leaf
Expand Down
1 change: 0 additions & 1 deletion crates/perry-codegen/src/root_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ const NON_COLLECTING: &[&str] = &[
"js_write_barrier_root_nanbox",
"perry_transition_cache_base",
"js_transition_ic_note_hit",
"js_inherited_read_cache_hit_f64",
// S2 GC-leaf IC hits; proven `Leaf` by the generated call-effects table.
"js_object_get_field_ic_fast",
// First-read D3: a generic read's miss front, proven `Leaf` likewise.
Expand Down
10 changes: 4 additions & 6 deletions crates/perry-codegen/src/runtime_decls/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ pub fn declare_phase_b_objects(module: &mut LlModule) {
// The key-add hit's chain-verdict generation and the store census
// (expr/put_value_store_ic.rs, expr/store_census.rs).
module.add_external_global("PERRY_PROTO_VALIDITY", I64);
// Sticky worker-start gate for process-global method sites. After it
// becomes nonzero, emitted sites use ordinary dispatch without touching
// primary-heap holder entries.
module.add_external_global("PERRY_METHOD_SITE_WORKERS_PRESENT", I8);
module.add_external_global("PERRY_STORE_CENSUS", I64);
// #10943: has ANY named property ever been installed on a non-ordinary
// cell in this process? Zero is the own-override guard's own proof that a
Expand Down Expand Up @@ -632,12 +636,6 @@ 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]);
// The per-agent pointer block (`expr/agent_ptr.rs`), read inline on ELF
// executables through the initial-exec TLS model; its slot-1 accessor,
// and the method-call site's miss entry (`expr/method_site.rs`).
Expand Down
2 changes: 0 additions & 2 deletions crates/perry-codegen/src/wasm32/runtime_abi.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1676,8 +1676,6 @@ js_import_meta_resolve f64 f64,f64,f64
js_import_meta_resolve_value f64 f64
js_in_operator f64 f64,f64
js_in_operator_presence_ic f64 f64,f64,ptr
js_inherited_read_cache_hit_f64 f64 ptr,ptr
js_inherited_read_cache_stats f64 i32s
js_inline_arena_slow_alloc ptr ptr,usize,usize
js_inline_arena_state ptr
js_install_global_value_surfaces void
Expand Down
6 changes: 0 additions & 6 deletions crates/perry-runtime/src/gc/dead_owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,6 @@ pub(super) const DEAD_KEY_PRUNES: &[DeadKeyPrune] = &[
// it needs a death story of its own -- otherwise a recycled holder address
// becomes a false hit that reads a live object's slot for the wrong key
// (rewrite_raw_addr's #8174 note).
DeadKeyPrune {
table: "INHERITED_READ_CACHE",
owner: DeadKeyOwner::Any,
prune: crate::object::inherited_read_cache::prune_dead_inherited_cache_entries,
young_prune: None,
},
// #6759 C1: shape records are keyed on keys_array addresses; drop the
// ones whose keys_array died (memory only — per-hit validation covers
// correctness for anything this misses).
Expand Down
11 changes: 3 additions & 8 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,17 +1067,12 @@ pub fn gc_init() {
// or Proxy trap can re-enter after moving GC. Rewrite that temporary
// identity so malformed prototype cycles remain bounded.
reg_scanner!(crate::object::prototype_chain::scan_prototype_resolution_stack_roots_mut,);
// Lane 3: the inherited-read cache records a holder ADDRESS per entry and
// a hit LOADS through it, so the slots are STRONG roots: marked, so the
// address cannot be recycled under the entry, and rewritten, so a
// compacting or copying pass leaves it pointing at the same object.
reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);
// Inherited-access lane: a store site's chain verdict names its interned
// A store site's chain verdict names its interned
// key and the receiver's recorded prototype, and compares them on every
// use, so both are STRONG roots (`object::chain_store`).
reg_scanner!(crate::object::chain_store::scan_chain_store_roots_mut);
// Method-calls lane: an inherited method-site entry holds the method
// closure it calls, so the closure is a STRONG root (`object::method_site`).
reg_scanner!(crate::proxy::scan_setter_site_roots_mut);
// An inherited method-site entry roots its direct prototype holder.
reg_scanner!(crate::object::method_site::scan_method_site_roots_mut);
// A read site's holder entry names the object that holds the answer (and
// the hops to it); the emitted hit loads through it, so each is a STRONG
Expand Down
Loading
Loading