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
9 changes: 9 additions & 0 deletions changelog.d/12986-prototype-getter-site-memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Repeated property reads can reuse a per-site shape/slot answer for own and inherited getters or data properties, including fixed-name Rust runtime sites and short computed string keys. Every use checks the live receiver and prototype shapes and loads the current holder slot. Getter redefinition, shadowing, prototype swaps, holes and Proxy chains retain ordinary property semantics.

The proofs contain shared atomic integer words in ordinary BSS. Process-global, never-reused ShapeIds validate each slot while readers follow their live links; racing publication can cause a miss without a validity flag, epoch or cached heap pointer. Four computed-key ways bind their content keys once. Unused sites require no per-thread clearing or eager initialization.

Named leaves pass four arguments and load the atom's existing hash only when needed. A short own-data hit reads neither hash nor memo. Computed reads share receiver handling with the complete slow path; proven Symbol keys have no short-string memo. Indexed collisions and unproved slot loads use shared tail continuations. A collision can use a validated memo before further traversal; unanswered collisions reconstruct their cursor without extending the ordinary hit's register lifetime.

Array element addressing cancels the header offset in word units before scaling. It preserves allocation padding, front offsets and named-property reserves while reducing repeated address arithmetic in property reads and shared callers.

Named memo hash reads use the shared string accessor with an explicit debug assertion of the canonical-atom contract. The thread-exit inventory records the memo as address-free shape/slot storage.
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ pub(super) fn compile_closure(
let mut ctx = FnCtx {
program_has_worker: cross_module.program_has_worker,
program_has_thread_agents: cross_module.program_has_thread_agents,
program_class_accessor_names: cross_module.program_class_accessor_names.clone(),
func: lf,
module_slug: crate::expr::native_region_slug(strings.module_prefix()),
source_function: format!("closure_{}", func_id),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ pub(super) fn compile_module_entry(
let mut ctx = FnCtx {
program_has_worker: cross_module.program_has_worker,
program_has_thread_agents: cross_module.program_has_thread_agents,
program_class_accessor_names: cross_module.program_class_accessor_names.clone(),
func: main,
module_slug: crate::expr::native_region_slug(strings.module_prefix()),
source_function: "module_init".to_string(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/entry/module_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ pub(super) fn compile_module_init(
let mut ctx = FnCtx {
program_has_worker: cross_module.program_has_worker,
program_has_thread_agents: cross_module.program_has_thread_agents,
program_class_accessor_names: cross_module.program_class_accessor_names.clone(),
func: init_fn,
module_slug: crate::expr::native_region_slug(strings.module_prefix()),
source_function: "module_init".to_string(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,7 @@ pub(super) fn compile_function(
let mut ctx = FnCtx {
program_has_worker: cross_module.program_has_worker,
program_has_thread_agents: cross_module.program_has_thread_agents,
program_class_accessor_names: cross_module.program_class_accessor_names.clone(),
func: lf,
module_slug: crate::expr::native_region_slug(strings.module_prefix()),
source_function: f.name.clone(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ pub(super) fn compile_method(
let mut ctx = FnCtx {
program_has_worker: cross_module.program_has_worker,
program_has_thread_agents: cross_module.program_has_thread_agents,
program_class_accessor_names: cross_module.program_class_accessor_names.clone(),
func: lf,
module_slug: crate::expr::native_region_slug(strings.module_prefix()),
source_function: format!("{}.{}", class.name, method.name),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/method_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ pub(in crate::codegen) fn compile_static_method(
let mut ctx = FnCtx {
program_has_worker: cross_module.program_has_worker,
program_has_thread_agents: cross_module.program_has_thread_agents,
program_class_accessor_names: cross_module.program_class_accessor_names.clone(),
func: lf,
module_slug: crate::expr::native_region_slug(strings.module_prefix()),
source_function: format!("{}.{}", class.name, f.name),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2930,6 +2930,7 @@ fn lower_module(
imported_async_funcs: opts.imported_async_funcs,
short_spread_method_candidates: Arc::clone(&opts.short_spread_method_candidates),
object_literal_method_candidates: Arc::clone(&opts.object_literal_method_candidates),
program_class_accessor_names: opts.program_class_accessor_names.clone(),
local_async_funcs,
local_generator_funcs,
fixed_generator_funcs,
Expand Down
23 changes: 22 additions & 1 deletion crates/perry-codegen/src/codegen/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ pub fn namespace_member_func_key(namespace: &str, member: &str) -> String {
pub struct ClassAccessorNames {
getters: std::collections::HashSet<String>,
setters: std::collections::HashSet<String>,
/// Getter names an object literal or a literal-keyed
/// `Object.defineProperty` declares. They never widen [`Self::may_get`];
/// they only give a read of that name its shape/slot memo.
literal_getters: std::collections::HashSet<String>,
}

mod computed_names;
Expand Down Expand Up @@ -157,6 +161,14 @@ impl ClassAccessorNames {
self.getters.contains(name)
}

/// May any getter of the program be declared under `name` (a class, an
/// object literal or a literal-keyed `Object.defineProperty`)? Only such
/// a fixed-name read owns a shape/slot memo; every other read keeps the
/// stateless probe and complete read.
pub fn may_read_through_getter(&self, name: &str) -> bool {
self.getters.contains(name) || self.literal_getters.contains(name)
}

/// May a compiled class declare a setter named `name`?
pub fn may_set(&self, name: &str) -> bool {
self.setters.contains(name)
Expand All @@ -166,9 +178,16 @@ impl ClassAccessorNames {
pub fn cache_key(&self) -> String {
let mut getters: Vec<&str> = self.getters.iter().map(String::as_str).collect();
let mut setters: Vec<&str> = self.setters.iter().map(String::as_str).collect();
let mut literal: Vec<&str> = self.literal_getters.iter().map(String::as_str).collect();
getters.sort_unstable();
setters.sort_unstable();
format!("get:{}|set:{}", getters.join(","), setters.join(","))
literal.sort_unstable();
format!(
"get:{}|set:{}|lit:{}",
getters.join(","),
setters.join(","),
literal.join(",")
)
}
}

Expand Down Expand Up @@ -970,6 +989,8 @@ pub(crate) struct CrossModuleCtx {
std::sync::Arc<std::collections::HashMap<String, Vec<ShortSpreadMethodCandidate>>>,
pub object_literal_method_candidates:
std::sync::Arc<std::collections::HashMap<String, Vec<ObjectLiteralMethodCandidate>>>,
/// The whole program class accessor names, carried from the compile options.
pub program_class_accessor_names: Option<std::sync::Arc<ClassAccessorNames>>,
/// FuncIds of locally-defined async functions in this module. Populated
/// from `hir.functions.is_async`. Used by `is_promise_expr` to refine
/// `let p = asyncFn();` to `Promise(_)` so subsequent `p.then(cb)`
Expand Down
19 changes: 18 additions & 1 deletion crates/perry-codegen/src/codegen/opts/computed_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct Facts {
returns: HashMap<u32, Vec<Expr>>,
properties: Vec<(String, Expr)>,
accessors: Vec<(Expr, bool, bool)>,
literal_getters: Vec<String>,
class_fields: HashMap<String, Vec<String>>,
visited_functions: HashSet<u32>,
}
Expand Down Expand Up @@ -120,14 +121,27 @@ impl Facts {
.extend(fields.iter().cloned().zip(args.iter().cloned()));
}
}
Expr::ObjectDefineProperty(_, key, descriptor) => {
if let (Expr::String(key), Expr::Object(fields)) =
(key.as_ref(), descriptor.as_ref())
{
if fields.iter().any(|(field, _)| field == "get") {
self.literal_getters.push(key.clone());
}
}
}
Expr::Call { callee, args, .. } => {
if let Expr::ExternFuncRef { name, .. } = callee.as_ref() {
if name == "js_object_define_accessor" && args.len() == 4 {
// Named object accessors already keep their existing
// path. Extend admission for computed expressions;
// widening unrelated named object reads adds miss
// guards without helping the computed-key workload.
if !matches!(args[1], Expr::String(_)) {
if let Expr::String(key) = &args[1] {
if !matches!(args[2], Expr::Undefined) {
self.literal_getters.push(key.clone());
}
} else {
self.accessors.push((
args[1].clone(),
!matches!(args[2], Expr::Undefined),
Expand Down Expand Up @@ -248,6 +262,9 @@ pub(super) fn collect<'m>(modules: impl IntoIterator<Item = &'m Module>) -> Clas
properties = next;
}
for f in &facts {
names
.literal_getters
.extend(f.literal_getters.iter().cloned());
for (key, get, set) in &f.accessors {
let candidates = f.names(key, &properties, 0);
if *get {
Expand Down
10 changes: 8 additions & 2 deletions crates/perry-codegen/src/expr/barrier_stem_census_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1402,8 +1402,14 @@ fn generator_fixed_slot_store_uses_the_shared_barrier_operation() {
"no per-site barrier scaffold"
);
assert!(
(ir.contains("call double @js_object_get_field_value(")
|| ir.contains("call double @js_object_get_field_slow(")),
[
"js_object_get_field_value",
"js_object_get_field_slow",
"js_object_get_field_value_site",
"js_object_get_field_site_slow",
]
.iter()
.any(|exit| ir.contains(&format!("call double @{exit}("))),
"generic continuation reads call the complete collecting operation"
);
assert!(
Expand Down
135 changes: 135 additions & 0 deletions crates/perry-codegen/src/expr/ic_fast_split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,138 @@ pub(crate) fn emit_checked_callee_unbox(ctx: &mut FnCtx<'_>, callee: &str) -> St
],
)
}

/// Does a fixed-name read of `property` own a shape/slot memo? Only a name
/// some getter of the program is declared under can reach the collecting
/// exit the memo serves; any other read keeps the stateless probe and owns
/// no storage. A compile without the program's accessor names gives every
/// read its memo.
pub(crate) fn read_may_reach_getter(ctx: &FnCtx<'_>, property: &str) -> bool {
ctx.program_class_accessor_names
.as_ref()
.map_or(true, |names| names.may_read_through_getter(property))
}

/// Shared atomic storage for a site's shape/slot answers. No heap pointer or
/// validity state is stored, so these zero-filled integer words need no root.
pub(crate) fn emit_read_site_memo(ctx: &mut FnCtx<'_>, words: usize) -> String {
let site = ctx.ic_site_counter;
ctx.ic_site_counter += 1;
let name = format!(
"@{}_read_shape_slots",
crate::expr::inline_cache_global_name(ctx, site)
);
ctx.typed_parse_rodata.push(format!(
"{name} = private global [{words} x i64] zeroinitializer, align 8"
));
name
}

#[cfg(test)]
mod tests {

#[test]
fn read_site_symbol_key_owns_no_short_string_memo() {
use perry_hir::{Expr, Module, Stmt};
let mut module = Module::new("read_site_symbol.ts");
module.init.push(Stmt::Expr(Expr::IndexGet {
object: Box::new(Expr::GlobalGet(100)),
index: Box::new(Expr::SymbolFor(Box::new(Expr::String("key".into())))),
}));
let ir = String::from_utf8(
crate::compile_module(
&module,
crate::CompileOptions {
emit_ir_only: true,
..Default::default()
},
)
.unwrap(),
)
.unwrap();
assert!(!ir.contains("_read_shape_slots = private"), "{ir}");
assert!(ir.contains("call double @perry_probe_get_keyed("), "{ir}");
assert!(
ir.contains("call double @js_object_get_keyed_slow("),
"{ir}"
);
assert!(
!ir.contains("call double @js_object_get_keyed_site_slow("),
"{ir}"
);
}
#[test]
fn only_a_declared_getter_name_owns_read_site_storage() {
use perry_hir::{Expr, Module, Stmt};
for (getter, memo) in [("width", true), ("height", false)] {
let mut module = Module::new("read_site_names.ts");
module.init.push(Stmt::Expr(Expr::PropertyGet {
object: Box::new(Expr::GlobalGet(100)),
property: "width".into(),
byte_offset: 0,
}));
let names = crate::codegen::ClassAccessorNames::from_names(
[getter.to_string()],
std::iter::empty::<String>(),
);
let ir = String::from_utf8(
crate::compile_module(
&module,
crate::CompileOptions {
emit_ir_only: true,
program_class_accessor_names: Some(std::sync::Arc::new(names)),
..Default::default()
},
)
.unwrap(),
)
.unwrap();
assert_eq!(ir.contains("_read_shape_slots = private"), memo, "{ir}");
assert_eq!(
ir.contains("call double @perry_probe_get_memo("),
memo,
"{ir}"
);
// Declarations name every entrance; only the calls differ.
let calls = |callee: &str| {
ir.lines()
.any(|l| !l.starts_with("declare") && l.contains(&format!("@{callee}(")))
};
assert_eq!(calls("js_object_get_field_value_site"), memo, "{ir}");
assert_eq!(calls("perry_probe_get"), !memo, "{ir}");
assert_eq!(calls("js_object_get_field_value"), !memo, "{ir}");
}
}

#[test]
fn read_site_storage_never_needs_tls_initialization() {
use perry_hir::{Expr, Module, Stmt};
for (worker, thread_agent) in [(false, false), (true, false), (false, true), (true, true)] {
let mut module = Module::new("read_site_agents.ts");
module.init.push(Stmt::Expr(Expr::PropertyGet {
object: Box::new(Expr::GlobalGet(100)),
property: "width".into(),
byte_offset: 0,
}));
let ir = String::from_utf8(
crate::compile_module(
&module,
crate::CompileOptions {
emit_ir_only: true,
program_has_worker: worker,
program_has_thread_agents: thread_agent,
..Default::default()
},
)
.unwrap(),
)
.unwrap();
assert!(ir.contains("call double @perry_probe_get_memo("), "{ir}");
let declaration = ir
.lines()
.find(|line| line.contains("_read_shape_slots = private"))
.expect("a generic read must own its shape/slot storage");
assert!(!declaration.contains("thread_local"), "{declaration}");
}
}
}
15 changes: 10 additions & 5 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1628,9 +1628,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// layout and its chain answer the key's atom
// (`perry_probe_get_keyed`, a GC leaf); only a decline
// takes the complete keyed read.
Ok(symbol_ic::emit_keyed_probe(ctx, obj_box, key_box, |ctx| {
symbol_ic::keyed_slow(ctx, obj_box, key_box, &site_id, false)
}))
Ok(symbol_ic::emit_keyed_probe(
ctx,
obj_box,
key_box,
|ctx, memo| {
symbol_ic::keyed_site_slow(ctx, obj_box, key_box, &site_id, false, memo)
},
))
});
}
// Last-resort fallback with runtime tag checks on the index.
Expand Down Expand Up @@ -1737,8 +1742,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// takes the by-value read (see the dynamic-string arm above).
let _ = str_obj_handle;
// The probe first (§3.7 case 3), as the dynamic-string arm.
let v_str = symbol_ic::emit_keyed_probe(ctx, &obj_box, &idx_box, |ctx| {
symbol_ic::keyed_slow(ctx, &obj_box, &idx_box, &site_id, false)
let v_str = symbol_ic::emit_keyed_probe(ctx, &obj_box, &idx_box, |ctx, memo| {
symbol_ic::keyed_site_slow(ctx, &obj_box, &idx_box, &site_id, false, memo)
});
let str_end_lbl = ctx.block().label.clone();
ctx.block().br(&merge_lbl);
Expand Down
Loading
Loading