From abf348aa5c9503c1e0e9144944e5a5547e9a1e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 17:22:53 +0000 Subject: [PATCH 1/8] hir: a static extends of a per-evaluation class reads the evaluated class A class declared in a function that is created per evaluation binds its name to the evaluated class object. When another class of the same name made the lowering scope-rename it, the heritage check took the rename as proof that the name is a class and resolved the parent statically, to the template every evaluation shares. The parent is now read from the local binding, as a dynamic extends does, whenever the renamed class is one lowered per evaluation. Part of #11759. --- crates/perry-hir/src/lower/context.rs | 21 +++++ crates/perry-hir/src/lower/tests.rs | 1 + .../tests/fresh_class_extends_renamed.rs | 76 +++++++++++++++++++ crates/perry-hir/src/lower_decl/class_decl.rs | 7 +- .../src/lower_decl/class_decl/from_ast.rs | 7 +- 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 crates/perry-hir/src/lower/tests/fresh_class_extends_renamed.rs diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index a65a17e07e..c3362e4c20 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -259,6 +259,27 @@ impl LoweringContext { self.classes_index.get(name).map(|&idx| self.classes[idx].1) } + /// Is the heritage identifier `name` a lexical LOCAL binding (the heritage + /// value must then be read from that binding at runtime), rather than a + /// statically resolvable class? + /// + /// A scope-renamed class (`class_renames`) normally resolves statically, + /// even though a same-named local may also be in scope. The exception is a + /// class lowered per evaluation (`per_evaluation_class_decls`): its + /// declared name IS a local bound to the evaluated class object, and a + /// static `extends` would instead reach the shared template, so the + /// subclass's [[Prototype]] and the instance chains of two evaluations + /// would be the template's instead of the evaluation's own. + pub(crate) fn heritage_ident_is_lexical_local(&self, name: &str) -> bool { + if self.locals.lookup(name).is_none() { + return false; + } + !self.class_renames.contains_key(name) + || self + .per_evaluation_class_decls + .contains(&self.resolve_class_name(name)) + } + /// Apply any active scope-local class-name alias (see `class_renames`). /// Identity for non-aliased names, so non-colliding classes are unaffected. pub(crate) fn resolve_class_name(&self, name: &str) -> String { diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index 2d8ed15361..4151c89f4f 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -77,6 +77,7 @@ fn a_lexical_fetch_result_is_not_registered_as_a_native_response() { mod buffer_static_values; mod class_decl_self_binding; +mod fresh_class_extends_renamed; mod instanceof_rhs; mod literal_shape; diff --git a/crates/perry-hir/src/lower/tests/fresh_class_extends_renamed.rs b/crates/perry-hir/src/lower/tests/fresh_class_extends_renamed.rs new file mode 100644 index 0000000000..9c9cc72b82 --- /dev/null +++ b/crates/perry-hir/src/lower/tests/fresh_class_extends_renamed.rs @@ -0,0 +1,76 @@ +//! `class D extends L`, with L a class created per evaluation whose name the +//! lowering had to scope-rename (another `class L` is declared elsewhere in the +//! file), takes its parent from the evaluated class bound to `L`, as a dynamic +//! `extends` does. Resolved statically it named the template every evaluation +//! of L shares, so two evaluations of the enclosing function gave D the same +//! parent. Without a same-named class elsewhere the heritage was dynamic +//! already; the rename is what hid the local from the heritage check. + +use crate::ir::{Class, Module}; + +fn lower(source: &str) -> Module { + let module = perry_parser::parse_typescript(source, "fresh-extends.ts").expect("source parses"); + crate::lower::lower_module(&module, "fresh-extends", "fresh-extends.ts").expect("source lowers") +} + +fn class<'a>(hir: &'a Module, name: &str) -> &'a Class { + hir.classes + .iter() + .find(|c| c.name == name) + .unwrap_or_else(|| panic!("class `{name}` must be lowered: {:?}", hir.classes)) +} + +const RENAMED_FRESH_PARENT: &str = r#" + export const other = () => { class L {} return L; }; + export function make(n: number) { + const k = n; + class L { v() { return k; } } + class D extends L {} + return D; + } +"#; + +#[test] +fn a_renamed_fresh_parent_is_read_from_its_binding() { + let hir = lower(RENAMED_FRESH_PARENT); + let d = class(&hir, "D"); + assert!( + d.extends_expr.is_some(), + "D must take its parent from the evaluated `L`, got extends={:?} extends_name={:?}", + d.extends, + d.extends_name + ); + assert!( + d.extends.is_none(), + "no static parent class: {:?}", + d.extends + ); + assert!( + d.extends_name.is_none(), + "no retained parent name for the static chain walks: {:?}", + d.extends_name + ); +} + +#[test] +fn a_renamed_shared_parent_stays_static() { + // Two same-named classes, neither per evaluation: the rename resolves the + // heritage statically to the right one, as before. + let hir = lower( + r#" + export const other = () => { class L {} return L; }; + export function make() { + class L { v() { return 1; } } + class D extends L {} + return D; + } + "#, + ); + let d = class(&hir, "D"); + assert!( + d.extends_expr.is_none(), + "a shared parent is static: {:?}", + d.extends_expr + ); + assert!(d.extends.is_some(), "a shared parent is static"); +} diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 094f466e97..d1b6bb9651 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -179,7 +179,7 @@ pub fn lower_class_decl( let heritage_lexically_shadowed = match class_decl.class.super_class.as_deref() { Some(ast::Expr::Ident(ident)) => { let n = ident.sym.to_string(); - !ctx.class_renames.contains_key(&n) && ctx.locals.lookup(&n).is_some() + ctx.heritage_ident_is_lexical_local(&n) } _ => false, }; @@ -287,9 +287,8 @@ pub fn lower_class_decl( .require_destructured_native_locals .get(&parent_name) .is_some_and(|key| *key == canonical_parent_name); - let locally_shadowed = !ctx.class_renames.contains_key(&parent_name) - && ctx.locals.lookup(&parent_name).is_some() - && !require_native_reexport; + let locally_shadowed = + ctx.heritage_ident_is_lexical_local(&parent_name) && !require_native_reexport; if native_parent.is_some() && !locally_shadowed { // Keep `extends_name` populated alongside `native_extends` // so SuperCall codegen + downstream chain walks still diff --git a/crates/perry-hir/src/lower_decl/class_decl/from_ast.rs b/crates/perry-hir/src/lower_decl/class_decl/from_ast.rs index c78736327c..426b99d6cc 100644 --- a/crates/perry-hir/src/lower_decl/class_decl/from_ast.rs +++ b/crates/perry-hir/src/lower_decl/class_decl/from_ast.rs @@ -64,7 +64,7 @@ pub(crate) fn lower_class_from_ast( let heritage_lexically_shadowed = match class.super_class.as_deref() { Some(ast::Expr::Ident(ident)) => { let n = ident.sym.to_string(); - !ctx.class_renames.contains_key(&n) && ctx.locals.lookup(&n).is_some() + ctx.heritage_ident_is_lexical_local(&n) } _ => false, }; @@ -138,9 +138,8 @@ pub(crate) fn lower_class_from_ast( .require_destructured_native_locals .get(&parent_name) .is_some_and(|key| *key == canonical_parent_name); - let locally_shadowed = !ctx.class_renames.contains_key(&parent_name) - && ctx.locals.lookup(&parent_name).is_some() - && !require_native_reexport; + let locally_shadowed = + ctx.heritage_ident_is_lexical_local(&parent_name) && !require_native_reexport; if native_parent.is_some() && !locally_shadowed { (None, Some(canonical_parent_name), native_parent, None) } else if locally_shadowed { From 95f3c10907aea953588b9844e2faeae6c23b9dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 17:23:17 +0000 Subject: [PATCH 2/8] runtime: a fresh class object owns its length, name and static methods A class object made per evaluation kept its static methods only in the template's registry, which every evaluation of the class shares, and answered length, name and prototype from the registry too. getOwnPropertyNames listed none of them, hasOwn/in/getOwnPropertyDescriptor missed them, and a delete was remembered per template or not at all. js_class_object_pin_parent now also gives the object its own length and name (non-writable, configurable) and every static method as a data property bound to the object, in class-body order, as a class's function object has them. A static field named length or name takes over the slot created for it, with the field's attributes (a mask argument from codegen). prototype is non-configurable and non-writable, so it is present for the object's whole life without being stored: getOwnPropertyNames, hasOwn, in and getOwnPropertyDescriptor answer for it. The object's own keys are now the authority: a deleted static method stays deleted for that evaluation alone, a call or read no longer falls back to the registry for a name the object's own template declares, an inherited one is found on the evaluation that declares it, and the shared function object's last-wins mirror of writes no longer retires a declaration for sibling evaluations. The object is now rooted for the whole evaluation, since defining its properties allocates. Part of #11759. --- .../src/expr/static_field_meta.rs | 120 +++++----- .../src/runtime_decls/objects.rs | 2 +- .../perry-codegen/src/wasm32/runtime_abi.tsv | 2 +- .../src/closure/dispatch/bound.rs | 15 ++ .../src/object/class_registry.rs | 13 +- .../evaluation_heritage/tests.rs | 6 +- .../object/class_registry/parent_static.rs | 33 ++- .../src/object/class_registry/state.rs | 11 + .../perry-runtime/src/object/class_value.rs | 15 +- .../perry-runtime/src/object/descriptors.rs | 56 +++++ .../perry-runtime/src/object/field_get_set.rs | 6 +- .../field_get_set/class_object_props.rs | 210 ++++++++++++++++-- .../object/field_get_set/get_field_by_name.rs | 17 +- .../src/object/field_get_set/has_property.rs | 14 +- crates/perry-runtime/src/object/mod.rs | 1 + .../native_call_method/primitive_methods.rs | 8 +- .../src/object/object_ops/has_own.rs | 9 + 17 files changed, 434 insertions(+), 104 deletions(-) diff --git a/crates/perry-codegen/src/expr/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs index 659ba0adc2..72173b55fe 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -608,7 +608,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } => { let template_cid = ctx.class_ids.get(template).copied().unwrap_or(0); let tcid_str = template_cid.to_string(); - let nfields = named_statics.len().to_string(); + // Room for the evaluation's own `length`, `name` and static + // methods (`js_class_object_pin_parent` defines them) besides its + // static fields. + let own_member_slots = 2 + ctx + .classes + .get(template) + .map_or(0, |c| c.static_methods.len()); + let nfields = (named_statics.len() + own_member_slots).to_string(); // Allocate with class_id = template; set_field_by_name below // performs the keys-array transition for the named statics. let obj = @@ -619,76 +626,55 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // class_id from this object rather than treating it as an instance. ctx.block() .call_void("js_object_mark_class", &[(I64, &obj)]); - // #6438: pin THIS evaluation's parent onto the object. The lowering - // sequences `RegisterClassParentDynamic` immediately ahead of this - // node, so `CLASS_DYNAMIC_PARENT_VALUE[template]` still holds this - // evaluation's parent; later evaluations overwrite it, but each - // object keeps its own edge. Without this, a factory invoked more - // than once (effect's `class DeclareClass extends make(ast) { … }`) - // has every instance walk to the LAST parent — reading that - // evaluation's `static ast` instead of its own. No-op when the class - // expression has no heritage. - ctx.block().call_void( - "js_class_object_pin_parent", - &[(I64, &obj), (I32, &tcid_str)], - ); - // #7154: the fresh class object is a raw SSA register while every - // static initializer and captured argument is lowered, and those - // allocate. An evacuating minor relocates it, after which each - // remaining `js_object_set_field_by_name` writes into from-space — - // the statics land on the abandoned copy. Same rooting contract - // `Expr::Object` has used since #6951. - // - // `captured_args` forces protection on its own, independently of - // whether the capture *expressions* collect: the snapshot below - // allocates a `js_array_alloc` accumulator and grows it with - // `js_array_push_f64` per element, and those are collection points - // even when every element is an inert `LocalGet`. + // #7154: the fresh class object is a raw SSA register while the + // evaluation allocates (its own members, every static initializer + // and captured argument), and an evacuating minor relocates it, + // after which each later `js_object_set_field_by_name` would write + // into from-space — the statics would land on the abandoned copy. + // So the object is always held in a rooted group (`Expr::Object`'s + // contract since #6951), re-read before each use. // - // So does a `static { … }` block, for the plainer reason that its - // body is arbitrary user code — which is why `block_fns` is computed - // HERE rather than at its loop below: the predicate has to see it. - // A class expression whose only statics are inert (`static x = 1`) - // but which carries a static block otherwise pushed no root at all, - // and the block's body could then relocate the object out from under - // the register the final `nanbox_pointer_inline` reads. + // It used to be skipped for a class whose body held nothing that + // allocates. That is no longer a case: `js_class_object_pin_parent` + // defines the object's own `length`, `name` and static methods + // right after it is created, which allocates for every class. + // (`js_object_mark_class` does not protect the register: it files + // the pointer in `CLASS_OBJECT_VALUES`, a forwarded root that keeps + // the OBJECT alive but never rewrites `%obj`.) let block_fns = static_block_fns(ctx, template); - // #7211: `!named_statics.is_empty()` is the disjunct the original - // predicate was missing, and its absence is the interesting part. - // - // Every other clause here asks the same question — "can something - // the AUTHOR wrote collect?" — about a captured argument, a symbol - // static, a `static { … }` body, or an initializer expression. - // None of them asks whether the lowering's OWN emitted calls can, - // and the loop directly below unconditionally emits one - // `js_object_set_field_by_name` per named static. That helper - // performs the keys-array transition and allocates. So - // `class C { static tag = tag }` — a single inert `LocalGet` - // initializer — took `protect_handle == false`, kept the fresh - // object in a bare SSA register across a collection point, and - // then bound a shadow slot to the pre-move address. - // - // `js_object_mark_class` does NOT cover this, and it is the - // natural reason to wave it off: it files the pointer in - // `CLASS_OBJECT_VALUES`, which is a registered root and IS - // forwarded (`class_registry/gc_roots.rs:138`). That keeps the - // OBJECT alive and the side table's copy correct — and does - // nothing for `%obj`, a separate copy the collector cannot see. - // Reachability is not the invariant; the invariant is that the - // register you are still going to use was rewritten. - // The old `any_may_trigger_gc(named_statics)` disjunct is gone - // rather than kept alongside: it is now strictly subsumed — it can - // only be true when `named_statics` is non-empty, which is the new - // clause. Leaving it would read as a second, narrower opinion - // about the same operand and invite someone to "restore" the - // narrow one. - let protect_handle = !named_statics.is_empty() - || !captured_args.is_empty() - || !computed_keys.is_empty() - || !computed_statics.is_empty() - || !block_fns.is_empty(); + let protect_handle = true; with_rooted_group(ctx, 1, |ctx, group| { let rooted = group.adopt_emitted(ctx, Repr::Ptr, &obj, protect_handle); + // #6438: pin THIS evaluation's parent onto the object. The lowering + // sequences `RegisterClassParentDynamic` immediately ahead of this + // node, so `CLASS_DYNAMIC_PARENT_VALUE[template]` still holds this + // evaluation's parent; later evaluations overwrite it, but each + // object keeps its own edge. Without this, a factory invoked more + // than once (effect's `class DeclareClass extends make(ast) { … }`) + // has every instance walk to the LAST parent — reading that + // evaluation's `static ast` instead of its own. The same call gives + // the object its own `length`, `name` and static methods. No parent + // pinned when the class expression has no heritage. + { + let obj = group.reread_emitted(ctx, rooted); + // A static FIELD named `length` / `name` takes over the + // intrinsic property; it is created as an ordinary one. + let field_mask = named_statics.iter().fold(0u32, |mask, (name, _)| match name + .as_str() + { + "length" => mask | 1, + "name" => mask | 2, + _ => mask, + }); + ctx.block().call_void( + "js_class_object_pin_parent", + &[ + (I64, &obj), + (I32, &tcid_str), + (I32, &field_mask.to_string()), + ], + ); + } // A named class expression's lexical self-binding is // initialized immediately after the class value is created, // before computed names and static initializers run. The diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 84c3c9d10e..86b417ac4a 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -117,7 +117,7 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { // new/instanceof read class_id from it. module.declare_function("js_object_mark_class", VOID, &[I64]); // #6438: pin a per-evaluation class object's own parent edge. - module.declare_function("js_class_object_pin_parent", VOID, &[I64, I32]); + module.declare_function("js_class_object_pin_parent", VOID, &[I64, I32, I32]); // Shape-cache-aware variant: pre-populates keys_array via SHAPE_INLINE_CACHE, // so subsequent field stores can use index-based set_field (skipping the // per-call linear key-search done by js_object_set_field_by_name). diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index fedebdeaa9..cb08ec91de 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -602,7 +602,7 @@ js_class_lexical_binding_set f64 f64,f64 js_class_method_bind f64 f64,ptr,usize js_class_method_bind_by_id f64 f64,i64 js_class_method_snapshot_bind f64 f64,ptr,usize -js_class_object_pin_parent void i64,i32u +js_class_object_pin_parent void i64,i32u,i32u js_class_object_refresh_capture_values void f64,i64,f64 js_class_prototype_method_value f64 f64,f64 js_class_register_capture_values void i32u,ptr,usize diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 103c9ef34a..6b4592f217 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -222,6 +222,21 @@ pub unsafe fn dispatch_bound_method( return result; } + // A static method value of a per-evaluation class object is stored in the + // object's own property of that name, so resolving the name on the object + // would find this very closure again. Run the declaration instead, in the + // evaluation the value was read from. + if method_name_len > 0 && !method_name_ptr.is_null() { + if let Some(result) = crate::object::class_object_static_method_call( + namespace_obj, + method_name_ptr as *const u8, + method_name_len, + args, + ) { + return result; + } + } + crate::object::js_native_call_method( namespace_obj, method_name_ptr, diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 0529c54037..c3126a99b0 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -99,7 +99,7 @@ pub(crate) use state::{ 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, + template_has_class_objects, throw_non_constructable_builtin_function, CLASS_OBJECT_EVER, }; pub use state::{ AccessorDecl, ClassVTable, VTableMethodEntry, CLASS_DECL_PROTOTYPE_OBJECTS, @@ -230,11 +230,12 @@ pub(crate) use parent_static::{ 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_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, instance_chain_parent_class_id, lookup_class_symbol_method_in_chain, + class_own_static_method_name_bytes, 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, instance_chain_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, }; diff --git a/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs b/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs index 8fe6dc2edf..0eb2812e1e 100644 --- a/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs +++ b/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs @@ -109,7 +109,7 @@ fn an_active_replay_resolves_this_evaluations_pinned_heritage() { // as codegen does right after `RegisterClassParentDynamic`. js_register_class_parent_dynamic(TEMPLATE, class_ref(FIRST_PARENT)); class_handle.with_mut_ptr::(|class| { - super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE) + super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE, 0) }); // A LATER evaluation of the same template overwrites the shared stash. @@ -196,7 +196,7 @@ fn sibling_class_objects_of_the_same_template_keep_distinct_pins() { }); js_register_class_parent_dynamic(TEMPLATE, class_ref(FIRST_PARENT)); first_handle.with_mut_ptr::(|class| { - super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE) + super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE, 0) }); let first_pin = first_handle.with_mut_ptr::(|class| { super::super::parent_static::class_object_pinned_parent(class as *const crate::ObjectHeader) @@ -216,7 +216,7 @@ fn sibling_class_objects_of_the_same_template_keep_distinct_pins() { }); js_register_class_parent_dynamic(TEMPLATE, class_ref(LAST_PARENT)); last_handle.with_mut_ptr::(|class| { - super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE) + super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE, 0) }); // The EARLIER evaluation's own pin must be UNCHANGED by the later one. 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 49964f336e..44676869c1 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -376,7 +376,11 @@ pub(crate) const CLASS_OBJECT_PARENT_KEY: &str = "__perry_parent_class"; /// A no-parent class expression pins nothing (the getter yields undefined or a /// static ClassRef fallback, which the field walk treats as "no own edge"). #[no_mangle] -pub extern "C" fn js_class_object_pin_parent(obj: i64, template_class_id: u32) { +pub extern "C" fn js_class_object_pin_parent( + obj: i64, + template_class_id: u32, + static_field_mask: u32, +) { const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; if obj == 0 || template_class_id == 0 { return; @@ -387,6 +391,14 @@ pub extern "C" fn js_class_object_pin_parent(obj: i64, template_class_id: u32) { // would answer with an enclosing constructor replay's parent when a factory // is re-entered from inside a constructor body. let parent = template_dynamic_parent_value(template_class_id); + // Every class object owns its `length`, `name` and static methods from + // creation, with or without heritage. + unsafe { + crate::object::field_get_set::define_class_object_own_properties( + obj as *mut crate::object::ObjectHeader, + static_field_mask, + ); + } if parent.to_bits() == TAG_UNDEFINED { return; } @@ -967,6 +979,25 @@ pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { .unwrap_or(false) } +/// The name of ClassBody static method `name` declared by class `class_id` +/// itself, as the bytes of its declaration's record: `None` when the class +/// declares no such method. +/// +/// The bytes are the record's own key, so they live as long as the class's +/// image — the agent that holds every value of the class. A value that names +/// the method for its whole life (a class object's bound static method) points +/// at them instead of keeping a copy. That holds only while no writer of +/// `CLASS_STATIC_METHODS` removes or re-keys a record: each must insert a new +/// record or update a value in place. +pub(crate) fn class_own_static_method_name_bytes( + class_id: u32, + name: &str, +) -> Option<(*const u8, usize)> { + let guard = CLASS_STATIC_METHODS.read().ok()?; + let (key, _) = guard.as_ref()?.get(&class_id)?.get_key_value(name)?; + Some((key.as_ptr(), key.len())) +} + /// ClassBody static method `name` declared by class `class_id` itself: /// `(func_ptr, param_count, has_rest)`. pub(crate) fn class_own_static_method_entry( diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index cadaac35f0..ef12ad3319 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -684,6 +684,17 @@ pub(crate) fn class_object_value_root_store(class_id: u32, obj_ptr: *mut ObjectH crate::gc::runtime_write_barrier_root_raw_ptr(obj_ptr); } +/// Has a class object of template `class_id` been created? The answer is the +/// template's own entry in `CLASS_OBJECT_VALUES`, stored with its first class +/// object, not a copy of it: the answer stays `true` only while no writer +/// removes an entry. A program with no per-evaluation class pays one relaxed +/// load. +#[inline] +pub(crate) fn template_has_class_objects(class_id: u32) -> bool { + CLASS_OBJECT_EVER.load(std::sync::atomic::Ordering::Relaxed) + && class_object_value_for_cid(class_id).is_some() +} + /// Read back the class object registered for `class_id`, or `None` when the /// class never materialized as a per-evaluation object (ordinary /// ClassRef-valued classes). diff --git a/crates/perry-runtime/src/object/class_value.rs b/crates/perry-runtime/src/object/class_value.rs index a3f3e266e7..299d0babc9 100644 --- a/crates/perry-runtime/src/object/class_value.rs +++ b/crates/perry-runtime/src/object/class_value.rs @@ -302,11 +302,11 @@ fn class_value_mint(class_id: u32) -> *mut ClosureHeader { const INTRINSIC_OWN_DATA_KEYS: [&str; 2] = ["length", "name"]; /// The attributes of a function's own `length` / `name`. -const INTRINSIC_ATTRS: (bool, bool, bool) = (false, false, true); +pub(crate) const INTRINSIC_ATTRS: (bool, bool, bool) = (false, false, true); /// The value of intrinsic own data property `key` of class `class_id`, if /// the class registered one. -fn intrinsic_own_data_value(class_id: u32, key: &str) -> Option { +pub(crate) fn intrinsic_own_data_value(class_id: u32, key: &str) -> Option { match key { "length" => super::class_registry::class_length_for_id(class_id).map(f64::from), "name" => super::class_registry::class_name_for_id(class_id).map(|name| { @@ -319,7 +319,7 @@ fn intrinsic_own_data_value(class_id: u32, key: &str) -> Option { /// Does a static method or accessor of class `class_id` own `key`? Then it, /// not the intrinsic data property, is the class's own `key`. -fn static_member_owns(class_id: u32, key: &str) -> bool { +pub(crate) fn static_member_owns(class_id: u32, key: &str) -> bool { super::class_registry::class_has_own_static_method(class_id, key) || super::class_registry::class_registered_static_accessor_ptrs(class_id, key).is_some() } @@ -471,6 +471,15 @@ pub(crate) fn static_method_property( if name.starts_with('#') || is_internal_static_key(name) { return live_or_next; } + // A template evaluated to class objects keeps its statics on those objects + // (`define_class_object_own_properties`), each evaluation its own. The + // shared function object minted for the template is only the last-wins + // mirror of writes to any of them (`mirror_class_object_static_write`): it + // says nothing about what the declaration is, and one evaluation's + // `C.m = f` must not retire `m` for its siblings. + if super::class_registry::template_has_class_objects(class_id) { + 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; diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 1d06a3d11f..69b316ba16 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -214,6 +214,16 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu let obj_value = f64::from_bits(metadata_obj_value.get_heap_word_u64()); let class_obj = extract_obj_ptr(obj_value); if !class_obj.is_null() { + // `prototype`: `{ !w, !e, !c }`, owned for the object's + // whole life (`class_object_has_prototype_property`). + if super::field_get_set::class_object_has_prototype_property( + method_name.as_bytes(), + ) { + let proto = f64::from_bits( + super::field_get_set::class_object_prototype_value(class_obj).bits(), + ); + return build_data_descriptor(proto, false, false, false); + } let class_id = super::js_object_get_class_id(class_obj); if let Some((acc, attrs)) = super::class_registry::class_dynamic_static_accessor_descriptor( @@ -973,6 +983,13 @@ pub(crate) use builders::{ /// Takes a NaN-boxed f64 object pointer, returns a NaN-boxed f64 array pointer. #[no_mangle] pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { + // A class object stores everything it owns except `prototype` + // (`class_object_has_prototype_property`), which was created with it, + // right after `length` and `name`. + if super::class_registry::is_class_object_value(obj_value) { + let names = js_object_get_own_property_names_shape(obj_value); + return class_object_names_with_prototype(names); + } // An elements-backed Array-subclass instance: present indices, then // `length`, then the shape's own string keys. if crate::array::subclass_elements::backed_value(obj_value).is_some() { @@ -992,6 +1009,45 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { js_object_get_own_property_names_shape(obj_value) } +/// `names` (a class object's stored own keys, in creation order) with +/// `prototype` inserted where ClassDefinitionEvaluation created it: after the +/// leading `length` / `name` that are still the object's first keys. +fn class_object_names_with_prototype(names: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let names = scope.root_nanbox_f64(names); + let mut out: Vec = Vec::new(); + unsafe { + let arr = crate::value::js_nanbox_get_pointer(names.get_nanbox_f64()) + as *const crate::array::ArrayHeader; + for i in 0..crate::array::js_array_length(arr) { + let v = crate::array::js_array_get_f64(arr, i); + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if let Some((p, len)) = crate::string::str_bytes_from_jsvalue(v, &mut scratch) { + out.push( + String::from_utf8_lossy(std::slice::from_raw_parts(p, len as usize)) + .into_owned(), + ); + } + } + } + let at = out + .iter() + .position(|n| n != "length" && n != "name") + .unwrap_or(out.len()); + out.insert(at, "prototype".to_string()); + let result = crate::array::js_array_alloc(out.len() as u32); + let result = scope.root_raw_mut_ptr(result); + for name in out { + let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + result.with_mut_ptr::(|r| { + crate::array::js_array_push(r, JSValue::string_ptr(s)); + }); + } + result.with_mut_ptr::(|r| { + f64::from_bits((r as u64) | 0x7FFD_0000_0000_0000) + }) +} + /// [`js_object_get_own_property_names`] over the shape alone. /// A receiver whose heap cell IS a `GC_TYPE_ARRAY`. `Array.isArray` is also /// true for a `class X extends Array` instance, but that is an `ObjectHeader` diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index b8e2134e85..aeeb35e87c 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -270,8 +270,10 @@ pub(crate) use accessors::{ primitive_tagged_prototype_property, string_index_value, }; pub(crate) use class_object_props::{ - class_evaluation_prototype_class_id, class_object_materialized_prototype, - class_object_prototype_value, + class_evaluation_prototype_class_id, class_object_has_prototype_property, + class_object_materialized_prototype, class_object_prototype_value, + class_object_registry_serves_static, class_object_source_text, class_object_static_method_call, + define_class_object_own_properties, }; pub(crate) use crypto_key::{ crypto_key_property_value, CLASS_ID_BOXED_BIGINT, CLASS_ID_BOXED_BOOLEAN, 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 37e81c58e5..2b2eb8c5e7 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 @@ -204,6 +204,195 @@ unsafe fn class_evaluation_prototype_value(obj: *const ObjectHeader) -> f64 { proto.with_mut_ptr::(|proto| crate::value::js_nanbox_pointer(proto as i64)) } +/// What a fresh class object owns from the moment it is created, the way a +/// class's function object owns them (`class_value_mint`): `length` and `name` +/// (ClassDefinitionEvaluation's SetFunctionLength / SetFunctionName, `{ !w, !e, +/// c }`), then every ClassBody static method as a data property `{ w, !e, c }` +/// whose value is that declaration bound to this object. They are real +/// properties of THIS object, so `getOwnPropertyNames` lists them, `delete` +/// removes them, and a deleted one stays gone: the object's own keys are the +/// authority, not the template's registry (which every evaluation of the class +/// shares). +/// +/// `prototype` is not stored here: it is `{ !w, !e, !c }`, so it is present on +/// every class object for its whole life ([`class_object_has_prototype_property`]) +/// and has no state to keep. It is built at its first read, which must follow +/// the class definition (computed members register while it evaluates). +/// +/// A static field of the same name (`static_field_mask`: bit 0 `length`, bit 1 +/// `name`) is stored over `length` / `name` right after, so its slot is created +/// in this position with the field's ordinary attributes. +pub(crate) unsafe fn define_class_object_own_properties( + obj: *mut ObjectHeader, + static_field_mask: u32, +) { + use super::super::class_value::{ + intrinsic_own_data_value, static_member_owns, INTRINSIC_ATTRS, + }; + let class_id = (*obj).class_id; + if class_id == 0 { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let class = scope.root_raw_mut_ptr(obj); + let define = |name: &str, value: f64, attrs: (bool, bool, bool)| { + let value = scope.root_nanbox_f64(value); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key = scope.root_string_ptr(key); + class.with_mut_ptr::(|class| { + key.with_const_ptr::(|key| { + define_builtin_data_property( + class, + key, + value.get_nanbox_f64(), + name.to_string(), + PropertyAttrs::new(attrs.0, attrs.1, attrs.2), + ) + }) + }); + }; + for (bit, key) in [(1, "length"), (2, "name")] { + // A static method of this name replaces the value (and the attributes) + // in the same position, below; a static accessor leaves nothing here. + let method_replaces = + super::super::class_registry::class_has_own_static_method(class_id, key); + if static_member_owns(class_id, key) && !method_replaces { + continue; + } + if static_field_mask & bit != 0 { + // The class body's static field of this name stores over it next: + // it keeps this position, and takes the field's ordinary attributes. + define( + key, + f64::from_bits(crate::value::TAG_UNDEFINED), + (true, true, true), + ); + } else if let Some(value) = intrinsic_own_data_value(class_id, key) { + define(key, value, INTRINSIC_ATTRS); + } + } + for name in super::super::class_registry::class_own_string_member_names(class_id, true) { + // The method value is bound to THIS class object, as a read of the + // static through the evaluation has always produced it: its call runs + // in this evaluation (its environment and self-binding), whatever + // `this` is. The bound closure keeps the name's address for its whole + // life, so the name is the declaration's own: the bytes of the + // template's record for this method (none for an accessor). + let Some((name_ptr, name_len)) = + super::super::class_registry::class_own_static_method_name_bytes(class_id, &name) + else { + continue; + }; + let class_value = class + .with_mut_ptr::(|class| crate::value::js_nanbox_pointer(class as i64)); + let f = super::super::native_module::build_bound_method_closure( + class_value, + name_ptr, + name_len, + ); + define(&name, f, (true, false, true)); + } +} + +/// Is `value` the method value `define_class_object_own_properties` stored for +/// static `name` on class object `holder`: a bound method closure naming this +/// method, bound to this object? Anything else in that slot is the program's. +unsafe fn is_declared_static_method_value( + value: f64, + holder: *const ObjectHeader, + name: &str, +) -> bool { + let value = JSValue::from_bits(value.to_bits()); + if !value.is_pointer() { + return false; + } + let closure = value.as_pointer::(); + if !crate::closure::is_closure_ptr(closure as usize) + || !std::ptr::eq((*closure).info, &crate::closure::BOUND_METHOD_INFO) + { + return false; + } + let receiver = crate::closure::js_closure_get_capture_f64(closure, 0); + let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 1) as *const u8; + let name_len = crate::closure::js_closure_get_capture_ptr(closure, 2) as usize; + receiver.to_bits() == crate::value::js_nanbox_pointer(holder as i64).to_bits() + && !name_ptr.is_null() + && std::slice::from_raw_parts(name_ptr, name_len) == name.as_bytes() +} + +/// May a call `C.name(..)` on class object `object` run the registered static +/// method (with `this` = `object`)? The evaluation that declares `name` keeps +/// it as an own property of its class object: when that object still holds the +/// declaration the registry runs it; when it no longer does (deleted, or the +/// program stored something else) the property decides, not the registry. A +/// declaration of a shared class (no class object) is the registry's. +pub(crate) unsafe fn class_object_registry_serves_static( + object: *const ObjectHeader, + name: &str, +) -> bool { + let Some((owner, _)) = + super::super::class_registry::lookup_static_method_owner((*object).class_id, name) + else { + return false; + }; + if !super::super::class_registry::template_has_class_objects(owner) { + return true; + } + let mut holder = object; + for _ in 0..32 { + if (*holder).class_id == owner { + return super::super::class_registry::class_object_own_field_bytes( + holder, + name.as_bytes(), + ) + .is_some_and(|v| is_declared_static_method_value(v, holder, name)); + } + let Some(parent) = super::super::class_registry::class_object_pinned_parent(holder) else { + break; + }; + let parent = JSValue::from_bits(parent.to_bits()); + if !parent.is_pointer() + || !super::super::class_registry::is_class_object_ptr(parent.as_pointer::()) + { + break; + } + holder = parent.as_pointer::(); + } + // No evaluation of the declaring template is in this object's chain. + true +} + +/// Run static method `name` for a bound method value whose receiver is class +/// object `receiver` (see [`define_class_object_own_properties`]). `None` when +/// `receiver` is not a class object or the registry has no such declaration. +pub(crate) unsafe fn class_object_static_method_call( + receiver: f64, + name_ptr: *const u8, + name_len: usize, + args: &[f64], +) -> Option { + if !super::super::class_registry::is_class_object_value(receiver) { + return None; + } + let obj = JSValue::from_bits(receiver.to_bits()).as_pointer::(); + let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)).ok()?; + super::super::class_registry::lookup_static_method_in_chain((*obj).class_id, name)?; + Some(super::super::class_registry::js_class_static_method_call( + receiver, + name_ptr, + name_len, + args.as_ptr(), + args.len(), + )) +} + +/// Does `obj` (a class object) own `key` without storing it? Only `prototype`: +/// `{ !w, !e, !c }`, created with the class, so it is there until the object +/// is gone and no `delete` or `defineProperty` can change that. +pub(crate) fn class_object_has_prototype_property(key: &[u8]) -> bool { + key == b"prototype" +} + /// #4949: heap class-expression values (`ClassExprFresh`) are real /// OBJECT_TYPE_CLASS objects, not INT32 class refs. Their `.prototype` /// read must still expose the live declared-class prototype object so @@ -235,25 +424,16 @@ pub(crate) fn class_object_materialized_prototype( (!proto.is_null()).then_some(proto) } -/// Resolve `.name` for an `OBJECT_TYPE_CLASS` heap object. An explicit -/// `static name` member (an own field on the class object) wins; a deleted -/// key still reads `undefined` (returns `None`). +/// Resolve `.name` for an `OBJECT_TYPE_CLASS` heap object: the object's OWN +/// `name` (`define_class_object_own_properties` creates it with the object, a +/// `static name` member or `defineProperty` replaces it), else nothing. A +/// `delete C.name` removes the property for this object only, and it stays gone +/// (`None`): the template's registered name is not a second source for it. pub(super) unsafe fn class_object_name_value( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> Option { - if let Some(v) = own_data_field_by_name(obj, key) { - return Some(v); - } - let class_id = (*obj).class_id; - 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)?; - let s = crate::string::js_string_from_bytes(cname.as_ptr(), cname.len() as u32); - Some(JSValue::from_bits( - crate::js_nanbox_string(s as i64).to_bits(), - )) + own_data_field_by_name(obj, key) } /// #6530 (size-gate split from `get_field_by_name_tail.rs` — pure 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 0de2916b13..f4a3c5a561 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 @@ -1128,8 +1128,21 @@ fn get_field_by_name_past_data_probe( if !name.is_empty() && !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() + // A class object owns every static method its template + // declares (`define_class_object_own_properties`), and so + // does each earlier evaluation it inherits from, so a + // declaration of a per-evaluation class found here was + // DELETED from its object: it must not come back. Only a + // method of a shared (never-evaluated-to-an-object) class + // is served from the registry. + if super::super::class_registry::lookup_static_method_owner(class_id, name) + .is_some_and(|(owner, _)| { + let object_owned = + super::super::class_registry::class_object_value_for_cid(owner) + .is_some() + || owner == class_id; + !object_owned + }) { let heap_name = { let layout = 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 01c8f5d835..e413649d4b 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 @@ -305,9 +305,21 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { if h.obj_type == crate::gc::GC_TYPE_OBJECT && super::super::exotic_expando::exotic_expando_kind(addr).is_none() { - return unsafe { + let present = unsafe { object_string_key_has_property(addr as *const ObjectHeader, key, key_val) }; + // A class object owns `prototype` without storing it. Only a + // miss on that one 9-byte key looks at the receiver's kind. + if present.to_bits() == nanbox_false.to_bits() { + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) } + .is_some_and(super::class_object_has_prototype_property) + && super::super::class_registry::is_class_object_ptr(addr as *const u8) + { + return nanbox_true; + } + } + return present; } } } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 56c930c465..55c9184016 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -367,6 +367,7 @@ pub(crate) use descriptor_state::{ }; pub(crate) use field_get_set::FieldLookupCaches; pub(crate) use field_get_set::{ + class_object_registry_serves_static, class_object_static_method_call, private_evaluation_brand_value, private_lexical_brand_pop, private_lexical_brand_push, private_lexical_brand_stack_restore, private_lexical_brand_stack_savepoint, private_member_access_hints_restore, private_member_access_hints_savepoint, diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index 9448498744..b7a880a098 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -402,8 +402,12 @@ pub(super) unsafe fn dispatch_primitive( if crate::object::class_registry::is_class_object_value(object) { let class_id = crate::object::js_object_get_class_id(jsval.as_pointer::()); if class_id != 0 - && crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) - .is_some() + && unsafe { + crate::object::class_object_registry_serves_static( + jsval.as_pointer::(), + method_name, + ) + } { let args = refreshed_args(); return Some(crate::object::class_registry::js_class_static_method_call( 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 d029721766..48751b4b86 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -246,6 +246,15 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); } + // A class object owns `prototype` without storing it. + if super::super::class_registry::is_class_object_value(obj_value) + && super::super::has_own_helpers::str_from_string_header(key_str).is_some_and(|key| { + super::super::field_get_set::class_object_has_prototype_property(key.as_bytes()) + }) + { + return f64::from_bits(TAG_TRUE); + } + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { let present = crate::typedarray_props::typed_array_has_own_property( addr as *const crate::typedarray::TypedArrayHeader, From 6171471efacde33509381d0650932d1cdc11b7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 17:23:33 +0000 Subject: [PATCH 3/8] runtime: String(C) and C.toString() on a fresh class are the class source A class object made per evaluation is a plain object with no Function.prototype of its own to inherit toString from, so String(C), a template or concatenation with it, C.toString() and Function.prototype.toString.call(C) gave [object Function] or threw. Each now answers with the class source the template registers, unless the program put a static toString (or Symbol.toPrimitive) in the way: those run as before. Part of #11759. --- .../perry-runtime/src/object/field_get_set.rs | 8 ++--- .../field_get_set/class_object_props.rs | 33 +++++++++++++++++++ .../src/object/global_this/array_error.rs | 5 +++ crates/perry-runtime/src/object/mod.rs | 10 +++--- .../native_call_method/common_methods.rs | 6 ++++ crates/perry-runtime/src/value/to_string.rs | 7 ++++ .../src/value/to_string_primitive.rs | 9 +++++ 7 files changed, 69 insertions(+), 9 deletions(-) diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index aeeb35e87c..47376017ba 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -270,10 +270,10 @@ pub(crate) use accessors::{ primitive_tagged_prototype_property, string_index_value, }; pub(crate) use class_object_props::{ - class_evaluation_prototype_class_id, class_object_has_prototype_property, - class_object_materialized_prototype, class_object_prototype_value, - class_object_registry_serves_static, class_object_source_text, class_object_static_method_call, - define_class_object_own_properties, + class_evaluation_prototype_class_id, class_object_default_to_string, + class_object_has_prototype_property, class_object_materialized_prototype, + class_object_prototype_value, class_object_registry_serves_static, class_object_source_text, + class_object_static_method_call, define_class_object_own_properties, }; pub(crate) use crypto_key::{ crypto_key_property_value, CLASS_ID_BOXED_BIGINT, CLASS_ID_BOXED_BOOLEAN, 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 2b2eb8c5e7..ee32d44aca 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 @@ -393,6 +393,39 @@ pub(crate) fn class_object_has_prototype_property(key: &[u8]) -> bool { key == b"prototype" } +/// `Function.prototype.toString` of a class object: the class's source text, +/// as for the class's function object (`class_ref_to_string`). Every +/// evaluation of a class shares its source, so the template id names it. +/// `None` when `value` is not a class object. +pub(crate) fn class_object_source_text(value: f64) -> Option { + if !super::super::class_registry::is_class_object_value(value) { + return None; + } + let obj = JSValue::from_bits(value.to_bits()).as_pointer::(); + // SAFETY: `is_class_object_value` proved a live class object. + let class_id = unsafe { (*obj).class_id }; + Some(super::super::class_registry::class_ref_to_string(class_id).into_owned()) +} + +/// The text `String(C)` / `` `${C}` `` produce for the class object `value` +/// when no `toString` of the program's is in the way, else `None`. A static +/// `toString` of this evaluation or of a class it inherits from answers +/// instead (the caller's ordinary conversion finds and runs it). +pub(crate) fn class_object_default_to_string(value: f64) -> Option { + let text = class_object_source_text(value)?; + let obj = JSValue::from_bits(value.to_bits()).as_pointer::(); + // SAFETY: a live class object (above). + let class_id = unsafe { (*obj).class_id }; + let own = super::super::class_registry::class_object_own_field_bytes(obj, b"toString") + .is_some_and(|v| !JSValue::from_bits(v.to_bits()).is_undefined()); + if own + || super::super::class_registry::lookup_static_method_owner(class_id, "toString").is_some() + { + return None; + } + Some(text) +} + /// #4949: heap class-expression values (`ClassExprFresh`) are real /// OBJECT_TYPE_CLASS objects, not INT32 class refs. Their `.prototype` /// read must still expose the live declared-class prototype object so diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index d91927fd30..7f43ca00e0 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -653,6 +653,11 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); } } + // A per-evaluation class object (`ClassExprFresh`) is a function too. + if let Some(source) = super::super::field_get_set::class_object_source_text(this_val) { + let str_ptr = crate::string::js_string_from_bytes(source.as_ptr(), source.len() as u32); + return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); + } super::super::object_ops::throw_object_type_error( b"Function.prototype.toString requires that 'this' be a Function", ); diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 55c9184016..f6582c279d 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -367,11 +367,11 @@ pub(crate) use descriptor_state::{ }; pub(crate) use field_get_set::FieldLookupCaches; pub(crate) use field_get_set::{ - class_object_registry_serves_static, class_object_static_method_call, - private_evaluation_brand_value, private_lexical_brand_pop, private_lexical_brand_push, - private_lexical_brand_stack_restore, private_lexical_brand_stack_savepoint, - private_member_access_hints_restore, private_member_access_hints_savepoint, - scan_private_lexical_brand_roots_mut, + class_object_default_to_string, class_object_registry_serves_static, + class_object_static_method_call, private_evaluation_brand_value, private_lexical_brand_pop, + private_lexical_brand_push, private_lexical_brand_stack_restore, + private_lexical_brand_stack_savepoint, private_member_access_hints_restore, + private_member_access_hints_savepoint, scan_private_lexical_brand_roots_mut, }; #[cfg(test)] pub(crate) use this_binding::js_derived_super_scope_push; 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 2a615b0e1a..da85a18afc 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 @@ -548,6 +548,12 @@ pub(super) unsafe fn dispatch_common( return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } } + // A per-evaluation class object (`ClassExprFresh`) is a function too. + if let Some(source) = super::field_get_set::class_object_default_to_string(object) { + let str_ptr = + crate::string::js_string_from_bytes(source.as_ptr(), source.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(object) { let payload_jsv = JSValue::from_bits(payload.to_bits()); match crate::builtins::boxed_primitive_to_string_tag(object) { diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 7596e0c5be..f2bf4c0fc4 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -216,6 +216,13 @@ pub(crate) fn js_jsvalue_to_string_impl( return js_jsvalue_to_string_impl(primitive, reject_symbol); } } + // A per-evaluation class object is a function: without a + // `toString` of the program's its text is the class source, as for + // a class ref (`ClassExprFresh`; the object has no + // `Function.prototype` of its own to inherit `toString` from). + if let Some(source) = crate::object::class_object_default_to_string(value) { + return crate::string::js_string_from_bytes(source.as_ptr(), source.len() as u32); + } // BufferHeader-backed values need handling before GC-header probes. // ArrayBuffer, SharedArrayBuffer, and DataView inherit object tags. if crate::buffer::is_registered_buffer(ptr as usize) { diff --git a/crates/perry-runtime/src/value/to_string_primitive.rs b/crates/perry-runtime/src/value/to_string_primitive.rs index 88d96b5702..2f76ce39a2 100644 --- a/crates/perry-runtime/src/value/to_string_primitive.rs +++ b/crates/perry-runtime/src/value/to_string_primitive.rs @@ -557,6 +557,15 @@ pub(crate) unsafe fn ordinary_to_primitive_number_for_add( } } + // A per-evaluation class object (`ClassExprFresh`) is a function: its + // `toString` is `Function.prototype.toString` unless the program put one + // in the way. + if let Some(source) = + crate::object::class_object_default_to_string(value_handle.get_nanbox_f64()) + { + let s = crate::string::js_string_from_bytes(source.as_ptr(), source.len() as u32); + return OrdinaryToPrimitiveOutcome::Primitive(crate::value::js_nanbox_string(s as i64)); + } match call_method_for_primitive(&scope, &value_handle, b"toString") { MethodOutcome::Primitive(p) => OrdinaryToPrimitiveOutcome::Primitive(p), MethodOutcome::NonPrimitive => OrdinaryToPrimitiveOutcome::TypeError, From 922ccc247a2e3fde80f927442ff925fc87a1fc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 17:23:33 +0000 Subject: [PATCH 4/8] tests: fresh class objects extend, own their members and stringify like node One fixture (node 26.5.1's output) and an integration test per group of lines: a static extends of a fresh class, the own properties of a fresh class object and delete on them, and the class source. A fourth run forces every minor collection to evacuate. Part of #11759. --- ...LACEHOLDER-fresh-class-object-semantics.md | 10 ++ .../tests/fresh_class_object_semantics.rs | 95 ++++++++++++++ .../fresh_class_object_semantics/expected.txt | 27 ++++ .../fresh_class_object_semantics/main.ts | 122 ++++++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 changelog.d/PLACEHOLDER-fresh-class-object-semantics.md create mode 100644 crates/perry/tests/fresh_class_object_semantics.rs create mode 100644 tests/fixtures/fresh_class_object_semantics/expected.txt create mode 100644 tests/fixtures/fresh_class_object_semantics/main.ts diff --git a/changelog.d/PLACEHOLDER-fresh-class-object-semantics.md b/changelog.d/PLACEHOLDER-fresh-class-object-semantics.md new file mode 100644 index 0000000000..6fa728d342 --- /dev/null +++ b/changelog.d/PLACEHOLDER-fresh-class-object-semantics.md @@ -0,0 +1,10 @@ +Fixed three gaps in classes that are created per evaluation (class expressions +in functions, and declarations whose evaluation has its own environment). The +number in this file name is a placeholder until the PR number is known. +`class D extends L` now extends the L of that evaluation instead of the shared +class when the name of L is scope-renamed. A fresh class object now owns +`length`, `name` and its static methods as real own properties, so +`Object.getOwnPropertyNames`, `Object.hasOwn`, `in` and +`Object.getOwnPropertyDescriptor` see them, and `delete C.s` removes the method +from that evaluation's class only. `String(C)`, `` `${C}` ``, `"" + C` and +`C.toString()` now return the class source text. diff --git a/crates/perry/tests/fresh_class_object_semantics.rs b/crates/perry/tests/fresh_class_object_semantics.rs new file mode 100644 index 0000000000..e6bcfb14eb --- /dev/null +++ b/crates/perry/tests/fresh_class_object_semantics.rs @@ -0,0 +1,95 @@ +//! A class created per evaluation (a "fresh" class: a class expression in a +//! function, or a declaration whose evaluation has its own environment) is an +//! ordinary class object: +//! +//! * `class D extends L` extends the L of that evaluation, not the one every +//! evaluation shares (`ext-*` lines); +//! * the class object owns `length`, `name`, `prototype` and its static methods +//! as real own properties, so reflection lists them and `delete` removes them +//! from that evaluation's class alone (`own-*` lines); +//! * `String(C)` and `C.toString()` are the class source (`str-*` lines). +//! +//! The program and node's output are the `fresh_class_object_semantics` +//! fixture. The second run forces every minor collection to evacuate: the +//! class object is created, given its own properties and filled with statics +//! while the collector may move it. + +use std::path::PathBuf; +use std::process::Command; + +const SOURCE: &str = include_str!("../../../tests/fixtures/fresh_class_object_semantics/main.ts"); +const EXPECTED: &str = + include_str!("../../../tests/fixtures/fresh_class_object_semantics/expected.txt"); + +fn run(extra_env: &[(&str, &str)]) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, SOURCE).expect("write entry"); + let compile = Command::new(PathBuf::from(env!("CARGO_BIN_EXE_perry"))) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .env("PERRY_NO_CACHE", "1") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstderr:\n{}", + String::from_utf8_lossy(&compile.stderr) + ); + let mut command = Command::new(&output); + command.current_dir(dir.path()); + for (key, value) in extra_env { + command.env(key, value); + } + let run = command.output().expect("run compiled binary"); + assert!( + run.status.success(), + "binary failed ({:?})\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stderr) + ); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +fn check(stdout: &str, group: &str) { + let wrong: Vec = EXPECTED + .lines() + .zip(stdout.lines()) + .filter(|(want, got)| want.starts_with(group) && want != got) + .map(|(want, got)| format!("got `{got}`, node `{want}`")) + .collect(); + assert!(wrong.is_empty(), "{group}: {wrong:#?}\n{stdout}"); + assert_eq!( + EXPECTED.lines().count(), + stdout.lines().count(), + "line count differs\n{stdout}" + ); +} + +#[test] +fn a_static_extends_reaches_the_evaluated_class() { + check(&run(&[]), "ext-"); +} + +#[test] +fn a_class_object_owns_its_length_name_prototype_and_statics() { + check(&run(&[]), "own-"); +} + +#[test] +fn a_fresh_class_stringifies_to_its_source() { + check(&run(&[]), "str-"); +} + +#[test] +fn the_whole_program_matches_node_while_every_minor_evacuates() { + let stdout = run(&[ + ("PERRY_GC_FORCE_EVACUATE", "1"), + ("PERRY_GC_POISON_FROMSPACE", "1"), + ]); + assert_eq!(EXPECTED, stdout); +} diff --git a/tests/fixtures/fresh_class_object_semantics/expected.txt b/tests/fixtures/fresh_class_object_semantics/expected.txt new file mode 100644 index 0000000000..926b773b87 --- /dev/null +++ b/tests/fixtures/fresh_class_object_semantics/expected.txt @@ -0,0 +1,27 @@ +ext-proto true true false +ext-protoproto true true false +ext-instanceof true false true false false +ext-instance 1 2 D>L1 D>L2 +ext-statics make1 make2 make1 make2 +ext-top top +own-names ["length","name","prototype","s","t","f"] +own-keys ["f"] +own-has true,true,true,true,true,true,false,false +own-in true,true,true,true,true,false +own-values 0 Q object s1 f1 +own-desc length=data:number,false,false,true name=data:string,false,false,true prototype=data:object,false,false,false s=data:function,true,false,true f=data:string,true,true,true +own-identity true false false +own-delete true true +own-after ["length","name","prototype","t","f"] false undefined false +own-sibling ["length","name","prototype","s","t","f"] s2 +own-delete-name true false true ["length","prototype","t","f"] +own-redefine again ["length","prototype","t","f","s"] +own-sibling2 Q s2 t2 +own-inherit Pa Pb false ["length","name","prototype"] +own-inherit-deleted undefined Pb function +own-over nw function lenw ["length","name","prototype","g"] data:string,true,true,true data:function,true,false,true +str-string true true +str-method true true +str-template true true +str-proto true +str-override custom1 custom1 custom1 diff --git a/tests/fixtures/fresh_class_object_semantics/main.ts b/tests/fixtures/fresh_class_object_semantics/main.ts new file mode 100644 index 0000000000..9166bad740 --- /dev/null +++ b/tests/fixtures/fresh_class_object_semantics/main.ts @@ -0,0 +1,122 @@ +// Classes created per evaluation ("fresh" classes) behave like any class: +// a subclass extends the evaluated class, and the class object owns its +// `length`, `name`, `prototype` and static methods as real properties. +// expected.txt is node 26.5.1's output. Three groups of lines: +// ext-* a static `extends` of a fresh class reaches that evaluation +// own-* own properties of a fresh class object, and `delete` +// str-* the source text of a fresh class +// The factories hold a capturing class so the declarations are fresh today, +// and a same-named class elsewhere so the heritage name is scope-renamed. + +function d(desc: PropertyDescriptor | undefined): string { + if (!desc) return "none"; + return [ + "value" in desc ? "data:" + typeof desc.value : "accessor", + desc.writable, desc.enumerable, desc.configurable, + ].join(","); +} + +// ---- ext: `class D extends L`, L fresh ------------------------------------ +class L { whoTop() { return "top"; } } +function make(n: number) { + const k = n; + class L { + static make() { return "make" + k; } + id() { return k; } + hello() { return "L" + k; } + } + class D extends L { + hello() { return "D>" + super.hello(); } + } + class G extends D {} + return { L, D, G }; +} +const e1 = make(1); +const e2 = make(2); +console.log("ext-proto", Object.getPrototypeOf(e1.D) === e1.L, Object.getPrototypeOf(e2.D) === e2.L, + Object.getPrototypeOf(e1.D) === e2.L); +console.log("ext-protoproto", Object.getPrototypeOf(e1.D.prototype) === e1.L.prototype, + Object.getPrototypeOf(e2.D.prototype) === e2.L.prototype, + Object.getPrototypeOf(e1.D.prototype) === e2.L.prototype); +console.log("ext-instanceof", new e1.G() instanceof e1.L, new e1.G() instanceof e2.L, + new e2.G() instanceof e2.L, new e2.G() instanceof e1.L, new e1.D() instanceof e1.G); +console.log("ext-instance", new e1.G().id(), new e2.G().id(), new e1.G().hello(), new e2.G().hello()); +console.log("ext-statics", e1.D.make(), e2.D.make(), e1.G.make(), e2.G.make()); +console.log("ext-top", new L().whoTop()); + +// ---- own: reflection over a fresh class object ---------------------------- +function mk(tag: string) { + const t = tag; + return class Q { + static s() { return "s" + t; } + static t() { return "t" + t; } + static f = "f" + t; + m() { return t; } + }; +} +const Q1: any = mk("1"); +const Q2: any = mk("2"); +console.log("own-names", JSON.stringify(Object.getOwnPropertyNames(Q1))); +console.log("own-keys", JSON.stringify(Object.keys(Q1))); +console.log("own-has", ["length", "name", "prototype", "s", "t", "f", "m", "x"].map((k) => Object.hasOwn(Q1, k)).join(",")); +console.log("own-in", ["length", "name", "prototype", "s", "f", "x"].map((k) => k in Q1).join(",")); +console.log("own-values", Q1.length, Q1.name, typeof Q1.prototype, Q1.s(), Q1.f); +console.log("own-desc", ["length", "name", "prototype", "s", "f"].map((k) => k + "=" + d(Object.getOwnPropertyDescriptor(Q1, k))).join(" ")); +console.log("own-identity", Q1.s === Q1.s, Q1.s === Q2.s, Q1.prototype === Q2.prototype); +console.log("own-delete", delete Q1.s, delete Q1.nothing); +console.log("own-after", JSON.stringify(Object.getOwnPropertyNames(Q1)), Object.hasOwn(Q1, "s"), typeof Q1.s, "s" in Q1); +console.log("own-sibling", JSON.stringify(Object.getOwnPropertyNames(Q2)), Q2.s()); +console.log("own-delete-name", delete Q1.name, Object.hasOwn(Q1, "name"), Q1.name === undefined || Q1.name === "", + JSON.stringify(Object.getOwnPropertyNames(Q1))); +Q1.s = function () { return "again"; }; +console.log("own-redefine", Q1.s(), JSON.stringify(Object.getOwnPropertyNames(Q1))); +console.log("own-sibling2", Q2.name, Q2.s(), Q2.t()); + +// A subclass sees the parent evaluation's own statics, deleted ones included. +function sub(tag: string) { + const t = tag; + class P { static s() { return "P" + t; } static u() { return "u" + t; } } + class C extends P {} + return { P, C }; +} +const s1 = sub("a"); +const s2 = sub("b"); +console.log("own-inherit", s1.C.s(), s2.C.s(), Object.hasOwn(s1.C, "s"), JSON.stringify(Object.getOwnPropertyNames(s1.C))); +delete (s1.P as any).s; +console.log("own-inherit-deleted", typeof (s1.C as any).s, s2.C.s(), typeof (s1.P as any).u); + +// An own `name` / `length` that a static member takes over. +function over(tag: string) { + const t = tag; + return class W { + static name = "n" + t; + static length() { return "len" + t; } + static g() { return t; } + }; +} +const W1: any = over("w"); +console.log("own-over", W1.name, typeof W1.length, W1.length(), JSON.stringify(Object.getOwnPropertyNames(W1)), + d(Object.getOwnPropertyDescriptor(W1, "name")), d(Object.getOwnPropertyDescriptor(W1, "length"))); + +// ---- str: the class source ------------------------------------------------ +function src(tag: string) { + const t = tag; + return class Src { + static s() { return t; } + }; +} +const S1: any = src("x"); +const S2: any = src("y"); +const text = "class Src {\n static s() { return t; }\n }"; +console.log("str-string", String(S1) === text, String(S2) === text); +console.log("str-method", S1.toString() === text, S2.toString() === text); +console.log("str-template", `${S1}` === text, ("" + S1) === text); +console.log("str-proto", Function.prototype.toString.call(S1) === text); +function ov(tag: string) { + const t = tag; + return class Ov { + static toString() { return "custom" + t; } + }; +} +const O1: any = ov("1"); +console.log("str-override", String(O1), O1.toString(), `${O1}`); From 374e3d5b0f3013da3de552d44db5b221d1535987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 05:27:17 +0000 Subject: [PATCH 5/8] changelog: name the fresh class object fragment after #11780 --- ...ject-semantics.md => 11780-fresh-class-object-semantics.md} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename changelog.d/{PLACEHOLDER-fresh-class-object-semantics.md => 11780-fresh-class-object-semantics.md} (87%) diff --git a/changelog.d/PLACEHOLDER-fresh-class-object-semantics.md b/changelog.d/11780-fresh-class-object-semantics.md similarity index 87% rename from changelog.d/PLACEHOLDER-fresh-class-object-semantics.md rename to changelog.d/11780-fresh-class-object-semantics.md index 6fa728d342..144df4bc60 100644 --- a/changelog.d/PLACEHOLDER-fresh-class-object-semantics.md +++ b/changelog.d/11780-fresh-class-object-semantics.md @@ -1,6 +1,5 @@ Fixed three gaps in classes that are created per evaluation (class expressions -in functions, and declarations whose evaluation has its own environment). The -number in this file name is a placeholder until the PR number is known. +in functions, and declarations whose evaluation has its own environment). `class D extends L` now extends the L of that evaluation instead of the shared class when the name of L is scope-renamed. A fresh class object now owns `length`, `name` and its static methods as real own properties, so From fbbb601b31f51d735d676717327987c32affb885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 11:09:27 +0000 Subject: [PATCH 6/8] runtime: fresh class objects and prototypes are born in their template shapes Every evaluation of a per-evaluation class (ClassExprFresh) after the template's first in an agent allocates its class object directly in the shape the first evaluation reached (length, name, static methods, pinned parent) and fills the slots; its captured environment and its prototype object are one recorded transition each. The prototype is allocated in the template's final prototype shape (constructor and methods, linked to the evaluation's parent prototype). The shapes are real shape records kept as external carriers; the per-agent template memo holds only their ids and how each slot is filled. Static methods are function objects running the declaration's closure-convention entry, at home in their evaluation (js_static_method_entry_enter_home); prototype methods of a per-evaluation template run a new __eclo entry the same way. Both are named and sized by their entry, so creating one is one closure allocation. js_class_evaluation_object replaces js_object_alloc + js_object_mark_class + js_class_object_pin_parent at the evaluation site; js_class_object_set_ctor_caps replaces the by-name __perry_ctor_caps store. A delete on one evaluation's prototype no longer edits the template's member order or prototype method records, and a read of a declared method an evaluation's prototype no longer owns is not answered from the template vtable. --- crates/perry-codegen/src/codegen/artifacts.rs | 1 + .../src/codegen/fresh_class_templates.rs | 54 ++ crates/perry-codegen/src/codegen/mod.rs | 1 + .../perry-codegen/src/codegen/string_pool.rs | 119 ++- .../src/expr/static_field_meta.rs | 120 ++- .../src/gc_effects/linux-x86_64.tsv | 7 +- .../src/gc_effects/macos-aarch64.tsv | 7 +- .../src/gc_effects/windows-x86_64.tsv | 7 +- crates/perry-codegen/src/root_reload.rs | 1 - .../src/runtime_decls/objects.rs | 3 +- .../runtime_decls/stdlib_ffi/language_core.rs | 8 + .../perry-codegen/src/wasm32/runtime_abi.tsv | 7 +- .../src/closure/dispatch/bound.rs | 15 - .../perry-runtime/src/object/alloc_basic.rs | 25 + .../src/object/class_constructors.rs | 11 +- .../src/object/class_registry.rs | 42 +- .../src/object/class_registry/class_meta.rs | 1 + .../class_registry/evaluation_heritage.rs | 8 +- .../evaluation_heritage/tests.rs | 30 +- .../object/class_registry/parent_static.rs | 116 ++- .../src/object/class_registry/registration.rs | 30 + .../src/object/class_registry/state.rs | 13 + .../perry-runtime/src/object/delete_rest.rs | 15 +- .../perry-runtime/src/object/field_get_set.rs | 6 +- .../field_get_set/class_object_props.rs | 275 ++++--- .../field_get_set/class_object_template.rs | 713 ++++++++++++++++++ .../class_object_template_tests.rs | 153 ++++ .../field_get_set/get_field_by_name_tail.rs | 21 +- .../ic_miss/private_member_access.rs | 2 +- crates/perry-runtime/src/object/mod.rs | 8 +- .../native_module/class_method_values.rs | 34 + .../src/object/object_ops/define_property.rs | 1 + crates/perry-runtime/src/object/shapes.rs | 43 +- .../perry-runtime/src/object/this_binding.rs | 90 +++ scripts/registry_lifetime_allowlist.json | 18 +- 35 files changed, 1643 insertions(+), 362 deletions(-) create mode 100644 crates/perry-codegen/src/codegen/fresh_class_templates.rs create mode 100644 crates/perry-runtime/src/object/field_get_set/class_object_template.rs create mode 100644 crates/perry-runtime/src/object/field_get_set/class_object_template_tests.rs diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 0601abfd59..f3ab2087b0 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1039,6 +1039,7 @@ pub(super) fn emit_module_artifacts( &user_fn_wrapper_strict, &user_fn_display_names, &user_fn_source, + &super::fresh_class_templates::fresh_class_templates(hir), ); progress.checkpoint("string pool and registration initializer"); diff --git a/crates/perry-codegen/src/codegen/fresh_class_templates.rs b/crates/perry-codegen/src/codegen/fresh_class_templates.rs new file mode 100644 index 0000000000..37da5085a5 --- /dev/null +++ b/crates/perry-codegen/src/codegen/fresh_class_templates.rs @@ -0,0 +1,54 @@ +//! The class templates this module evaluates once per evaluation +//! (`Expr::ClassExprFresh`): each evaluation creates its own class object, and +//! the template's static method function objects are at home in it +//! (`js_static_method_entry_enter_home`). + +use perry_hir::{Expr, Stmt}; +use std::collections::HashSet; + +/// Every `ClassExprFresh` template named anywhere in `hir`: module init, +/// function bodies, class members and nested closures. +pub(crate) fn fresh_class_templates(hir: &perry_hir::Module) -> HashSet { + fn visit_expr(e: &Expr, out: &mut HashSet) { + if let Expr::ClassExprFresh { template, .. } = e { + out.insert(template.clone()); + } + if let Expr::Closure { body, .. } = e { + visit_body(body, out); + } + perry_hir::walker::walk_expr_children(e, &mut |c| visit_expr(c, out)); + } + fn visit_body(body: &[Stmt], out: &mut HashSet) { + for s in body { + perry_hir::walker::stmt_any_expr(s, &mut |e| { + visit_expr(e, out); + false + }); + } + } + let mut out = HashSet::new(); + visit_body(&hir.init, &mut out); + for f in &hir.functions { + visit_body(&f.body, &mut out); + } + for class in &hir.classes { + if let Some(ctor) = &class.constructor { + visit_body(&ctor.body, &mut out); + } + for f in class + .methods + .iter() + .chain(class.static_methods.iter()) + .chain(class.getters.iter().map(|(_, f)| f)) + .chain(class.setters.iter().map(|(_, f)| f)) + { + visit_body(&f.body, &mut out); + } + for field in class.fields.iter().chain(class.static_fields.iter()) { + if let Some(init) = &field.init { + visit_expr(init, &mut out); + } + } + } + out +} diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index e57f57131e..8b4721976d 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -204,6 +204,7 @@ mod emission_order_tests; mod entry; pub mod entry_outline; mod export_value_wrappers; +mod fresh_class_templates; pub(crate) mod func_registry; mod function; mod function_source_header; diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs index 6f585dc641..e660d3ac49 100644 --- a/crates/perry-codegen/src/codegen/string_pool.rs +++ b/crates/perry-codegen/src/codegen/string_pool.rs @@ -231,6 +231,9 @@ pub(super) fn emit_string_pool( // `js_register_function_source_static` call in `__perry_init_strings_` // so `fn.toString()` can reconstruct the source. user_fn_source: &[(String, String, bool)], + // The templates the module evaluates per evaluation (`ClassExprFresh`): + // their static methods' entries run in the function object's home. + fresh_class_templates: &std::collections::HashSet, ) { for entry in strings.iter() { // .rodata bytes — `[N+1 x i8]` because we include the null terminator. @@ -1061,6 +1064,7 @@ pub(super) fn emit_string_pool( .iter() .any(|p| p.is_rest && p.arguments_object.is_none()), has_synth_args: f.params.iter().any(|p| p.arguments_object.is_some()), + home: fresh_class_templates.contains(class_name), }); } // #1787: the standalone constructor `___constructor` @@ -1138,6 +1142,10 @@ pub(super) fn emit_string_pool( .unwrap_or(0); ctor_triples.push((cid, ctor_symbol, ctor_params, ctor_sig_caps)); } + let fresh_cids: std::collections::HashSet = fresh_class_templates + .iter() + .filter_map(|name| class_ids.get(name).copied()) + .collect(); method_triples.sort_unstable(); for ( cid, @@ -1203,6 +1211,38 @@ pub(super) fn emit_string_pool( (I64, &spec_length.to_string()), ], ); + // A per-evaluation template's prototype holds one function object per + // method for each evaluation, running this entry at home in that + // evaluation (`class_method_entry_enter_home`). + if fresh_cids.contains(&cid) { + let (entry_ref, entry_info_ref) = emit_class_method_entry( + &mut chunker, + &StaticMethodEntry { + cid, + llvm_name: llvm_name.clone(), + param_count, + spec_length, + has_user_rest: has_rest, + has_synth_args, + home: true, + }, + ); + 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_method_entry", + &[ + (I64, &cid.to_string()), + (I64, &bytes_i64), + (I64, &len_str), + (I64, &entry_i64), + ], + ); + } } // #1788: register static methods into CLASS_STATIC_METHODS so inherited // static methods (subclass extends a class-expression value) resolve at @@ -1274,6 +1314,7 @@ pub(super) fn emit_string_pool( spec_length, has_user_rest, has_synth_args, + home: fresh_cids.contains(&cid), }, ); let blk = chunker.current_block(); @@ -1895,6 +1936,9 @@ struct StaticMethodEntry { spec_length: u32, has_user_rest: bool, has_synth_args: bool, + /// The class is a per-evaluation template: each evaluation's function + /// object for this method is at home in that evaluation's class object. + home: bool, } /// Define `__clo(callee, this, args...)`, the code of a ClassBody static @@ -1921,10 +1965,17 @@ fn emit_static_method_entry( .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")], - ); + if e.home { + b.call_void( + "js_static_method_entry_enter_home", + &[(I32, &e.cid.to_string()), (I64, "%this"), (I64, "%callee")], + ); + } else { + 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(); @@ -1955,3 +2006,63 @@ fn emit_static_method_entry( let info_ref = chunker.current_block().fn_info_ref(&entry_name); (format!("@{}", entry_name), info_ref) } + +/// Define `__eclo(callee, this, args...)`, the code of the function +/// object a per-evaluation class's prototype holds for an instance method: +/// the call's `this` is the body's receiver, and the body runs in the +/// evaluation the function object belongs to (its home class object, the +/// object's one capture) for its private names and captured environment. +/// Arity, rest bundling, length and strictness are facts of the entry's own +/// `JsFunctionInfo`. Returns `(@__eclo, @__eclo$info)`. +fn emit_class_method_entry( + chunker: &mut InitChunker<'_>, + e: &StaticMethodEntry, +) -> (String, String) { + use crate::fn_info::RestKind; + let entry_name = format!("{}__eclo", 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(); + let depth = b.call( + I64, + "js_class_method_entry_enter_home", + &[(I32, &e.cid.to_string()), (I64, "%this"), (I64, "%callee")], + ); + let this_box = b.bitcast_i64_to_double("%this"); + let arg_names: Vec = (0..n).map(|i| format!("%a{}", i)).collect(); + let mut call_args: Vec<(crate::types::LlvmType, &str)> = vec![(DOUBLE, this_box.as_str())]; + call_args.extend(arg_names.iter().map(|a| (DOUBLE, a.as_str()))); + let r = b.call(DOUBLE, &e.llvm_name, &call_args); + b.call_void("js_class_method_entry_leave", &[(I64, &depth)]); + 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/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs index 72173b55fe..80c58b8bfd 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -609,72 +609,57 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let template_cid = ctx.class_ids.get(template).copied().unwrap_or(0); let tcid_str = template_cid.to_string(); // Room for the evaluation's own `length`, `name` and static - // methods (`js_class_object_pin_parent` defines them) besides its - // static fields. - let own_member_slots = 2 + ctx - .classes - .get(template) - .map_or(0, |c| c.static_methods.len()); + // methods, its pinned parent, its captured environment and its + // prototype object besides its static fields, so the template's + // shapes are all inline slots (`class_object_template`). + let own_member_slots = + 3 + ctx.classes.get(template).map_or(0, |c| { + c.static_methods.len() + usize::from(c.extends_expr.is_some()) + }) + usize::from(!captured_args.is_empty()); let nfields = (named_statics.len() + own_member_slots).to_string(); - // Allocate with class_id = template; set_field_by_name below - // performs the keys-array transition for the named statics. - let obj = - ctx.block() - .call(I64, "js_object_alloc", &[(I32, &tcid_str), (I32, &nfields)]); - // #1789: mark it as a class object (ShapeObjectKind::Class) - // so `typeof` reports "function" and `new`/`instanceof` read the - // class_id from this object rather than treating it as an instance. - ctx.block() - .call_void("js_object_mark_class", &[(I64, &obj)]); + // A static FIELD named `length` / `name` takes over the intrinsic + // property; it is created as an ordinary one. + let field_mask = + named_statics + .iter() + .fold(0u32, |mask, (name, _)| match name.as_str() { + "length" => mask | 1, + "name" => mask | 2, + _ => mask, + }); + // #1789 / #6438: the evaluation's class object, stamped with + // class_id = template and marked a class object + // (ShapeObjectKind::Class, so `typeof` reports "function" and + // `new`/`instanceof` read the class_id from it), owning its + // `length`, `name` and static methods and pinned to THIS + // evaluation's parent. The lowering sequences + // `RegisterClassParentDynamic` immediately ahead of this node, so + // `CLASS_DYNAMIC_PARENT_VALUE[template]` still holds that parent; + // later evaluations overwrite it, but each object keeps its own + // edge. Every evaluation after the template's first is allocated + // directly in the template's final shape. + let obj = ctx.block().call( + I64, + "js_class_evaluation_object", + &[ + (I32, &tcid_str), + (I32, &nfields), + (I32, &field_mask.to_string()), + ], + ); // #7154: the fresh class object is a raw SSA register while the - // evaluation allocates (its own members, every static initializer - // and captured argument), and an evacuating minor relocates it, - // after which each later `js_object_set_field_by_name` would write - // into from-space — the statics would land on the abandoned copy. - // So the object is always held in a rooted group (`Expr::Object`'s - // contract since #6951), re-read before each use. - // - // It used to be skipped for a class whose body held nothing that - // allocates. That is no longer a case: `js_class_object_pin_parent` - // defines the object's own `length`, `name` and static methods - // right after it is created, which allocates for every class. - // (`js_object_mark_class` does not protect the register: it files - // the pointer in `CLASS_OBJECT_VALUES`, a forwarded root that keeps - // the OBJECT alive but never rewrites `%obj`.) + // evaluation allocates (every static initializer and captured + // argument), and an evacuating minor relocates it, after which each + // later `js_object_set_field_by_name` would write into from-space — + // the statics would land on the abandoned copy. So the object is + // always held in a rooted group (`Expr::Object`'s contract since + // #6951), re-read before each use. (`CLASS_OBJECT_VALUES` does not + // protect the register: it is a forwarded root that keeps the + // OBJECT alive but never rewrites `%obj`.) let block_fns = static_block_fns(ctx, template); let protect_handle = true; with_rooted_group(ctx, 1, |ctx, group| { let rooted = group.adopt_emitted(ctx, Repr::Ptr, &obj, protect_handle); - // #6438: pin THIS evaluation's parent onto the object. The lowering - // sequences `RegisterClassParentDynamic` immediately ahead of this - // node, so `CLASS_DYNAMIC_PARENT_VALUE[template]` still holds this - // evaluation's parent; later evaluations overwrite it, but each - // object keeps its own edge. Without this, a factory invoked more - // than once (effect's `class DeclareClass extends make(ast) { … }`) - // has every instance walk to the LAST parent — reading that - // evaluation's `static ast` instead of its own. The same call gives - // the object its own `length`, `name` and static methods. No parent - // pinned when the class expression has no heritage. - { - let obj = group.reread_emitted(ctx, rooted); - // A static FIELD named `length` / `name` takes over the - // intrinsic property; it is created as an ordinary one. - let field_mask = named_statics.iter().fold(0u32, |mask, (name, _)| match name - .as_str() - { - "length" => mask | 1, - "name" => mask | 2, - _ => mask, - }); - ctx.block().call_void( - "js_class_object_pin_parent", - &[ - (I64, &obj), - (I32, &tcid_str), - (I32, &field_mask.to_string()), - ], - ); - } // A named class expression's lexical self-binding is // initialized immediately after the class value is created, // before computed names and static initializers run. The @@ -790,19 +775,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { }, |ctx, arr| Ok(nanbox_pointer_inline(ctx.block(), arr)), )?; - let key_idx = ctx.strings.intern("__perry_ctor_caps"); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); // #7154: re-read the class object — the capture lowerings above // are arbitrary expressions and may have moved it. let obj = group.reread_emitted(ctx, rooted); - let blk = ctx.block(); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, crate::nanbox::POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &obj), (I64, &key_raw), (DOUBLE, &caps_box)], + // Its own `__perry_ctor_caps`: one recorded transition from + // the template's final shape (`class_object_template`). + ctx.block().call_void( + "js_class_object_set_ctor_caps", + &[(I64, &obj), (DOUBLE, &caps_box)], ); // A guarded class environment learns this evaluation; the // first one publishes its captures into the slots. 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 92777144e5..431ddddd3e 100644 --- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv @@ -581,6 +581,7 @@ js_class_env_register_slot Leaf js_class_env_register_state Leaf js_class_env_set Reenters js_class_env_stamp AllocOnly +js_class_evaluation_object Reenters js_class_field_add Reenters js_class_field_get_ic Reenters js_class_field_get_ic_fast Leaf @@ -593,9 +594,11 @@ js_class_lexical_binding_get Reenters js_class_lexical_binding_set Reenters js_class_method_bind Reenters js_class_method_bind_by_id Reenters +js_class_method_entry_enter_home Reenters +js_class_method_entry_leave Leaf js_class_method_snapshot_bind Reenters -js_class_object_pin_parent Reenters js_class_object_refresh_capture_values Reenters +js_class_object_set_ctor_caps Reenters js_class_prototype_method_value Reenters js_class_register_capture_values Leaf js_class_register_static_field Reenters @@ -2739,6 +2742,7 @@ js_register_class_id Leaf js_register_class_length Reenters js_register_class_method Leaf js_register_class_method_bind_length Leaf +js_register_class_method_entry Leaf js_register_class_name Reenters js_register_class_parent Leaf js_register_class_parent_dynamic Reenters @@ -3034,6 +3038,7 @@ js_state_get Reenters js_state_init Reenters js_state_set Reenters js_static_method_entry_enter Reenters +js_static_method_entry_enter_home Reenters js_static_method_entry_leave Leaf js_static_this_arm_classref Reenters js_static_this_arm_value Leaf diff --git a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv index 1b5c63f54a..0756c1b97b 100644 --- a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv +++ b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv @@ -581,6 +581,7 @@ js_class_env_register_slot Leaf js_class_env_register_state Leaf js_class_env_set Reenters js_class_env_stamp AllocOnly +js_class_evaluation_object Reenters js_class_field_add Reenters js_class_field_get_ic Reenters js_class_field_get_ic_fast Leaf @@ -593,9 +594,11 @@ js_class_lexical_binding_get Reenters js_class_lexical_binding_set Reenters js_class_method_bind Reenters js_class_method_bind_by_id Reenters +js_class_method_entry_enter_home Reenters +js_class_method_entry_leave Leaf js_class_method_snapshot_bind Reenters -js_class_object_pin_parent Reenters js_class_object_refresh_capture_values Reenters +js_class_object_set_ctor_caps Reenters js_class_prototype_method_value Reenters js_class_register_capture_values Leaf js_class_register_static_field Reenters @@ -2737,6 +2740,7 @@ js_register_class_id Reenters js_register_class_length Reenters js_register_class_method Leaf js_register_class_method_bind_length Reenters +js_register_class_method_entry Leaf js_register_class_name Reenters js_register_class_parent Reenters js_register_class_parent_dynamic Reenters @@ -3031,6 +3035,7 @@ js_state_get Reenters js_state_init Reenters js_state_set Reenters js_static_method_entry_enter Reenters +js_static_method_entry_enter_home Reenters js_static_method_entry_leave Leaf js_static_this_arm_classref Reenters js_static_this_arm_value Leaf diff --git a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv index a47603ed85..5011b9e1d3 100644 --- a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv @@ -581,6 +581,7 @@ js_class_env_register_slot Leaf js_class_env_register_state Leaf js_class_env_set Reenters js_class_env_stamp AllocOnly +js_class_evaluation_object Reenters js_class_field_add Reenters js_class_field_get_ic Reenters js_class_field_get_ic_fast Leaf @@ -593,9 +594,11 @@ js_class_lexical_binding_get Reenters js_class_lexical_binding_set Reenters js_class_method_bind Reenters js_class_method_bind_by_id Reenters +js_class_method_entry_enter_home Reenters +js_class_method_entry_leave Leaf js_class_method_snapshot_bind Reenters -js_class_object_pin_parent Reenters js_class_object_refresh_capture_values Reenters +js_class_object_set_ctor_caps Reenters js_class_prototype_method_value Reenters js_class_register_capture_values Leaf js_class_register_static_field Reenters @@ -2737,6 +2740,7 @@ js_register_class_id Leaf js_register_class_length Reenters js_register_class_method Leaf js_register_class_method_bind_length Leaf +js_register_class_method_entry Leaf js_register_class_name Reenters js_register_class_parent Leaf js_register_class_parent_dynamic Reenters @@ -3031,6 +3035,7 @@ js_state_get Reenters js_state_init Reenters js_state_set Reenters js_static_method_entry_enter Reenters +js_static_method_entry_enter_home Reenters js_static_method_entry_leave Leaf js_static_this_arm_classref Reenters js_static_this_arm_value Leaf diff --git a/crates/perry-codegen/src/root_reload.rs b/crates/perry-codegen/src/root_reload.rs index 2c4f4a3c02..0e7f425837 100644 --- a/crates/perry-codegen/src/root_reload.rs +++ b/crates/perry-codegen/src/root_reload.rs @@ -261,7 +261,6 @@ const NON_COLLECTING: &[&str] = &[ "js_object_get_class_id", "js_object_get_own_field_or_undef", "js_object_mark_class", - "js_class_object_pin_parent", "js_new_target_get", "js_new_target_set", "js_ctor_return_override", diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 86b417ac4a..d25dcf2423 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -117,7 +117,8 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { // new/instanceof read class_id from it. module.declare_function("js_object_mark_class", VOID, &[I64]); // #6438: pin a per-evaluation class object's own parent edge. - module.declare_function("js_class_object_pin_parent", VOID, &[I64, I32, I32]); + module.declare_function("js_class_evaluation_object", I64, &[I32, I32, I32]); + module.declare_function("js_class_object_set_ctor_caps", VOID, &[I64, DOUBLE]); // Shape-cache-aware variant: pre-populates keys_array via SHAPE_INLINE_CACHE, // so subsequent field stores can use index-based set_field (skipping the // per-call linear key-search done by js_object_set_field_by_name). 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 0eb513372f..314de9c357 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 @@ -427,6 +427,14 @@ pub(crate) fn declare_core(module: &mut LlModule) { 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_enter_home", VOID, &[I32, I64, I64]); + module.declare_function("js_class_method_entry_enter_home", I64, &[I32, I64, I64]); + module.declare_function("js_class_method_entry_leave", VOID, &[I64]); + module.declare_function( + "js_register_class_method_entry", + VOID, + &[I64, I64, I64, I64], + ); module.declare_function("js_static_method_entry_leave", VOID, &[]); module.declare_function( "js_class_static_call_guard", diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index cb08ec91de..0be2820fe5 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -589,6 +589,7 @@ js_class_env_register_slot void i32u,i32u,ptr js_class_env_register_state void i32u,ptr js_class_env_set void f64,i32u,i32u,f64 js_class_env_stamp f64 f64,i32u,f64 +js_class_evaluation_object i64 i32u,i32u,i32u js_class_field_add f64 f64,f64,f64 js_class_field_get_ic f64 i64,f64,i32u,i32u,ptr,i32u,i32s,ptr js_class_field_get_ic_fast f64 i64,f64,i32u,i32u,ptr,i32u,i32s,ptr @@ -601,9 +602,11 @@ js_class_lexical_binding_get f64 f64 js_class_lexical_binding_set f64 f64,f64 js_class_method_bind f64 f64,ptr,usize js_class_method_bind_by_id f64 f64,i64 +js_class_method_entry_enter_home i64 i32u,i64,i64 +js_class_method_entry_leave void i64 js_class_method_snapshot_bind f64 f64,ptr,usize -js_class_object_pin_parent void i64,i32u,i32u js_class_object_refresh_capture_values void f64,i64,f64 +js_class_object_set_ctor_caps void i64,f64 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 @@ -3214,6 +3217,7 @@ js_register_class_id void i32u js_register_class_length void i32u,i32u js_register_class_method void i64,ptr,i64,i64,i64,i64,i64 js_register_class_method_bind_length void i64,ptr,i64,i64 +js_register_class_method_entry void i64,ptr,i64,i64 js_register_class_name void i32u,ptr,i32u js_register_class_parent void i32u,i32u js_register_class_parent_dynamic void i32u,f64 @@ -3516,6 +3520,7 @@ 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_enter_home void i32u,i64,i64 js_static_method_entry_leave void js_static_this_arm_classref void i32u js_static_this_arm_value void f64 diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 6b4592f217..103c9ef34a 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -222,21 +222,6 @@ pub unsafe fn dispatch_bound_method( return result; } - // A static method value of a per-evaluation class object is stored in the - // object's own property of that name, so resolving the name on the object - // would find this very closure again. Run the declaration instead, in the - // evaluation the value was read from. - if method_name_len > 0 && !method_name_ptr.is_null() { - if let Some(result) = crate::object::class_object_static_method_call( - namespace_obj, - method_name_ptr as *const u8, - method_name_len, - args, - ) { - return result; - } - } - crate::object::js_native_call_method( namespace_obj, method_name_ptr, diff --git a/crates/perry-runtime/src/object/alloc_basic.rs b/crates/perry-runtime/src/object/alloc_basic.rs index 9d65b65261..249b37d29e 100644 --- a/crates/perry-runtime/src/object/alloc_basic.rs +++ b/crates/perry-runtime/src/object/alloc_basic.rs @@ -158,6 +158,31 @@ pub(crate) fn object_alloc_born( ptr } +/// The storage `js_object_alloc(class_id, field_count)` allocates (header, +/// `undefined` slots, pointer-free layout) with NO shape published: its +/// stamp word is 0. The caller stamps the object's shape before anything +/// else can allocate: a per-evaluation class object or prototype born +/// directly in its template's final shape (`class_object_template`). +pub(crate) fn object_alloc_unpublished(class_id: u32, field_count: u32) -> *mut ObjectHeader { + let header_size = std::mem::size_of::(); + let alloc_field_count = std::cmp::max(field_count as usize, crate::object::INLINE_SLOT_FLOOR); + let total_size = header_size + alloc_field_count * std::mem::size_of::(); + let ptr = arena_alloc_gc(total_size, 8, crate::gc::GC_TYPE_OBJECT) as *mut ObjectHeader; + unsafe { + (*ptr).class_id = class_id; + (*ptr).parent_class_id = 0; + // GC_STORE_AUDIT(INIT): fresh object starts with no per-object meta record (#6759 B). + (*ptr).meta = ptr::null_mut(); + let fields_ptr = (ptr as *mut u8).add(header_size) as *mut JSValue; + for i in 0..alloc_field_count { + // GC_STORE_AUDIT(INIT): freshly allocated object field slot is initialized pointer-free. + ptr::write(fields_ptr.add(i), JSValue::undefined()); + } + crate::gc::layout_init_pointer_free(ptr as *mut u8); + } + ptr +} + /// Fast object allocation using bump allocator - NO field initialization /// This is significantly faster for hot paths where constructor immediately sets all fields /// Returns a pointer to the object header with UNINITIALIZED fields diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 6a5cb67d4e..5f63c0ca59 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -375,12 +375,13 @@ pub unsafe extern "C" fn js_class_register_capture_values( if class_id == 0 || values_ptr.is_null() { return; } - let mut values = Vec::with_capacity(len); - for i in 0..len { - values.push((*values_ptr.add(i)).to_bits()); - } + // Every evaluation of a capturing class registers its snapshot: refill + // the template's entry in place rather than allocate a new one. CLASS_CAPTURE_VALUES.with(|m| { - m.borrow_mut().insert(class_id, values); + let mut m = m.borrow_mut(); + let values = m.entry(class_id).or_default(); + values.clear(); + values.extend((0..len).map(|i| (*values_ptr.add(i)).to_bits())); }); } diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index c3126a99b0..224b14b233 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -86,20 +86,21 @@ 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_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, decl_prototype_identity_id, - global_object_prototype_bits, is_bound_native_constructor_closure_value, - is_non_constructable_builtin_function_value, parent_closure_in_chain, - template_has_class_objects, throw_non_constructable_builtin_function, CLASS_OBJECT_EVER, + class_id_for_decl_prototype_object, class_method_entry, 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, decl_prototype_identity_id, global_object_prototype_bits, + is_bound_native_constructor_closure_value, is_non_constructable_builtin_function_value, + parent_closure_in_chain, template_has_class_objects, throw_non_constructable_builtin_function, + CLASS_OBJECT_EVER, }; pub use state::{ AccessorDecl, ClassVTable, VTableMethodEntry, CLASS_DECL_PROTOTYPE_OBJECTS, @@ -207,7 +208,7 @@ pub(crate) use registration::{ }; pub use registration::{ is_class_id_registered, js_register_class_getter, js_register_class_method, - js_register_class_method_bind_length, js_register_class_setter, + js_register_class_method_bind_length, js_register_class_method_entry, js_register_class_setter, js_register_class_static_getter, js_register_class_static_method_bind_length, js_register_class_static_setter, js_register_class_string_member_order, }; @@ -230,12 +231,11 @@ pub(crate) use parent_static::{ 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_static_method_code, class_own_static_method_entry, - class_own_static_method_name_bytes, 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, instance_chain_parent_class_id, - lookup_class_symbol_method_in_chain, + 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, instance_chain_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, }; diff --git a/crates/perry-runtime/src/object/class_registry/class_meta.rs b/crates/perry-runtime/src/object/class_registry/class_meta.rs index 4644a78999..61fa1426cb 100644 --- a/crates/perry-runtime/src/object/class_registry/class_meta.rs +++ b/crates/perry-runtime/src/object/class_registry/class_meta.rs @@ -756,6 +756,7 @@ mod anon_shape_collision_tests { param_count: 1, has_synthetic_arguments: false, has_rest: false, + entry: 0, }, ); let mut guard = CLASS_VTABLE_REGISTRY.write().unwrap(); diff --git a/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs b/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs index 82bfdabc25..22ec156cd9 100644 --- a/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs +++ b/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs @@ -18,7 +18,7 @@ //! ``` //! //! Every evaluation already carries its OWN heritage as an own property on its -//! class object (`js_class_object_pin_parent`), and the two consumers that were +//! class object (`js_class_evaluation_object`), and the two consumers that were //! already per-evaluation — the prototype chain //! (`class_evaluation_prototype_value`) and the capture snapshot //! (`pinned_class_object_for_ancestor`) — read it from there. The `super()` leg @@ -152,7 +152,7 @@ pub(crate) fn is_self_heritage_value(class_id: u32, parent_bits: u64) -> bool { } /// #10624: monotone "has any class object ever pinned its own heritage" -/// flag. `js_class_object_pin_parent` arms it before its own write, so +/// flag. `js_class_evaluation_object` arms it before its own write, so /// anything it can EVER make true (an instance pinned to its constructing /// class object, or a class_id that has more than one live per-evaluation /// parent) is only reachable once this is armed. `instanceof`'s value-aware @@ -169,7 +169,7 @@ pub(crate) static CLASS_OBJECT_HERITAGE_PIN_LATCH: crate::registry_latch::Regist /// `new ()`) remembers which SPECIFIC evaluation /// built it (#10624). /// -/// `js_class_object_pin_parent`'s pin lives on the CLASS OBJECT and answers +/// `js_class_evaluation_object`'s pin lives on the CLASS OBJECT and answers /// "what is MY parent" — `super()`, the prototype chain, and capture /// resolution above all already consult it. Nothing, though, gave the /// resulting INSTANCE a way back to that same evaluation: an instance @@ -210,7 +210,7 @@ pub(crate) fn pin_instance_constructing_class(inst: *mut ObjectHeader, classobj_ if instance_pinned_constructing_class(inst).is_some() { return; } - // `js_class_object_pin_parent` already armed `CLASS_OBJECT_HERITAGE_PIN_LATCH` + // `js_class_evaluation_object` already armed `CLASS_OBJECT_HERITAGE_PIN_LATCH` // before writing `class_ptr`'s own pin above (the ordering rule in // `registry_latch.rs`) — that write happens-before this one in this // thread's program order, so the latch is already armed here. diff --git a/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs b/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs index 0eb2812e1e..6cfe53bee0 100644 --- a/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs +++ b/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs @@ -108,8 +108,14 @@ fn an_active_replay_resolves_this_evaluations_pinned_heritage() { // This evaluation's heritage, pinned onto its own class object exactly // as codegen does right after `RegisterClassParentDynamic`. js_register_class_parent_dynamic(TEMPLATE, class_ref(FIRST_PARENT)); - class_handle.with_mut_ptr::(|class| { - super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE, 0) + class_handle.with_mut_ptr::(|class| unsafe { + super::super::parent_static::class_object_define_members( + class, + TEMPLATE, + 0, + super::super::parent_static::template_dynamic_parent_value(TEMPLATE), + &|_, _| {}, + ); }); // A LATER evaluation of the same template overwrites the shared stash. @@ -195,8 +201,14 @@ fn sibling_class_objects_of_the_same_template_keep_distinct_pins() { crate::object::class_registry::js_object_mark_class(class as i64) }); js_register_class_parent_dynamic(TEMPLATE, class_ref(FIRST_PARENT)); - first_handle.with_mut_ptr::(|class| { - super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE, 0) + first_handle.with_mut_ptr::(|class| unsafe { + super::super::parent_static::class_object_define_members( + class, + TEMPLATE, + 0, + super::super::parent_static::template_dynamic_parent_value(TEMPLATE), + &|_, _| {}, + ); }); let first_pin = first_handle.with_mut_ptr::(|class| { super::super::parent_static::class_object_pinned_parent(class as *const crate::ObjectHeader) @@ -215,8 +227,14 @@ fn sibling_class_objects_of_the_same_template_keep_distinct_pins() { crate::object::class_registry::js_object_mark_class(class as i64) }); js_register_class_parent_dynamic(TEMPLATE, class_ref(LAST_PARENT)); - last_handle.with_mut_ptr::(|class| { - super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE, 0) + last_handle.with_mut_ptr::(|class| unsafe { + super::super::parent_static::class_object_define_members( + class, + TEMPLATE, + 0, + super::super::parent_static::template_dynamic_parent_value(TEMPLATE), + &|_, _| {}, + ); }); // The EARLIER evaluation's own pin must be UNCHANGED by the later one. 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 44676869c1..d1e9181a3a 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -355,69 +355,54 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, mut parent_val /// Own-property key under which a per-evaluation class object /// (`ClassExprFresh`) pins ITS OWN parent class value. See -/// `js_class_object_pin_parent`. +/// `js_class_evaluation_object`. pub(crate) const CLASS_OBJECT_PARENT_KEY: &str = "__perry_parent_class"; -/// #6438: pin THIS evaluation's parent onto a per-evaluation class object. -/// -/// `CLASS_DYNAMIC_PARENT_VALUE` is keyed by the child's **class id**, i.e. by -/// the compile-time template — so a class expression evaluated N times with a -/// DIFFERENT parent each time (effect's -/// `class DeclareClass extends make(ast) { … }`, where `make(ast)` returns a -/// fresh class per call) collapses to last-wins: every DeclareClass would walk -/// to the LAST `make(ast)` and read that evaluation's `static ast`. -/// -/// Codegen calls this immediately after `RegisterClassParentDynamic` in the -/// same lowered Sequence, so the table still holds *this* evaluation's parent. -/// Copy it onto the class object as an own property; later evaluations -/// overwrite the table but each object already carries its own edge. Same -/// write-right-before-use shape the capture snapshot already uses. -/// -/// A no-parent class expression pins nothing (the getter yields undefined or a -/// static ClassRef fallback, which the field walk treats as "no own edge"). -#[no_mangle] -pub extern "C" fn js_class_object_pin_parent( - obj: i64, +/// The ordinary path of one evaluation's class object +/// (`js_class_evaluation_object`): give `obj`, a newborn class object of +/// template `template_class_id`, its own `length`, `name` and static methods +/// (with or without heritage), then pin `parent`, this evaluation's heritage, +/// onto it. Without the pin, a factory invoked more than once (effect's +/// `class DeclareClass extends make(ast) { … }`) has every instance walk to +/// the LAST parent (#6438). `record` sees the finished object and its parent. +/// Returns `obj`'s current address. +pub(crate) unsafe fn class_object_define_members( + obj: *mut crate::object::ObjectHeader, template_class_id: u32, static_field_mask: u32, -) { + parent: f64, + record: &dyn Fn(*mut crate::object::ObjectHeader, f64), +) -> *mut crate::object::ObjectHeader { const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - if obj == 0 || template_class_id == 0 { - return; - } - // The template stash, deliberately: this records the heritage the - // `RegisterClassParentDynamic` call immediately preceding us evaluated for - // THIS evaluation. `js_get_dynamic_parent_value`'s active-replay override - // would answer with an enclosing constructor replay's parent when a factory - // is re-entered from inside a constructor body. - let parent = template_dynamic_parent_value(template_class_id); - // Every class object owns its `length`, `name` and static methods from - // creation, with or without heritage. - unsafe { - crate::object::field_get_set::define_class_object_own_properties( - obj as *mut crate::object::ObjectHeader, - static_field_mask, - ); - } - if parent.to_bits() == TAG_UNDEFINED { - return; + if obj.is_null() || template_class_id == 0 { + return obj; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let class = scope.root_raw_mut_ptr(obj); + let parent = scope.root_nanbox_f64(parent); + class.with_mut_ptr::(|obj| { + crate::object::field_get_set::define_class_object_own_properties(obj, static_field_mask); + }); + if parent.get_nanbox_f64().to_bits() != TAG_UNDEFINED { + // #10624: arm BEFORE the write it advertises (the ordering rule in + // `registry_latch.rs`) — everything the latch gates (this own-property + // write, and `pin_instance_constructing_class`'s later instance pin, + // which never fires without this one already having happened) follows + // in this thread's program order. + super::evaluation_heritage::CLASS_OBJECT_HERITAGE_PIN_LATCH.arm(); + let key_bytes = CLASS_OBJECT_PARENT_KEY.as_bytes(); + let key = crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); + class.with_mut_ptr::(|obj| { + crate::object::js_object_set_field_by_name(obj, key, parent.get_nanbox_f64()) + }); } - // #10624: arm BEFORE the write it advertises (the ordering rule in - // `registry_latch.rs`) — everything the latch gates (this own-property - // write, and `pin_instance_constructing_class`'s later instance pin, - // which never fires without this one already having happened) follows - // in this thread's program order. - super::evaluation_heritage::CLASS_OBJECT_HERITAGE_PIN_LATCH.arm(); - let key_bytes = CLASS_OBJECT_PARENT_KEY.as_bytes(); - let key = crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); - crate::object::js_object_set_field_by_name( - obj as *mut crate::object::ObjectHeader, - key, - parent, - ); + class.with_mut_ptr::(|obj| { + record(obj, parent.get_nanbox_f64()); + obj + }) } -/// Read back the parent pinned by `js_class_object_pin_parent`, or `None` when +/// Read back the parent pinned by `js_class_evaluation_object`, or `None` when /// this class object has no own parent edge. /// /// Scans the keys array DIRECTLY rather than going through the by-name read @@ -787,6 +772,7 @@ pub unsafe extern "C" fn js_register_class_computed_method( param_count: param_count as u32, has_synthetic_arguments: false, has_rest: has_rest != 0, + entry: 0, }, ); } @@ -860,6 +846,7 @@ pub unsafe extern "C" fn js_register_class_computed_method( // so they never receive a synthesized arguments object. has_synthetic_arguments: false, has_rest: has_rest != 0, + entry: 0, }, ); // Backfill when reflection already materialized `C.prototype`. @@ -979,25 +966,6 @@ pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { .unwrap_or(false) } -/// The name of ClassBody static method `name` declared by class `class_id` -/// itself, as the bytes of its declaration's record: `None` when the class -/// declares no such method. -/// -/// The bytes are the record's own key, so they live as long as the class's -/// image — the agent that holds every value of the class. A value that names -/// the method for its whole life (a class object's bound static method) points -/// at them instead of keeping a copy. That holds only while no writer of -/// `CLASS_STATIC_METHODS` removes or re-keys a record: each must insert a new -/// record or update a value in place. -pub(crate) fn class_own_static_method_name_bytes( - class_id: u32, - name: &str, -) -> Option<(*const u8, usize)> { - let guard = CLASS_STATIC_METHODS.read().ok()?; - let (key, _) = guard.as_ref()?.get(&class_id)?.get_key_value(name)?; - Some((key.as_ptr(), key.len())) -} - /// ClassBody static method `name` declared by class `class_id` itself: /// `(func_ptr, param_count, has_rest)`. pub(crate) fn class_own_static_method_entry( diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index 8618d9b6a5..3f16dbd418 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -171,6 +171,7 @@ pub unsafe extern "C" fn js_register_class_method( param_count: param_count as u32, has_synthetic_arguments: has_synthetic_arguments != 0, has_rest: has_rest != 0, + entry: 0, }, ); VTABLE_GEN.fetch_add(1, Ordering::Release); @@ -531,6 +532,35 @@ static KEEP_REGISTER_STATIC_SETTER: unsafe extern "C" fn(i64, *const u8, i64, i6 /// Record the spec `.length` (params before the first default/rest) for a class /// method or accessor. Codegen emits one call per method at module init. +/// Register the closure-convention entry of method `name` of per-evaluation +/// class `class_id` on its vtable entry, which `js_register_class_method` +/// created first. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_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 Ok(mut guard) = CLASS_VTABLE_REGISTRY.write() else { + return; + }; + if let Some(method) = guard + .as_mut() + .and_then(|all| all.get_mut(&(class_id as u32))) + .and_then(|vtable| vtable.methods.get_mut(name)) + { + method.entry = entry as usize; + } +} + #[no_mangle] pub unsafe extern "C" fn js_register_class_method_bind_length( class_id: i64, diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index ef12ad3319..2b333eecdd 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -345,6 +345,11 @@ pub struct VTableMethodEntry { /// `has_synthetic_arguments`: the rest slot holds only the args from the /// rest position onward, so apply/dynamic dispatch bundles them correctly. pub has_rest: bool, + /// The method's closure-convention entry (`__eclo`'s + /// `JsFunctionInfo`) when its class is evaluated per evaluation + /// (`ClassExprFresh`): each evaluation's prototype holds one function + /// object running it. 0 otherwise. + pub entry: usize, } /// The compiled halves of one declared accessor, each 0 when that half is @@ -468,6 +473,14 @@ pub static CLASS_STRING_MEMBER_ORDERS: ImageTable>>> = ImageTable::new(|image| &image.method_bind_lengths); +/// The closure-convention entry registered for method `name` of per-evaluation +/// class `class_id` (its vtable entry's `entry`), if any. +pub(crate) fn class_method_entry(class_id: u32, name: &str) -> Option { + let guard = CLASS_VTABLE_REGISTRY.read().ok()?; + let entry = guard.as_ref()?.get(&class_id)?.methods.get(name)?.entry; + (entry != 0).then_some(entry) +} + /// Default-aware spec `.length` for STATIC methods, keyed (class_id, name). /// Distinct from `CLASS_METHOD_BIND_LENGTHS` (instance methods) so a class with /// both `static m(a, b = 1)` and `m(c)` keeps independent lengths instead of diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index 8a3c2026dd..6068862e15 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -311,11 +311,16 @@ pub extern "C" fn js_object_delete_field( { // 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, - ); + // assignment, its dispatch entry: remove both. One + // evaluation's prototype (`ClassExprFresh`) owns its + // members alone: the template's records belong to + // every evaluation, so they stay. + if !super::field_get_set::is_evaluation_prototype_with_methods(obj, cid) { + super::class_registry::class_prototype_method_root_remove(cid, name); + super::class_registry::invalidate_class_string_member_order( + cid, name, false, + ); + } super::class_registry::invalidate_class_prototype_fast_guards_for_method( name, ); diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 47376017ba..59cf173fdf 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -206,6 +206,10 @@ pub(crate) use accessors::scan_accessor_receiver_override_root_mut; mod array_retargeted_proto; mod buffer_own_prop; mod class_object_props; +mod class_object_template; +pub(crate) use class_object_template::{ + is_evaluation_prototype_with_methods, static_method_value_runs, +}; mod crypto_key; pub(crate) mod entries_shape; pub(crate) mod enumeration; @@ -273,7 +277,7 @@ pub(crate) use class_object_props::{ class_evaluation_prototype_class_id, class_object_default_to_string, class_object_has_prototype_property, class_object_materialized_prototype, class_object_prototype_value, class_object_registry_serves_static, class_object_source_text, - class_object_static_method_call, define_class_object_own_properties, + define_class_object_own_properties, }; pub(crate) use crypto_key::{ crypto_key_property_value, CLASS_ID_BOXED_BIGINT, CLASS_ID_BOXED_BOOLEAN, 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 ee32d44aca..7e507145d9 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 @@ -5,7 +5,7 @@ use super::*; -const CLASS_EVALUATION_PROTOTYPE_KEY: &[u8] = b"#"; +pub(crate) const CLASS_EVALUATION_PROTOTYPE_KEY: &[u8] = b"#"; /// Set once the first per-evaluation prototype is materialized, so /// [`class_evaluation_prototype_class_id`] costs one relaxed load for the @@ -70,22 +70,87 @@ pub(crate) fn class_evaluation_prototype_class_id(ptr: usize) -> Option { /// but observable method identity and private-name closures belong to the /// evaluation, not to that shared template. unsafe fn class_evaluation_prototype_value(obj: *const ObjectHeader) -> f64 { + if let Some(existing) = super::super::class_registry::class_object_own_field_bytes( + obj, + CLASS_EVALUATION_PROTOTYPE_KEY, + ) + .filter(|value| value.to_bits() != crate::value::TAG_UNDEFINED) + { + return existing; + } let scope = crate::gc::RuntimeHandleScope::new(); let class = scope.root_raw_mut_ptr(obj as *mut ObjectHeader); - let hidden_key = crate::string::js_string_from_bytes( - CLASS_EVALUATION_PROTOTYPE_KEY.as_ptr(), - CLASS_EVALUATION_PROTOTYPE_KEY.len() as u32, - ); - let hidden_key = scope.root_string_ptr(hidden_key); - let existing = class.with_mut_ptr::(|class| { - hidden_key - .with_const_ptr::(|key| own_data_field_by_name(class, key)) + let class_id = class.with_mut_ptr::(|class| (*class).class_id); + + // Each evaluation owns a distinct prototype object, and that object's + // [[Prototype]] follows this evaluation's pinned heritage edge rather than + // the template-id (last-wins) parent table. + let pinned_parent = class.with_const_ptr::(|class| { + super::super::class_registry::class_object_pinned_parent(class) }); - if let Some(existing) = existing.filter(|value| !value.is_undefined()) { - return f64::from_bits(existing.bits()); + let parent_proto = match pinned_parent { + Some(parent) if parent.to_bits() == crate::value::TAG_NULL => Some(crate::value::TAG_NULL), + Some(parent) => { + let parent = scope.root_nanbox_f64(parent); + let parent_value = parent.get_nanbox_f64(); + if super::super::class_registry::is_class_object_value(parent_value) { + let parent_obj = + JSValue::from_bits(parent_value.to_bits()).as_pointer::(); + (!parent_obj.is_null()) + .then(|| class_evaluation_prototype_value(parent_obj).to_bits()) + } else if let Some(parent_id) = super::super::class_ref_id(parent_value) { + Some(super::super::class_registry::class_decl_prototype_value(parent_id).to_bits()) + } else { + let parent_js = JSValue::from_bits(parent_value.to_bits()); + if parent_js.is_pointer() + && crate::closure::is_closure_ptr(parent_js.as_pointer::() as usize) + { + let value = crate::closure::closure_get_dynamic_prop( + parent_js.as_pointer::() as usize, + "prototype", + ); + let value_js = JSValue::from_bits(value.to_bits()); + if value.to_bits() == crate::value::TAG_NULL { + Some(crate::value::TAG_NULL) + } else { + value_js.is_pointer().then_some(value.to_bits()) + } + } else { + None + } + } + } + None => super::super::class_registry::global_object_prototype_bits(), + }; + let parent_proto = parent_proto.map(|bits| scope.root_heap_word_u64(bits)); + + // Every evaluation after the template's first in this agent is born in + // the shape that one reached (`class_object_template`). + if let Some(parent_proto) = &parent_proto { + if let Some(proto) = class.with_mut_ptr::(|class| { + super::class_object_template::prototype_from_template( + class, + class_id, + parent_proto.get_heap_word_u64(), + ) + }) { + CLASS_EVALUATION_PROTOTYPES_MATERIALIZED + .store(true, std::sync::atomic::Ordering::Relaxed); + let proto = scope.root_raw_mut_ptr(proto); + let proto_value = proto + .with_mut_ptr::(|p| crate::value::js_nanbox_pointer(p as i64)); + class.with_mut_ptr::(|class| { + super::class_object_template::class_object_add_internal( + class, + super::class_object_template::InternalKey::EvaluationPrototype, + proto_value, + ) + }); + return proto + .with_mut_ptr::(|p| crate::value::js_nanbox_pointer(p as i64)); + } } - let class_id = class.with_mut_ptr::(|class| (*class).class_id); // Inline room for `constructor` and every declared member (see // `class_decl_prototype_value`). let members = super::super::class_registry::class_prototype_member_names(class_id).len() as u32; @@ -131,11 +196,18 @@ unsafe fn class_evaluation_prototype_value(obj: *const ObjectHeader) -> f64 { let key = scope.root_string_ptr(key); let class_value = class .with_mut_ptr::(|class| crate::value::js_nanbox_pointer(class as i64)); - let method = super::super::native_module::class_evaluation_method_value_for_name( - class_id, - &name, - class_value, - ); + // The evaluation's own function object for the method: its entry runs + // the method at home in this evaluation (`__eclo`). A template + // compiled without entries keeps the by-name method value. + let method = + super::class_object_template::evaluation_method_value(class_id, &name, class_value) + .unwrap_or_else(|| { + super::super::native_module::class_evaluation_method_value_for_name( + class_id, + &name, + class_value, + ) + }); proto.with_mut_ptr::(|proto| { key.with_const_ptr::(|key| { js_object_set_field_by_name(proto, key, method) @@ -144,62 +216,34 @@ unsafe fn class_evaluation_prototype_value(obj: *const ObjectHeader) -> f64 { }); } - // Each evaluation owns a distinct prototype object, and that object's - // [[Prototype]] follows this evaluation's pinned heritage edge rather than - // the template-id (last-wins) parent table. - let pinned_parent = class.with_const_ptr::(|class| { - super::super::class_registry::class_object_pinned_parent(class) - }); - let parent_proto = match pinned_parent { - Some(parent) if parent.to_bits() == crate::value::TAG_NULL => Some(crate::value::TAG_NULL), - Some(parent) => { - let parent = scope.root_nanbox_f64(parent); - let parent_value = parent.get_nanbox_f64(); - if super::super::class_registry::is_class_object_value(parent_value) { - let parent_obj = - JSValue::from_bits(parent_value.to_bits()).as_pointer::(); - (!parent_obj.is_null()) - .then(|| class_evaluation_prototype_value(parent_obj).to_bits()) - } else if let Some(parent_id) = super::super::class_ref_id(parent_value) { - Some(super::super::class_registry::class_decl_prototype_value(parent_id).to_bits()) - } else { - let parent_js = JSValue::from_bits(parent_value.to_bits()); - if parent_js.is_pointer() - && crate::closure::is_closure_ptr(parent_js.as_pointer::() as usize) - { - let value = crate::closure::closure_get_dynamic_prop( - parent_js.as_pointer::() as usize, - "prototype", - ); - let value_js = JSValue::from_bits(value.to_bits()); - if value.to_bits() == crate::value::TAG_NULL { - Some(crate::value::TAG_NULL) - } else { - value_js.is_pointer().then_some(value.to_bits()) - } - } else { - None - } - } - } - None => super::super::class_registry::global_object_prototype_bits(), - }; - if let Some(parent_proto) = parent_proto { - let parent_proto = scope.root_heap_word_u64(parent_proto); + if let Some(parent_proto) = &parent_proto { proto.with_mut_ptr::(|proto| { super::super::prototype_chain::object_link_class_evaluation_prototype( proto as usize, parent_proto.get_heap_word_u64(), ) }); + proto.with_mut_ptr::(|proto| { + class.with_const_ptr::(|class| { + super::class_object_template::record_prototype_template( + proto, + class, + class_id, + members + 1, + parent_proto.get_heap_word_u64(), + ) + }) + }); } let proto_value = proto .with_mut_ptr::(|proto| crate::value::js_nanbox_pointer(proto as i64)); class.with_mut_ptr::(|class| { - hidden_key.with_const_ptr::(|key| { - js_object_set_field_by_name(class, key, proto_value) - }) + super::class_object_template::class_object_add_internal( + class, + super::class_object_template::InternalKey::EvaluationPrototype, + proto_value, + ) }); proto.with_mut_ptr::(|proto| crate::value::js_nanbox_pointer(proto as i64)) } @@ -272,52 +316,65 @@ pub(crate) unsafe fn define_class_object_own_properties( } } for name in super::super::class_registry::class_own_string_member_names(class_id, true) { - // The method value is bound to THIS class object, as a read of the - // static through the evaluation has always produced it: its call runs - // in this evaluation (its environment and self-binding), whatever - // `this` is. The bound closure keeps the name's address for its whole - // life, so the name is the declaration's own: the bytes of the - // template's record for this method (none for an accessor). - let Some((name_ptr, name_len)) = - super::super::class_registry::class_own_static_method_name_bytes(class_id, &name) + // The method's own function object runs the declaration's + // closure-convention entry, as the shared class's function object's + // does (`install_declared_static_method`): a call's `this` is the + // body's `this`, and its `name` and `length` are facts of the entry. + // One per evaluation, so evaluations never share a method value; its + // home is this class object, the evaluation its body runs in however + // it is called (`js_static_method_entry_enter_home`). An accessor has + // no entry. + let Some(code) = + super::super::class_registry::class_own_static_method_code(class_id, &name) else { continue; }; - let class_value = class - .with_mut_ptr::(|class| crate::value::js_nanbox_pointer(class as i64)); - let f = super::super::native_module::build_bound_method_closure( - class_value, - name_ptr, - name_len, + let f = super::class_object_template::static_method_value(code); + if f.is_null() { + continue; + } + class.with_mut_ptr::(|class| { + super::class_object_template::set_static_method_home( + f, + crate::value::js_nanbox_pointer(class as i64), + ) + }); + define( + &name, + crate::value::js_nanbox_pointer(f as i64), + (true, false, true), ); - define(&name, f, (true, false, true)); } } /// Is `value` the method value `define_class_object_own_properties` stored for -/// static `name` on class object `holder`: a bound method closure naming this -/// method, bound to this object? Anything else in that slot is the program's. +/// static `name` of template `owner` on class object `holder`: a function +/// object running that declaration's entry, at home in `holder`? Anything else +/// in that slot is the program's. unsafe fn is_declared_static_method_value( value: f64, - holder: *const ObjectHeader, + owner: u32, name: &str, + holder: *const ObjectHeader, ) -> bool { - let value = JSValue::from_bits(value.to_bits()); - if !value.is_pointer() { - return false; - } - let closure = value.as_pointer::(); - if !crate::closure::is_closure_ptr(closure as usize) - || !std::ptr::eq((*closure).info, &crate::closure::BOUND_METHOD_INFO) - { - return false; - } - let receiver = crate::closure::js_closure_get_capture_f64(closure, 0); - let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 1) as *const u8; - let name_len = crate::closure::js_closure_get_capture_ptr(closure, 2) as usize; - receiver.to_bits() == crate::value::js_nanbox_pointer(holder as i64).to_bits() - && !name_ptr.is_null() - && std::slice::from_raw_parts(name_ptr, name_len) == name.as_bytes() + // The function object's home first: one capture read, before the + // registry lookup of the declaration's entry. + let v = JSValue::from_bits(value.to_bits()); + v.is_pointer() + && crate::closure::is_closure_ptr(v.as_pointer::() as usize) + && crate::closure::js_closure_get_capture_bits( + v.as_pointer::(), + 0, + ) == crate::value::js_nanbox_pointer(holder as i64).to_bits() + && super::super::class_registry::class_own_static_method_code(owner, name).is_some_and( + |code| { + super::class_object_template::static_method_value_runs( + value.to_bits(), + code, + holder, + ) + }, + ) } /// May a call `C.name(..)` on class object `object` run the registered static @@ -345,7 +402,7 @@ pub(crate) unsafe fn class_object_registry_serves_static( holder, name.as_bytes(), ) - .is_some_and(|v| is_declared_static_method_value(v, holder, name)); + .is_some_and(|v| is_declared_static_method_value(v, owner, name, holder)); } let Some(parent) = super::super::class_registry::class_object_pinned_parent(holder) else { break; @@ -362,30 +419,6 @@ pub(crate) unsafe fn class_object_registry_serves_static( true } -/// Run static method `name` for a bound method value whose receiver is class -/// object `receiver` (see [`define_class_object_own_properties`]). `None` when -/// `receiver` is not a class object or the registry has no such declaration. -pub(crate) unsafe fn class_object_static_method_call( - receiver: f64, - name_ptr: *const u8, - name_len: usize, - args: &[f64], -) -> Option { - if !super::super::class_registry::is_class_object_value(receiver) { - return None; - } - let obj = JSValue::from_bits(receiver.to_bits()).as_pointer::(); - let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)).ok()?; - super::super::class_registry::lookup_static_method_in_chain((*obj).class_id, name)?; - Some(super::super::class_registry::js_class_static_method_call( - receiver, - name_ptr, - name_len, - args.as_ptr(), - args.len(), - )) -} - /// Does `obj` (a class object) own `key` without storing it? Only `prototype`: /// `{ !w, !e, !c }`, created with the class, so it is there until the object /// is gone and no `delete` or `defineProperty` can change that. diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs new file mode 100644 index 0000000000..68158cf056 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs @@ -0,0 +1,713 @@ +//! A per-evaluation class object is born in its template's final shape. +//! +//! Every evaluation of one class template (`ClassExprFresh`) gives its class +//! object the same own keys, in the same order and with the same attributes: +//! `length` and `name` (or the placeholder a static field of that name stores +//! over), one function object per ClassBody static method +//! ([`define_class_object_own_properties`](super::define_class_object_own_properties)), +//! then its pinned parent when the template has heritage. So the shape the +//! class object is created in is a fact of the template, not of the +//! evaluation. The first evaluation in an agent builds it key by key through +//! the ordinary define path, and the shape it reaches is a real shape record: +//! canonical keys with their attribute entries, the class kind, the prototype +//! identity, the rep. Every later evaluation allocates its class object +//! directly in that shape and fills the slots: no birth shape, no class-kind +//! transition, no key-add transition, no by-name define, no attribute edit. +//! The captured environment, which the evaluation adds next, and the +//! evaluation's prototype object, built at its first use, are each one +//! recorded transition (`class_object_add_internal`). +//! +//! The evaluation's prototype object is born the same way, in the template's +//! final prototype shape: `constructor`, then one function object per method +//! running the method's closure-convention entry (`__eclo`) at home in +//! the evaluation, linked to the evaluation's parent prototype. +//! +//! What the template remembers is ShapeIds and what fills each slot; it holds +//! nothing about any object. Its records are external carriers for the +//! agent's life, so no id it names can come to name other facts. + +use super::*; +use std::rc::Rc; + +/// How one slot of a template's final class-object shape is filled. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Fill { + /// The same non-pointer value in every evaluation (`length`, or the + /// `undefined` placeholder of a static field named `length` / `name`). + Bits(u64), + /// The class's `name` string. + Name, + /// A new function object running this ClassBody static method's + /// closure-convention entry (its `JsFunctionInfo`), at home in the class + /// object. + Method(usize), + /// The evaluation's pinned parent (`__perry_parent_class`). + Parent, +} + +struct ClassObjectTemplate { + /// The width the compiled evaluation allocates its class object with. + field_count: u32, + static_field_mask: u32, + has_parent: bool, + final_shape: u32, + fills: Rc<[Fill]>, + /// Recorded additions of an internal own key (`InternalKey`) to a class + /// object of this template: from shape, key, to shape, slot. Every id is + /// a retained record. + edges: Vec<(u32, InternalKey, u32, u32)>, +} + +/// The most internal-key transitions one template records. +const MAX_TEMPLATE_EDGES: usize = 8; + +/// An own key the runtime adds to a per-evaluation class object after its +/// members: the captured environment (`__perry_ctor_caps`) and the +/// evaluation's prototype object (`#`). +/// Neither is reachable from JS, so adding one never runs user code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum InternalKey { + CtorCaps, + EvaluationPrototype, +} + +impl InternalKey { + pub(crate) fn bytes(self) -> &'static [u8] { + match self { + InternalKey::CtorCaps => CTOR_CAPS_KEY, + InternalKey::EvaluationPrototype => { + super::class_object_props::CLASS_EVALUATION_PROTOTYPE_KEY + } + } + } +} + +#[cfg(test)] +thread_local! { + /// How many class objects and prototype objects this thread built from a + /// template's shapes (tests only). + static TEMPLATE_HITS: std::cell::Cell<(u32, u32)> = const { std::cell::Cell::new((0, 0)) }; +} + +#[cfg(test)] +fn template_hits() -> (u32, u32) { + TEMPLATE_HITS.with(std::cell::Cell::get) +} + +#[cfg(test)] +fn note_template_hit(prototype: bool) { + TEMPLATE_HITS.with(|c| { + let (o, p) = c.get(); + c.set(if prototype { (o, p + 1) } else { (o + 1, p) }); + }); +} + +thread_local! { + /// This agent's template shapes, by template class id. ShapeIds name an + /// agent's own records, so the memo is the agent's too. + static TEMPLATES: std::cell::RefCell> = + std::cell::RefCell::new(crate::fast_hash::new_ptr_hash_map()); +} + +/// The own-property key of the evaluation's captured environment. +pub(crate) const CTOR_CAPS_KEY: &[u8] = b"__perry_ctor_caps"; + +/// A static method's own function object for one evaluation: it runs the +/// declaration's closure-convention entry `code`; its one capture, its home +/// class object, is installed next with [`set_static_method_home`]. +pub(super) unsafe fn static_method_value(code: usize) -> *mut crate::closure::ClosureHeader { + crate::closure::js_closure_alloc(code as *const crate::closure::JsFunctionInfo, 1) +} + +/// Give `f`, fresh from [`static_method_value`] with no allocation since, +/// its home class object `home` (NaN-boxed). +pub(super) unsafe fn set_static_method_home(f: *mut crate::closure::ClosureHeader, home: f64) { + crate::closure::closure_install_boxed_captures(f, &[home.to_bits()]); +} + +/// Is `bits` the function object of static method entry `code` whose home is +/// class object `home`? +pub(crate) unsafe fn static_method_value_runs( + bits: u64, + code: usize, + home: *const ObjectHeader, +) -> bool { + let value = JSValue::from_bits(bits); + if !value.is_pointer() { + return false; + } + let closure = value.as_pointer::(); + crate::closure::is_closure_ptr(closure as usize) + && (*closure).info as usize == code + && crate::closure::js_closure_get_capture_bits(closure, 0) + == crate::value::js_nanbox_pointer(home as i64).to_bits() +} + +/// One evaluation's class object of template `template_class_id` +/// (`ClassExprFresh`): allocated `field_count` slots wide, a class object, with +/// its own `length`, `name` and static methods and its pinned parent. +/// `static_field_mask` (bit 0 `length`, bit 1 `name`) names the static fields +/// that take over an intrinsic key. +#[no_mangle] +pub extern "C" fn js_class_evaluation_object( + template_class_id: u32, + field_count: u32, + static_field_mask: u32, +) -> i64 { + let template = TEMPLATES.with(|t| { + t.borrow() + .get(&template_class_id) + .filter(|t| t.field_count == field_count && t.static_field_mask == static_field_mask) + .map(|t| (t.final_shape, t.fills.clone(), t.has_parent)) + }); + // The template stash, deliberately: this records the heritage the + // `RegisterClassParentDynamic` call immediately preceding the evaluation + // evaluated for THIS evaluation. `js_get_dynamic_parent_value`'s + // active-replay override would answer with an enclosing constructor + // replay's parent when a factory is re-entered from inside a constructor + // body. A template recorded without heritage has none to read. + let has_parent = template.as_ref().is_none_or(|t| t.2); + let parent = if has_parent { + crate::object::parent_static::template_dynamic_parent_value(template_class_id) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + if let Some((final_shape, fills, has_parent)) = template { + if has_parent == (parent.to_bits() != crate::value::TAG_UNDEFINED) { + return unsafe { + class_object_in_template_shape( + template_class_id, + field_count, + final_shape, + &fills, + parent, + ) + } as i64; + } + } + let obj = crate::object::js_object_alloc(template_class_id, field_count); + crate::object::class_registry::js_object_mark_class(obj as i64); + unsafe { + crate::object::parent_static::class_object_define_members( + obj, + template_class_id, + static_field_mask, + parent, + &|obj, parent| { + record_class_object_template( + obj, + template_class_id, + field_count, + static_field_mask, + parent, + ) + }, + ) as i64 + } +} + +/// Allocate a class object of template `class_id` directly in the template's +/// `final_shape` and fill its slots. +unsafe fn class_object_in_template_shape( + class_id: u32, + field_count: u32, + final_shape: u32, + fills: &[Fill], + parent: f64, +) -> *mut ObjectHeader { + #[cfg(test)] + note_template_hit(false); + let scope = crate::gc::RuntimeHandleScope::new(); + let parent = scope.root_nanbox_f64(parent); + // The allocation `js_object_alloc(class_id, field_count)` makes, so the + // object is exactly as wide as the one the shape was recorded on. + let obj = crate::object::alloc_basic::object_alloc_unpublished(class_id, field_count); + // Born a class object in its final shape (all `Any` lanes, so every slot + // holds a valid value while still `undefined`). + crate::object::shapes::stamp_object_shape_id_with_carrier_note(obj, final_shape); + // The keys carry non-default attribute entries from birth. + crate::object::descriptor_state::note_attrs_born_with_keys(obj as usize); + // `js_object_mark_class`'s bookkeeping for a class object. + crate::object::field_get_set::note_private_template_evaluated(class_id); + crate::object::class_registry::class_object_value_root_store(class_id, obj); + let class = scope.root_raw_mut_ptr(obj); + for (slot, fill) in fills.iter().enumerate() { + let bits = match *fill { + Fill::Bits(bits) => bits, + Fill::Name => super::super::class_value::intrinsic_own_data_value(class_id, "name") + .map_or(crate::value::TAG_UNDEFINED, f64::to_bits), + Fill::Method(code) => { + let f = static_method_value(code); + class.with_mut_ptr::(|class| { + set_static_method_home(f, crate::value::js_nanbox_pointer(class as i64)) + }); + crate::value::js_nanbox_pointer(f as i64).to_bits() + } + Fill::Parent => { + // `js_class_evaluation_object`'s ordering rule: arm before + // the write it advertises. + super::super::class_registry::evaluation_heritage::CLASS_OBJECT_HERITAGE_PIN_LATCH + .arm(); + parent.get_nanbox_f64().to_bits() + } + }; + class.with_mut_ptr::(|class| { + crate::object::slot_store::store_object_field_slot(class, slot, bits) + }); + } + class.with_mut_ptr::(|class| class) +} + +/// After `obj` got its own keys the ordinary way, record the shape it reached +/// as template `class_id`'s, if its keys are exactly the template's, each in +/// its inline slot, and its lanes are all `Any`. +unsafe fn record_class_object_template( + obj: *mut ObjectHeader, + class_id: u32, + field_count: u32, + static_field_mask: u32, + parent: f64, +) { + if TEMPLATES.with(|t| t.borrow().contains_key(&class_id)) { + return; + } + let Some(d) = crate::object::shapes::object_shape_descriptor(obj) else { + return; + }; + let final_shape = crate::object::shapes::object_shape_id(obj); + let count = d.logical_key_count as usize; + if final_shape == 0 + || d.hole_count != 0 + || d.rep != crate::object::field_rep::REP_ANY + || d.live_inline_slot_count < d.logical_key_count + || count > (field_count as usize).max(crate::object::INLINE_SLOT_FLOOR) + || crate::object::shapes::shape_object_kind_by_id(final_shape) + != Some(crate::object::shapes::ShapeObjectKind::Class) + { + return; + } + let has_parent = parent.to_bits() != crate::value::TAG_UNDEFINED; + let keys = d.keys_view(); + let statics = super::super::class_registry::class_own_string_member_names(class_id, true); + let fields = (obj as *const u8).add(std::mem::size_of::()) as *const u64; + let mut fills = Vec::with_capacity(count); + let mut saw_parent = false; + for slot in 0..count { + let key = keys.get(slot as u32); + let bits = *fields.add(slot); + let value = JSValue::from_bits(bits); + let is = |name: &[u8]| crate::string::js_string_key_matches_bytes(key, name); + let fill = if is(crate::object::parent_static::CLASS_OBJECT_PARENT_KEY.as_bytes()) + && has_parent + && bits == parent.to_bits() + { + saw_parent = true; + Fill::Parent + } else if let Some(name) = statics.iter().find(|name| is(name.as_bytes())) { + // A static method's own function object (a static accessor of + // this name leaves an accessor, which no template records). + let Some(code) = + super::super::class_registry::class_own_static_method_code(class_id, name) + else { + return; + }; + if !static_method_value_runs(bits, code, obj) { + return; + } + Fill::Method(code) + } else if (is(b"length") || is(b"name")) && bits == crate::value::TAG_UNDEFINED { + // The placeholder a static field of this name stores over. + if static_field_mask == 0 { + return; + } + Fill::Bits(bits) + } else if is(b"length") && !value.is_pointer() && !value.is_any_string() { + Fill::Bits(bits) + } else if is(b"name") && value.is_any_string() { + Fill::Name + } else { + return; + }; + fills.push(fill); + } + if saw_parent != has_parent { + return; + } + // The record stays this agent's for its life, so the template never + // stamps an id that names other facts. + crate::object::shapes::note_external_shape_carrier(Some(d)); + TEMPLATES.with(|t| { + t.borrow_mut().insert( + class_id, + ClassObjectTemplate { + field_count, + static_field_mask, + has_parent, + final_shape, + fills: fills.into(), + edges: Vec::new(), + }, + ) + }); +} + +/// Add internal own key `key` with `value` to class object `obj`. From a shape +/// the template recorded the addition from, that is one transition: stamp its +/// target, store the slot. Any other object takes the ordinary store, and the +/// template records the transition it made when it appended exactly this key +/// in an inline `Any` slot. +pub(crate) unsafe fn class_object_add_internal( + obj: *mut ObjectHeader, + key: InternalKey, + value: f64, +) { + let class_id = (*obj).class_id; + let from = crate::object::shapes::object_shape_id(obj); + let edge = TEMPLATES.with(|t| { + t.borrow().get(&class_id).map(|t| { + ( + t.edges + .iter() + .find(|e| e.0 == from && e.1 == key) + .map(|e| (e.2, e.3)), + t.edges.len() < MAX_TEMPLATE_EDGES, + ) + }) + }); + if let Some((Some((to, slot)), _)) = edge { + // `obj` carries the recorded predecessor, so it is exactly as wide as + // the object the transition was recorded on: `slot` is inline. + crate::object::shapes::stamp_object_shape_id_with_carrier_note(obj, to); + crate::object::slot_store::store_object_field_slot(obj, slot as usize, value.to_bits()); + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let class = scope.root_raw_mut_ptr(obj); + let value = scope.root_nanbox_f64(value); + let bytes = key.bytes(); + let key_str = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + class.with_mut_ptr::(|obj| { + crate::object::js_object_set_field_by_name(obj, key_str, value.get_nanbox_f64()) + }); + if edge != Some((None, true)) || from == 0 { + return; + } + class.with_mut_ptr::(|obj| { + let Some(d) = crate::object::shapes::object_shape_descriptor(obj) else { + return; + }; + let to = crate::object::shapes::object_shape_id(obj); + let Some(slot) = d.logical_key_count.checked_sub(1) else { + return; + }; + let fields = (obj as *const u8).add(std::mem::size_of::()) as *const u64; + if to == 0 + || to == from + || d.hole_count != 0 + || d.rep != crate::object::field_rep::REP_ANY + || d.live_inline_slot_count <= slot + || !crate::string::js_string_key_matches_bytes(d.keys_view().get(slot), bytes) + || *fields.add(slot as usize) != value.get_nanbox_f64().to_bits() + { + return; + } + crate::object::shapes::note_external_shape_carrier( + crate::object::shapes::shape_descriptor_by_id(from), + ); + crate::object::shapes::note_external_shape_carrier(Some(d)); + TEMPLATES.with(|t| { + if let Some(t) = t.borrow_mut().get_mut(&class_id) { + t.edges.push((from, key, to, slot)); + } + }); + }); +} + +/// `__perry_ctor_caps` of a per-evaluation class object (`ClassExprFresh`): +/// the captured environment its constructor replays with. +#[no_mangle] +pub extern "C" fn js_class_object_set_ctor_caps(obj: i64, caps: f64) { + if obj == 0 { + return; + } + unsafe { class_object_add_internal(obj as *mut ObjectHeader, InternalKey::CtorCaps, caps) } +} + +/// How one slot of a template's final prototype shape is filled. +#[derive(Clone, Copy, PartialEq, Eq)] +enum ProtoFill { + /// `constructor`: the evaluation's class object. + Constructor, + /// A new function object running this method's closure-convention entry + /// (its `JsFunctionInfo`), at home in the class object. + Method(usize), +} + +struct PrototypeTemplate { + /// The width the prototype object is allocated with. + field_count: u32, + /// The [[Prototype]] identity the final shape names. + proto_id: u64, + final_shape: u32, + fills: Rc<[ProtoFill]>, +} + +thread_local! { + /// This agent's template prototype shapes, by template class id. + static PROTOTYPES: std::cell::RefCell> = + std::cell::RefCell::new(crate::fast_hash::new_ptr_hash_map()); +} + +/// The function object one evaluation's prototype holds for method `name` of +/// template `class_id`: it runs the method's closure-convention entry, and its +/// one capture is its home, class object `class`. `None` when the template +/// registered no entry for `name`. +pub(crate) unsafe fn evaluation_method_value(class_id: u32, name: &str, class: f64) -> Option { + let code = super::super::class_registry::class_method_entry(class_id, name)?; + let scope = crate::gc::RuntimeHandleScope::new(); + let class = scope.root_nanbox_f64(class); + let f = static_method_value(code); + if f.is_null() { + return None; + } + set_static_method_home(f, class.get_nanbox_f64()); + Some(crate::value::js_nanbox_pointer(f as i64)) +} + +/// Build the prototype object of class object `class` (template `class_id`) +/// in the template's final prototype shape, linked to `parent_proto`. `None` +/// when the template has no recorded prototype shape or that shape names +/// another [[Prototype]]. +pub(crate) unsafe fn prototype_from_template( + class: *mut ObjectHeader, + class_id: u32, + parent_proto: u64, +) -> Option<*mut ObjectHeader> { + let (field_count, final_shape, fills) = PROTOTYPES.with(|t| { + let t = t.borrow(); + let t = t.get(&class_id)?; + (crate::object::shapes::stable_linked_proto_id(class_id, parent_proto) == Some(t.proto_id)) + .then(|| (t.field_count, t.final_shape, t.fills.clone())) + })?; + #[cfg(test)] + note_template_hit(true); + let scope = crate::gc::RuntimeHandleScope::new(); + let class = scope.root_raw_mut_ptr(class); + let parent = scope.root_heap_word_u64(parent_proto); + let proto = scope.root_raw_mut_ptr(crate::object::js_object_alloc(class_id, field_count)); + // The links `class_evaluation_prototype_value` makes, written into the + // prototype's meta record directly: its evaluation (lexical owner) and its + // [[Prototype]], whose identity the final shape already names. + let (meta, _) = proto.across_mut::(|| { + crate::object::object_meta_ensure(proto.get_raw_mut_ptr::()) + }); + let owner = + class.with_const_ptr::(|c| crate::value::js_nanbox_pointer(c as i64)); + let parent_bits = parent.get_heap_word_u64(); + (*meta).private_evaluation_brand = owner.to_bits(); + crate::gc::runtime_write_barrier_slot( + meta as usize, + &(*meta).private_evaluation_brand as *const u64 as usize, + owner.to_bits(), + ); + (*meta).prototype = parent_bits; + (*meta).flags |= crate::object::OBJECT_META_FLAG_PROTO_DIVERGED + | crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO; + crate::gc::runtime_write_barrier_slot( + meta as usize, + &(*meta).prototype as *const u64 as usize, + parent_bits, + ); + proto.with_mut_ptr::(|proto| { + crate::object::shapes::stamp_object_shape_id_with_carrier_note(proto, final_shape); + crate::object::descriptor_state::note_attrs_born_with_keys(proto as usize); + }); + for (slot, fill) in fills.iter().enumerate() { + let bits = match *fill { + ProtoFill::Constructor => class + .with_const_ptr::(|c| crate::value::js_nanbox_pointer(c as i64)), + ProtoFill::Method(code) => { + let f = static_method_value(code); + class.with_mut_ptr::(|c| { + set_static_method_home(f, crate::value::js_nanbox_pointer(c as i64)) + }); + crate::value::js_nanbox_pointer(f as i64) + } + }; + proto.with_mut_ptr::(|proto| { + crate::object::slot_store::store_object_field_slot(proto, slot, bits.to_bits()) + }); + } + Some(proto.get_raw_mut_ptr::()) +} + +/// After `proto`, the prototype object of class object `class` (template +/// `class_id`), was built the ordinary way and linked to `parent_proto`, record +/// its shape as the template's, if its keys are exactly `constructor` and the +/// template's methods (each an entry-backed function object at home in +/// `class`), each in its inline slot, with all lanes `Any`. +pub(crate) unsafe fn record_prototype_template( + proto: *mut ObjectHeader, + class: *const ObjectHeader, + class_id: u32, + field_count: u32, + parent_proto: u64, +) { + if PROTOTYPES.with(|t| t.borrow().contains_key(&class_id)) { + return; + } + let Some(proto_id) = crate::object::shapes::stable_linked_proto_id(class_id, parent_proto) + else { + return; + }; + let Some(d) = crate::object::shapes::object_shape_descriptor(proto) else { + return; + }; + let final_shape = crate::object::shapes::object_shape_id(proto); + let count = d.logical_key_count as usize; + let meta = (*proto).meta; + let owner = crate::value::js_nanbox_pointer(class as i64).to_bits(); + if final_shape == 0 + || d.proto_id != proto_id + || d.hole_count != 0 + || d.rep != crate::object::field_rep::REP_ANY + || d.live_inline_slot_count < d.logical_key_count + || count > (field_count as usize).max(crate::object::INLINE_SLOT_FLOOR) + || meta.is_null() + || (*meta).prototype != parent_proto + || (*meta).private_evaluation_brand != owner + || (*meta).flags + & !(crate::object::OBJECT_META_FLAG_PROTO_DIVERGED + | crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO) + != 0 + || (*meta).spill != 0 + { + return; + } + let keys = d.keys_view(); + let fields = (proto as *const u8).add(std::mem::size_of::()) as *const u64; + let members = super::super::class_registry::class_prototype_member_names(class_id); + let mut fills = Vec::with_capacity(count); + for slot in 0..count { + let key = keys.get(slot as u32); + let bits = *fields.add(slot); + let is = |name: &[u8]| crate::string::js_string_key_matches_bytes(key, name); + let fill = if slot == 0 && is(b"constructor") && bits == owner { + ProtoFill::Constructor + } else if let Some((name, false)) = members.iter().find(|(name, _)| is(name.as_bytes())) { + let Some(code) = super::super::class_registry::class_method_entry(class_id, name) + else { + return; + }; + if !static_method_value_runs(bits, code, class) { + return; + } + ProtoFill::Method(code) + } else { + return; + }; + fills.push(fill); + } + crate::object::shapes::note_external_shape_carrier(Some(d)); + PROTOTYPES.with(|t| { + t.borrow_mut().insert( + class_id, + PrototypeTemplate { + field_count, + proto_id, + final_shape, + fills: fills.into(), + }, + ) + }); +} + +/// Is `obj` an evaluation prototype of template `class_id` built with the +/// template's method entries, i.e. the sole owner of its declared methods? +pub(crate) unsafe fn is_evaluation_prototype_with_methods( + obj: *const ObjectHeader, + class_id: u32, +) -> bool { + super::class_object_props::class_evaluation_prototype_class_id(obj as usize) == Some(class_id) + && template_has_method_entries(class_id) +} + +/// Does template `class_id` give its evaluations' prototypes their own method +/// function objects (`__eclo`)? +fn template_has_method_entries(class_id: u32) -> bool { + super::super::class_registry::class_prototype_member_names(class_id) + .iter() + .any(|(name, accessor)| { + !accessor && super::super::class_registry::class_method_entry(class_id, name).is_some() + }) +} + +/// For an own-key miss on `obj` whose recorded chain was walked and does not +/// carry `key`: was `key` a method of `obj`'s template that the chain's +/// evaluation prototype no longer owns? Only an object linked through a class +/// evaluation (an evaluation prototype, or an instance of one evaluation) can +/// answer yes, and only when that prototype lost a member: while it still +/// carries every key of the template's final prototype shape, every method the +/// template declares is on the chain, so the miss is not one of them. +/// Otherwise the template's method entries decide. +pub(crate) unsafe fn evaluation_chain_lost_method( + obj: *const ObjectHeader, + key: *const crate::string::StringHeader, +) -> bool { + let meta = (*obj).meta; + if meta.is_null() || (*meta).flags & crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO == 0 + { + return false; + } + let class_id = (*obj).class_id; + let template = PROTOTYPES + .with(|t| t.borrow().get(&class_id).map(|t| t.final_shape)) + .and_then(crate::object::shapes::shape_descriptor_by_id); + if let Some(template) = template { + // The template's prototype keys, all still there: marking the + // prototype (its first instance) restamps its shape but keeps them. + let intact = |o: *const ObjectHeader| { + crate::object::shapes::object_shape_descriptor(o).is_some_and(|d| { + d.keys == template.keys + && d.logical_key_count == template.logical_key_count + && d.hole_count == 0 + }) + }; + if intact(obj) { + return false; + } + let proto = JSValue::from_bits((*meta).prototype); + if proto.is_pointer() { + let proto = proto.as_pointer::(); + if crate::value::addr_class::try_read_gc_header(proto as usize) + .is_some_and(|h| h.obj_type == crate::gc::GC_TYPE_OBJECT) + && (*proto).class_id == class_id + && intact(proto) + { + return false; + } + } + } + let bytes = std::slice::from_raw_parts( + (key as *const u8).add(std::mem::size_of::()), + (*key).byte_len as usize, + ); + let Ok(name) = std::str::from_utf8(bytes) else { + return false; + }; + let mut cid = class_id; + for _ in 0..32 { + if super::super::class_registry::class_method_entry(cid, name).is_some() { + return true; + } + match super::super::class_registry::get_parent_class_id(cid) { + Some(parent) if parent != 0 && parent != cid => cid = parent, + _ => return false, + } + } + false +} + +#[cfg(test)] +#[path = "class_object_template_tests.rs"] +mod tests; diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_template_tests.rs b/crates/perry-runtime/src/object/field_get_set/class_object_template_tests.rs new file mode 100644 index 0000000000..e1bea2fec9 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/class_object_template_tests.rs @@ -0,0 +1,153 @@ +//! The template shapes of a per-evaluation class: every evaluation after the +//! first is born in them, and what it is born with is its own. + +use super::*; + +fn register(cid: u32) { + let mut guard = crate::object::REGISTERED_CLASS_IDS.write().unwrap(); + guard + .get_or_insert_with(crate::fast_hash::new_ptr_hash_set) + .insert(cid); +} + +extern "C" fn body(_: *const crate::closure::ClosureHeader, _this: crate::closure::JsThis) -> f64 { + 0.0 +} + +extern "C" fn method(_this: f64) -> f64 { + 0.0 +} + +unsafe fn slot(obj: *const ObjectHeader, name: &[u8]) -> u64 { + super::super::class_registry::class_object_own_field_bytes(obj, name) + .expect("own data property") + .to_bits() +} + +/// A template with a static method `s` and a method `m`, both with their +/// closure-convention entries, evaluated three times: the second and third +/// class objects and prototypes are built from the template's shapes (the +/// counters say so), carry exactly the first one's ShapeIds, and hold their +/// own function objects, each at home in its own class object. +#[test] +fn later_evaluations_are_born_in_the_template_shapes() { + let cid = 0x6E01; + register(cid); + let info = crate::fn_info!(body, 0) as *const crate::closure::JsFunctionInfo as usize; + unsafe { + crate::object::js_register_class_name(cid, b"Tpl".as_ptr(), 3); + crate::object::js_register_class_length(cid, 0); + super::super::class_registry::js_register_class_static_method( + cid as i64, + b"s".as_ptr(), + 1, + method as *const () as usize as i64, + 0, + 0, + ); + super::super::class_registry::parent_static::js_register_class_static_method_entry( + cid as i64, + b"s".as_ptr(), + 1, + info as i64, + ); + super::super::class_registry::js_register_class_method( + cid as i64, + b"m".as_ptr(), + 1, + method as *const () as usize as i64, + 0, + 0, + 0, + ); + super::super::class_registry::js_register_class_method_entry( + cid as i64, + b"m".as_ptr(), + 1, + info as i64, + ); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let before = template_hits(); + let classes: Vec<_> = (0..3) + .map(|_| scope.root_raw_mut_ptr(js_class_evaluation_object(cid, 6, 0) as *mut ObjectHeader)) + .collect(); + let protos: Vec<_> = classes + .iter() + .map(|c| { + let p = c.with_mut_ptr::(|c| unsafe { + super::class_object_props::class_object_prototype_value(c) + }); + assert!(p.is_pointer(), "a prototype object"); + scope.root_raw_mut_ptr(p.as_pointer::() as *mut ObjectHeader) + }) + .collect(); + let after = template_hits(); + assert_eq!( + after.0 - before.0, + 2, + "class objects 2 and 3 come from the template" + ); + assert_eq!( + after.1 - before.1, + 2, + "prototypes 2 and 3 come from the template" + ); + unsafe { + let shape = |h: &crate::gc::RuntimeHandle<'_>| { + h.with_mut_ptr::(|o| crate::object::shapes::object_shape_id(o)) + }; + let c = classes + .iter() + .map(|h| h.get_raw_mut_ptr::()) + .collect::>(); + let p = protos + .iter() + .map(|h| h.get_raw_mut_ptr::()) + .collect::>(); + assert!( + c[0] != c[1] && p[0] != p[1] && p[1] != p[2], + "one object per evaluation" + ); + assert_eq!(shape(&classes[0]), shape(&classes[1])); + assert_eq!(shape(&classes[1]), shape(&classes[2])); + assert_eq!(shape(&protos[0]), shape(&protos[1])); + assert_eq!(shape(&protos[1]), shape(&protos[2])); + for i in 0..3 { + let s = slot(c[i], b"s"); + assert!( + static_method_value_runs(s, info, c[i]), + "s of evaluation {i} is at home in it" + ); + let m = slot(p[i], b"m"); + assert!( + static_method_value_runs(m, info, c[i]), + "m of evaluation {i} is at home in it" + ); + assert_eq!( + slot(p[i], b"constructor"), + crate::value::js_nanbox_pointer(c[i] as i64).to_bits(), + "prototype {i}'s constructor is its class object" + ); + assert_eq!( + super::super::class_registry::class_object_own_field_bytes( + c[i], + super::class_object_props::CLASS_EVALUATION_PROTOTYPE_KEY, + ) + .map(f64::to_bits), + Some(crate::value::js_nanbox_pointer(p[i] as i64).to_bits()), + "class object {i} links its own prototype" + ); + } + assert_ne!( + slot(c[1], b"s"), + slot(c[2], b"s"), + "statics are per evaluation" + ); + assert_ne!( + slot(p[1], b"m"), + slot(p[2], b"m"), + "methods are per evaluation" + ); + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 00414edb95..38e25f04ca 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -1274,7 +1274,9 @@ pub(crate) fn get_field_by_name_object_tail( &mut proto_read_miss, ) { - return v; + if !super::class_object_template::evaluation_chain_lost_method(obj, key) { + return v; + } } } let key_bytes = std::slice::from_raw_parts( @@ -1308,7 +1310,10 @@ pub(crate) fn get_field_by_name_object_tail( ) { return v; } - if lookup_class_method_in_chain(class_id, name).is_some() { + if class_walk + && lookup_class_method_in_chain(class_id, name).is_some() + && !super::class_object_template::evaluation_chain_lost_method(obj, key) + { let heap_name = { let layout = std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) @@ -1685,7 +1690,12 @@ pub(crate) fn get_field_by_name_object_tail( receiver, &mut proto_read_miss, ) { - return v; + // An evaluation's prototype that lost one of the template's + // methods (`class_object_template`): the template's + // prototype, shared by every evaluation, must not answer. + if !super::class_object_template::evaluation_chain_lost_method(obj, key) { + return v; + } } } @@ -1738,7 +1748,10 @@ pub(crate) fn get_field_by_name_object_tail( // name still shadows it). Actual `obj.method(args)` calls don't flow // through here — they lower directly to `js_native_call_method`. if let Ok(name) = std::str::from_utf8(key_bytes) { - if lookup_class_method_in_chain(class_id, name).is_some() { + if class_walk + && lookup_class_method_in_chain(class_id, name).is_some() + && !super::class_object_template::evaluation_chain_lost_method(obj, key) + { // Allocate a fresh i8 buffer for the method name owned // by the closure. The keys_array's StringHeader bytes // could in theory be GC'd if the keys_array is not diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs index 3bec0f2136..f255ff2b03 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs @@ -282,7 +282,7 @@ pub(crate) fn private_member_set_by_name( /// being accessed was declared by an ancestor. Every ancestor evaluation whose /// constructor ran on the instance through `super()` is reachable from that /// stamp by the per-evaluation parent edge each fresh class object pins -/// (`js_class_object_pin_parent`), so walk it and answer with the ancestor +/// (`js_class_evaluation_object`), so walk it and answer with the ancestor /// evaluation belonging to `declaring_class_id`'s template. Comparing only the /// stamp rejected every legal `this.#x` in an inherited method when both /// classes are per-evaluation — function-local classes (#11127), and diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index f6582c279d..0926cf2808 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -368,10 +368,10 @@ pub(crate) use descriptor_state::{ pub(crate) use field_get_set::FieldLookupCaches; pub(crate) use field_get_set::{ class_object_default_to_string, class_object_registry_serves_static, - class_object_static_method_call, private_evaluation_brand_value, private_lexical_brand_pop, - private_lexical_brand_push, private_lexical_brand_stack_restore, - private_lexical_brand_stack_savepoint, private_member_access_hints_restore, - private_member_access_hints_savepoint, scan_private_lexical_brand_roots_mut, + private_evaluation_brand_value, private_lexical_brand_pop, private_lexical_brand_push, + private_lexical_brand_stack_restore, private_lexical_brand_stack_savepoint, + private_member_access_hints_restore, private_member_access_hints_savepoint, + scan_private_lexical_brand_roots_mut, }; #[cfg(test)] pub(crate) use this_binding::js_derived_super_scope_push; diff --git a/crates/perry-runtime/src/object/native_module/class_method_values.rs b/crates/perry-runtime/src/object/native_module/class_method_values.rs index 923f2ab62e..fe36f8475a 100644 --- a/crates/perry-runtime/src/object/native_module/class_method_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_method_values.rs @@ -3,6 +3,14 @@ pub(crate) fn class_evaluation_method_value_for_name( method_name: &str, evaluation_brand: f64, ) -> f64 { + // A template compiled with method entries gives each evaluation's + // prototype its own function object per method (`__eclo`): that + // is the evaluation's method value while the prototype still holds it. + if let Some(value) = + evaluation_prototype_method_value(owner_class_id, method_name, evaluation_brand) + { + return value; + } let cache_key = format!("#"); let cached = crate::object::js_object_get_own_field_or_undef( evaluation_brand, @@ -128,3 +136,29 @@ pub(crate) fn build_bound_method_closure( ) -> f64 { build_bound_method_closure_with_private_brand(instance, method_name_ptr, method_name_len, None) } + +/// The function object evaluation `brand` (a class object of template +/// `owner_class_id`) holds for method `name` in its prototype, while that +/// prototype's own `name` is still the declaration's entry-backed function at +/// home in `brand`. `None` for a template without method entries. +fn evaluation_prototype_method_value(owner_class_id: u32, name: &str, brand: f64) -> Option { + let code = class_registry::class_method_entry(owner_class_id, name)?; + if !class_registry::is_class_object_value(brand) { + return None; + } + let class = JSValue::from_bits(brand.to_bits()).as_pointer::(); + if class.is_null() || crate::object::js_object_get_class_id(class) != owner_class_id { + return None; + } + let proto = unsafe { crate::object::field_get_set::class_object_prototype_value(class) }; + if !proto.is_pointer() { + return None; + } + let class = JSValue::from_bits(brand.to_bits()).as_pointer::(); + let value = + class_registry::class_object_own_field_bytes(proto.as_pointer::(), name.as_bytes())?; + unsafe { + crate::object::field_get_set::static_method_value_runs(value.to_bits(), code, class) + } + .then_some(value) +} 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 8f7a1c9316..b281a3eec3 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -72,6 +72,7 @@ unsafe fn define_class_prototype_method(target_cid: u32, name: &str, value_bits: param_count, has_synthetic_arguments, has_rest, + entry: 0, }, ); drop(guard); diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 96ae4508c8..ec9f4c229c 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -3852,6 +3852,32 @@ unsafe fn prototype_serial(bits: u64) -> u64 { } } +/// The [[Prototype]] identity of an ordinary object of class `class_id` whose +/// meta record links prototype `bits` (NaN-boxed, or `TAG_NULL`): the rule +/// [`object_proto_id`] applies to a recorded prototype. `None` when that link +/// has no stable identity (a prototype with no serial, or a serial past the +/// mixed band), which `object_proto_id` answers with a fresh unique id. +/// +/// # Safety +/// `bits` is a live prototype value or `TAG_NULL`. +pub(crate) unsafe fn stable_linked_proto_id(class_id: u32, bits: u64) -> Option { + if bits == crate::value::TAG_NULL { + return Some(PROTO_ID_NULL); + } + let serial = prototype_serial(bits); + if serial == 0 { + return None; + } + let class = vtable_class(class_id); + if class == 0 { + return Some(serial); + } + if serial >= 1 << PROTO_ID_MIXED_SERIAL_BITS { + return None; + } + Some(PROTO_ID_MIXED | u64::from(class) << PROTO_ID_MIXED_SERIAL_BITS | serial) +} + /// `obj`'s [[Prototype]] identity, read off the object: what a mint with no /// lineage to copy stamps into the shape. Allocation-free. /// @@ -3880,21 +3906,8 @@ pub(crate) unsafe fn object_proto_id(obj: *const crate::object::ObjectHeader) -> let class_id = (*obj).class_id; let class = vtable_class(class_id); if !meta.is_null() && (*meta).prototype != 0 { - let bits = (*meta).prototype; - if bits == crate::value::TAG_NULL { - return PROTO_ID_NULL; - } - let serial = prototype_serial(bits); - if serial == 0 { - return fresh_unique_proto_id(); - } - if class == 0 { - return serial; - } - if serial >= 1 << PROTO_ID_MIXED_SERIAL_BITS { - return fresh_unique_proto_id(); - } - return PROTO_ID_MIXED | u64::from(class) << PROTO_ID_MIXED_SERIAL_BITS | serial; + return stable_linked_proto_id(class_id, (*meta).prototype) + .unwrap_or_else(fresh_unique_proto_id); } if class != 0 { return PROTO_ID_CLASS | u64::from(class); diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index 17127a063b..1af6cd6b9d 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -388,6 +388,96 @@ pub extern "C" fn js_static_method_entry_enter(class_id: u32, this_bits: u64) { static_this_arm(this.get_nanbox_f64()); } +/// Prologue of the closure-convention entry of a static method declared by a +/// per-evaluation class (`ClassExprFresh`): as [`js_static_method_entry_enter`], +/// except that the body runs in the evaluation the function object belongs +/// to. That evaluation is the function object's one capture, its home class +/// object (`define_class_object_own_properties`), so `const f = C.s; f()` and +/// `C.s.call(D)` read `C`'s environment, as `C.s()` does. A function object +/// without a home (the template's shared function object) runs as +/// [`js_static_method_entry_enter`] does. +#[cfg(feature = "keepalive-anchors")] +#[used(compiler)] +static KEEP_JS_STATIC_METHOD_ENTRY_ENTER_HOME: extern "C" fn(u32, u64, i64) = + js_static_method_entry_enter_home; + +#[no_mangle] +pub extern "C" fn js_static_method_entry_enter_home(class_id: u32, this_bits: u64, callee: i64) { + let home = if callee == 0 { + crate::value::TAG_UNDEFINED + } else { + crate::closure::js_closure_get_capture_bits( + callee as *const crate::closure::ClosureHeader, + 0, + ) + }; + let home = f64::from_bits(home); + let is_home = super::class_registry::is_class_object_value(home) + && super::js_object_get_class_id( + crate::value::JSValue::from_bits(home.to_bits()).as_pointer::(), + ) == class_id; + if !is_home { + js_static_method_entry_enter(class_id, this_bits); + return; + } + static_private_owner_push(home); + static_this_arm(f64::from_bits(this_bits)); +} + +/// Prologue of `__eclo`, the code of the function object a +/// per-evaluation class's prototype holds for one of its methods: the body +/// runs in the evaluation the function object belongs to, its home class +/// object (its one capture), as a call through that evaluation's method value +/// always has (`call_vtable_method_value`'s private brand). A function object +/// without a home runs in its receiver's evaluation. Returns the depth +/// [`js_class_method_entry_leave`] restores. +#[cfg(feature = "keepalive-anchors")] +#[used(compiler)] +static KEEP_JS_CLASS_METHOD_ENTRY_ENTER_HOME: extern "C" fn(u32, u64, i64) -> i64 = + js_class_method_entry_enter_home; + +#[no_mangle] +pub extern "C" fn js_class_method_entry_enter_home( + class_id: u32, + this_bits: u64, + callee: i64, +) -> i64 { + let home = if callee == 0 { + crate::value::TAG_UNDEFINED + } else { + crate::closure::js_closure_get_capture_bits( + callee as *const crate::closure::ClosureHeader, + 0, + ) + }; + let home = f64::from_bits(home); + let brand = if super::class_registry::is_class_object_value(home) + && super::js_object_get_class_id( + crate::value::JSValue::from_bits(home.to_bits()).as_pointer::(), + ) == class_id + { + home + } else { + super::private_evaluation_brand_value(f64::from_bits(this_bits)) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + let depth = derived_super_binding_stack_savepoint(); + super::private_lexical_brand_push(brand); + depth as i64 +} + +#[cfg(feature = "keepalive-anchors")] +#[used(compiler)] +static KEEP_JS_CLASS_METHOD_ENTRY_LEAVE: extern "C" fn(i64) = js_class_method_entry_leave; + +/// Epilogue of `__eclo`: drops what +/// [`js_class_method_entry_enter_home`] set up. +#[no_mangle] +pub extern "C" fn js_class_method_entry_leave(depth: i64) { + super::private_lexical_brand_pop(); + derived_super_binding_stack_restore(depth as usize); +} + /// 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. diff --git a/scripts/registry_lifetime_allowlist.json b/scripts/registry_lifetime_allowlist.json index 07d9666843..2376af4f9e 100644 --- a/scripts/registry_lifetime_allowlist.json +++ b/scripts/registry_lifetime_allowlist.json @@ -263,12 +263,6 @@ "verdict": "diagnostic_only", "why": "Attribute census counters, compiled only with the attr-census feature and armed by PERRY_ATTR_DIAG" }, - { - "file": "crates/perry-runtime/src/object/class_constructors.rs", - "name": "CLASS_CAPTURE_VALUES", - "verdict": "bounded_by_program", - "why": "Keyed by codegen class_id from js_class_register_capture_values at the declaration site; re-runs overwrite" - }, { "file": "crates/perry-runtime/src/object/class_env.rs", "name": "CLASS_ENVS", @@ -553,6 +547,18 @@ "name": "UNTRANSFERABLE_OBJECTS", "verdict": "open_leak", "why": "NaN-box bits of each object passed to markAsUntransferable: one per marked object, never unmarked" + }, + { + "file": "crates/perry-runtime/src/object/field_get_set/class_object_template.rs", + "name": "TEMPLATES", + "verdict": "bounded_by_program", + "why": "Per agent, keyed by per-evaluation class template id: the final class-object ShapeId and slot fills, recorded once per template (the records are external carriers)" + }, + { + "file": "crates/perry-runtime/src/object/field_get_set/class_object_template.rs", + "name": "PROTOTYPES", + "verdict": "bounded_by_program", + "why": "Per agent, keyed by per-evaluation class template id: the final prototype ShapeId and slot fills, recorded once per template (the record is an external carrier)" } ] } From c1e4afec2fbe35d2004898800ba2296d1cd52c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 11:09:27 +0000 Subject: [PATCH 7/8] tests: evaluations built from template shapes own their statics, prototype and methods --- .../tests/fresh_class_object_semantics.rs | 11 +++++- .../fresh_class_object_semantics/expected.txt | 11 ++++++ .../fresh_class_object_semantics/main.ts | 37 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/perry/tests/fresh_class_object_semantics.rs b/crates/perry/tests/fresh_class_object_semantics.rs index e6bcfb14eb..b06cb947eb 100644 --- a/crates/perry/tests/fresh_class_object_semantics.rs +++ b/crates/perry/tests/fresh_class_object_semantics.rs @@ -7,7 +7,11 @@ //! * the class object owns `length`, `name`, `prototype` and its static methods //! as real own properties, so reflection lists them and `delete` removes them //! from that evaluation's class alone (`own-*` lines); -//! * `String(C)` and `C.toString()` are the class source (`str-*` lines). +//! * `String(C)` and `C.toString()` are the class source (`str-*` lines); +//! * every evaluation after a template's first is built from the template's +//! class-object and prototype shapes, and still owns its statics, its +//! prototype and its methods: distinct per evaluation, at home in it, and a +//! delete or redefinition on one leaves the others alone (`tpl-*` lines). //! //! The program and node's output are the `fresh_class_object_semantics` //! fixture. The second run forces every minor collection to evacuate: the @@ -85,6 +89,11 @@ fn a_fresh_class_stringifies_to_its_source() { check(&run(&[]), "str-"); } +#[test] +fn evaluations_built_from_the_template_shapes_own_their_members() { + check(&run(&[]), "tpl-"); +} + #[test] fn the_whole_program_matches_node_while_every_minor_evacuates() { let stdout = run(&[ diff --git a/tests/fixtures/fresh_class_object_semantics/expected.txt b/tests/fixtures/fresh_class_object_semantics/expected.txt index 926b773b87..193dc1d798 100644 --- a/tests/fixtures/fresh_class_object_semantics/expected.txt +++ b/tests/fixtures/fresh_class_object_semantics/expected.txt @@ -25,3 +25,14 @@ str-method true true str-template true true str-proto true str-override custom1 custom1 custom1 +tpl-own ["length","name","prototype","s","who"] ["constructor","m","n"] +tpl-statics s1 s2 s3 false false s 0 +tpl-detached s2 s3 s2 s3 s3 +tpl-methods m1 m2 m3 3 b +tpl-method-identity false true n 1 function +tpl-proto true true true true false +tpl-desc {"writable":true,"enumerable":false,"configurable":true} {"writable":true,"enumerable":false,"configurable":true} +tpl-isolated undefined re s1 s4 +tpl-proto-isolated undefined function ["constructor","m","n"] ["constructor","m"] +tpl-loop sk39mk390 +tpl-accessor-isolated ["constructor","g","m"] m3 m2 undefined 3 diff --git a/tests/fixtures/fresh_class_object_semantics/main.ts b/tests/fixtures/fresh_class_object_semantics/main.ts index 9166bad740..2779676199 100644 --- a/tests/fixtures/fresh_class_object_semantics/main.ts +++ b/tests/fixtures/fresh_class_object_semantics/main.ts @@ -5,6 +5,8 @@ // ext-* a static `extends` of a fresh class reaches that evaluation // own-* own properties of a fresh class object, and `delete` // str-* the source text of a fresh class +// tpl-* many evaluations of one class: each after the first is built from +// the template's shapes, and owns its statics, prototype and methods // The factories hold a capturing class so the declarations are fresh today, // and a same-named class elsewhere so the heritage name is scope-renamed. @@ -120,3 +122,38 @@ function ov(tag: string) { } const O1: any = ov("1"); console.log("str-override", String(O1), O1.toString(), `${O1}`); +// ---- tpl: evaluations after the first are born in the template shapes ---- +function tp(tag: string, base: any) { + const t = tag; + class T extends base { + static s() { return "s" + t; } + static who() { return this.s(); } + m() { return "m" + t; } + n(a: number, b = 1) { return a + b; } + } + return T; +} +class B0 { b() { return "b"; } } +const T1: any = tp("1", B0), T2: any = tp("2", B0), T3: any = tp("3", B0); +console.log("tpl-own", JSON.stringify(Object.getOwnPropertyNames(T2)), JSON.stringify(Object.getOwnPropertyNames(T3.prototype))); +console.log("tpl-statics", T1.s(), T2.s(), T3.s(), T1.s === T2.s, T2.s === T3.s, T2.s.name, T2.s.length); +const g2 = T2.s, g3 = T3.s; +console.log("tpl-detached", g2(), g3(), T2.s.call(T3), T3.who(), T2.who.call(T3)); +console.log("tpl-methods", new T1().m(), new T2().m(), new T3().m(), new T3().n(2), new T3().b()); +console.log("tpl-method-identity", T1.prototype.m === T2.prototype.m, new T2().m === T2.prototype.m, + T3.prototype.n.name, T3.prototype.n.length, typeof T3.prototype.m); +console.log("tpl-proto", Object.getPrototypeOf(T2.prototype) === B0.prototype, T2.prototype.constructor === T2, + Object.getPrototypeOf(new T3()) === T3.prototype, new T3() instanceof B0, new T3() instanceof T2); +console.log("tpl-desc", JSON.stringify(Object.getOwnPropertyDescriptor(T3, "s")), JSON.stringify(Object.getOwnPropertyDescriptor(T3.prototype, "m"))); +delete T2.s; T3.s = () => "re"; +console.log("tpl-isolated", typeof T2.s, T3.s(), T1.s(), tp("4", B0).s()); +delete T2.prototype.n; +console.log("tpl-proto-isolated", typeof T2.prototype.n, typeof T3.prototype.n, + JSON.stringify(Object.getOwnPropertyNames(tp("5", B0).prototype)), JSON.stringify(Object.getOwnPropertyNames(T2.prototype))); +let acc = ""; for (let i = 0; i < 40; i++) { const K: any = tp("k" + i, B0); acc = K.s() + new K().m() + K.length; } +console.log("tpl-loop", acc); +function ta(tag: string) { const t = tag; class A { get g() { return t; } m() { return "m" + t; } } return A; } +const A1: any = ta("1"), A2: any = ta("2"); +delete A1.prototype.m; +const A3: any = ta("3"); +console.log("tpl-accessor-isolated", JSON.stringify(Object.getOwnPropertyNames(A3.prototype)), new A3().m(), new A2().m(), typeof A1.prototype.m, new A3().g); From 436dc759760a045b03fe210da5f9d671534ebe02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 11:09:27 +0000 Subject: [PATCH 8/8] changelog: fresh classes are built from their template shapes --- .../11780-fresh-class-object-semantics.md | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/changelog.d/11780-fresh-class-object-semantics.md b/changelog.d/11780-fresh-class-object-semantics.md index 144df4bc60..00d5b1cae4 100644 --- a/changelog.d/11780-fresh-class-object-semantics.md +++ b/changelog.d/11780-fresh-class-object-semantics.md @@ -1,9 +1,21 @@ -Fixed three gaps in classes that are created per evaluation (class expressions -in functions, and declarations whose evaluation has its own environment). +Fixed gaps in classes that are created per evaluation (class expressions in +functions, and declarations whose evaluation has its own environment), and made +creating them cheap. + `class D extends L` now extends the L of that evaluation instead of the shared class when the name of L is scope-renamed. A fresh class object now owns `length`, `name` and its static methods as real own properties, so `Object.getOwnPropertyNames`, `Object.hasOwn`, `in` and `Object.getOwnPropertyDescriptor` see them, and `delete C.s` removes the method -from that evaluation's class only. `String(C)`, `` `${C}` ``, `"" + C` and -`C.toString()` now return the class source text. +from that evaluation's class only. Each evaluation's static methods and +prototype methods are its own function objects, and they run in that +evaluation even when called detached (`const f = C.s; f()`) or with another +receiver. Deleting a method from one evaluation's prototype no longer removes +it from other evaluations or from classes evaluated later. `String(C)`, +`` `${C}` ``, `"" + C` and `C.toString()` now return the class source text. + +Every evaluation after a class's first is built directly in the class's +recorded shapes: its class object and its prototype are each allocated in +their final shape and filled, without per-property definitions. Evaluating a +class with a captured variable costs about 5,600 instructions instead of about +25,000, and each static method adds about 660 instead of about 19,800.