diff --git a/changelog.d/11667-class-builtin-parent-no-class-object.md b/changelog.d/11667-class-builtin-parent-no-class-object.md
new file mode 100644
index 0000000000..9e4412c059
--- /dev/null
+++ b/changelog.d/11667-class-builtin-parent-no-class-object.md
@@ -0,0 +1,4 @@
+`Object.getPrototypeOf` of a class, a static `super` parent lookup and a
+static `super[k] = v` no longer create a class function object for a builtin
+parent id (`class E extends Error`): the builtin constructor is the class's
+dynamic parent value, and a class without one is a root.
diff --git a/changelog.d/11667-class-registration-by-identity.md b/changelog.d/11667-class-registration-by-identity.md
new file mode 100644
index 0000000000..115742fc48
--- /dev/null
+++ b/changelog.d/11667-class-registration-by-identity.md
@@ -0,0 +1,3 @@
+Fixed class registration being keyed by class name: two classes with the same
+name in one module each register their own methods, static methods, accessors
+and constructors under their own class id.
diff --git a/changelog.d/11667-class-static-accessor-entry-convention.md b/changelog.d/11667-class-static-accessor-entry-convention.md
new file mode 100644
index 0000000000..5469d328f5
--- /dev/null
+++ b/changelog.d/11667-class-static-accessor-entry-convention.md
@@ -0,0 +1,4 @@
+Fixed a class static setter receiving the class instead of the assigned value
+on the generic property path (`C.x = v`, directly, through a variable or on a
+subclass, and `Reflect.set`) and through the reflected setter function
+(`Object.getOwnPropertyDescriptor(C, "x").set`), string- and symbol-keyed.
diff --git a/changelog.d/11667-class-static-attrs-on-keys.md b/changelog.d/11667-class-static-attrs-on-keys.md
new file mode 100644
index 0000000000..65b0fea11c
--- /dev/null
+++ b/changelog.d/11667-class-static-attrs-on-keys.md
@@ -0,0 +1,6 @@
+A class static's attributes (`writable`/`enumerable`/`configurable`, set by
+`Object.defineProperty`, `Object.freeze` or a class's intrinsic `name` and
+`length`) are now the key attributes of the class function object's own
+properties, as for any ordinary object, instead of a separate per-class table.
+Deleting such a static and assigning it again yields an ordinary writable,
+enumerable property (the old table kept the deleted key's attributes).
diff --git a/changelog.d/11667-class-static-call-guard-cheap.md b/changelog.d/11667-class-static-call-guard-cheap.md
new file mode 100644
index 0000000000..501ceea5ec
--- /dev/null
+++ b/changelog.d/11667-class-static-call-guard-cheap.md
@@ -0,0 +1,4 @@
+Made a compiled `C.m()` static call cheaper: the check that the class still
+holds the declared method is now one shape-word read and compare per class
+the call reads (a direct call costs 6 instructions more than an unguarded
+call, down from 17).
diff --git a/changelog.d/11667-class-static-methods-own-properties.md b/changelog.d/11667-class-static-methods-own-properties.md
new file mode 100644
index 0000000000..17acaeb3f9
--- /dev/null
+++ b/changelog.d/11667-class-static-methods-own-properties.md
@@ -0,0 +1,9 @@
+Fixed class static methods not being ordinary own properties of the class.
+A static method, named or computed, is now a writable, non-enumerable,
+configurable data property of the class function object, whose value is the
+method's own function object: `Object.getOwnPropertyDescriptor(C, "m").value
+=== C.m`, `C.m === Sub.m`, and replacing, redefining (`Object.defineProperty`,
+`Reflect.set`, `Object.assign`) or deleting it is seen by every later call,
+including `C.m()` call sites compiled before the change. A deleted static or
+prototype method no longer reappears on read, and `getOwnPropertyNames` lists
+a class's keys in creation order.
diff --git a/changelog.d/11667-class-static-names-fast-hash.md b/changelog.d/11667-class-static-names-fast-hash.md
new file mode 100644
index 0000000000..7da06ea044
--- /dev/null
+++ b/changelog.d/11667-class-static-names-fast-hash.md
@@ -0,0 +1,3 @@
+Class static method and accessor lookups by name hash with ahash instead of
+SipHash, so a static call that misses its site guard probes each class on the
+parent chain faster.
diff --git a/changelog.d/11667-object-intrinsics-standalone.md b/changelog.d/11667-object-intrinsics-standalone.md
new file mode 100644
index 0000000000..3fbc45036e
--- /dev/null
+++ b/changelog.d/11667-object-intrinsics-standalone.md
@@ -0,0 +1,4 @@
+Made the first use of a class cheaper: `Object` and `Object.prototype` are
+built on their own (about 1M instructions) instead of by building the whole
+global object (about 50M instructions, several hundred builtins); the global
+object adopts the same two objects when it is built.
diff --git a/crates/perry-abi/src/lib.rs b/crates/perry-abi/src/lib.rs
index 222d8c90b7..50c6f6fbb0 100644
--- a/crates/perry-abi/src/lib.rs
+++ b/crates/perry-abi/src/lib.rs
@@ -50,6 +50,17 @@ pub const CLOSURE_INFO_OFFSET: usize = 8;
pub const CLOSURE_PROPS_OFFSET: usize = 16;
pub const CLOSURE_HEADER_SIZE: usize = 24;
+/// `object::ObjectHeader::parent_class_id`: the object's ShapeId word (LP64
+/// and ILP32 alike; `CLOSURE_SHAPE_OFFSET` is the same word of a closure).
+pub const OBJECT_SHAPE_OFFSET: usize = 4;
+
+/// `object::class_value::StaticCallMemo` (LP64) — the words the emitted
+/// static-call guard reads (`perry-codegen/src/expr/static_method.rs`).
+pub const STATIC_CALL_MEMO_KEY_OFFSET: usize = 0;
+pub const STATIC_CALL_MEMO_C_OFFSET: usize = 8;
+pub const STATIC_CALL_MEMO_OWNER_OFFSET: usize = 16;
+pub const STATIC_CALL_MEMO_VALUE_OFFSET: usize = 24;
+
/// `gc::GC_TYPE_CLOSURE`: the GcHeader type byte (at payload - 8) that makes a
/// cell a function object. The kind is this byte, never a payload magic.
pub const GC_TYPE_CLOSURE: u8 = 4;
diff --git a/crates/perry-codegen/src/codegen/artifact_source_text.rs b/crates/perry-codegen/src/codegen/artifact_source_text.rs
index 4126144511..ccfd025861 100644
--- a/crates/perry-codegen/src/codegen/artifact_source_text.rs
+++ b/crates/perry-codegen/src/codegen/artifact_source_text.rs
@@ -29,6 +29,7 @@ pub(super) fn extend_class_method_source_text(
.iter()
.map(|(symbol, _, _)| symbol.clone())
.collect();
+ let mut entry_sources: Vec<(String, String, bool)> = Vec::new();
let mut push_defined = |func_id: FuncId, symbol: String| {
let Some(source) = hir.closure_source_text.get(&func_id) else {
return;
@@ -103,27 +104,59 @@ pub(super) fn extend_class_method_source_text(
push_defined(setter.id, symbol);
}
for method in &class.static_methods {
- push_defined(
- method.id,
- scoped_static_method_name(module_prefix, class.id, &class.name, &method.name),
- );
+ let body =
+ scoped_static_method_name(module_prefix, class.id, &class.name, &method.name);
+ // The method's own function object runs `
__clo` (string
+ // pool): its toString is the method's source too.
+ if !method.name.starts_with("__perry_static_init_") && llmod.has_function(&body) {
+ if let Some(source) = hir.closure_source_text.get(&method.id) {
+ entry_sources.push((
+ format!("{body}__clo"),
+ super::function_source_header::retained_function_text(
+ hir,
+ closures,
+ method.id,
+ &source.text,
+ ),
+ source.is_non_strict_ordinary,
+ ));
+ }
+ }
+ push_defined(method.id, body);
}
for member in class
.computed_members
.iter()
.filter(|member| member.is_static)
{
- push_defined(
- member.function.id,
- scoped_static_method_name(
- module_prefix,
- class.id,
- &class.name,
- &member.function.name,
- ),
+ let body = scoped_static_method_name(
+ module_prefix,
+ class.id,
+ &class.name,
+ &member.function.name,
);
+ // A computed-name static method's function object runs
+ // `__clo` too (string pool).
+ if matches!(member.kind, perry_hir::ClassComputedMemberKind::Method)
+ && llmod.has_function(&body)
+ {
+ if let Some(source) = hir.closure_source_text.get(&member.function.id) {
+ entry_sources.push((
+ format!("{body}__clo"),
+ super::function_source_header::retained_function_text(
+ hir,
+ closures,
+ member.function.id,
+ &source.text,
+ ),
+ source.is_non_strict_ordinary,
+ ));
+ }
+ }
+ push_defined(member.function.id, body);
}
}
+ user_fn_source.extend(entry_sources);
}
/// Collect retained `Function.prototype.toString` source text for every user
diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs
index b2e1551c8f..0be569cbfe 100644
--- a/crates/perry-codegen/src/codegen/artifacts.rs
+++ b/crates/perry-codegen/src/codegen/artifacts.rs
@@ -1004,6 +1004,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
class_header_image_inits,
class_ids,
class_table,
+ &hir.classes,
&hir.class_display_names,
&class_source_text,
&ctor_arity_overrides,
diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs
index 3a8ebb4f32..d1bc18823d 100644
--- a/crates/perry-codegen/src/codegen/string_pool.rs
+++ b/crates/perry-codegen/src/codegen/string_pool.rs
@@ -50,6 +50,12 @@ impl<'a> InitChunker<'a> {
/// Start a fresh chunk function if the current one is full. Call ONCE at the
/// top of each loop iteration (one independent init op), before
/// [`current_block`]. Closes the previous chunk with `ret void`.
+ /// The module, for a definition an init op registers (the chunk being
+ /// filled is addressed by index, so appending functions is fine).
+ fn module(&mut self) -> &mut LlModule {
+ self.llmod
+ }
+
fn roll_if_full(&mut self) {
if self.ops_in_current >= self.ops_per_chunk {
if !self.chunk_names.is_empty() {
@@ -124,6 +130,11 @@ pub(super) fn emit_string_pool(
class_header_image_inits: &std::collections::HashMap,
class_ids: &HashMap,
classes: &HashMap,
+ // The classes this module defines, by identity: the registration loops
+ // below (names, methods, static methods and their function-object
+ // entries, constructors, accessors) key each class by its ClassId, never
+ // by a name (`classes` above maps names, and two classes may share one).
+ module_classes: &[perry_hir::Class],
// #5592: user-visible `.name` overrides keyed by ClassId, for classes
// whose HIR registration key was uniquified away from their JS name.
class_display_names: &HashMap,
@@ -340,21 +351,18 @@ pub(super) fn emit_string_pool(
// Pre-allocate string constants for class-name registration. We need
// these BEFORE `init_fn` is created, because once `init_fn` borrows
// `llmod` we can no longer mutate the module's constant pool. (#1021.)
+ // Every class this module defines, keyed by identity (its ClassId).
+ // Imported stubs are not here: the defining module registers them.
+ let local_classes: Vec<(u32, &perry_hir::Class)> = module_classes
+ .iter()
+ .filter(|c| c.id != 0)
+ .map(|c| (c.id, c))
+ .collect();
let mut named_class_name_constants: Vec<(u32, String, usize)> = Vec::new();
{
let mut named: Vec<(u32, String)> = Vec::new();
- for (class_name, class) in classes.iter() {
- // Imported stubs (id == 0) use consumer lookup keys, which may
- // be aliases or synthetic namespace keys. Only the defining
- // module owns the JavaScript display name; an importer must not
- // overwrite it when its string initializer runs.
- if class.id == 0 || *class_name != class.name {
- continue;
- }
- let cid = match class_ids.get(class_name).copied() {
- Some(c) if c != 0 => c,
- _ => continue,
- };
+ for &(cid, class) in &local_classes {
+ let class_name = &class.name;
if !class_name.starts_with("__AnonShape_") {
// #5592: prefer the recorded JS name when the registration
// key was uniquified (e.g. a second `C = class {…}`).
@@ -865,7 +873,10 @@ pub(super) fn emit_string_pool(
// subclass whose parent is a class-expression value inherits the parent's
// static methods (`class Sub extends make(...) {}; Sub.greet()`); has_rest
// tells the dispatcher to bundle trailing args for a `...rest` param.
- let mut static_method_triples: Vec<(u32, String, String, u32, bool, u32, u32)> = Vec::new();
+ #[allow(clippy::type_complexity)]
+ let mut static_method_triples: Vec<(u32, String, String, u32, bool, u32, u32, bool, bool)> =
+ Vec::new();
+ let mut computed_static_entries: Vec = Vec::new();
// #1787: (cid, standalone-constructor symbol, total_param_count).
// Registered into CLASS_CONSTRUCTORS so `new ()` (a
// class-expression value constructed dynamically) can replay the class's
@@ -890,30 +901,8 @@ pub(super) fn emit_string_pool(
// (Next.js `new c.AppPageRouteModule({...})`). A fact of the class's
// constructor table, keyed by class id like every other one.
let mut ctor_flag_regs: Vec<(u32, bool, bool, i64)> = Vec::new();
- for (class_name, class) in classes.iter() {
- // Refs #486: skip alias keys (class_table now contains both the
- // canonical name and self-binding aliases like `_X` from
- // `var X = class _X`); the symbol emission iterates by canonical
- // class.name. Without this skip the alias key generates bogus
- // symbol names like `perry_method____X__method` (extra
- // leading underscore from sanitize("_X")) that don't resolve at
- // link time.
- if *class_name != class.name {
- continue;
- }
- // Imported class stubs carry id == 0 (they're typed-name
- // placeholders for cross-module dispatch; the defining module's init
- // registers their methods). Skip them here so we don't re-emit the
- // registration. Previously this filter was `method.body.is_empty()`;
- // the id check is equivalent for stubs and also catches getter/setter
- // and property-decorator init that legitimately has an empty body.
- if class.id == 0 {
- continue;
- }
- let cid = match class_ids.get(class_name) {
- Some(&c) if c != 0 => c,
- _ => continue,
- };
+ for &(cid, class) in &local_classes {
+ let class_name = &class.name;
for method in &class.methods {
let llvm_name = format!(
"perry_method_{}__{}__{}",
@@ -992,8 +981,42 @@ pub(super) fn emit_string_pool(
has_rest,
spec_length,
sm.id,
+ sm.params
+ .iter()
+ .any(|p| p.is_rest && p.arguments_object.is_none()),
+ sm.params.iter().any(|p| p.arguments_object.is_some()),
));
}
+ // A computed-name static method is a ClassBody static method too: its
+ // own function object runs the same closure-convention entry. Its
+ // key (and so its `name`) exists only when the class definition
+ // evaluates, which registers the entry with it
+ // (`js_register_class_computed_method`).
+ for member in class
+ .computed_members
+ .iter()
+ .filter(|m| m.is_static && matches!(m.kind, perry_hir::ClassComputedMemberKind::Method))
+ {
+ let f = &member.function;
+ let mut spec_length = 0u32;
+ for p in &f.params {
+ if p.arguments_object.is_some() || p.is_rest || p.default.is_some() {
+ break;
+ }
+ spec_length += 1;
+ }
+ computed_static_entries.push(StaticMethodEntry {
+ cid,
+ llvm_name: scoped_static_method_name(module_prefix, cid, class_name, &f.name),
+ param_count: f.params.len() as u32,
+ spec_length,
+ has_user_rest: f
+ .params
+ .iter()
+ .any(|p| p.is_rest && p.arguments_object.is_none()),
+ has_synth_args: f.params.iter().any(|p| p.arguments_object.is_some()),
+ });
+ }
// #1787: the standalone constructor `___constructor`
// (emitted unconditionally in `artifacts.rs`). Its arity is the
// constructor's full param list — user params plus the synthesized
@@ -1139,8 +1162,17 @@ pub(super) fn emit_string_pool(
// static methods (subclass extends a class-expression value) resolve at
// runtime via the class_id parent-chain walk.
static_method_triples.sort_unstable();
- for (cid, method_name, llvm_name, param_count, has_rest, spec_length, definition_order) in
- static_method_triples
+ for (
+ cid,
+ method_name,
+ llvm_name,
+ param_count,
+ has_rest,
+ spec_length,
+ definition_order,
+ has_user_rest,
+ has_synth_args,
+ ) in static_method_triples
{
chunker.roll_if_full();
let blk = chunker.current_block();
@@ -1187,6 +1219,37 @@ pub(super) fn emit_string_pool(
(I64, &spec_length.to_string()),
],
);
+ let (entry_ref, entry_info_ref) = emit_static_method_entry(
+ &mut chunker,
+ &StaticMethodEntry {
+ cid,
+ llvm_name: llvm_name.clone(),
+ param_count,
+ spec_length,
+ has_user_rest,
+ has_synth_args,
+ },
+ );
+ let blk = chunker.current_block();
+ blk.call_void(
+ register_name_fn,
+ &[(PTR, &entry_ref), (PTR, &bytes_global), (I32, &len_str)],
+ );
+ let entry_i64 = blk.ptrtoint(&entry_info_ref, I64);
+ blk.call_void(
+ "js_register_class_static_method_entry",
+ &[
+ (I64, &cid.to_string()),
+ (I64, &bytes_i64),
+ (I64, &len_str),
+ (I64, &entry_i64),
+ ],
+ );
+ }
+ computed_static_entries.sort_unstable_by(|a, b| a.llvm_name.cmp(&b.llvm_name));
+ for e in &computed_static_entries {
+ chunker.roll_if_full();
+ let _ = emit_static_method_entry(&mut chunker, e);
}
// #1787: register each class's standalone constructor into
// CLASS_CONSTRUCTORS. ptrtoint @symbol both stores the function pointer
@@ -1359,24 +1422,8 @@ pub(super) fn emit_string_pool(
// (class_id, prop_name, llvm_symbol, is_static) — static accessors register
// onto the class constructor (CLASS_STATIC_ACCESSORS), not the instance vtable.
let mut getter_pairs: Vec<(u32, String, String, bool, u32)> = Vec::new();
- for (class_name, class) in classes.iter() {
- // Refs #486: skip alias keys (see method-emission loop above).
- if *class_name != class.name {
- continue;
- }
- // Imported class stubs carry id == 0 (they're typed-name
- // placeholders for cross-module dispatch; the defining module's init
- // registers their methods). Skip them here so we don't re-emit the
- // registration. Previously this filter was `method.body.is_empty()`;
- // the id check is equivalent for stubs and also catches getter/setter
- // and property-decorator init that legitimately has an empty body.
- if class.id == 0 {
- continue;
- }
- let cid = match class_ids.get(class_name).copied() {
- Some(c) if c != 0 => c,
- _ => continue,
- };
+ for &(cid, class) in &local_classes {
+ let class_name = &class.name;
for (prop, getter_fn) in &class.getters {
// The local-emit path at codegen.rs:1858 prepends `__get_`
// to the HIR-assigned getter name (`get_`), giving
@@ -1463,23 +1510,8 @@ pub(super) fn emit_string_pool(
// the runtime fell back to the setter's ABI arity (1), over-counting the
// defaulted param.
let mut setter_pairs: Vec<(u32, String, String, bool, u32, u32)> = Vec::new();
- for (class_name, class) in classes.iter() {
- if *class_name != class.name {
- continue;
- }
- // Imported class stubs carry id == 0 (they're typed-name
- // placeholders for cross-module dispatch; the defining module's init
- // registers their methods). Skip them here so we don't re-emit the
- // registration. Previously this filter was `method.body.is_empty()`;
- // the id check is equivalent for stubs and also catches getter/setter
- // and property-decorator init that legitimately has an empty body.
- if class.id == 0 {
- continue;
- }
- let cid = match class_ids.get(class_name).copied() {
- Some(c) if c != 0 => c,
- _ => continue,
- };
+ for &(cid, class) in &local_classes {
+ let class_name = &class.name;
for (prop, setter_fn) in &class.setters {
let is_static = class.static_accessor_fn_ids.contains(&setter_fn.id);
let llvm_name = if is_static {
@@ -1710,3 +1742,72 @@ fn record_fn_info_facts(llmod: &LlModule, module_prefix: &str, src: FnInfoFactSo
#[cfg(test)]
#[path = "class_name_registration_tests.rs"]
mod class_name_registration_tests;
+
+/// A ClassBody static method's closure-convention entry (`__clo`).
+struct StaticMethodEntry {
+ cid: u32,
+ llvm_name: String,
+ param_count: u32,
+ spec_length: u32,
+ has_user_rest: bool,
+ has_synth_args: bool,
+}
+
+/// Define `__clo(callee, this, args...)`, the code of a ClassBody static
+/// method's own function object: the call's `this` (the JS body ABI's receiver
+/// parameter) becomes the body's `this` (enter), the body runs, leave drops
+/// what enter set up. Arity, rest bundling, length and strictness are facts of
+/// the entry's own `JsFunctionInfo`, as for any function body (the caller
+/// registers the name against the code). Returns `(@__clo,
+/// @__clo$info)`: the code and the info a function object allocates
+/// from.
+fn emit_static_method_entry(
+ chunker: &mut InitChunker<'_>,
+ e: &StaticMethodEntry,
+) -> (String, String) {
+ use crate::fn_info::RestKind;
+ let entry_name = format!("{}__clo", e.llvm_name);
+ {
+ let n = e.param_count as usize;
+ let mut params: Vec<(crate::types::LlvmType, String)> =
+ vec![(I64, "%callee".to_string()), (I64, "%this".to_string())];
+ params.extend((0..n).map(|i| (DOUBLE, format!("%a{}", i))));
+ let f = chunker
+ .module()
+ .define_function(&entry_name, DOUBLE, params);
+ let _ = f.create_block("entry");
+ let b = f.block_mut(0).unwrap();
+ b.call_void(
+ "js_static_method_entry_enter",
+ &[(I32, &e.cid.to_string()), (I64, "%this")],
+ );
+ let arg_names: Vec = (0..n).map(|i| format!("%a{}", i)).collect();
+ let call_args: Vec<(crate::types::LlvmType, &str)> =
+ arg_names.iter().map(|a| (DOUBLE, a.as_str())).collect();
+ let r = b.call(DOUBLE, &e.llvm_name, &call_args);
+ b.call_void("js_static_method_entry_leave", &[]);
+ b.ret(DOUBLE, &r);
+ }
+ let (rest, rest_kind) = match (e.has_user_rest, e.has_synth_args) {
+ (true, true) => (
+ Some(e.param_count.saturating_sub(2)),
+ Some(RestKind::UserAndArguments),
+ ),
+ (true, false) => (Some(e.param_count.saturating_sub(1)), Some(RestKind::User)),
+ (false, true) => (
+ Some(e.param_count.saturating_sub(1)),
+ Some(RestKind::SyntheticArguments),
+ ),
+ (false, false) => (None, None),
+ };
+ chunker.module().note_fn_info(&entry_name, |f| {
+ match (rest, rest_kind) {
+ (Some(fixed), Some(kind)) => f.set_rest(fixed as usize, kind),
+ _ => f.set_declared(e.param_count),
+ }
+ f.set_length(e.spec_length);
+ f.set_strict();
+ });
+ let info_ref = chunker.current_block().fn_info_ref(&entry_name);
+ (format!("@{}", entry_name), info_ref)
+}
diff --git a/crates/perry-codegen/src/expr/literals_vars.rs b/crates/perry-codegen/src/expr/literals_vars.rs
index 992ca93074..a21615c3ea 100644
--- a/crates/perry-codegen/src/expr/literals_vars.rs
+++ b/crates/perry-codegen/src/expr/literals_vars.rs
@@ -1116,52 +1116,12 @@ pub(crate) fn typeof_compile_time_answer(ctx: &FnCtx<'_>, operand: &Expr) -> Opt
}
}
} else {
- // Refs #915 (gap 2 from #899): `typeof C.staticMethod`
- // where `C` is `Expr::ClassRef` or a `LocalGet`
- // aliased to a class. Without this fold, the
- // generic PropertyGet path returns `undefined`
- // for static methods (the runtime `class_has_own_method`
- // checks the prototype vtable, not the static
- // method registry), so `typeof Cls.pipe` reported
- // `"undefined"` instead of `"function"`. The actual
- // dispatch fix lives in `lower_call.rs`'s ClassRef
- // static-method arm — but a typeof read isn't a
- // call, so it needs its own fold here.
- let cls_opt: Option = match object.as_ref() {
- Expr::ClassRef(cls_name) => Some(cls_name.clone()),
- Expr::LocalGet(id) => ctx
- .local_id_to_name
- .get(id)
- .and_then(|name| ctx.local_class_aliases.get(name).cloned()),
- _ => None,
- };
- if let Some(cls) = cls_opt {
- // Walk own static methods + extends chain.
- let mut cur = Some(cls);
- let mut found = false;
- while let Some(c) = cur {
- if let Some(class_info) = ctx.classes.get(&c) {
- if class_info
- .static_methods
- .iter()
- .any(|m| m.name == *property)
- {
- found = true;
- break;
- }
- cur = class_info.extends_name.clone();
- } else {
- break;
- }
- }
- if found {
- Some("function")
- } else {
- None
- }
- } else {
- None
- }
+ // A class static method is an own property of the class
+ // function object (charter step 3f): the generic read returns
+ // its current value, so `typeof C.m` after `delete C.m` or
+ // `C.m = 5` must come from that read, never from the
+ // declaration list.
+ None
}
}
_ => None,
diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs
index a7fbff3d5d..f9a15702fc 100644
--- a/crates/perry-codegen/src/expr/mod.rs
+++ b/crates/perry-codegen/src/expr/mod.rs
@@ -3115,7 +3115,7 @@ pub(crate) mod proxy_reflect;
pub(crate) mod put_value_store_ic;
pub(crate) mod receiver_range;
mod static_field_meta;
-mod static_method;
+pub(crate) mod static_method;
pub(crate) mod store_census;
mod string_regex_proc;
mod super_method;
diff --git a/crates/perry-codegen/src/expr/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs
index 779454c010..659ba0adc2 100644
--- a/crates/perry-codegen/src/expr/static_field_meta.rs
+++ b/crates/perry-codegen/src/expr/static_field_meta.rs
@@ -503,6 +503,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
let is_static_str = (*is_static as i64).to_string();
let has_rest_str = (*has_rest as i64).to_string();
let definition_order_str = definition_order.to_string();
+ // A static one's own function object runs its
+ // closure-convention entry's `JsFunctionInfo` (string pool).
+ let entry_i64 = if *is_static {
+ let info = ctx.block().fn_info_ref(&format!("{llvm_name}__clo"));
+ ctx.block().ptrtoint(&info, I64)
+ } else {
+ "0".to_string()
+ };
ctx.block().call_void(
"js_register_class_computed_method",
&[
@@ -513,6 +521,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
(I64, &is_static_str),
(I64, &has_rest_str),
(I64, &definition_order_str),
+ (I64, &entry_i64),
],
);
}
diff --git a/crates/perry-codegen/src/expr/static_method.rs b/crates/perry-codegen/src/expr/static_method.rs
index f3c6d2d1cf..f28427c23e 100644
--- a/crates/perry-codegen/src/expr/static_method.rs
+++ b/crates/perry-codegen/src/expr/static_method.rs
@@ -17,6 +17,132 @@ use super::{
import_origin_suffix_ns, lower_expr, nanbox_pointer_inline, unbox_to_i64, FnCtx,
};
+/// A static-call site's guard (`js_class_static_call_guard`): the declared
+/// body runs directly only while the property the call reads is still that
+/// declaration's function object, a fact carried by the class function
+/// objects' shapes. The site's memo is a runtime `StaticCallMemo` (four
+/// words; thread-local when the program starts workers, so each agent arms
+/// its own) holding the (pinned) class function objects. The hit is inline,
+/// one shape word per class the read consults:
+///
+/// ```text
+/// [receiver == memo.c_value] else MISS
+/// [[memo.c + PROPS] + SHAPE] == low half of memo.key else MISS
+/// [[[memo.owner + PROPS] + SHAPE] == high half of memo.key else MISS]
+/// MISS: ok = miss_fn(miss_args..., memo) (re-validates, re-arms)
+/// ```
+///
+/// Never armed, both object words point at the site's constant, whose
+/// own-property word points at itself and whose shape word (0) never equals
+/// a half of the unarmed key (all ones).
+///
+/// `same_owner`: the class the call names declares the body itself, so its
+/// one shape word is both halves. `receiver_bits`: a site whose receiver is a
+/// value must also be looking at the memo's class function object. Returns
+/// the i1 "the body may run directly".
+pub(crate) fn emit_static_call_guard(
+ ctx: &mut FnCtx<'_>,
+ receiver_bits: Option<&str>,
+ same_owner: bool,
+ miss_fn: &str,
+ miss_args: &[(crate::types::LlvmType, String)],
+) -> String {
+ use crate::types::I1;
+ let site = ctx.ic_site_counter;
+ ctx.ic_site_counter += 1;
+ let site_name = crate::expr::inline_cache_global_name(ctx, site);
+ let memo = format!("@{site_name}_smemo");
+ let unarmed = format!("@{site_name}_sunarmed");
+ let tls = if crate::codegen::program_has_worker() {
+ "thread_local "
+ } else {
+ ""
+ };
+ // `[unarmed + PROPS]` is `unarmed`; `[unarmed + SHAPE]` is 0.
+ let props_words = crate::runtime_abi::CLOSURE_PROPS_OFFSET / 8;
+ debug_assert!(crate::runtime_abi::OBJECT_SHAPE_OFFSET + 4 <= 8 * props_words);
+ ctx.typed_parse_rodata.push(format!(
+ "{unarmed} = private constant {{ [{props_words} x i64], ptr }} {{ [{props_words} x i64] zeroinitializer, ptr {unarmed} }}, align 8"
+ ));
+ ctx.typed_parse_rodata.push(format!(
+ "{memo} = private {tls}global {{ i64, ptr, ptr, i64 }} {{ i64 -1, ptr {unarmed}, ptr {unarmed}, i64 0 }}, align 8"
+ ));
+ let mut args: Vec<(crate::types::LlvmType, &str)> =
+ miss_args.iter().map(|(t, v)| (*t, v.as_str())).collect();
+ args.push((PTR, &memo));
+ // The inline hit reads little-endian LP64 layouts (8-byte memo words, the
+ // key's halves); other targets always ask the runtime.
+ let triple = ctx.target_triple;
+ let lp64_le = (triple.starts_with("x86_64") || triple.starts_with("aarch64"))
+ && !triple.starts_with("aarch64_be")
+ && !triple.contains("32");
+ if !lp64_le {
+ let ok = ctx.block().call(I32, miss_fn, &args);
+ return ctx.block().icmp_ne(I32, &ok, "0");
+ }
+ let miss_idx = ctx.new_block("static_guard.miss");
+ let join_idx = ctx.new_block("static_guard.join");
+ let miss_l = ctx.block_label(miss_idx);
+ let join_l = ctx.block_label(join_idx);
+ // One test per block, each expected to pass: the hit falls straight
+ // through to the direct call, every miss branches out of line.
+ let test = |ctx: &mut FnCtx<'_>, pass: &str, last: bool| {
+ let pass = ctx
+ .block()
+ .call(I1, "llvm.expect.i1", &[(I1, pass), (I1, "true")]);
+ if last {
+ ctx.block().cond_br(&pass, &join_l, &miss_l);
+ } else {
+ let next = ctx.new_block("static_guard.check");
+ let next_l = ctx.block_label(next);
+ ctx.block().cond_br(&pass, &next_l, &miss_l);
+ ctx.current_block = next;
+ }
+ };
+ let memo_word = |ctx: &mut FnCtx<'_>, ty: crate::types::LlvmType, offset: usize| -> String {
+ let p = ctx
+ .block()
+ .gep(crate::types::I8, &memo, &[(I64, &offset.to_string())]);
+ ctx.block().load(ty, &p)
+ };
+ let shape_matches = |ctx: &mut FnCtx<'_>, fo_offset: usize, key_offset: usize| -> String {
+ let fo = memo_word(ctx, PTR, fo_offset);
+ let props = crate::runtime_abi::CLOSURE_PROPS_OFFSET.to_string();
+ let pp = ctx.block().gep(crate::types::I8, &fo, &[(I64, &props)]);
+ let bag = ctx.block().load(PTR, &pp);
+ let shape = crate::runtime_abi::OBJECT_SHAPE_OFFSET.to_string();
+ let sp = ctx.block().gep(crate::types::I8, &bag, &[(I64, &shape)]);
+ let w = ctx.block().load(I32, &sp);
+ let k = memo_word(ctx, I32, key_offset);
+ ctx.block().icmp_eq(I32, &w, &k)
+ };
+ if let Some(bits) = receiver_bits {
+ let v = memo_word(ctx, I64, crate::runtime_abi::STATIC_CALL_MEMO_VALUE_OFFSET);
+ let same = ctx.block().icmp_eq(I64, bits, &v);
+ test(ctx, &same, false);
+ }
+ let key = crate::runtime_abi::STATIC_CALL_MEMO_KEY_OFFSET;
+ let c_ok = shape_matches(ctx, crate::runtime_abi::STATIC_CALL_MEMO_C_OFFSET, key);
+ test(ctx, &c_ok, same_owner);
+ if !same_owner {
+ let o_ok = shape_matches(
+ ctx,
+ crate::runtime_abi::STATIC_CALL_MEMO_OWNER_OFFSET,
+ key + 4,
+ );
+ test(ctx, &o_ok, true);
+ }
+ let hit_pred = ctx.block().label.clone();
+ ctx.current_block = miss_idx;
+ let ok = ctx.block().call(I32, miss_fn, &args);
+ let ok = ctx.block().icmp_ne(I32, &ok, "0");
+ let miss_pred = ctx.block().label.clone();
+ ctx.block().br(&join_l);
+ ctx.current_block = join_idx;
+ ctx.block()
+ .phi(I1, &[("true", &hit_pred), (&ok, &miss_pred)])
+}
+
fn downgrade_unknown_call_args(ctx: &mut FnCtx<'_>, args: &[Expr]) {
for arg in args {
downgrade_buffer_aliases_in_expr(ctx, arg, MaterializationReason::UnknownCallEscape);
@@ -131,17 +257,88 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
.get(class_name)
.map(|c| c.static_methods.iter().any(|m| m.name == *method_name))
.unwrap_or(true);
- if !owns_method {
- if let Some(&cid) = ctx.class_ids.get(class_name) {
+ // The shape is the authority: the body runs directly only while
+ // the property a call reads (`C.m`, on C or the class it is
+ // inherited from) is still this declaration's function object.
+ // The check precedes the arguments, as the property read does;
+ // otherwise the call reads the property and calls its value on
+ // the class function object (pinned, so it survives the
+ // argument evaluation).
+ // A `static { }` block's synthetic method is not a member: the
+ // class initializer calls it directly, it has no property.
+ let static_cid = if method_name.starts_with("__perry_static_init_") {
+ None
+ } else {
+ ctx.class_ids.get(class_name).copied()
+ };
+ // The class whose ClassBody declares the body this call runs.
+ let owner_cid = {
+ let mut cur = class_name.clone();
+ let mut found = 0u32;
+ for _ in 0..32 {
+ let Some(c) = ctx.classes.get(&cur) else {
+ break;
+ };
+ if c.static_methods.iter().any(|m| m.name == *method_name) {
+ found = ctx.class_ids.get(&cur).copied().unwrap_or(0);
+ break;
+ }
+ match c.extends_name.clone() {
+ Some(p) => cur = p,
+ None => break,
+ }
+ }
+ found
+ };
+ let name_idx = ctx.strings.intern(method_name);
+ let name_entry = ctx.strings.entry(name_idx);
+ let name_bytes = format!("@{}", name_entry.bytes_global);
+ let name_len = name_entry.byte_len.to_string();
+ let guard = match static_cid {
+ Some(cid) => {
let cid_str = cid.to_string();
- ctx.block()
- .call_void("js_static_this_arm_classref", &[(I32, &cid_str)]);
+ // Per-site memo of the class function objects' shapes
+ // that proved the declaration (validated per use).
+ let body_i64 = ctx.block().ptrtoint(&format!("@{}", fn_name), I64);
+ let ok = emit_static_call_guard(
+ ctx,
+ None,
+ owner_cid == cid,
+ "js_class_static_call_guard",
+ &[
+ (I32, cid_str.clone()),
+ (I32, owner_cid.to_string()),
+ (PTR, name_bytes.clone()),
+ (I64, name_len.clone()),
+ (I64, body_i64),
+ ],
+ );
+ let recv_idx = ctx.new_block("static_call.receiver");
+ let args_idx = ctx.new_block("static_call.args");
+ let recv_label = ctx.block_label(recv_idx);
+ let args_label = ctx.block_label(args_idx);
+ let pre_label = ctx.block().label.clone();
+ ctx.block().cond_br(&ok, &args_label, &recv_label);
+ ctx.current_block = recv_idx;
+ let recv = ctx
+ .block()
+ .call(DOUBLE, "js_class_value", &[(I32, &cid_str)]);
+ let recv_pred = ctx.block().label.clone();
+ ctx.block().br(&args_label);
+ ctx.current_block = args_idx;
+ let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
+ let recv = ctx
+ .block()
+ .phi(DOUBLE, &[(&undef, &pre_label), (&recv, &recv_pred)]);
+ Some((ok, recv))
}
- }
+ None => None,
+ };
let mut lowered: Vec = Vec::with_capacity(args.len());
for a in args {
lowered.push(lower_expr(ctx, a)?);
}
+ let raw_args = lowered.clone();
// Issue #894: static methods with synthetic `...arguments`
// rest params (or any user-declared rest param) need their
// trailing args bundled into an array. Without this,
@@ -243,7 +440,55 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
}
let arg_slices: Vec<(crate::types::LlvmType, &str)> =
lowered.iter().map(|s| (DOUBLE, s.as_str())).collect();
- return Ok(ctx.block().call(DOUBLE, &fn_name, &arg_slices));
+ let Some((ok, recv)) = guard else {
+ return Ok(ctx.block().call(DOUBLE, &fn_name, &arg_slices));
+ };
+ let direct_idx = ctx.new_block("static_call.direct");
+ let generic_idx = ctx.new_block("static_call.property");
+ let join_idx = ctx.new_block("static_call.join");
+ let direct_label = ctx.block_label(direct_idx);
+ let generic_label = ctx.block_label(generic_idx);
+ let join_label = ctx.block_label(join_idx);
+ ctx.block().cond_br(&ok, &direct_label, &generic_label);
+ ctx.current_block = direct_idx;
+ if !owns_method {
+ let cid_str = static_cid.unwrap_or(0).to_string();
+ ctx.block()
+ .call_void("js_static_this_arm_classref", &[(I32, &cid_str)]);
+ }
+ let direct = ctx.block().call(DOUBLE, &fn_name, &arg_slices);
+ let direct_pred = ctx.block().label.clone();
+ ctx.block().br(&join_label);
+ ctx.current_block = generic_idx;
+ let (args_ptr, args_len) = if raw_args.is_empty() {
+ ("null".to_string(), "0".to_string())
+ } else {
+ let buf = ctx.func.alloca_entry_array(DOUBLE, raw_args.len());
+ let blk = ctx.block();
+ for (i, value) in raw_args.iter().enumerate() {
+ let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]);
+ blk.store(DOUBLE, value, &slot);
+ }
+ (buf, raw_args.len().to_string())
+ };
+ let via_property = ctx.block().call(
+ DOUBLE,
+ "js_native_call_method",
+ &[
+ (DOUBLE, &recv),
+ (PTR, &name_bytes),
+ (I64, &name_len),
+ (PTR, &args_ptr),
+ (I64, &args_len),
+ ],
+ );
+ let generic_pred = ctx.block().label.clone();
+ ctx.block().br(&join_label);
+ ctx.current_block = join_idx;
+ return Ok(ctx.block().phi(
+ DOUBLE,
+ &[(&direct, &direct_pred), (&via_property, &generic_pred)],
+ ));
}
// #310: when the receiver is a namespace alias from an
// `import { Foo } from "pkg"` where the source module did
diff --git a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv
index 7f84742d39..3bb4f1b118 100644
--- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv
+++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv
@@ -601,9 +601,11 @@ js_class_prototype_method_value Reenters
js_class_register_capture_values Leaf
js_class_register_static_field Reenters
js_class_register_static_symbol Reenters
+js_class_static_call_guard Reenters
js_class_static_field_get Reenters
js_class_static_field_put Reenters
js_class_static_method_call Reenters
+js_class_static_value_call_guard Reenters
js_class_value Reenters
js_clear_exception Leaf
js_clear_immediate_value Reenters
@@ -2747,6 +2749,7 @@ js_register_class_source_static Leaf
js_register_class_static_getter Reenters
js_register_class_static_method Reenters
js_register_class_static_method_bind_length Leaf
+js_register_class_static_method_entry Reenters
js_register_class_static_setter Reenters
js_register_class_string_member_order Leaf
js_register_class_to_string_tag Leaf
@@ -3027,6 +3030,8 @@ js_stack_overflow Reenters
js_state_get Reenters
js_state_init Reenters
js_state_set Reenters
+js_static_method_entry_enter Reenters
+js_static_method_entry_leave Leaf
js_static_this_arm_classref Reenters
js_static_this_arm_value Leaf
js_static_this_resolve Leaf
diff --git a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs
index 6ec927deec..8cb9cd4994 100644
--- a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs
+++ b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs
@@ -43,6 +43,8 @@ pub(crate) fn try_lower_static_dispatch(
}
// (fn_name, is_static, declared_param_count, has_rest, is_synthetic_arguments)
let mut resolved: Option<(String, bool, usize, bool, bool)> = None;
+ // The class whose ClassBody declares the resolved body.
+ let mut owner_class: Option = None;
let mut cur = Some(cls_name.clone());
while let Some(c) = cur {
if let Some(class_info) = ctx.classes.get(&c) {
@@ -64,6 +66,7 @@ pub(crate) fn try_lower_static_dispatch(
.map(|p| p.arguments_object.is_some())
.unwrap_or(false);
resolved = Some((fname, true, declared, has_rest, is_synth_args));
+ owner_class = Some(c.clone());
break;
}
}
@@ -151,6 +154,79 @@ pub(crate) fn try_lower_static_dispatch(
has_rest || crate::rooting::any_operand_may_collect(ctx, args.iter());
let recv_idx = group.adopt(ctx, object, &recv_box, collects);
+ // The shape is the authority: the declared body runs directly
+ // only while `.` is still that declaration's
+ // function object (`js_class_static_value_call_guard`, checked
+ // before the arguments, as the property read is). Otherwise the
+ // call reads the property and calls its value.
+ let owner_cid = owner_class
+ .as_ref()
+ .and_then(|c| ctx.class_ids.get(c).copied())
+ .filter(|&c| c != 0 && !property.starts_with("__perry_static_init_"));
+ let guarded = if let Some(owner_cid) = owner_cid {
+ let name_idx = ctx.strings.intern(property);
+ let name_entry = ctx.strings.entry(name_idx);
+ let name_bytes = format!("@{}", name_entry.bytes_global);
+ let name_len = name_entry.byte_len.to_string();
+ let recv_now = group.reread(ctx, recv_idx)?;
+ let recv_bits = ctx.block().bitcast_double_to_i64(&recv_now);
+ let body_i64 = ctx.block().ptrtoint(&format!("@{}", fn_name), I64);
+ let ok = crate::expr::static_method::emit_static_call_guard(
+ ctx,
+ Some(&recv_bits),
+ false,
+ "js_class_static_value_call_guard",
+ &[
+ (DOUBLE, recv_now.clone()),
+ (I32, owner_cid.to_string()),
+ (crate::types::PTR, name_bytes.clone()),
+ (I64, name_len.clone()),
+ (I64, body_i64),
+ ],
+ );
+ let direct_idx = ctx.new_block("static_value_call.direct");
+ let generic_idx = ctx.new_block("static_value_call.property");
+ let join_idx = ctx.new_block("static_value_call.join");
+ let direct_label = ctx.block_label(direct_idx);
+ let generic_label = ctx.block_label(generic_idx);
+ ctx.block().cond_br(&ok, &direct_label, &generic_label);
+ ctx.current_block = generic_idx;
+ let mut raw: Vec = Vec::with_capacity(args.len());
+ for a in args {
+ raw.push(lower_expr(ctx, a)?);
+ }
+ let (args_ptr, args_len) = if raw.is_empty() {
+ ("null".to_string(), "0".to_string())
+ } else {
+ let buf = ctx.func.alloca_entry_array(DOUBLE, raw.len());
+ let blk = ctx.block();
+ for (i, value) in raw.iter().enumerate() {
+ let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]);
+ blk.store(DOUBLE, value, &slot);
+ }
+ (buf, raw.len().to_string())
+ };
+ let recv_g = group.reread(ctx, recv_idx)?;
+ let via_property = ctx.block().call(
+ DOUBLE,
+ "js_native_call_method",
+ &[
+ (DOUBLE, &recv_g),
+ (crate::types::PTR, &name_bytes),
+ (I64, &name_len),
+ (crate::types::PTR, &args_ptr),
+ (I64, &args_len),
+ ],
+ );
+ let generic_pred = ctx.block().label.clone();
+ let join_label = ctx.block_label(join_idx);
+ ctx.block().br(&join_label);
+ ctx.current_block = direct_idx;
+ Some((via_property, generic_pred, join_idx))
+ } else {
+ None
+ };
+
// Refs #915 (gap 3 / #321 follow-up): Effect's `class
// SchemaClass { static pipe() { ... arguments ... } }`
// factory returns an anon class whose `pipe` reads
@@ -276,7 +352,17 @@ pub(crate) fn try_lower_static_dispatch(
let arg_slices: Vec<(crate::types::LlvmType, &str)> =
lowered.iter().map(|s| (DOUBLE, s.as_str())).collect();
let result = ctx.block().call(DOUBLE, &fn_name, &arg_slices);
- Ok(Some(result))
+ let Some((via_property, generic_pred, join_idx)) = guarded else {
+ return Ok(Some(result));
+ };
+ let direct_pred = ctx.block().label.clone();
+ let join_label = ctx.block_label(join_idx);
+ ctx.block().br(&join_label);
+ ctx.current_block = join_idx;
+ Ok(Some(ctx.block().phi(
+ DOUBLE,
+ &[(&result, &direct_pred), (&via_property, &generic_pred)],
+ )))
});
}
// #1787 / #321: the call target is a static FIELD holding a callable,
diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
index ce7be62902..0eb513372f 100644
--- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
+++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
@@ -426,6 +426,23 @@ pub(crate) fn declare_core(module: &mut LlModule) {
module.declare_function("js_static_this_resolve", DOUBLE, &[DOUBLE]);
module.declare_function("js_static_this_resolve_class", DOUBLE, &[I32, PTR]);
module.declare_function("js_static_this_arm_classref", VOID, &[I32]);
+ module.declare_function("js_static_method_entry_enter", VOID, &[I32, I64]);
+ module.declare_function("js_static_method_entry_leave", VOID, &[]);
+ module.declare_function(
+ "js_class_static_call_guard",
+ I32,
+ &[I32, I32, PTR, I64, I64, PTR],
+ );
+ module.declare_function(
+ "js_class_static_value_call_guard",
+ I32,
+ &[DOUBLE, I32, PTR, I64, I64, PTR],
+ );
+ module.declare_function(
+ "js_register_class_static_method_entry",
+ VOID,
+ &[I64, I64, I64, I64],
+ );
module.declare_function("js_static_this_arm_value", VOID, &[DOUBLE]);
module.declare_function("js_ctor_return_override", DOUBLE, &[DOUBLE, DOUBLE, I32]);
module.declare_function("js_new_target_get", DOUBLE, &[]);
diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs
index 04cab5c459..2894c1b579 100644
--- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs
+++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs
@@ -249,7 +249,7 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
module.declare_function(
"js_register_class_computed_method",
VOID,
- &[I64, DOUBLE, I64, I64, I64, I64, I64],
+ &[I64, DOUBLE, I64, I64, I64, I64, I64, I64],
);
module.declare_function(
"js_register_class_computed_accessor",
diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv
index dcc2aced8e..e719ca3886 100644
--- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv
+++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv
@@ -609,9 +609,11 @@ js_class_prototype_method_value f64 f64,f64
js_class_register_capture_values void i32u,ptr,usize
js_class_register_static_field void i32u,ptr,usize,f64,ptr
js_class_register_static_symbol void i32u,f64,f64
+js_class_static_call_guard i32s i32s,i32s,ptr,i64,i64,ptr
js_class_static_field_get f64 i32s,ptr,i64
js_class_static_field_put void i32s,ptr,i64,f64
js_class_static_method_call f64 f64,ptr,usize,ptr,usize
+js_class_static_value_call_guard i32s f64,i32s,ptr,i64,i64,ptr
js_class_value f64 i32s
js_clear_exception void
js_clear_immediate_value void f64
@@ -3202,7 +3204,7 @@ js_register_aux_has_active void ptr
js_register_aux_pump void ptr
js_register_aux_tick_begin void ptr
js_register_class_computed_accessor void i64,f64,i64,i64,i64,i64
-js_register_class_computed_method void i64,f64,i64,i64,i64,i64,i64
+js_register_class_computed_method void i64,f64,i64,i64,i64,i64,i64,i64
js_register_class_constructor void i64,i64,i64,i64
js_register_class_constructor_flags void i64,i64,i64,i64
js_register_class_extends_data_view void i32u
@@ -3224,6 +3226,7 @@ js_register_class_source_static void i32u,ptr,i32u
js_register_class_static_getter void i64,ptr,i64,i64
js_register_class_static_method void i64,ptr,i64,i64,i64,i64
js_register_class_static_method_bind_length void i64,ptr,i64,i64
+js_register_class_static_method_entry void i64,ptr,i64,i64
js_register_class_static_setter void i64,ptr,i64,i64,i32s
js_register_class_string_member_order void i64,ptr,i64,i64,i64
js_register_class_to_string_tag void i32u,i64
@@ -3511,6 +3514,8 @@ js_stack_overflow void
js_state_get f64 f64
js_state_init void f64,f64
js_state_set void f64,f64
+js_static_method_entry_enter void i32u,i64
+js_static_method_entry_leave void
js_static_this_arm_classref void i32u
js_static_this_arm_value void f64
js_static_this_resolve f64 f64
diff --git a/crates/perry-codegen/tests/static_symbol_hygiene.rs b/crates/perry-codegen/tests/static_symbol_hygiene.rs
index 302cece1e4..068b79e120 100644
--- a/crates/perry-codegen/tests/static_symbol_hygiene.rs
+++ b/crates/perry-codegen/tests/static_symbol_hygiene.rs
@@ -219,20 +219,51 @@ fn duplicate_class_static_methods_use_class_id_in_symbols() {
let ir = String::from_utf8(compile_module(&duplicate_static_module(), empty_opts()).unwrap())
.unwrap();
+ // The body: exactly one definition.
assert_eq!(
count(
&ir,
- "define double @perry_static_marked_symbol_hygiene_ts__x__c11__lex"
+ "define double @perry_static_marked_symbol_hygiene_ts__x__c11__lex("
),
1
);
+ // This module hands codegen two classes of one name. Registration keys
+ // each class by its ClassId, so both static methods are registered, each
+ // with its own function-object entry `__clo`.
assert_eq!(
count(
&ir,
- "define double @perry_static_marked_symbol_hygiene_ts__x__c12__lex"
+ "define double @perry_static_marked_symbol_hygiene_ts__x__c11__lex__clo("
),
1
);
+ // The body, and its function object's closure-convention entry
+ // `__clo`: exactly one definition each.
+ assert_eq!(
+ count(
+ &ir,
+ "define double @perry_static_marked_symbol_hygiene_ts__x__c12__lex("
+ ),
+ 1
+ );
+ assert_eq!(
+ count(
+ &ir,
+ "define double @perry_static_marked_symbol_hygiene_ts__x__c12__lex__clo("
+ ),
+ 1
+ );
+ // Each class registers its own static method under its own id.
+ for cid in [11, 12] {
+ assert_eq!(
+ count(
+ &ir,
+ &format!("call void @js_register_class_static_method_entry(i64 {cid}, ")
+ ),
+ 1,
+ "class {cid}"
+ );
+ }
assert_eq!(
count(
&ir,
@@ -256,10 +287,19 @@ fn static_and_instance_methods_with_same_name_keep_distinct_symbols() {
),
1
);
+ // The body, and its function object's closure-convention entry
+ // `__clo`: exactly one definition each.
+ assert_eq!(
+ count(
+ &ir,
+ "define double @perry_static_static_instance_symbol_hygiene_ts__x__c11__lex("
+ ),
+ 1
+ );
assert_eq!(
count(
&ir,
- "define double @perry_static_static_instance_symbol_hygiene_ts__x__c11__lex"
+ "define double @perry_static_static_instance_symbol_hygiene_ts__x__c11__lex__clo("
),
1
);
diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs
index 1be3d46eab..6e03b6d85c 100644
--- a/crates/perry-runtime/src/array/mod.rs
+++ b/crates/perry-runtime/src/array/mod.rs
@@ -223,8 +223,9 @@ pub use self::numeric_range::{
};
pub use self::prototype_addr::scan_prototype_addr_cache_roots_mut;
pub(crate) use self::prototype_addr::{
- array_prototype_addr, function_prototype_addr, object_prototype_addr,
- object_prototype_addr_if_resolved, object_prototype_addr_matches, prime_prototype_addr_cache,
+ array_prototype_addr, function_prototype_addr, note_object_prototype_intrinsic,
+ object_prototype_addr, object_prototype_addr_if_resolved, object_prototype_addr_matches,
+ prime_prototype_addr_cache,
};
#[cfg(test)]
pub(crate) use self::prototype_addr::{
diff --git a/crates/perry-runtime/src/array/prototype_addr.rs b/crates/perry-runtime/src/array/prototype_addr.rs
index 4504def4b9..d5f8fa2522 100644
--- a/crates/perry-runtime/src/array/prototype_addr.rs
+++ b/crates/perry-runtime/src/array/prototype_addr.rs
@@ -245,8 +245,10 @@ fn resolve_prototype_addr(slot: usize) -> usize {
// and that read MATERIALIZES the realm global when this thread has none —
// allocating the singleton and running `populate_global_this_builtins`, which
// its own `[gc-globalthis-bootstrap]` diagnostic measures at ~5 ms. Before the
- // realm global exists, neither intrinsic prototype object has been allocated,
- // so NO address can be one of them and the honest answer is already known:
+ // realm global exists, Array.prototype and Function.prototype have not been
+ // allocated, and %Object.prototype% (which can exist without the realm
+ // global) memoized its row when it was built, so an unmemoized row here
+ // means NO address can be the intrinsic and the honest answer is already known:
// "not resolved" (0), which every caller of these accessors handles because
// `bootstrap_prototype_addr` can return it anyway.
//
@@ -276,6 +278,12 @@ fn resolve_prototype_addr(slot: usize) -> usize {
#[cold]
#[inline(never)]
fn bootstrap_prototype_addr(slot: usize) -> usize {
+ if slot == OBJECT_PROTO_CACHE {
+ // %Object.prototype% is built on its own (`ensure_object_intrinsics`),
+ // which memoizes this row itself; 0 only while that build is running.
+ let (_, proto) = crate::object::ensure_object_intrinsics();
+ return proto as usize;
+ }
let builtin = PROTOTYPE_ADDR_BUILTINS[slot];
let ctor = crate::object::js_get_global_this_builtin_value(builtin.as_ptr(), builtin.len());
let ctor_value = crate::value::JSValue::from_bits(ctor.to_bits());
@@ -320,6 +328,13 @@ pub(crate) fn object_prototype_addr_if_resolved() -> usize {
memoized_prototype_addr(&prototype_addrs()[OBJECT_PROTO_CACHE]).unwrap_or(0)
}
+/// Memoize THIS realm's `%Object.prototype%` the moment it is built, so the
+/// store path's "is this Object.prototype?" check answers for it even before
+/// the realm global exists (the class prototype chain reaches it first).
+pub(crate) fn note_object_prototype_intrinsic(addr: usize) {
+ prototype_addrs()[OBJECT_PROTO_CACHE].set(addr);
+}
+
/// **This realm's** `%Function.prototype%` address, or 0 while this thread
/// has no `globalThis` yet (no intrinsic exists, so nothing can be it). See
/// the row-2 note on [`prototype_addrs`] (#10497).
diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs
index cfd931383f..7f1e894a08 100644
--- a/crates/perry-runtime/src/closure/dynamic_props.rs
+++ b/crates/perry-runtime/src/closure/dynamic_props.rs
@@ -615,13 +615,13 @@ pub(crate) fn function_prototype_fallback_target(ptr: usize, prop: &str) -> Opti
if reentrant {
return None;
}
- let proto_val = crate::object::builtin_prototype_value("Function");
+ // THIS realm's %Function.prototype% (the memoized intrinsic), not whatever
+ // `globalThis.Function` names now. It is 0 while the realm global has not
+ // been built: %Function.prototype% does not exist yet, so no descriptor
+ // can sit on it — and asking must not build the realm global (defining a
+ // static on %Object% or a class function object would otherwise do so).
+ let proto_ptr = crate::array::function_prototype_addr();
IN_FN_PROTO_FALLBACK.with(|c| c.set(false));
- let proto_jv = crate::value::JSValue::from_bits(proto_val.to_bits());
- if !proto_jv.is_pointer() {
- return None;
- }
- let proto_ptr = (proto_jv.bits() & crate::value::POINTER_MASK) as usize;
if proto_ptr == 0 || proto_ptr == ptr || is_closure_ptr(proto_ptr) {
return None;
}
diff --git a/crates/perry-runtime/src/closure/props.rs b/crates/perry-runtime/src/closure/props.rs
index d6c2a4abe4..ca9b9d9136 100644
--- a/crates/perry-runtime/src/closure/props.rs
+++ b/crates/perry-runtime/src/closure/props.rs
@@ -118,8 +118,46 @@ unsafe fn object_own_set(obj: *mut ObjectHeader, key: &str, value: f64) {
/// `ptr` is a proven, live closure cell.
pub(crate) unsafe fn bag_set(ptr: usize, key: &str, value: f64) {
let _no_move = crate::gc::GcSuppressScope::new();
+ let declared = crate::object::class_value::holds_declared_static_method(ptr, key);
let bag = bag_ensure(ptr);
object_own_set(bag, key, value);
+ declared_value_replaced(ptr, key, declared);
+}
+
+/// After a write to own `key`: when it held the declaration (`declared`) and
+/// no longer does, the shape transitions.
+unsafe fn declared_value_replaced(ptr: usize, key: &str, declared: Option) {
+ let Some(old) = declared else { return };
+ if bag_get(ptr, key.as_bytes()).is_some_and(|v| v.to_bits() == old.to_bits()) {
+ return;
+ }
+ crate::object::shapes::transition_object_shape_semantics(bag_of(ptr));
+}
+
+/// [[DefineOwnProperty]] of own data property `key` with just a value: the
+/// value is stored and the key keeps (or, when new, gets default) attributes.
+/// Unlike [`bag_set`] it ignores the key's `writable` attribute; a caller
+/// that is performing a [[Set]] has checked it (a class function object's
+/// statics, whose attributes live with these keys).
+///
+/// # Safety
+/// `ptr` is a proven, live closure cell.
+pub(crate) unsafe fn bag_define_value(ptr: usize, key: &str, value: f64) {
+ let _no_move = crate::gc::GcSuppressScope::new();
+ let declared = crate::object::class_value::holds_declared_static_method(ptr, key);
+ let bag = bag_ensure(ptr);
+ if !bag_has_own(ptr, key.as_bytes()) {
+ // A NEW property — including one `delete` removed earlier: it is
+ // appended as any new key is, so it enumerates last. The in-place
+ // store below would find the deleted key's tombstoned entry and
+ // bring the property back at its old position.
+ object_own_set(bag, key, value);
+ declared_value_replaced(ptr, key, declared);
+ return;
+ }
+ let key_hdr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32);
+ crate::object::object_ops::define_property_force_store_value(bag, key_hdr, value);
+ declared_value_replaced(ptr, key, declared);
}
/// Remove the function's own data property `key`; true when it existed.
@@ -132,8 +170,12 @@ pub(crate) unsafe fn bag_remove(ptr: usize, key: &str) -> bool {
return false;
}
let _no_move = crate::gc::GcSuppressScope::new();
+ let declared = crate::object::class_value::holds_declared_static_method(ptr, key);
let key_hdr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32);
crate::object::js_object_delete_field(bag, key_hdr);
+ // Deleting the declaration must not let a re-add reach the shape that
+ // proved it (a removed last key re-added lands on the same key list).
+ declared_value_replaced(ptr, key, declared);
true
}
@@ -192,6 +234,40 @@ pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec {
out
}
+/// Every own property name (data AND accessor) in creation order — the
+/// order of the bag's key list. A deleted key is gone from that order; a
+/// key defined again after a delete is a new key and comes last.
+///
+/// # Safety
+/// `ptr` is a proven, live closure cell.
+pub(crate) unsafe fn bag_own_key_names(ptr: usize) -> Vec {
+ let bag = bag_of(ptr);
+ if bag.is_null() {
+ return Vec::new();
+ }
+ let keys = crate::object::object_keys(bag);
+ let arr = keys.arr();
+ if arr.is_null() {
+ return Vec::new();
+ }
+ let live = crate::object::object_live_slot_count(bag);
+ let mut out = Vec::new();
+ for i in 0..keys.count() {
+ if !crate::object::key_attrs::key_is_accessor_at(arr, i)
+ && crate::object::object_field_at_with_live(bag, i, live).bits()
+ == crate::value::TAG_HOLE
+ {
+ continue;
+ }
+ let key = JSValue::from_bits(crate::array::js_array_get_f64(arr, i).to_bits());
+ let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
+ if let Some(bytes) = crate::string::js_string_key_bytes(key, &mut scratch) {
+ out.push(String::from_utf8_lossy(bytes).into_owned());
+ }
+ }
+ out
+}
+
/// Every own data property in ECMA-262 own-key order: integer indices
/// ascending, then other strings in creation order.
///
diff --git a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
index 3fc8184d75..c4631fe3c2 100644
--- a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
+++ b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
@@ -253,7 +253,7 @@ fn realm_owned_intrinsic_module_and_storage_roots_are_distinct() {
let a = a.join().expect("agent A panicked");
let b = b.join().expect("agent B panicked");
- assert_eq!(a.len(), 26, "the gate must cover every #8002/#8003 root");
+ assert_eq!(a.len(), 28, "the gate must cover every #8002/#8003 root");
assert_eq!(a.len(), b.len());
for ((a_name, a_slot, a_root), (b_name, b_slot, b_root)) in a.iter().zip(&b) {
assert_eq!(a_name, b_name, "snapshot wiring diverged between agents");
diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe.rs b/crates/perry-runtime/src/json/stringify_tojson_probe.rs
index a20133dc85..66de736954 100644
--- a/crates/perry-runtime/src/json/stringify_tojson_probe.rs
+++ b/crates/perry-runtime/src/json/stringify_tojson_probe.rs
@@ -482,7 +482,6 @@ fn class_chain_may_have_to_json_uncached(class_id: u32) -> bool {
/// | `Object.setPrototypeOf`, a descriptor install, or a `delete` anywhere | the SEMANTIC property epoch |
/// | a prototype OBJECT materializing for this class or an ancestor — the very thing the walk looks for, since such an object can carry arbitrary later-added properties | `CLASS_LOOKUP_SURFACE_GEN`, bumped inside `class_prototype_object_root_store` and `class_decl_prototype_object_root_store` |
/// | `js_register_class_generic_origin`, which redirects both prototype-object readers and `lookup_prototype_method`'s chain hop | `CLASS_LOOKUP_SURFACE_GEN` |
-/// | re-exposing a `delete`d prototype key through the in-place `CLASS_DELETED_KEYS` un-mark in `class_dynamic_prop_root_store` | `CLASS_LOOKUP_SURFACE_GEN` |
///
/// Garbage collection is deliberately NOT an input. The class side-table
/// scanners only rewrite EXISTING slots, so no collection can add a registry
diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs b/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs
index dbd5ef2d55..226db1628b 100644
--- a/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs
+++ b/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs
@@ -315,7 +315,7 @@ fn a_late_generic_origin_edge_retires_the_cached_chain_verdict() {
}
#[test]
-fn un_marking_a_deleted_prototype_key_retires_the_cached_chain_verdict() {
+fn a_static_store_never_resurrects_a_deleted_prototype_key() {
let class_id = probe_test_class_id(0x61);
crate::object::class_prototype_method_root_store(
class_id,
@@ -323,27 +323,22 @@ fn un_marking_a_deleted_prototype_key_retires_the_cached_chain_verdict() {
probe_test_method_bits(),
);
assert!(super::test_class_chain_may_have_to_json(class_id));
- crate::object::class_mark_key_deleted(class_id, "toJSON");
- // `delete C.prototype.toJSON` reaches `class_mark_key_deleted` through
- // `js_object_delete_field`, which bumps the semantic epoch; stand in for
- // that here so the memo holds the post-delete `false` the runtime would.
+ // `delete C.prototype.toJSON` removes the runtime assignment's entry
+ // through `js_object_delete_field`, which bumps the semantic epoch; stand
+ // in for that here so the memo holds the post-delete `false`.
+ crate::object::class_prototype_method_root_remove(class_id, "toJSON");
crate::object::prop_plan::prop_plan_epoch_bump();
assert!(!super::test_class_chain_may_have_to_json(class_id));
- // `CLASS_DELETED_KEYS` is shared between a class's prototype keys and its
- // STATIC field keys (`class_registry/state.rs`), so a later `C.toJSON = 1`
- // static store un-marks the key IN PLACE inside
- // `class_dynamic_prop_root_store` and re-exposes the prototype method to
- // `lookup_prototype_method` — with no vtable write and no descriptor
- // install to move either of the other two generations.
+ // The static side lives on the class function object, the prototype side
+ // on the prototype: `C.toJSON = 1` cannot bring the prototype key back.
crate::object::class_dynamic_prop_root_store(
class_id,
"toJSON",
f64::from_bits(probe_test_method_bits()),
);
assert!(
- super::test_class_chain_may_have_to_json(class_id),
- "un-marking a deleted key re-exposes the prototype method and must \
- retire the cached verdict"
+ !super::test_class_chain_may_have_to_json(class_id),
+ "a static store must not re-expose a deleted prototype method"
);
}
diff --git a/crates/perry-runtime/src/object/accessor_pair.rs b/crates/perry-runtime/src/object/accessor_pair.rs
index 3eff999d99..9689ad72c0 100644
--- a/crates/perry-runtime/src/object/accessor_pair.rs
+++ b/crates/perry-runtime/src/object/accessor_pair.rs
@@ -15,8 +15,18 @@
//! |---|---|
//! | [`PAIR_GET`] | the getter as a NaN-boxed closure, or `undefined` |
//! | [`PAIR_SET`] | the setter as a NaN-boxed closure, or `undefined` |
-//! | [`PAIR_RAW_GET`] | a class getter's compiled entry `fn(this) -> value` (its address bits), or 0 |
-//! | [`PAIR_RAW_SET`] | a class setter's compiled entry `fn(this, v) -> value` (its address bits), or 0 |
+//! | [`PAIR_RAW_GET`] | a class getter's compiled entry (its address bits, see below), or 0 |
+//! | [`PAIR_RAW_SET`] | a class setter's compiled entry (its address bits, see below), or 0 |
+//!
+//! A compiled entry has one of two calling conventions, and the word says
+//! which. An INSTANCE accessor's entry takes the receiver as a parameter
+//! (`fn(this) -> value` / `fn(this, v)`); its word is the bare address. A
+//! STATIC accessor's entry (a ClassBody `static get`/`static set`, installed
+//! on the class function object) takes no receiver (`fn() -> value` /
+//! `fn(v)`, `this` armed by the caller); its word carries
+//! [`STATIC_ENTRY_BIT`] above the address. Decoding splits the two into
+//! different [`Accessor`] fields, so a reader that calls with the receiver
+//! as a parameter (`raw_get`/`raw_set`) is never handed a static entry.
//!
//! The two closure words are ordinary traced slots. The raw entries are code
//! addresses stored as their plain bits: an address below 2^48 has none of the
@@ -52,26 +62,44 @@ pub(crate) struct Accessor {
pub get: u64,
/// NaN-boxed setter closure bits, 0 when absent.
pub set: u64,
- /// A class getter's compiled entry, 0 when absent.
+ /// An instance class getter's compiled entry `fn(this) -> value`, 0 when
+ /// absent.
pub raw_get: usize,
- /// A class setter's compiled entry, 0 when absent.
+ /// An instance class setter's compiled entry `fn(this, v)`, 0 when absent.
pub raw_set: usize,
+ /// A static class getter's compiled entry `fn() -> value` (`this` armed
+ /// by the caller), 0 when absent. Never set together with `raw_get`.
+ pub static_get: usize,
+ /// A static class setter's compiled entry `fn(v)` (`this` armed by the
+ /// caller), 0 when absent. Never set together with `raw_set`.
+ pub static_set: usize,
}
/// Largest code address a raw word can hold: below it no NaN-box tag bit is
/// set, so the word is a Number to the collector.
const RAW_ADDRESS_LIMIT: u64 = 1 << 48;
+/// Marks a raw word's entry as a STATIC accessor's (`fn()` / `fn(v)`, no
+/// receiver parameter). It sits just above the address bits and below every
+/// NaN-box tag bit, so the word is still a Number to the collector.
+pub(crate) const STATIC_ENTRY_BIT: u64 = RAW_ADDRESS_LIMIT;
+
+/// The word for one half: its instance entry `raw`, or its static entry
+/// `stat` tagged with [`STATIC_ENTRY_BIT`]; a half has at most one.
#[inline]
-fn raw_word(raw: usize) -> u64 {
- debug_assert!((raw as u64) < RAW_ADDRESS_LIMIT);
- if (raw as u64) < RAW_ADDRESS_LIMIT {
+fn raw_word(raw: usize, stat: usize) -> u64 {
+ debug_assert!(raw == 0 || stat == 0);
+ debug_assert!((raw as u64) < RAW_ADDRESS_LIMIT && (stat as u64) < RAW_ADDRESS_LIMIT);
+ if raw != 0 && (raw as u64) < RAW_ADDRESS_LIMIT {
raw as u64
+ } else if stat != 0 && (stat as u64) < RAW_ADDRESS_LIMIT {
+ stat as u64 | STATIC_ENTRY_BIT
} else {
0
}
}
+/// A raw word's INSTANCE entry, 0 when it holds none (or a static one).
#[inline]
fn raw_of(word: u64) -> usize {
if word < RAW_ADDRESS_LIMIT {
@@ -81,6 +109,16 @@ fn raw_of(word: u64) -> usize {
}
}
+/// A raw word's STATIC entry, 0 when it holds none (or an instance one).
+#[inline]
+fn static_of(word: u64) -> usize {
+ if word & !(RAW_ADDRESS_LIMIT - 1) == STATIC_ENTRY_BIT {
+ (word & (RAW_ADDRESS_LIMIT - 1)) as usize
+ } else {
+ 0
+ }
+}
+
/// The accessor a pair VALUE holds, without re-proving that it is one — for a
/// cache hit whose entry proved it at prime time (the holder's key is an
/// accessor, and a slot of an accessor key is written only by an accessor
@@ -91,11 +129,14 @@ fn raw_of(word: u64) -> usize {
#[inline(always)]
pub(crate) unsafe fn pair_of_value_unchecked(value: u64) -> Accessor {
let w = crate::array::array_elements_ptr((value & POINTER_MASK) as *const ArrayHeader);
+ let (raw_get_word, raw_set_word) = (*w.add(PAIR_RAW_GET), *w.add(PAIR_RAW_SET));
Accessor {
get: closure_of(*w.add(PAIR_GET)),
set: closure_of(*w.add(PAIR_SET)),
- raw_get: raw_of(*w.add(PAIR_RAW_GET)),
- raw_set: raw_of(*w.add(PAIR_RAW_SET)),
+ raw_get: raw_of(raw_get_word),
+ raw_set: raw_of(raw_set_word),
+ static_get: static_of(raw_get_word),
+ static_set: static_of(raw_set_word),
}
}
@@ -131,8 +172,8 @@ pub(crate) unsafe fn pair_new(acc: Accessor) -> *mut ArrayHeader {
// the slots below, after `length` covers them, before anything can read it.
*w.add(PAIR_GET) = get.get_nanbox_u64();
*w.add(PAIR_SET) = set.get_nanbox_u64();
- *w.add(PAIR_RAW_GET) = raw_word(acc.raw_get);
- *w.add(PAIR_RAW_SET) = raw_word(acc.raw_set);
+ *w.add(PAIR_RAW_GET) = raw_word(acc.raw_get, acc.static_get);
+ *w.add(PAIR_RAW_SET) = raw_word(acc.raw_set, acc.static_set);
(*pair).length = PAIR_LEN as u32;
crate::object::gc_slots::rebuild_array_layout_from_slots(pair);
if crate::arena::pointer_in_old_gen(pair as usize) {
@@ -160,11 +201,14 @@ pub(crate) unsafe fn pair_of_value(value: u64) -> Option {
return None;
}
let w = crate::array::array_elements_ptr(pair);
+ let (raw_get_word, raw_set_word) = (*w.add(PAIR_RAW_GET), *w.add(PAIR_RAW_SET));
Some(Accessor {
get: closure_of(*w.add(PAIR_GET)),
set: closure_of(*w.add(PAIR_SET)),
- raw_get: raw_of(*w.add(PAIR_RAW_GET)),
- raw_set: raw_of(*w.add(PAIR_RAW_SET)),
+ raw_get: raw_of(raw_get_word),
+ raw_set: raw_of(raw_set_word),
+ static_get: static_of(raw_get_word),
+ static_set: static_of(raw_set_word),
})
}
@@ -220,8 +264,7 @@ pub(crate) fn pair_from(acc: &crate::object::AccessorDescriptor) -> Accessor {
Accessor {
get: acc.get,
set: acc.set,
- raw_get: 0,
- raw_set: 0,
+ ..Accessor::default()
}
}
diff --git a/crates/perry-runtime/src/object/accessor_pair_tests.rs b/crates/perry-runtime/src/object/accessor_pair_tests.rs
index 0804641778..ab5cd191c4 100644
--- a/crates/perry-runtime/src/object/accessor_pair_tests.rs
+++ b/crates/perry-runtime/src/object/accessor_pair_tests.rs
@@ -27,6 +27,8 @@ fn a_pair_round_trips_both_forms() {
set: 0,
raw_get: 0x5555_1234_5678,
raw_set: 0,
+ static_get: 0,
+ static_set: 0x5555_8765_4320,
};
let pair = pair_new(acc);
let value = crate::value::js_nanbox_pointer(pair as i64).to_bits();
diff --git a/crates/perry-runtime/src/object/class_image.rs b/crates/perry-runtime/src/object/class_image.rs
index 165310b58d..ed5f36de07 100644
--- a/crates/perry-runtime/src/object/class_image.rs
+++ b/crates/perry-runtime/src/object/class_image.rs
@@ -86,15 +86,20 @@ pub(crate) const PARENT_DENSE_CAP: usize = 1 << 16;
/// class_id -> { name -> (func_ptr, param_count, has_rest) } for static methods.
///
-/// OUTER map only takes the fast hasher (see `ClassImageTables`); the INNER
-/// `HashMap` stays on SipHash because its keys are JS-supplied
-/// member names.
-pub type StaticMethodTable = PtrHashMap>;
+/// OUTER map takes the fast pointer hasher (see `ClassImageTables`); the
+/// INNER map is a [`StaticNameMap`], keyed by member name.
+/// (body func_ptr, param_count, has_rest, closure-convention entry or 0).
+pub type StaticMethodTable = PtrHashMap>;
/// class_id -> { name -> accessor halves (and the setter's `.length`) } for
/// static accessors.
-/// Outer map fast-hashed, inner `String`-keyed map deliberately not — see above.
-pub type StaticAccessorTable =
- PtrHashMap>;
+/// Outer map fast-hashed, inner map a [`StaticNameMap`] — see above.
+pub type StaticAccessorTable = PtrHashMap>;
+/// A class's static members by name. The names come from program source, so
+/// the map keeps a randomly keyed, flood-resistant hasher: `ahash::RandomState`,
+/// the runtime's hasher for untrusted string keys (as in `json::parser`), in
+/// place of std's slower SipHash. A static call that misses its site guard
+/// probes this map once per class on the parent chain.
+pub type StaticNameMap = HashMap;
/// `(class_id, is_static, property_name) -> source-order token` for declared
/// string-keyed methods and accessors. The token is the member function's HIR
/// id, which is allocated while walking the class body and therefore orders
@@ -122,10 +127,11 @@ pub type ConstructorFlagTable = PtrHashMap)>;
///
/// Iteration order is not observable for any of these: nothing in the tree
/// iterates them (only `get` / `insert` / `contains`). The `String`-keyed
-/// INNER maps of [`StaticMethodTable`] / [`StaticAccessorTable`], and the
-/// `(u32, String)`-keyed `method_bind_lengths` pair below, deliberately stay
-/// on SipHash — their keys are JS-supplied member names, and the inner maps
-/// are enumerated on paths that reach user-visible output.
+/// INNER maps of [`StaticMethodTable`] / [`StaticAccessorTable`] keep a
+/// randomly keyed hasher ([`StaticNameMap`]), and the `(u32, String)`-keyed
+/// `method_bind_lengths` pair below stays on SipHash: their keys are
+/// JS-supplied member names. The inner maps are enumerated on paths that
+/// reach user-visible output, so no caller may rely on their order.
pub struct ClassImageTables {
pub(crate) vtables: RwLock