diff --git a/changelog.d/11780-fresh-class-object-semantics.md b/changelog.d/11780-fresh-class-object-semantics.md new file mode 100644 index 0000000000..00d5b1cae4 --- /dev/null +++ b/changelog.d/11780-fresh-class-object-semantics.md @@ -0,0 +1,21 @@ +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. 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. 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 659ba0adc2..80c58b8bfd 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -608,85 +608,56 @@ 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(); - // 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)]); - // #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)], + // Room for the evaluation's own `length`, `name` and static + // 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(); + // 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 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`. - // - // 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. + // #7154: the fresh class object is a raw SSA register while the + // 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); - // #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); // A named class expression's lexical self-binding is @@ -804,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 84c3c9d10e..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]); + 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 fedebdeaa9..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 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-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 { 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 0529c54037..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, - 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, }; 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 8fe6dc2edf..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) + 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) + 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) + 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 49964f336e..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,57 +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, template_class_id: u32) { +/// 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; + if obj.is_null() || template_class_id == 0 { + return obj; } - // 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); - if parent.to_bits() == TAG_UNDEFINED { - return; + 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 @@ -775,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, }, ); } @@ -848,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`. 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 cadaac35f0..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 @@ -684,6 +697,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/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/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..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; @@ -270,8 +274,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_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, + 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..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,66 +216,249 @@ 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)) } +/// 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'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 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), + ); + } +} + +/// Is `value` the method value `define_class_object_own_properties` stored for +/// 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, + owner: u32, + name: &str, + holder: *const ObjectHeader, +) -> bool { + // 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 +/// 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, owner, name, holder)); + } + 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 +} + +/// 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" +} + +/// `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 @@ -235,25 +490,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/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.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/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/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/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/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 56c930c465..0926cf2808 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_default_to_string, class_object_registry_serves_static, 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/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/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/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/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, 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/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, 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..b06cb947eb --- /dev/null +++ b/crates/perry/tests/fresh_class_object_semantics.rs @@ -0,0 +1,104 @@ +//! 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); +//! * 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 +//! 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 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(&[ + ("PERRY_GC_FORCE_EVACUATE", "1"), + ("PERRY_GC_POISON_FROMSPACE", "1"), + ]); + assert_eq!(EXPECTED, stdout); +} 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)" } ] } 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..193dc1d798 --- /dev/null +++ b/tests/fixtures/fresh_class_object_semantics/expected.txt @@ -0,0 +1,38 @@ +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 +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 new file mode 100644 index 0000000000..2779676199 --- /dev/null +++ b/tests/fixtures/fresh_class_object_semantics/main.ts @@ -0,0 +1,159 @@ +// 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 +// 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. + +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}`); +// ---- 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);