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
11 changes: 11 additions & 0 deletions changelog.d/11902-class-method-lookup-shapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
A by-name method call on a class instance (an untyped receiver, a computed
key `obj[k]()`, or a compiled class-method arm's miss edge) now finds the
method on the instance's prototype chain by shapes: each prototype's key list
names the holder and the slot, and the slot's ConstFn lane names the body. The
runtime's per-(class id, name) dispatch caches `VTABLE_IC` and
`OBJ_DISPATCH_IC` are deleted. A computed-key call site and a compiled arm's
miss edge keep a chain memo of the walk that answered them (the receiver's
word, every prototype's word, the holder's slot), compared word by word on
every use: `obj[key]()` drops from about 1,275 to 753-800 instructions per
call at holder depth 1-6, and the #10507 `decimal_class` row from 1,447 to
785.
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/gc_effects/linux-x86_64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1766,8 +1766,10 @@ js_native_call_method_nullsafe Reenters
js_native_call_method_patched_proto Reenters
js_native_call_method_patched_proto_apply Reenters
js_native_call_method_str_key Reenters
js_native_call_method_str_key_memo Reenters
js_native_call_method_value Reenters
js_native_call_method_value_apply Reenters
js_native_call_method_value_memo Reenters
js_native_call_value Reenters
js_native_handle_dispose Reenters
js_native_handle_mark_main_thread Leaf
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/gc_effects/macos-aarch64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1764,8 +1764,10 @@ js_native_call_method_nullsafe Reenters
js_native_call_method_patched_proto Reenters
js_native_call_method_patched_proto_apply Reenters
js_native_call_method_str_key Reenters
js_native_call_method_str_key_memo Reenters
js_native_call_method_value Reenters
js_native_call_method_value_apply Reenters
js_native_call_method_value_memo Reenters
js_native_call_value Reenters
js_native_handle_dispose Reenters
js_native_handle_mark_main_thread Leaf
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/gc_effects/windows-x86_64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1764,8 +1764,10 @@ js_native_call_method_nullsafe Reenters
js_native_call_method_patched_proto Reenters
js_native_call_method_patched_proto_apply Reenters
js_native_call_method_str_key Reenters
js_native_call_method_str_key_memo Reenters
js_native_call_method_value Reenters
js_native_call_method_value_apply Reenters
js_native_call_method_value_memo Reenters
js_native_call_value Reenters
js_native_handle_dispose Reenters
js_native_handle_mark_main_thread Leaf
Expand Down
38 changes: 38 additions & 0 deletions crates/perry-codegen/src/lower_call/direct_method_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,44 @@ pub(crate) fn emit_direct_method_site_word(ctx: &mut FnCtx<'_>) -> String {
format!("@{slot_name}")
}

/// [`emit_direct_method_site_word`] followed by the site's chain memo slot
/// (null, see [`emit_chain_memo_slot`]): `{ i64 word, ptr memo }`. The
/// emitted code reads only the word; the miss edge
/// (`js_native_call_method_by_id_learn`) finds the memo slot next to it.
pub(crate) fn emit_direct_method_site_word_with_memo(ctx: &mut FnCtx<'_>) -> String {
let site_id = ctx.ic_site_counter;
ctx.ic_site_counter += 1;
let prefix = ctx.strings.module_prefix();
let slot_name = if prefix.is_empty() {
format!("perry_mdirect_site_{site_id}")
} else {
format!("perry_mdirect_site_{prefix}__{site_id}")
};
ctx.typed_parse_rodata.push(format!(
"@{slot_name} = private global {{ i64, ptr }} {{ i64 -1, ptr null }}, align 8"
));
format!("@{slot_name}")
}

/// One pointer owned by a by-name method call site: the slot of its chain
/// memo (`perry-runtime` `object/method_site/chain_memo.rs`), which the
/// runtime allocates on the site's first class-method answer and which
/// records the prototype walk that answered it, validated by ShapeId on every
/// use. Starts null; emitted code never reads it.
pub(crate) fn emit_chain_memo_slot(ctx: &mut FnCtx<'_>) -> String {
let site_id = ctx.ic_site_counter;
ctx.ic_site_counter += 1;
let prefix = ctx.strings.module_prefix();
let slot_name = if prefix.is_empty() {
format!("perry_cmemo_{site_id}")
} else {
format!("perry_cmemo_{prefix}__{site_id}")
};
ctx.typed_parse_rodata
.push(format!("@{slot_name} = private global ptr null, align 8"));
format!("@{slot_name}")
}

/// `i1`: `recv_box` is a heap object of `GC_TYPE_OBJECT`, not forwarded, whose
/// exact receiver word equals the site's learned word. Emits
/// its own pointer gate, so it is safe for any value.
Expand Down
8 changes: 6 additions & 2 deletions crates/perry-codegen/src/lower_call/early_branches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,16 @@ pub fn try_lower_index_get_call(
};
let recv_box = group.reread(ctx, recv_idx)?;
let (args_ptr, args_len) = build_dispatch_args_buffer(ctx, group, &arg_idxs)?;
let memo_slot = super::direct_method_guard::emit_chain_memo_slot(ctx);
return Ok(Some(ctx.block().call(
DOUBLE,
"js_native_call_method_str_key",
"js_native_call_method_str_key_memo",
&[
(DOUBLE, &recv_box),
(I64, &name_handle),
(crate::types::PTR, &args_ptr),
(I64, &args_len),
(crate::types::PTR, &memo_slot),
],
)));
}
Expand All @@ -320,14 +322,16 @@ pub fn try_lower_index_get_call(
let recv_box = group.reread(ctx, recv_idx)?;
let key_box = group.reread(ctx, key_idx)?;
let (args_ptr, args_len) = build_dispatch_args_buffer(ctx, group, &arg_idxs)?;
let memo_slot = super::direct_method_guard::emit_chain_memo_slot(ctx);
Ok(Some(ctx.block().call(
DOUBLE,
"js_native_call_method_value",
"js_native_call_method_value_memo",
&[
(DOUBLE, &recv_box),
(DOUBLE, &key_box),
(crate::types::PTR, &args_ptr),
(I64, &args_len),
(crate::types::PTR, &memo_slot),
],
)))
});
Expand Down
15 changes: 10 additions & 5 deletions crates/perry-codegen/src/lower_call/method_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use crate::native_value::LoweredValue;
use crate::types::{DOUBLE, I1, I32, I64, I8};

use super::direct_method_guard::{
emit_direct_method_site_word, emit_inline_direct_method_shape_guard, emit_learned_word_hit,
emit_direct_method_site_word, emit_direct_method_site_word_with_memo,
emit_inline_direct_method_shape_guard, emit_learned_word_hit,
};

pub(super) const POINTER_TAG_HI16: &str = "32765"; // 0x7FFD
Expand Down Expand Up @@ -1042,7 +1043,7 @@ pub(super) fn emit_guarded_direct_method_call(
|| typed_string_direct_fn.is_some());
let learned_site: Option<(String, usize)> =
(inline_single_arm || probe_before_runtime_guard || multi_arm).then(|| {
let word = emit_direct_method_site_word(ctx);
let word = emit_direct_method_site_word_with_memo(ctx);
(word, ctx.new_block("method_direct.learned"))
});
let learned_label = learned_site.as_ref().map(|(_, idx)| ctx.block_label(*idx));
Expand Down Expand Up @@ -1898,11 +1899,15 @@ pub(super) fn emit_guarded_direct_method_call(
// The learned word is consulted only behind the prototype guard
// bytes, so while they are set (a prototype member of this
// method's name was assigned, deleted or redefined) nothing the
// runtime could learn would ever be read: the miss edge passes
// no site, and the runtime dispatches without proving anything.
// runtime could learn would ever be read: the miss edge then
// passes the site's address tagged with bit 0, and the runtime
// learns nothing. Either way the word is followed by the site's
// chain memo slot, from which a receiver the arms decline repeats
// its by-name answer instead of walking the prototype chain.
let blk = ctx.block();
let prototype_ok = emit_prototype_method_guard_ok(blk, &method_guard_slot_str);
let site = blk.select(I1, &prototype_ok, crate::types::PTR, word, "null");
let no_learn = blk.gep(I8, word, &[(I64, "1")]);
let site = blk.select(I1, &prototype_ok, crate::types::PTR, word, &no_learn);
ctx.block().call(
DOUBLE,
"js_native_call_method_by_id_learn",
Expand Down
25 changes: 16 additions & 9 deletions crates/perry-codegen/src/runtime_decls/strings_part2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,15 +918,10 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
DOUBLE,
&[DOUBLE, I64, PTR, I64],
);
// Miss edge of a class-method site that keeps a learned receiver word
// (`lower_call/method_override.rs`): learns, then dispatches as above.
module.declare_function(
"js_native_call_method_by_id_learn",
DOUBLE,
&[DOUBLE, I64, PTR, I64, PTR, I32],
);
// Miss edge of a class-method site that keeps a learned receiver word
// (): learns, then dispatches as above.
// Miss edge of a class-method site (`lower_call/method_override.rs`):
// learns the receiver word (unless the site address is tagged with bit
// 0), then dispatches as above with the chain memo slot that follows the
// word.
module.declare_function(
"js_native_call_method_by_id_learn",
DOUBLE,
Expand All @@ -953,6 +948,18 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
DOUBLE,
&[DOUBLE, I64, PTR, I64],
);
// The computed-key call site's forms that pass its chain memo slot
// (`lower_call/early_branches.rs`).
module.declare_function(
"js_native_call_method_str_key_memo",
DOUBLE,
&[DOUBLE, I64, PTR, I64, PTR],
);
module.declare_function(
"js_native_call_method_value_memo",
DOUBLE,
&[DOUBLE, DOUBLE, PTR, I64, PTR],
);
// #321: dispatch obj[key](args) for a runtime-value key (not statically a
// string). Binds `this = obj` for any key type — string keys go through
// the full dispatch tower, symbol/other keys read the property then call
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/wasm32/runtime_abi.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1987,8 +1987,10 @@ js_native_call_method_nullsafe f64 f64,ptr,usize,ptr,usize
js_native_call_method_patched_proto f64 f64,ptr,usize,ptr,usize
js_native_call_method_patched_proto_apply f64 f64,ptr,usize,i64
js_native_call_method_str_key f64 f64,i64,ptr,usize
js_native_call_method_str_key_memo f64 f64,i64,ptr,usize,ptr
js_native_call_method_value f64 f64,f64,ptr,usize
js_native_call_method_value_apply f64 f64,f64,i64
js_native_call_method_value_memo f64 f64,f64,ptr,usize,ptr
js_native_call_value f64 f64,i64,ptr,usize
js_native_handle_dispose i32s f64
js_native_handle_mark_main_thread void
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/closure/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ pub use calln::{
};
pub use direct::{DirectCall1, DirectCall2, DirectCall3, DirectCall4};

pub(crate) use value_call::{call_compiled_closure_this, native_call_value_this};
pub(crate) use value_call::{
call_compiled_body_this, call_compiled_closure_this, native_call_value_this,
};
pub use value_call::{
js_closure_call_apply_with_spread, js_closure_call_array, js_native_call_value,
};
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-runtime/src/closure/dispatch/value_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ pub(crate) unsafe fn call_compiled_closure_this(
call_closure_body(closure, info, func_ptr, this, args_ptr, args_len)
}

/// [`call_compiled_closure_this`] for a caller that already holds the
/// closure's body `info` (a shape's ConstFn lane names it): no info read or
/// validation of the closure cell.
///
/// # Safety
/// `closure` is a live closure of `info`, a compiled body (`FN_COMPILED_BODY`);
/// `args_ptr` holds `args_len` values.
#[inline]
pub(crate) unsafe fn call_compiled_body_this(
closure: *const ClosureHeader,
info: &'static crate::closure::JsFunctionInfo,
this: crate::closure::JsThis,
args_ptr: *const f64,
args_len: usize,
) -> f64 {
call_closure_body(closure, Some(info), info.code, this, args_ptr, args_len)
}

/// The arity-padding / rest-bundling tail of a value call, once the callee is
/// known to be a closure with a body.
#[inline(always)]
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/closure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ pub(crate) use dispatch::{
coerce_call_this, rebind_explicit_this, rebind_explicit_this_allocates,
reify_function_method_value, reset_throw_not_callable_counter,
};
pub(crate) use dispatch::{call_compiled_closure_this, native_call_value_this};
pub(crate) use dispatch::{
call_compiled_body_this, call_compiled_closure_this, native_call_value_this,
};
pub use dispatch::{
clean_closure_ptr, dispatch_bound_function, dispatch_bound_method, get_valid_func_ptr,
get_valid_info, js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11,
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,10 @@ pub fn gc_init() {
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 site's chain memo names every prototype from the receiver's
// [[Prototype]] to the holder of the method it answers; it compares each
// one's header word on use, so each is a STRONG root.
reg_scanner!(crate::object::method_site::chain_memo::scan_chain_memo_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
// root (`object::method_site::read_holder`).
Expand Down
17 changes: 8 additions & 9 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
//! registers its methods, getters, and setters at startup;
//! `js_native_call_method` / `js_dynamic_object_get_property` look up
//! the vtable by the object's `class_id` when static dispatch isn't
//! possible. Also home for the per-callsite inline cache
//! (`vtable_ic_*` / `call_vtable_method`) and the parent-chain
//! registration helpers used by codegen.
//! possible. Also home for `call_vtable_method` and the parent-chain
//! registration helpers used by codegen. A by-name call of a class
//! instance's string-keyed method does not consult the vtable: its
//! prototype chain's shapes answer it (`native_call_method::class_holder`).
//!
//! Split out of `object/mod.rs` (issue #1103). Pure relocation — no
//! logic changes.
Expand Down Expand Up @@ -123,10 +124,9 @@ pub use state::{
pub(crate) use prototype_objects::{
class_decl_prototype_relinked, class_prototype_object, decl_prototype_relinked,
ensure_function_prototype_object, function_class_id, function_value_for_class_id,
instance_class_prototype_object, object_proto_chain_symbol_slot, relinked_class_prototype_read,
resolve_proto_chain_field, resolve_proto_chain_field_noting_miss,
resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol,
synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE,
object_proto_chain_symbol_slot, relinked_class_prototype_read, resolve_proto_chain_field,
resolve_proto_chain_field_noting_miss, resolve_proto_chain_field_with_receiver,
resolve_proto_chain_symbol, synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE,
};
pub use prototype_objects::{
js_set_function_prototype, js_set_prototype_property, NEXT_SYNTHETIC_CLASS_ID,
Expand Down Expand Up @@ -229,8 +229,7 @@ pub(crate) use dispatch::test_bump_vtable_generation;
pub(crate) use dispatch::{
call_vtable_method, call_vtable_method_value, call_vtable_method_with_private_brand,
class_lookup_surface_gen_bump, class_lookup_surface_generation, fetch_parent_kind_in_chain,
obj_dispatch_ic_insert, obj_dispatch_ic_lookup, vtable_generation, vtable_ic_insert,
vtable_ic_lookup, VTABLE_GEN,
vtable_generation, VTABLE_GEN,
};

// ── parent_static.rs ────────────────────────────────────────────────────────
Expand Down
Loading
Loading