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>>, pub(crate) static_methods: RwLock>, diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 7bc2302555..d8b96f4597 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -84,20 +84,20 @@ pub(crate) use state::{ builtin_parent_ctor_in_chain, class_decl_prototype_method_names, class_decl_prototype_object, class_decl_prototype_value, class_decl_prototype_value_for_instance_class, class_delete_own_dynamic_prop, class_dynamic_prop_root_store, class_has_own_dynamic_prop, - class_id_for_decl_prototype_object, class_is_key_deleted, class_mark_key_deleted, - class_object_value_for_cid, class_object_value_root_store, class_own_dynamic_prop_names, - class_own_enumerable_field_names, class_own_static_field_value, class_own_string_member_names, - class_parent_closure, class_parent_closure_root_store, class_prototype_member_names, - class_prototype_method_is_enumerable, class_prototype_method_set_enumerable, - class_prototype_method_value_cache_root_store, class_prototype_object_addr_index_contains, - class_prototype_object_addr_index_rekey, class_prototype_object_root_store, - class_ref_dynamic_prop_root_store, class_register_declared_static_global_slot, - class_static_alias_sync, class_static_clear_defined_attrs, class_static_defined_attrs, + class_id_for_decl_prototype_object, class_object_value_for_cid, class_object_value_root_store, + class_own_dynamic_prop_names, class_own_enumerable_field_names, class_own_static_field_value, + class_own_string_member_names, class_parent_closure, class_parent_closure_root_store, + class_proto_key_deleted, class_prototype_member_names, class_prototype_method_is_enumerable, + class_prototype_method_set_enumerable, class_prototype_method_value_cache_root_store, + class_prototype_object_addr_index_contains, class_prototype_object_addr_index_rekey, + class_prototype_object_root_store, class_ref_dynamic_prop_root_store, + class_register_declared_static_global_slot, class_static_alias_sync, + class_static_clear_defined_attrs, class_static_defined_attrs, class_static_key_deleted, class_static_prototype, class_static_prototype_is_nulled, class_static_prototype_root_clear, - class_static_prototype_root_store, class_static_set_defined_attrs, class_unmark_key_deleted, - decl_prototype_identity_id, global_object_prototype_bits, - is_bound_native_constructor_closure_value, is_non_constructable_builtin_function_value, - parent_closure_in_chain, throw_non_constructable_builtin_function, CLASS_OBJECT_EVER, + class_static_prototype_root_store, class_static_set_defined_attrs, decl_prototype_identity_id, + global_object_prototype_bits, is_bound_native_constructor_closure_value, + is_non_constructable_builtin_function_value, parent_closure_in_chain, + throw_non_constructable_builtin_function, CLASS_OBJECT_EVER, }; pub use state::{ AccessorDecl, ClassVTable, VTableMethodEntry, CLASS_DECL_PROTOTYPE_OBJECTS, @@ -199,7 +199,7 @@ pub(crate) use gc_roots::{ pub(crate) use registration::{ class_accessor_function_value, class_accessor_source_func_ptr, class_own_accessor_ptrs, class_own_setter_length, class_registered_static_accessor_ptrs, - class_static_accessor_function_value, invalidate_class_string_member_order, + invalidate_class_string_member_order, }; pub use registration::{ is_class_id_registered, js_register_class_getter, js_register_class_method, @@ -225,12 +225,13 @@ pub(crate) use parent_static::{ class_dynamic_static_accessor_getter_value, class_has_instance_getter, class_has_own_static_method, class_has_own_symbol_member, class_has_symbol_member_in_chain, class_instance_setter_apply, class_method_bind_length, class_object_own_field_bytes, - class_object_pinned_parent, class_own_symbol_accessor_ptrs, class_own_symbol_member_keys, - class_own_symbol_method, class_private_instance_getter_value, - class_private_instance_setter_apply, class_static_accessor_getter_value, - class_static_accessor_setter_apply, class_symbol_getter_value, class_symbol_setter_apply, - dynamic_value_class_id, get_parent_class_id, lookup_class_symbol_method_in_chain, - lookup_static_method_in_chain, register_class, register_class_dynamic_static_accessor, + class_object_pinned_parent, class_own_static_method_code, class_own_static_method_entry, + class_own_symbol_accessor_ptrs, class_own_symbol_member_keys, class_own_symbol_method, + class_private_instance_getter_value, class_private_instance_setter_apply, + class_static_accessor_getter_value, class_static_accessor_setter_apply, + class_symbol_getter_value, class_symbol_setter_apply, dynamic_value_class_id, + get_parent_class_id, lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, + lookup_static_method_owner, register_class, register_class_dynamic_static_accessor, static_accessor_in_chain, }; pub use parent_static::{ diff --git a/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs index 532d49a560..6a3fdda263 100644 --- a/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs @@ -7,9 +7,6 @@ use super::*; /// parent-class chain so methods registered on a base class are found /// via subclass instances. pub(crate) fn lookup_own_prototype_method(class_id: u32, name: &str) -> Option { - if class_is_key_deleted(class_id, name) { - return None; - } CLASS_PROTOTYPE_METHODS.with(|table| { let guard = table.read().ok()?; let bits = guard.as_ref()?.get(&class_id)?.get(name)?; @@ -24,7 +21,7 @@ pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option let mut cid = class_id; let mut depth = 0usize; while depth < 32 { - if !class_is_key_deleted(cid, name) { + if !class_proto_key_deleted(cid, name) { if let Some(per_class) = map.get(&cid) { if let Some(&bits) = per_class.get(name) { return Some(f64::from_bits(bits)); diff --git a/crates/perry-runtime/src/object/class_registry/decl_accessors.rs b/crates/perry-runtime/src/object/class_registry/decl_accessors.rs index d0b4a52e67..e2bd374995 100644 --- a/crates/perry-runtime/src/object/class_registry/decl_accessors.rs +++ b/crates/perry-runtime/src/object/class_registry/decl_accessors.rs @@ -57,7 +57,7 @@ pub(crate) fn install_decl_prototype_accessor(proto: *mut ObjectHeader, class_id } else { None }; - class_accessor_function_value(raw, is_setter, name, set_length).to_bits() + class_accessor_function_value(raw, is_setter, false, name, set_length).to_bits() } }; let get = scope.root_nanbox_u64(half(raw_get, existing.raw_get, existing.get, false)); @@ -67,6 +67,8 @@ pub(crate) fn install_decl_prototype_accessor(proto: *mut ObjectHeader, class_id set, raw_get, raw_set, + static_get: 0, + static_set: 0, }; proto_h.with_mut_ptr(|p: *mut ObjectHeader| { crate::object::set_builtin_accessor_pair( @@ -87,7 +89,7 @@ pub(crate) fn note_instance_accessor_registered(class_id: u32, name: &str) { return; } let proto = class_decl_prototype_object(class_id); - if !proto.is_null() && !class_is_key_deleted(class_id, name) { + if !proto.is_null() && !class_proto_key_deleted(class_id, name) { install_decl_prototype_accessor(proto, class_id, name); } } @@ -106,8 +108,8 @@ pub(crate) fn decl_prototype_own_accessor(class_id: u32, name: &str) -> Option(); - let declared = - !class_is_key_deleted(class_id, name) && class_own_accessor_ptrs(class_id, name).is_some(); + let declared = !class_proto_key_deleted(class_id, name) + && class_own_accessor_ptrs(class_id, name).is_some(); // SAFETY: `obj` is the live decl prototype; nothing below allocates. let holds = declared || unsafe { diff --git a/crates/perry-runtime/src/object/class_registry/dispatch.rs b/crates/perry-runtime/src/object/class_registry/dispatch.rs index 6317156ac0..fd5c637957 100644 --- a/crates/perry-runtime/src/object/class_registry/dispatch.rs +++ b/crates/perry-runtime/src/object/class_registry/dispatch.rs @@ -59,10 +59,8 @@ pub(crate) fn test_bump_vtable_generation() { /// `Object.getPrototypeOf(instance)`, a `super` chain); /// * `js_register_class_generic_origin` — redirects BOTH prototype-object /// readers and `lookup_prototype_method`'s chain hop to another class id; -/// * the in-place `CLASS_DELETED_KEYS` un-mark inside -/// `class_dynamic_prop_root_store` — re-exposes a `delete`d prototype key. /// -/// Bumped INSIDE those four writers, after the store, so a new call site +/// Bumped INSIDE those three writers, after the store, so a new call site /// cannot forget it — the same enforced-funnel rule `prop_plan_epoch_bump` /// follows. Kept separate from `VTABLE_GEN` precisely so that a consumer of /// this counter does not impose the dispatch-speculation cost that bumping @@ -97,7 +95,7 @@ pub(crate) fn class_lookup_surface_gen_bump() { // and recorded prototype bits all unchanged and the old prototype object // unmutated, so nothing else in that entry's guard can see it — and the // entry would then answer with a different object than the chain walk - // beside it. All four callers are registry stores on cold paths. + // beside it. All three callers are registry stores on cold paths. crate::object::proto_validity::bump_proto_validity(); } diff --git a/crates/perry-runtime/src/object/class_registry/gc_roots.rs b/crates/perry-runtime/src/object/class_registry/gc_roots.rs index eff9940f46..0033089974 100644 --- a/crates/perry-runtime/src/object/class_registry/gc_roots.rs +++ b/crates/perry-runtime/src/object/class_registry/gc_roots.rs @@ -586,11 +586,7 @@ fn visit_metadata_nanbox_key( #[cfg(test)] pub(crate) fn test_clear_class_side_table_roots() { - // Disambiguate: CLASS_DELETED_KEYS is reachable via both `use super::*` - // and `use crate::object::*`; name the canonical definition explicitly. - use super::state::CLASS_DELETED_KEYS; super::state::CLASS_DECLARED_STATIC_GLOBAL_SLOTS.with(|m| m.borrow_mut().clear()); - CLASS_DELETED_KEYS.with(|m| m.borrow_mut().clear()); CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| cache.borrow_mut().clear()); CLASS_PROTOTYPE_METHODS.with(|table| { if let Ok(mut guard) = table.write() { diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 81f710c844..0503f9644c 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -525,7 +525,9 @@ pub(crate) fn template_dynamic_parent_value(class_id: u32) -> f64 { // registered-constructor flat dispatch, which fills user args and // snapshot caps by the signature split. if let Some(parent_cid) = crate::object::get_parent_class_id(class_id) { - if parent_cid != 0 { + // Only a compiled parent class has a class function object; a + // builtin parent id (`extends Error`) never gets one. + if parent_cid != 0 && crate::object::is_class_id_registered(parent_cid) { return crate::object::class_value::class_value(parent_cid); } } @@ -620,14 +622,49 @@ pub unsafe extern "C" fn js_register_class_static_method( .unwrap() .entry(class_id as u32) .or_default() - .insert( - name.clone(), - (func_ptr as usize, param_count as u32, has_rest != 0), - ); + .entry(name.clone()) + .and_modify(|e| { + (e.0, e.1, e.2) = (func_ptr as usize, param_count as u32, has_rest != 0) + }) + .or_insert((func_ptr as usize, param_count as u32, has_rest != 0, 0)); } crate::object::class_value::note_intrinsic_registration(class_id as u32, &name); } +/// Record the `JsFunctionInfo` of the closure-convention entry +/// `__clo(callee, this, args...)` codegen emitted for ClassBody static method `name` of class `class_id`: +/// the body of the method's own function object (one per class and method). +/// Its arity, length and strictness are facts of that info; its name and source +/// were registered on the code. +/// Emitted at module init after `js_register_class_static_method`. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_static_method_entry( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + entry: i64, +) { + if class_id == 0 || name_ptr.is_null() || name_len <= 0 || entry == 0 { + return; + } + let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) + else { + return; + }; + { + let mut guard = CLASS_STATIC_METHODS.write().unwrap(); + let Some(record) = guard + .as_mut() + .and_then(|all| all.get_mut(&(class_id as u32))) + .and_then(|m| m.get_mut(name)) + else { + return; + }; + record.3 = entry as usize; + } + crate::object::class_value::note_intrinsic_registration(class_id as u32, name); +} + fn property_key_string(key: f64) -> Option { let property_key = unsafe { crate::object::js_to_property_key(key) }; if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 { @@ -645,7 +682,13 @@ fn property_key_string(key: f64) -> Option { } } +/// Register a computed-key ClassBody method when the class definition +/// evaluates its key. A static one with a string key is a ClassBody static +/// method like any other: `entry` is the `JsFunctionInfo` of its +/// closure-convention entry (`__clo`), the body of its own function object, whose `name` is the +/// key. #[no_mangle] +#[allow(clippy::too_many_arguments)] pub unsafe extern "C" fn js_register_class_computed_method( class_id: i64, key: f64, @@ -654,6 +697,7 @@ pub unsafe extern "C" fn js_register_class_computed_method( is_static: i64, has_rest: i64, definition_order: i64, + entry: i64, ) { if class_id == 0 || func_ptr == 0 { return; @@ -757,9 +801,34 @@ pub unsafe extern "C" fn js_register_class_computed_method( if guard.is_none() { *guard = Some(crate::fast_hash::new_ptr_hash_map()); } - guard.as_mut().unwrap().entry(class_id).or_default().insert( - name.clone(), - (func_ptr as usize, param_count as u32, has_rest != 0), + guard + .as_mut() + .unwrap() + .entry(class_id) + .or_default() + .entry(name.clone()) + .and_modify(|e| { + (e.0, e.1, e.2, e.3) = ( + func_ptr as usize, + param_count as u32, + has_rest != 0, + entry as usize, + ) + }) + .or_insert(( + func_ptr as usize, + param_count as u32, + has_rest != 0, + entry as usize, + )); + } + if entry != 0 { + // SetFunctionName(F, key): the key is known only now. + // The name is keyed by the body's code; `entry` is its info. + crate::builtins::js_register_function_name( + (*(entry as *const crate::closure::JsFunctionInfo)).code, + name.as_ptr(), + name.len() as u32, ); } crate::object::class_value::note_intrinsic_registration(class_id, &name); @@ -898,19 +967,59 @@ pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { .unwrap_or(false) } -pub(crate) fn lookup_static_method_in_chain( +/// ClassBody static method `name` declared by class `class_id` itself: +/// `(func_ptr, param_count, has_rest)`. +pub(crate) fn class_own_static_method_entry( class_id: u32, name: &str, ) -> Option<(usize, u32, bool)> { let guard = CLASS_STATIC_METHODS.read().ok()?; - let map = guard.as_ref()?; + let e = guard.as_ref()?.get(&class_id)?.get(name).copied()?; + Some((e.0, e.1, e.2)) +} + +/// The closure-convention entry of ClassBody static method `name` declared by +/// class `class_id` itself: the code of its own function object. +pub(crate) fn class_own_static_method_code(class_id: u32, name: &str) -> Option { + let guard = CLASS_STATIC_METHODS.read().ok()?; + let e = guard.as_ref()?.get(&class_id)?.get(name).copied()?; + (e.3 != 0).then_some(e.3) +} + +/// The static method `name` a call on class `class_id` runs: the nearest +/// declaration whose own property on its class's function object is still +/// that declaration. A deleted one is skipped (the parent's applies); a +/// redefined one ends the lookup (the property's value is what runs). +pub(crate) fn lookup_static_method_in_chain( + class_id: u32, + name: &str, +) -> Option<(usize, u32, bool)> { + lookup_static_method_owner(class_id, name).map(|(_, e)| e) +} + +/// [`lookup_static_method_in_chain`] plus the class whose declaration runs. +/// The walk reads the class function objects: a class whose object owns +/// `name` (declared, assigned, or deleted and reassigned) ends it — its +/// declaration when the property still is that declaration's function, +/// otherwise nothing (the property's value is what a call runs). +pub(crate) fn lookup_static_method_owner( + class_id: u32, + name: &str, +) -> Option<(u32, (usize, u32, bool))> { + use crate::object::class_value::StaticMethodProperty; let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { - if let Some(m) = map.get(&cid) { - if let Some(&entry) = m.get(name) { - return Some(entry); + let entry = { + let guard = CLASS_STATIC_METHODS.read().ok()?; + guard.as_ref()?.get(&cid).and_then(|m| m.get(name)).copied() + }; + match crate::object::class_value::static_method_property(cid, name, entry.map(|e| e.3)) { + StaticMethodProperty::Live => { + return entry.map(|e| (cid, (e.0, e.1, e.2))); } + StaticMethodProperty::Replaced => return None, + StaticMethodProperty::Deleted => {} } match get_parent_class_id(cid) { Some(p) if p != 0 && p != cid => { @@ -1784,19 +1893,33 @@ pub(crate) use crate::object::class_meta_registry::get_parent_class_id; /// if found, `None` otherwise. /// Used by `js_assimilate_thenable` (refs #586) and other runtime callers /// that need to probe a class for a method without invoking it. +/// +/// A declared method removed from its class's materialized prototype object +/// (`delete C.prototype.m`) is not provided by that class: the prototype +/// object's own keys are the truth, the vtable entry only names the body. +/// The walk then continues to the parent, as the JS prototype chain does. pub fn lookup_class_method_in_chain(class_id: u32, name: &str) -> Option<(usize, u32, bool, bool)> { - let registry = CLASS_VTABLE_REGISTRY.read().unwrap(); - let reg = registry.as_ref()?; let mut cur = class_id; for _ in 0..32 { - if let Some(vt) = reg.get(&cur) { - if let Some(entry) = vt.methods.get(name) { - return Some(( - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - )); + let found = { + let registry = CLASS_VTABLE_REGISTRY.read().unwrap(); + let reg = registry.as_ref()?; + reg.get(&cur) + .and_then(|vt| vt.methods.get(name)) + .map(|entry| { + ( + entry.func_ptr, + entry.param_count, + entry.has_synthetic_arguments, + entry.has_rest, + ) + }) + }; + if let Some(entry) = found { + // Checked with the registry lock released: the deletedness probe + // reads the class tables again. + if !super::class_proto_key_deleted(cur, name) { + return Some(entry); } } match get_parent_class_id(cur) { diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index cd3b3493af..b2b69356b6 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -126,6 +126,8 @@ pub(crate) fn register_class_dynamic_static_accessor( set: set_bits.map(|_| set.get_nanbox_u64()).unwrap_or(have.set), raw_get: if get_bits.is_some() { 0 } else { have.raw_get }, raw_set: if set_bits.is_some() { 0 } else { have.raw_set }, + static_get: if get_bits.is_some() { 0 } else { have.static_get }, + static_set: if set_bits.is_some() { 0 } else { have.static_set }, }; let enumerable = enumerable .or(existing.map(|(_, e, _)| e)) @@ -319,7 +321,7 @@ pub(crate) unsafe fn call_private_static_method_for_owner( args_ptr: *const f64, args_len: usize, ) -> Option { - let (func_ptr, param_count, has_rest) = CLASS_STATIC_METHODS + let (func_ptr, param_count, has_rest, _) = CLASS_STATIC_METHODS .read() .ok()? .as_ref()? diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index 3e62dd6f8c..e808ddf10a 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -211,9 +211,6 @@ pub(crate) fn invalidate_class_prototype_fast_guards() { } pub(crate) fn class_prototype_method_root_store(class_id: u32, name: String, value_bits: u64) { - // Assignment / defineProperty after `delete C.prototype.m` recreates the - // own property and must make it visible to dispatch again. - class_unmark_key_deleted(class_id, &name); CLASS_PROTOTYPE_METHODS.with(|table| { let mut guard = table.write().unwrap(); if guard.is_none() { diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index a6499bee71..8618d9b6a5 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -232,6 +232,17 @@ static CLASS_ACCESSOR_SETTER_THUNK_INFO: crate::closure::JsFunctionInfo = class_accessor_setter_thunk as crate::codegen_abi::JsBody1, ); +static CLASS_STATIC_ACCESSOR_GETTER_THUNK_INFO: crate::closure::JsFunctionInfo = + crate::closure::JsFunctionInfo::of( + class_static_accessor_getter_thunk + as crate::codegen_abi::JsBody0, + ); +static CLASS_STATIC_ACCESSOR_SETTER_THUNK_INFO: crate::closure::JsFunctionInfo = + crate::closure::JsFunctionInfo::of( + class_static_accessor_setter_thunk + as crate::codegen_abi::JsBody1, + ); + /// Trampoline giving a raw vtable getter func_ptr (`fn(this) -> f64`) the /// closure calling convention. The receiver is the `this` argument passed /// by the method-call dispatch the closure value travels through. @@ -248,37 +259,9 @@ extern "C" fn class_accessor_getter_thunk( f(this) } -/// Trampoline for a raw vtable setter func_ptr (`fn(this, value) -> f64`). -extern "C" fn class_accessor_setter_thunk( - closure: *const crate::closure::ClosureHeader, - this: crate::closure::JsThis, - value: f64, -) -> f64 { - let raw = crate::closure::js_closure_get_capture_ptr(closure, 0) as usize; - if raw == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let this = this.as_f64(); - let f = unsafe { crate::closure::body_call::js_method_body_fn!(raw as *const u8; a0) }; - f(this, value) -} - -static CLASS_STATIC_ACCESSOR_GETTER_THUNK_INFO: crate::closure::JsFunctionInfo = - crate::closure::JsFunctionInfo::of( - class_static_accessor_getter_thunk - as crate::codegen_abi::JsBody0, - ); -static CLASS_STATIC_ACCESSOR_SETTER_THUNK_INFO: crate::closure::JsFunctionInfo = - crate::closure::JsFunctionInfo::of( - class_static_accessor_setter_thunk - as crate::codegen_abi::JsBody1, - ); - -/// Trampoline for a raw STATIC getter (`fn() -> f64`, #11669). A compiled -/// static accessor body takes no `this` parameter: it reads `this` from the -/// static-this override and its private statics from the owner stack, exactly -/// as `class_value::class_static_accessor_call_get` arms them. Capture 1 is the -/// declaring class id (the private-static owner). +/// Trampoline for a raw STATIC getter func_ptr (`fn() -> f64`): the closure +/// call's `this` is the class the getter runs on, armed as its static `this` +/// and its private/capture owner exactly as a direct static access arms them. extern "C" fn class_static_accessor_getter_thunk( closure: *const crate::closure::ClosureHeader, this: crate::closure::JsThis, @@ -288,9 +271,8 @@ extern "C" fn class_static_accessor_getter_thunk( return f64::from_bits(crate::value::TAG_UNDEFINED); } let this = this.as_f64(); - let owner = static_accessor_thunk_owner(closure, this); crate::object::static_this_arm_if_unarmed(this); - crate::object::static_private_owner_push(owner); + crate::object::static_private_owner_push(this); let f = unsafe { crate::closure::body_call::js_bare_body_fn!(raw as *const u8;) }; let result = f(); crate::object::static_private_owner_pop(); @@ -298,10 +280,8 @@ extern "C" fn class_static_accessor_getter_thunk( result } -/// Trampoline for a raw STATIC setter (`fn(value) -> f64`, #11669). The -/// instance trampoline calls `raw(this, value)`, which hands a static setter -/// the receiver (the class itself) as its value; see -/// [`class_static_accessor_getter_thunk`] for the `this`/owner protocol. +/// Trampoline for a raw STATIC setter func_ptr (`fn(value) -> f64`): the +/// value is its only parameter; `this` is armed as for the getter. extern "C" fn class_static_accessor_setter_thunk( closure: *const crate::closure::ClosureHeader, this: crate::closure::JsThis, @@ -312,26 +292,28 @@ extern "C" fn class_static_accessor_setter_thunk( return f64::from_bits(crate::value::TAG_UNDEFINED); } let this = this.as_f64(); - let owner = static_accessor_thunk_owner(closure, this); crate::object::static_this_arm_if_unarmed(this); - crate::object::static_private_owner_push(owner); + crate::object::static_private_owner_push(this); let f = unsafe { crate::closure::body_call::js_bare_body_fn!(raw as *const u8; value) }; - let _ = f(value); + let result = f(value); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); - f64::from_bits(crate::value::TAG_UNDEFINED) + result } -/// The private-static owner of a static accessor trampoline: the class -/// function object of the declaring class id in capture 1 (a Number, never a -/// heap word), or `this` when absent. -fn static_accessor_thunk_owner(closure: *const crate::closure::ClosureHeader, this: f64) -> f64 { - let cid = crate::closure::js_closure_get_capture_f64(closure, 1); - if cid.is_finite() && cid >= 1.0 { - crate::object::class_value::class_value(cid as u32) - } else { - this +/// Trampoline for a raw vtable setter func_ptr (`fn(this, value) -> f64`). +extern "C" fn class_accessor_setter_thunk( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + let raw = crate::closure::js_closure_get_capture_ptr(closure, 0) as usize; + if raw == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); } + let this = this.as_f64(); + let f = unsafe { crate::closure::body_call::js_method_body_fn!(raw as *const u8; a0) }; + f(this, value) } /// Return the raw getter/setter body wrapped by a descriptor-reflection @@ -368,50 +350,26 @@ pub(crate) unsafe fn class_accessor_source_func_ptr( pub(crate) fn class_accessor_function_value( raw_ptr: usize, is_setter: bool, + is_static: bool, prop_name: &str, setter_length: Option, -) -> f64 { - accessor_function_value(raw_ptr, is_setter, prop_name, setter_length, None) -} - -/// [`class_accessor_function_value`] for a STATIC accessor of `class_id`: the -/// compiled entry takes no `this` parameter (`fn() -> f64` / `fn(v)`), so the -/// closure uses the static trampolines (#11669). -pub(crate) fn class_static_accessor_function_value( - raw_ptr: usize, - is_setter: bool, - prop_name: &str, - setter_length: Option, - class_id: u32, -) -> f64 { - accessor_function_value(raw_ptr, is_setter, prop_name, setter_length, Some(class_id)) -} - -fn accessor_function_value( - raw_ptr: usize, - is_setter: bool, - prop_name: &str, - setter_length: Option, - static_class_id: Option, ) -> f64 { if raw_ptr == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let thunk = match (static_class_id.is_some(), is_setter) { - (false, true) => &CLASS_ACCESSOR_SETTER_THUNK_INFO, + // The thunk matches the entry calling convention: an instance entry takes + // the receiver as a parameter, a static one is a bare body. + let thunk = match (is_setter, is_static) { + (true, false) => &CLASS_ACCESSOR_SETTER_THUNK_INFO, (false, false) => &CLASS_ACCESSOR_GETTER_THUNK_INFO, (true, true) => &CLASS_STATIC_ACCESSOR_SETTER_THUNK_INFO, - (true, false) => &CLASS_STATIC_ACCESSOR_GETTER_THUNK_INFO, + (false, true) => &CLASS_STATIC_ACCESSOR_GETTER_THUNK_INFO, }; - let captures = if static_class_id.is_some() { 2 } else { 1 }; - let closure = crate::closure::js_closure_alloc(thunk, captures); + let closure = crate::closure::js_closure_alloc(thunk, 1); if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } crate::closure::js_closure_set_capture_ptr(closure, 0, raw_ptr as i64); - if let Some(cid) = static_class_id { - crate::closure::js_closure_set_capture_f64(closure, 1, cid as f64); - } // Spec `.length`: params before the first default/rest. A getter takes no // params (0); a setter takes exactly one formal param — but `set m(x = 42)` // has `.length === 0` (defaults don't count). Codegen records the setter's diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index 886b33d3bd..b8a023d162 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -7,8 +7,6 @@ use std::collections::HashMap; use std::sync::RwLock; crate::perry_thread_local! { - pub(crate) static CLASS_DELETED_KEYS: std::cell::RefCell>> = - std::cell::RefCell::new(std::collections::HashMap::new()); /// Backing LLVM globals for declared static fields, keyed exactly like /// `CLASS_DYNAMIC_PROPS`. Direct compiled reads use these cells, while /// computed/member writes reach the runtime side table. Remembering the @@ -42,33 +40,62 @@ pub(crate) fn throw_non_constructable_builtin_function() -> ! { super::super::object_ops::throw_object_type_error(b"Function is not a constructor") } -pub(crate) fn class_mark_key_deleted(class_id: u32, key: &str) { - if class_id == 0 { - return; +/// Has `delete` removed class `class_id`'s own ClassBody prototype member +/// `name` (a method, an accessor, or `constructor`)? Derived from the object +/// that owns the member: it was declared, the class's decl prototype exists, +/// and neither that object nor a runtime prototype assignment holds the key. +/// Every delete of a prototype member retires the per-name prototype fast +/// guard first, so a name whose guard is intact was never deleted anywhere. +pub(crate) fn class_proto_key_deleted(class_id: u32, name: &str) -> bool { + if class_id == 0 + || !class_prototype_fast_guard_invalidated_for_method(class_prototype_method_guard_slot( + name, + )) + { + return false; } - CLASS_DELETED_KEYS.with(|m| { - m.borrow_mut() - .entry(class_id) - .or_default() - .insert(key.to_string()); - }); -} - -pub(crate) fn class_is_key_deleted(class_id: u32, key: &str) -> bool { - CLASS_DELETED_KEYS.with(|m| { - m.borrow() - .get(&class_id) - .map(|keys| keys.contains(key)) + let declared = name == "constructor" + || class_own_accessor_ptrs(class_id, name).is_some() + || super::super::native_module::class_has_own_method(class_id, name); + if !declared { + return false; + } + let proto = class_decl_prototype_object(class_id); + if proto.is_null() { + // Never materialized: nothing was deleted from it. + return false; + } + let assigned = CLASS_PROTOTYPE_METHODS.with(|table| { + table + .read() + .ok() + .and_then(|g| { + g.as_ref() + .map(|m| m.get(&class_id).is_some_and(|p| p.contains_key(name))) + }) .unwrap_or(false) - }) + }); + // SAFETY: `proto` is this realm's live decl prototype; nothing below + // allocates. + !assigned + && !unsafe { + let keys = crate::object::object_keys(proto); + let arr = keys.arr(); + !arr.is_null() + && crate::object::keys_find_slot_by_bytes_resolved( + arr, + keys.count(), + name.as_bytes(), + ) + .is_some() + } } -pub(crate) fn class_unmark_key_deleted(class_id: u32, key: &str) { - CLASS_DELETED_KEYS.with(|m| { - if let Some(keys) = m.borrow_mut().get_mut(&class_id) { - keys.remove(key); - } - }); +/// Has `delete` removed class `class_id`'s own static member `name` (a +/// ClassBody static method or accessor, or the intrinsic `name` / `length`)? +/// Derived from the class function object that owns it. +pub(crate) fn class_static_key_deleted(class_id: u32, name: &str) -> bool { + crate::object::class_value::class_static_key_deleted(class_id, name) } /// Record `C. = value` in the class-ref side table that dynamic reads @@ -83,26 +110,10 @@ pub(crate) fn class_unmark_key_deleted(class_id: u32, key: &str) { /// constructor runs it once per construction (144,000 times in /// gc-handoff/apps/shapes.ts) and the key exists after the first. /// -/// The in-place update also skips the `CLASS_DELETED_KEYS` probe — but only -/// when NO class key has ever been deleted, which is the state of essentially -/// every program (`delete C.x` on a class constructor is vanishingly rare). -/// Once anything has been deleted the original sequence runs verbatim, so the -/// interaction between a deleted PROTOTYPE key and a same-named static field -/// (`class C { m() {} static m = 1 }` — both land under one class_id) keeps -/// whatever behaviour it had. +/// A static store touches only the class function object: the prototype +/// side lives on the prototype object, so `C.m = 1` can never resurrect a +/// deleted `C.prototype.m`. pub(crate) fn class_dynamic_prop_root_store(class_id: u32, name: &str, value: f64) { - // Un-marking re-exposes a previously `delete`d prototype key to - // `class_instance_has_member` / `lookup_prototype_method` — the one - // direction a cached "this chain resolves nothing" verdict must not - // survive (#10696). - let was_deleted = CLASS_DELETED_KEYS.with(|m| { - m.borrow_mut() - .get_mut(&class_id) - .is_some_and(|keys| keys.remove(name)) - }); - if was_deleted { - super::class_lookup_surface_gen_bump(); - } // The class function object's own-property bag (barriered, traced). crate::object::class_value::class_static_set(class_id, name, value); class_static_alias_sync(class_id, name); @@ -125,8 +136,7 @@ pub(crate) fn class_static_alias_sync(class_id: u32, name: &str) { }) else { return; }; - let plain = !class_is_key_deleted(class_id, name) - && class_static_defined_attrs(class_id, name).is_none_or(|(writable, _, _)| writable) + let plain = class_static_defined_attrs(class_id, name).is_none_or(|(writable, _, _)| writable) && !crate::object::class_value::class_static_has_own_accessor(class_id, name); let value = plain .then(|| crate::object::class_value::class_static_get(class_id, name)) @@ -202,10 +212,11 @@ pub(crate) fn class_own_dynamic_prop_names(class_id: u32) -> Vec { .collect() } -/// #7190: record a `defineProperty`-installed static key's attributes. Called -/// only from the define path; `static x = …` never touches it, so a declared -/// field keeps its CreateDataPropertyOrThrow `(writable, enumerable) = (true, -/// true)` reporting. +/// #7190: set the attributes of class `class_id`'s own static data property +/// `name`. They are the key attributes of the class function object's +/// own-property object, as for any ordinary object: a declared `static x = …` +/// field never sets any, so it keeps CreateDataPropertyOrThrow's +/// `(true, true, true)`, and a delete removes them with the key. pub(crate) fn class_static_set_defined_attrs( class_id: u32, name: &str, @@ -213,37 +224,56 @@ pub(crate) fn class_static_set_defined_attrs( enumerable: bool, configurable: bool, ) { - crate::object::CLASS_STATIC_DEFINED_ATTRS.with(|m| { - m.borrow_mut() - .entry(class_id) - .or_default() - .insert(name.to_string(), (writable, enumerable, configurable)); - }); + { + let _no_collect = crate::gc::GcSuppressScope::new(); + let ptr = crate::object::class_value::class_value_ptr(class_id) as usize; + // SAFETY: this agent's live class function object; no collection in + // this scope. + let bag = unsafe { crate::closure::props::bag_ensure(ptr) }; + crate::object::set_builtin_property_attrs( + bag as usize, + name.to_string(), + crate::object::PropertyAttrs::new(writable, enumerable, configurable), + ); + } class_static_alias_sync(class_id, name); } -/// Forget the recorded attributes of static `name` (it becomes an ordinary -/// writable, enumerable, configurable data property again) and re-sync its -/// compiled alias. A static FIELD definition does this: DefineField creates -/// the property with CreateDataPropertyOrThrow, replacing e.g. the class's -/// own intrinsic `name`. +/// Static `name` becomes an ordinary writable, enumerable, configurable data +/// property again, and its compiled alias is re-synced. A static FIELD +/// definition does this: DefineField creates the property with +/// CreateDataPropertyOrThrow, replacing e.g. the class's own intrinsic `name`. pub(crate) fn class_static_clear_defined_attrs(class_id: u32, name: &str) { - let removed = crate::object::CLASS_STATIC_DEFINED_ATTRS.with(|m| { - m.borrow_mut() - .get_mut(&class_id) - .and_then(|k| k.remove(name)) - .is_some() - }); - if removed { - class_static_alias_sync(class_id, name); + let Some(ptr) = crate::object::class_value::class_value_if_minted(class_id) else { + return; + }; + // SAFETY: this agent's live class function object. + let bag = unsafe { crate::closure::props::bag_of(ptr as usize) }; + if bag.is_null() { + return; } + crate::object::clear_property_attrs(bag as usize, name); + class_static_alias_sync(class_id, name); } -/// `(writable, enumerable)` if this static key was installed by -/// `Object.defineProperty`; `None` for a declared `static x = …` field. +/// `(writable, enumerable, configurable)` of class `class_id`'s own static +/// DATA property `name`; `None` when it owns no such data property. Reads +/// the key of the function object's own-property object and never mints the +/// function object (one never created owns no properties). pub(crate) fn class_static_defined_attrs(class_id: u32, name: &str) -> Option<(bool, bool, bool)> { - crate::object::CLASS_STATIC_DEFINED_ATTRS - .with(|m| m.borrow().get(&class_id).and_then(|k| k.get(name)).copied()) + let ptr = crate::object::class_value::class_value_if_minted(class_id)? as usize; + // SAFETY: this agent's live class function object. + unsafe { + if crate::object::is_internal_runtime_key(name) + || crate::closure::props::bag_get(ptr, name.as_bytes()).is_none() + { + return None; + } + let bag = crate::closure::props::bag_of(ptr); + let attrs = crate::object::get_property_attrs(bag as usize, name) + .unwrap_or(crate::object::PropertyAttrs::new(true, true, true)); + Some((attrs.writable(), attrs.enumerable(), attrs.configurable())) + } } pub(crate) fn class_static_key_is_non_enumerable(class_id: u32, name: &str) -> bool { @@ -1307,22 +1337,7 @@ pub(crate) fn class_decl_prototype_value_for_instance_class(class_id: u32) -> Op } pub(crate) fn global_object_prototype_bits() -> Option { - let object_ctor = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - let ctor_bits = object_ctor.to_bits(); - if (ctor_bits >> 48) != 0x7FFD { - return None; - } - let ctor_ptr = (ctor_bits & crate::value::POINTER_MASK) as usize; - if ctor_ptr == 0 { - return None; - } - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_bits = proto.to_bits(); - if (proto_bits >> 48) == 0x7FFD { - Some(proto_bits) - } else { - None - } + crate::object::object_prototype_intrinsic_bits() } #[cfg(test)] @@ -1419,28 +1434,18 @@ mod class_dynamic_prop_store_tests { assert_eq!(keys, vec!["made".to_string(), "other".to_string()]); } - /// The fast path is gated on "nothing has ever been deleted". Once a key - /// IS deleted, a re-store must still clear it from the deleted set — the - /// behaviour the unconditional probe used to provide. + /// `delete C.k` removes the key from the class function object; a later + /// store defines it again. #[test] - fn store_after_delete_clears_the_deleted_mark() { + fn store_after_delete_defines_the_key_again() { let cid = 0x7c01_0002; class_dynamic_prop_root_store(cid, "k", 1.0); - // Delete the way `delete C.k` does: drop the value AND mark the key. class_delete_own_dynamic_prop(cid, "k"); - class_mark_key_deleted(cid, "k"); - assert!(class_is_key_deleted(cid, "k")); assert_eq!(stored(cid, "k"), None); class_dynamic_prop_root_store(cid, "k", 2.0); - assert!( - !class_is_key_deleted(cid, "k"), - "re-storing a deleted static key must un-delete it" - ); assert_eq!(stored(cid, "k"), Some(2.0)); - // And a subsequent store, now on the slow arm (the deleted-keys map - // is non-empty for the whole process), still updates the value. class_dynamic_prop_root_store(cid, "k", 3.0); assert_eq!(stored(cid, "k"), Some(3.0)); } diff --git a/crates/perry-runtime/src/object/class_value.rs b/crates/perry-runtime/src/object/class_value.rs index 4779e87ca3..e338f1284f 100644 --- a/crates/perry-runtime/src/object/class_value.rs +++ b/crates/perry-runtime/src/object/class_value.rs @@ -274,9 +274,25 @@ fn class_value_mint(class_id: u32) -> *mut ClosureHeader { for key in INTRINSIC_OWN_DATA_KEYS { install_intrinsic_own_data(class_id, key); } - // ClassBody static accessors, in ClassBody order. + // MakeConstructor: `prototype` { !w, !e, !c } is created with the class, + // after `length` and `name` and before every ClassBody static, so the own + // key order is the bag's creation order. + let proto = super::class_registry::class_decl_prototype_value(class_id); + if crate::value::JSValue::from_bits(proto.to_bits()).is_pointer() { + // SAFETY: `ptr` is the class closure minted above. + unsafe { crate::closure::props::bag_define_value(ptr as usize, "prototype", proto) }; + super::class_registry::class_static_set_defined_attrs( + class_id, + "prototype", + false, + false, + false, + ); + } + // ClassBody static methods and accessors, in ClassBody order. for key in super::class_registry::class_own_string_member_names(class_id, true) { install_declared_static_accessor(class_id, &key); + install_declared_static_method(class_id, &key); } ptr } @@ -366,9 +382,13 @@ pub(crate) fn note_intrinsic_registration(class_id: u32, key: &str) { if super::class_registry::class_registered_static_accessor_ptrs(class_id, key).is_some() { install_declared_static_accessor(class_id, key); } - if !INTRINSIC_OWN_DATA_KEYS.contains(&key) { - return; + if INTRINSIC_OWN_DATA_KEYS.contains(&key) { + note_intrinsic_key_registration(class_id, key); } + install_declared_static_method(class_id, key); +} + +fn note_intrinsic_key_registration(class_id: u32, key: &str) { if holds_intrinsic(class_id, key) { if static_member_owns(class_id, key) { class_static_remove(class_id, key); @@ -377,12 +397,364 @@ pub(crate) fn note_intrinsic_registration(class_id: u32, key: &str) { class_static_set(class_id, key, value); } } else if class_static_get(class_id, key).is_none() - && !super::class_registry::class_is_key_deleted(class_id, key) + // SAFETY: minted above (`class_value_cached`). + && !unsafe { crate::closure::props::state_is_deleted(class_value_ptr(class_id) as usize, key) } { install_intrinsic_own_data(class_id, key); } } +/// `delete C.` removed own `name` of class `class_id`: an intrinsic +/// `name` / `length` is remembered on the object (as for any function, #3655), +/// so a later registration does not install it again. +pub(crate) fn note_static_key_deleted(class_id: u32, name: &str) { + if INTRINSIC_OWN_DATA_KEYS.contains(&name) { + // SAFETY: this agent's live class closure. + unsafe { + crate::closure::props::state_mark_deleted(class_value_ptr(class_id) as usize, name) + }; + } +} + +/// Has `delete` removed class `class_id`'s own static member `name` — a +/// ClassBody static method or accessor, or the intrinsic `name` / `length`? +/// Derived from the function object: the member was declared and the object +/// no longer has the key. A never-minted object has deleted nothing. +pub(crate) fn class_static_key_deleted(class_id: u32, name: &str) -> bool { + if name.starts_with('#') || is_internal_static_key(name) { + return false; + } + let Some(ptr) = class_value_if_minted(class_id) else { + return false; + }; + let declared = static_member_owns(class_id, name) + || (INTRINSIC_OWN_DATA_KEYS.contains(&name) + && intrinsic_own_data_registered(class_id, name)); + // SAFETY: this agent's live class closure; the lookup does not allocate. + declared && !unsafe { crate::closure::props::bag_has_own(ptr as usize, name.as_bytes()) } +} + +fn intrinsic_own_data_registered(class_id: u32, key: &str) -> bool { + match key { + "length" => super::class_registry::class_length_for_id(class_id).is_some(), + "name" => super::class_registry::class_name_for_id(class_id).is_some(), + _ => false, + } +} + +/// What own property `name` of class `class_id`'s function object says about +/// the ClassBody static method `name` whose code is `func_ptr`. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum StaticMethodProperty { + /// The property is still the declaration's function: dispatch it. + Live, + /// `delete` removed it: the lookup continues at the parent class. + Deleted, + /// The program redefined it: the property's value wins. + Replaced, +} + +/// Class `class_id`'s answer for static method `name` during a chain walk. +/// `declared` is its own ClassBody declaration of `name` (the declaration's +/// closure-convention entry, 0 when it has none), `None` when it declares no +/// such method. +pub(crate) fn static_method_property( + class_id: u32, + name: &str, + declared: Option, +) -> StaticMethodProperty { + let live_or_next = if declared.is_some() { + StaticMethodProperty::Live + } else { + StaticMethodProperty::Deleted + }; + if name.starts_with('#') || is_internal_static_key(name) { + return live_or_next; + } + // A never-minted object owns exactly its declarations. + let Some(ptr) = class_value_if_minted(class_id) else { + return live_or_next; + }; + let code = declared.unwrap_or(0); + // SAFETY: this agent's live class closure; nothing below allocates. + unsafe { + match crate::closure::props::bag_get(ptr as usize, name.as_bytes()) { + Some(v) if code != 0 && static_method_code_of(v) == Some(code) => { + StaticMethodProperty::Live + } + Some(_) => StaticMethodProperty::Replaced, + None if crate::closure::props::bag_has_own(ptr as usize, name.as_bytes()) => { + StaticMethodProperty::Replaced + } + // A declaration without an entry is never installed as a property. + None if declared == Some(0) => StaticMethodProperty::Live, + None => StaticMethodProperty::Deleted, + } + } +} + +/// The `JsFunctionInfo` of `value` when it is a function object (a closure): the +/// identity of the body it runs. +unsafe fn static_method_code_of(value: f64) -> Option { + let js = crate::JSValue::from_bits(value.to_bits()); + if !js.is_pointer() { + return None; + } + let f = js.as_pointer::(); + if !crate::closure::is_closure_ptr(f as usize) { + return None; + } + Some((*f).info as usize) +} + +/// The class whose ClassBody static method `name` a read of `name` on class +/// `class_id` finds, when the property found is still that declaration's +/// function object: its value, minting the owner's function object (so every +/// subclass reads the one own property, `Q.a === P.a`). +pub(crate) fn inherited_static_method_value(class_id: u32, name: &str) -> Option { + let (owner, _) = super::class_registry::lookup_static_method_owner(class_id, name)?; + super::class_registry::class_own_static_method_code(owner, name)?; + class_value(owner); + class_static_get(owner, name) +} + +/// Does own `key` of the class function object `ptr` hold its ClassBody +/// static method's function object (the declaration's code)? That fact is +/// part of the object's shape: a write that ends it transitions the shape +/// (`object::class_value::js_class_static_call_guard`). +pub(crate) unsafe fn holds_declared_static_method(ptr: usize, key: &str) -> Option { + let cid = class_closure_id_unchecked(ptr as *const ClosureHeader)?; + let code = super::class_registry::class_own_static_method_code(cid, key)?; + let v = crate::closure::props::bag_get(ptr, key.as_bytes())?; + let js = crate::JSValue::from_bits(v.to_bits()); + if !js.is_pointer() { + return None; + } + let f = js.as_pointer::(); + (crate::closure::is_closure_ptr(f as usize) && (*f).info as usize == code).then_some(v) +} + +/// [`js_class_static_call_guard`] for a call whose receiver is a value +/// (`(C as any).m()`, a local holding the class): the receiver must be its +/// class's function object on this agent, or the call reads the property. +/// +/// # Safety +/// As [`js_class_static_call_guard`]. +#[no_mangle] +pub unsafe extern "C" fn js_class_static_value_call_guard( + receiver: f64, + owner_id: i32, + name_ptr: *const u8, + name_len: i64, + body: i64, + memo: *mut u64, +) -> i32 { + let bits = receiver.to_bits(); + let Some(cid) = class_value_id_bits(bits) else { + return 0; + }; + if legacy_class_value_word(bits).is_none() { + let js = crate::JSValue::from_bits(bits); + if !js.is_pointer() + || class_value_cached(cid).map(|c| c as usize) != Some(js.as_pointer::() as usize) + { + return 0; + } + } + js_class_static_call_guard(cid as i32, owner_id, name_ptr, name_len, body, memo) +} + +/// The shape code of class `class_id`'s function object for a static-call +/// memo: 0 when this agent never minted it (it owns exactly its +/// declarations), `u32::MAX - 1` when it has no own-property bag, else the +/// bag's ShapeId. +fn static_call_shape_code(class_id: u32) -> u32 { + match class_value_cached(class_id) { + None => 0, + // SAFETY: this agent's live class closure; a shape load. + Some(c) => unsafe { + let bag = crate::closure::props::bag_of(c as usize); + if bag.is_null() { + u32::MAX - 1 + } else { + super::shapes::object_shape_stamp(bag) + } + }, + } +} + +/// Codegen's direct static call `C.m(..)` runs the declared body `body` only +/// while the property the call reads — own `m` of C, or of the class `owner` +/// it inherits from — is still that declaration's function object. The fact +/// lives in the class function objects' shapes: installing a declaration, +/// storing a different value over it, and deleting it each transition the +/// shape (`closure::props`), so an unchanged pair of shapes proves it. +/// +/// `memo` is the site's [`StaticCallMemo`]. The site's hit is inline in +/// compiled code: it loads the two function objects' shape words through the +/// memo's pointers and compares them with `key`; only a miss calls this, +/// which re-validates by reading the property and re-arms the memo (only for +/// a one-link chain, whose two shapes cover every object the read consults). +/// +/// # Safety +/// `name_ptr` points at `name_len` bytes; `memo` is null or the site's +/// [`StaticCallMemo`] (thread-local when the program starts workers, so the +/// function objects it names are this agent's). +#[no_mangle] +pub unsafe extern "C" fn js_class_static_call_guard( + class_id: i32, + owner_id: i32, + name_ptr: *const u8, + name_len: i64, + body: i64, + memo: *mut u64, +) -> i32 { + let cid = class_id as u32; + let owner = owner_id as u32; + // SAFETY: null or the site's memo (see `# Safety`). + let memo = (!memo.is_null()).then(|| &mut *(memo as *mut StaticCallMemo)); + if name_ptr.is_null() || name_len <= 0 { + return 1; + } + let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) + else { + return 1; + }; + match super::class_registry::lookup_static_method_owner(cid, name) { + Some((found, (func_ptr, ..))) if func_ptr == body as usize => { + let one_link = + found == cid || super::class_registry::get_parent_class_id(cid) == Some(found); + if let Some(m) = memo { + if owner != 0 && found == owner && one_link { + arm_static_call_memo(m, cid, owner); + } + } + 1 + } + _ => 0, + } +} + +/// A static-call site's memo (`perry-codegen/src/expr/static_method.rs`), +/// four words the emitted hit reads (`perry_abi::STATIC_CALL_MEMO_*`): +/// `key` = (C's shape word | owner's shape word << 32) of the last +/// validation; the two class function objects whose own-property objects' +/// shape words the hit loads (pinned for the agent's life, so the pointers +/// never go stale); and C's function object as a value, for a site whose +/// receiver is a value. +/// +/// Never armed, `c` and `owner` point at a constant of the site's whose +/// own-property word points at itself, a shape word of 0, and `key` is all +/// ones, so the hit needs no "armed?" test: that shape word never equals a +/// half of the unarmed key. +#[repr(C)] +pub struct StaticCallMemo { + pub key: u64, + pub c: usize, + pub owner: usize, + pub c_value: u64, +} + +const _: () = { + assert!( + std::mem::offset_of!(StaticCallMemo, key) + == crate::codegen_abi::STATIC_CALL_MEMO_KEY_OFFSET + ); + #[cfg(target_pointer_width = "64")] + { + assert!( + std::mem::offset_of!(StaticCallMemo, c) + == crate::codegen_abi::STATIC_CALL_MEMO_C_OFFSET + ); + assert!( + std::mem::offset_of!(StaticCallMemo, owner) + == crate::codegen_abi::STATIC_CALL_MEMO_OWNER_OFFSET + ); + assert!( + std::mem::offset_of!(StaticCallMemo, c_value) + == crate::codegen_abi::STATIC_CALL_MEMO_VALUE_OFFSET + ); + } +}; + +/// Arm `memo` for the one-link chain (`cid`, `owner`) just validated. The +/// inline hit compares raw shape words, so it is armed only when both are +/// ShapeIds (a raw word equals a ShapeId only when it is that ShapeId); the +/// function objects are minted here (an unobservable act: a fresh object owns +/// exactly its declarations), so a class used only through static calls +/// still gets the inline hit. +fn arm_static_call_memo(memo: &mut StaticCallMemo, cid: u32, owner: u32) { + let c = class_value_ptr(cid); + let o = class_value_ptr(owner); + let (sc, so) = (static_call_shape_code(cid), static_call_shape_code(owner)); + if !super::shapes::is_shape_id(sc) || !super::shapes::is_shape_id(so) { + return; + } + // GC_STORE_AUDIT(ROOT): a compiled site's memo naming PINNED class + // function objects (never move), also rooted by the class-value table. + memo.c = c as usize; + memo.owner = o as usize; + memo.c_value = crate::value::POINTER_TAG | (c as u64); + memo.key = u64::from(sc) | u64::from(so) << 32; +} + +/// Does class `class_id`'s function object own static method `name` as a data +/// property it has not deleted (the declaration, or a value put in its place)? +pub(crate) fn class_static_owns_method(class_id: u32, name: &str) -> bool { + if class_static_get(class_id, name).is_some() { + return true; + } + super::class_registry::class_own_static_method_entry(class_id, name).is_some() + && class_value_if_minted(class_id).is_none_or(|_| { + super::class_registry::class_own_static_method_code(class_id, name).is_none() + }) +} + +/// ClassDefinitionEvaluation's static methods: ClassBody static method +/// `name` of class `class_id` is an own data property of its function object, +/// `{ writable: true, enumerable: false, configurable: true }`, whose value is +/// one function object per (class, method) running exactly this +/// declaration's code (a resolved entry, never a by-name dispatch). A +/// property the program redefined is left alone; a re-registered entry +/// (a class expression evaluated again) refreshes the function. +fn install_declared_static_method(class_id: u32, name: &str) { + if name.starts_with('#') || is_internal_static_key(name) { + return; + } + let Some(code) = super::class_registry::class_own_static_method_code(class_id, name) else { + return; + }; + let ptr = class_value_ptr(class_id) as usize; + // SAFETY: this agent's live class closure. + unsafe { + match crate::closure::props::bag_get(ptr, name.as_bytes()) { + // Already this declaration's function, or a value the program + // put in its place: leave it. + Some(_) => return, + None if crate::closure::props::bag_has_own(ptr, name.as_bytes()) + || crate::closure::props::state_is_deleted(ptr, name) => + { + return + } + None => {} + } + } + let _no_collect = crate::gc::GcSuppressScope::new(); + let f = crate::closure::js_closure_alloc(code as *const crate::closure::JsFunctionInfo, 0); + if f.is_null() { + return; + } + class_static_set(class_id, name, crate::value::js_nanbox_pointer(f as i64)); + super::class_registry::class_static_set_defined_attrs(class_id, name, true, false, true); + // The object's shape now carries "own `name` is this declaration": a + // process-unique successor, so no object that got `name` any other way + // (and no other agent's object) shares it (`js_class_static_call_guard`). + // SAFETY: this agent's live class closure; the bag exists (just stored). + unsafe { + super::shapes::transition_object_shape_semantics(crate::closure::props::bag_of(ptr)); + } +} + /// The class function object for `class_id` if this agent has minted it. /// A read that finds none has its answer without minting one: an object that /// was never created owns no properties. (A builtin parent such as `Error` @@ -568,9 +940,10 @@ pub(crate) const CLASS_ACCESSOR_DEFAULT_ATTRS: (bool, bool) = (false, true); /// Install — or refresh, when a half arrives later — the ClassBody static /// accessor `name` of `class_id` as an accessor property of its function /// object's own-property object: the pair holds the reflected closures and -/// the compiled static entries (`fn() -> value` / `fn(v)`, `this` armed by -/// the caller — NOT the instance `fn(this)` convention; only this module and -/// its callers read a class function object's pairs). A half whose compiled +/// the compiled static entries in the pair's STATIC fields (`fn() -> value` +/// / `fn(v)`, `this` armed by the caller — NOT the instance `fn(this)` +/// convention, so a generic reader of `raw_get`/`raw_set` never sees them). +/// A half whose compiled /// entry is unchanged keeps its closure, so reflection hands out the same /// function every time; attributes a `defineProperty` set are kept. /// Private (`#x`) accessors are not properties and are never installed. @@ -604,26 +977,28 @@ fn install_declared_static_accessor(class_id: u32, name: &str) { } else { None }; - super::class_registry::class_static_accessor_function_value( + super::class_registry::class_accessor_function_value( raw, is_setter, + true, name, setter_length, - class_id, ) .to_bits() } }; - let get = half(raw_get, have.raw_get, have.get, false); - let set = half(raw_set, have.raw_set, have.set, true); + let get = half(raw_get, have.static_get, have.get, false); + let set = half(raw_set, have.static_set, have.set, true); class_static_define_accessor( class_id, name, crate::object::accessor_pair::Accessor { get, set, - raw_get, - raw_set, + raw_get: 0, + raw_set: 0, + static_get: raw_get, + static_set: raw_set, }, enumerable, configurable, @@ -716,10 +1091,10 @@ pub(crate) unsafe fn class_static_accessor_call_get( receiver: f64, ) -> f64 { let this = crate::object::field_get_set::accessor_receiver_override_take().unwrap_or(receiver); - if acc.raw_get != 0 { + if acc.static_get != 0 { crate::object::static_this_arm_if_unarmed(this); crate::object::static_private_owner_push(receiver); - let f = crate::closure::body_call::js_bare_body_fn!(acc.raw_get as *const u8;); + let f = crate::closure::body_call::js_bare_body_fn!(acc.static_get as *const u8;); let result = f(); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); @@ -740,10 +1115,10 @@ pub(crate) unsafe fn class_static_accessor_call_set( receiver: f64, value: f64, ) -> bool { - if acc.raw_set != 0 { + if acc.static_set != 0 { crate::object::static_this_arm_if_unarmed(receiver); crate::object::static_private_owner_push(receiver); - let f = crate::closure::body_call::js_bare_body_fn!(acc.raw_set as *const u8; value); + let f = crate::closure::body_call::js_bare_body_fn!(acc.static_set as *const u8; value); let _ = f(value); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); @@ -799,7 +1174,9 @@ pub(crate) fn class_static_get(class_id: u32, name: &str) -> Option { } } -/// Define/overwrite class `class_id`'s own static data property `name`. +/// Define/overwrite class `class_id`'s own static data property `name`: the +/// value only, the key keeps its attributes. Callers performing a [[Set]] +/// have checked `writable` (the attributes live with the key). pub(crate) fn class_static_set(class_id: u32, name: &str, value: f64) { let ptr = class_value_ptr(class_id) as usize; // SAFETY: as above; the bag writers run under a GcSuppressScope. @@ -807,7 +1184,7 @@ pub(crate) fn class_static_set(class_id: u32, name: &str, value: f64) { if is_internal_static_key(name) { crate::closure::props::state_internal_set(ptr, name, value); } else { - crate::closure::props::bag_set(ptr, name, value); + crate::closure::props::bag_define_value(ptr, name, value); } } } @@ -826,6 +1203,48 @@ pub(crate) fn class_static_remove(class_id: u32, name: &str) -> bool { } } +/// `Object.freeze` / `Object.seal` of class `class_id`'s function object: +/// every own string-keyed property becomes non-configurable, and with +/// `drop_writable` every data property non-writable. The attributes are +/// those of the own-property object's keys. +pub(crate) fn class_static_restrict_all(class_id: u32, drop_writable: bool) { + for (name, _) in class_static_entries(class_id) { + if let Some((writable, enumerable, _)) = + super::class_registry::class_static_defined_attrs(class_id, &name) + { + super::class_registry::class_static_set_defined_attrs( + class_id, + &name, + writable && !drop_writable, + enumerable, + false, + ); + } + } + for name in class_static_accessor_names(class_id) { + if let Some((_, enumerable, _)) = class_static_own_accessor(class_id, &name) { + class_static_set_accessor_attrs(class_id, &name, enumerable, false); + } + } +} + +/// TestIntegrityLevel over class `class_id`'s own string-keyed properties +/// (the object is already known non-extensible): none configurable, and +/// when `frozen` no data property writable. +pub(crate) fn class_static_integrity(class_id: u32, frozen: bool) -> bool { + for (name, _) in class_static_entries(class_id) { + let (writable, _, configurable) = + super::class_registry::class_static_defined_attrs(class_id, &name) + .unwrap_or((true, true, true)); + if configurable || (frozen && writable) { + return false; + } + } + class_static_accessor_names(class_id).iter().all(|name| { + class_static_own_accessor(class_id, name).is_none_or(|(_, _, configurable)| !configurable) + }) +} + /// Class `class_id`'s own static data properties in own-key order (integer /// keys ascending, then creation order). Internal keys are not properties and /// never appear. @@ -912,6 +1331,19 @@ mod tests { crate::symbol::class_static_symbol_keys_for_class(crate::error::CLASS_ID_ERROR) .is_empty() ); + // Object.getPrototypeOf(C), the static `super` parent value and a + // static `super[k] = v` all step to the parent id; none may mint. + let _ = crate::object::js_object_get_prototype_of(recv); + let _ = crate::object::class_registry::parent_static::template_dynamic_parent_value(cid); + let _ = crate::proxy::js_super_put_value_set( + crate::error::CLASS_ID_ERROR, + crate::value::js_nanbox_string( + crate::string::js_string_from_bytes(b"zz".as_ptr(), 2) as i64 + ), + 1.0, + recv, + 0, + ); assert!( class_value_cached(crate::error::CLASS_ID_ERROR).is_none(), "the builtin Error id must not get a class function object" @@ -1068,10 +1500,30 @@ mod tests { 0, ) }; + extern "C" fn static_name_entry(_closure: i64) -> f64 { + 0.0 + } + unsafe { + crate::object::class_registry::parent_static::js_register_class_static_method_entry( + cid as i64, + b"name".as_ptr(), + 4, + static_name_entry as *const () as usize as i64, + ) + }; + // node: `class Zed { static name() {} }` -> Zed.name is the method, an + // own data property { writable, !enumerable, configurable }. + let method = unsafe { crate::closure::props::bag_get(ptr, b"name") } + .expect("a static method named `name` is the class's own `name`"); assert_eq!( - unsafe { crate::closure::props::bag_get(ptr, b"name") }, - None, - "a static method named `name` is the class's own `name`" + unsafe { static_method_code_of(method) }, + Some(static_name_entry as *const () as usize), + "its value is the method's function object" + ); + assert_eq!( + crate::object::class_registry::class_static_defined_attrs(cid, "name"), + Some((true, false, true)), + "method attributes, not the intrinsic's" ); } diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index b16a65e4d2..29d2362887 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -304,13 +304,15 @@ pub extern "C" fn js_object_delete_field( { return 0; } - if name != "constructor" - && (super::class_registry::class_own_accessor_ptrs(cid, name).is_some() - || super::native_module::class_has_own_method(cid, name) - || super::class_registry::lookup_own_prototype_method(cid, name) - .is_some()) + if name == "constructor" + || super::class_registry::class_own_accessor_ptrs(cid, name).is_some() + || super::native_module::class_has_own_method(cid, name) + || super::class_registry::lookup_own_prototype_method(cid, name).is_some() { - super::class_registry::class_mark_key_deleted(cid, name); + // The member's storage is this object's key (removed + // by the scan below) plus, for a runtime prototype + // assignment, its dispatch entry: remove both. + super::class_registry::class_prototype_method_root_remove(cid, name); super::class_registry::invalidate_class_string_member_order( cid, name, false, ); @@ -797,34 +799,33 @@ fn class_delete_own_key(class_id: u32, name: &str) -> i32 { return 0; } super::class_registry::class_delete_own_dynamic_prop(class_id, name); - super::class_registry::class_mark_key_deleted(class_id, name); + crate::object::class_value::note_static_key_deleted(class_id, name); super::class_registry::invalidate_class_string_member_order(class_id, name, true); 1 } fn delete_class_prototype_key(class_id: u32, name: &str) -> i32 { - if let Some(proto) = super::class_registry::decl_prototype_own_accessor(class_id, name) { - // S2: the accessor is a real property of the declared prototype - // object; delete it there (which also records the class key deleted). - let scope = crate::gc::RuntimeHandleScope::new(); - let proto = scope.root_nanbox_f64(proto); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - return js_object_delete_field( - (proto.get_nanbox_f64().to_bits() & crate::value::POINTER_MASK) as *mut ObjectHeader, - key, - ); - } let has_own = name == "constructor" + || super::class_registry::decl_prototype_own_accessor(class_id, name).is_some() || super::native_module::class_has_own_method(class_id, name) || super::class_registry::lookup_own_prototype_method(class_id, name).is_some(); if !has_own { return 1; } - super::class_registry::class_mark_key_deleted(class_id, name); - super::class_registry::invalidate_class_string_member_order(class_id, name, false); - super::class_registry::invalidate_class_prototype_fast_guards_for_method(name); - crate::typed_feedback::invalidate_method_change(class_id); - 1 + // The members are real properties of the class's prototype object + // (materialized first): the delete happens there. + let proto = super::class_registry::class_decl_prototype_value(class_id); + let js = crate::JSValue::from_bits(proto.to_bits()); + if !js.is_pointer() { + return 1; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let proto = scope.root_nanbox_f64(proto); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_delete_field( + (proto.get_nanbox_f64().to_bits() & crate::value::POINTER_MASK) as *mut ObjectHeader, + key, + ) } /// `delete prim.field` (static key): once RequireObjectCoercible has rejected diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index b58124d957..d6baa110a1 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -644,6 +644,22 @@ pub(crate) fn note_attrs_born_with_keys(obj: usize) { /// Look up the property descriptor for (obj, key). Returns None if no entry exists, /// in which case the JS default `{ writable: true, enumerable: true, configurable: true }` applies. pub(crate) fn get_property_attrs(obj: usize, key: &str) -> Option { + // A function object's own properties, and their attributes, live in + // its bag (`closure::props`): its keys answer. + if crate::closure::is_closure_ptr(obj) { + // SAFETY: a proven live closure; its bag is null or a live object. + let bag = unsafe { crate::closure::props::bag_of(obj) }; + if !bag.is_null() { + let entry = unsafe { + super::key_attrs::object_key_entry(bag as *const ObjectHeader, key.as_bytes()) + }; + if entry != 0 { + return Some(PropertyAttrs { + bits: super::key_attrs::entry_to_attr_bits(entry), + }); + } + } + } // A STORED descriptor wins over the synthesized index default: // `Object.defineProperty` / `Object.freeze` on a wrapper installs a real // entry, and the §10.4.3 default must not shadow it. Synthesis therefore diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 8ed0e47fcd..2185528f55 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -231,35 +231,6 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu ); } } - // #6943: `js_string_coerce` allocates for every non-heap-string - // key and can run a user `toString` / `valueOf` for an object - // key, so it can trigger a GC that **evacuates**. `obj` — the - // receiver's header, resolved on the line above and - // dereferenced by `own_key_present` / `js_object_get_class_id` - // below — and `obj_value` (passed to `js_class_method_bind`) - // were raw Rust locals across the call. - let scope = crate::gc::RuntimeHandleScope::new(); - let obj_value_handle = scope.root_heap_word_u64(obj_value.to_bits()); - let obj_handle = scope.root_raw_mut_ptr(extract_obj_ptr(obj_value)); - let key_str = crate::builtins::js_string_coerce(key_value); - let obj_value = f64::from_bits(obj_value_handle.get_heap_word_u64()); - let obj = obj_handle.get_raw_mut_ptr::(); - if !obj.is_null() && !key_str.is_null() && !own_key_present(obj, key_str) { - let class_id = super::js_object_get_class_id(obj as *const ObjectHeader); - if class_id != 0 - && !method_name.starts_with('#') - && !super::class_registry::class_is_key_deleted(class_id, &method_name) - && super::class_registry::class_has_own_static_method( - class_id, - &method_name, - ) - { - let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); - let value = - super::js_class_method_bind(obj_value, leaked.as_ptr(), leaked.len()); - return build_data_descriptor(value, true, false, true); - } - } } } @@ -441,7 +412,11 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if let Some(class_id) = class_ref_id(obj_value) { let method_name = metadata_key_to_string(key_value); if let Some(method_name) = method_name { - if super::class_registry::class_is_key_deleted(class_id, &method_name) { + if if class_prototype_ref_id(obj_value).is_some() { + super::class_registry::class_proto_key_deleted(class_id, &method_name) + } else { + super::class_registry::class_static_key_deleted(class_id, &method_name) + } { return f64::from_bits(crate::value::TAG_UNDEFINED); } // Private registry entries retain their source spelling, but @@ -561,23 +536,6 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu } // Static methods are own properties of the class *constructor* // (not the prototype). `getOwnPropertyDescriptor(C, "m")` for a - // `static m() {}` must report a `{ writable, enumerable: false, - // configurable }` data property — `hasOwnProperty(C, "m")` - // already returns true, so without this the two disagreed and - // verifyProperty threw "reading 'enumerable'" on undefined - // (Test262 elements/after-same-line-static-*). - if super::class_prototype_ref_id(obj_value).is_none() - && super::class_registry::class_has_own_static_method(class_id, &method_name) - { - // Bind the static method to the constructor ref to produce a - // callable value, mirroring the `C.m` read path. The name - // bytes are leaked (bounded by the static descriptor set) so - // the pointer js_class_method_bind stashes stays valid. - let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); - let value = - super::js_class_method_bind(obj_value, leaked.as_ptr(), leaked.len()); - return build_data_descriptor(value, true, false, true); - } // Static FIELDS are own data properties of the constructor, // created via CreateDataPropertyOrThrow → writable, enumerable, // configurable all true. Codegen registers each declared @@ -1166,6 +1124,44 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { } if let Some(class_id) = class_ref_id(obj_value) { let is_prototype_ref = super::class_prototype_ref_id(obj_value).is_some(); + // The class function object's own keys, in its bag's creation + // order: a key deleted and defined again is a new key and comes + // last, as for any object. + if !is_prototype_ref { + if let Some(fo) = crate::object::class_value::class_value_if_minted(class_id) { + let ptr = fo as usize; + // This agent's live class closure (the enclosing `unsafe`). + let names = crate::closure::props::bag_own_key_names(ptr); + let mut out: Vec = Vec::new(); + for intrinsic in ["length", "name", "prototype"] { + if !names.iter().any(|n| n == intrinsic) + && !crate::closure::closure_is_key_deleted(ptr, intrinsic) + && (intrinsic == "prototype" + || !super::class_registry::class_static_key_deleted( + class_id, intrinsic, + )) + { + out.push(intrinsic.to_string()); + } + } + for name in names { + if name.starts_with('#') + || super::field_get_set::is_internal_runtime_key(&name) + { + continue; + } + push_unique_name(&mut out, name); + } + sort_property_names_ecma(&mut out); + let result = crate::array::js_array_alloc(out.len() as u32); + for name in out { + let str_ptr = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(result, JSValue::string_ptr(str_ptr)); + } + return f64::from_bits((result as u64) | 0x7FFD_0000_0000_0000); + } + } let mut names: Vec = if is_prototype_ref { vec!["constructor".to_string()] } else { @@ -1178,7 +1174,12 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { for name in super::class_registry::class_own_string_member_names(class_id, !is_prototype_ref) { - if !super::class_registry::class_is_key_deleted(class_id, &name) { + let deleted = if is_prototype_ref { + super::class_registry::class_proto_key_deleted(class_id, &name) + } else { + super::class_registry::class_static_key_deleted(class_id, &name) + }; + if !deleted { push_unique_name(&mut names, name); } } @@ -1196,7 +1197,7 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { // A deleted `length` / `name` is no longer own. && !(!is_prototype_ref && matches!(n.as_str(), "length" | "name") - && super::class_registry::class_is_key_deleted(class_id, n)) + && super::class_registry::class_static_key_deleted(class_id, n)) }); sort_property_names_ecma(&mut names); let result = crate::array::js_array_alloc(names.len() as u32); @@ -1277,45 +1278,46 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { } } - // #3655: functions/closures. Own keys are `length`, `name`, then any - // user-attached props, then `prototype` (constructors) — matching V8's - // ordering. All honor `delete`. Reading `keys_array` off a closure - // (below) would be out of bounds. + // #3655: functions/closures. Own keys come in creation order: the + // function object's own property bag holds them in that order, so a + // key deleted and defined again is a new key and comes last, as for + // any object. `length`, `name` and `prototype` a function has not + // materialized into its bag yet were created with the function, + // before every bag key; each honors `delete`. Reading `keys_array` + // off a closure (below) would be out of bounds. if obj_jv.is_pointer() { let ptr = crate::value::js_nanbox_get_pointer(obj_value) as usize; if crate::closure::is_closure_ptr(ptr) { + // `is_closure_ptr` proved a live closure cell. + let bag_keys = crate::closure::props::bag_own_key_names(ptr); + let in_bag = |k: &str| bag_keys.iter().any(|n| n == k); let mut names: Vec = Vec::new(); - if !crate::closure::closure_is_key_deleted(ptr, "length") { - names.push("length".to_string()); + for intrinsic in ["length", "name"] { + if !in_bag(intrinsic) && !crate::closure::closure_is_key_deleted(ptr, intrinsic) + { + names.push(intrinsic.to_string()); + } } - if !crate::closure::closure_is_key_deleted(ptr, "name") { - names.push("name".to_string()); + if !in_bag("prototype") + && crate::closure::closure_has_own_dynamic_prop(ptr, "prototype") + && !crate::closure::closure_is_key_deleted(ptr, "prototype") + { + names.push("prototype".to_string()); } - let has_prototype = crate::closure::closure_has_own_dynamic_prop(ptr, "prototype") - && !crate::closure::closure_is_key_deleted(ptr, "prototype"); - // User-attached props (snapshot is already sorted); the - // built-in slots are emitted explicitly so skip them here. - for (name, _) in crate::closure::closure_dynamic_props_snapshot(ptr) { - if matches!(name.as_str(), "length" | "name" | "prototype") { - continue; - } - if crate::closure::closure_is_key_deleted(ptr, &name) { + for name in bag_keys.iter() { + if crate::closure::closure_is_key_deleted(ptr, name) { continue; } - names.push(name); + push_unique_name(&mut names, name.clone()); } + // Accessors a function keeps outside its bag (the descriptor + // side table) follow the bag keys. for name in super::accessor_descriptor_keys_for_obj(ptr) { - if matches!(name.as_str(), "length" | "name" | "prototype") { - continue; - } if crate::closure::closure_is_key_deleted(ptr, &name) { continue; } push_unique_name(&mut names, name); } - if has_prototype { - names.push("prototype".to_string()); - } sort_property_names_ecma(&mut names); let result = crate::array::js_array_alloc(names.len() as u32); for name in names { @@ -1403,7 +1405,7 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { names.push("constructor".to_string()); } for name in super::class_registry::class_own_string_member_names(class_id, false) { - if super::class_registry::class_is_key_deleted(class_id, &name) { + if super::class_registry::class_proto_key_deleted(class_id, &name) { continue; } if physical.contains(&name) { diff --git a/crates/perry-runtime/src/object/descriptors/builders.rs b/crates/perry-runtime/src/object/descriptors/builders.rs index af30665e6f..59f540bd1b 100644 --- a/crates/perry-runtime/src/object/descriptors/builders.rs +++ b/crates/perry-runtime/src/object/descriptors/builders.rs @@ -73,12 +73,14 @@ pub(crate) unsafe fn symbol_own_property_descriptor(obj_value: f64, key_value: f super::class_registry::class_accessor_function_value( get, false, + is_static, &display_name, None, ), super::class_registry::class_accessor_function_value( set, true, + is_static, &display_name, None, ), diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs index 6ee17f3d33..37e81c58e5 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs @@ -246,7 +246,7 @@ pub(super) unsafe fn class_object_name_value( return Some(v); } let class_id = (*obj).class_id; - if super::super::class_registry::class_is_key_deleted(class_id, "name") { + if super::super::class_registry::class_static_key_deleted(class_id, "name") { return None; } let cname = super::super::class_registry::class_name_for_id(class_id)?; diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 2832ae8929..1a588da96f 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -254,7 +254,7 @@ pub(crate) fn class_value_get_field( } } if !name.is_empty() { - if super::super::class_registry::class_is_key_deleted(class_id, name) { + if super::super::class_registry::class_static_key_deleted(class_id, name) { // Not an own property any more: the read continues on the // class's [[Prototype]]. return crate::object::class_value::class_prototype_get(class_id, key, class_value); @@ -358,7 +358,7 @@ pub(crate) fn class_value_get_field( // Mid.foo` must let `Sub.foo` inherit `Base.foo`, not // resolve to undefined. Skip the registry read for the // deleted level and keep walking up. - if !super::super::class_registry::class_is_key_deleted(p, name) { + if !super::super::class_registry::class_static_key_deleted(p, name) { let inherited = crate::object::class_value::class_static_get(p, name); if let Some(v) = inherited { return JSValue::from_bits(v.to_bits()); @@ -370,6 +370,13 @@ pub(crate) fn class_value_get_field( } if super::super::class_registry::lookup_static_method_in_chain(class_id, name).is_some() { + // The declaration's own function object, on whichever class + // declares it (`Q.a === P.a`). + if let Some(v) = + crate::object::class_value::inherited_static_method_value(class_id, name) + { + return JSValue::from_bits(v.to_bits()); + } let heap_name = { let layout = std::alloc::Layout::from_size_align(name_len.max(1), 1).unwrap(); let ptr = std::alloc::alloc(layout); @@ -447,7 +454,7 @@ pub(crate) fn class_value_get_field( // `thrown.constructor.name` to label the thrown error. if name == "name" && class_id != 0 - && !super::super::class_registry::class_is_key_deleted(class_id, name) + && !super::super::class_registry::class_static_key_deleted(class_id, name) { if let Some(cname) = super::super::class_registry::class_name_for_id(class_id) { let s = crate::string::js_string_from_bytes(cname.as_ptr(), cname.len() as u32); @@ -457,7 +464,7 @@ pub(crate) fn class_value_get_field( if name == "length" && class_id != 0 && !is_prototype_ref - && !super::super::class_registry::class_is_key_deleted(class_id, name) + && !super::super::class_registry::class_static_key_deleted(class_id, name) { if let Some(length) = super::super::class_registry::class_length_for_id(class_id) { return JSValue::number(length as f64); @@ -1149,7 +1156,7 @@ fn get_field_by_name_past_data_probe( let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) .unwrap_or(""); if !name.is_empty() - && !super::super::class_registry::class_is_key_deleted(class_id, name) + && !super::super::class_registry::class_static_key_deleted(class_id, name) { if super::super::class_registry::lookup_static_method_in_chain(class_id, name) .is_some() diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index 53b6638d3e..01c8f5d835 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -199,7 +199,7 @@ unsafe fn class_ref_has_inherited_static_data( Some(parent) if parent != 0 && parent != child => parent, _ => break, }; - if !super::super::class_registry::class_is_key_deleted(parent, name) + if !super::super::class_registry::class_static_key_deleted(parent, name) && super::super::class_registry::class_has_own_dynamic_prop(parent, name) { return true; @@ -446,17 +446,19 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { ) }; let present = matches!(name, "prototype" | "name" | "length" | "constructor") - || (!super::super::class_registry::class_is_key_deleted(class_id, name) - && (super::super::class_registry::class_has_own_dynamic_prop( - class_id, name, - ) || super::super::class_registry::lookup_static_method_in_chain( + || (!super::super::class_registry::class_static_key_deleted( + class_id, name, + ) && (super::super::class_registry::class_has_own_dynamic_prop( + class_id, name, + ) + || super::super::class_registry::lookup_static_method_in_chain( class_id, name, ) .is_some() - || super::super::class_registry::static_accessor_in_chain( - class_id, name, - ) - || inherited_data)); + || super::super::class_registry::static_accessor_in_chain( + class_id, name, + ) + || inherited_data)); if present { return nanbox_true; } diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs index 2fbbda0955..eb07ba1d81 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -18,7 +18,8 @@ pub extern "C" fn js_object_get_field_by_name_f64( if (obj as usize) > 0 && (obj as usize) < 0x10000 && !key.is_null() { if let Some(name) = unsafe { super::super::has_own_helpers::str_from_string_header(key) } { let class_id = obj as usize as u32; - if name == "name" && !super::super::class_registry::class_is_key_deleted(class_id, name) + if name == "name" + && !super::super::class_registry::class_static_key_deleted(class_id, name) { if let Some(cname) = super::super::class_registry::class_name_for_id(class_id) { let s = crate::string::js_string_from_bytes(cname.as_ptr(), cname.len() as u32); diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 2c7d3d35d8..48332b0578 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -441,7 +441,7 @@ pub extern "C" fn js_object_set_field_by_name( let is_prototype_ref = super::class_prototype_ref_id(recv).is_some(); if !is_prototype_ref && name == "name" - && !super::class_registry::class_is_key_deleted(class_id, &name) + && !super::class_registry::class_static_key_deleted(class_id, &name) && super::class_registry::lookup_static_method_in_chain(class_id, &name) .is_none() && super::class_registry::class_static_defined_attrs(class_id, &name) diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 3e8cd2fc5e..9cab44668d 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -45,6 +45,7 @@ mod fetch_globals; mod generator; mod install_static; mod math_temporal; +mod object_intrinsic; mod populate; mod proto_methods; mod typed_array; @@ -143,7 +144,7 @@ pub(crate) use fetch_globals::{ global_this_date_thunk, global_this_eval_thunk, global_this_file_thunk, global_this_headers_thunk, global_this_is_materialized, global_this_request_thunk, global_this_response_error_thunk, global_this_response_json_thunk, - global_this_response_redirect_thunk, global_this_response_thunk, + global_this_response_redirect_thunk, global_this_response_thunk, is_thread_realm_global, }; pub use fetch_globals::{ js_fetch_or_value_super, js_get_global_this, js_global_or_console_property_by_name, @@ -173,6 +174,9 @@ pub(crate) use math_temporal::install_temporal_namespace; #[cfg(feature = "temporal")] pub(crate) use math_temporal::temporal_kind_prototype; pub(crate) use math_temporal::{install_math_namespace, temporal_ctor_kind}; +pub(crate) use object_intrinsic::{ + ensure_object_intrinsics, object_intrinsics_for_realm, object_prototype_intrinsic_bits, +}; pub(crate) use populate::{ default_prepare_stack_trace_func_ptr, populate_global_this_builtins, scan_error_constructor_root_mut, ERROR_CONSTRUCTOR_PTR, diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index 5839d8cdf3..5cf625eb19 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -86,6 +86,13 @@ pub(crate) fn global_this_is_materialized() -> bool { THREAD_GLOBAL_THIS.with(|c| c.get()) != 0 } +/// Whether `obj` is THIS thread's realm global, as opposed to the global of a +/// `vm` context or an eval realm that `populate_global_this_builtins` also +/// fills (each of those gets its own intrinsics). +pub(crate) fn is_thread_realm_global(obj: *mut ObjectHeader) -> bool { + !obj.is_null() && THREAD_GLOBAL_THIS.with(|c| c.get()) == obj as i64 +} + /// Issue #611: lazily allocate `globalThis` for computed global access. #[no_mangle] pub extern "C" fn js_get_global_this() -> f64 { diff --git a/crates/perry-runtime/src/object/global_this/object_intrinsic.rs b/crates/perry-runtime/src/object/global_this/object_intrinsic.rs new file mode 100644 index 0000000000..cbefb294f6 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/object_intrinsic.rs @@ -0,0 +1,189 @@ +use super::*; + +/// A realm's `%Object%` constructor and `%Object.prototype%`. +type ObjectPair = (*mut crate::closure::ClosureHeader, *mut ObjectHeader); + +crate::perry_thread_local! { + /// Set while THIS thread builds its `%Object%` / `%Object.prototype%`. + /// The build stores into the objects it creates, and the store path asks + /// "is this receiver %Object.prototype%?", which lands back in + /// [`ensure_object_intrinsics`]. That nested question has an honest answer + /// without building: the intrinsic does not exist yet, so nothing is it. + static OBJECT_INTRINSICS_BUILDING: std::sync::atomic::AtomicBool = + const { std::sync::atomic::AtomicBool::new(false) }; +} + +/// This realm's `%Object%` constructor and `%Object.prototype%`, built on +/// first use, complete: the constructor with its statics (`keys`, `create`, +/// `getPrototypeOf`, ...), `name`/`length`, and a non-writable `prototype`; +/// the prototype with `constructor` and its methods (`toString`, +/// `hasOwnProperty`, `__proto__`, ...). +/// +/// Nothing here reads `globalThis`. A declared class's prototype object has +/// `%Object.prototype%` as its `[[Prototype]]`, and so does every ordinary +/// object; reaching it through `globalThis.Object` made the first class +/// materialization build the whole realm global (several hundred builtins, +/// ~50M instructions) for one pointer. `populate_global_this_builtins` +/// ADOPTS these two objects as `globalThis.Object` / `Object.prototype`, so +/// there is exactly one of each per realm whichever side asks first. +/// +/// Both are rooted in `scan_object_cache_roots_mut` and the prototype's +/// address is memoized in the `Object.prototype` row of the per-thread +/// prototype-address cache, like the other realm intrinsics. +/// +/// Returns null pointers only while this thread is inside the build itself +/// (see [`OBJECT_INTRINSICS_BUILDING`]) or if an allocation failed. +pub(crate) fn ensure_object_intrinsics() -> ObjectPair { + let loaded = || { + ( + crate::object::OBJECT_INTRINSIC_PTR.load(Ordering::Acquire), + crate::object::OBJECT_INTRINSIC_PROTO_PTR.load(Ordering::Acquire), + ) + }; + let (ctor, proto) = loaded(); + if ctor != 0 && proto != 0 { + return (ctor as *mut _, proto as *mut _); + } + if OBJECT_INTRINSICS_BUILDING.with(|b| b.swap(true, Ordering::AcqRel)) { + return (std::ptr::null_mut(), std::ptr::null_mut()); + } + let built = build_object_intrinsics(); + if let Some((ctor, proto)) = built { + crate::object::OBJECT_INTRINSIC_PTR.store(ctor as i64, Ordering::Release); + crate::object::OBJECT_INTRINSIC_PROTO_PTR.store(proto as i64, Ordering::Release); + crate::array::note_object_prototype_intrinsic(proto as usize); + } + OBJECT_INTRINSICS_BUILDING.with(|b| b.store(false, Ordering::Release)); + built.unwrap_or((std::ptr::null_mut(), std::ptr::null_mut())) +} + +/// The `%Object%` / `%Object.prototype%` pair for the realm whose global is +/// `global`: this thread's own pair (see [`ensure_object_intrinsics`]) when +/// `global` is the thread's realm global, a fresh pair for any other realm +/// (a `vm` context or an eval realm), whose intrinsics are its own. +pub(crate) fn object_intrinsics_for_realm(global: *mut ObjectHeader) -> ObjectPair { + if is_thread_realm_global(global) { + return ensure_object_intrinsics(); + } + build_object_intrinsics().unwrap_or((std::ptr::null_mut(), std::ptr::null_mut())) +} + +/// `%Object.prototype%` as NaN-boxed pointer bits, built on first use. +pub(crate) fn object_prototype_intrinsic_bits() -> Option { + let (_, proto) = ensure_object_intrinsics(); + (!proto.is_null()).then(|| crate::value::js_nanbox_pointer(proto as i64).to_bits()) +} + +fn build_object_intrinsics() -> Option { + // The same no-move window and immortal-layout scope as the realm + // bootstrap (see `populate_global_this_builtins`): the constructor and + // prototype are held as raw pointers across every allocating install + // below, and both live for the life of the realm. + let _no_move = crate::gc::GcSuppressScope::new(); + let _immortal = crate::gc::ImmortalLayoutScope::new(); + let info = crate::fn_info!(global_this_object_thunk, 1; with_declared(1)); + let closure_ptr = crate::closure::js_closure_alloc(info, 0); + if closure_ptr.is_null() { + return None; + } + install_builtin_constructor_statics("Object", closure_ptr); + super::super::native_module::set_bound_native_closure_name(closure_ptr, "Object"); + if let Some(len) = builtin_constructor_spec_length("Object") { + super::super::native_module::set_builtin_closure_length(closure_ptr as usize, len); + } + for key in ["name", "length"] { + super::super::set_builtin_property_attrs( + closure_ptr as usize, + key.to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + } + let proto_obj = js_object_alloc(0, 0); + if proto_obj.is_null() { + return None; + } + let ctor_value = crate::value::js_nanbox_pointer(closure_ptr as i64); + let proto_key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), 9); + super::super::define_builtin_data_property( + closure_ptr as *mut ObjectHeader, + proto_key, + crate::value::js_nanbox_pointer(proto_obj as i64), + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + let ctor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + super::super::define_builtin_data_property( + proto_obj, + ctor_key, + ctor_value, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + populate_builtin_prototype_methods("Object", proto_obj); + Some((closure_ptr, proto_obj)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn on_a_fresh_thread(body: impl FnOnce() + Send + 'static) { + std::thread::Builder::new() + .stack_size(16 << 20) + .spawn(body) + .expect("spawn object-intrinsic test thread") + .join() + .expect("object-intrinsic test thread panicked"); + } + + /// A declared class's prototype reaches %Object.prototype% without + /// building the realm global, and the realm global then adopts the very + /// same %Object% / %Object.prototype% rather than building a second pair. + #[test] + fn object_prototype_is_built_without_the_realm_global_and_adopted_by_it() { + on_a_fresh_thread(|| { + assert!(!crate::object::global_this_is_materialized()); + let class_parent = crate::object::class_registry::global_object_prototype_bits() + .expect("%Object.prototype% for a class prototype"); + let default_parent = crate::object::prototype_chain::default_object_prototype_bits() + .expect("%Object.prototype% for an ordinary object"); + assert_eq!(class_parent, default_parent); + assert!( + !crate::object::global_this_is_materialized(), + "reaching %Object.prototype% built the realm global" + ); + let (ctor, proto) = ensure_object_intrinsics(); + let proto_addr = proto as usize; + assert_eq!( + crate::array::object_prototype_addr_if_resolved(), + proto_addr + ); + assert!(crate::array::object_prototype_addr_matches(proto_addr)); + // Complete before any realm global: statics, prototype methods. + let keys = crate::closure::closure_get_dynamic_prop(ctor as usize, "keys"); + assert!( + JSValue::from_bits(keys.to_bits()).is_pointer(), + "Object.keys missing" + ); + let has_own = crate::string::js_string_from_bytes(b"hasOwnProperty".as_ptr(), 14); + let method = js_object_get_field_by_name(proto, has_own); + assert!(method.is_pointer(), "hasOwnProperty missing"); + + crate::object::js_get_global_this(); + let global_object = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let (ctor_now, proto_now) = ensure_object_intrinsics(); + assert_eq!( + global_object.to_bits(), + crate::value::js_nanbox_pointer(ctor_now as i64).to_bits(), + "globalThis.Object is not the intrinsic" + ); + let global_proto = + crate::closure::closure_get_dynamic_prop(ctor_now as usize, "prototype"); + assert_eq!( + global_proto.to_bits(), + crate::value::js_nanbox_pointer(proto_now as i64).to_bits() + ); + assert_eq!(crate::array::object_prototype_addr(), proto_now as usize); + }); + } +} diff --git a/crates/perry-runtime/src/object/global_this/populate.rs b/crates/perry-runtime/src/object/global_this/populate.rs index f1aa6884e8..798e877737 100644 --- a/crates/perry-runtime/src/object/global_this/populate.rs +++ b/crates/perry-runtime/src/object/global_this/populate.rs @@ -193,6 +193,23 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade ); continue; } + if name == "Object" { + // %Object% / %Object.prototype% are built on their own (a class + // prototype needs them without the realm global); the thread's + // realm global adopts them, a `vm`/eval realm gets its own pair. + let (object_ctor, _) = object_intrinsics_for_realm(singleton()); + if !object_ctor.is_null() { + let name_key = crate::string::js_string_from_bytes(b"Object".as_ptr(), 6); + super::super::define_builtin_data_property( + singleton(), + name_key, + crate::value::js_nanbox_pointer(object_ctor as i64), + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + continue; + } let info = match name { "Array" => crate::fn_info!(global_this_array_thunk, 1; with_rest(0)), // #10423: `F(p, body)` through a `Function` value creates a @@ -200,7 +217,6 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade "Function" => { crate::fn_info!(unwind_in_tests global_this_function_call_thunk, 1; with_rest(0)) } - "Object" => crate::fn_info!(global_this_object_thunk, 1; with_declared(1)), "String" => crate::fn_info!(global_this_string_thunk, 1; with_declared(1)), // #2889: call-form `Number(x)` / `Boolean(x)` through a rebound // global value coerce like the bare-call lowering does. diff --git a/crates/perry-runtime/src/object/inherited_read_cache_tests.rs b/crates/perry-runtime/src/object/inherited_read_cache_tests.rs index b83be0a8e9..ee9c9ac5de 100644 --- a/crates/perry-runtime/src/object/inherited_read_cache_tests.rs +++ b/crates/perry-runtime/src/object/inherited_read_cache_tests.rs @@ -837,7 +837,7 @@ unsafe fn install_class_getter(proto: *mut ObjectHeader, name: &str) { get: crate::value::js_nanbox_pointer(getter as i64).to_bits(), set: 0, raw_get: forty_two_raw_getter as *const () as usize, - raw_set: 0, + ..Default::default() }, crate::object::PropertyAttrs::new(true, false, true), ); diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 47396062c7..53df0e3df5 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -471,6 +471,8 @@ crate::perry_thread_local! { static LOCAL_STORAGE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static SESSION_STORAGE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; static URL_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static OBJECT_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static OBJECT_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; } static HTTP_METHODS_CACHE: RealmAtomicU64 = RealmAtomicU64::new(&HTTP_METHODS_CACHE_SLOT); @@ -512,6 +514,12 @@ pub(crate) static ASYNC_GENERATOR_PROTOTYPE_PTR: RealmAtomicI64 = /// native constructor builds must keep the real component accessors (#11585). pub(crate) static URL_INTRINSIC_PROTO_PTR: RealmAtomicI64 = RealmAtomicI64::new(&URL_INTRINSIC_PROTO_PTR_SLOT); +/// `%Object%` and `%Object.prototype%`, built by `ensure_object_intrinsics` +/// without the realm global and adopted by it. +pub(crate) static OBJECT_INTRINSIC_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&OBJECT_INTRINSIC_PTR_SLOT); +pub(crate) static OBJECT_INTRINSIC_PROTO_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&OBJECT_INTRINSIC_PROTO_PTR_SLOT); pub(crate) static LOCAL_STORAGE_PTR: RealmAtomicI64 = RealmAtomicI64::new(&LOCAL_STORAGE_PTR_SLOT); pub(crate) static SESSION_STORAGE_PTR: RealmAtomicI64 = RealmAtomicI64::new(&SESSION_STORAGE_PTR_SLOT); @@ -742,17 +750,6 @@ pub(crate) struct ShapeCacheEntry { keys_array: *mut ArrayHeader, } -crate::perry_thread_local! { - /// #7190: `(writable, enumerable)` for static own keys installed by - /// `Object.defineProperty(C, k, desc)`. They live in `CLASS_DYNAMIC_PROPS` - /// next to `static x = …` fields, which are writable AND enumerable by - /// CreateDataPropertyOrThrow — a data descriptor defaults to neither. An - /// ABSENT entry therefore means "declared static field", and keeps the - /// previous `(true, true)` reporting untouched. - pub(crate) static CLASS_STATIC_DEFINED_ATTRS: std::cell::RefCell>> = - std::cell::RefCell::new(std::collections::HashMap::new()); -} - // Storage: `ObjectHotTables::{shape_inline_cache, shape_cache_overflow}`. /// Look up a static shape's keys by shape_id. `ObjectKeys::NONE` on miss. @@ -1512,6 +1509,8 @@ pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<' &LOCAL_STORAGE_PTR, &SESSION_STORAGE_PTR, &URL_INTRINSIC_PROTO_PTR, + &OBJECT_INTRINSIC_PTR, + &OBJECT_INTRINSIC_PROTO_PTR, ] { slot.with_slot(|slot| { visitor.visit_atomic_i64_slot(slot, Ordering::Acquire, Ordering::Release); @@ -1773,6 +1772,9 @@ pub(crate) unsafe fn object_is_shaped(obj: *const ObjectHeader) -> bool { // 16-byte header with `meta` last (target_layout.rs): offset 8 LP64, 12 ILP32. const _: () = assert!(std::mem::offset_of!(ObjectHeader, meta) == 16 - size_of::()); +const _: () = assert!( + std::mem::offset_of!(ObjectHeader, parent_class_id) == crate::codegen_abi::OBJECT_SHAPE_OFFSET +); const _: () = assert!(std::mem::size_of::() == 8); pub(crate) mod cell_meta; diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 9aa6857f2f..b6d13f8533 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -2375,7 +2375,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if class_id != 0 && (!class_prototype_fast_guard_invalidated_for_method( class_prototype_method_guard_slot(method_name), - ) || !class_is_key_deleted(class_id, method_name)) + ) || !class_proto_key_deleted(class_id, method_name)) { if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { if let Some(ref reg) = *registry { @@ -2762,7 +2762,10 @@ pub unsafe extern "C-unwind" fn js_native_call_method( !recv.is_null() && !crate::value::addr_class::is_small_handle(recv as usize) && { let class_id = crate::object::js_object_get_class_id(recv); class_id != 0 - && !crate::object::class_registry::class_is_key_deleted(class_id, method_name) + && !crate::object::class_registry::class_proto_key_deleted( + class_id, + method_name, + ) && crate::object::class_registry::class_chain_has_instance_accessor( class_id, method_name, diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs index eec74a29b6..2a615b0e1a 100644 --- a/crates/perry-runtime/src/object/native_call_method/common_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -85,7 +85,7 @@ pub(super) unsafe fn dispatch_common( super::has_own_helpers::str_from_string_header(key_str) .map(|key| { matches!(key, "length" | "name" | "prototype") - && !super::class_registry::class_is_key_deleted(class_id, key) + && !super::class_registry::class_static_key_deleted(class_id, key) }) .unwrap_or(false) }; @@ -106,22 +106,20 @@ pub(super) unsafe fn dispatch_common( if let Some(class_id) = super::class_ref_id(object) { let present = super::has_own_helpers::str_from_string_header(key_str) .map(|key| { - if super::class_registry::class_is_key_deleted(class_id, key) { + if super::class_registry::class_static_key_deleted(class_id, key) { false } else if key == "name" - && super::class_registry::lookup_static_method_in_chain( + && !crate::object::class_value::class_static_owns_method( class_id, key, ) - .is_none() { super::class_registry::class_name_for_id(class_id).is_some() } else { crate::object::class_value::class_static_get(class_id, key) .is_some() - || super::class_registry::lookup_static_method_in_chain( + || crate::object::class_value::class_static_owns_method( class_id, key, ) - .is_some() } }) .unwrap_or(false); diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index 11479c3988..f61fa77a8d 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -1058,7 +1058,7 @@ pub(super) unsafe fn dispatch_handle( let mut depth = 0u32; while depth < 32 { let deleted = - prototype_mutated && class_is_key_deleted(cur_cid, method_name); + prototype_mutated && class_proto_key_deleted(cur_cid, method_name); // A runtime assignment is an own property of this // exact prototype and replaces the declared vtable // entry. Resolve it first; deletion hides both. diff --git a/crates/perry-runtime/src/object/native_module/class_ref_values.rs b/crates/perry-runtime/src/object/native_module/class_ref_values.rs index fab0d06f12..eb90b4be4a 100644 --- a/crates/perry-runtime/src/object/native_module/class_ref_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_ref_values.rs @@ -118,7 +118,7 @@ fn class_chain_declares(class_id: u32, name: &str, accessors: bool) -> bool { if let Some(vtable) = reg.get(&cid) { // Honor `delete C.prototype.m`: a deleted key must report `false` // from `'m' in new C()`, matching the descriptor/static lookup paths. - if !super::class_registry::class_is_key_deleted(cid, name) + if !super::class_registry::class_proto_key_deleted(cid, name) && (vtable.methods.contains_key(name) || (accessors && vtable.accessor_decl(name).is_some())) { diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index 1ddbfaae08..35519e18a1 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -78,7 +78,6 @@ unsafe fn define_class_prototype_method(target_cid: u32, name: &str, value_bits: super::super::class_registry::class_prototype_method_root_remove( target_cid, name, ); - super::super::class_registry::class_unmark_key_deleted(target_cid, name); super::super::class_registry::invalidate_class_prototype_fast_guards_for_method( name, ); diff --git a/crates/perry-runtime/src/object/object_ops/has_own.rs b/crates/perry-runtime/src/object/object_ops/has_own.rs index 3aeed7440d..d029721766 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -221,15 +221,13 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { .map(|key| { if super::super::field_get_set::is_internal_runtime_key(key) { false - } else if super::super::class_registry::class_is_key_deleted(class_id, key) { + } else if super::super::class_registry::class_static_key_deleted(class_id, key) + { false } else if matches!(key, "length" | "prototype") { true } else if key == "name" - && super::super::class_registry::lookup_static_method_in_chain( - class_id, key, - ) - .is_none() + && !crate::object::class_value::class_static_owns_method(class_id, key) { super::super::class_registry::class_name_for_id(class_id).is_some() } else { @@ -237,13 +235,11 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { crate::object::class_value::class_static_get(class_id, key).is_some(); has_public_data || (!key.starts_with('#') - && (super::super::class_registry::lookup_static_method_in_chain( + && (crate::object::class_value::class_static_owns_method( + class_id, key, + ) || crate::object::class_value::class_static_has_own_accessor( class_id, key, - ) - .is_some() - || crate::object::class_value::class_static_has_own_accessor( - class_id, key, - ))) + ))) } }) .unwrap_or(false); @@ -467,7 +463,7 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { super::super::class_registry::class_id_for_decl_prototype_object(obj as usize) { if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { - if !super::super::class_registry::class_is_key_deleted(cid, key) + if !super::super::class_registry::class_proto_key_deleted(cid, key) && (key == "constructor" || (!key.starts_with('#') && super::super::native_module::class_has_own_method(cid, key))) diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index 91583fd0c4..56a0f61a39 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -442,7 +442,13 @@ fn get_prototype_of_resolved(obj_value: f64) -> f64 { // Treat it as a root: a native-module namespace's [[Prototype]] is // %Object.prototype%, so the synthetic class whose proto was that // namespace inherits Object.prototype too. - if parent_id != 0 && parent_id != super::super::native_module::NATIVE_MODULE_CLASS_ID { + // A builtin parent id (`extends Error`) is not a compiled class + // and never gets a class function object: its constructor is the + // dynamic parent value above, and without one this is a root. + if parent_id != 0 + && parent_id != super::super::native_module::NATIVE_MODULE_CLASS_ID + && crate::object::is_class_id_registered(parent_id) + { return crate::object::class_value::class_value(parent_id); } } diff --git a/crates/perry-runtime/src/object/object_ops_frozen.rs b/crates/perry-runtime/src/object/object_ops_frozen.rs index 164cb678c9..77a532f62d 100644 --- a/crates/perry-runtime/src/object/object_ops_frozen.rs +++ b/crates/perry-runtime/src/object/object_ops_frozen.rs @@ -213,6 +213,15 @@ pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 { // Closures: own props are `name`/`length` + dynamic props — the // keys_array walk below would read garbage off the ClosureHeader. // Record explicit non-writable/non-configurable attrs. + // A class function object's own properties (and their + // attributes) live in its own-property object. + if let Some(class_id) = crate::object::class_value::class_closure_id(obj as usize) { + crate::object::class_value::class_static_restrict_all(class_id, true); + mark_all_symbol_keys( + obj, /*drop_writable=*/ true, /*drop_configurable=*/ true, + ); + return obj_value; + } if crate::closure::is_closure_ptr(obj as usize) { let owner = obj as usize; for builtin in ["name", "length"] { @@ -321,6 +330,13 @@ pub extern "C" fn js_object_seal(obj_value: f64) -> f64 { } // Closures: seal via the side tables (drop configurable only) — // see the matching arm in `js_object_freeze`. + if let Some(class_id) = crate::object::class_value::class_closure_id(obj as usize) { + crate::object::class_value::class_static_restrict_all(class_id, false); + mark_all_symbol_keys( + obj, /*drop_writable=*/ false, /*drop_configurable=*/ true, + ); + return obj_value; + } if crate::closure::is_closure_ptr(obj as usize) { let owner = obj as usize; for builtin in ["name", "length"] { @@ -433,6 +449,9 @@ unsafe fn object_integrity_level(obj: *mut ObjectHeader, frozen: bool) -> bool { // ClosureHeader. `name`/`length` are non-writable but configurable by // default, so an un-frozen function fails both levels; `js_object_freeze` // / `seal` record explicit attrs that satisfy them. + if let Some(class_id) = crate::object::class_value::class_closure_id(obj as usize) { + return crate::object::class_value::class_static_integrity(class_id, frozen); + } if (*gc).obj_type == crate::gc::GC_TYPE_CLOSURE || crate::closure::is_closure_ptr(obj as usize) { let owner = obj as usize; diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index f3f5e9bd5f..0c03bcc90e 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -658,6 +658,7 @@ mod property_key_tests { 0, 0, 1, + 0, ); let method = crate::object::class_registry::lookup_class_symbol_method_in_chain( class_id, sym_key, false, diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index f457330246..7b7e45fe3d 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -872,22 +872,7 @@ pub(crate) fn class_default_prototype_superseded(obj_ptr: usize) -> bool { } pub(crate) fn default_object_prototype_bits() -> Option { - let object_ctor = super::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - let ctor_bits = object_ctor.to_bits(); - if (ctor_bits >> 48) != 0x7FFD { - return None; - } - let ctor_ptr = (ctor_bits & crate::value::POINTER_MASK) as usize; - if ctor_ptr == 0 { - return None; - } - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_bits = proto.to_bits(); - if (proto_bits >> 48) == 0x7FFD { - Some(proto_bits) - } else { - None - } + crate::object::object_prototype_intrinsic_bits() } pub(crate) unsafe fn default_object_prototype_for_owner(obj_ptr: usize) -> Option { diff --git a/crates/perry-runtime/src/object/test_root_helpers.rs b/crates/perry-runtime/src/object/test_root_helpers.rs index fdc441ad0d..7dde1e66b4 100644 --- a/crates/perry-runtime/src/object/test_root_helpers.rs +++ b/crates/perry-runtime/src/object/test_root_helpers.rs @@ -166,6 +166,8 @@ pub(crate) fn test_realm_owned_root_snapshot() -> Vec<(&'static str, usize, u64) "ASYNC_GENERATOR_PROTOTYPE_PTR", &ASYNC_GENERATOR_PROTOTYPE_PTR, ), + ("OBJECT_INTRINSIC_PTR", &OBJECT_INTRINSIC_PTR), + ("OBJECT_INTRINSIC_PROTO_PTR", &OBJECT_INTRINSIC_PROTO_PTR), ("LOCAL_STORAGE_PTR", &LOCAL_STORAGE_PTR), ("SESSION_STORAGE_PTR", &SESSION_STORAGE_PTR), ( diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index 49c8bf60ff..17127a063b 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -367,3 +367,36 @@ pub extern "C" fn js_derived_this_check_current() -> f64 { } f64::from_bits(crate::value::TAG_UNDEFINED) } + +/// Prologue of a static method's closure-convention entry +/// (`__clo`): the call's `this` (the entry's receiver parameter, +/// passed by whatever called the function object: `C.m()`, `f.call(x)`, a bare +/// `f()`) becomes the body's `this`; class `class_id` (the declaring class) is its +/// static-private owner, whatever the receiver. Paired with +/// [`js_static_method_entry_leave`] after the body returns. +// #1561-style force-keep: only generated IR calls this. +#[cfg(feature = "keepalive-anchors")] +#[used(compiler)] +static KEEP_JS_STATIC_METHOD_ENTRY_ENTER: extern "C" fn(u32, u64) = js_static_method_entry_enter; + +#[no_mangle] +pub extern "C" fn js_static_method_entry_enter(class_id: u32, this_bits: u64) { + // Minting the owner's function object allocates: root `this` across it. + let this_scope = crate::gc::RuntimeHandleScope::new(); + let this = this_scope.root_nanbox_f64(f64::from_bits(this_bits)); + static_private_owner_push(super::class_value::class_value(class_id)); + static_this_arm(this.get_nanbox_f64()); +} + +/// Epilogue of a static method's closure-convention entry: pops the owner the +/// prologue pushed and drops an override the body never consumed. +// #1561-style force-keep: only generated IR calls this. +#[cfg(feature = "keepalive-anchors")] +#[used(compiler)] +static KEEP_JS_STATIC_METHOD_ENTRY_LEAVE: extern "C" fn() = js_static_method_entry_leave; + +#[no_mangle] +pub extern "C" fn js_static_method_entry_leave() { + static_private_owner_pop(); + static_this_disarm(); +} diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 49f4a54321..938c93787c 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -2469,11 +2469,14 @@ pub extern "C" fn js_super_put_value_set( // the value captured at class-definition time. The previous instance-only // path looked at `Parent.prototype` and made valid static writes fail. if let Some(child_id) = crate::object::class_ref_id(receiver) { - let target = if parent_class_id != 0 { - crate::object::class_value::class_value(parent_class_id) - } else { - crate::object::js_get_dynamic_parent_value(child_id) - }; + // A builtin parent id (`extends Error`) never gets a class function + // object; its constructor is the class's dynamic parent value. + let target = + if parent_class_id != 0 && crate::object::is_class_id_registered(parent_class_id) { + crate::object::class_value::class_value(parent_class_id) + } else { + crate::object::js_get_dynamic_parent_value(child_id) + }; let tv = crate::value::JSValue::from_bits(target.to_bits()); if !tv.is_undefined() && !tv.is_null() { return js_put_value_set(target, key, value, receiver, strict); diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 2ae7b3edf0..3fc485fac3 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -2729,6 +2729,26 @@ "name": "CLASS_SHAPE", "verdict": "not_a_gc_pointer", "why": "This agent's class-constructor ShapeId (a u32 shape-directory index minted once and pinned as an external shape carrier), never a heap reference." + }, + { + "file": "crates/perry-runtime/src/object/global_this/object_intrinsic.rs", + "name": "OBJECT_INTRINSICS_BUILDING", + "verdict": "not_a_gc_pointer", + "why": "A bool: set while this thread builds its %Object% / %Object.prototype% pair, so the build's own stores do not re-enter the build. No address is ever stored in it." + }, + { + "file": "crates/perry-runtime/src/object/mod.rs", + "name": "OBJECT_INTRINSIC_PTR_SLOT", + "verdict": "covered_elsewhere", + "scanner": "object::scan_object_cache_roots_mut (crates/perry-runtime/src/object/mod.rs), through the RealmAtomicI64 wrapper OBJECT_INTRINSIC_PTR", + "why": "This realm's %Object% constructor (a ClosureHeader), built by ensure_object_intrinsics and adopted as globalThis.Object. The registered object-cache scanner visits the slot through its RealmAtomicI64 wrapper, whose name differs from the thread-local's, so the same-file rule cannot match it." + }, + { + "file": "crates/perry-runtime/src/object/mod.rs", + "name": "OBJECT_INTRINSIC_PROTO_PTR_SLOT", + "verdict": "covered_elsewhere", + "scanner": "object::scan_object_cache_roots_mut (crates/perry-runtime/src/object/mod.rs), through the RealmAtomicI64 wrapper OBJECT_INTRINSIC_PROTO_PTR", + "why": "This realm's %Object.prototype%, built by ensure_object_intrinsics and adopted as globalThis.Object.prototype. The registered object-cache scanner visits the slot through its RealmAtomicI64 wrapper, whose name differs from the thread-local's, so the same-file rule cannot match it. Its address is also memoized in the prototype-address cache row, which scan_prototype_addr_cache_roots_mut rewrites." } ], "_FRONTIER_README": "Identity-pinned debt ratchet over new perry-ui* candidates and otherwise-unclassified core raw/Perry TLS declarations (see the census docstring, \u201cThe identity-pinned frontier\u201d). A new uncovered holder fails until it is scanned, receives a researched holders verdict, or is deliberately pinned as debt. Moving a researched false positive to holders graduates it from this list. A fixed or classified holder makes its old frontier pin stale, so the receipt must be deleted.", @@ -3658,10 +3678,6 @@ "file": "crates/perry-runtime/src/object/class_registry/state.rs", "name": "CLASS_DECL_PROTOTYPE_OBJECTS" }, - { - "file": "crates/perry-runtime/src/object/class_registry/state.rs", - "name": "CLASS_DELETED_KEYS" - }, { "file": "crates/perry-runtime/src/object/class_registry/state.rs", "name": "CLASS_DYNAMIC_PARENT_VALUE" @@ -3764,10 +3780,6 @@ "file": "crates/perry-runtime/src/object/mod.rs", "name": "CLASS_PROTOTYPE_METHOD_VALUES" }, - { - "file": "crates/perry-runtime/src/object/mod.rs", - "name": "CLASS_STATIC_DEFINED_ATTRS" - }, { "file": "crates/perry-runtime/src/object/mod.rs", "name": "FS_CONSTANTS_CACHE_SLOT" diff --git a/test-files/test_gap_class_computed_static_method.ts b/test-files/test_gap_class_computed_static_method.ts new file mode 100644 index 0000000000..45ff7dc718 --- /dev/null +++ b/test-files/test_gap_class_computed_static_method.ts @@ -0,0 +1,46 @@ +// A computed-name ClassBody static method is an own data property of the class +// function object, like a named one: one function object per (class, method), +// `name` = the key, the declared attributes, inherited by a subclass, and a +// direct call runs it only while the property still holds it. +const k = "comp"; +const w = "who"; +let order: string[] = []; +function key(s: string): string { + order.push(s); + return s; +} +class A { + static first() { + return 0; + } + static [k](x: number) { + return x + 1; + } + static [key("late")](a: number, b = 2) { + return a + b; + } +} +console.log(order.join(",")); +console.log(Object.getOwnPropertyNames(A).join(",")); +console.log(typeof A.comp, A.comp(1), A.comp.name, A.comp.length); +console.log((A as any).late.name, (A as any).late.length, (A as any).late(1)); +const d = Object.getOwnPropertyDescriptor(A, "comp")!; +console.log(d.writable, d.enumerable, d.configurable, d.value === A.comp); +console.log(A.comp === A.comp, Object.keys(A).length); +class B extends A {} +console.log(B.comp === A.comp, B.comp(2), Object.hasOwn(B, "comp")); +(A as any).comp = function (x: number) { + return -x; +}; +console.log(B.comp(2), A.comp(3)); +delete (A as any).comp; +console.log(typeof (A as any).comp, "comp" in A, "comp" in B); +class W { + static [w]() { + return this === W ? "W" : this === V ? "V" : "other"; + } +} +class V extends W {} +console.log((W as any).who(), (V as any).who()); +const f = (W as any).who; +console.log(f.call(V), String(f).startsWith("[w]") || String(f).includes("return this")); diff --git a/test-files/test_gap_class_delete_redefine.ts b/test-files/test_gap_class_delete_redefine.ts new file mode 100644 index 0000000000..ca61eaaeed --- /dev/null +++ b/test-files/test_gap_class_delete_redefine.ts @@ -0,0 +1,44 @@ +// delete is a real delete, and a later definition brings the key back +class A { + static s() { return "A.s"; } + static f = 1; + m() { return "A.m"; } +} +class B extends A { + static s() { return "B.s"; } + m() { return "B.m"; } +} +const b = new B(); +console.log(B.s(), b.m()); +delete (B as any).s; +console.log(B.s(), Object.prototype.hasOwnProperty.call(B, "s"), Object.getOwnPropertyNames(B).join(",")); +delete (B.prototype as any).m; +console.log(b.m(), Object.prototype.hasOwnProperty.call(B.prototype, "m"), "m" in b); +(B as any).s = function () { return "B.s2"; }; +console.log(B.s(), Object.getOwnPropertyDescriptor(B, "s")!.enumerable); +(B.prototype as any).m = function () { return "B.m2"; }; +console.log(b.m(), Object.prototype.hasOwnProperty.call(B.prototype, "m")); +Object.defineProperty(B, "s", { value: () => "B.s3", writable: false, enumerable: false, configurable: true }); +console.log(B.s(), JSON.stringify(Object.getOwnPropertyDescriptor(B, "s")!.writable)); +// A static store of the same name never resurrects a deleted prototype member +class C { k() { return "C.k"; } } +const c = new C(); +delete (C.prototype as any).k; +(C as any).k = 5; +console.log(typeof (c as any).k, (C as any).k, "k" in c); +// intrinsic name/length: delete, then define again +class D { constructor(a: number, b: number) {} } +delete (D as any).name; +console.log(Object.prototype.hasOwnProperty.call(D, "name"), JSON.stringify(D.name)); +Object.defineProperty(D, "name", { value: "Dee" }); +console.log(D.name, D.length); +delete (D as any).length; +console.log(D.length, Object.getOwnPropertyNames(D).join(",")); +// static field delete then re-store +delete (A as any).f; +console.log((A as any).f, (B as any).f, Object.prototype.hasOwnProperty.call(A, "f")); +(A as any).f = 7; +console.log((A as any).f, (B as any).f); +// delete the parent's static: the child now misses it too +delete (A as any).s; +console.log(typeof (B as any).s, typeof (A as any).s); diff --git a/test-files/test_gap_class_same_name_scopes.ts b/test-files/test_gap_class_same_name_scopes.ts new file mode 100644 index 0000000000..41f29ffc16 --- /dev/null +++ b/test-files/test_gap_class_same_name_scopes.ts @@ -0,0 +1,92 @@ +// Two (and more) classes with the same name in different scopes are distinct +// classes: each has its own static data properties, static methods (own +// function objects running their own bodies), inheritance and identity. + +function makeA() { + class Box { + static tag = "a"; + static count = 1; + static make(x: number) { + return x + 1; + } + static who() { + return "A:" + this.tag; + } + get kind() { + return "boxA"; + } + } + return Box; +} + +function makeB() { + class Box { + static tag = "b"; + static make(x: number) { + return x * 10; + } + static who() { + return "B:" + this.tag; + } + static only() { + return "only-b"; + } + get kind() { + return "boxB"; + } + } + return Box; +} + +const A = makeA(); +const B = makeB(); +console.log(A.name, B.name, A === B); +console.log(A.tag, B.tag, (A as any).count, (B as any).count); +console.log(A.make(1), B.make(1)); +console.log(A.who(), B.who()); +console.log(typeof (A as any).only, typeof (B as any).only); +console.log(A.make === B.make, A.who === B.who); +console.log(Object.getOwnPropertyNames(A).join(",")); +console.log(Object.getOwnPropertyNames(B).join(",")); +console.log(new A().kind, new B().kind); +console.log(new A() instanceof A, new A() instanceof B, new B() instanceof B); + +// A write on one never reaches the other. +(A as any).make = (x: number) => x - 100; +console.log(A.make(1), B.make(1)); +delete (B as any).who; +console.log(typeof A.who, typeof (B as any).who); + +// Block scopes, including one shadowing a module-level class of the same name. +class Pt { + static origin() { + return "outer"; + } + static z = 0; +} +{ + class Pt { + static origin() { + return "block1"; + } + static z = 1; + } + console.log(Pt.origin(), Pt.z); + class Sub extends Pt {} + console.log(Sub.origin(), Sub.z); +} +{ + class Pt { + static origin() { + return "block2:" + this.z; + } + static z = 2; + } + console.log(Pt.origin(), Pt.z); +} +console.log(Pt.origin(), Pt.z); + +// Static calls in loops through each same-named class (the static-call guard). +let s = 0; +for (let i = 0; i < 5; i++) s = A.make(s) + B.make(1); +console.log(s); diff --git a/test-files/test_gap_class_static_attrs_delete.ts b/test-files/test_gap_class_static_attrs_delete.ts new file mode 100644 index 0000000000..0e88fa9508 --- /dev/null +++ b/test-files/test_gap_class_static_attrs_delete.ts @@ -0,0 +1,34 @@ +// Class static attributes live with the keys of the class function object. +class C { + static f = 1; + static g() { return 2; } +} +const d = (k: string) => JSON.stringify(Object.getOwnPropertyDescriptor(C, k)); +console.log(d("f"), d("name"), d("length")); +Object.defineProperty(C, "x", { value: 1, writable: false, enumerable: false, configurable: true }); +console.log(d("x"), Object.keys(C).join(",")); +try { (C as any).x = 2; } catch (e) { console.log("threw x"); } +console.log((C as any).x); +delete (C as any).x; +console.log(d("x"), "x" in C); +(C as any).x = 3; +console.log(d("x"), (C as any).x, Object.keys(C).join(",")); +Object.defineProperty(C, "f", { enumerable: false }); +console.log(d("f"), Object.keys(C).join(",")); +delete (C as any).f; +(C as any).f = 4; +console.log(d("f"), Object.keys(C).join(",")); +Object.defineProperty(C, "name", { value: "K" }); +console.log(d("name"), C.name); +delete (C as any).name; +console.log(d("name"), typeof C.name); +try { (C as any).name = "Z"; } catch (e) { console.log("threw name"); } +console.log(d("name"), C.name); +class D { static name = "field"; } +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(D, "name"))); +(D as any).name = "w"; +console.log(D.name); +Object.freeze(C); +console.log(Object.isFrozen(C), d("f")); +try { (C as any).f = 9; } catch (e) { console.log("threw f"); } +console.log((C as any).f); diff --git a/test-files/test_gap_class_static_inherited_store.ts b/test-files/test_gap_class_static_inherited_store.ts new file mode 100644 index 0000000000..33ff871af6 --- /dev/null +++ b/test-files/test_gap_class_static_inherited_store.ts @@ -0,0 +1,20 @@ +// A static-call site on a subclass (`S.m()` where only the parent R declares +// `m`) memoizes BOTH classes' shapes: storing `R.m` transitions R's shape and +// leaves S's alone, so a guard that checked only S would keep calling the old +// body. The loop bound is not a compile-time constant, so the site is one +// site that is armed on the first call and hit on the next ones. +class R { + static m() { + return "R.m"; + } +} +class S extends R {} +class T extends S {} +const n = process.argv.length + 5; +const out: string[] = []; +for (let i = 0; i < n; i++) { + if (i === 3) (R as any).m = function () { return "stored"; }; + if (i === 5) (S as any).m = function () { return "S.own"; }; + out.push(S.m() + "/" + T.m()); +} +console.log(out.join(",")); diff --git a/test-files/test_gap_class_static_method_props.ts b/test-files/test_gap_class_static_method_props.ts new file mode 100644 index 0000000000..2d5b94dfb1 --- /dev/null +++ b/test-files/test_gap_class_static_method_props.ts @@ -0,0 +1,77 @@ +// static methods are own data properties of the class function object +class P { + static a() { return "P.a:" + (this === P ? "P" : (this as any).name); } + static get g() { return 1; } + static b(x: number, y: number) { return x + y; } + static z = 3; +} +class Q extends P {} +const d = Object.getOwnPropertyDescriptor(P, "a")!; +console.log(typeof d.value, d.writable, d.enumerable, d.configurable); +console.log(P.a === P.a, Q.a === P.a, d.value === P.a); +console.log(Object.prototype.hasOwnProperty.call(Q, "a"), Object.keys(P).join(",")); +console.log(Object.getOwnPropertyNames(P).join(",")); +console.log(P.a(), Q.a(), P.b.length, P.b.name, P.b(2, 3)); +const saved = P.a; +console.log(saved.call(Q)); +(P as any).a = function () { return "replaced"; }; +console.log(P.a(), Q.a(), saved.call(P)); +delete (P as any).a; +console.log(typeof (P as any).a, typeof (Q as any).a, "a" in P); +for (const k in P) console.log("enum", k); +console.log(Object.isFrozen(Object.freeze(Q)), Object.getOwnPropertyDescriptor(P, "b")!.configurable); +// One call site, armed before the store: its memo must see the store. +class R { + static m() { return "R.m"; } +} +class S extends R {} +function callBoth() { return R.m() + "," + S.m(); } +function callValue(c: any) { return c.m(); } +for (let i = 0; i < 3; i++) console.log(callBoth(), callValue(R), callValue(S)); +(R as any).m = function () { return "stored:" + (this === S ? "S" : "R"); }; +console.log(callBoth(), callValue(R), callValue(S)); +(S as any).m = function () { return "own S"; }; +console.log(callBoth(), callValue(S)); +delete (R as any).m; +delete (S as any).m; +console.log(typeof (R as any).m, typeof (S as any).m); +// A class nothing inherits from: its shape changes only because the +// declaration was replaced, redefined or deleted. +class T { + static m() { return "T.m"; } +} +function callT() { return T.m(); } +function callTv(c: any) { return c.m(); } +for (let i = 0; i < 3; i++) console.log(callT(), callTv(T)); +(T as any).m = function () { return "T stored"; }; +console.log(callT(), callTv(T)); +Object.defineProperty(T, "m", { value: function () { return "T defined"; } }); +console.log(callT(), callTv(T)); +delete (T as any).m; +try { callT(); } catch (e) { console.log("callT", e instanceof TypeError); } +try { callTv(T); } catch (e) { console.log("callTv", e instanceof TypeError); } +// Every store form over an armed site's declaration reaches the call. +const forms: [string, (R: any) => void][] = [ + ["assign", (R) => { R.m = function () { return "assign"; }; }], + ["define", (R) => { Object.defineProperty(R, "m", { value: function () { return "define"; } }); }], + ["reflect", (R) => { Reflect.set(R, "m", function () { return "reflect"; }); }], + ["assignObj", (R) => { Object.assign(R, { m: function () { return "assignObj"; } }); }], + ["defineAll", (R) => { Object.defineProperties(R, { m: { value: function () { return "defineAll"; } } }); }], +]; +class A1 { static m() { return "decl"; } } +function s1() { return A1.m(); } +class A2 { static m() { return "decl"; } } +function s2() { return A2.m(); } +class A3 { static m() { return "decl"; } } +function s3() { return A3.m(); } +class A4 { static m() { return "decl"; } } +function s4() { return A4.m(); } +class A5 { static m() { return "decl"; } } +function s5() { return A5.m(); } +const cs: any[] = [A1, A2, A3, A4, A5]; +const ss = [s1, s2, s3, s4, s5]; +for (let i = 0; i < 5; i++) { + ss[i](); ss[i](); + forms[i][1](cs[i]); + console.log(forms[i][0], ss[i]()); +} diff --git a/test-files/test_gap_class_static_setter_receives_value.ts b/test-files/test_gap_class_static_setter_receives_value.ts new file mode 100644 index 0000000000..201be04b14 --- /dev/null +++ b/test-files/test_gap_class_static_setter_receives_value.ts @@ -0,0 +1,75 @@ +// A class's static setter receives the assigned VALUE on every route: a +// direct store, a store through a variable or a shared (warmed) put site, an +// inherited store, Reflect.set, and the reflected setter function called with +// an explicit receiver. A static accessor's compiled entry takes no receiver +// parameter (unlike an instance accessor's), so a route that calls it with +// the instance convention hands the setter the class instead of the value. +// +// Output must be byte-identical to node. +class HasStatic { + static seen: unknown = null; + static get tag(): unknown { + return HasStatic.seen; + } + static set tag(v: unknown) { + HasStatic.seen = "set:" + String(v); + } +} +class SubStatic extends HasStatic {} +let P: any = HasStatic; +let Q: any = SubStatic; +function put(o: any, v: unknown): void { + o.tag = v; +} +HasStatic.tag = 7; +console.log("direct", HasStatic.seen); +P.tag = 8; +console.log("var", HasStatic.seen); +put(P, 1); +console.log("fn-cold", HasStatic.seen); +Q.tag = 3; +console.log("inherited", HasStatic.seen); +put({ a: 1 }, 0); +put({ b: 1, c: 2 }, 0); +put(new Map(), 0); +put(P, 5); +console.log("fn-warm", HasStatic.seen); +put(Q, 4); +console.log("fn-warm-inherited", HasStatic.seen); +Reflect.set(P, "tag", 6); +console.log("reflect", HasStatic.seen); +const d = Object.getOwnPropertyDescriptor(HasStatic, "tag")!; +d.set!.call(HasStatic, 9); +console.log("descriptor-set", HasStatic.seen, d.get!.call(HasStatic)); + +// `this` in a static accessor is the receiver the access went through. +class ThisStatic { + static store: string = ""; + static get who(): string { + return (this as any).name; + } + static set who(v: string) { + (this as any).store = (this as any).name + "=" + v; + } +} +class ThisSub extends ThisStatic {} +ThisSub.who = "a"; +console.log("this-inherited", ThisSub.who, (ThisSub as any).store, ThisStatic.store); +const dw = Object.getOwnPropertyDescriptor(ThisStatic, "who")!; +dw.set!.call(ThisSub, "b"); +console.log("this-descriptor", dw.get!.call(ThisSub), (ThisSub as any).store); + +// A symbol-keyed static accessor, reflected. +const k = Symbol("k"); +class SymStatic { + static v: number = 0; + static get [k](): number { + return SymStatic.v; + } + static set [k](n: number) { + SymStatic.v = n * 2; + } +} +const ds = Object.getOwnPropertyDescriptor(SymStatic, k)!; +ds.set!.call(SymStatic, 11); +console.log("symbol-descriptor", SymStatic.v, ds.get!.call(SymStatic)); diff --git a/test-files/test_issue_336_class_keys_collision.ts b/test-files/test_issue_336_class_keys_collision.ts index fbfae30777..34661aa6b8 100644 --- a/test-files/test_issue_336_class_keys_collision.ts +++ b/test-files/test_issue_336_class_keys_collision.ts @@ -41,12 +41,8 @@ namespace B { // (deduplicated) class. Pre-fix this test failed at clang IR validation; // post-fix it compiles + runs. // -// The lookup pipeline is purely name-based today, so the dedup keeps the -// FIRST `Refinement` (the one in namespace A) and both `make()` bodies -// resolve `new Refinement()` to it — `b.kind` therefore prints `a`, not -// `b`. That's the existing function-scoped semantics extended to namespace -// scope; pinning a distinct identity per scope would need scope info on -// `Expr::New { class_name }` and is a separate, larger change. +// Each namespace resolves its own Refinement (classes are +// registered by identity), so b.kind prints b. const a = A.make(); const b = B.make(); console.log("a.kind:", a.kind); diff --git a/test-parity/expected/test_issue_336_class_keys_collision.txt b/test-parity/expected/test_issue_336_class_keys_collision.txt index 20e5e041ef..cf0df79b10 100644 --- a/test-parity/expected/test_issue_336_class_keys_collision.txt +++ b/test-parity/expected/test_issue_336_class_keys_collision.txt @@ -1,6 +1,6 @@ a.kind: a a.value: 1 a.describe(): a/1 -b.kind: a -b.value: 1 -b.describe(): a/1 +b.kind: b +b.value: 2 +b.describe(): b/2