Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
056abd7
perf(runtime): one string per property-key text, so a key confirm is …
Sep 23, 2026
ee4b31a
perf(runtime): confirm a megamorphic site's slot guess against the re…
Sep 26, 2026
e8a6c40
fix(runtime): an atom is key identity, never interned-key eligibility
Sep 27, 2026
e8e5307
docs(changelog): megamorphic reads confirm the slot guess by key atom
Sep 28, 2026
d98497f
changelog: name the fragment after PR #11633
Sep 28, 2026
a875735
Merge remote-tracking branch 'origin/main' into pr11633
Sep 28, 2026
71406b4
fix(runtime): an SSO key slot is its own atom; say so in code for the…
Sep 28, 2026
330dfd6
Merge origin/main into perf-megamorphic-atoms
Sep 28, 2026
805b222
regen: js_string_pool_atom in the wasm ABI table and the linux gc-cal…
Sep 28, 2026
5bfb01c
test(runtime): atoms survive a moving minor via the atom young log
Sep 28, 2026
93aee48
perf(runtime): POSBOUND, the shape record's position bound as one field
Sep 28, 2026
c519ea9
perf: one GC-leaf miss front per generic read site (D3, D3b)
Sep 29, 2026
70c6e2f
changelog: name the fragment after #11657
Sep 29, 2026
06d8f06
Merge PerryTS/main into perf-megamorphic-front
Sep 29, 2026
269c1a0
merge fixups: stack guard knows WindowsTeb; census reads the Slot slab
Sep 29, 2026
cdc9844
Merge commit 'refs/tmp/mf-main' into HEAD
Sep 29, 2026
617b834
rustfmt; say that an in-place rep deprecation leaves POSBOUND as it is
Sep 29, 2026
e7d4f0e
Merge commit 'refs/tmp/fx57-main' into HEAD
Sep 29, 2026
f51aea9
shapes tests: the position-bound rep test passes no static id request
Sep 29, 2026
3158e10
Merge commit 'refs/tmp/m57b-main' into m57b-front
Sep 29, 2026
159470f
lint: thread-exit verdicts for the shared-empty shape statics; drop a…
Sep 29, 2026
b4dcefb
Merge remote-tracking branch 'origin/main' into HEAD
Sep 29, 2026
776412c
shapes tests: the seeded-literal confirm test follows the dir-passing…
Sep 29, 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
11 changes: 11 additions & 0 deletions changelog.d/11657-megamorphic-read-miss-front.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
A generic property read keeps only the ShapeId compare and the slot load
inline. Its miss makes one GC-leaf call, `js_object_get_field_ic_front`, which
answers a polymorphic way, a spill entry, or a latched megamorphic site whose
slot guess the receiver's own shape record confirms (one POSBOUND bound
compare and one key-atom word compare, with a bounded second chance over the
first 32 positional keys that re-aims the guess). Only what the front declines
reaches the collecting slow entry, which also asks the inherited-read cache
for a never-primed site. Nothing is spilled or relocated across the front
call, and the site reads its agent's shape directory without a call on ELF
executables, Windows x86-64 and Apple aarch64. The shape record grows from 40
to 48 bytes; tsc's `.text` shrinks by 9%.
9 changes: 6 additions & 3 deletions crates/perry-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ pub const STATIC_SHAPE_ID_COUNT: u32 = 1 << 20;
pub const ARRAY_HEADER_SIZE: usize = 8;

/// `agent_ptrs::PERRY_AGENT_PTRS`: the number of per-agent pointer slots.
/// Slot 0 is reserved (the megamorphic follow-up's shape-record directory);
/// slot 1 held the implicit-`this` cell's address until this-as-a-parameter
/// deleted the cell, and is free; slot 2 is the stack limit.
pub const AGENT_PTR_SLOTS: usize = 4;
/// Slot 0: the address of this agent's ordinary shape-directory mirror
/// (`shapes_store::ORDINARY_DIR`), which a generic read site passes to its
/// GC-leaf miss front (`js_object_get_field_ic_front`) so the front reads no
/// thread-local. Slot 1 held the implicit-`this` cell's address until
/// this-as-a-parameter deleted the cell, and is free; slot 2 is the stack limit.
pub const AGENT_PTR_SHAPE_DIR: usize = 0;
/// Slot 2: this agent's stack limit (#10812) — not a pointer to anything, the
/// lowest frame address a compiled prologue accepts before it throws
/// `RangeError: Maximum call stack size exceeded`. Null means unchecked.
Expand Down
129 changes: 120 additions & 9 deletions crates/perry-codegen/src/expr/agent_ptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,22 @@
//! model (every thread-local access is a TLV thunk call), so the block's
//! address is read from the runtime's `HotTls` cache through the pthread
//! TSD fast path (`hot_tls.rs`), at `HOT_TLS_AGENT_PTRS_OFFSET`.
//! * [`AgentPtrAccess::Call`] — everything else (Windows, wasm, arm64_32,
//! x86-64 Darwin, dylib/staticlib outputs): the runtime accessor.
//! * [`AgentPtrAccess::WindowsTeb`] — Windows x86-64, any output kind: the
//! same sequence the compiler emits for a native thread-local, spelled out
//! because the runtime cannot export the block under a stable name there
//! (`agent_ptrs.rs`): `gs:[0x58]` (the TEB's `ThreadLocalStoragePointer`)
//! indexed by the image's `_tls_index` gives this thread's TLS block for
//! the image, and the block sits at `PERRY_AGENT_PTRS_SECREL` (a `.secrel32`
//! the runtime emits for its own static) inside it. Emitted code and the
//! runtime are linked into ONE image, so `_tls_index` is theirs.
//! * [`AgentPtrAccess::Call`] — everything else (wasm, arm64_32, Windows
//! aarch64, x86-64 Darwin, ELF dylib/staticlib outputs): the runtime
//! accessor. x86-64 Darwin has no call-free thread-local model (every
//! Mach-O thread-local access is a TLV thunk call) and the runtime's
//! pthread-TSD fast path (`HotTls`, the Apple aarch64 route) is built for
//! aarch64 only.
//!
//! In both inline forms a null slot means "not published yet" and takes the
//! In every inline form a null slot means "not published yet" and takes the
//! accessor call, which publishes it; so the inline forms and the call are
//! equivalent by construction.

Expand All @@ -35,10 +47,18 @@ pub(crate) const AGENT_PTRS_SYMBOL: &str = "PERRY_AGENT_PTRS";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AgentPtrAccess {
InitialExec,
WindowsTeb,
AppleTsd,
Call,
}

/// `PERRY_AGENT_PTRS`'s offset in the image's TLS block on Windows x86-64
/// (`agent_ptrs.rs`), and the image's TLS index.
pub(crate) const AGENT_PTRS_SECREL_SYMBOL: &str = "PERRY_AGENT_PTRS_SECREL";
pub(crate) const TLS_INDEX_SYMBOL: &str = "_tls_index";
/// `NT_TIB64`/`TEB64.ThreadLocalStoragePointer`, off `gs`.
const TEB_TLS_POINTER_OFFSET: &str = "88";

thread_local! {
/// Whether the module being compiled is linked into an EXECUTABLE (set per
/// module by `codegen::compile_module`); anything else must not assume
Expand All @@ -62,12 +82,107 @@ pub(crate) fn agent_ptr_access(ctx: &FnCtx<'_>) -> AgentPtrAccess {
if elf && OUTPUT_IS_EXECUTABLE.with(|c| c.get()) {
return AgentPtrAccess::InitialExec;
}
if triple.starts_with("x86_64") && triple.contains("windows") {
return AgentPtrAccess::WindowsTeb;
}
if super::hot_tls::inline_hot_tls_enabled(ctx) {
return AgentPtrAccess::AppleTsd;
}
AgentPtrAccess::Call
}

/// The current value of per-agent pointer `slot`, for a GC-leaf callee that
/// accepts `absent` (a constant operand meaning "not available here") in its
/// place: one initial-exec load in an ELF executable (the slot must never be
/// null there); the `HotTls` read on Apple aarch64, `absent` when the direct
/// TSD path is unavailable or the block is not published; the slot's `gc-leaf`
/// runtime accessor everywhere else. No null test and no fallback call on the
/// inline forms, so a site pays only the read. Ends in the block where the
/// returned register holds the value.
pub(crate) fn emit_agent_ptr_or(ctx: &mut FnCtx<'_>, slot: usize, absent: &str) -> String {
debug_assert!(slot < AGENT_PTR_SLOTS);
let slot_off = (slot * 8).to_string();
let access = agent_ptr_access(ctx);
match access {
AgentPtrAccess::InitialExec | AgentPtrAccess::WindowsTeb => {
let at = emit_slot_addr(ctx, access, &slot_off);
ctx.block().load(PTR, &at)
}
AgentPtrAccess::AppleTsd => {
let lookup = super::hot_tls::emit_hot_tls_lookup(ctx, "agent_ptr");
let field = super::hot_tls::hot_tls_field(
ctx,
&lookup.hot,
&HOT_TLS_AGENT_PTRS_OFFSET.to_string(),
);
let blk = ctx.block();
let block_ptr = blk.load(PTR, &field);
let at = blk.gep(
crate::types::I8,
&block_ptr,
&[(crate::types::I64, &slot_off)],
);
let val = blk.load(PTR, &at);
let fast_pred = blk.label.clone();
let join_idx = ctx.new_block("agent_ptr.join");
let join_label = ctx.block_label(join_idx);
ctx.block().br(&join_label);
ctx.current_block = lookup.slow_idx;
let slow_pred = ctx.block().label.clone();
ctx.block().br(&join_label);
ctx.current_block = join_idx;
ctx.block()
.phi(PTR, &[(&val, &fast_pred), (absent, &slow_pred)])
}
AgentPtrAccess::Call => ctx.block().call(PTR, agent_ptr_accessor(slot), &[]),
}
}

/// The address of the slot `slot_off` bytes into this thread's block, for the
/// two forms that name the block through the thread pointer (module docs).
pub(crate) fn emit_slot_addr(
ctx: &mut FnCtx<'_>,
access: AgentPtrAccess,
slot_off: &str,
) -> String {
let blk = ctx.block();
let block = match access {
AgentPtrAccess::InitialExec => format!("@{AGENT_PTRS_SYMBOL}"),
AgentPtrAccess::WindowsTeb => {
// `mov gs:[0x58]` — a plain load in the x86 `gs` address space
// (256). Plain, not volatile: it is re-read after every call, and
// a thread switch happens only inside a call.
let tls_array = blk.next_reg();
blk.emit_raw(format!(
" {tls_array} = load ptr, ptr addrspace(256) inttoptr (i64 {TEB_TLS_POINTER_OFFSET} to ptr addrspace(256)), align 8"
));
let index = blk.load(crate::types::I32, &format!("@{TLS_INDEX_SYMBOL}"));
let index = blk.zext(crate::types::I32, &index, crate::types::I64);
let entry = blk.gep(PTR, &tls_array, &[(crate::types::I64, &index)]);
let image_block = blk.load(PTR, &entry);
let secrel = blk.load(crate::types::I32, &format!("@{AGENT_PTRS_SECREL_SYMBOL}"));
let secrel = blk.zext(crate::types::I32, &secrel, crate::types::I64);
blk.gep(
crate::types::I8,
&image_block,
&[(crate::types::I64, &secrel)],
)
}
AgentPtrAccess::AppleTsd | AgentPtrAccess::Call => {
unreachable!("{access:?} does not name the block through the thread pointer")
}
};
blk.gep(crate::types::I8, &block, &[(crate::types::I64, slot_off)])
}

/// The `gc-leaf` runtime accessor of per-agent pointer `slot`.
fn agent_ptr_accessor(slot: usize) -> &'static str {
match slot {
crate::runtime_abi::AGENT_PTR_SHAPE_DIR => "perry_shape_dir_cell",
_ => unreachable!("agent pointer slot {slot} has no accessor"),
}
}

/// Emit a load of per-agent pointer `slot`, falling back to `fallback_fn`
/// (a `gc-leaf` runtime accessor `() -> ptr` that also publishes it). Ends
/// in a fresh block where the returned register holds the pointer.
Expand All @@ -79,15 +194,11 @@ pub(crate) fn emit_agent_ptr(ctx: &mut FnCtx<'_>, slot: usize, fallback_fn: &str
}
let slot_off = (slot * 8).to_string();
let (fast_pred, fast_val, slow_idx) = match access {
AgentPtrAccess::InitialExec => {
AgentPtrAccess::InitialExec | AgentPtrAccess::WindowsTeb => {
let slow_idx = ctx.new_block("agent_ptr.slow");
let fast_idx = ctx.new_block("agent_ptr.fast");
let at = emit_slot_addr(ctx, access, &slot_off);
let blk = ctx.block();
let at = blk.gep(
crate::types::I8,
&format!("@{AGENT_PTRS_SYMBOL}"),
&[(crate::types::I64, &slot_off)],
);
let val = blk.load(PTR, &at);
let ok = blk.icmp_ne(PTR, &val, "null");
let fast_label = ctx.block_label(fast_idx);
Expand Down
21 changes: 20 additions & 1 deletion crates/perry-codegen/src/expr/property_get/array_length_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,26 @@ fn a_length_read_serves_a_live_plain_array_off_the_shape_compare() {
// cannot heal in one edge — continues exactly where the compare's false
// edge used to go.
let refused = assert_plain_array_arm(&blocks, "pget.array_kind", true);
assert_eq!(strip_suffix(&refused), "pic.token.miss");
assert_eq!(strip_suffix(&refused), "pic.miss.front");

// S6: a `length` site's miss front is handed the runtime's EMPTY
// directory, so its latched edge is never confirmed from the receiver's
// shape. An Array-subclass receiver serves `length` from its elements
// store; the `length` its shape may name is not the answer
// (`read_confirm::tests::a_length_site_is_never_confirmed_from_the_shape`
// is the runtime half).
let front = ir
.lines()
.find(|l| l.contains(" = call double @js_object_get_field_ic_front("))
.unwrap_or_else(|| panic!("expected the miss front call:\n{ir}"));
assert!(
front.contains("@js_object_get_field_ic_front(ptr @PERRY_EMPTY_SHAPE_DIR, "),
"a `length` site must pass the empty directory:\n{front}"
);
assert!(
!ir.contains("ptr @PERRY_AGENT_PTRS, i64 0"),
"a `length` site reads no directory at all:\n{ir}"
);

// The merge takes the arm's value.
let (load_label, load_body) = block(&blocks, "pget.array_length");
Expand Down
Loading
Loading