diff --git a/Cargo.lock b/Cargo.lock index 56cb11a139..fe47c4c86e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6291,6 +6291,7 @@ dependencies = [ "swc_common", "swc_ecma_ast", "swc_ecma_parser 32.0.0", + "swc_ecma_visit", "thiserror 1.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index 2623e8a948..d477a7be10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -343,6 +343,7 @@ not_unsafe_ptr_arg_deref = "allow" # SWC for TypeScript parsing swc_ecma_parser = "32.0" swc_ecma_ast = "19.0" +swc_ecma_visit = "19.0" swc_common = "18.0" swc_ecma_codegen = "21.0" swc_ecma_transforms_base = "32.0" diff --git a/changelog.d/8630-class-semantics-tail.md b/changelog.d/8630-class-semantics-tail.md new file mode 100644 index 0000000000..7a7a8ba89f --- /dev/null +++ b/changelog.d/8630-class-semantics-tail.md @@ -0,0 +1,3 @@ +Completed the remaining class-semantics tail: derived construction, per-evaluation +private elements, computed and static fields, native built-in subclasses, and +class reflection now follow JavaScript behavior consistently. diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index ebf038aa98..f994096e7d 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -758,6 +758,23 @@ pub(super) fn compile_closure( ); let v = blk.bitcast_i64_to_double(&bits); blk.store(DOUBLE, &v, &slot); + } else if let Some(class_id) = enclosing_class + .as_ref() + .and_then(|class_name| class_ids.get(class_name)) + .copied() + .filter(|class_id| *class_id != 0) + { + // Static field initialization substitutes lexical `this` with the + // class constructor and then drops the ordinary this-capture slot. + // `super.x` encodes its receiver implicitly, though, so there is no + // Expr::This node for that substitution to rewrite. Seed the + // closure's synthetic this slot with the enclosing ClassRef rather + // than the old 0.0 sentinel so arrows in static fields retain the + // class constructor as their SuperProperty receiver. + let class_ref = crate::nanbox::double_literal(f64::from_bits( + crate::nanbox::INT32_TAG | class_id as u64, + )); + blk.store(DOUBLE, &class_ref, &slot); } else { blk.store(DOUBLE, "0.0", &slot); } @@ -934,6 +951,18 @@ pub(super) fn compile_closure( this_stack, new_target_stack, class_stack, + super_called_stack: Vec::new(), + shared_super_scope_active: false, + lexical_this_uses_derived_binding: captures_this + && enclosing_class + .as_ref() + .and_then(|name| classes.get(name).copied()) + .is_some_and(|class| { + class.extends.is_some() + || class.extends_name.is_some() + || class.native_extends.is_some() + || class.extends_expr.is_some() + }), inline_ctor_return: Vec::new(), methods, module_globals, diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index f64e9036a5..b2886b6285 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -783,6 +783,9 @@ pub(super) fn compile_module_entry( pending_labels: Vec::new(), classes, this_stack: Vec::new(), + super_called_stack: Vec::new(), + shared_super_scope_active: false, + lexical_this_uses_derived_binding: false, inline_ctor_return: Vec::new(), new_target_stack: Vec::new(), class_stack: Vec::new(), @@ -1473,6 +1476,9 @@ pub(super) fn compile_module_entry( pending_labels: Vec::new(), classes, this_stack: Vec::new(), + super_called_stack: Vec::new(), + shared_super_scope_active: false, + lexical_this_uses_derived_binding: false, inline_ctor_return: Vec::new(), new_target_stack: Vec::new(), class_stack: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 3e70bd35c9..2378e506d0 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -1032,6 +1032,9 @@ pub(super) fn compile_function( pending_labels: Vec::new(), classes, this_stack: Vec::new(), + super_called_stack: Vec::new(), + shared_super_scope_active: false, + lexical_this_uses_derived_binding: false, inline_ctor_return: Vec::new(), new_target_stack: Vec::new(), class_stack: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index bc7c28e872..2362fc8d37 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -1552,6 +1552,22 @@ fn collect_inline_invoked_static_blocks( { out.insert((class_name.clone(), method_name.clone())); } + // `ClassExprFresh` invokes its static blocks directly from the + // per-evaluation source-order plan. Treat those calls as inline too; + // otherwise the module-init fallback below invokes every block once + // more with no fresh class object armed as `this`. + if let Expr::ClassExprFresh { + template, + static_init_order, + .. + } = e + { + for step in static_init_order { + if let perry_hir::ClassFreshStaticInit::Block(index) = step { + out.insert((template.clone(), format!("__perry_static_init_{index}"))); + } + } + } if let Expr::Closure { body, .. } = e { for s in body { walk_stmt(s, out); diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 4d834a46ce..aaf1046a02 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -485,6 +485,9 @@ pub(super) fn compile_method( pending_labels: Vec::new(), classes, this_stack: vec![this_slot], + super_called_stack: Vec::new(), + shared_super_scope_active: false, + lexical_this_uses_derived_binding: false, inline_ctor_return: Vec::new(), new_target_stack: Vec::new(), class_stack: vec![class.name.clone()], @@ -728,6 +731,14 @@ pub(super) fn compile_method( // as uninitialized register values (read as NaN-boxed undefined). let is_constructor_method = method.name == format!("{}_constructor", class.name); if is_constructor_method { + if class.extends.is_some() + || class.extends_name.is_some() + || class.native_extends.is_some() + || class.extends_expr.is_some() + { + crate::expr::this_super_call::push_shared_super_called_slot(&mut ctx); + ctx.shared_super_scope_active = true; + } // Stage field initializers around the parent body chain so leaf // fields can read state set by parent body (Refs #420): // - has extends: apply only ancestors here; self-fields apply @@ -953,7 +964,7 @@ pub(super) fn compile_method( } // Load `this` from the this_stack. let this_slot = ctx.this_stack.last().cloned(); - let this_box = if let Some(slot) = this_slot { + let this_box = if let Some(ref slot) = this_slot { ctx.block().load(DOUBLE, &slot) } else { undef_lit.clone() @@ -973,7 +984,20 @@ pub(super) fn compile_method( // real signature (see codegen/mod.rs). ctx.pending_declares .push((ctor_sym.clone(), DOUBLE, ctor_param_types)); - let _ = ctx.block().call(DOUBLE, &ctor_sym, &ctor_args); + let parent_result = ctx.block().call(DOUBLE, &ctor_sym, &ctor_args); + if let Some(this_slot) = this_slot { + let current_this = ctx.block().load(DOUBLE, &this_slot); + let bound_this = ctx.block().call( + DOUBLE, + "js_ctor_return_override", + &[ + (DOUBLE, ¤t_this), + (DOUBLE, &parent_result), + (crate::types::I32, "0"), + ], + ); + ctx.block().store(DOUBLE, &bound_this, &this_slot); + } } } } @@ -1009,7 +1033,16 @@ pub(super) fn compile_method( // .pathname` threw. Forward this synthesized ctor's params to the // runtime dynamic-parent super dispatcher, mirroring the explicit // `Expr::SuperCall` dynamic-parent path in `expr/this_super_call.rs`. - if builtin_parent_runtime.is_none() && class.extends_expr.is_some() { + let parent_is_uncallable_builtin = class + .extends_name + .as_deref() + .map(crate::expr::is_other_builtin_constructor_name) + .unwrap_or(false) + && class.extends_name.as_deref() != Some("SharedArrayBuffer"); + if builtin_parent_runtime.is_none() + && class.extends_expr.is_some() + && !parent_is_uncallable_builtin + { if let Some(cid) = ctx.class_ids.get(&class.name).copied().filter(|c| *c != 0) { let undef_lit = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); @@ -1049,7 +1082,7 @@ pub(super) fn compile_method( Some(slot) => ctx.block().load(DOUBLE, &slot), None => undef_lit.clone(), }; - let _ = ctx.block().call( + let parent_result = ctx.block().call( DOUBLE, "js_fetch_or_value_super", &[ @@ -1059,9 +1092,28 @@ pub(super) fn compile_method( (I64, &args_len), ], ); + if let Some(this_slot) = ctx.this_stack.last().cloned() { + let current_this = ctx.block().load(DOUBLE, &this_slot); + let bound_this = ctx.block().call( + DOUBLE, + "js_ctor_return_override", + &[ + (DOUBLE, ¤t_this), + (DOUBLE, &parent_result), + (crate::types::I32, "0"), + ], + ); + ctx.block().store(DOUBLE, &bound_this, &this_slot); + } } } + // The synthesized default derived constructor has now completed + // its implicit `super(...arguments)` path. Publish that fact to + // both this standalone function and any arrow closures before + // evaluating the class's own instance fields. + crate::expr::this_super_call::bind_derived_this_after_super(&mut ctx); + // Apply self field initializers AFTER the parent body chain has // run, so they can read state set by the parent body (e.g. drizzle's // PgText.enumValues = this.config.enumValues — this.config is set @@ -1102,6 +1154,31 @@ pub(super) fn compile_method( && !crate::lower_call::ctor_body_uses_this(&ctor.body)) && !crate::lower_call::ctor_body_has_value_return(&ctor.body) }); + // Standalone constructor symbols use the same internal completion slot as + // an inlined `new`: every explicit/bare return funnels to one block, where + // constructor return-override semantics are applied against the CURRENT + // `this` binding. This matters for a derived `super()` whose base returns + // a replacement object — an implicit/bare return must publish that object + // to the caller, not `undefined` (which would make the caller retain its + // original pre-super allocation). + let standalone_ctor_return = if is_constructor_method && !ctor_no_super_throw { + let result_slot = ctx.func.alloca_entry(DOUBLE); + let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + ctx.block().store(DOUBLE, &undef, &result_slot); + let after_idx = ctx.new_block("standalone.ctor.return.after"); + let target = crate::expr::InlineCtorReturn { + result_slot, + after_label: ctx.block_label(after_idx), + is_derived: class.extends.is_some() + || class.extends_name.is_some() + || class.native_extends.is_some() + || class.extends_expr.is_some(), + }; + ctx.inline_ctor_return.push(target.clone()); + Some((target, after_idx)) + } else { + None + }; if ctor_no_super_throw { ctx.block() .call(DOUBLE, "js_throw_reference_error_this_before_super", &[]); @@ -1119,8 +1196,39 @@ pub(super) fn compile_method( })?; } + if let Some((target, after_idx)) = standalone_ctor_return.as_ref() { + let _ = ctx + .inline_ctor_return + .pop() + .expect("standalone constructor return target"); + if !ctx.block().is_terminated() { + ctx.block().br(&target.after_label); + } + ctx.current_block = *after_idx; + } + if !ctx.block().is_terminated() { let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + let return_value = if let Some((target, _)) = standalone_ctor_return.as_ref() { + let raw = ctx.block().load(DOUBLE, &target.result_slot); + let this_value = ctx + .this_stack + .last() + .cloned() + .map(|slot| ctx.block().load(DOUBLE, &slot)) + .unwrap_or_else(|| undef.clone()); + crate::lower_call::emit_ctor_return_override( + &mut ctx, + &this_value, + &raw, + target.is_derived, + ) + } else { + undef.clone() + }; + if ctx.shared_super_scope_active { + ctx.block().call_void("js_derived_super_scope_pop", &[]); + } if method.is_async { let handle = ctx .block() @@ -1128,7 +1236,7 @@ pub(super) fn compile_method( let boxed = crate::expr::nanbox_pointer_inline_pub(ctx.block(), &handle); ctx.block().ret(DOUBLE, &boxed); } else { - ctx.block().ret(DOUBLE, &undef); + ctx.block().ret(DOUBLE, &return_value); } } let ic_globals = std::mem::take(&mut ctx.ic_globals); @@ -1600,6 +1708,9 @@ pub(super) fn compile_static_method( pending_labels: Vec::new(), classes, this_stack: vec![this_slot], + super_called_stack: Vec::new(), + shared_super_scope_active: false, + lexical_this_uses_derived_binding: false, inline_ctor_return: Vec::new(), new_target_stack: Vec::new(), // A static method's `this` is the class constructor (bound above to diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs index f759b4a06a..028675b562 100644 --- a/crates/perry-codegen/src/codegen/string_pool.rs +++ b/crates/perry-codegen/src/codegen/string_pool.rs @@ -1055,6 +1055,41 @@ pub(super) fn emit_string_pool( ], ); } + // Class refs are immediate values, not heap Function objects. Register + // each constructor's visible arity so the field-get path can reify the + // Function-compatible own `length` property. + let mut class_lengths: Vec<(u32, u32)> = classes + .iter() + .filter(|(class_name, class)| *class_name == &class.name && class.id != 0) + .filter_map(|(class_name, class)| { + let cid = class_ids.get(class_name).copied()?; + let length = class + .constructor + .as_ref() + .map(|ctor| { + ctor.params + .iter() + .take_while(|p| { + !p.is_rest && p.default.is_none() && !p.name.starts_with("__perry_cap_") + }) + .count() as u32 + }) + .unwrap_or(0); + Some((cid, length)) + }) + .collect(); + class_lengths.sort_unstable_by_key(|(cid, _)| *cid); + class_lengths.dedup_by_key(|(cid, _)| *cid); + for (cid, length) in class_lengths { + chunker.roll_if_full(); + chunker.current_block().call_void( + "js_register_class_length", + &[ + (crate::types::I32, &cid.to_string()), + (crate::types::I32, &length.to_string()), + ], + ); + } // Refs #486 (hono logger middleware): also register every class // getter in the runtime VTABLE_REGISTRY. Without this, cross-module diff --git a/crates/perry-codegen/src/collectors/refs.rs b/crates/perry-codegen/src/collectors/refs.rs index 98f5265036..c83d409453 100644 --- a/crates/perry-codegen/src/collectors/refs.rs +++ b/crates/perry-codegen/src/collectors/refs.rs @@ -906,15 +906,18 @@ pub fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet) { } Expr::ClassExprFresh { named_statics, - symbol_statics, + computed_keys, + computed_statics, captured_args, .. } => { for (_, v) in named_statics { walk(v, out); } - for (k, v) in symbol_statics { - walk(k, out); + for (_, key) in computed_keys { + walk(key, out); + } + for (_, v) in computed_statics { walk(v, out); } for a in captured_args { diff --git a/crates/perry-codegen/src/eh_mode.rs b/crates/perry-codegen/src/eh_mode.rs index 6e6a0aaa64..e5e4a2328d 100644 --- a/crates/perry-codegen/src/eh_mode.rs +++ b/crates/perry-codegen/src/eh_mode.rs @@ -90,6 +90,8 @@ pub(crate) fn callee_is_nothrow(name: &str) -> bool { | "js_get_exception" | "js_clear_exception" | "js_has_exception" + | "js_derived_super_scope_push" + | "js_derived_super_scope_pop" ) || !crate::module::helper_decl_attrs(name).is_empty() } diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 2a560a8616..74962321e7 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -99,6 +99,7 @@ pub(crate) fn builtin_parent_reserved_class_id(name: &str) -> Option { "Set" => 0xFFFF0023, "Array" => 0xFFFF0024, "ArrayBuffer" => 0xFFFF0025, + "SharedArrayBuffer" => 0xFFFF002E, "DataView" => 0xFFFF002B, "WeakMap" => 0xFFFF002C, "WeakSet" => 0xFFFF002D, @@ -464,6 +465,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // `ArrayBuffer` — runtime detects BufferHeader storage marked // with Perry's ArrayBuffer side registry. "ArrayBuffer" => 0xFFFF0025u32, + "SharedArrayBuffer" => 0xFFFF002Eu32, // WeakMap / WeakSet: real instances match via a runtime probe // (CLASS_ID_WEAKMAP/CLASS_ID_WEAKSET in weakref.rs, #5834) — // see the matching arm in perry-runtime/src/object/instanceof.rs. diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index c7f66d6609..5a278fb05d 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -880,14 +880,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } Expr::PrivateBrandCheck { class_name, + class_id: declaring_class_id, field_name, + kind, + is_static, object, } => { let obj = lower_expr(ctx, object)?; // The rooting window between these two operands is empty: // lowering `this` only reads the current binding and cannot GC. let brand_owner = lower_expr(ctx, &Expr::This)?; - let class_id = ctx.class_ids.get(class_name).copied().unwrap_or(0); + let class_id = if *declaring_class_id != 0 { + *declaring_class_id + } else { + ctx.class_ids.get(class_name).copied().unwrap_or(0) + }; let key_label = emit_string_literal_global(ctx, field_name); Ok(ctx.block().call( DOUBLE, @@ -898,6 +905,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (I32, &class_id.to_string()), (PTR, &key_label), (I32, &field_name.len().to_string()), + (I32, &kind.to_string()), + (I32, if *is_static { "1" } else { "0" }), ], )) } diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 5d58852af8..e19662636a 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -354,6 +354,16 @@ pub(crate) struct FnCtx<'a> { /// Stack of `this` slot pointers — set when lowering inside a class /// constructor body. `Expr::This` loads from the top entry. pub this_stack: Vec, + /// Per-inlined-constructor flag slots used by `super()`. A successful + /// super-constructor return binds derived `this` exactly once; a second + /// successful call must throw before instance elements run again. + pub super_called_stack: Vec, + /// The outermost standalone derived-constructor binding was also exposed + /// to nested arrow functions through the runtime binding stack. + pub shared_super_scope_active: bool, + /// This separately-emitted closure captures lexical `this` from a derived + /// constructor and must consult that constructor's shared TDZ cell. + pub lexical_this_uses_derived_binding: bool, /// Stack of lexical `new.target` slot pointers. Arrow closures that /// reference `new.target` capture the enclosing value here. pub new_target_stack: Vec, @@ -2183,7 +2193,7 @@ mod static_field_meta; mod static_method; mod string_regex_proc; mod super_method; -mod this_super_call; +pub(crate) mod this_super_call; pub(crate) use this_super_call::is_other_builtin_constructor_name; mod unary; mod url_main; diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 05462b0d91..15a0daaf3a 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -1391,7 +1391,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .get(&class_name) .map(|c| c.static_accessor_names.iter().any(|n| n == property)) .unwrap_or(false); - if receiver_class_is_proven && !is_static_accessor { + if receiver_class_is_proven + && !is_static_accessor + // A source `#name` registry entry is a PrivateName, not a + // String-named accessor. Public computed `obj["#name"]` + // must perform ordinary own-property lookup instead. + && !property.starts_with('#') + { if let Some(fn_name) = ctx.methods.get(&getter_key).cloned() { let recv_box = lower_expr(ctx, object)?; return Ok(ctx.block().call(DOUBLE, &fn_name, &[(DOUBLE, &recv_box)])); @@ -1917,7 +1923,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // prototype methods — every method reference returned // `undefined`. let method_key = (class_name.clone(), property.clone()); - if receiver_class_is_proven && ctx.methods.contains_key(&method_key) { + if receiver_class_is_proven + && !property.starts_with('#') + && ctx.methods.contains_key(&method_key) + { return lower_class_method_bind(ctx, object, property); } } diff --git a/crates/perry-codegen/src/expr/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs index 94b1860f30..1b598c2ac3 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -9,8 +9,7 @@ use perry_hir::Expr; use crate::nanbox::double_literal; use crate::rooting::{ - any_operand_may_collect, with_operands_rooted, with_rooted_accumulator, with_rooted_group, Arg, - Repr, + any_operand_may_collect, with_rooted_accumulator, with_rooted_group, Arg, Repr, }; use crate::types::{DOUBLE, I32, I64, PTR}; @@ -42,6 +41,10 @@ fn static_block_fns(ctx: &FnCtx<'_>, template: &str) -> Vec { .unwrap_or_default() } +fn private_static_storage_name(class_id: u32, field_name: &str) -> String { + format!("#") +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::StaticFieldGet { @@ -76,7 +79,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // (when the class ref is in an Any-typed local) find it. // Refs #420 / #618 followup. if let Some(&class_id) = ctx.class_ids.get(class_name) { - let idx = ctx.strings.intern(field_name); + let runtime_field_name = if field_name.starts_with('#') { + private_static_storage_name(class_id, field_name) + } else { + field_name.clone() + }; + let idx = ctx.strings.intern(&runtime_field_name); let entry = ctx.strings.entry(idx); let bytes_ref = format!("@{}", entry.bytes_global); let len_str = entry.byte_len.to_string(); @@ -425,7 +433,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::ClassExprFresh { template, named_statics, - symbol_statics, + computed_keys, + computed_statics, + static_init_order, captured_args, } => { let template_cid = ctx.class_ids.get(template).copied().unwrap_or(0); @@ -506,23 +516,28 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // narrow one. let protect_handle = !named_statics.is_empty() || !captured_args.is_empty() - || !symbol_statics.is_empty() + || !computed_keys.is_empty() + || !computed_statics.is_empty() || !block_fns.is_empty(); with_rooted_group(ctx, 1, |ctx, group| { let rooted = group.adopt_emitted(ctx, Repr::Ptr, &obj, protect_handle); - for (name, init) in named_statics { + // Resolve all ComputedPropertyNames before any static field + // initializer, preserving class-body order. Hidden own slots + // carry the resulting PropertyKeys for both the static phase + // below and later instance construction. + for (name, key_expr) in computed_keys { + let key_value = lower_expr(ctx, key_expr)?; let key_idx = ctx.strings.intern(name); let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); - let v = lower_expr(ctx, init)?; 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); + let storage_key = blk.load(DOUBLE, &key_handle_global); + let storage_bits = blk.bitcast_double_to_i64(&storage_key); + let storage_raw = blk.and(I64, &storage_bits, crate::nanbox::POINTER_MASK_I64); blk.call_void( "js_object_set_field_by_name", - &[(I64, &obj), (I64, &key_raw), (DOUBLE, &v)], + &[(I64, &obj), (I64, &storage_raw), (DOUBLE, &key_value)], ); } // #1787: snapshot the captured outer-scope values onto the class @@ -594,56 +609,72 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(I64, &obj), (I64, &key_raw), (DOUBLE, &caps_box)], ); } - for (key, init) in symbol_statics { - // #7154: `key` is lowered before `init`, so the Symbol sits in - // an SSA register across an arbitrary initializer — the same - // exposure the receiver has, one operand over. Root it. - // - // The inner scope is cut per iteration rather than by the - // group's own release: the setter this call may invoke is user - // code, and N statics would otherwise hold N slots across all - // of them. A release is a stack CUT, so the inner scope drops - // only what it pushed above the class object. - with_operands_rooted(ctx, &[key, init], |ctx, values| { - // #7154: both lowerings above can collect; re-derive the - // receiver from the root rather than reusing the register. - let obj = group.reread_emitted(ctx, rooted); - let obj_box = nanbox_pointer_inline(ctx.block(), &obj); - ctx.block().call( - DOUBLE, - "js_object_set_symbol_property", - &[ - (DOUBLE, &obj_box), - (DOUBLE, &values[0]), - (DOUBLE, &values[1]), - ], - ); - Ok(()) - })?; - } - // #685: run the class's `static { … }` blocks NOW — at the class - // expression's evaluation, with `this` = THIS fresh class object. - // The `ClassExprFresh` fast path previously never invoked them - // (they are also skipped by the module-init fallback when another - // evaluation site invokes them inline), so `return class { static - // { this.viaBlock = tag } }` factories produced objects whose - // blocks simply never ran. Arm the one-shot static-`this` - // override before each call so the compiled body's - // `js_static_this_resolve` prologue binds `this` to the fresh - // object (writes land as own properties of this evaluation's - // object, not the shared template). Blocks run after the named - // static fields above — the source interleaving of fields and - // blocks is not reproduced on this path (pre-existing limitation). - // - // `block_fns` is computed above, next to `protect_handle`. - for fn_name in block_fns { - // #7154: a static block runs arbitrary user code, so re-derive - // the receiver from the root before each one. - let obj = group.reread_emitted(ctx, rooted); - let obj_box = nanbox_pointer_inline(ctx.block(), &obj); - ctx.block() - .call_void("js_static_this_arm_value", &[(DOUBLE, &obj_box)]); - ctx.block().call(DOUBLE, &fn_name, &[]); + // Static fields and blocks execute only after every computed + // name has been resolved, then in their original ClassBody + // order. Each vector index is recorded by HIR lowering. + for step in static_init_order { + match step { + perry_hir::ClassFreshStaticInit::Named(index) => { + let Some((name, init)) = named_statics.get(*index as usize) else { + continue; + }; + let storage_name = if name.starts_with('#') { + private_static_storage_name(template_cid, name) + } else { + name.clone() + }; + let key_idx = ctx.strings.intern(&storage_name); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let value = lower_expr(ctx, init)?; + 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, &value)], + ); + } + perry_hir::ClassFreshStaticInit::Computed(index) => { + let Some((key_slot, init)) = computed_statics.get(*index as usize) + else { + continue; + }; + let value = lower_expr(ctx, init)?; + let key_idx = ctx.strings.intern(key_slot); + let entry = ctx.strings.entry(key_idx); + let key_bytes = format!("@{}", entry.bytes_global); + let key_len = entry.byte_len.to_string(); + let obj = group.reread_emitted(ctx, rooted); + let obj_box = nanbox_pointer_inline(ctx.block(), &obj); + let resolved_key = ctx.block().call( + DOUBLE, + "js_object_get_own_field_or_undef", + &[(DOUBLE, &obj_box), (PTR, &key_bytes), (I64, &key_len)], + ); + ctx.block().call( + DOUBLE, + "js_object_set_property_key", + &[ + (DOUBLE, &obj_box), + (DOUBLE, &resolved_key), + (DOUBLE, &value), + ], + ); + } + perry_hir::ClassFreshStaticInit::Block(index) => { + let Some(fn_name) = block_fns.get(*index as usize) else { + continue; + }; + let obj = group.reread_emitted(ctx, rooted); + let obj_box = nanbox_pointer_inline(ctx.block(), &obj); + ctx.block() + .call_void("js_static_this_arm_value", &[(DOUBLE, &obj_box)]); + ctx.block().call(DOUBLE, fn_name, &[]); + } + } } let obj = group.reread_emitted(ctx, rooted); let obj_box = nanbox_pointer_inline(ctx.block(), &obj); diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index 677dc3d390..e59b29af56 100644 --- a/crates/perry-codegen/src/expr/super_method.rs +++ b/crates/perry-codegen/src/expr/super_method.rs @@ -15,6 +15,7 @@ use super::{emit_string_literal_global, lower_expr, nanbox_pointer_inline, FnCtx pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::SuperMethodCall { method, args } => { + super::this_super_call::check_derived_this_initialized(ctx); // Find the current class from the class_stack. let Some(current_class_name) = ctx.class_stack.last().cloned() else { // No enclosing class — fall back to stub. @@ -200,6 +201,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // dynamic path (which handles both native-prototype and user-class // JS parents). Expr::SuperMethodCallSpread { method, args } => { + super::this_super_call::check_derived_this_initialized(ctx); use perry_hir::CallArg; let Some(current_class_name) = ctx.class_stack.last().cloned() else { for a in args { @@ -271,6 +273,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Call-form `super.method(...)` never reaches this arm — it // is lowered to `Expr::SuperMethodCall` in lower_call.rs. Expr::SuperPropertyGet { property } => { + super::this_super_call::check_derived_this_initialized(ctx); let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); let Some(current_class_name) = ctx.class_stack.last().cloned() else { return Ok(undef); @@ -301,9 +304,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .and_then(|p| ctx.class_ids.get(p)) .copied() .unwrap_or(0); - if parent_cid == 0 { - return Ok(undef); - } let recv_v = if let Some(this_slot) = ctx.this_stack.last().cloned() { ctx.block().load(DOUBLE, &this_slot) } else { @@ -358,6 +358,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { key, value, } => { + super::this_super_call::check_derived_this_initialized(ctx); let parent_cid = if *parent_class_id != 0 { *parent_class_id } else if let Some(parent_name) = parent_class_name { diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index e29581d8d7..f5131b78f0 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -9,13 +9,121 @@ use perry_hir::Expr; use crate::lower_call::{bind_inline_constructor_params, restore_inline_constructor_scope}; use crate::nanbox::{double_literal, POINTER_MASK_I64}; -use crate::types::{DOUBLE, I32, I64}; +use crate::types::{DOUBLE, I1, I32, I64, PTR}; use super::{ lower_array_super_init, lower_event_emitter_subclass_init, lower_expr, lower_node_stream_super_init, lower_stream_super_init, nanbox_pointer_inline, FnCtx, }; +/// Enter one derived constructor's `super()` binding scope. +pub(crate) fn push_super_called_slot(ctx: &mut FnCtx<'_>) { + let slot = ctx.func.alloca_entry(I1); + ctx.block().store(I1, "0", &slot); + ctx.super_called_stack.push(slot); +} + +/// Leave the constructor scope established by [`push_super_called_slot`]. +pub(crate) fn pop_super_called_slot(ctx: &mut FnCtx<'_>) { + ctx.super_called_stack.pop(); +} + +/// Enter a constructor scope whose binding must also be visible to arrows +/// compiled as separate LLVM functions. +pub(crate) fn push_shared_super_called_slot(ctx: &mut FnCtx<'_>) { + push_super_called_slot(ctx); + let slot = ctx + .super_called_stack + .last() + .cloned() + .expect("shared super binding slot"); + ctx.block() + .call_void("js_derived_super_scope_push", &[(PTR, &slot)]); +} + +pub(crate) fn pop_shared_super_called_slot(ctx: &mut FnCtx<'_>) { + ctx.block().call_void("js_derived_super_scope_pop", &[]); + pop_super_called_slot(ctx); +} + +/// Enforce the derived-constructor `this` TDZ before materializing a lexical +/// `this` value. The allocation used while running `super()` already exists, +/// but ECMAScript does not initialize the constructor's `this` binding until +/// that call returns successfully. +pub(crate) fn check_derived_this_initialized(ctx: &mut FnCtx<'_>) { + if let Some(slot) = ctx.super_called_stack.last().cloned() { + // While an inlined base-constructor body is running, its own `this` + // is initialized even though the outer derived constructor's binding + // is not. `class_stack` follows the body currently being lowered, so + // only apply the local cell to a derived body. + let current_body_is_derived = ctx + .class_stack + .last() + .and_then(|name| ctx.classes.get(name).copied()) + .is_some_and(|class| { + class.extends.is_some() + || class.extends_name.is_some() + || class.native_extends.is_some() + || class.extends_expr.is_some() + }); + if !current_body_is_derived { + return; + } + let initialized_idx = ctx.new_block("derived.this.initialized"); + let uninitialized_idx = ctx.new_block("derived.this.uninitialized"); + let initialized_label = ctx.block_label(initialized_idx); + let uninitialized_label = ctx.block_label(uninitialized_idx); + let initialized = ctx.block().load(I1, &slot); + ctx.block() + .cond_br(&initialized, &initialized_label, &uninitialized_label); + + ctx.current_block = uninitialized_idx; + ctx.block() + .call(DOUBLE, "js_throw_reference_error_this_before_super", &[]); + ctx.block().unreachable(); + + ctx.current_block = initialized_idx; + } else if ctx.lexical_this_uses_derived_binding { + // Arrows are emitted as separate LLVM functions and therefore cannot + // name the enclosing constructor's alloca. The runtime stack mirrors + // the active outer binding; outside a derived constructor this helper + // is deliberately a no-op. + let _ = ctx + .block() + .call(DOUBLE, "js_derived_this_check_current", &[]); + } +} + +/// Bind derived `this` after a successful parent-constructor return. The +/// parent is deliberately invoked before this check: a second `super()` runs +/// the base constructor, then throws `ReferenceError` while binding `this`, +/// and must not execute the derived class's fields a second time. +pub(crate) fn bind_derived_this_after_super(ctx: &mut FnCtx<'_>) { + let Some(slot) = ctx.super_called_stack.last().cloned() else { + // Arrow functions compile in their own FnCtx. If they lexically occur + // inside an inline derived constructor, bind that constructor's cell. + let _ = ctx + .block() + .call(DOUBLE, "js_derived_super_bind_current", &[]); + return; + }; + let duplicate_idx = ctx.new_block("super.bind.duplicate"); + let continue_idx = ctx.new_block("super.bind.continue"); + let duplicate_label = ctx.block_label(duplicate_idx); + let continue_label = ctx.block_label(continue_idx); + let already_called = ctx.block().load(I1, &slot); + ctx.block() + .cond_br(&already_called, &duplicate_label, &continue_label); + + ctx.current_block = duplicate_idx; + ctx.block() + .call(DOUBLE, "js_throw_reference_error_this_before_super", &[]); + ctx.block().unreachable(); + + ctx.current_block = continue_idx; + ctx.block().store(I1, "1", &slot); +} + /// Built-in constructor names (beyond Error/stream/fetch, which have their own /// SuperCall arms) that can appear as a class heritage. `super(...)` to these /// must NOT be routed through the runtime-value dispatch path @@ -64,6 +172,7 @@ pub(crate) fn is_other_builtin_constructor_name(name: &str) -> bool { pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::This => { + check_derived_this_initialized(ctx); if let Some(slot) = ctx.this_stack.last().cloned() { Ok(ctx.block().load(DOUBLE, &slot)) } else { @@ -180,6 +289,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (DOUBLE, &first), ], ); + bind_derived_this_after_super(ctx); crate::lower_call::apply_field_initializers_recursive( ctx, ¤t_class_name, @@ -208,6 +318,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_url_search_params_subclass_init", &[(DOUBLE, &this_box), (DOUBLE, &first)], ); + bind_derived_this_after_super(ctx); crate::lower_call::apply_field_initializers_recursive( ctx, ¤t_class_name, @@ -240,6 +351,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_dom_exception_subclass_init", &[(DOUBLE, &this_box), (DOUBLE, &message), (DOUBLE, &name)], ); + bind_derived_this_after_super(ctx); crate::lower_call::apply_field_initializers_recursive( ctx, ¤t_class_name, @@ -256,6 +368,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(I32, &cid_str), (DOUBLE, &this_box), (DOUBLE, &arr_box)], ); } + bind_derived_this_after_super(ctx); // Spec: subclass field initializers run AFTER super() returns // (mirrors every other super arm). crate::lower_call::apply_field_initializers_recursive( @@ -350,6 +463,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_url_search_params_subclass_init", &[(DOUBLE, &this_box), (DOUBLE, &init)], ); + bind_derived_this_after_super(ctx); crate::lower_call::apply_field_initializers_recursive( ctx, ¤t_class_name, @@ -511,7 +625,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // back to the ordinary implicit-`this`-bound // `js_native_call_value` (unchanged behavior for // every other runtime-value parent). - let _ = ctx.block().call( + let parent_result = ctx.block().call( DOUBLE, "js_fetch_or_value_super", &[ @@ -521,6 +635,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (I64, &args_len), ], ); + // A duplicate `super()` still evaluates/calls the + // parent, but it must throw before replacing the + // already-initialized derived `this` binding with + // the parent's second return object. + bind_derived_this_after_super(ctx); + // `super()` binds an object returned by a callable + // base constructor (for example a Proxy) as the + // derived `this`. A primitive return is ignored. + if let Some(this_slot) = ctx.this_stack.last().cloned() { + let current_this = ctx.block().load(DOUBLE, &this_slot); + let effective_this = crate::lower_call::emit_ctor_return_override( + ctx, + ¤t_this, + &parent_result, + false, + ); + ctx.block().store(DOUBLE, &effective_this, &this_slot); + } // Per JS spec: subclass field initializers run AFTER // super() returns. Same call the user-class branch makes @@ -547,6 +679,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { }; if let Some(kind) = node_stream_kind { let result = lower_node_stream_super_init(ctx, kind, super_args)?; + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -563,6 +696,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // would otherwise leave it length-less with no Array methods. if parent_name == "Array" { let result = lower_array_super_init(ctx, super_args)?; + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -586,6 +720,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { }; if let Some(kind) = stream_kind { let result = lower_stream_super_init(ctx, kind, super_args)?; + bind_derived_this_after_super(ctx); // Per JS spec field initializers run AFTER super() // returns. Without this, `this.foo = []` declared // on the subclass never executes — instance reads @@ -610,6 +745,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { }; if let Some(kind) = node_stream_kind { let result = lower_node_stream_super_init(ctx, kind, super_args)?; + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -656,6 +792,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (DOUBLE, &iterable), ], ); + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -679,6 +816,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), }; lower_event_emitter_subclass_init(ctx, &this_box); + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -734,6 +872,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (I32, &is_custom), ], ); + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -765,6 +904,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_dom_exception_subclass_init", &[(DOUBLE, &this_box), (DOUBLE, &arg0), (DOUBLE, &arg1)], ); + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -797,6 +937,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_promise_subclass_init", &[(DOUBLE, &this_box), (DOUBLE, &executor)], ); + bind_derived_this_after_super(ctx); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); crate::lower_call::apply_field_initializers_recursive( @@ -828,6 +969,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { runtime_fn, &[(DOUBLE, &this_box), (DOUBLE, &arg0), (DOUBLE, &arg1)], ); + bind_derived_this_after_super(ctx); // Per JS spec, subclass field initializers run after // super() returns (mirrors the stream/error arms above). let current_class_name = @@ -845,6 +987,86 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // so downstream `err.message` / `err.name` access works. // `instanceof Error` walking the extends chain is handled // elsewhere; this just makes `err.message` non-undefined. + if matches!( + parent_name.as_str(), + "ArrayBuffer" + | "SharedArrayBuffer" + | "DataView" + | "Boolean" + | "Number" + | "String" + | "Date" + | "RegExp" + | "Function" + | "BigInt" + | "Symbol" + | "Object" + | "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + ) { + let mut lowered_args = Vec::with_capacity(super_args.len()); + for arg in super_args { + lowered_args.push(lower_expr(ctx, arg)?); + } + let (args_ptr, args_len) = if lowered_args.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + let buf = ctx.func.alloca_entry_array(DOUBLE, lowered_args.len()); + for (index, value) in lowered_args.iter().enumerate() { + let slot = + ctx.block().gep(DOUBLE, &buf, &[(I64, &index.to_string())]); + ctx.block().store(DOUBLE, value, &slot); + } + let ptr = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr, + lowered_args.len(), + buf + )); + (ptr, lowered_args.len().to_string()) + }; + let class_id = ctx + .class_ids + .get(¤t_class_name) + .copied() + .unwrap_or(0) + .to_string(); + let name_idx = ctx.strings.intern(&parent_name); + let entry = ctx.strings.entry(name_idx); + let name_bytes = format!("@{}", entry.bytes_global); + let name_len = entry.byte_len.to_string(); + let constructed = ctx.block().call( + DOUBLE, + "js_builtin_subclass_construct", + &[ + (I32, &class_id), + (crate::types::PTR, &name_bytes), + (I64, &name_len), + (crate::types::PTR, &args_ptr), + (I64, &args_len), + ], + ); + bind_derived_this_after_super(ctx); + if let Some(this_slot) = ctx.this_stack.last().cloned() { + ctx.block().store(DOUBLE, &constructed, &this_slot); + } + crate::lower_call::apply_field_initializers_recursive( + ctx, + ¤t_class_name, + crate::lower_call::FieldInitMode::SelfOnly, + )?; + return Ok(constructed); + } let is_error_like = matches!( parent_name.as_str(), "Error" @@ -921,6 +1143,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } } } + bind_derived_this_after_super(ctx); return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); } }; @@ -955,6 +1178,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &this_box, &lowered_args, ); + bind_derived_this_after_super(ctx); // Spec: derived-class field initializers run AFTER `super()` // returns. The native base is the chain root and has no TS // fields, so everything after it still needs initializing — @@ -1021,6 +1245,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { effective_parent_class = gp_class; } + let mut super_binding_done = false; if let Some(parent_ctor) = &effective_parent_class.constructor { // The parent's synthesized `__perry_cap_*` params (a parent // class that captures enclosing locals) are NOT in the @@ -1085,9 +1310,63 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { parent_capture_fill, ); + let parent_is_derived = effective_parent_class.extends.is_some() + || effective_parent_class.extends_name.is_some() + || effective_parent_class.native_extends.is_some() + || effective_parent_class.extends_expr.is_some(); + let parent_result_slot = ctx.func.alloca_entry(DOUBLE); + ctx.block().store( + DOUBLE, + &double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), + &parent_result_slot, + ); + let parent_after_idx = ctx.new_block("super.parent.return.after"); + let parent_after_label = ctx.block_label(parent_after_idx); + ctx.inline_ctor_return.push(super::InlineCtorReturn { + result_slot: parent_result_slot, + after_label: parent_after_label.clone(), + is_derived: parent_is_derived, + }); ctx.class_stack.push(effective_parent_name.clone()); - crate::stmt::lower_stmts(ctx, &parent_ctor.body)?; + if parent_is_derived { + push_shared_super_called_slot(ctx); + } + // This body is inlined into the caller, but a `return` in the + // base constructor completes only that constructor. It must + // not pop any `try` handlers belonging to the source-level + // `super()` call site. Keep the caller's EH scope active for + // emitted invokes while making return cleanup relative to the + // inlined body itself. + let caller_try_depth = ctx.try_depth; + ctx.try_depth = 0; + let lower_result = crate::stmt::lower_stmts(ctx, &parent_ctor.body); + ctx.try_depth = caller_try_depth; + lower_result?; ctx.class_stack.pop(); + let parent_return = ctx + .inline_ctor_return + .pop() + .expect("super parent constructor return target"); + if !ctx.block().is_terminated() { + ctx.block().br(&parent_after_label); + } + ctx.current_block = parent_after_idx; + if parent_is_derived { + pop_shared_super_called_slot(ctx); + } + let parent_raw = ctx.block().load(DOUBLE, &parent_return.result_slot); + if let Some(this_slot) = ctx.this_stack.last().cloned() { + let inherited_this = ctx.block().load(DOUBLE, &this_slot); + let effective_this = crate::lower_call::emit_ctor_return_override( + ctx, + &inherited_this, + &parent_raw, + parent_return.is_derived, + ); + bind_derived_this_after_super(ctx); + super_binding_done = true; + ctx.block().store(DOUBLE, &effective_this, &this_slot); + } restore_inline_constructor_scope(ctx, saved_scope); } else if let Some(error_kind) = { @@ -1227,6 +1506,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Walk parent → ... → effective_parent_name (exclusive), // collect intermediate names. Apply SelfOnly for each in // root-most-first order, then for current_class_name. + if !super_binding_done { + bind_derived_this_after_super(ctx); + } let mut intermediates: Vec = Vec::new(); let mut walker = current_class.extends_name.as_deref().map(|s| s.to_string()); while let Some(pname) = walker { diff --git a/crates/perry-codegen/src/expr/write_barrier.rs b/crates/perry-codegen/src/expr/write_barrier.rs index 6a2a8c9f52..13b4c4091a 100644 --- a/crates/perry-codegen/src/expr/write_barrier.rs +++ b/crates/perry-codegen/src/expr/write_barrier.rs @@ -1022,13 +1022,9 @@ pub(crate) fn lower_node_stream_super_init( /// (mirrors `lower_node_stream_super_init` / the EventEmitter subclass init). pub(crate) fn lower_array_super_init(ctx: &mut FnCtx<'_>, super_args: &[Expr]) -> Result { let undef_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - let n = if let Some(first) = super_args.first() { - lower_expr(ctx, first)? - } else { - undef_lit.clone() - }; - for arg in super_args.iter().skip(1) { - let _ = lower_expr(ctx, arg)?; + let mut args = Vec::with_capacity(super_args.len()); + for arg in super_args { + args.push(lower_expr(ctx, arg)?); } let this_box = match ctx.this_stack.last().cloned() { @@ -1036,10 +1032,33 @@ pub(crate) fn lower_array_super_init(ctx: &mut FnCtx<'_>, super_args: &[Expr]) - None => undef_lit.clone(), }; + let (args_ptr, args_len) = if args.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + let buf = ctx.func.alloca_entry_array(DOUBLE, args.len()); + for (i, value) in args.iter().enumerate() { + let slot = ctx + .block() + .gep(DOUBLE, &buf, &[(crate::types::I64, &i.to_string())]); + ctx.block().store(DOUBLE, value, &slot); + } + let ptr = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr, + args.len(), + buf + )); + (ptr, args.len().to_string()) + }; ctx.block().call( DOUBLE, - "js_array_subclass_init", - &[(DOUBLE, &this_box), (DOUBLE, &n)], + "js_array_subclass_init_args", + &[ + (DOUBLE, &this_box), + (crate::types::PTR, &args_ptr), + (crate::types::I64, &args_len), + ], ); Ok(undef_lit) diff --git a/crates/perry-codegen/src/lower_call/console_promise.rs b/crates/perry-codegen/src/lower_call/console_promise.rs index b39f8b3479..e0240a0a19 100644 --- a/crates/perry-codegen/src/lower_call/console_promise.rs +++ b/crates/perry-codegen/src/lower_call/console_promise.rs @@ -877,6 +877,13 @@ pub fn try_lower_native_method_str_dispatch( | "isPrototypeOf" | "toLocaleString" | "valueOf" + // Function.prototype operations on a class constructor. + // ClassRefs are statically known classes, so without this + // carve-out `C.call/apply/bind(...)` skips runtime method + // dispatch and falls into the generic property-call path. + | "call" + | "apply" + | "bind" // Annex B §B.2.2 Object.prototype accessor helpers — handled // by `js_native_call_method`; the static class-dispatch tower // would read them as a non-callable property and throw diff --git a/crates/perry-codegen/src/lower_call/field_init.rs b/crates/perry-codegen/src/lower_call/field_init.rs index 16f0ae0d0d..c7d4a201d3 100644 --- a/crates/perry-codegen/src/lower_call/field_init.rs +++ b/crates/perry-codegen/src/lower_call/field_init.rs @@ -11,7 +11,7 @@ use perry_hir::{Expr, Stmt}; use crate::expr::{lower_expr, FnCtx}; use crate::nanbox::{double_literal, POINTER_MASK_I64}; -use crate::types::{DOUBLE, I32, I64}; +use crate::types::{DOUBLE, I32, I64, PTR}; /// The field name a constructor-prologue statement assigns from a plain /// parameter, or `None` if the statement is not of that shape. @@ -638,8 +638,8 @@ pub(crate) fn apply_field_initializers_recursive( .map(ctor_prologue_param_assigned_fields) .unwrap_or_default() }); - let mut init_pairs: Vec<(String, Expr)> = Vec::new(); - let mut init_pairs_computed: Vec<(Expr, Expr)> = Vec::new(); + let mut init_pairs: Vec<(String, Expr, bool)> = Vec::new(); + let mut init_pairs_computed: Vec<(String, Expr)> = Vec::new(); for field in &class_fields { // Wall 46: synthesized capture fields (`__perry_cap_*`) are populated // EXCLUSIVELY by the constructor's capture-param assignments — for a @@ -673,18 +673,82 @@ pub(crate) fn apply_field_initializers_recursive( None => Expr::Undefined, }; match &field.key_expr { - Some(key) => init_pairs_computed.push((key.clone(), init)), - None => init_pairs.push((field.name.clone(), init)), + Some(_) => init_pairs_computed.push((field.name.clone(), init)), + None => init_pairs.push((field.name.clone(), init, field.is_private)), } } - if init_pairs.is_empty() && init_pairs_computed.is_empty() { + let (class_has_private_elements, class_has_private_brand) = ctx + .classes + .get(&class_name_in_chain) + .copied() + .map(|class| { + ( + class.has_private_instance_elements(), + class.has_private_instance_brand(), + ) + }) + .unwrap_or((false, false)); + if init_pairs.is_empty() && init_pairs_computed.is_empty() && !class_has_private_elements { continue; } // Temporarily swap class_stack so `this.field` in the init // resolves against the correct class. ctx.class_stack.push(class_name_in_chain.clone()); - for (prop, init_expr) in init_pairs { + // Private methods/accessors are installed before fields and share a + // single per-class brand. Private fields are added individually below + // so their initializer ordering and duplicate check remain observable. + if class_has_private_brand { + let this_val = ctx + .this_stack + .last() + .cloned() + .map(|slot| ctx.block().load(DOUBLE, &slot)) + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + let class_id = ctx + .class_ids + .get(&class_name_in_chain) + .copied() + .unwrap_or(0) + .to_string(); + ctx.block().call( + DOUBLE, + "js_private_brand_add", + &[(DOUBLE, &this_val), (I32, &class_id)], + ); + } + for (prop, init_expr, is_private) in init_pairs { + if is_private { + let value = lower_expr(ctx, &init_expr)?; + let this_val = ctx + .this_stack + .last() + .cloned() + .map(|slot| ctx.block().load(DOUBLE, &slot)) + .unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let key_idx = ctx.strings.intern(&prop); + let key_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let key = ctx.block().load(DOUBLE, &key_global); + let class_id = ctx + .class_ids + .get(&class_name_in_chain) + .copied() + .unwrap_or(0) + .to_string(); + ctx.block().call( + DOUBLE, + "js_private_field_add", + &[ + (DOUBLE, &this_val), + (I32, &class_id), + (DOUBLE, &key), + (DOUBLE, &value), + ], + ); + continue; + } // Issue #263: arrow-function class fields like // `arrowField = () => this.value` need their reserved `this` // capture slot patched with the constructor's `this` AFTER @@ -738,42 +802,78 @@ pub(crate) fn apply_field_initializers_recursive( let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); 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, POINTER_MASK_I64); - let this_bits = blk.bitcast_double_to_i64(&this_val); - let this_raw = blk.and(I64, &this_bits, POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &this_raw), (I64, &key_raw), (DOUBLE, &closure_val)], + blk.call( + DOUBLE, + "js_class_field_add", + &[ + (DOUBLE, &this_val), + (DOUBLE, &key_box), + (DOUBLE, &closure_val), + ], ); continue; } - // Non-closure (or non-this-capturing closure) initializer: - // build a PropertySet { this, prop, init_expr } and lower - // through the existing path. - let set_expr = Expr::PropertySet { - object: Box::new(Expr::This), - property: prop, - value: Box::new(init_expr), - }; - let _ = lower_expr(ctx, &set_expr)?; + // DefineField uses CreateDataProperty semantics: an inherited + // setter must not run, while a Proxy receiver must observe its + // `defineProperty` trap. + let value = lower_expr(ctx, &init_expr)?; + let this_val = ctx + .this_stack + .last() + .cloned() + .map(|slot| ctx.block().load(DOUBLE, &slot)) + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + let key_idx = ctx.strings.intern(&prop); + let key_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let key = ctx.block().load(DOUBLE, &key_global); + ctx.block().call( + DOUBLE, + "js_class_field_add", + &[(DOUBLE, &this_val), (DOUBLE, &key), (DOUBLE, &value)], + ); } - // Computed-key fields: `[Parent.Symbol.X] = init` lowers to - // `this[Parent.Symbol.X] = init`. The key expression is evaluated - // at construction time per ES spec — `Object.defineProperty(this, k, …)` - // semantics through the IndexSet path. arrow-with-this-capture is + // Computed-key fields reuse the PropertyKey resolved once during + // ClassDefinitionEvaluation. DefineField uses CreateDataProperty + // semantics (including Proxy [[DefineOwnProperty]]), not assignment. + // arrow-with-this-capture is // unusual on a computed-key field; if it ever surfaces in real code // we extend this branch the same way the string-keyed loop above // does. - for (key_expr, init_expr) in init_pairs_computed { - let set_expr = Expr::IndexSet { - object: Box::new(Expr::This), - index: Box::new(key_expr), - value: Box::new(init_expr), - }; - let _ = lower_expr(ctx, &set_expr)?; + for (key_slot, init_expr) in init_pairs_computed { + let value = lower_expr(ctx, &init_expr)?; + let this_val = ctx + .this_stack + .last() + .cloned() + .map(|slot| ctx.block().load(DOUBLE, &slot)) + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + let class_id = ctx + .class_ids + .get(&class_name_in_chain) + .copied() + .unwrap_or(0) + .to_string(); + let key_idx = ctx.strings.intern(&key_slot); + let entry = ctx.strings.entry(key_idx); + let key_bytes = format!("@{}", entry.bytes_global); + let key_len = entry.byte_len.to_string(); + let key = ctx.block().call( + DOUBLE, + "js_class_computed_field_key", + &[ + (DOUBLE, &this_val), + (I32, &class_id), + (PTR, &key_bytes), + (I64, &key_len), + ], + ); + ctx.block().call( + DOUBLE, + "js_class_field_add", + &[(DOUBLE, &this_val), (DOUBLE, &key), (DOUBLE, &value)], + ); } ctx.class_stack.pop(); } diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index bff4d52057..f7d3a34af3 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -72,6 +72,7 @@ mod new; pub(crate) mod new_alloc; mod new_ctor_args; mod new_helpers; +pub(crate) use new_helpers::emit_ctor_return_override; mod omitted_native_params; mod options; /// `pub(crate)` so `type_analysis` can reuse the exact receiver/method diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index cc56b845af..4576bcf463 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -587,7 +587,14 @@ fn lower_new_impl_inner<'a>( // with `super(...)`/rest params) round-trip correctly through the call. let force_ctor_call = std::env::var_os("PERRY_INLINE_CTOR").is_none() && class.constructor.is_some() - && local_constructor_symbol_exists(ctx, class); + && local_constructor_symbol_exists(ctx, class) + // These bases create an exotic object in `super()`. Keep their own + // constructors inline so the replacement derived-this value remains + // authoritative at the surrounding `new` expression. + && !class + .extends_name + .as_deref() + .is_some_and(crate::expr::is_other_builtin_constructor_name); if ctx.class_stack.iter().any(|active| active == class_name) || ctor_alias_collision || force_ctor_call @@ -915,7 +922,7 @@ fn lower_new_impl_inner<'a>( } else { ctx.block().store(DOUBLE, &obj_box, &this_slot); } - ctx.this_stack.push(this_slot); + ctx.this_stack.push(this_slot.clone()); ctx.class_stack.push(class_name.to_string()); // #2768/new.target: `new C()` is fully inlined here, so the runtime @@ -1114,6 +1121,9 @@ fn lower_new_impl_inner<'a>( || class.extends_name.is_some() || class.native_extends.is_some() || class.extends_expr.is_some(); + if is_derived_class { + crate::expr::this_super_call::push_shared_super_called_slot(ctx); + } // A closure-captured `super()` may run during construction, so it // suppresses the static throw — but only when the body never touches // `this` directly (a direct `this` in a no-direct-super derived ctor @@ -1128,8 +1138,20 @@ fn lower_new_impl_inner<'a>( .call(DOUBLE, "js_throw_reference_error_this_before_super", &[]); ctx.block().unreachable(); } else { - // Lower the constructor body. Errors propagate. - crate::stmt::lower_stmts(ctx, &class.constructor.as_ref().unwrap().body)?; + // A constructor body is inlined into the surrounding function. + // Its `return` completes the constructor, not an enclosing + // source-level `try`, so return cleanup must count only handlers + // opened by the inlined body. The caller's LLVM EH scope remains + // active and still receives every throwing invoke. + let caller_try_depth = ctx.try_depth; + ctx.try_depth = 0; + let lower_result = + crate::stmt::lower_stmts(ctx, &class.constructor.as_ref().unwrap().body); + ctx.try_depth = caller_try_depth; + lower_result?; + } + if is_derived_class { + crate::expr::this_super_call::pop_shared_super_called_slot(ctx); } // Restore the enclosing function's local scope. @@ -1170,7 +1192,70 @@ fn lower_new_impl_inner<'a>( ctx.class_stack.pop(); ctx.class_stack.push(pname.to_string()); - crate::stmt::lower_stmts(ctx, &parent_ctor.body)?; + // The inherited body is the `super(...args)` half of this + // class's implicit default derived constructor. A + // `return ` in that ANCESTOR replaces the value of + // `this`, but it does not complete the leaf constructor: + // the leaf's instance fields and private elements still + // have to be installed on the replacement object. + // + // Reusing the leaf's inline-return target made the parent + // return branch straight to the end of `new C(...)`. + // Besides skipping the leaf initializers, subsequent + // lowering happened in an already-terminated block and + // produced references to SSA names that were never + // emitted. Give the parent body its own completion slot, + // apply its constructor return-override here, and publish + // the resulting `this` through the rooted this-slot before + // continuing with the leaf initialization. + let parent_result_slot = ctx.func.alloca_entry(DOUBLE); + ctx.block().store( + DOUBLE, + &double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), + &parent_result_slot, + ); + let parent_after_idx = ctx.new_block("inherited.ctor.return.after"); + let parent_after_label = ctx.block_label(parent_after_idx); + let parent_is_derived = parent_class.extends.is_some() + || parent_class.extends_name.is_some() + || parent_class.native_extends.is_some() + || parent_class.extends_expr.is_some(); + ctx.inline_ctor_return.push(crate::expr::InlineCtorReturn { + result_slot: parent_result_slot, + after_label: parent_after_label.clone(), + is_derived: parent_is_derived, + }); + if parent_is_derived { + crate::expr::this_super_call::push_shared_super_called_slot(ctx); + } + let caller_try_depth = ctx.try_depth; + ctx.try_depth = 0; + let lower_result = crate::stmt::lower_stmts(ctx, &parent_ctor.body); + ctx.try_depth = caller_try_depth; + lower_result?; + if parent_is_derived { + crate::expr::this_super_call::pop_shared_super_called_slot(ctx); + } + let parent_return = ctx + .inline_ctor_return + .pop() + .expect("inherited constructor return target"); + if !ctx.block().is_terminated() { + ctx.block().br(&parent_after_label); + } + ctx.current_block = parent_after_idx; + let parent_raw = ctx.block().load(DOUBLE, &parent_return.result_slot); + let inherited_this = ctx.block().load(DOUBLE, &this_slot); + let effective_this = super::new_helpers::emit_ctor_return_override( + ctx, + &inherited_this, + &parent_raw, + parent_return.is_derived, + ); + ctx.block().store(DOUBLE, &effective_this, &this_slot); + if instance.protected { + crate::expr::root_entry_alloca(ctx, &this_slot); + } // Restore class_stack to the child. ctx.class_stack.pop(); @@ -1178,26 +1263,11 @@ fn lower_new_impl_inner<'a>( restore_inline_constructor_scope(ctx, saved_scope); - // Apply the field initializers of every class BELOW the - // inherited-ctor class — the leaf and any intermediates — - // now that the parent ctor body has run (the post-super() - // step, mirroring the own-ctor path's SelfOnly-after). The - // up-front pass above used `UpToInclusive(inherited)`, which - // keeps `chain[0..=idx(inherited)]` and therefore EXCLUDES - // the leaf, so without this a no-own-ctor subclass's own - // field initializers never ran — e.g. zod's - // `class ZodObject extends ZodType { private _cached = null }` - // left `_cached` at the raw-0 slot, so `_getCached()`'s - // `this._cached !== null` was true (0 !== null) and returned - // 0; `_parse` then destructured `{ keys }` off 0, iterated - // nothing, and every `z.object({...}).parse()` dropped all - // fields. - apply_field_initializers_recursive( - ctx, - class_name, - FieldInitMode::BetweenExclusiveTo(pname.to_string()), - )?; - + // The shared post-constructor tail below installs every + // class below this inherited constructor exactly once. + // Keeping a second copy here was harmless for ordinary + // assignment-like fields, but became observable as a + // duplicate private-element installation. found_inherited_ctor = true; break; // Found and inlined the parent ctor. } @@ -1257,6 +1327,66 @@ fn lower_new_impl_inner<'a>( found_inherited_ctor = true; } } + // The remaining native builtins require real exotic instances rather + // than state stamped onto Perry's initially allocated plain object. + // Invoke their [[Construct]] with this class as newTarget and replace + // the derived `this` binding with the returned branded value. + if !found_inherited_ctor && !has_imported_ctor { + if let Some(parent) = class.extends_name.as_deref().filter(|name| { + matches!( + *name, + "ArrayBuffer" + | "SharedArrayBuffer" + | "DataView" + | "Boolean" + | "Number" + | "String" + | "Date" + | "RegExp" + | "Function" + | "BigInt" + | "Symbol" + | "Object" + | "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + ) + }) { + lowered_args = refresh_rooted_args(ctx, group)?; + let (args_ptr, args_len) = lower_js_args_array(ctx, &lowered_args); + let class_id = ctx + .class_ids + .get(class_name) + .copied() + .unwrap_or(0) + .to_string(); + let name_idx = ctx.strings.intern(parent); + let entry = ctx.strings.entry(name_idx); + let name_bytes = format!("@{}", entry.bytes_global); + let name_len = entry.byte_len.to_string(); + let constructed = ctx.block().call( + DOUBLE, + "js_builtin_subclass_construct", + &[ + (I32, &class_id), + (PTR, &name_bytes), + (I64, &name_len), + (PTR, &args_ptr), + (I64, &args_len), + ], + ); + ctx.block().store(DOUBLE, &constructed, &this_slot); + found_inherited_ctor = true; + } + } // Issue #573: if the parent walk reached an Error-like built-in // without finding any user-class constructor, synthesize the JS // spec default ctor `constructor(...args) { super(...args); }` — @@ -1611,7 +1741,12 @@ fn lower_new_impl_inner<'a>( .extends_name .as_deref() .map(crate::expr::is_other_builtin_constructor_name) - .unwrap_or(false); + .unwrap_or(false) + // SharedArrayBuffer construction now returns a real branded + // buffer and honors the subclass newTarget/prototype in the + // runtime dispatcher. It must run rather than retaining Perry's + // provisional plain-object receiver. + && class.extends_name.as_deref() != Some("SharedArrayBuffer"); if !found_inherited_ctor && class.extends_expr.is_some() && !parent_is_uncallable_builtin { if let Some(cid) = ctx.class_ids.get(class_name).copied().filter(|c| *c != 0) { let parent_val = ctx.block().call( @@ -1646,7 +1781,7 @@ fn lower_new_impl_inner<'a>( // outer function's `this` (or undef at module scope). Use // `obj_box` — the freshly-allocated object — directly. let this_box = obj_box.clone(); - let _ = ctx.block().call( + let parent_result = ctx.block().call( DOUBLE, "js_fetch_or_value_super", &[ @@ -1656,6 +1791,19 @@ fn lower_new_impl_inner<'a>( (I64, &args_len), ], ); + // A function-valued base constructor can return a replacement + // object (notably a Proxy). The implicit derived constructor + // binds that object as `this`; primitives retain the allocation. + // Keep the rooted slot authoritative so field initialization and + // the final `new` result both use the replacement. + let current_this = ctx.block().load(DOUBLE, &this_slot); + let effective_this = super::new_helpers::emit_ctor_return_override( + ctx, + ¤t_this, + &parent_result, + false, + ); + ctx.block().store(DOUBLE, &effective_this, &this_slot); } } } @@ -1708,11 +1856,35 @@ fn lower_new_impl_inner<'a>( apply_field_initializers_recursive(ctx, class_name, FieldInitMode::AfterRoot)?; } } + // Close the inline constructor's control flow before emitting anything + // that consumes the constructed receiver. An explicit `return` has + // already terminated the body block and branched to `after_idx`; emitting + // a receiver reload while that terminated block is still current only + // manufactures an SSA name with no defining instruction (invalid LLVM). + let inline_return = ctx.inline_ctor_return.pop(); + if let Some(ret) = inline_return.as_ref() { + if !ctx.block().is_terminated() { + ctx.block().br(&ret.after_label); + } + ctx.current_block = after_idx; + } + // #7154: same re-read as the standalone-symbol path above. The inlined // constructor body (field initializers, `super(...)`, nested `new`s) can // reach a back-edge poll, and the evacuating minor there relocates the // instance out from under `obj_handle`/`obj_box`. - let (obj_handle, obj_box) = reload_instance(ctx, group, &instance, &obj_handle, &obj_box); + let (obj_handle, obj_box) = if instance.protected { + // `super()` is allowed to replace `this` (an ancestor constructor may + // return an object). The rooted this-slot is the authoritative value + // after constructor execution; the allocation root still names the + // original leaf allocation in that case. + let boxed = ctx.block().load(DOUBLE, &this_slot); + let bits = ctx.block().bitcast_double_to_i64(&boxed); + let handle = ctx.block().and(I64, &bits, POINTER_MASK_I64); + (handle, boxed) + } else { + reload_instance(ctx, group, &instance, &obj_handle, &obj_box) + }; emit_typed_shape_layout_init(ctx, class_name, &obj_handle); // Close the inline-constructor return: fall through (or branch) to the @@ -1721,11 +1893,7 @@ fn lower_new_impl_inner<'a>( // (initial value) or the raw value from an explicit `return`. The override // runs HERE (outside any `try` in the body) so a derived ctor's // `try { return ; } catch {}` still throws uncaught. - let final_box = if let Some(ret) = ctx.inline_ctor_return.pop() { - if !ctx.block().is_terminated() { - ctx.block().br(&ret.after_label); - } - ctx.current_block = after_idx; + let final_box = if let Some(ret) = inline_return { let raw = ctx.block().load(DOUBLE, &ret.result_slot); super::new_helpers::emit_ctor_return_override(ctx, &obj_box, &raw, ret.is_derived) } else { diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs index 16dc4072c7..1ba9e15d68 100644 --- a/crates/perry-codegen/src/lower_call/new_helpers.rs +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -34,8 +34,11 @@ use crate::types::{DOUBLE, I32}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum NativeInstanceBase { EventEmitter, + Array, Map, Set, + WeakMap, + WeakSet, Event, CustomEvent, DomException, @@ -53,8 +56,11 @@ pub(crate) enum NativeInstanceBase { pub(crate) fn native_instance_base(name: &str) -> Option { match name { "EventEmitter" => Some(NativeInstanceBase::EventEmitter), + "Array" | "ReadonlyArray" => Some(NativeInstanceBase::Array), "Map" => Some(NativeInstanceBase::Map), "Set" => Some(NativeInstanceBase::Set), + "WeakMap" => Some(NativeInstanceBase::WeakMap), + "WeakSet" => Some(NativeInstanceBase::WeakSet), "Event" => Some(NativeInstanceBase::Event), "CustomEvent" => Some(NativeInstanceBase::CustomEvent), "DOMException" => Some(NativeInstanceBase::DomException), @@ -123,6 +129,35 @@ pub(crate) fn emit_native_instance_base_init( // (already lowered for their side effects) are not forwarded. crate::expr::lower_event_emitter_subclass_init(ctx, this_box); } + NativeInstanceBase::Array => { + let n = lowered_args.len(); + let (args_ptr, args_len) = if n == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, value) in lowered_args.iter().enumerate() { + let slot = + ctx.block() + .gep(DOUBLE, &buf, &[(crate::types::I64, &i.to_string())]); + ctx.block().store(DOUBLE, value, &slot); + } + let ptr = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr, n, buf + )); + (ptr, n.to_string()) + }; + ctx.block().call( + DOUBLE, + "js_array_subclass_init_args", + &[ + (DOUBLE, this_box), + (crate::types::PTR, &args_ptr), + (crate::types::I64, &args_len), + ], + ); + } NativeInstanceBase::Map | NativeInstanceBase::Set => { let kind: i32 = if base == NativeInstanceBase::Map { 0 @@ -142,6 +177,23 @@ pub(crate) fn emit_native_instance_base_init( ], ); } + NativeInstanceBase::WeakMap | NativeInstanceBase::WeakSet => { + let kind = if base == NativeInstanceBase::WeakMap { + 0 + } else { + 1 + }; + let iterable = lowered_args.first().cloned().unwrap_or(undef); + ctx.block().call( + DOUBLE, + "js_weak_collection_subclass_init", + &[ + (DOUBLE, this_box), + (I32, &kind.to_string()), + (DOUBLE, &iterable), + ], + ); + } NativeInstanceBase::Event | NativeInstanceBase::CustomEvent => { let arg0 = lowered_args .first() @@ -611,7 +663,7 @@ pub(crate) fn ctor_chain_uses_new_target(ctx: &FnCtx<'_>, class: &Class) -> bool /// the runtime's answer. Skipping the call cannot lose a relocation either: /// `js_ctor_return_override` is in `root_reload`'s no-reload set, so no live /// value's address depends on having made it. -pub(super) fn emit_ctor_return_override( +pub(crate) fn emit_ctor_return_override( ctx: &mut FnCtx<'_>, obj_box: &str, ctor_ret: &str, diff --git a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs index c05fac8970..d49de12ce3 100644 --- a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs +++ b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs @@ -367,6 +367,17 @@ pub(crate) fn try_lower_static_dispatch( // class_id registries. `resolve_static_dispatch_cls` already gated // these on known-class membership, so reaching here means the receiver // really is a class. + // These are inherited Function/Object prototype operations, not class + // statics. Let the normal method-call tower route them to + // `js_native_call_method`; the class-static miss helper deliberately + // returns its receiver unchanged, which made `C.call()` silently + // succeed and made `C.bind()` lose its bound arguments. + if matches!( + property, + "bind" | "call" | "apply" | "isPrototypeOf" | "toString" + ) { + return Ok(None); + } let receiver_is_dispatchable_class = matches!(object, Expr::ClassRef(_)) || matches!(object, Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name)) || matches!(object, Expr::PropertyGet { object: inner, property, .. } 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 0647ceee3f..df1c5204d1 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 @@ -368,6 +368,8 @@ pub(crate) fn declare_core(module: &mut LlModule) { // dispatch through CLASS_VTABLE_REGISTRY instead of returning undefined. module.declare_function("js_class_method_bind", DOUBLE, &[DOUBLE, I64, I64]); module.declare_function("js_class_method_bind_by_id", DOUBLE, &[DOUBLE, I64]); + module.declare_function("js_class_lexical_binding_get", DOUBLE, &[DOUBLE]); + module.declare_function("js_class_lexical_binding_set", DOUBLE, &[DOUBLE, DOUBLE]); module.declare_function("js_class_prototype_method_value", DOUBLE, &[DOUBLE, DOUBLE]); // #519: read the implicit `this` thread-local set by // `js_native_call_method`'s field-scan dispatch when invoking a @@ -385,6 +387,10 @@ pub(crate) fn declare_core(module: &mut LlModule) { module.declare_function("js_ctor_return_override", DOUBLE, &[DOUBLE, DOUBLE, I32]); module.declare_function("js_new_target_get", DOUBLE, &[]); module.declare_function("js_new_target_set", DOUBLE, &[DOUBLE]); + module.declare_function("js_derived_super_scope_push", VOID, &[PTR]); + module.declare_function("js_derived_super_scope_pop", VOID, &[]); + module.declare_function("js_derived_super_bind_current", DOUBLE, &[]); + module.declare_function("js_derived_this_check_current", DOUBLE, &[]); // ========== Runtime init / module loader ========== module.declare_function("js_get_export", DOUBLE, &[I64, I64, I64]); diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs index a31fa4b841..a35162b2b7 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs @@ -3,13 +3,19 @@ //! (extracted from stdlib_ffi.rs). use crate::module::LlModule; -use crate::types::{DOUBLE, I32, I64, VOID}; +use crate::types::{DOUBLE, I32, I64, PTR, VOID}; pub(crate) fn declare_streams_events(module: &mut LlModule) { // ========== node:stream stubs (issue #631) ========== module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE]); // #5137 EE subclass init module.declare_function("js_array_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Array + module.declare_function("js_array_subclass_init_args", DOUBLE, &[DOUBLE, PTR, I64]); module.declare_function("js_map_set_subclass_init", DOUBLE, &[DOUBLE, I32, DOUBLE]); // class extends Map/Set + module.declare_function( + "js_weak_collection_subclass_init", + DOUBLE, + &[DOUBLE, I32, DOUBLE], + ); module.declare_function("js_promise_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Promise module.declare_function("js_node_stream_readable_new", DOUBLE, &[DOUBLE]); module.declare_function( diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 380cb702b6..4fa4fc7a37 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -417,13 +417,25 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function( "js_private_brand_check", DOUBLE, - &[DOUBLE, DOUBLE, I32, PTR, I32], + &[DOUBLE, DOUBLE, I32, PTR, I32, I32, I32], ); module.declare_function( "js_private_guard", DOUBLE, &[DOUBLE, DOUBLE, I32, PTR, I32, I32, I32], ); + module.declare_function("js_private_brand_add", DOUBLE, &[DOUBLE, I32]); + module.declare_function( + "js_private_field_add", + DOUBLE, + &[DOUBLE, I32, DOUBLE, DOUBLE], + ); + module.declare_function("js_class_field_add", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_class_computed_field_key", + DOUBLE, + &[DOUBLE, I32, PTR, I64], + ); module.declare_function("js_fs_to_unix_timestamp", DOUBLE, &[DOUBLE]); module.declare_function("js_fs_write_file_sync", I32, &[DOUBLE, DOUBLE]); module.declare_function( @@ -1289,6 +1301,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { // is non-empty. Codegen emits one call per registered class id at // program init, mirroring `js_register_class_id`. module.declare_function("js_register_class_name", VOID, &[I32, PTR, I32]); + module.declare_function("js_register_class_length", VOID, &[I32, I32]); // Anon-shape class registration so `.constructor` reads on object // literals (`{ x: 1 }`) return the global `Object` constructor // instead of the synthetic class ref. Refs #968 / date-fns diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index 2fb33bbf4c..bb85f2a274 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -1245,6 +1245,11 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { DOUBLE, &[DOUBLE, DOUBLE, PTR, I64], ); + module.declare_function( + "js_builtin_subclass_construct", + DOUBLE, + &[I32, PTR, I64, PTR, I64], + ); // ────────────────────────────────────────────────────────────────── // AbortController / AbortSignal — perry-runtime/src/url.rs. diff --git a/crates/perry-codegen/src/stmt/mod.rs b/crates/perry-codegen/src/stmt/mod.rs index bb594065f3..ebe0da2e38 100644 --- a/crates/perry-codegen/src/stmt/mod.rs +++ b/crates/perry-codegen/src/stmt/mod.rs @@ -320,6 +320,9 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> { for _ in 0..ctx.try_depth { ctx.block().call_void("js_try_end", &[]); } + if ctx.shared_super_scope_active { + ctx.block().call_void("js_derived_super_scope_pop", &[]); + } ctx.block().ret(DOUBLE, &final_v); Ok(()) } @@ -354,12 +357,18 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> { for _ in 0..ctx.try_depth { ctx.block().call_void("js_try_end", &[]); } + if ctx.shared_super_scope_active { + ctx.block().call_void("js_derived_super_scope_pop", &[]); + } ctx.block().ret(DOUBLE, &boxed); } else { // Pop open try frames first (see above). for _ in 0..ctx.try_depth { ctx.block().call_void("js_try_end", &[]); } + if ctx.shared_super_scope_active { + ctx.block().call_void("js_derived_super_scope_pop", &[]); + } ctx.block().ret(DOUBLE, &undef); } Ok(()) diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index edb39054ee..25833b877a 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -272,6 +272,7 @@ fn class_with_computed_member(id: u32, name: &str, fields: Vec) -> C }, is_static: false, kind: ClassComputedMemberKind::Method, + source_order: 0, }); class } @@ -10421,6 +10422,7 @@ fn scalar_method_boolean_negative_module(case: &str) -> Module { }, is_static: false, kind: ClassComputedMemberKind::Method, + source_order: 0, }); } "inherited_field_shadow" => { diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index 187b6fd6f0..cf928819eb 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -818,12 +818,10 @@ fn typed_feedback_guards_direct_class_method_specialization() { assert!(ir.contains("js_typed_feedback_method_direct_call_guard")); assert!(ir.contains("method_direct.fast")); assert!(ir.contains("method_direct.fallback")); - // #5334 lever A: this class has a field `x` whose synthesized field-set - // routes its guard-miss arm through the outlined fallback. (The - // method-direct fallback only records when its site_id is Some, which it - // isn't here — the old `record_fallback_call` assertion was incidentally - // satisfied by the field-set fallback that is now folded into this call.) - assert!(ir.contains("call void @js_class_field_set_fallback")); + // Class field initialization follows DefineField semantics, so the + // synthesized initializer uses the class-field add helper rather than the + // ordinary property-set fallback. + assert!(ir.contains("call double @js_class_field_add")); assert!(ir.contains("call double @js_native_call_method")); } diff --git a/crates/perry-hir/src/analysis/value_types_tests.rs b/crates/perry-hir/src/analysis/value_types_tests.rs index c7b9e9a2c5..98222004df 100644 --- a/crates/perry-hir/src/analysis/value_types_tests.rs +++ b/crates/perry-hir/src/analysis/value_types_tests.rs @@ -1179,7 +1179,9 @@ fn infers_class_prototype_and_super_meta_value_shapes() { &Expr::ClassExprFresh { template: "Widget".to_string(), named_statics: Vec::new(), - symbol_statics: Vec::new(), + computed_keys: Vec::new(), + computed_statics: Vec::new(), + static_init_order: Vec::new(), captured_args: Vec::new(), }, &env, diff --git a/crates/perry-hir/src/destructuring/assignment_expr.rs b/crates/perry-hir/src/destructuring/assignment_expr.rs index a08cd3323e..c7cec25d53 100644 --- a/crates/perry-hir/src/destructuring/assignment_expr.rs +++ b/crates/perry-hir/src/destructuring/assignment_expr.rs @@ -75,13 +75,17 @@ pub(crate) fn lower_destructuring_assignment( // `[this.#field] = arr` — brand-guard the // receiver so a wrong-receiver write throws. ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); + let private_name = format!("#{}", private.name); let object = crate::lower::wrap_private_guard( ctx, object, - &property, + &private_name, crate::lower::PRIV_OP_WRITE, ); + let property = crate::lower::private_storage_property( + ctx, + &private_name, + ); exprs.push(Expr::PropertySet { object, property, diff --git a/crates/perry-hir/src/destructuring/assignment_stmt.rs b/crates/perry-hir/src/destructuring/assignment_stmt.rs index b9e704ed47..742a4dfe70 100644 --- a/crates/perry-hir/src/destructuring/assignment_stmt.rs +++ b/crates/perry-hir/src/destructuring/assignment_stmt.rs @@ -108,6 +108,38 @@ fn lower_array_assignment_from_expr( let mut body = Vec::new(); for elem in &arr_pat.elems { + if let Some(ast::Pat::Rest(rest_pat)) = elem { + // AssignmentRestElement evaluates its target and drains every + // remaining iterator value into a fresh Array, then performs the + // assignment even when the iterator was already exhausted. The + // old `PreparedTarget::Skip` arm silently discarded this entire + // operation, so `[...this.#x] = []` never performed its required + // private-brand check. + let (prepare, target, _) = prepare_assignment_target(ctx, &rest_pat.arg)?; + body.extend(prepare); + let (rest_id, rest_name) = fresh_destruct_local(ctx, "destruct_rest", Type::Any); + body.push(Stmt::Let { + id: rest_id, + name: rest_name, + ty: Type::Any, + mutable: false, + init: Some(runtime_iterator_call( + "iteratorRestToArray", + vec![Expr::LocalGet(iter_id), Expr::LocalGet(done_id)], + )), + }); + body.push(Stmt::Expr(Expr::LocalSet( + done_id, + Box::new(Expr::Bool(true)), + ))); + body.extend(assign_prepared_target( + ctx, + target, + Expr::LocalGet(rest_id), + )?); + break; + } + let (value_id, value_name) = fresh_destruct_local(ctx, "destruct_value", Type::Any); body.push(Stmt::Let { id: value_id, @@ -455,13 +487,14 @@ fn prepare_assignment_target( // a getter-only accessor / private method) throws TypeError, // matching `this.#field = v`. ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); + let private_name = format!("#{}", private.name); let guarded = crate::lower::wrap_private_guard( ctx, Box::new(Expr::LocalGet(object_id)), - &property, + &private_name, crate::lower::PRIV_OP_WRITE, ); + let property = crate::lower::private_storage_property(ctx, &private_name); Ok(( prepare, PreparedTarget::Property { diff --git a/crates/perry-hir/src/ir/decl.rs b/crates/perry-hir/src/ir/decl.rs index 380750a82a..4683d1b220 100644 --- a/crates/perry-hir/src/ir/decl.rs +++ b/crates/perry-hir/src/ir/decl.rs @@ -284,21 +284,37 @@ pub struct Class { } impl Class { - /// Whether evaluating this class creates any private names whose brands - /// must be distinct from every other evaluation of the same HIR template. - pub fn has_private_elements(&self) -> bool { + /// Whether construction installs any instance-private element. + pub fn has_private_instance_elements(&self) -> bool { self.fields.iter().any(|field| field.is_private) - || self.static_fields.iter().any(|field| field.is_private) || self .methods .iter() .any(|method| method.name.starts_with('#')) + || self.getters.iter().any(|(name, _)| name.starts_with('#')) + || self.setters.iter().any(|(name, _)| name.starts_with('#')) + } + + /// Whether construction installs the shared brand used by private + /// instance methods and accessors. Private fields carry their own + /// presence and duplicate-initialization check. + pub fn has_private_instance_brand(&self) -> bool { + self.methods + .iter() + .any(|method| method.name.starts_with('#')) + || self.getters.iter().any(|(name, _)| name.starts_with('#')) + || self.setters.iter().any(|(name, _)| name.starts_with('#')) + } + + /// Whether evaluating this class creates any private names whose brands + /// must be distinct from every other evaluation of the same HIR template. + pub fn has_private_elements(&self) -> bool { + self.has_private_instance_elements() + || self.static_fields.iter().any(|field| field.is_private) || self .static_methods .iter() .any(|method| method.name.starts_with('#')) - || self.getters.iter().any(|(name, _)| name.starts_with('#')) - || self.setters.iter().any(|(name, _)| name.starts_with('#')) } } @@ -315,6 +331,9 @@ pub struct ClassComputedMember { pub function: Function, pub is_static: bool, pub kind: ClassComputedMemberKind, + /// Zero-based position in the source ClassBody. Computed field and member + /// names share this ordering during ClassDefinitionEvaluation. + pub source_order: usize, } /// A class field diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index 24e89b2ca7..3a72c08a1c 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -17,6 +17,15 @@ pub enum WithSetFallback { SloppyImplicit(LocalId), } +/// One source-ordered static initialization step on a per-evaluation class +/// object. Computed names have already been evaluated before these steps run. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ClassFreshStaticInit { + Named(u32), + Computed(u32), + Block(u32), +} + /// Expression #[derive(Debug, Clone)] pub enum Expr { @@ -272,7 +281,13 @@ pub enum Expr { /// so ordinary public string keys cannot satisfy private-field syntax. PrivateBrandCheck { class_name: String, + /// Declaring class identity; 0 only when lexical resolution failed. + class_id: u32, field_name: String, + /// Private member kind, using [`PrivKind`](crate::lower::PrivKind)'s + /// wire values: 0=field, 1=method, 2=getter, 3=setter, 4=get+set. + kind: u8, + is_static: bool, object: Box, }, @@ -533,8 +548,10 @@ pub enum Expr { /// `new` / `instanceof` keep dispatching through the existing class_id /// machinery — and writes the per-evaluation static fields as the /// object's OWN properties (`named_statics` via - /// `js_object_set_field_by_name`, `symbol_statics` via - /// `js_object_set_symbol_property`). The class value is the object + /// `js_object_set_field_by_name`, `computed_statics` via the generic + /// PropertyKey setter). Computed field keys are resolved first into hidden + /// own slots so instance construction and computed static initialization + /// reuse the same PropertyKey. The class value is the object /// POINTER, so `make(a) !== make(b)` (distinct heap allocations) and /// each carries its own `static ast`. Because it is a normal traced /// heap object it is collectible — no leak. The static-field @@ -543,7 +560,13 @@ pub enum Expr { ClassExprFresh { template: String, named_statics: Vec<(String, Expr)>, - symbol_statics: Vec<(Expr, Expr)>, + computed_keys: Vec<(String, Expr)>, + /// (hidden resolved-key slot name, initializer) + computed_statics: Vec<(String, Expr)>, + /// Static fields and blocks in ClassBody source order. Indices address + /// `named_statics`, `computed_statics`, or the template's static-block + /// function list respectively. + static_init_order: Vec, /// #1787: the captured outer-scope values this class expression /// closes over, in the synthesized constructor's capture-param /// order (see `synthesize_class_captures`). Each entry is a diff --git a/crates/perry-hir/src/ir/mod.rs b/crates/perry-hir/src/ir/mod.rs index a51ab856c9..cdc83cca55 100644 --- a/crates/perry-hir/src/ir/mod.rs +++ b/crates/perry-hir/src/ir/mod.rs @@ -60,7 +60,8 @@ pub use stmt::{CatchClause, Stmt, SwitchCase}; // ---- expr.rs ---- pub use expr::{ - BoxedPrimitiveKind, Expr, PathWin32Method, ProcessStdinLifecycleMethod, WithSetFallback, + BoxedPrimitiveKind, ClassFreshStaticInit, Expr, PathWin32Method, ProcessStdinLifecycleMethod, + WithSetFallback, }; // ---- ops.rs ---- diff --git a/crates/perry-hir/src/lower/const_fold_fn.rs b/crates/perry-hir/src/lower/const_fold_fn.rs index ab260d390a..39bb4f830f 100644 --- a/crates/perry-hir/src/lower/const_fold_fn.rs +++ b/crates/perry-hir/src/lower/const_fold_fn.rs @@ -600,7 +600,9 @@ fn resolve_fn_ctor_arg( FnCtorShape::ObjToString(body) => eval_tostring(&mut ctx.fn_ctor_env, &body), // A dynamic-function ctor VALUE used as a ToString-able arg // isn't a constant string. - FnCtorShape::DynCtor(_) | FnCtorShape::FnLiteral(_) => None, + FnCtorShape::DynCtor(_) + | FnCtorShape::FnLiteral(_) + | FnCtorShape::IndirectEvalFactory { .. } => None, }; } // A constant EXPRESSION over env entries — `Function(p + "," + p, @@ -1301,6 +1303,9 @@ pub(crate) fn try_eval_function_call_fold( ctx: &mut LoweringContext, call: &ast::CallExpr, ) -> Result> { + if let Some(expr) = try_indirect_eval_factory_call(ctx, call)? { + return Ok(Some(expr)); + } if let Some(expr) = try_indirect_eval_globalthis(ctx, call) { return Ok(Some(expr)); } @@ -1363,6 +1368,68 @@ pub(crate) fn try_eval_function_call_fold( Ok(None) } +fn try_indirect_eval_factory_call( + ctx: &mut LoweringContext, + call: &ast::CallExpr, +) -> Result> { + if call.args.len() != 1 || call.args[0].spread.is_some() { + return Ok(None); + } + let ast::Callee::Expr(callee) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(factory) = callee.as_ref() else { + return Ok(None); + }; + let ast::Expr::Ident(eval_arg) = call.args[0].expr.as_ref() else { + return Ok(None); + }; + if eval_arg.sym.as_ref() != "eval" + || ctx.lookup_local("eval").is_some() + || ctx.lookup_func("eval").is_some() + || ctx.lookup_imported_func("eval").is_some() + { + return Ok(None); + } + let Some(super::fn_ctor_env::FnCtorShape::IndirectEvalFactory { + source_name, + construct, + }) = ctx.fn_ctor_env.entries.get(factory.sym.as_str()).cloned() + else { + return Ok(None); + }; + let Some(super::fn_ctor_env::FnCtorShape::Str(source)) = + ctx.fn_ctor_env.entries.get(&source_name).cloned() + else { + return Ok(None); + }; + let module = match perry_parser::parse_typescript(&source, ".cjs") { + Ok(module) => module, + Err(_) => return Ok(None), + }; + let [ast::ModuleItem::Stmt(ast::Stmt::Expr(statement))] = module.body.as_slice() else { + return Ok(None); + }; + + // The evaluated class lives in eval's own execution context, not at the + // surrounding module top. Raising the synthetic scope depth selects the + // ClassExprFresh lowering used for function/factory evaluations, so a + // repeated call site creates a distinct private brand each time. + ctx.scope_depth += 1; + let lowered = super::lower_expr(ctx, &statement.expr); + ctx.scope_depth -= 1; + let lowered = lowered?; + if construct { + Ok(Some(Expr::NewDynamic { + callee: Box::new(lowered), + args: Vec::new(), + byte_offset: 0, + })) + } else { + Ok(Some(lowered)) + } +} + /// Fold `Function.call(thisArg, ...ctorArgs)` / `Function.apply(thisArg, /// [ctorArgs])` — CreateDynamicFunction ignores its `this`, so these are the /// plain constructor call with the leading argument dropped (Test262 diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index 1aa0471358..dcf55a28ac 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -225,6 +225,7 @@ impl LoweringContext { optional_require_try_depth: 0, require_local_is_create_require: false, fn_ctor_env: super::fn_ctor_env::FnCtorEnv::default(), + dynamic_function_subclasses: HashMap::new(), expr_lower_depth: 0, prelowered_member_receiver: None, in_nonarrow_fn: false, diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index bae1569e16..7fe12a937d 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -437,7 +437,17 @@ pub(crate) fn lower_ident_assignment( strict: ctx.current_strict, }); } - if let Some(id) = ctx.lookup_local(&name) { + let targets_class_inner = ctx.current_class_inner_name.as_deref() == Some(name.as_str()) + && !ctx + .local_decl_scope_depth(&name) + .zip(ctx.current_class_scope_depth) + .is_some_and(|(local_depth, class_depth)| local_depth > class_depth); + if targets_class_inner { + Ok(Expr::Sequence(vec![ + *value, + throw_type_error_const_assignment(&name), + ])) + } else if let Some(id) = ctx.lookup_local(&name) { if ctx.is_local_immutable(id) { // `const c = 1; c = 9` (and every wrapped spelling of the same // target) evaluates the RHS for side effects, then throws @@ -447,19 +457,38 @@ pub(crate) fn lower_ident_assignment( throw_type_error_const_assignment(&name), ])); } - Ok(Expr::LocalSet(id, value)) - } else if ctx.current_class_inner_name.as_deref() == Some(name.as_str()) { - // Assigning to the class own-name binding from inside the class - // body targets the immutable inner `const` binding -> TypeError - // (test262 language/statements/class/name-binding/const). Evaluate - // the RHS for side effects first, then throw. A local/param that - // shadows the name was already handled by the `lookup_local` arm - // above, so this only fires for the genuine class binding. - Ok(Expr::Sequence(vec![ - *value, - throw_type_error_const_assignment(&name), - ])) - } else if ctx.lookup_class(&name).is_some() || ctx.lookup_func(&name).is_some() { + let local_set = Expr::LocalSet(id, value); + let mirrors_script_var = super::lower_expr::global_script_this_enabled() + && ctx.script_var_decl_names.contains(&name) + && ctx.local_decl_scope_depth(&name) == Some(0); + if mirrors_script_var { + let global_this = Box::new(Expr::GlobalThisExpr); + Ok(Expr::Sequence(vec![ + local_set, + Expr::PutValueSet { + target: global_this.clone(), + key: Box::new(Expr::String(name)), + value: Box::new(Expr::LocalGet(id)), + receiver: global_this, + strict: ctx.current_strict, + }, + ])) + } else { + Ok(local_set) + } + } else if ctx.lookup_class(&name).is_some() || ctx.forward_class_shadows_local(&name) { + let class_name = ctx.resolve_class_name(&name); + Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_class_lexical_binding_set".to_string(), + param_types: vec![crate::types::Type::Any, crate::types::Type::Any], + return_type: crate::types::Type::Any, + }), + args: vec![Expr::ClassRef(class_name), *value], + type_args: Vec::new(), + byte_offset: 0, + }) + } else if ctx.lookup_func(&name).is_some() { // v0.5.757: don't shadow a class/function binding with an // implicit local for ` = X` patterns. Drizzle's // sql.js uses `((sql2) => { ... })(sql || (sql = {}))` @@ -516,28 +545,28 @@ fn lower_assignment_target( } ast::AssignTarget::Simple(ast::SimpleAssignTarget::Member(member)) => { // Proxy set: `proxy.foo = v` / `proxy[k] = v` - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let obj_name = obj_ident.sym.to_string(); - if ctx.is_proxy_local(&obj_name) { - let proxy = Box::new(if let Some(id) = ctx.lookup_local(&obj_name) { - Expr::LocalGet(id) - } else { - lower_expr(ctx, &member.obj)? - }); - let key = Box::new(match &member.prop { - ast::MemberProp::Ident(i) => Expr::String(i.sym.to_string()), - ast::MemberProp::Computed(c) => lower_expr(ctx, &c.expr)?, - ast::MemberProp::PrivateName(p) => { - Expr::String(format!("#{}", p.name.as_str())) - } - }); - return Ok(Expr::PutValueSet { - target: proxy.clone(), - key, - value, - receiver: proxy, - strict: ctx.current_strict, - }); + if !matches!(member.prop, ast::MemberProp::PrivateName(_)) { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let obj_name = obj_ident.sym.to_string(); + if ctx.is_proxy_local(&obj_name) { + let proxy = Box::new(if let Some(id) = ctx.lookup_local(&obj_name) { + Expr::LocalGet(id) + } else { + lower_expr(ctx, &member.obj)? + }); + let key = Box::new(match &member.prop { + ast::MemberProp::Ident(i) => Expr::String(i.sym.to_string()), + ast::MemberProp::Computed(c) => lower_expr(ctx, &c.expr)?, + ast::MemberProp::PrivateName(_) => unreachable!("guarded above"), + }); + return Ok(Expr::PutValueSet { + target: proxy.clone(), + key, + value, + receiver: proxy, + strict: ctx.current_strict, + }); + } } } // Check if this is a static field assignment (e.g., Counter.count = 5) @@ -1138,13 +1167,14 @@ fn lower_assignment_target( // Private field assignment: this.#field = value. Guard the // receiver so a write to a wrong receiver — or to a // getter-only accessor / a private method — throws. - let property = format!("#{}", private.name); + let private_name = format!("#{}", private.name); let object = super::expr_member::wrap_private_guard( ctx, object, - &property, + &private_name, super::expr_member::PRIV_OP_WRITE, ); + let property = super::expr_member::private_storage_property(ctx, &private_name); Ok(wrap_assign_object_prelude( prelude.take(), Expr::PropertySet { @@ -1157,15 +1187,6 @@ fn lower_assignment_target( } } ast::AssignTarget::Simple(ast::SimpleAssignTarget::SuperProp(super_prop)) => { - if ctx.current_class_member_is_static { - let mut exprs = Vec::new(); - if let ast::SuperProp::Computed(computed) = &super_prop.prop { - exprs.push(lower_expr(ctx, &computed.expr)?); - } - exprs.push(*value); - exprs.push(throw_type_error_const_assignment("")); - return Ok(Expr::Sequence(exprs)); - } let key = match &super_prop.prop { ast::SuperProp::Ident(ident) => Box::new(Expr::String(ident.sym.to_string())), ast::SuperProp::Computed(computed) => Box::new(lower_expr(ctx, &computed.expr)?), diff --git a/crates/perry-hir/src/lower/expr_call/mod.rs b/crates/perry-hir/src/lower/expr_call/mod.rs index 193980f0d3..05b32c1fce 100644 --- a/crates/perry-hir/src/lower/expr_call/mod.rs +++ b/crates/perry-hir/src/lower/expr_call/mod.rs @@ -464,19 +464,32 @@ fn lower_call_inner(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Result { + s.value.as_str().map(|s| s.to_string()) + } + ast::Expr::Lit(ast::Lit::Num(n)) + if n.value.is_finite() + && n.value.fract() == 0.0 + && n.value >= i64::MIN as f64 + && n.value <= i64::MAX as f64 => + { + Some(if n.value == 0.0 { + "0".to_string() + } else { + (n.value as i64).to_string() + }) + } + _ => None, + }; + if let Some(method) = literal_method { + if let Some(spread_args) = spread_args.clone() { + return Ok(Expr::SuperMethodCallSpread { + method, + args: spread_args, }); } + return Ok(Expr::SuperMethodCall { method, args }); } } } diff --git a/crates/perry-hir/src/lower/expr_call/static_and_instance.rs b/crates/perry-hir/src/lower/expr_call/static_and_instance.rs index a411720ed0..bb29ba08df 100644 --- a/crates/perry-hir/src/lower/expr_call/static_and_instance.rs +++ b/crates/perry-hir/src/lower/expr_call/static_and_instance.rs @@ -189,19 +189,10 @@ pub(super) fn try_static_method_and_instance( })); } } - // Private static method: WithPrivateStatic.#helper() - ast::MemberProp::PrivateName(priv_ident) => { - let method_name = format!("#{}", priv_ident.name); - if ctx.has_static_method(&resolved_class, &method_name) - && !static_call_has_spread - { - return Ok(Ok(Expr::StaticMethodCall { - class_name: resolved_class, - method_name, - args, - })); - } - } + // Private calls must retain the runtime receiver for the + // lexical brand check (notably when it is a Proxy), so + // they deliberately use the generic member-call path. + ast::MemberProp::PrivateName(_) => {} _ => {} } } diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index c505b0fa19..8282981d63 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -707,7 +707,11 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul } let outer_strict = ctx.current_strict; - let is_strict = outer_strict || block_has_use_strict(fn_expr.function.body.as_ref()); + // `strict` already combines the surrounding strict context with this + // function body's own directive prologue. In particular, a function + // expression evaluated as a class heritage expression inherits the class + // definition's strict mode even when its body has no `"use strict"`. + let is_strict = strict; ctx.current_strict = is_strict; // Annex B B.3.3 (#5297): this function-expression body owns its own diff --git a/crates/perry-hir/src/lower/expr_member.rs b/crates/perry-hir/src/lower/expr_member.rs index 6992c0a3ee..837ac06067 100644 --- a/crates/perry-hir/src/lower/expr_member.rs +++ b/crates/perry-hir/src/lower/expr_member.rs @@ -38,7 +38,9 @@ pub(crate) use native_dispatch::{ is_native_dispatch_member, is_net_server_method_name, is_net_socket_method_name, is_stream_api_member, is_url_pattern_data_property, is_worker_instance_value_property, }; -pub(crate) use private_guard::{wrap_private_guard, PRIV_OP_READ, PRIV_OP_WRITE}; +pub(crate) use private_guard::{ + private_storage_property, wrap_private_guard, PRIV_OP_READ, PRIV_OP_WRITE, +}; pub(crate) use process_literals::{process_allowed_node_flags_literal, process_features_literal}; pub(crate) use process_props::{ is_ws_ready_state_receiver, lower_process_named_property, process_metadata_native_property, @@ -1089,25 +1091,25 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re e } let inner = unwrap_member_obj(member.obj.as_ref()); - if let ast::Expr::Ident(obj_ident) = inner { - let obj_name = obj_ident.sym.to_string(); - if ctx.is_proxy_local(&obj_name) { - let proxy_expr = if let Some(id) = ctx.lookup_local(&obj_name) { - Expr::LocalGet(id) - } else { - lower_expr(ctx, &member.obj)? - }; - let key_expr = match &member.prop { - ast::MemberProp::Ident(i) => Expr::String(i.sym.to_string()), - ast::MemberProp::Computed(c) => lower_expr(ctx, &c.expr)?, - ast::MemberProp::PrivateName(pn) => { - Expr::String(format!("#{}", pn.name.as_str())) - } - }; - return Ok(Expr::ProxyGet { - proxy: Box::new(proxy_expr), - key: Box::new(key_expr), - }); + if !matches!(member.prop, ast::MemberProp::PrivateName(_)) { + if let ast::Expr::Ident(obj_ident) = inner { + let obj_name = obj_ident.sym.to_string(); + if ctx.is_proxy_local(&obj_name) { + let proxy_expr = if let Some(id) = ctx.lookup_local(&obj_name) { + Expr::LocalGet(id) + } else { + lower_expr(ctx, &member.obj)? + }; + let key_expr = match &member.prop { + ast::MemberProp::Ident(i) => Expr::String(i.sym.to_string()), + ast::MemberProp::Computed(c) => lower_expr(ctx, &c.expr)?, + ast::MemberProp::PrivateName(_) => unreachable!("guarded above"), + }; + return Ok(Expr::ProxyGet { + proxy: Box::new(proxy_expr), + key: Box::new(key_expr), + }); + } } } } diff --git a/crates/perry-hir/src/lower/expr_member/member_tail.rs b/crates/perry-hir/src/lower/expr_member/member_tail.rs index 8627d7caa5..13d69c6ddd 100644 --- a/crates/perry-hir/src/lower/expr_member/member_tail.rs +++ b/crates/perry-hir/src/lower/expr_member/member_tail.rs @@ -855,8 +855,9 @@ pub(crate) fn lower_member_tail( // Private field access: this.#field -> PropertyGet with "#field". // Wrap the receiver in a brand+kind guard so accessing the private // member on a wrong receiver throws TypeError per spec. - let property = format!("#{}", private.name); - let object = wrap_private_guard(ctx, object, &property, PRIV_OP_READ); + let private_name = format!("#{}", private.name); + let object = wrap_private_guard(ctx, object, &private_name, PRIV_OP_READ); + let property = private_storage_property(ctx, &private_name); Ok(Expr::PropertyGet { // #5247: `this.#field` — carry the member offset for nullish-receiver // localization (consistency with the public-property path). diff --git a/crates/perry-hir/src/lower/expr_member/private_guard.rs b/crates/perry-hir/src/lower/expr_member/private_guard.rs index cbdc8c5fd3..9cabd52a49 100644 --- a/crates/perry-hir/src/lower/expr_member/private_guard.rs +++ b/crates/perry-hir/src/lower/expr_member/private_guard.rs @@ -12,6 +12,24 @@ use super::LoweringContext; pub(crate) const PRIV_OP_READ: u8 = 0; pub(crate) const PRIV_OP_WRITE: u8 = 1; +/// Return the physical property key used for a private field value. Private +/// methods and accessors live in the class registry and keep their source +/// spelling for dispatch, but field values need a key that cannot collide with +/// an ordinary computed property such as `["#x"]`. +pub(crate) fn private_storage_property(ctx: &LoweringContext, field_name: &str) -> String { + match ctx.resolve_private(field_name) { + Some((_, class_id, member)) => { + let family = if member.kind == super::super::PrivKind::Field { + "value" + } else { + "member" + }; + format!("#") + } + None => field_name.to_string(), + } +} + /// Wrap the receiver of a private member access `obj.#name` in a brand+kind /// guard so an access on a non-conforming receiver throws `TypeError`. If the /// name cannot be resolved to a declaring class in scope, the object is diff --git a/crates/perry-hir/src/lower/expr_misc.rs b/crates/perry-hir/src/lower/expr_misc.rs index 146b203894..f0a176f850 100644 --- a/crates/perry-hir/src/lower/expr_misc.rs +++ b/crates/perry-hir/src/lower/expr_misc.rs @@ -103,6 +103,22 @@ pub(super) fn lower_super_prop( }) } else if let Some(key) = match computed.expr.as_ref() { ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str().map(|s| s.to_string()), + ast::Expr::Lit(ast::Lit::Num(n)) + if n.value.is_finite() + && n.value.fract() == 0.0 + // Outside the safe-integer range, formatting an exact + // f64 integer through i64 is not ECMAScript Number:: + // toString (for example 2^63 becomes the property key + // "9223372036854776000"). Let runtime ToPropertyKey + // perform the shortest-decimal conversion instead. + && n.value.abs() <= 9_007_199_254_740_991.0 => + { + Some(if n.value == 0.0 { + "0".to_string() + } else { + (n.value as i64).to_string() + }) + } _ => None, } { // `super['fromA']` in a CLASS method with a string-literal key: @@ -240,9 +256,23 @@ pub(super) fn lower_update(ctx: &mut LoweringContext, update: &ast::UpdateExpr) }) } ast::MemberProp::PrivateName(priv_name) => { - let property = format!("#{}", priv_name.name); + let private_name = format!("#{}", priv_name.name); + let object = crate::lower::expr_member::wrap_private_guard( + ctx, + Box::new(object), + &private_name, + crate::lower::expr_member::PRIV_OP_READ, + ); + let object = crate::lower::expr_member::wrap_private_guard( + ctx, + object, + &private_name, + crate::lower::expr_member::PRIV_OP_WRITE, + ); + let property = + crate::lower::expr_member::private_storage_property(ctx, &private_name); Ok(Expr::PropertyUpdate { - object: Box::new(object), + object, property, op: binary_op, prefix: update.prefix, diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 41b6f84c3c..4c9835e5d6 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -655,6 +655,32 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } } + // `var GeneratorFunction = Object.getPrototypeOf(function*() {}) + // .constructor; class G extends GeneratorFunction {}`. Perry is + // ahead-of-time compiled, so a constant construction site can use + // the same kind-aware fold as a direct dynamic-function-constructor + // call. The trivial explicit constructor supplies no arguments; + // the implicit constructor forwards the new-site arguments. + if let Some((kind, forward_args)) = + ctx.dynamic_function_subclasses.get(&class_name).copied() + { + let empty_args: &[ast::ExprOrSpread] = &[]; + let args_slice = if forward_args { + new_expr.args.as_deref().unwrap_or(empty_args) + } else { + empty_args + }; + if let Some(folded) = super::const_fold_fn::try_const_fold_function_construct_kind( + ctx, + args_slice, + crate::eval_classifier::EvalSurface::NewFunction, + new_expr.span, + kind, + )? { + return Ok(folded); + } + } + // #1677 `new Function(...)` handling, when `Function` is not // shadowed. Phase 1 (#1679) first: when every argument is a // compile-time-constant string, fold the call into a real @@ -1469,6 +1495,27 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } } } + // An identifier that resolves to no lexical, function, class, + // import, native-module, or global-constructor binding fails while + // evaluating the constructor reference. That is a ReferenceError + // (`new Missing()`), distinct from the TypeError produced when a + // present binding's value is non-constructable. + if ctx.lookup_class(&class_name).is_none() + && ctx.resolve_class_alias(&class_name).is_none() + && ctx.lookup_local(&class_name).is_none() + && ctx.lookup_func(&class_name).is_none() + && ctx.lookup_imported_func(&class_name).is_none() + && ctx.lookup_native_module(&class_name).is_none() + && !is_reified_global_builtin_constructor(&class_name) + { + return Ok(Expr::NewDynamic { + callee: Box::new(super::throw_reference_error_expr( + "js_throw_reference_error_unresolved_get", + )), + args, + byte_offset: new_byte_offset, + }); + } // Issue #212: classes nested in a function may capture // enclosing-scope locals. `lower_class_decl` extended the // constructor with one synthesized param per captured id; diff --git a/crates/perry-hir/src/lower/fn_ctor_env.rs b/crates/perry-hir/src/lower/fn_ctor_env.rs index 13b742a5ab..cb75722c01 100644 --- a/crates/perry-hir/src/lower/fn_ctor_env.rs +++ b/crates/perry-hir/src/lower/fn_ctor_env.rs @@ -47,6 +47,14 @@ pub(crate) enum FnCtorShape { #[allow(dead_code)] // payload retained for the planned `f.constructor` kind resolution; matched only via `_` today FnLiteral(DynFnCtorKind), + /// A single-assignment helper of the form + /// `function (_eval) { return [new] _eval(sourceName); }`. Calls that pass + /// the unshadowed global `eval` can lower the constant source AOT while + /// preserving a fresh class evaluation at every call site. + IndirectEvalFactory { + source_name: String, + construct: bool, + }, } /// Which dynamic-function intrinsic a `.constructor` read names. @@ -379,7 +387,15 @@ pub(crate) fn build_fn_ctor_env(module: &ast::Module) -> FnCtorEnv { env.entries.insert(name.clone(), FnCtorShape::UndefinedVar); } Some(expr) if write_count == 0 => { - if let Some(s) = wrapper_const_string(expr) { + if let Some((source_name, construct)) = indirect_eval_factory_shape(expr) { + env.entries.insert( + name.clone(), + FnCtorShape::IndirectEvalFactory { + source_name, + construct, + }, + ); + } else if let Some(s) = wrapper_const_string(expr) { env.entries.insert(name.clone(), FnCtorShape::Str(s)); } else if let Some(kind) = dyn_fn_ctor_kind_of(expr, &fn_literal_vars) { env.entries.insert(name.clone(), FnCtorShape::DynCtor(kind)); @@ -418,6 +434,67 @@ pub(crate) fn build_fn_ctor_env(module: &ast::Module) -> FnCtorEnv { env } +fn indirect_eval_factory_shape(expr: &ast::Expr) -> Option<(String, bool)> { + let mut expr = expr; + while let ast::Expr::Paren(paren) = expr { + expr = paren.expr.as_ref(); + } + let ast::Expr::Fn(function) = expr else { + return None; + }; + // The direct-eval rewrite below executes the wrapper body immediately and + // returns the evaluated value. That is equivalent only for an ordinary + // synchronous function: async wrappers must return a Promise, while a + // generator body must not run until the iterator is advanced. + if function.function.is_async || function.function.is_generator { + return None; + } + if function.function.params.len() != 1 { + return None; + } + let ast::Pat::Ident(eval_param) = &function.function.params[0].pat else { + return None; + }; + let body = function.function.body.as_ref()?; + let [ast::Stmt::Return(ret)] = body.stmts.as_slice() else { + return None; + }; + let returned = ret.arg.as_deref()?; + let (call, construct) = match returned { + ast::Expr::Call(call) => (call, false), + ast::Expr::New(new_expr) => { + if new_expr.args.as_ref().is_some_and(|args| !args.is_empty()) { + return None; + } + let mut callee = new_expr.callee.as_ref(); + while let ast::Expr::Paren(paren) = callee { + callee = paren.expr.as_ref(); + } + let ast::Expr::Call(call) = callee else { + return None; + }; + (call, true) + } + _ => return None, + }; + if call.args.len() != 1 || call.args[0].spread.is_some() { + return None; + } + let ast::Callee::Expr(callee) = &call.callee else { + return None; + }; + let ast::Expr::Ident(callee) = callee.as_ref() else { + return None; + }; + if callee.sym != eval_param.id.sym { + return None; + } + let ast::Expr::Ident(source) = call.args[0].expr.as_ref() else { + return None; + }; + Some((source.sym.to_string(), construct)) +} + fn numeric_literal_of(expr: &ast::Expr) -> Option { let mut e = expr; while let ast::Expr::Paren(p) = e { @@ -1300,3 +1377,29 @@ fn scan_expr_writes(expr: &ast::Expr, writes: &mut HashMap, shado _ => {} } } + +#[cfg(test)] +mod tests { + use super::*; + + fn first_var_initializer(source: &str) -> Box { + let module = perry_parser::parse_typescript(source, "factory-shape.js").unwrap(); + let ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Var(var))) = &module.body[0] else { + panic!("expected variable declaration"); + }; + var.decls[0].init.clone().expect("expected initializer") + } + + #[test] + fn indirect_eval_factory_rejects_async_wrapper() { + let init = + first_var_initializer("const factory = async function (ev) { return ev(src); };"); + assert!(indirect_eval_factory_shape(&init).is_none()); + } + + #[test] + fn indirect_eval_factory_rejects_generator_wrapper() { + let init = first_var_initializer("const factory = function* (ev) { return ev(src); };"); + assert!(indirect_eval_factory_shape(&init).is_none()); + } +} diff --git a/crates/perry-hir/src/lower/for_head.rs b/crates/perry-hir/src/lower/for_head.rs index 9a101c3bf3..eb835c66cc 100644 --- a/crates/perry-hir/src/lower/for_head.rs +++ b/crates/perry-hir/src/lower/for_head.rs @@ -172,13 +172,15 @@ pub(crate) fn for_head_binding_stmts( // private field, brand-guarding the receiver (write op) so a // receiver without the field throws TypeError per spec // (test262 elements/privatefieldset-typeerror-6/7). - let property = format!("#{}", p.name); + let private_name = format!("#{}", p.name); let object = crate::lower::expr_member::wrap_private_guard( ctx, object, - &property, + &private_name, crate::lower::expr_member::PRIV_OP_WRITE, ); + let property = + crate::lower::expr_member::private_storage_property(ctx, &private_name); Expr::PropertySet { object, property, diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index 268923c754..574841cf8f 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -9,14 +9,18 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R // Handle 'in' operator: property in object if matches!(bin.op, ast::BinaryOp::In) { if let ast::Expr::PrivateName(private) = bin.left.as_ref() { - let class_name = ctx.current_class.clone().ok_or_else(|| { - anyhow!("Private name brand check is only supported inside a class") - })?; let field_name = format!("#{}", private.name); + let (class_name, class_id, member) = + ctx.resolve_private(&field_name).ok_or_else(|| { + anyhow!("Private name brand check is only supported inside its declaring class") + })?; let object = Box::new(lower_expr(ctx, &bin.right)?); return Ok(Expr::PrivateBrandCheck { class_name, + class_id, field_name, + kind: member.kind as u8, + is_static: member.is_static, object, }); } diff --git a/crates/perry-hir/src/lower/lower_expr/arm_class.rs b/crates/perry-hir/src/lower/lower_expr/arm_class.rs index 69e2df4d0a..066447ef2f 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_class.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_class.rs @@ -34,21 +34,27 @@ pub(crate) fn lower_class_expr( // ClassRef directly, so the original name is not needed at module // scope. The `current_class` guard avoids renaming the rare // self-referential `class C { … new C() … }` expression form. - let ident_name = match ident_name { + let (ident_name, named_display_override) = match ident_name { Some(n) if (ctx.module_class_decl_names.contains(&n) || ctx.lookup_class(&n).is_some() - || ctx.lookup_imported_func(&n).is_some()) + || ctx.lookup_imported_func(&n).is_some() + // A named class expression's identifier is visible only in + // its ClassBody. When the surrounding binding has a different + // name (or there is no inferred binding), never publish that + // inner identifier as the registry key visible to outer code. + || assignment_name.as_deref() != Some(n.as_str())) && ctx.current_class.as_deref() != Some(n.as_str()) => { - Some(format!("{}__class_expr_{}", n, ctx.fresh_class())) + let synthetic = format!("{}__class_expr_{}", n, ctx.fresh_class()); + (Some(synthetic), Some(n)) } - other => other, + other => (other, None), }; // When the HIR registration key we pick below diverges from the // class's user-visible `.name`, record the real name here so codegen // registers it instead of the synthetic key (#5592). - let mut display_override: Option = None; + let mut display_override: Option = named_display_override; let synthetic_name = match ident_name { Some(n) => n, None => { @@ -87,7 +93,12 @@ pub(crate) fn lower_class_expr( display_override = Some(name.clone()); format!("{}__anon_dup_{}", name, ctx.fresh_class()) } - None => format!("__anon_class_{}", ctx.fresh_class()), + None => { + // The registry key must be unique, but an uninferred + // anonymous class expression has the observable name "". + display_override = Some(String::new()); + format!("__anon_class_{}", ctx.fresh_class()) + } } } }; @@ -126,14 +137,25 @@ pub(crate) fn lower_class_expr( // canonical case: `isSchema(C)` was called from Schema.ts's // own top-level `class extends transform(...)` chains, which // run before the module's `init_static_fields_late`. - let static_symbol_registrations: Vec<(Expr, Expr)> = class + let (computed_name_evaluations, computed_keys, computed_member_registrations) = + crate::lower_decl::prepare_ordered_class_computed_names( + ctx, + &class_expr.class.body, + &class, + ); + let computed_statics: Vec<(String, Expr)> = class .static_fields .iter() - .filter_map(|sf| match (sf.key_expr.as_ref(), sf.init.as_ref()) { - (Some(k), Some(v)) => Some((k.clone(), v.clone())), - _ => None, + .filter_map(|sf| { + sf.key_expr + .as_ref() + .map(|_| (sf.name.clone(), sf.init.clone().unwrap_or(Expr::Undefined))) }) .collect(); + let static_init_order = crate::lower_decl::fresh_class_static_init_order( + &class_expr.class.body, + &class.static_fields, + ); // Issue #1772: regular-named static fields with an initializer // (`static ast = ast`). #894 only handled the Symbol-key case; // these need the same per-evaluation treatment, otherwise a class @@ -142,16 +164,11 @@ pub(crate) fn lower_class_expr( let named_statics: Vec<(String, Expr)> = class .static_fields .iter() - .filter_map(|sf| match (sf.key_expr.as_ref(), sf.init.as_ref()) { - (None, Some(v)) => Some((sf.name.clone(), v.clone())), - _ => None, + .filter_map(|sf| match sf.key_expr.as_ref() { + None => Some((sf.name.clone(), sf.init.clone().unwrap_or(Expr::Undefined))), + Some(_) => None, }) .collect(); - let computed_member_registrations: Vec = class - .computed_members - .iter() - .map(|member| class_computed_member_registration_expr(&synthetic_name, member)) - .collect(); let captured_args: Vec = ctx .lookup_class_captures(&synthetic_name) .map(|ids| ids.iter().map(|id| Expr::LocalGet(*id)).collect()) @@ -230,7 +247,7 @@ pub(crate) fn lower_class_expr( }; if !at_module_top && (!named_statics.is_empty() - || !static_symbol_registrations.is_empty() + || !computed_keys.is_empty() || !captured_args.is_empty() || has_private_elements) { @@ -272,7 +289,9 @@ pub(crate) fn lower_class_expr( let fresh_expr = Expr::ClassExprFresh { template: synthetic_name.clone(), named_statics, - symbol_statics: static_symbol_registrations, + computed_keys, + computed_statics, + static_init_order, captured_args, }; let mut seq: Vec = Vec::new(); @@ -282,6 +301,7 @@ pub(crate) fn lower_class_expr( parent_expr: p, }); } + seq.extend(computed_name_evaluations); seq.extend(computed_member_registrations); let fresh_expr = if let Some(owner) = capture_owner { Expr::Sequence(vec![ @@ -304,6 +324,7 @@ pub(crate) fn lower_class_expr( parent_expr: p, }); } + seq.extend(computed_name_evaluations); // #5437 (p-queue PQueue undefined-`.default` capture): a class EXPRESSION // that captures enclosing-scope locals AND reaches the shared-template // (`ClassRef`) path — i.e. one with heritage (`class extends t { … uses @@ -341,11 +362,22 @@ pub(crate) fn lower_class_expr( captures: captured_args.clone(), }); } + for (field_name, value) in computed_keys { + seq.push(Expr::StaticFieldSet { + class_name: synthetic_name.clone(), + field_name, + value: Box::new(value), + }); + } seq.extend(computed_member_registrations); - for (k, v) in static_symbol_registrations { + for (slot, v) in computed_statics { seq.push(Expr::RegisterClassStaticSymbol { class_name: synthetic_name.clone(), - key_expr: Box::new(k), + key_expr: Box::new(Expr::PropertyGet { + object: Box::new(Expr::ClassRef(synthetic_name.clone())), + property: slot, + byte_offset: 0, + }), value_expr: Box::new(v), }); } diff --git a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs index 04228ae49f..6dcec4ec7f 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs @@ -6,6 +6,28 @@ use crate::types::Type; use anyhow::Result; use swc_ecma_ast as ast; +fn class_binding_read(ctx: &LoweringContext, source_name: &str) -> Expr { + let class_name = ctx.resolve_class_name(source_name); + // Keep the normal, overwhelmingly-common class read as a ClassRef + // immediate. Besides preserving static class recognition downstream, it + // prevents the GC root pass from treating a runtime-call result containing + // an INT32-tagged ClassRef as a heap pointer. Only a class declaration + // actually reassigned from a closure needs the mutable side-table bridge. + if !ctx.reassigned_top_level_identifiers.contains(source_name) { + return Expr::ClassRef(class_name); + } + Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_class_lexical_binding_get".to_string(), + param_types: vec![Type::Any], + return_type: Type::Any, + }), + args: vec![Expr::ClassRef(class_name)], + type_args: Vec::new(), + byte_offset: 0, + } +} + pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> Result { let expr_ident = ast::Expr::Ident(ident.clone()); let expr = &expr_ident; @@ -37,6 +59,18 @@ pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> ctx.with_env_stack = saved_with_envs; return Ok(wrap_with_gets(&name, fallback?, with_envs)); } + // A class body has its own immutable lexical binding for the class name. + // It shadows an outer local of the same name; only a parameter/local + // introduced inside the method body may shadow it in turn. + let nearer_local = ctx + .local_decl_scope_depth(&name) + .zip(ctx.current_class_scope_depth) + .is_some_and(|(local_depth, class_depth)| local_depth > class_depth); + if ctx.current_class_inner_name.as_deref() == Some(name.as_str()) && !nearer_local { + if let Some(current) = ctx.current_class.clone() { + return Ok(Expr::ClassRef(current)); + } + } // A class declared in the current function body lexically shadows a // same-named binding from an OUTER scope. Resolution normally checks // `lookup_local` (which finds outer-scope locals) before the class, @@ -65,7 +99,7 @@ pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> // (`forward_class_shadows_local` is shared with the `new ` arm; see // #8040 for what happened while the two disagreed.) if ctx.forward_class_shadows_local(&name) { - return Ok(Expr::ClassRef(ctx.resolve_class_name(&name))); + return Ok(class_binding_read(ctx, &name)); } // Chained-assignment class self-alias referenced from inside one of the // class's own method/getter/setter bodies. tsc's decorator-capture form @@ -138,14 +172,14 @@ pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> }) } else if ctx.lookup_class(&name).is_some() { // Class used as a first-class value (e.g., { Point: Point }) - Ok(Expr::ClassRef(ctx.resolve_class_name(&name))) + Ok(class_binding_read(ctx, &name)) } else if ctx.forward_class_names.contains(&name) { // Forward reference to a sibling class declared LATER in the // same function body (vendored zod: ZodType.optional() → // ZodOptional.create(...)). JS resolves this at call time; // emit a ClassRef by name — codegen resolves it from the // class registry, which has every pending class by then. - Ok(Expr::ClassRef(ctx.resolve_class_name(&name))) + Ok(class_binding_read(ctx, &name)) } else if name == "undefined" { // Global undefined identifier Ok(Expr::Undefined) diff --git a/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs b/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs index da954ee16f..87d3c61804 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs @@ -87,13 +87,14 @@ pub(crate) fn lower_opt_chain_expr( } } ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); + let private_name = format!("#{}", private.name); let object = expr_member::wrap_private_guard( ctx, Box::new(obj_expr.clone()), - &property, + &private_name, expr_member::PRIV_OP_READ, ); + let property = expr_member::private_storage_property(ctx, &private_name); Expr::PropertyGet { byte_offset: 0, object, @@ -187,13 +188,15 @@ pub(crate) fn lower_opt_chain_expr( } } ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); + let private_name = format!("#{}", private.name); let guarded = expr_member::wrap_private_guard( ctx, Box::new(obj.clone()), - &property, + &private_name, expr_member::PRIV_OP_READ, ); + let property = + expr_member::private_storage_property(ctx, &private_name); Expr::PropertyGet { byte_offset: 0, object: guarded, diff --git a/crates/perry-hir/src/lower/lower_expr/assignment.rs b/crates/perry-hir/src/lower/lower_expr/assignment.rs index 3438d64acd..c4db5b3a68 100644 --- a/crates/perry-hir/src/lower/lower_expr/assignment.rs +++ b/crates/perry-hir/src/lower/lower_expr/assignment.rs @@ -103,13 +103,14 @@ pub(crate) fn lower_expr_assignment( } } ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); + let private_name = format!("#{}", private.name); let object = expr_member::wrap_private_guard( ctx, object, - &property, + &private_name, expr_member::PRIV_OP_WRITE, ); + let property = expr_member::private_storage_property(ctx, &private_name); Expr::PropertySet { object, property, diff --git a/crates/perry-hir/src/lower/lower_module_fn.rs b/crates/perry-hir/src/lower/lower_module_fn.rs index 1f91aaca79..10bdb7f8eb 100644 --- a/crates/perry-hir/src/lower/lower_module_fn.rs +++ b/crates/perry-hir/src/lower/lower_module_fn.rs @@ -350,6 +350,109 @@ fn collect_direct_top_level_reassigned_identifiers(ast_module: &ast::Module) -> out } +/// Class bindings can also be reassigned by a closure created before the class +/// declaration (`var set = function(){ C = null }; class C {}`). Those reads +/// and writes need the mutable class-binding bridge just like a direct module +/// assignment. This deliberately scans only top-level function/arrow +/// initializers and ignores names shadowed by their parameters or top-level +/// local declarations; nested class/method bodies have their own class-name +/// binding and are not part of this outer-binding scan. +fn collect_top_level_closure_reassigned_identifiers(ast_module: &ast::Module) -> HashSet { + fn bind_pat(pat: &ast::Pat, bound: &mut HashSet) { + match pat { + ast::Pat::Ident(ident) => { + bound.insert(ident.id.sym.to_string()); + } + ast::Pat::Assign(assign) => bind_pat(&assign.left, bound), + ast::Pat::Rest(rest) => bind_pat(&rest.arg, bound), + _ => {} + } + } + + fn collect_expr(expr: &ast::Expr, bound: &HashSet, out: &mut HashSet) { + match expr { + ast::Expr::Assign(assign) => { + if let ast::AssignTarget::Simple(ast::SimpleAssignTarget::Ident(ident)) = + &assign.left + { + let name = ident.id.sym.to_string(); + if !bound.contains(&name) { + out.insert(name); + } + } + collect_expr(&assign.right, bound, out); + } + ast::Expr::Paren(paren) => collect_expr(&paren.expr, bound, out), + ast::Expr::Seq(seq) => { + for expr in &seq.exprs { + collect_expr(expr, bound, out); + } + } + _ => {} + } + } + + fn collect_stmts(stmts: &[ast::Stmt], params: &[ast::Pat], out: &mut HashSet) { + let mut bound = HashSet::new(); + for param in params { + bind_pat(param, &mut bound); + } + for stmt in stmts { + match stmt { + ast::Stmt::Decl(ast::Decl::Var(var)) => { + for decl in &var.decls { + bind_pat(&decl.name, &mut bound); + } + } + ast::Stmt::Decl(ast::Decl::Fn(function)) => { + bound.insert(function.ident.sym.to_string()); + } + ast::Stmt::Decl(ast::Decl::Class(class)) => { + bound.insert(class.ident.sym.to_string()); + } + _ => {} + } + } + for stmt in stmts { + if let ast::Stmt::Expr(statement) = stmt { + collect_expr(&statement.expr, &bound, out); + } + } + } + + let mut out = HashSet::new(); + for item in &ast_module.body { + let ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Var(var))) = item else { + continue; + }; + for decl in &var.decls { + let Some(init) = decl.init.as_deref() else { + continue; + }; + match init { + ast::Expr::Fn(function) => { + if let Some(body) = function.function.body.as_ref() { + let params: Vec = function + .function + .params + .iter() + .map(|param| param.pat.clone()) + .collect(); + collect_stmts(&body.stmts, ¶ms, &mut out); + } + } + ast::Expr::Arrow(arrow) => { + if let ast::BlockStmtOrExpr::BlockStmt(body) = arrow.body.as_ref() { + collect_stmts(&body.stmts, &arrow.params, &mut out); + } + } + _ => {} + } + } + } + out +} + pub fn lower_module( ast_module: &ast::Module, name: &str, @@ -599,6 +702,8 @@ pub fn lower_module_full( // `collect_direct_top_level_reassigned_identifiers`'s). ctx.reassigned_top_level_identifiers = collect_direct_top_level_reassigned_identifiers(ast_module); + ctx.reassigned_top_level_identifiers + .extend(collect_top_level_closure_reassigned_identifiers(ast_module)); for item in &ast_module.body { // Extract function declaration from both regular statements and export declarations let fn_decl = match item { diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 7dda4662f0..ac9e7aa562 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -998,6 +998,12 @@ pub struct LoweringContext { /// object literals, counters). Built once per module in /// `lower_module_full`; consumed by `const_fold_fn`. pub(crate) fn_ctor_env: super::fn_ctor_env::FnCtorEnv, + /// Direct subclasses of hidden dynamic-function constructors whose + /// construction can use the existing AOT CreateDynamicFunction fold. + /// The boolean is true for an implicit argument-forwarding constructor + /// and false for the exact `constructor() { super(); }` form. + pub(crate) dynamic_function_subclasses: + HashMap, /// Current recursion depth of `lower_expr` (#5259). Incremented on entry, /// decremented on exit. Once it exceeds either the broad /// `MAX_EXPR_LOWER_DEPTH` ceiling or the lower stack-heavy chain ceiling, diff --git a/crates/perry-hir/src/lower/mod.rs b/crates/perry-hir/src/lower/mod.rs index 518dedf449..d6956269a5 100644 --- a/crates/perry-hir/src/lower/mod.rs +++ b/crates/perry-hir/src/lower/mod.rs @@ -42,7 +42,7 @@ mod expr_call; pub(crate) mod expr_function; pub(crate) use expr_function::capture_function_source; mod expr_member; -pub(crate) use expr_member::{wrap_private_guard, PRIV_OP_WRITE}; +pub(crate) use expr_member::{private_storage_property, wrap_private_guard, PRIV_OP_WRITE}; mod expr_misc; mod expr_new; mod expr_new_builtins; @@ -72,7 +72,7 @@ pub(crate) use pre_scan::*; mod closure_analysis; mod const_fold_fn; mod eval_super_scan; -mod fn_ctor_env; +pub(crate) mod fn_ctor_env; mod global_eval_hoist; mod shared_mutable_capture; pub(crate) mod type_widening; diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index 07ff2a052e..cfaf674698 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -1301,36 +1301,34 @@ pub(crate) fn lower_module_decl( parent_expr: extends_expr.clone(), })); } - for member in &class.computed_members { - module - .init - .push(Stmt::Expr(class_computed_member_registration_expr( - &class_name, - member, - ))); - } - // Inject static-field-init statements in source order - // (see non-export class arm below for rationale). - for sf in &class.static_fields { - if let Some(init) = &sf.init { - // Computed-key static fields (`static [sym] = v`) - // emit a runtime-register call instead of a - // string-keyed StaticFieldSet. Refs #420. - if let Some(key) = sf.key_expr.as_ref() { - module.init.push(Stmt::Expr(Expr::ClassStaticSymbolSet { - class_name: class_name.clone(), - key: Box::new(key.clone()), - value: Box::new(init.clone()), - })); - } else { - module.init.push(Stmt::Expr(Expr::StaticFieldSet { - class_name: class_name.clone(), - field_name: sf.name.clone(), - value: Box::new(init.clone()), - })); - } - } + let (computed_name_evaluations, computed_keys, computed_member_registrations) = + crate::lower_decl::prepare_ordered_class_computed_names( + ctx, + &class_decl.class.body, + &class, + ); + module + .init + .extend(computed_name_evaluations.into_iter().map(Stmt::Expr)); + for (field_name, value) in computed_keys { + module.init.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: class_name.clone(), + field_name, + value: Box::new(value), + })); } + module + .init + .extend(computed_member_registrations.into_iter().map(Stmt::Expr)); + module.init.extend( + crate::lower_decl::build_interleaved_static_init_stmts_after_computed_names( + &class_decl.class.body, + &class_name, + &class.fields, + &class.static_fields, + &class.static_methods, + ), + ); append_legacy_decorator_init_for_class(ctx, &mut module.init, &class); push_class_dedup(module, class); module.exports.push(Export::Named { @@ -1876,33 +1874,34 @@ pub(crate) fn lower_module_decl( parent_expr: extends_expr.clone(), })); } - for member in &class.computed_members { - module - .init - .push(Stmt::Expr(class_computed_member_registration_expr( - &class_name, - member, - ))); - } - // Inject static-field-init statements in source order - // (see non-export class arm for rationale). - for sf in &class.static_fields { - if let Some(init) = &sf.init { - if let Some(key) = sf.key_expr.as_ref() { - module.init.push(Stmt::Expr(Expr::ClassStaticSymbolSet { - class_name: class_name.clone(), - key: Box::new(key.clone()), - value: Box::new(init.clone()), - })); - } else { - module.init.push(Stmt::Expr(Expr::StaticFieldSet { - class_name: class_name.clone(), - field_name: sf.name.clone(), - value: Box::new(init.clone()), - })); - } - } + let (computed_name_evaluations, computed_keys, computed_member_registrations) = + crate::lower_decl::prepare_ordered_class_computed_names( + ctx, + &synth_class_decl.class.body, + &class, + ); + module + .init + .extend(computed_name_evaluations.into_iter().map(Stmt::Expr)); + for (field_name, value) in computed_keys { + module.init.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: class_name.clone(), + field_name, + value: Box::new(value), + })); } + module + .init + .extend(computed_member_registrations.into_iter().map(Stmt::Expr)); + module.init.extend( + crate::lower_decl::build_interleaved_static_init_stmts_after_computed_names( + &synth_class_decl.class.body, + &class_name, + &class.fields, + &class.static_fields, + &class.static_methods, + ), + ); append_legacy_decorator_init_for_class(ctx, &mut module.init, &class); push_class_dedup(module, class); // The `local != exported` shape lets the #485 alias loop diff --git a/crates/perry-hir/src/lower/shared_mutable_capture.rs b/crates/perry-hir/src/lower/shared_mutable_capture.rs index 35a2b95aeb..4fc004ce2f 100644 --- a/crates/perry-hir/src/lower/shared_mutable_capture.rs +++ b/crates/perry-hir/src/lower/shared_mutable_capture.rs @@ -146,7 +146,36 @@ pub(crate) fn desugar_shared_mutable_captures(module: &mut Module) { // ---- declaring bodies: rewrite with ONLY the ids detected in them ------- for (f, s) in module.functions.iter_mut().zip(fn_shared.iter()) { if !s.is_empty() { + // Parameters have no `Stmt::Let` for `rewrite_stmt` to wrap. Turn + // each flagged parameter into the same one-element shared cell at + // function entry, then let the already-rewritten body use + // `param[0]`. Add this after rewriting so the initializer's + // `LocalGet(param)` reads the incoming scalar rather than being + // rewritten into an index read before the cell exists. Retype the + // holder to `Any`: its slot now carries an array pointer, not the + // source parameter's scalar representation. + let shared_params: Vec = f + .params + .iter_mut() + .filter_map(|param| { + if s.contains(¶m.id) { + param.ty = Type::Any; + Some(param.id) + } else { + None + } + }) + .collect(); rewrite_stmts(&mut f.body, s, s); + for id in shared_params.into_iter().rev() { + f.body.insert( + 0, + Stmt::Expr(Expr::LocalSet( + id, + Box::new(Expr::Array(vec![Expr::LocalGet(id)])), + )), + ); + } } } if !init_shared.is_empty() { @@ -614,11 +643,27 @@ fn find_regs_stmt(stmt: &Stmt, out: &mut Vec<(String, Vec)>) { } fn find_regs_expr(expr: &Expr, out: &mut Vec<(String, Vec)>) { - if let Expr::RegisterClassCaptures { - class_name, - captures, - } = expr - { + let registration = match expr { + Expr::RegisterClassCaptures { + class_name, + captures, + } => Some((class_name, captures)), + // A fresh class expression carries the same capture vector as a + // declaration snapshot, but it deliberately has no + // `RegisterClassCaptures`: each evaluation stores its environment on + // its own heap class object. Treat that vector as a registration for + // shared-mutable detection too. Otherwise a mutation nested in a + // fresh class member (for example a defineProperty setter created by + // a static method) receives a private scalar copy while sibling + // methods keep reading the class object's stale capture value. + Expr::ClassExprFresh { + template, + captured_args, + .. + } => Some((template, captured_args)), + _ => None, + }; + if let Some((class_name, captures)) = registration { let ids: Vec = captures .iter() .filter_map(|c| match c { @@ -889,15 +934,18 @@ fn rewrite_expr(expr: &mut Expr, shared: &HashSet, index_uses: &HashSet // Statics' initializer values are ordinary reads and still rewrite. Expr::ClassExprFresh { named_statics, - symbol_statics, + computed_keys, + computed_statics, captured_args, .. } => { for (_, v) in named_statics.iter_mut() { rewrite_expr(v, shared, index_uses); } - for (k, v) in symbol_statics.iter_mut() { - rewrite_expr(k, shared, index_uses); + for (_, key) in computed_keys.iter_mut() { + rewrite_expr(key, shared, index_uses); + } + for (_, v) in computed_statics.iter_mut() { rewrite_expr(v, shared, index_uses); } for a in captured_args.iter_mut() { diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index 1685999c94..f44e80c8b4 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -711,33 +711,27 @@ pub(crate) fn lower_stmt( // that would conflict, and the binding name isn't // already a class (no shadow). if ctx.lookup_class(&bind_name).is_none() { - // Refs #486: `var X = class _X { ... new _X() ... }` — - // the inner self-binding name `_X` references the same - // class as the outer binding `X`. Pre-register the inner - // name as a class alias BEFORE lowering the class body - // so any `Ident("_X")` inside method bodies (e.g. - // `new _X()`) lowers to `Expr::ClassRef("_X")` instead - // of falling through to ExternFuncRef. The HIR `new` - // ident path keys off `lookup_class`. Hono's - // `var Node = class _Node { ... }` and similar npm dist - // shapes hit this. + // `var X = class _X { ... new _X() ... }` gives `_X` + // a lexical binding only inside the class body. Allocate + // the class id under the outer binding here; the lowering + // context's `current_class_inner_name` handling resolves + // `_X` while lowering methods and initializers. Registering + // `_X` globally as an alias leaks it outside the class + // expression (`typeof _X` must remain `"undefined"`). let inner_name_for_register = class_expr .ident .as_ref() .map(|i| i.sym.to_string()) .filter(|n| n != &bind_name); - if let Some(ref inner_name) = inner_name_for_register { - // Allocate the class id eagerly so we can register - // it under both names; lower_class_from_ast picks up - // the same id via lookup_class(bind_name). + if inner_name_for_register.is_some() { + // Allocate the class id eagerly so + // lower_class_from_ast picks up the same id via + // lookup_class(bind_name). let class_id = ctx.fresh_class(); ctx.register_class(bind_name.clone(), class_id); - ctx.register_class(inner_name.clone(), class_id); // The bind-name local holds ITS OWN // class (see inferred_class_bindings). ctx.inferred_class_bindings.insert(bind_name.clone()); - ctx.class_expr_aliases - .insert(inner_name.clone(), bind_name.clone()); } // The inner (const) binding visible inside the // body is the class expression's own source ident @@ -750,7 +744,7 @@ pub(crate) fn lower_stmt( .map(|i| i.sym.to_string()); // Lower the class with the binding name so // `new BindName(...)` works unchanged. - let mut lowered_class = + let lowered_class = crate::lower_decl::lower_class_from_ast( ctx, &class_expr.class, @@ -775,7 +769,6 @@ pub(crate) fn lower_stmt( // `bind_name` registration key. ctx.class_display_names .insert(lowered_class.id, inner_name.clone()); - lowered_class.aliases.push(inner_name); } // Computed member keys (`static get [expr]()`, // `[expr]() {}`) register at runtime against the @@ -837,6 +830,7 @@ pub(crate) fn lower_stmt( crate::lower_decl::build_interleaved_static_init_stmts( &class_expr.class.body, &bind_name, + &lowered_class.fields, &lowered_class.static_fields, &lowered_class.static_methods, ); @@ -1159,6 +1153,27 @@ pub(crate) fn lower_stmt( } } module.init.extend(stmts); + // Script `var` bindings are properties of the global + // object. Perry normally keeps module locals in stack + // slots, so the runtime global-eval interpreter could + // not observe a preceding `var arguments = 1` from a + // nested indirect eval. Publish simple top-level vars + // in global-script mode at their source position; CJS + // modules retain their isolated locals. + if is_var && super::lower_expr::global_script_this_enabled() { + if let ast::Pat::Ident(ident) = &decl.name { + let name = ident.id.sym.to_string(); + if let Some(id) = ctx.lookup_local(&name) { + module.init.push(Stmt::Expr(Expr::PutValueSet { + target: Box::new(Expr::GlobalThisExpr), + key: Box::new(Expr::String(name)), + value: Box::new(Expr::LocalGet(id)), + receiver: Box::new(Expr::GlobalThisExpr), + strict: false, + })); + } + } + } } } ast::Decl::Class(class_decl) => { @@ -1217,14 +1232,25 @@ pub(crate) fn lower_stmt( parent_expr: extends_expr.clone(), })); } - for member in &class.computed_members { - module - .init - .push(Stmt::Expr(class_computed_member_registration_expr( - &class.name, - member, - ))); + let (computed_name_evaluations, computed_keys, computed_member_registrations) = + crate::lower_decl::prepare_ordered_class_computed_names( + ctx, + &class_decl.class.body, + &class, + ); + module + .init + .extend(computed_name_evaluations.into_iter().map(Stmt::Expr)); + for (field_name, value) in computed_keys { + module.init.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: class.name.clone(), + field_name, + value: Box::new(value), + })); } + module + .init + .extend(computed_member_registrations.into_iter().map(Stmt::Expr)); // Inject static-field-init and static-block-call // statements at the source position of the class // declaration, INTERLEAVED in source order (see @@ -1244,9 +1270,10 @@ pub(crate) fn lower_stmt( // declaration path; it skips blocks already invoked via // this inline call. module.init.extend( - crate::lower_decl::build_interleaved_static_init_stmts( + crate::lower_decl::build_interleaved_static_init_stmts_after_computed_names( &class_decl.class.body, &class.name, + &class.fields, &class.static_fields, &class.static_methods, ), diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index 86d4094089..c1d9e96418 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -13,9 +13,7 @@ use crate::lower::{ }; use crate::lower_patterns::*; -use super::class_computed::{ - class_computed_member_registration_expr, push_deduped_class_computed_keys, -}; +use super::class_computed::push_deduped_class_computed_keys; use super::helpers::{async_iterator_method_call, is_filehandle_readlines_for_await_target}; use super::*; @@ -285,12 +283,13 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result = if fresh_binding { class .static_fields .iter() .filter_map( |field| match (field.key_expr.as_ref(), field.init.as_ref()) { - (None, Some(value)) => Some((field.name.clone(), value.clone())), - _ => None, + (None, init) => Some(( + field.name.clone(), + init.cloned().unwrap_or(Expr::Undefined), + )), + (Some(_), _) => None, }, ) .collect() } else { Vec::new() }; - let symbol_statics: Vec<(Expr, Expr)> = if fresh_binding { + let computed_statics: Vec<(String, Expr)> = if fresh_binding { class .static_fields .iter() - .filter_map( - |field| match (field.key_expr.as_ref(), field.init.as_ref()) { - (Some(key), Some(value)) => Some((key.clone(), value.clone())), - _ => None, - }, - ) + .filter_map(|field| { + field.key_expr.as_ref().map(|_| { + ( + field.name.clone(), + field.init.clone().unwrap_or(Expr::Undefined), + ) + }) + }) .collect() } else { Vec::new() }; + let static_init_order = crate::lower_decl::fresh_class_static_init_order( + &class_decl.class.body, + &class.static_fields, + ); // Static field initializers + static blocks for a // function-nested class. The module-level path // (`lower/stmt.rs`) emits these into `module.init`; here they @@ -373,12 +382,25 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result (Vec, Vec<(String, Expr)>, Vec) { + let mut ordered: Vec<(usize, Expr)> = Vec::new(); + let mut field_keys = Vec::new(); + for (source_order, name, value) in super::computed_field_key_initializers_with_order( + class_body, + &class.fields, + &class.static_fields, + ) { + let local = ctx.define_local( + format!("__perry_computed_field_name_{}_{}", class.id, source_order), + Type::Any, + ); + ordered.push((source_order, Expr::LocalSet(local, Box::new(value)))); + field_keys.push((name, Expr::LocalGet(local))); + } + + let mut member_registrations = Vec::new(); + for member in &class.computed_members { + let local = ctx.define_local( + format!( + "__perry_computed_member_name_{}_{}", + class.id, member.source_order + ), + Type::Any, + ); + let to_property_key = Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_to_property_key".to_string(), + param_types: vec![Type::Any], + return_type: Type::Any, + }), + args: vec![member.key_expr.clone()], + type_args: Vec::new(), + byte_offset: 0, + }; + ordered.push(( + member.source_order, + Expr::LocalSet(local, Box::new(to_property_key)), + )); + let mut resolved = member.clone(); + resolved.key_expr = Expr::LocalGet(local); + member_registrations.push(class_computed_member_registration_expr( + &class.name, + &resolved, + )); + } + ordered.sort_by_key(|(source_order, _)| *source_order); + ( + ordered.into_iter().map(|(_, expr)| expr).collect(), + field_keys, + member_registrations, + ) +} + +/// Reconstruct the source order of static fields and static blocks for the +/// `ClassExprFresh` codegen path. Computed-name evaluation remains a separate, +/// earlier phase as required by ClassDefinitionEvaluation. +pub(crate) fn fresh_class_static_init_order( + class_body: &[ast::ClassMember], + static_fields: &[ClassField], +) -> Vec { + let mut result = Vec::new(); + let mut static_field_index = 0usize; + let mut named_index = 0u32; + let mut computed_index = 0u32; + let mut block_index = 0u32; + for member in class_body { + match member { + ast::ClassMember::ClassProp(prop) + if prop.is_static && !prop.declare && !prop.is_abstract => + { + if let Some(field) = static_fields.get(static_field_index) { + if field.key_expr.is_some() { + result.push(ClassFreshStaticInit::Computed(computed_index)); + computed_index += 1; + } else { + result.push(ClassFreshStaticInit::Named(named_index)); + named_index += 1; + } + } + static_field_index += 1; + } + ast::ClassMember::PrivateProp(prop) if prop.is_static => { + result.push(ClassFreshStaticInit::Named(named_index)); + named_index += 1; + static_field_index += 1; + } + ast::ClassMember::StaticBlock(_) => { + result.push(ClassFreshStaticInit::Block(block_index)); + block_index += 1; + } + _ => {} + } + } + result +} + /// A class declared inside a function body is name-deduped against an earlier /// same-named class (Perry's codegen is name-keyed; #336). But ECMA-262 /// ClassDefinitionEvaluation still evaluates every `class` expression's diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index a5fcde2bc3..9ba1218b97 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -40,6 +40,9 @@ fn is_genuine_node_stream_parent(ctx: &LoweringContext, name: &str) -> bool { matches!(ctx.lookup_native_module(name), Some(("stream", _))) } +mod class_heritage; +use class_heritage::*; + use super::*; fn generic_computed_member_key<'a>( @@ -129,6 +132,7 @@ fn lower_generic_computed_class_member( ctx: &mut LoweringContext, method: &ast::ClassMethod, computed: &ast::ComputedPropName, + source_order: usize, ) -> Result { let key_expr = lower_expr(ctx, &computed.expr)?; let function_name = computed_member_name(method.kind, computed); @@ -157,6 +161,7 @@ fn lower_generic_computed_class_member( function, is_static: method.is_static, kind, + source_order, }) } @@ -176,6 +181,7 @@ fn lower_noncomputed_class_member_registration( ctx: &mut LoweringContext, method: &ast::ClassMethod, prop_name: &str, + source_order: usize, ) -> Result { let function_name = noncomputed_member_registration_name(method.kind, method); let (kind, function) = match method.kind { @@ -203,6 +209,7 @@ fn lower_noncomputed_class_member_registration( function, is_static: method.is_static, kind, + source_order, }) } @@ -310,6 +317,16 @@ pub fn lower_class_decl( id } }; + if let Some(ast::Expr::Ident(parent)) = class_decl.class.super_class.as_deref() { + if let Some(crate::lower::fn_ctor_env::FnCtorShape::DynCtor(kind)) = + ctx.fn_ctor_env.entries.get(parent.sym.as_ref()).cloned() + { + if let Some(forward_args) = dynamic_function_forwarding_mode(&class_decl.class) { + ctx.dynamic_function_subclasses + .insert(name.clone(), (kind, forward_args)); + } + } + } // Set current class for arrow function `this` capture tracking let old_class = ctx.current_class.take(); @@ -368,7 +385,20 @@ pub fn lower_class_decl( let (extends, extends_name, native_extends, extends_expr) = if let Some(ref super_class) = class_decl.class.super_class { - if let ast::Expr::Ident(ident) = super_class.as_ref() { + if ctx + .current_class_inner_name + .as_deref() + .is_some_and(|inner| is_class_self_heritage(super_class, inner)) + { + ( + None, + None, + None, + Some(Box::new(crate::lower::throw_reference_error_expr( + "js_throw_reference_error_this_before_super", + ))), + ) + } else if let ast::Expr::Ident(ident) = super_class.as_ref() { let parent_name = ident.sym.to_string(); // First check if it's a native module class let native_parent = match parent_name.as_str() { @@ -450,7 +480,7 @@ pub fn lower_class_decl( // type-facts) to an UNRELATED same-named class, corrupting the // subclass. Matches the fully-dynamic `class X extends // ` shape (`extends`+`extends_name` both None). - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, None, None, Some(Box::new(expr))), Err(_) => (None, None, None, None), } @@ -486,7 +516,7 @@ pub fn lower_class_decl( // textual parent name (super-call codegen, etc.) // but extends_expr takes precedence on the // method-dispatch path. - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, Some(parent_name), None, Some(Box::new(expr))), Err(_) => (None, Some(parent_name), None, None), } @@ -537,7 +567,7 @@ pub fn lower_class_decl( // guard in `extract_top_level_class_decls` keeps this class // inside the IIFE so the require alias is assigned before the // registration runs. - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, Some(parent_name), None, Some(Box::new(expr))), Err(_) => (None, Some(parent_name), None, None), } @@ -562,7 +592,7 @@ pub fn lower_class_decl( // is handled by the `parent_name == name` arm above. (Refs #488 // drizzle-sqlite for the original cross-module link.) let resolved = ctx.lookup_class(&parent_name); - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (resolved, Some(parent_name), None, Some(Box::new(expr))), Err(_) => (resolved, Some(parent_name), None, None), } @@ -581,7 +611,7 @@ pub fn lower_class_decl( // the rest of the program still compiles (the // method-dispatch catch-all in object.rs surfaces the // missing-method case clearly enough). - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, None, None, Some(Box::new(expr))), Err(_) => (None, None, None, None), } @@ -727,7 +757,7 @@ pub fn lower_class_decl( let mut seen_generic_computed_member = false; // Second pass: actually lower the class members - for member in &class_decl.class.body { + for (member_index, member) in class_decl.class.body.iter().enumerate() { match member { ast::ClassMember::Constructor(ctor) => { constructor = Some(lower_constructor(ctx, &name, ctor)?); @@ -738,8 +768,12 @@ pub fn lower_class_decl( continue; } if let Some(computed) = generic_computed_member_key(ctx, method) { - computed_members - .push(lower_generic_computed_class_member(ctx, method, computed)?); + computed_members.push(lower_generic_computed_class_member( + ctx, + method, + computed, + member_index, + )?); seen_generic_computed_member = true; continue; } @@ -802,7 +836,10 @@ pub fn lower_class_decl( })?; if seen_generic_computed_member && can_source_order_register { computed_members.push(lower_noncomputed_class_member_registration( - ctx, method, &prop_name, + ctx, + method, + &prop_name, + member_index, )?); } if method.is_static { @@ -818,7 +855,10 @@ pub fn lower_class_decl( })?; if seen_generic_computed_member && can_source_order_register { computed_members.push(lower_noncomputed_class_member_registration( - ctx, method, &prop_name, + ctx, + method, + &prop_name, + member_index, )?); } if method.is_static { @@ -852,7 +892,10 @@ pub fn lower_class_decl( } if seen_generic_computed_member && can_source_order_register { computed_members.push(lower_noncomputed_class_member_registration( - ctx, method, &prop_name, + ctx, + method, + &prop_name, + member_index, )?); } if method.is_static { @@ -1335,9 +1378,9 @@ pub fn lower_class_from_ast( let old_inner_name = ctx.current_class_inner_name.take(); // A class-expression caller stashes the source ident here; fall back // to the (possibly synthetic) registration name when absent. - ctx.current_class_inner_name = ctx - .pending_class_inner_name - .take() + let explicit_inner_name = ctx.pending_class_inner_name.take(); + ctx.current_class_inner_name = explicit_inner_name + .clone() .or_else(|| Some(name.to_string())); let old_is_derived = ctx.current_class_is_derived; ctx.current_class_is_derived = class.super_class.is_some(); @@ -1376,7 +1419,19 @@ pub fn lower_class_from_ast( let (extends, extends_name, native_extends, extends_expr) = if let Some(ref super_class) = class.super_class { - if let ast::Expr::Ident(ident) = super_class.as_ref() { + if explicit_inner_name + .as_deref() + .is_some_and(|inner| is_class_self_heritage(super_class, inner)) + { + ( + None, + None, + None, + Some(Box::new(crate::lower::throw_reference_error_expr( + "js_throw_reference_error_this_before_super", + ))), + ) + } else if let ast::Expr::Ident(ident) = super_class.as_ref() { let parent_name = ident.sym.to_string(); let native_parent = match parent_name.as_str() { "EventEmitter" => Some(("events".to_string(), "EventEmitter".to_string())), @@ -1460,7 +1515,7 @@ pub fn lower_class_from_ast( // method / vtable / type-facts), corrupting the subclass. The // dynamic `extends_expr` path registers the correct parent edge at // runtime via `RegisterClassParentDynamic` + `function_class_id`. - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, None, None, Some(Box::new(expr))), Err(_) => (None, None, None, None), } @@ -1477,7 +1532,7 @@ pub fn lower_class_from_ast( // falls through to extends_expr capture so a // function-with-prototype value can be resolved at // runtime via `function_class_id`. - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, Some(parent_name), None, Some(Box::new(expr))), Err(_) => (None, Some(parent_name), None, None), } @@ -1505,7 +1560,7 @@ pub fn lower_class_from_ast( // matching `.default` arm in `lower_class_decl` above: route // through `extends_expr` so `super()` re-evaluates the alias // at construction time and the parent edge is registered. - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, Some(parent_name), None, Some(Box::new(expr))), Err(_) => (None, Some(parent_name), None, None), } @@ -1516,7 +1571,7 @@ pub fn lower_class_from_ast( // Keep in lockstep with the matching arm in `lower_class_decl` // (wall 48: NodeNextRequest extends _index.BaseNextRequest). let resolved = ctx.lookup_class(&parent_name); - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (resolved, Some(parent_name), None, Some(Box::new(expr))), Err(_) => (resolved, Some(parent_name), None, None), } @@ -1527,7 +1582,7 @@ pub fn lower_class_from_ast( // expression so codegen can evaluate it at the class // declaration site and call // `js_register_class_parent_dynamic` at runtime. - match lower_expr(ctx, super_class) { + match lower_class_heritage_expr(ctx, super_class) { Ok(expr) => (None, None, None, Some(Box::new(expr))), Err(_) => (None, None, None, None), } @@ -1578,7 +1633,7 @@ pub fn lower_class_from_ast( let mut computed_members = Vec::new(); let mut seen_generic_computed_member = false; - for member in &class.body { + for (member_index, member) in class.body.iter().enumerate() { match member { ast::ClassMember::Constructor(ctor) => { constructor = Some(lower_constructor(ctx, name, ctor)?); @@ -1589,8 +1644,12 @@ pub fn lower_class_from_ast( continue; } if let Some(computed) = generic_computed_member_key(ctx, method) { - computed_members - .push(lower_generic_computed_class_member(ctx, method, computed)?); + computed_members.push(lower_generic_computed_class_member( + ctx, + method, + computed, + member_index, + )?); seen_generic_computed_member = true; continue; } @@ -1645,7 +1704,10 @@ pub fn lower_class_from_ast( })?; if seen_generic_computed_member && can_source_order_register { computed_members.push(lower_noncomputed_class_member_registration( - ctx, method, &prop_name, + ctx, + method, + &prop_name, + member_index, )?); } if method.is_static { @@ -1660,7 +1722,10 @@ pub fn lower_class_from_ast( })?; if seen_generic_computed_member && can_source_order_register { computed_members.push(lower_noncomputed_class_member_registration( - ctx, method, &prop_name, + ctx, + method, + &prop_name, + member_index, )?); } if method.is_static { @@ -1683,7 +1748,10 @@ pub fn lower_class_from_ast( } if seen_generic_computed_member && can_source_order_register { computed_members.push(lower_noncomputed_class_member_registration( - ctx, method, &prop_name, + ctx, + method, + &prop_name, + member_index, )?); } if method.is_static { diff --git a/crates/perry-hir/src/lower_decl/class_decl/class_heritage.rs b/crates/perry-hir/src/lower_decl/class_decl/class_heritage.rs new file mode 100644 index 0000000000..a07fe8791a --- /dev/null +++ b/crates/perry-hir/src/lower_decl/class_decl/class_heritage.rs @@ -0,0 +1,56 @@ +use super::*; + +/// Class heritage is evaluated inside the class's lexical name binding. That +/// binding is uninitialized until the heritage expression finishes, so +/// `class C extends C {}` (including a parenthesized `C`) must fail with a +/// ReferenceError instead of resolving an outer binding or creating a +/// recursive static parent edge. +pub(super) fn is_class_self_heritage(expr: &ast::Expr, inner_name: &str) -> bool { + match expr { + ast::Expr::Ident(ident) => ident.sym == inner_name, + ast::Expr::Paren(paren) => is_class_self_heritage(&paren.expr, inner_name), + _ => false, + } +} + +/// Class definitions are strict-mode code, including function expressions +/// created while evaluating the heritage. Keep the strict context scoped to +/// the heritage expression so a superclass such as +/// `class D extends function(){ arguments.callee } {}` gets a strict +/// arguments object without leaking strictness into surrounding source. +pub(super) fn lower_class_heritage_expr( + ctx: &mut LoweringContext, + expr: &ast::Expr, +) -> Result { + ctx.enter_strict_mode(true); + let lowered = lower_expr(ctx, expr); + ctx.exit_strict_mode(); + lowered +} + +/// Whether this class delegates directly to a dynamic-function heritage. +/// `true` means its implicit constructor forwards the `new` site's arguments; +/// `false` is the exact no-argument `constructor() { super(); }` form. +pub(super) fn dynamic_function_forwarding_mode(class: &ast::Class) -> Option { + let constructor = class.body.iter().find_map(|member| match member { + ast::ClassMember::Constructor(constructor) => Some(constructor), + _ => None, + }); + let Some(constructor) = constructor else { + return Some(true); + }; + if !constructor.params.is_empty() { + return None; + } + let [ast::Stmt::Expr(statement)] = constructor.body.as_ref()?.stmts.as_slice() else { + return None; + }; + let ast::Expr::Call(call) = statement.expr.as_ref() else { + return None; + }; + if matches!(call.callee, ast::Callee::Super(_)) && call.args.is_empty() { + Some(false) + } else { + None + } +} diff --git a/crates/perry-hir/src/lower_decl/class_members.rs b/crates/perry-hir/src/lower_decl/class_members.rs index c17d613eaf..d830af083a 100644 --- a/crates/perry-hir/src/lower_decl/class_members.rs +++ b/crates/perry-hir/src/lower_decl/class_members.rs @@ -19,6 +19,8 @@ pub fn lower_constructor( let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); + let saved_current_strict = ctx.current_strict; + ctx.current_strict = true; // Track that we're inside a constructor body so `new.target` can resolve // to a placeholder object with `.name = class_name`. Saved/restored in @@ -218,6 +220,7 @@ pub fn lower_constructor( crate::lower::expr_function::apply_class_expr_capture_refreshes(&mut body, class_expr_entries); ctx.exit_strict_mode(); + ctx.current_strict = saved_current_strict; ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; ctx.in_constructor_class = saved_ctor_class; @@ -483,6 +486,8 @@ pub fn lower_class_method_with_name( let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); + let saved_current_strict = ctx.current_strict; + ctx.current_strict = true; // Add 'this' for instance methods if !method.is_static { @@ -657,6 +662,7 @@ pub fn lower_class_method_with_name( } ctx.exit_strict_mode(); + ctx.current_strict = saved_current_strict; ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; @@ -731,11 +737,22 @@ pub fn lower_getter_method_with_name( let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); + let saved_current_strict = ctx.current_strict; + ctx.current_strict = true; // Add 'this' for instance getters ctx.define_local("this".to_string(), Type::Any); - // Getters have no parameters + // Getters have no user parameters, but still receive their own empty + // `arguments` object. Accessors use a fixed runtime ABI, so model that + // zero-argument value as a local array instead of adding a hidden formal + // (which would change the registered getter signature). + let arguments_id = method + .function + .body + .as_ref() + .is_some_and(|b| body_uses_arguments(&b.stmts)) + .then(|| ctx.define_local("arguments".to_string(), Type::Any)); // Extract return type. Phase 4: body-based inference when no annotation. let has_explicit_return_annotation = method.function.return_type.is_some(); @@ -747,11 +764,23 @@ pub fn lower_getter_method_with_name( .unwrap_or(Type::Any); // Lower body — see issue #569. - let body = if let Some(ref block) = method.function.body { + let mut body = if let Some(ref block) = method.function.body { lower_fn_body_block_stmt(ctx, block)? } else { Vec::new() }; + if let Some(arguments_id) = arguments_id { + body.insert( + 0, + Stmt::Let { + id: arguments_id, + name: "arguments".to_string(), + ty: Type::Any, + mutable: false, + init: Some(Expr::Array(Vec::new())), + }, + ); + } // Phase 4: getters can't be async/generator by JS syntax, so just the // plain body-walk + unify path. Feeds `class.getters[i].1.return_type` @@ -766,6 +795,7 @@ pub fn lower_getter_method_with_name( } ctx.exit_strict_mode(); + ctx.current_strict = saved_current_strict; ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; @@ -813,12 +843,31 @@ pub fn lower_setter_method_with_name( let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); + let saved_current_strict = ctx.current_strict; + ctx.current_strict = true; // Add 'this' for instance setters ctx.define_local("this".to_string(), Type::Any); // Setters have exactly one parameter let mut params = Vec::new(); + let user_has_arguments_param = method + .function + .params + .iter() + .any(|p| get_pat_name(&p.pat).ok().as_deref() == Some("arguments")); + let needs_arguments = !user_has_arguments_param + && (method + .function + .body + .as_ref() + .is_some_and(|b| body_uses_arguments(&b.stmts)) + || params_use_arguments(&method.function.params)); + // Like getters, setters have a fixed dispatch ABI. Bind `arguments` + // locally and initialize it from the raw setter value before applying any + // parameter defaults or destructuring. + let arguments_id = + needs_arguments.then(|| ctx.define_local("arguments".to_string(), Type::Any)); // Issue #572: setter param can be a destructuring pattern (`set v({ x }) {...}`). let mut destructuring_params: Vec<(LocalId, ast::Pat)> = Vec::new(); for param in &method.function.params { @@ -891,12 +940,30 @@ pub fn lower_setter_method_with_name( new_body.append(&mut body); body = new_body; } + if let Some(arguments_id) = arguments_id { + let raw_args = params + .iter() + .filter(|p| p.name != "this") + .map(|p| Expr::LocalGet(p.id)) + .collect(); + body.insert( + 0, + Stmt::Let { + id: arguments_id, + name: "arguments".to_string(), + ty: Type::Any, + mutable: false, + init: Some(Expr::Array(raw_args)), + }, + ); + } let class_expr_entries = ctx .body_class_expr_captures .split_off(class_expr_capture_mark); crate::lower::expr_function::apply_class_expr_capture_refreshes(&mut body, class_expr_entries); ctx.exit_strict_mode(); + ctx.current_strict = saved_current_strict; ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; @@ -920,10 +987,11 @@ pub fn lower_setter_method_with_name( pub fn lower_class_prop(ctx: &mut LoweringContext, prop: &ast::ClassProp) -> Result { // Computed property keys (`[Symbol.for("k")]`, `[Parent.Symbol.X]`, etc.) - // can't be reduced to a string at compile time — the key expression is - // evaluated at construction time. We capture the lowered key expression - // in `key_expr` and synthesize a placeholder `name` for HIR identity - // (string-keyed lookup paths skip these fields via `key_expr.is_some()`). + // can't be reduced to a string at compile time. ClassDefinitionEvaluation + // resolves it once and stores the resulting PropertyKey in a hidden class + // slot; instance construction later reads that slot. `name` is the hidden + // slot spelling as well as the HIR identity (ordinary string-keyed field + // paths skip it via `key_expr.is_some()`). let (name, key_expr) = match &prop.key { ast::PropName::Ident(ident) => (ident.sym.to_string(), None), ast::PropName::Str(s) => (s.value.as_str().unwrap_or("").to_string(), None), @@ -938,7 +1006,7 @@ pub fn lower_class_prop(ctx: &mut LoweringContext, prop: &ast::ClassProp) -> Res // HIR's field-list iterators that key on `name` will still see // distinct entries because each computed-key field lowers in its // own call. - let synth = format!("__computed_field_{}_{}", c.span.lo.0, c.span.hi.0); + let synth = format!("__perry_computed_field_key_{}_{}", c.span.lo.0, c.span.hi.0); (synth, Some(key)) } }; diff --git a/crates/perry-hir/src/lower_decl/mod.rs b/crates/perry-hir/src/lower_decl/mod.rs index 50b171ead5..3b4ad692c1 100644 --- a/crates/perry-hir/src/lower_decl/mod.rs +++ b/crates/perry-hir/src/lower_decl/mod.rs @@ -38,7 +38,10 @@ pub(crate) use block::{ pub(crate) use body_stmt::gen_capture_scan::forward_referenced_nested_generators; pub(crate) use body_stmt::{find_native_return_in_stmts, lower_body_stmt}; pub(crate) use class_captures::{append_new_args_stmt, synthesize_class_captures}; -pub(crate) use class_computed::class_computed_member_registration_expr; +pub(crate) use class_computed::fresh_class_static_init_order; +pub(crate) use class_computed::{ + class_computed_member_registration_expr, prepare_ordered_class_computed_names, +}; pub(crate) use class_decl::{lower_class_decl, lower_class_from_ast}; pub(crate) use class_members::{ lower_class_method, lower_class_method_with_name, lower_class_prop, lower_constructor, @@ -62,5 +65,8 @@ pub(crate) use private_members::{ build_private_scope, lower_private_getter, lower_private_method, lower_private_prop, lower_private_setter, }; -pub(crate) use static_init::build_interleaved_static_init_stmts; +pub(crate) use static_init::{ + build_interleaved_static_init_stmts, build_interleaved_static_init_stmts_after_computed_names, + computed_field_key_initializers_with_order, +}; pub(crate) use type_alias::lower_type_alias_decl; diff --git a/crates/perry-hir/src/lower_decl/static_init.rs b/crates/perry-hir/src/lower_decl/static_init.rs index d3656e0859..5bac232287 100644 --- a/crates/perry-hir/src/lower_decl/static_init.rs +++ b/crates/perry-hir/src/lower_decl/static_init.rs @@ -6,6 +6,77 @@ use swc_ecma_ast as ast; use crate::ir::*; +fn to_property_key(key: Expr) -> Expr { + Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_to_property_key".to_string(), + param_types: vec![crate::types::Type::Any], + return_type: crate::types::Type::Any, + }), + args: vec![key], + type_args: Vec::new(), + byte_offset: 0, + } +} + +/// Hidden slot/value pairs for public computed fields in source order. The +/// values include the required ToPropertyKey coercion. +pub(crate) fn computed_field_key_initializers( + class_body: &[ast::ClassMember], + fields: &[ClassField], + static_fields: &[ClassField], +) -> Vec<(String, Expr)> { + computed_field_key_initializers_with_order(class_body, fields, static_fields) + .into_iter() + .map(|(_, name, value)| (name, value)) + .collect() +} + +/// [`computed_field_key_initializers`] plus each element's absolute ClassBody +/// position, used to merge field names with computed methods/accessors. +pub(crate) fn computed_field_key_initializers_with_order( + class_body: &[ast::ClassMember], + fields: &[ClassField], + static_fields: &[ClassField], +) -> Vec<(usize, String, Expr)> { + let mut result = Vec::new(); + let mut field_idx = 0usize; + let mut static_field_idx = 0usize; + for (source_order, member) in class_body.iter().enumerate() { + match member { + ast::ClassMember::ClassProp(prop) if !prop.declare && !prop.is_abstract => { + let field = if prop.is_static { + let field = static_fields.get(static_field_idx); + static_field_idx += 1; + field + } else { + let field = fields.get(field_idx); + field_idx += 1; + field + }; + if let Some(field) = field { + if let Some(key) = field.key_expr.as_ref() { + result.push(( + source_order, + field.name.clone(), + to_property_key(key.clone()), + )); + } + } + } + ast::ClassMember::PrivateProp(prop) => { + if prop.is_static { + static_field_idx += 1; + } else { + field_idx += 1; + } + } + _ => {} + } + } + result +} + /// Per ClassDefinitionEvaluation step 34, a class's static fields and /// static blocks evaluate in a single pass over source order — a static /// block sequenced between two static fields must run between them, not @@ -30,8 +101,46 @@ use crate::ir::*; pub(crate) fn build_interleaved_static_init_stmts( class_body: &[ast::ClassMember], class_name: &str, + fields: &[ClassField], + static_fields: &[ClassField], + static_methods: &[Function], +) -> Vec { + build_interleaved_static_init_stmts_impl( + class_body, + class_name, + fields, + static_fields, + static_methods, + true, + ) +} + +/// Static initialization after a caller has already evaluated and stored all +/// computed names in source order. +pub(crate) fn build_interleaved_static_init_stmts_after_computed_names( + class_body: &[ast::ClassMember], + class_name: &str, + fields: &[ClassField], static_fields: &[ClassField], static_methods: &[Function], +) -> Vec { + build_interleaved_static_init_stmts_impl( + class_body, + class_name, + fields, + static_fields, + static_methods, + false, + ) +} + +fn build_interleaved_static_init_stmts_impl( + class_body: &[ast::ClassMember], + class_name: &str, + fields: &[ClassField], + static_fields: &[ClassField], + static_methods: &[Function], + emit_computed_names: bool, ) -> Vec { let emit_field = |out: &mut Vec, sf: &ClassField| { // A COMPUTED-key static field with no initializer still performs @@ -50,10 +159,14 @@ pub(crate) fn build_interleaved_static_init_stmts( &mut init_value, &Expr::ClassRef(class_name.to_string()), ); - out.push(if let Some(key) = sf.key_expr.as_ref() { + out.push(if sf.key_expr.is_some() { Stmt::Expr(Expr::ClassStaticSymbolSet { class_name: class_name.to_string(), - key: Box::new(key.clone()), + key: Box::new(Expr::PropertyGet { + object: Box::new(Expr::ClassRef(class_name.to_string())), + property: sf.name.clone(), + byte_offset: 0, + }), value: Box::new(init_value), }) } else { @@ -65,12 +178,31 @@ pub(crate) fn build_interleaved_static_init_stmts( }); }; + // ClassDefinitionEvaluation first evaluates every ComputedPropertyName in + // source order. Keep the resolved keys on hidden static slots so static + // initialization and each later instance construction reuse the same key. let mut out = Vec::new(); + if emit_computed_names { + for (field_name, value) in + computed_field_key_initializers(class_body, fields, static_fields) + { + out.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: class_name.to_string(), + field_name, + value: Box::new(value), + })); + } + } + + // Static fields and blocks initialize only after all computed keys above + // have been resolved. Their relative source order is still preserved. let mut field_idx = 0usize; let mut block_idx = 0usize; for member in class_body { match member { - ast::ClassMember::ClassProp(prop) if !prop.declare && prop.is_static => { + ast::ClassMember::ClassProp(prop) + if !prop.declare && !prop.is_abstract && prop.is_static => + { if let Some(sf) = static_fields.get(field_idx) { emit_field(&mut out, sf); } diff --git a/crates/perry-hir/src/monomorph/specialize.rs b/crates/perry-hir/src/monomorph/specialize.rs index 6b544ac394..ec883fab52 100644 --- a/crates/perry-hir/src/monomorph/specialize.rs +++ b/crates/perry-hir/src/monomorph/specialize.rs @@ -227,6 +227,7 @@ pub fn specialize_class(class: &Class, type_args: &[Type], new_id: ClassId) -> C .iter() .map(|member| ClassComputedMember { key_expr: substitute_expr(&member.key_expr, &substitutions), + source_order: member.source_order, function: Function { id: member.function.id, name: member.function.name.clone(), diff --git a/crates/perry-hir/src/stable_hash/decls.rs b/crates/perry-hir/src/stable_hash/decls.rs index af59b6ecc3..56b89cad25 100644 --- a/crates/perry-hir/src/stable_hash/decls.rs +++ b/crates/perry-hir/src/stable_hash/decls.rs @@ -81,6 +81,7 @@ impl SH for ClassComputedMember { self.function.hash(h); self.is_static.hash(h); self.kind.hash(h); + self.source_order.hash(h); } } diff --git a/crates/perry-hir/src/stable_hash/expr.rs b/crates/perry-hir/src/stable_hash/expr.rs index 4bdef45e23..680cd7baad 100644 --- a/crates/perry-hir/src/stable_hash/expr.rs +++ b/crates/perry-hir/src/stable_hash/expr.rs @@ -78,7 +78,7 @@ impl SH for Expr { Expr::Void(e) => { tag(h, 37); e.as_ref().hash(h); } Expr::InstanceOf { expr, ty, ty_expr } => { tag(h, 38); expr.as_ref().hash(h); ty.hash(h); ty_expr.hash(h); } Expr::In { property, object } => { tag(h, 39); property.as_ref().hash(h); object.as_ref().hash(h); } - Expr::PrivateBrandCheck { class_name, field_name, object } => { tag(h, 12401); class_name.hash(h); field_name.hash(h); object.as_ref().hash(h); } + Expr::PrivateBrandCheck { class_name, class_id, field_name, kind, is_static, object } => { tag(h, 12401); class_name.hash(h); class_id.hash(h); field_name.hash(h); kind.hash(h); is_static.hash(h); object.as_ref().hash(h); } Expr::PrivateGuard { class_name, class_id, field_name, kind, op, object } => { tag(h, 12402); class_name.hash(h); class_id.hash(h); field_name.hash(h); kind.hash(h); op.hash(h); object.as_ref().hash(h); } Expr::Await(e) => { tag(h, 40); e.as_ref().hash(h); } Expr::Yield { value, delegate } => { tag(h, 41); value.hash(h); delegate.hash(h); } @@ -649,7 +649,7 @@ impl SH for Expr { Expr::RegisterClassStaticSymbol { class_name, key_expr, value_expr, } => { tag(h, 12025); class_name.hash(h); key_expr.as_ref().hash(h); value_expr.as_ref().hash(h); } Expr::RegisterClassComputedMethod { class_name, key_expr, method_name, is_static, param_count, has_rest } => { tag(h, 12233); class_name.hash(h); key_expr.as_ref().hash(h); method_name.hash(h); is_static.hash(h); param_count.hash(h); has_rest.hash(h); } Expr::RegisterClassComputedAccessor { class_name, key_expr, getter_name, setter_name, is_static } => { tag(h, 12234); class_name.hash(h); key_expr.as_ref().hash(h); getter_name.hash(h); setter_name.hash(h); is_static.hash(h); } - Expr::ClassExprFresh { template, named_statics, symbol_statics, captured_args, } => { tag(h, 12026); template.hash(h); for (n, v) in named_statics { n.hash(h); v.hash(h); } for (k, v) in symbol_statics { k.hash(h); v.hash(h); } for a in captured_args { a.hash(h); } } + Expr::ClassExprFresh { template, named_statics, computed_keys, computed_statics, static_init_order, captured_args, } => { tag(h, 12026); template.hash(h); for (n, v) in named_statics { n.hash(h); v.hash(h); } for (n, k) in computed_keys { n.hash(h); k.hash(h); } for (n, v) in computed_statics { n.hash(h); v.hash(h); } for step in static_init_order { match step { ClassFreshStaticInit::Named(index) => { tag(h, 0); index.hash(h); }, ClassFreshStaticInit::Computed(index) => { tag(h, 1); index.hash(h); }, ClassFreshStaticInit::Block(index) => { tag(h, 2); index.hash(h); }, } } for a in captured_args { a.hash(h); } } Expr::SetFunctionPrototype { func, proto } => { tag(h, 448); func.as_ref().hash(h); proto.as_ref().hash(h); } Expr::RegisterPrototypeMethod { class_name, method_name, value, } => { tag(h, 463); class_name.hash(h); method_name.hash(h); value.as_ref().hash(h); } Expr::RegisterFunctionPrototypeMethod { func, method_name, value, } => { tag(h, 464); func.as_ref().hash(h); method_name.hash(h); value.as_ref().hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index e4977b5de6..fbc7b699e6 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -619,15 +619,18 @@ where } Expr::ClassExprFresh { named_statics, - symbol_statics, + computed_keys, + computed_statics, captured_args, .. } => { for (_, v) in named_statics.iter_mut() { f(v); } - for (k, v) in symbol_statics.iter_mut() { - f(k); + for (_, key) in computed_keys.iter_mut() { + f(key); + } + for (_, v) in computed_statics.iter_mut() { f(v); } for a in captured_args.iter_mut() { diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index cd652acba8..f991d28488 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -620,15 +620,18 @@ where } Expr::ClassExprFresh { named_statics, - symbol_statics, + computed_keys, + computed_statics, captured_args, .. } => { for (_, v) in named_statics { f(v); } - for (k, v) in symbol_statics { - f(k); + for (_, key) in computed_keys { + f(key); + } + for (_, v) in computed_statics { f(v); } for a in captured_args { diff --git a/crates/perry-parser/Cargo.toml b/crates/perry-parser/Cargo.toml index 1ac263a749..b11bbab19c 100644 --- a/crates/perry-parser/Cargo.toml +++ b/crates/perry-parser/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] swc_ecma_parser.workspace = true swc_ecma_ast.workspace = true +swc_ecma_visit.workspace = true swc_common.workspace = true thiserror.workspace = true diff --git a/crates/perry-parser/src/lib.rs b/crates/perry-parser/src/lib.rs index 1ffc4b552b..ca433f65db 100644 --- a/crates/perry-parser/src/lib.rs +++ b/crates/perry-parser/src/lib.rs @@ -9,6 +9,7 @@ use std::path::Path; use swc_common::{input::StringInput, sync::Lrc, FileName, SourceMap}; use swc_ecma_ast::{Module, ModuleItem, Script}; use swc_ecma_parser::{lexer::Lexer, EsSyntax, Parser, Syntax, TsSyntax}; +use swc_ecma_visit::{VisitMut, VisitMutWith}; // Re-export AST types for consumers that need to inspect the AST pub use swc_ecma_ast; @@ -46,7 +47,9 @@ pub fn parse_typescript_with_cache( filename: &str, cache: &mut SourceCache, ) -> Result { - let parse_source = normalize_unicode_identifier_escapes(source); + let unicode_source = normalize_unicode_identifier_escapes(source); + let normalized = normalize_swc_class_syntax_with_metadata(&unicode_source); + let parse_source = normalized.source; // Add the source to the cache let file_id = cache.add_file(filename, source.to_string()); @@ -58,7 +61,7 @@ pub fn parse_typescript_with_cache( ); let mut diagnostics = Diagnostics::new(); - let (module, mut parser) = + let (mut module, mut parser) = parse_source_file_with_typescript_fallback(&source_file, filename, &parse_source).map_err( |e| { // Convert SWC error to our diagnostic @@ -71,6 +74,11 @@ pub fn parse_typescript_with_cache( anyhow::anyhow!("Parse error: {}", e.kind().msg()) }, )?; + restore_await_class_identifiers( + &mut module, + source_file.start_pos.0, + &normalized.await_name_starts, + ); // Collect recoverable errors as warnings for error in parser.take_errors() { @@ -97,16 +105,23 @@ pub fn parse_typescript_with_cache( /// This is the original parsing function for backward compatibility. /// For new code, prefer `parse_typescript_with_cache` for better diagnostics. pub fn parse_typescript(source: &str, filename: &str) -> Result { - let parse_source = normalize_unicode_identifier_escapes(source); + let unicode_source = normalize_unicode_identifier_escapes(source); + let normalized = normalize_swc_class_syntax_with_metadata(&unicode_source); + let parse_source = normalized.source; let source_map: Lrc = Default::default(); let source_file = source_map.new_source_file( Lrc::new(FileName::Custom(filename.to_string())), parse_source, ); - let (module, mut parser) = + let (mut module, mut parser) = parse_source_file_with_typescript_fallback(&source_file, filename, &source_file.src) .map_err(|e| anyhow::anyhow!("Parse error: {:?}", e))?; + restore_await_class_identifiers( + &mut module, + source_file.start_pos.0, + &normalized.await_name_starts, + ); // Check for recoverable errors for error in parser.take_errors() { @@ -839,6 +854,169 @@ fn normalize_unicode_identifier_escapes(source: &str) -> String { out } +/// Normalize two valid class grammar corners that SWC currently rejects. +/// String/comment contents are masked before tokenization, so source text that +/// merely mentions these spellings is never rewritten. +struct NormalizedClassSyntax { + source: String, + /// Byte offsets in the normalized source where SWC sees the synthetic + /// `_wait` class identifier. The AST is restored to the source spelling + /// after parsing so `.name` and the class-body inner binding stay correct. + await_name_starts: Vec, +} + +fn normalize_swc_class_syntax_with_metadata(source: &str) -> NormalizedClassSyntax { + #[derive(Clone, Copy)] + struct Token<'a> { + start: usize, + end: usize, + text: &'a str, + } + + let masked = strip_comments_and_strings(source); + // `strip_comments_and_strings` preserves characters, not UTF-8 byte + // widths. Map its byte boundaries back to the original source so a + // non-ASCII comment before a class cannot skew replacement offsets. + let mut source_boundaries = source.char_indices().map(|(i, _)| i).collect::>(); + source_boundaries.push(source.len()); + let mut masked_boundaries = masked.char_indices().map(|(i, _)| i).collect::>(); + masked_boundaries.push(masked.len()); + let mut source_offset_for_masked = vec![0usize; masked.len() + 1]; + for (masked_offset, source_offset) in masked_boundaries + .into_iter() + .zip(source_boundaries.into_iter()) + { + source_offset_for_masked[masked_offset] = source_offset; + } + let bytes = masked.as_bytes(); + let mut tokens = Vec::new(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i].is_ascii_whitespace() { + i += 1; + continue; + } + let start = i; + if bytes[i].is_ascii_alphabetic() || matches!(bytes[i], b'_' | b'$') { + i += 1; + while bytes + .get(i) + .is_some_and(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'$')) + { + i += 1; + } + } else { + i += masked[i..] + .chars() + .next() + .expect("token cursor must be on a character boundary") + .len_utf8(); + } + tokens.push(Token { + start, + end: i, + text: &masked[start..i], + }); + } + + let mut replacements: Vec<(usize, usize, &str)> = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + if token.text == "await" + && index > 0 + && tokens[index - 1].text == "class" + && tokens + .get(index + 1) + .is_some_and(|next| matches!(next.text, "{" | "extends")) + { + // Script grammar permits `await` as a BindingIdentifier here. + // Keep the replacement byte-for-byte the same length. + replacements.push(( + source_offset_for_masked[token.start], + source_offset_for_masked[token.end], + "_wait", + )); + continue; + } + if token.text != "constructor" || tokens.get(index + 1).map(|t| t.text) != Some("(") { + continue; + } + let is_static_method = match index.checked_sub(1).map(|i| tokens[i].text) { + Some("static") => true, + Some("async" | "get" | "set") => index >= 2 && tokens[index - 2].text == "static", + Some("*") => { + (index >= 2 && tokens[index - 2].text == "static") + || (index >= 3 + && tokens[index - 2].text == "async" + && tokens[index - 3].text == "static") + } + _ => false, + }; + if is_static_method { + // A computed spelling prevents SWC from mistaking a static method + // named `constructor` for the class's special constructor. + replacements.push(( + source_offset_for_masked[token.start], + source_offset_for_masked[token.end], + "[\"constructor\"]", + )); + } + } + + let await_name_starts = replacements + .iter() + .filter(|(_, _, replacement)| *replacement == "_wait") + .map(|(start, _, _)| { + let shift: isize = replacements + .iter() + .filter(|(prior_start, _, _)| prior_start < start) + .map(|(prior_start, prior_end, replacement)| { + replacement.len() as isize - (*prior_end - *prior_start) as isize + }) + .sum(); + (*start as isize + shift) as usize + }) + .collect(); + let mut result = source.to_string(); + for (start, end, replacement) in replacements.into_iter().rev() { + result.replace_range(start..end, replacement); + } + NormalizedClassSyntax { + source: result, + await_name_starts, + } +} + +#[cfg(test)] +fn normalize_swc_class_syntax(source: &str) -> String { + normalize_swc_class_syntax_with_metadata(source).source +} + +fn restore_await_class_identifiers( + module: &mut Module, + file_start: u32, + await_name_starts: &[usize], +) { + if await_name_starts.is_empty() { + return; + } + struct RestoreAwaitNames<'a> { + file_start: u32, + starts: &'a [usize], + } + impl VisitMut for RestoreAwaitNames<'_> { + fn visit_mut_ident(&mut self, ident: &mut swc_ecma_ast::Ident) { + let local_start = ident.span.lo.0.saturating_sub(self.file_start) as usize; + if ident.sym == *"_wait" && self.starts.contains(&local_start) { + ident.sym = "await".into(); + } + } + } + module.visit_mut_with(&mut RestoreAwaitNames { + file_start, + starts: await_name_starts, + }); +} + /// Utility to convert SWC span to our span type. /// /// This is useful when processing SWC AST nodes and need to create @@ -1249,6 +1427,50 @@ if (!ASCII_WHITESPACE_REPLACE_REGEX.test(' ')) { ); } + #[test] + fn normalize_valid_static_constructor_methods_for_swc() { + let source = r#" +// André: static constructor() in a comment is untouched. +const text = "static async constructor()"; +class C { + static constructor() {} + static async constructor() {} + static *constructor() {} + static async *constructor() {} + constructor() {} +} +"#; + let normalized = normalize_swc_class_syntax(source); + assert!(normalized.contains("// André: static constructor() in a comment")); + assert!(normalized.contains("\"static async constructor()\"")); + assert!(normalized.contains("static [\"constructor\"]()")); + assert!(normalized.contains("static async [\"constructor\"]()")); + assert!(normalized.contains("static *[\"constructor\"]()")); + assert!(normalized.contains("static async *[\"constructor\"]()")); + assert!(normalized.contains("\n constructor() {}")); + parse_typescript(&normalized, "static-constructor.js").unwrap(); + } + + #[test] + fn normalize_await_class_expression_name_for_script_parser() { + let normalized = normalize_swc_class_syntax("var C = class await {};"); + assert_eq!(normalized, "var C = class _wait {};"); + let module = parse_typescript("var C = class await {};", "await-name.js").unwrap(); + let swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(var))) = + &module.body[0] + else { + panic!("expected var declaration"); + }; + let Some(swc_ecma_ast::Expr::Class(class)) = var.decls[0].init.as_deref() else { + panic!("expected class expression initializer"); + }; + assert_eq!( + class.ident.as_ref().map(|ident| ident.sym.as_ref()), + Some("await") + ); + parse_typescript(r"var C = class \u0061wait {};", "await-name-escaped.js").unwrap(); + } + #[test] fn test_parse_js_inside_type_module_package_uses_module_parser() { let dir = diff --git a/crates/perry-runtime/src/array/subclass.rs b/crates/perry-runtime/src/array/subclass.rs index 5c0db32c74..89322e533e 100644 --- a/crates/perry-runtime/src/array/subclass.rs +++ b/crates/perry-runtime/src/array/subclass.rs @@ -334,7 +334,7 @@ pub(crate) fn array_object_set_length(recv: f64, new_length: f64) { let raw = (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; crate::object::js_object_delete_dynamic(raw, k as f64); } - let raw = (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - crate::object::js_object_set_field_by_name(raw, key, new_length); + let raw = (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; + crate::object::set_field_by_name_object_tail(raw, key, new_length); } diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 449388cce2..74d753cc2b 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -11,6 +11,8 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) let mut namespace_obj = js_closure_get_capture_f64(closure, 0); let method_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const i8; let method_name_len = js_closure_get_capture_ptr(closure, 2) as usize; + let private_brand = (crate::closure::real_capture_count((*closure).capture_count) >= 4) + .then(|| js_closure_get_capture_f64(closure, 3)); // #6173: a SYMBOL-keyed class method read as a value — there is no name to // re-resolve; the captures carry the already-resolved func_ptr + arity @@ -46,15 +48,45 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) // private method body runs against — for `f.call(o)` it is // `o`, for a bare `f()` it is undefined. let call_this = crate::object::js_implicit_this_get(); - return crate::object::call_vtable_method( - func_ptr, - call_this.to_bits() as i64, + return if let Some(brand) = private_brand { + crate::object::call_vtable_method_with_private_brand( + func_ptr, + call_this.to_bits() as i64, + args.as_ptr(), + args.len(), + param_count, + has_synth_args, + has_rest, + brand, + ) + } else { + crate::object::call_vtable_method( + func_ptr, + call_this.to_bits() as i64, + args.as_ptr(), + args.len(), + param_count, + has_synth_args, + has_rest, + ) + }; + } + } + if crate::object::class_prototype_ref_id(namespace_obj).is_none() { + if let (Some(owner_id), Some(brand)) = + (crate::object::class_ref_id(namespace_obj), private_brand) + { + let call_this = crate::object::js_implicit_this_get(); + if let Some(result) = crate::object::call_private_static_method_for_owner( + owner_id, + name, + call_this, + brand, args.as_ptr(), args.len(), - param_count, - has_synth_args, - has_rest, - ); + ) { + return result; + } } } } @@ -95,15 +127,28 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) if let Some((func_ptr, param_count, has_synth_args, has_rest)) = crate::object::lookup_class_method_in_chain(owner_id, name) { - return crate::object::call_vtable_method( - func_ptr, - call_receiver.to_bits() as i64, - args.as_ptr(), - args.len(), - param_count, - has_synth_args, - has_rest, - ); + return if let Some(brand) = private_brand { + crate::object::call_vtable_method_with_private_brand( + func_ptr, + call_receiver.to_bits() as i64, + args.as_ptr(), + args.len(), + param_count, + has_synth_args, + has_rest, + brand, + ) + } else { + crate::object::call_vtable_method( + func_ptr, + call_receiver.to_bits() as i64, + args.as_ptr(), + args.len(), + param_count, + has_synth_args, + has_rest, + ) + }; } } } @@ -382,13 +427,27 @@ pub unsafe extern "C" fn js_function_bind( let err = crate::error::js_typeerror_new(msg); crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); } - if !target_jv.is_pointer() { - return target_value; - } - let target_closure = target_jv.as_pointer::(); - if target_closure.is_null() || (*target_closure).type_tag != CLOSURE_MAGIC { + let target_class_id = crate::object::class_ref_id(target_value).or_else(|| { + ((target_value.to_bits() >> 48) == 0x7FFE + && crate::object::class_prototype_ref_id(target_value).is_none()) + .then_some((target_value.to_bits() & 0xFFFF_FFFF) as u32) + }); + let target_closure = if target_jv.is_pointer() { + let ptr = target_jv.as_pointer::(); + if ptr.is_null() || (*ptr).type_tag != CLOSURE_MAGIC { + // Preserve the existing conservative pass-through for callable + // native handles that do not use the closure representation. + return target_value; + } + Some(ptr) + } else if target_class_id.is_some() { + // ClassRefs are callable/constructable INT32-tagged values rather + // than heap closures. They still need a real BoundFunction wrapper + // so `new C.bind(_, ...args)()` prepends its captured arguments. + None + } else { return target_value; - } + }; let bound_this = if args_len >= 1 && !args_ptr.is_null() { coerce_call_this(target_value, *args_ptr) @@ -419,7 +478,7 @@ pub unsafe extern "C" fn js_function_bind( // boundArgs.length). An `Object.defineProperty(fn, "length", {value})` // override (own dynamic prop) wins over the registered declared length, // and the value may be NaN (→ 0), ±Infinity, or beyond int32. - let target_len_f = + let target_len_f = if let Some(target_closure) = target_closure { match crate::closure::closure_get_own_dynamic_prop(target_closure as usize, "length") { Some(v) => { let jv = JSValue::from_bits(v.to_bits()); @@ -432,7 +491,13 @@ pub unsafe extern "C" fn js_function_bind( } } None => crate::closure::closure_length(target_closure).unwrap_or(0) as f64, - }; + } + } else { + // Constructor arity is not currently retained in the class registry. + // This is still the spec default for a synthesized constructor and is + // independent of bound-argument forwarding/constructibility. + 0.0 + }; let target_len_f = if target_len_f.is_nan() { 0.0 } else { @@ -458,9 +523,15 @@ pub unsafe extern "C" fn js_function_bind( // `f.bind().bind().name` chains to `"bound bound …"`). Fall back to the // declared name from the func-ptr registry for plain named functions, which // don't materialize a `name` data property. - let target_name = read_function_name_property(target_closure as usize) - .or_else(|| crate::builtins::function_name_for_ptr((*target_closure).func_ptr as usize)) - .unwrap_or_default(); + let target_name = if let Some(target_closure) = target_closure { + read_function_name_property(target_closure as usize) + .or_else(|| crate::builtins::function_name_for_ptr((*target_closure).func_ptr as usize)) + .unwrap_or_default() + } else { + target_class_id + .and_then(crate::object::class_name_for_id) + .unwrap_or_default() + }; let bound_name = format!("bound {target_name}"); let name_ptr = crate::string::js_string_from_bytes(bound_name.as_ptr(), bound_name.len() as u32); diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index 30f6cc17ee..dc9b934c99 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -36,6 +36,14 @@ pub unsafe extern "C" fn js_native_call_value( let jsval = JSValue::from_bits(func_value.to_bits()); + // ES class constructors have [[Construct]] but no [[Call]]. ClassRefs use + // Perry's INT32-tagged constructor representation, so letting one fall + // into the legacy raw-pointer path below reinterprets the tag bits as a + // ClosureHeader address. A direct `C()` must instead throw TypeError. + if (func_value.to_bits() >> 48) == 0x7FFE { + throw_not_callable(); + } + // #3656: a Proxy value invoked as a function dispatches through its `apply` // trap (or, absent a trap, forwards to the target). The compiler emits a // `ProxyApply` node when it can statically prove the callee is a proxy, but diff --git a/crates/perry-runtime/src/dyn_eval/mod.rs b/crates/perry-runtime/src/dyn_eval/mod.rs index 199d2eeb13..ad93c0ea7b 100644 --- a/crates/perry-runtime/src/dyn_eval/mod.rs +++ b/crates/perry-runtime/src/dyn_eval/mod.rs @@ -348,6 +348,15 @@ pub fn scan_dyn_eval_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) /// something outside the interpreter subset. pub fn dyn_function_from_strings(args: &[String]) -> f64 { let fn_id = prepare_function_args(args); + let function_length = lookup_fn(fn_id) + .map(|function| { + function + .params + .iter() + .take_while(|pat| !matches!(pat, ast::Pat::Assign(_) | ast::Pat::Rest(_))) + .count() + }) + .unwrap_or(0); // Preserve Function-constructor semantics: each instance owns a private // sloppy-assignment root, while universal globals resolve in this realm. let base = roots_len(); @@ -375,6 +384,16 @@ pub fn dyn_function_from_strings(args: &[String]) -> f64 { true, true, ); + let closure_idx = root_push(closure); + let closure_ptr = crate::value::js_nanbox_get_pointer(root_get(closure_idx)) + as *mut crate::closure::ClosureHeader; + if !closure_ptr.is_null() { + crate::object::set_bound_native_closure_name(closure_ptr, "anonymous"); + let closure_ptr = crate::value::js_nanbox_get_pointer(root_get(closure_idx)) + as *mut crate::closure::ClosureHeader; + crate::object::set_builtin_closure_length(closure_ptr as usize, function_length as u32); + } + let closure = root_get(closure_idx); roots_truncate(base); closure } diff --git a/crates/perry-runtime/src/exception.rs b/crates/perry-runtime/src/exception.rs index 3d846b517e..745311e754 100644 --- a/crates/perry-runtime/src/exception.rs +++ b/crates/perry-runtime/src/exception.rs @@ -122,6 +122,17 @@ struct ExceptionState { /// bypass a static method/accessor's normal pop, so catch entry restores /// the stack to its handler-entry state. static_private_owner_depths: Box<[usize]>, + /// Lexical private-brand dispatch stack depth at each handler. Generated + /// throws bypass normal method epilogues, so the catch path truncates the + /// orphaned entries exactly like the shadow and runtime-handle stacks. + private_lexical_brand_depths: Box<[usize]>, + /// Active derived-constructor binding cells at handler entry. A caught + /// throw can skip an inline constructor's normal scope pop. + derived_super_binding_depths: Box<[usize]>, + /// Pending private-member dispatch hints at handler entry. A throw while + /// evaluating the right-hand side of a guarded private write skips the + /// normal consumer, so catch entry must discard the orphaned hint. + private_member_access_hint_depths: Box<[usize]>, /// #6559: dyn-eval interpreter state (rooted-stack length + interpreter /// call depth, packed) captured when each `try` was pushed. A throw /// `longjmp`s past interpreter Rust frames without running their @@ -148,6 +159,9 @@ impl ExceptionState { call_method_depths: vec![0u32; MAX_TRY_DEPTH].into_boxed_slice(), prototype_resolution_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), static_private_owner_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), + private_lexical_brand_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), + derived_super_binding_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), + private_member_access_hint_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), #[cfg(feature = "dyn-eval")] dyn_eval_savepoints: vec![0u64; MAX_TRY_DEPTH].into_boxed_slice(), try_depth: 0, @@ -209,6 +223,12 @@ fn try_push_with_kind(kind: HandlerKind) -> *mut i32 { crate::object::prototype_chain::resolution_stack_savepoint(); (*s).static_private_owner_depths[depth] = crate::object::static_private_owner_stack_savepoint(); + (*s).private_lexical_brand_depths[depth] = + crate::object::private_lexical_brand_stack_savepoint(); + (*s).derived_super_binding_depths[depth] = + crate::object::derived_super_binding_stack_savepoint(); + (*s).private_member_access_hint_depths[depth] = + crate::object::private_member_access_hints_savepoint(); // #6559: capture the dyn-eval interpreter's rooted-stack length + // call depth, so a caught throw restores interpreter state exactly // like the shadow stack. @@ -330,6 +350,15 @@ pub extern "C-unwind" fn js_throw(value: f64) -> ! { (*s).prototype_resolution_depths[depth], ); crate::object::static_private_owner_stack_restore((*s).static_private_owner_depths[depth]); + crate::object::private_lexical_brand_stack_restore( + (*s).private_lexical_brand_depths[depth], + ); + crate::object::derived_super_binding_stack_restore( + (*s).derived_super_binding_depths[depth], + ); + crate::object::private_member_access_hints_restore( + (*s).private_member_access_hint_depths[depth], + ); // #6559: restore the dyn-eval interpreter's rooted stack + call depth // (interpreter Rust frames unwound by this longjmp never run their // truncate/decrement epilogues). @@ -655,6 +684,9 @@ pub(crate) fn test_unwind_innermost_shadow_restore() { crate::object::prototype_chain::resolution_stack_restore( (*s).prototype_resolution_depths[depth], ); + crate::object::private_member_access_hints_restore( + (*s).private_member_access_hint_depths[depth], + ); }); } diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 13d72ebc9a..ec8ec6d4df 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -1641,12 +1641,16 @@ pub(super) unsafe fn gc_child_slots(header: *mut GcHeader) -> HeapChildSlotItera ) } GcLayoutSlotKind::ObjectMeta => { - // Prototype is the prefix slot; spill and the private-evaluation - // brand are the two contiguous child slots that follow it. + // Prototype and the private-evaluation brand are explicit prefix + // edges. Keep the brand out of the payload selection: its class + // object can be reachable only through this metadata record, so + // treating it as ordinary payload lets a stale/partial layout + // mask silently collect the class evaluation identity. let meta = user_ptr as *mut crate::object::ObjectMeta; let proto_slot = Some(&mut (*meta).prototype as *mut u64); - let range = HeapSlotRange::new(&mut (*meta).spill as *mut u64, 2); - HeapChildSlotIterator::new(header, proto_slot, range) + let brand_slot = Some(&mut (*meta).private_evaluation_brand as *mut u64); + let range = HeapSlotRange::new(&mut (*meta).spill as *mut u64, 1); + HeapChildSlotIterator::new(header, proto_slot, range).with_meta_slot(brand_slot) } GcLayoutSlotKind::ClosureCaptures => { let closure = user_ptr as *mut crate::closure::ClosureHeader; diff --git a/crates/perry-runtime/src/gc/layout/typed_shape.rs b/crates/perry-runtime/src/gc/layout/typed_shape.rs index 3254b6b2c6..20f410f56f 100644 --- a/crates/perry-runtime/src/gc/layout/typed_shape.rs +++ b/crates/perry-runtime/src/gc/layout/typed_shape.rs @@ -180,6 +180,14 @@ unsafe fn init_typed_shape_layout( if user_ptr < GC_HEADER_SIZE + 0x1000 { return; } + // Constructor return override may replace a freshly allocated instance + // with an exotic object represented by a registry handle (notably Proxy). + // Codegen still offers the completed receiver to this post-construction + // layout hook. Reject anything that is not an actual GC allocation before + // deriving and dereferencing its preceding header. + if crate::value::addr_class::try_read_gc_header(user_ptr).is_none() { + return; + } let header = header_from_user_ptr(user_ptr as *const u8); if gc_type_layout_slot_kind((*header).obj_type) != GcLayoutSlotKind::ObjectFields { return; diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 411c3e9681..ce7b02b9ae 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -920,6 +920,11 @@ pub fn gc_init() { // under concurrent load) must rewrite the cell, or the body's next // `this`-derived dispatch derefs a relocated receiver → SIGSEGV. reg_scanner!(crate::object::scan_implicit_this_roots_mut); + // Fresh class evaluations are lexical environments, not merely template + // class ids. Method dispatch keeps the active evaluation here so private + // accesses remain exact across `.call`/`.apply`; root and rewrite those + // class objects while a moving collection runs inside the method body. + reg_scanner!(crate::object::scan_private_lexical_brand_roots_mut); // Connected inspector sessions are retained only by the inspector's // thread-local registry while they receive protocol notifications. reg_scanner!(crate::node_inspector::scan_inspector_roots_mut); diff --git a/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs b/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs index 810181fbfa..38a3f8fafe 100644 --- a/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs +++ b/crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs @@ -773,6 +773,46 @@ fn test_object_meta_prototype_survives_copied_minor_move() { js_shadow_slot_set(1, 0); } +/// A class-evaluation object may be reachable only through an instance's +/// hidden ObjectMeta brand. The edge must retain and rewrite that class object +/// when a copied minor moves the owner, its metadata, and the brand together. +#[test] +fn test_object_meta_private_evaluation_brand_survives_copied_minor_move() { + let _guard = CopyingNurseryTestGuard::new(1); + + let (owner, _) = unsafe { alloc_nursery_test_object(0) }; + let (class, _) = unsafe { alloc_nursery_test_object(0) }; + unsafe { + crate::object::js_object_mark_class(class as i64); + let meta = crate::object::object_meta_ensure(owner); + (*meta).private_evaluation_brand = ptr_bits(class as usize); + } + let old_owner = owner as usize; + let old_class = class as usize; + js_shadow_slot_set(0, ptr_bits(old_owner)); + + let _ = gc_collect_minor(); + + let new_owner = (js_shadow_slot_get(0) & POINTER_MASK) as usize; + assert_ne!(new_owner, old_owner, "test premise: the owner must move"); + let brand = unsafe { + let meta = (*(new_owner as *mut crate::object::ObjectHeader)).meta; + assert!( + !meta.is_null(), + "the moved owner must retain its meta record" + ); + (*meta).private_evaluation_brand + }; + let new_class = (brand & POINTER_MASK) as usize; + assert_ne!(new_class, old_class, "test premise: the brand must move"); + assert!( + crate::object::is_class_object_value(f64::from_bits(brand)), + "the metadata edge must retain and rewrite the class evaluation brand" + ); + + js_shadow_slot_set(0, 0); +} + /// #6759 Phase C2: the per-key descriptor summary in the meta record gates /// table probes — exactly (no false negatives) for installed keys, and /// authoritatively negative for a fresh owner and for keys whose Bloom bit diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index f5d4f96f18..23797f42b4 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -137,6 +137,40 @@ pub extern "C" fn js_array_subclass_init(this: f64, n: f64) -> f64 { this } +/// Array's overloaded constructor semantics for a source-compiled subclass. +/// One numeric argument is a length; every other argument list becomes the +/// initial indexed elements. +#[no_mangle] +pub unsafe extern "C" fn js_array_subclass_init_args( + this: f64, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let args = if args_ptr.is_null() || args_len == 0 { + &[][..] + } else { + std::slice::from_raw_parts(args_ptr, args_len) + }; + if args.len() == 1 && JSValue::from_bits(args[0].to_bits()).is_number() { + return js_array_subclass_init(this, args[0]); + } + + let scope = crate::gc::RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let args = scope.root_nanbox_f64_slice(args); + js_array_subclass_init(this.get_nanbox_f64(), args.len() as f64); + for (index, value) in args.iter().enumerate() { + let name = index.to_string(); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let receiver = this.get_nanbox_f64(); + let raw = raw_ptr_from_value(receiver) as *mut ObjectHeader; + if !raw.is_null() { + js_object_set_field_by_name(raw, key, value.get_nanbox_f64()); + } + } + this.get_nanbox_f64() +} + /// `Array.prototype.fill`-equivalent installed on an Array-subclass instance: /// fills the receiver's own indexed slots `0..length` with `value`. Delegates /// to the generic array-like fill (which reads `length` off the receiver). diff --git a/crates/perry-runtime/src/node_submodules/test_runner.rs b/crates/perry-runtime/src/node_submodules/test_runner.rs index 4ce596a49e..bd2b6af912 100644 --- a/crates/perry-runtime/src/node_submodules/test_runner.rs +++ b/crates/perry-runtime/src/node_submodules/test_runner.rs @@ -100,7 +100,7 @@ enum HookKind { AfterEach, } -thread_local! { +crate::perry_thread_local! { static TEST_RUNNER: RefCell = RefCell::new(RunnerState::new()); static ACTIVE_CHILDREN: RefCell>> = const { RefCell::new(Vec::new()) }; static ACTIVE_ANCESTORS: RefCell> = const { RefCell::new(Vec::new()) }; diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index a2738d1064..93f845d51e 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -1113,6 +1113,28 @@ static KEEP_JS_ERROR_SUBCLASS_DEFAULT_INIT: unsafe extern "C" fn( *const crate::StringHeader, ) = js_error_subclass_default_init; +/// Find the per-evaluation class object that owns `target_cid` while walking a +/// fresh derived class's pinned parent chain. The template class-id registry +/// identifies which constructor to replay, but it cannot identify which +/// evaluation's captured environment belongs to that constructor. +fn pinned_class_object_for_ancestor(start: f64, target_cid: u32) -> Option { + let mut current = start; + let mut depth = 0usize; + while depth < 32 && super::class_registry::is_class_object_value(current) { + let object = + crate::value::JSValue::from_bits(current.to_bits()).as_pointer::(); + if object.is_null() { + return None; + } + if super::js_object_get_class_id(object) == target_cid { + return Some(current); + } + current = super::class_registry::class_object_pinned_parent(object)?; + depth += 1; + } + None +} + pub(crate) unsafe fn replay_class_object_constructor( classobj_value: f64, class_cid: u32, @@ -1159,19 +1181,29 @@ pub(crate) unsafe fn replay_class_object_constructor( }; // Read the snapshotted captures (an own array, in capture-param order). - // Absent → no captures. The `__perry_ctor_caps` snapshot on this class - // object belongs to ITS OWN ctor — when the walk above resolved an - // ANCESTOR's ctor, that snapshot doesn't apply; use the ancestor's - // decl-site snapshot (CLASS_CAPTURE_VALUES) via the fallback below. - let caps_val = if ctor_cid == class_cid { - crate::object::js_object_get_own_field_or_undef( - classobj_handle.get_nanbox_f64(), - b"__perry_ctor_caps".as_ptr(), - 17, - ) + // When the implicit derived constructor walk resolves an ancestor, follow + // THIS class object's pinned per-evaluation parent chain and take the + // capture array from the matching ancestor object. Falling straight back + // to the template-wide declaration snapshot loses a fresh parent's + // environment (`class extends makeParent(tag) {}`), so inherited methods + // read `undefined` even though the instance's prototype chain is correct. + let capture_owner = if ctor_cid == class_cid { + classobj_handle.get_nanbox_f64() } else { - f64::from_bits(crate::value::TAG_UNDEFINED) + pinned_class_object_for_ancestor(classobj_handle.get_nanbox_f64(), ctor_cid) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) }; + let capture_owner_handle = scope.root_nanbox_f64(capture_owner); + let caps_val = + if super::class_registry::is_class_object_value(capture_owner_handle.get_nanbox_f64()) { + crate::object::js_object_get_own_field_or_undef( + capture_owner_handle.get_nanbox_f64(), + b"__perry_ctor_caps".as_ptr(), + 17, + ) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; let caps_jv = crate::value::JSValue::from_bits(caps_val.to_bits()); let (caps_arr, n_caps): (*const crate::array::ArrayHeader, u32) = if caps_jv.is_pointer() { let arr = caps_jv.as_pointer::(); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 299ce2dd3d..fecbf27961 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -55,7 +55,7 @@ mod vm_brand; #[cfg(test)] pub(crate) use state::class_decl_prototype_object_root_store; pub(crate) use state::{ - class_decl_prototype_object, class_decl_prototype_value, + class_decl_prototype_method_names, class_decl_prototype_object, class_decl_prototype_value, class_decl_prototype_value_for_instance_class, class_delete_own_dynamic_prop, class_dynamic_prop_root_store, class_has_own_dynamic_prop, class_id_for_decl_prototype_object, class_is_key_deleted, class_mark_key_deleted, class_object_value_for_cid, @@ -89,10 +89,11 @@ pub(crate) use class_meta::test_text_encoding_stream_new_with_constructor; #[cfg(feature = "global-text")] pub(crate) use class_meta::text_decoder_bool_option; pub use class_meta::{ - class_name_for_id, is_anon_shape_class_id, js_compression_stream_new, + class_length_for_id, class_name_for_id, is_anon_shape_class_id, js_compression_stream_new, js_decompression_stream_new, js_register_anon_shape_class_id, js_register_class_id, - js_register_class_name, js_text_decoder_stream_new, js_text_encoder_stream_new, - js_text_encoding_stream_new, ANON_SHAPE_CLASS_IDS, CLASS_NAMES, + js_register_class_length, js_register_class_name, js_text_decoder_stream_new, + js_text_encoder_stream_new, js_text_encoding_stream_new, ANON_SHAPE_CLASS_IDS, CLASS_LENGTHS, + CLASS_NAMES, }; pub(crate) use class_meta::{ identify_global_builtin_constructor, report_dispatch_miss, @@ -160,19 +161,24 @@ pub use registration::{ #[cfg(test)] pub(crate) use dispatch::test_bump_vtable_generation; pub(crate) use dispatch::{ - call_vtable_method, fetch_parent_kind_in_chain, obj_dispatch_ic_insert, obj_dispatch_ic_lookup, - vtable_generation, vtable_ic_insert, vtable_ic_lookup, VTABLE_GEN, + call_vtable_method, call_vtable_method_with_private_brand, fetch_parent_kind_in_chain, + obj_dispatch_ic_insert, obj_dispatch_ic_lookup, vtable_generation, vtable_ic_insert, + vtable_ic_lookup, VTABLE_GEN, }; // ── parent_static.rs ──────────────────────────────────────────────────────── pub(crate) use parent_static::{ - call_registered_static_method, call_static_method, class_chain_has_instance_accessor, - class_has_instance_getter, class_has_own_static_method, class_has_symbol_member_in_chain, - class_instance_setter_apply, class_method_bind_length, class_object_own_field_bytes, - class_object_pinned_parent, class_own_symbol_member_keys, class_static_accessor_getter_value, - class_static_accessor_setter_apply, class_symbol_getter_value, class_symbol_setter_apply, - get_parent_class_id, lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, - register_class, + call_private_static_method_for_owner, call_registered_static_method, call_static_method, + class_chain_has_instance_accessor, class_dynamic_static_accessor_descriptor, + class_dynamic_static_accessor_getter_value, class_has_instance_getter, + class_has_own_static_method, class_has_symbol_member_in_chain, class_instance_setter_apply, + class_method_bind_length, class_object_own_field_bytes, class_object_pinned_parent, + class_own_symbol_accessor_ptrs, class_own_symbol_member_keys, class_own_symbol_method, + class_private_instance_getter_value, class_private_instance_setter_apply, + class_static_accessor_getter_value, class_static_accessor_setter_apply, + class_symbol_getter_value, class_symbol_setter_apply, get_parent_class_id, + lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, register_class, + register_class_dynamic_static_accessor, }; pub use parent_static::{ is_class_object_ptr, is_class_object_value, is_registered_class_prototype_object, 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 423182d5bd..745de82517 100644 --- a/crates/perry-runtime/src/object/class_registry/class_meta.rs +++ b/crates/perry-runtime/src/object/class_registry/class_meta.rs @@ -23,6 +23,11 @@ pub unsafe extern "C" fn js_register_class_id(class_id: u32) { /// from `v8::Function::builder(...)` would collide every module under the /// same token. (#1021.) pub static CLASS_NAMES: RwLock>> = RwLock::new(None); +/// Maps `class_id → ECMAScript constructor length` (formal parameters before +/// the first default/rest parameter). Class refs are integer immediates rather +/// than heap Function objects, so their own `length` property is reified from +/// this table alongside `CLASS_NAMES`. +pub static CLASS_LENGTHS: RwLock>> = RwLock::new(None); /// Register the user-visible name of a class so the V8 bridge can label /// the V8-side wrapper for nice `metatype.name` reads. Idempotent. @@ -57,6 +62,22 @@ pub fn class_name_for_id(class_id: u32) -> Option { guard.as_ref()?.get(&class_id).cloned() } +#[no_mangle] +pub extern "C" fn js_register_class_length(class_id: u32, length: u32) { + if class_id == 0 { + return; + } + let mut guard = CLASS_LENGTHS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, length); +} + +pub fn class_length_for_id(class_id: u32) -> Option { + CLASS_LENGTHS.read().ok()?.as_ref()?.get(&class_id).copied() +} + /// Whether dynamic-dispatch miss diagnostics are enabled (`PERRY_DISPATCH_DIAG`, /// any non-empty/non-falsey value). Cached on first read. /// diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index 1e751edb14..ea3d72b198 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -473,7 +473,8 @@ pub unsafe extern "C" fn js_new_function_construct( if args.len() == 1 { return crate::date::js_date_new_from_value(args[0]); } - let mut vals = [f64::from_bits(crate::value::TAG_UNDEFINED); 7]; + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let mut vals = [undefined, undefined, 1.0, 0.0, 0.0, 0.0, 0.0]; for (i, slot) in vals.iter_mut().enumerate() { if i < args.len() { *slot = args[i]; @@ -483,6 +484,27 @@ pub unsafe extern "C" fn js_new_function_construct( vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], ); } + "Boolean" => { + let value = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::builtins::js_boxed_boolean_new(value); + } + "Number" => { + let value = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::builtins::js_boxed_number_new(value); + } + "String" => { + let value = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::builtins::js_boxed_string_new(value, (!args.is_empty()) as i32); + } "Array" => { if args.len() == 1 { let arr = crate::array::js_array_constructor_single(args[0]); @@ -915,6 +937,9 @@ pub unsafe extern "C" fn js_new_function_construct( // address. Reproduced by `new C()` where `C = mk()` is a class // EXPRESSION value. let inst_handle = scope.root_raw_mut_ptr(inst); + inst_handle.with_mut_ptr::(|inst| { + link_class_object_instance_prototype(class_handle.get_nanbox_f64(), inst) + }); // Every evaluation gets a distinct brand despite sharing its // class id. Stamp it before replay, where private access may occur. inst_handle.with_mut_ptr::(|inst| { @@ -937,20 +962,31 @@ pub unsafe extern "C" fn js_new_function_construct( args_len, ); }); - let inst: *mut ObjectHeader = inst_handle.get_raw_mut_ptr(); // `class X extends Request/Response {}` constructed via the dynamic // (class-expression value) path: the replayed ctor's `super()` // can't statically route an aliased parent, so attach the native // fetch handle here when the registered parent is a fetch builtin // and the instance didn't already get one. Refs `@hono/node-server`. if let Some(kind) = fetch_parent_kind_in_chain(class_cid) { - if super::super::field_get_set::fetch_subclass_handle_id(inst as usize).is_none() { - super::super::attach_fetch_handle_for_construction( - inst, kind, args_ptr, args_len, - ); + let has_handle = inst_handle.with_mut_ptr::(|inst| { + super::super::field_get_set::fetch_subclass_handle_id(inst as usize).is_some() + }); + if !has_handle { + inst_handle.with_mut_ptr::(|inst| { + super::super::attach_fetch_handle_for_construction( + inst, kind, args_ptr, args_len, + ) + }); } } - // Re-read: `attach_fetch_handle_for_construction` allocates. + // Class-expression values can also extend Promise and reach this + // dynamic construct path. The synthesized default constructor does + // not call construct-only builtins as plain functions; attach the + // Promise backing here, matching the ClassRef path below. An + // explicit `super(executor)` has already installed it, so avoid + // invoking the executor twice. + ensure_promise_subclass_backing(&inst_handle, class_cid, args_ptr, args_len); + // Re-read: the fetch attachment and Promise executor both allocate. return crate::value::js_nanbox_pointer( inst_handle.get_raw_mut_ptr::() as i64 ); @@ -1366,33 +1402,9 @@ fn new_target_class_id(new_target: f64) -> Option { constructor_class_ref_id(new_target).or_else(|| class_object_class_id(new_target)) } -/// True when class `cid` (or an ancestor) `extends Promise` — its registered -/// dynamic-parent value resolves to the intrinsic `Promise` constructor. Used to -/// run `js_promise_subclass_init` on the dynamic (runtime) `new Subclass(exec)` -/// path, where codegen's `super()` Promise branch never emitted the init (e.g. -/// `NewPromiseCapability(Subclass)` inside a combinator, which calls the runtime -/// `js_new_function_construct` directly rather than a compiled `new`). -pub(crate) fn promise_parent_in_chain(class_id: u32) -> bool { - let mut cid = class_id; - let mut depth = 0u32; - while depth < 32 && cid != 0 { - let parent_val = js_get_dynamic_parent_value(cid); - if matches!( - identify_global_builtin_constructor(parent_val), - Some("Promise") - ) { - return true; - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} +include!("construct/class_return.rs"); +include!("construct/class_object.rs"); +include!("construct/promise_subclass.rs"); unsafe fn construct_registered_class_ref( target_cid: u32, @@ -1468,22 +1480,8 @@ unsafe fn construct_registered_class_ref( super::super::attach_fetch_handle_for_construction(inst, kind, args_ptr, args_len); } } - // ClassRef `new` of a Promise subclass — run the Promise constructor against - // a hidden backing cell (only when the compiled ctor's `super()` didn't - // already attach one). `NewPromiseCapability(Subclass)` reaches here. - if promise_parent_in_chain(target_cid) { - // Re-read: `attach_fetch_handle_for_construction` above allocates. - let inst_val = - crate::value::js_nanbox_pointer(inst_handle.get_raw_mut_ptr::() as i64); - if crate::promise::subclass_backing_promise(inst_val).is_none() { - let executor = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - crate::promise::js_promise_subclass_init(inst_val, executor); - } - } + // `NewPromiseCapability(Subclass)` reaches this dynamic ClassRef path. + ensure_promise_subclass_backing(&inst_handle, target_cid, args_ptr, args_len); // Re-read once more: the executor `js_promise_subclass_init` runs is user // code, so the last two blocks are both collection points. crate::value::js_nanbox_pointer(inst_handle.get_raw_mut_ptr::() as i64) @@ -1494,6 +1492,13 @@ unsafe fn construct_registered_class_ref( /// is an object (so a typed-array view should adopt it as its `[[Prototype]]`), /// or `None` when it is a primitive (so the default per-kind prototype applies). fn new_target_custom_object_prototype(new_target: f64) -> Option { + if let Some(class_id) = constructor_class_ref_id(new_target) { + let declared = super::class_decl_prototype_value(class_id); + if unsafe { super::super::value_is_object_like(declared) } { + return Some(declared.to_bits()); + } + return Some(super::super::class_prototype_ref_value(class_id).to_bits()); + } let bits = new_target.to_bits(); if (bits >> 48) != 0x7FFD { return None; @@ -1589,6 +1594,16 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( // `Object.getPrototypeOf` and `.constructor` resolve through it (test262 // `ctors*/use-custom-proto-if-object` / `use-default-proto-if-…`). if let Some(ta_name) = identify_global_builtin_constructor(func_value) { + // Symbol and BigInt are callable conversion functions but have no + // [[Construct]] slot. A distinct newTarget (the subclass case) must + // not turn either into a generic constructable closure. + if matches!(ta_name, "Symbol" | "BigInt") { + return if ta_name == "Symbol" { + crate::error::js_throw_symbol_constructor_type_error() + } else { + crate::error::js_throw_bigint_constructor_type_error() + }; + } // `Reflect.construct(Date, args, newTarget)` (#5989) — Next.js 16's // cacheComponents Date extension constructs through exactly this // shape: its installed wrapper runs @@ -1599,13 +1614,20 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( // `GetPrototypeFromConstructor(newTarget)` like the typed-array arm // below so `instanceof newTarget` and subclass prototypes hold. if ta_name == "Date" { - let proto_bits = new_target_custom_object_prototype(nt); - let result = js_new_function_construct(func_value, args_ptr, args_len); - if let Some(proto_bits) = proto_bits { + let scope = crate::gc::RuntimeHandleScope::new(); + let nt = scope.root_nanbox_f64(nt); + let func = scope.root_nanbox_f64(func_value); + let proto = new_target_custom_object_prototype(nt.get_nanbox_f64()) + .map(|bits| scope.root_heap_word_u64(bits)); + let result = js_new_function_construct(func.get_nanbox_f64(), args_ptr, args_len); + if let Some(proto) = proto { let jv = crate::value::JSValue::from_bits(result.to_bits()); if jv.is_pointer() { let addr = (jv.bits() & crate::value::POINTER_MASK) as usize; - super::super::prototype_chain::object_set_static_prototype(addr, proto_bits); + super::super::prototype_chain::object_set_static_prototype( + addr, + proto.get_heap_word_u64(), + ); } } return result; @@ -1630,11 +1652,57 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( // AllocateTypedArray, so a throwing `prototype` getter must surface // here even when later steps would also throw (test262 // `throw-type-error-before-custom-proto-access` agreement). - let proto_bits = new_target_custom_object_prototype(nt); - let result = js_new_function_construct(func_value, args_ptr, args_len); + let scope = crate::gc::RuntimeHandleScope::new(); + let nt = scope.root_nanbox_f64(nt); + let func = scope.root_nanbox_f64(func_value); + let proto = new_target_custom_object_prototype(nt.get_nanbox_f64()) + .map(|bits| scope.root_heap_word_u64(bits)); + let result = js_new_function_construct(func.get_nanbox_f64(), args_ptr, args_len); if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(result) { - if let Some(proto_bits) = proto_bits { - super::super::prototype_chain::object_set_static_prototype(addr, proto_bits); + if let Some(proto) = proto { + super::super::prototype_chain::object_set_static_prototype( + addr, + proto.get_heap_word_u64(), + ); + } + } + return result; + } + if matches!( + ta_name, + "ArrayBuffer" + | "SharedArrayBuffer" + | "DataView" + | "Boolean" + | "Number" + | "String" + | "RegExp" + | "Function" + ) { + let scope = crate::gc::RuntimeHandleScope::new(); + let nt = scope.root_nanbox_f64(nt); + let func = scope.root_nanbox_f64(func_value); + let proto = new_target_custom_object_prototype(nt.get_nanbox_f64()) + .map(|bits| scope.root_heap_word_u64(bits)); + let result = js_new_function_construct(func.get_nanbox_f64(), args_ptr, args_len); + if let Some(proto) = proto { + let bits = result.to_bits(); + let addr = if (bits >> 48) == 0x7FFD { + (bits & crate::value::POINTER_MASK) as usize + } else if (bits >> 48) == 0 + && crate::value::addr_class::is_plausible_heap_addr(bits as usize) + { + // ArrayBuffer and SharedArrayBuffer are represented by a + // raw BufferHeader pointer rather than a NaN-boxed object. + bits as usize + } else { + 0 + }; + if addr != 0 { + super::super::prototype_chain::object_set_static_prototype( + addr, + proto.get_heap_word_u64(), + ); } } return result; @@ -1721,80 +1789,6 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( inst_handle.get_nanbox_f64() } -fn constructor_return_overrides_this(value: f64) -> bool { - use crate::value::JSValue; - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - if is_callable_function_value(value) { - return true; - } - let raw = jv.as_pointer::(); - if raw.is_null() { - return false; - } - if super::super::is_arguments_object(raw as *const ObjectHeader) { - return true; - } - unsafe { - let arr = crate::array::clean_arr_ptr(raw as *const crate::array::ArrayHeader); - if !arr.is_null() { - return true; - } - if !is_valid_obj_ptr(raw as *const u8) { - return false; - } - let gc_header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - matches!( - (*gc_header).obj_type, - // Per spec, a constructor returning ANY Object overrides the - // implicit `this`. Promises are objects — a user constructor like - // `function P(exec){ return new Promise(...) }` (the - // `NewPromiseCapability` shape exercised by the Promise-combinator - // test262 cases) must yield that Promise, not the empty default. - // GC_TYPE_TEMPORAL: `new Temporal.Duration(...)` (and every other - // Temporal constructor) is dispatched through this generic path — - // the constructor thunk allocates a Temporal cell and returns it, so - // that cell must override the empty default `this` (#4687). - crate::gc::GC_TYPE_OBJECT - | crate::gc::GC_TYPE_ERROR - | crate::gc::GC_TYPE_PROMISE - | crate::gc::GC_TYPE_TEMPORAL - ) - } -} - -/// Apply ECMAScript constructor return-override semantics for an inlined -/// constructor body's explicit `return `. Given the implicit `this` -/// and the returned value: -/// - returned value is an Object → it becomes the construction result; -/// - returned value is `undefined` → result is `this`; -/// - returned value is any other primitive → for a derived constructor -/// (`class X extends Y`) this is a TypeError; for a base constructor the -/// primitive is ignored and the result is `this`. -/// `is_derived` is 1 for a class with an `extends` clause, 0 otherwise. -/// Refs class/subclass/derived-class-return-override-*. -#[no_mangle] -pub extern "C" fn js_ctor_return_override(this_val: f64, return_val: f64, is_derived: i32) -> f64 { - use crate::value::JSValue; - if constructor_return_overrides_this(return_val) { - return return_val; - } - let jv = JSValue::from_bits(return_val.to_bits()); - if jv.is_undefined() { - return this_val; - } - if is_derived != 0 { - crate::collection_iter::throw_type_error( - "Derived constructors may only return object or undefined", - ); - } - // Base constructor: a returned primitive is ignored. - this_val -} - /// Verify that a JSValue is a NaN-boxed pointer to a registered /// closure header. `js_native_call_value` itself doesn't validate the /// pointer shape — it dereferences whatever lower-48 bits it gets — so diff --git a/crates/perry-runtime/src/object/class_registry/construct/class_object.rs b/crates/perry-runtime/src/object/class_registry/construct/class_object.rs new file mode 100644 index 0000000000..1883a4d8a2 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct/class_object.rs @@ -0,0 +1,19 @@ +/// Link an instance constructed through a fresh class value to that +/// evaluation's distinct prototype object. Class-id dispatch alone follows +/// the shared template and cannot preserve per-evaluation inheritance. +fn link_class_object_instance_prototype(class_value: f64, instance: *mut ObjectHeader) { + let scope = crate::gc::RuntimeHandleScope::new(); + let class = scope.root_nanbox_f64(class_value); + let instance = scope.root_raw_mut_ptr(instance); + let class_obj = crate::value::JSValue::from_bits(class.get_nanbox_f64().to_bits()) + .as_pointer::(); + let prototype = + unsafe { super::super::field_get_set::class_object_prototype_value(class_obj) }; + let prototype = scope.root_heap_word_u64(prototype.bits()); + instance.with_mut_ptr::(|instance| { + super::super::prototype_chain::object_link_class_default_prototype( + instance as usize, + prototype.get_heap_word_u64(), + ) + }); +} diff --git a/crates/perry-runtime/src/object/class_registry/construct/class_return.rs b/crates/perry-runtime/src/object/class_registry/construct/class_return.rs new file mode 100644 index 0000000000..6da8ef6a1e --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct/class_return.rs @@ -0,0 +1,130 @@ +/// Construct a native built-in base with a declared Perry class as newTarget. +/// The native constructor supplies its internal slots; the distinct newTarget +/// supplies the subclass prototype used by `instanceof` and inherited methods. +#[no_mangle] +pub unsafe extern "C" fn js_builtin_subclass_construct( + class_id: u32, + name_ptr: *const u8, + name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + if class_id == 0 || name_ptr.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let target = super::super::js_get_global_this_builtin_value(name_ptr, name_len); + let new_target = super::super::class_constructor_ref_value(class_id); + js_new_function_construct_with_new_target(target, args_ptr, args_len, new_target) +} + +#[used] +static KEEP_JS_BUILTIN_SUBCLASS_CONSTRUCT: unsafe extern "C" fn( + u32, + *const u8, + usize, + *const f64, + usize, +) -> f64 = js_builtin_subclass_construct; + +fn constructor_return_overrides_this(value: f64) -> bool { + use crate::value::JSValue; + let jv = JSValue::from_bits(value.to_bits()); + // Typed arrays and buffer-backed exotic objects may be represented by a + // raw registered owner pointer rather than a NaN-boxed heap object. They + // are still ECMAScript Objects and a base constructor returning one must + // replace the derived constructor's provisional `this` binding. + if crate::typedarray_props::typed_array_addr_from_value(value).is_some() { + return true; + } + let bits = value.to_bits(); + let raw_addr = if jv.is_pointer() { + (bits & crate::value::POINTER_MASK) as usize + } else if (bits >> 48) == 0 + && crate::value::addr_class::is_plausible_heap_addr(bits as usize) + { + bits as usize + } else { + 0 + }; + if raw_addr != 0 && crate::buffer::is_registered_buffer(raw_addr) { + return true; + } + if !jv.is_pointer() { + return false; + } + if is_callable_function_value(value) { + return true; + } + // A Proxy is represented by a registered handle rather than a directly + // dereferenceable heap pointer. It is nevertheless an Object and therefore + // overrides the default receiver when returned from a constructor. Detect + // it before the raw object/array probes below inspect the pointer payload. + if crate::proxy::js_proxy_is_proxy(value) != 0 { + return true; + } + let raw = jv.as_pointer::(); + if raw.is_null() { + return false; + } + if super::super::is_arguments_object(raw as *const ObjectHeader) { + return true; + } + unsafe { + let arr = crate::array::clean_arr_ptr(raw as *const crate::array::ArrayHeader); + if !arr.is_null() { + return true; + } + let Some(gc_header) = crate::value::addr_class::try_read_gc_header(raw as usize) else { + return false; + }; + matches!( + gc_header.obj_type, + // Per spec, a constructor returning ANY Object overrides the + // implicit `this`. Promises are objects — a user constructor like + // `function P(exec){ return new Promise(...) }` (the + // `NewPromiseCapability` shape exercised by the Promise-combinator + // test262 cases) must yield that Promise, not the empty default. + // GC_TYPE_TEMPORAL: `new Temporal.Duration(...)` (and every other + // Temporal constructor) is dispatched through this generic path — + // the constructor thunk allocates a Temporal cell and returns it, so + // that cell must override the empty default `this` (#4687). + crate::gc::GC_TYPE_OBJECT + | crate::gc::GC_TYPE_ERROR + | crate::gc::GC_TYPE_PROMISE + | crate::gc::GC_TYPE_TEMPORAL + | crate::gc::GC_TYPE_MAP + | crate::gc::GC_TYPE_SET + | crate::gc::GC_TYPE_DATE_CELL + | crate::gc::GC_TYPE_REGEXP + ) + } +} + +/// Apply ECMAScript constructor return-override semantics for an inlined +/// constructor body's explicit `return `. Given the implicit `this` +/// and the returned value: +/// - returned value is an Object → it becomes the construction result; +/// - returned value is `undefined` → result is `this`; +/// - returned value is any other primitive → for a derived constructor +/// (`class X extends Y`) this is a TypeError; for a base constructor the +/// primitive is ignored and the result is `this`. +/// `is_derived` is 1 for a class with an `extends` clause, 0 otherwise. +/// Refs class/subclass/derived-class-return-override-*. +#[no_mangle] +pub extern "C" fn js_ctor_return_override(this_val: f64, return_val: f64, is_derived: i32) -> f64 { + use crate::value::JSValue; + if constructor_return_overrides_this(return_val) { + return return_val; + } + let jv = JSValue::from_bits(return_val.to_bits()); + if jv.is_undefined() { + return this_val; + } + if is_derived != 0 { + crate::collection_iter::throw_type_error( + "Derived constructors may only return object or undefined", + ); + } + // Base constructor: a returned primitive is ignored. + this_val +} diff --git a/crates/perry-runtime/src/object/class_registry/construct/promise_subclass.rs b/crates/perry-runtime/src/object/class_registry/construct/promise_subclass.rs new file mode 100644 index 0000000000..013739680c --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct/promise_subclass.rs @@ -0,0 +1,52 @@ +/// True when `class_id` or an ancestor extends the intrinsic Promise. +pub(crate) fn promise_parent_in_chain(class_id: u32) -> bool { + let mut cid = class_id; + let mut depth = 0u32; + while depth < 32 && cid != 0 { + let parent = js_get_dynamic_parent_value(cid); + if matches!(identify_global_builtin_constructor(parent), Some("Promise")) { + return true; + } + match get_parent_class_id(cid) { + Some(parent_id) if parent_id != 0 && parent_id != cid => { + cid = parent_id; + depth += 1; + } + _ => break, + } + } + false +} + +/// Install the hidden Promise backing on a dynamically-constructed subclass +/// unless an explicit `super(executor)` already did so. +unsafe fn ensure_promise_subclass_backing( + instance: &crate::gc::RuntimeHandle<'_>, + class_id: u32, + args_ptr: *const f64, + args_len: usize, +) { + if !promise_parent_in_chain(class_id) { + return; + } + let has_backing = instance.with_mut_ptr::(|instance| { + crate::promise::subclass_backing_promise(crate::value::js_nanbox_pointer( + instance as i64, + )) + .is_some() + }); + if has_backing { + return; + } + let executor = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + instance.with_mut_ptr::(|instance| { + crate::promise::js_promise_subclass_init( + crate::value::js_nanbox_pointer(instance as i64), + executor, + ) + }); +} diff --git a/crates/perry-runtime/src/object/class_registry/dispatch.rs b/crates/perry-runtime/src/object/class_registry/dispatch.rs index baffe9b001..2157217063 100644 --- a/crates/perry-runtime/src/object/class_registry/dispatch.rs +++ b/crates/perry-runtime/src/object/class_registry/dispatch.rs @@ -401,6 +401,50 @@ pub(crate) unsafe fn call_vtable_method( param_count: u32, has_synthetic_arguments: bool, has_rest: bool, +) -> f64 { + call_vtable_method_inner( + func_ptr, + this, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + None, + ) +} + +pub(crate) unsafe fn call_vtable_method_with_private_brand( + func_ptr: usize, + this: i64, + args_ptr: *const f64, + args_len: usize, + param_count: u32, + has_synthetic_arguments: bool, + has_rest: bool, + private_brand: f64, +) -> f64 { + call_vtable_method_inner( + func_ptr, + this, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + Some(private_brand), + ) +} + +unsafe fn call_vtable_method_inner( + func_ptr: usize, + this: i64, + args_ptr: *const f64, + args_len: usize, + param_count: u32, + has_synthetic_arguments: bool, + has_rest: bool, + explicit_private_brand: Option, ) -> f64 { // (`arg_or_undefined` — the spec-correct missing-argument padding — is a // module-level helper now, shared with `call_fn_with_this_and_args`.) @@ -490,13 +534,21 @@ pub(crate) unsafe fn call_vtable_method( param_count, MAX_VTABLE_DISPATCH_ARITY ); - call_fn_with_this_and_args( + let private_brand = explicit_private_brand + .or_else(|| crate::object::private_evaluation_brand_value(this_f64)) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let derived_super_depth = crate::object::derived_super_binding_stack_savepoint(); + crate::object::private_lexical_brand_push(private_brand); + let result = call_fn_with_this_and_args( func_ptr, this_f64, call_args_ptr, call_args_len, param_count_usize, - ) + ); + crate::object::private_lexical_brand_pop(); + crate::object::derived_super_binding_stack_restore(derived_super_depth); + result } /// Walk the class parent chain looking for a recorded fetch-builtin parent 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 447d41dbd7..95690f6b51 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -90,6 +90,14 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, mut parent_val // and Event-shaped dispatch gates. Builtins without a class id keep the // parentless baseline (no throw — they ARE constructors). if let Some(name) = identify_global_builtin_constructor(parent_value) { + // `%Proxy%` is constructable but intentionally has no usable + // `prototype` property, so it fails ClassDefinitionEvaluation's + // prototype-object-or-null check. + if name == "Proxy" { + super::super::object_ops::throw_object_type_error( + b"Class extends value has invalid prototype property", + ); + } let parent_cid = super::super::instanceof::global_builtin_constructor_class_id(name); if parent_cid != 0 && parent_cid != class_id { register_class(class_id, parent_cid); @@ -811,6 +819,8 @@ pub(crate) fn lookup_class_symbol_method_in_chain( }) } +include!("parent_static/private_and_dynamic.rs"); + /// Presence-only check (`[[HasProperty]]`, never `[[Get]]`) for a Symbol-keyed /// METHOD or ACCESSOR declared on `class_id` or any ancestor. These computed /// members register into `CLASS_SYMBOL_METHODS` / `CLASS_SYMBOL_ACCESSORS`, which @@ -988,12 +998,17 @@ pub(crate) unsafe fn class_static_accessor_getter_value( name: &str, receiver: f64, ) -> Option { - let guard = CLASS_STATIC_ACCESSORS.read().ok()?; - let map = guard.as_ref()?; + let guard = CLASS_STATIC_ACCESSORS.read().ok(); + let map = guard.as_ref().and_then(|guard| guard.as_ref()); let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { - if let Some(accessors) = map.get(&cid) { + // A descriptor installed by `defineProperty` replaces an existing + // class-body accessor at the same inheritance level. + if let Some(result) = class_dynamic_static_accessor_getter_value(cid, name, receiver) { + return Some(result); + } + if let Some(accessors) = map.and_then(|map| map.get(&cid)) { if let Some(&(getter, _)) = accessors.get(name) { if getter == 0 { return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); @@ -1028,17 +1043,17 @@ pub(crate) unsafe fn class_static_accessor_setter_apply( receiver: f64, value: f64, ) -> bool { - let guard = match CLASS_STATIC_ACCESSORS.read() { - Ok(g) => g, - Err(_) => return false, - }; - let Some(map) = guard.as_ref() else { - return false; - }; + let guard = CLASS_STATIC_ACCESSORS.read().ok(); + let map = guard.as_ref().and_then(|guard| guard.as_ref()); let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { - if let Some(accessors) = map.get(&cid) { + if let Some(applied) = + class_dynamic_static_accessor_setter_apply(cid, name, receiver, value) + { + return applied; + } + if let Some(accessors) = map.and_then(|map| map.get(&cid)) { if let Some(&(_, setter)) = accessors.get(name) { if setter != 0 { // Mirror the getter path: the compiled static-accessor @@ -1448,10 +1463,17 @@ pub unsafe extern "C" fn js_class_static_method_call( if name_ptr.is_null() || name_len == 0 { return receiver; } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + let storage_name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { Ok(s) => s, Err(_) => return receiver, }; + let private_name = if storage_name.starts_with("# Option<(usize, u32, bool)> { + CLASS_SYMBOL_METHODS.with(|table| { + table + .read() + .ok()? + .as_ref()? + .get(&(class_id, sym_key, is_static)) + .copied() + }) +} + +pub(crate) fn class_own_symbol_accessor_ptrs( + class_id: u32, + sym_key: usize, + is_static: bool, +) -> Option<(usize, usize)> { + CLASS_SYMBOL_ACCESSORS.with(|table| { + table + .read() + .ok()? + .as_ref()? + .get(&(class_id, sym_key, is_static)) + .copied() + }) +} + +fn dynamic_static_accessor_key(name: &str) -> String { + let mut key = String::with_capacity(name.len() + 24); + key.push('\0'); + key.push_str("perry:class-static:"); + key.push_str(name); + key +} + +fn shared_dynamic_static_accessor_owner(class_id: u32) -> usize { + let value = class_decl_prototype_value(class_id); + let bits = value.to_bits(); + if (bits >> 48) == 0x7FFD { + (bits & crate::value::POINTER_MASK) as usize + } else { + 0 + } +} + +/// Resolve the constructor object which owns a dynamic static descriptor at +/// this point in the receiver's per-evaluation heritage chain. Heap class +/// values with the same template id are distinct constructor objects; an +/// immediate ClassRef continues to use the shared declared prototype owner. +fn dynamic_static_accessor_owner(class_id: u32, receiver: f64) -> usize { + let mut current = receiver; + let mut depth = 0usize; + while depth < 32 { + if is_class_object_value(current) { + let object = crate::value::JSValue::from_bits(current.to_bits()) + .as_pointer::(); + if object.is_null() { + return 0; + } + let current_id = unsafe { (*object).class_id }; + if current_id == class_id { + return object as usize; + } + let Some(parent) = class_object_pinned_parent(object) else { + return 0; + }; + current = parent; + depth += 1; + continue; + } + if super::super::class_ref_id(current).is_some() { + return shared_dynamic_static_accessor_owner(class_id); + } + return 0; + } + 0 +} + +fn dynamic_static_accessor_storage_key(owner: usize, name: &str) -> String { + if is_class_object_ptr(owner as *const u8) { + name.to_string() + } else { + dynamic_static_accessor_key(name) + } +} + +/// Store an accessor installed dynamically on a class constructor through +/// `Object.defineProperty(C, key, { get, set })`. Class constructors are +/// immediate ClassRef values rather than heap objects, so keep the rooted +/// accessor descriptor on the class's materialized prototype under an +/// internal key; the public static lookup paths consult it by class id. +pub(crate) fn register_class_dynamic_static_accessor( + class_id: u32, + receiver: f64, + name: &str, + get_bits: Option, + set_bits: Option, + enumerable: Option, + configurable: Option, +) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let get = scope.root_nanbox_u64(get_bits.unwrap_or(0)); + let set = scope.root_nanbox_u64(set_bits.unwrap_or(0)); + let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); + if owner == 0 { + return; + } + let key = dynamic_static_accessor_storage_key(owner, name); + let existing = crate::object::get_accessor_descriptor(owner, &key).unwrap_or_default(); + crate::object::set_accessor_descriptor( + owner, + key.clone(), + crate::object::AccessorDescriptor { + get: get_bits.map(|_| get.get_nanbox_u64()).unwrap_or(existing.get), + set: set_bits.map(|_| set.get_nanbox_u64()).unwrap_or(existing.set), + }, + ); + let existing_attrs = if is_class_object_ptr(owner as *const u8) { + crate::object::get_property_attrs(owner, &key) + .map(|attrs| (attrs.enumerable(), attrs.configurable())) + } else { + class_static_defined_attrs(class_id, name).map(|(_, enumerable, configurable)| { + (enumerable, configurable) + }) + }; + let enumerable = enumerable + .or_else(|| existing_attrs.map(|attrs| attrs.0)) + .unwrap_or(false); + let configurable = configurable + .or_else(|| existing_attrs.map(|attrs| attrs.1)) + .unwrap_or(false); + if is_class_object_ptr(owner as *const u8) { + crate::object::set_property_attrs( + owner, + key, + crate::object::PropertyAttrs::new(false, enumerable, configurable), + ); + } else { + class_static_set_defined_attrs(class_id, name, false, enumerable, configurable); + } +} + +pub(crate) fn class_dynamic_static_accessor_descriptor( + class_id: u32, + name: &str, + receiver: f64, +) -> Option<(crate::object::AccessorDescriptor, crate::object::PropertyAttrs)> { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); + if owner == 0 { + return None; + } + let key = dynamic_static_accessor_storage_key(owner, name); + let descriptor = crate::object::get_accessor_descriptor(owner, &key)?; + let attrs = if is_class_object_ptr(owner as *const u8) { + crate::object::get_property_attrs(owner, &key) + } else { + class_static_defined_attrs(class_id, name).map(|(_, enumerable, configurable)| { + crate::object::PropertyAttrs::new(false, enumerable, configurable) + }) + } + .unwrap_or(crate::object::PropertyAttrs::new(false, false, false)); + Some((descriptor, attrs)) +} + +pub(crate) unsafe fn class_dynamic_static_accessor_getter_value( + class_id: u32, + name: &str, + receiver: f64, +) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); + let descriptor = (owner != 0) + .then(|| { + crate::object::get_accessor_descriptor( + owner, + &dynamic_static_accessor_storage_key(owner, name), + ) + }) + .flatten()?; + if descriptor.get == 0 { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + Some(f64::from_bits( + crate::object::invoke_accessor_getter(descriptor.get, receiver.get_nanbox_f64()).bits(), + )) +} + +/// `Some(true)` means a setter was invoked, `Some(false)` means an accessor +/// exists but has no setter, and `None` means this class has no such dynamic +/// accessor. +pub(crate) unsafe fn class_dynamic_static_accessor_setter_apply( + class_id: u32, + name: &str, + receiver: f64, + value: f64, +) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let value = scope.root_nanbox_f64(value); + let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); + let descriptor = (owner != 0) + .then(|| { + crate::object::get_accessor_descriptor( + owner, + &dynamic_static_accessor_storage_key(owner, name), + ) + }) + .flatten()?; + if descriptor.set == 0 { + return Some(false); + } + crate::object::invoke_accessor_setter( + descriptor.set, + receiver.get_nanbox_f64(), + value.get_nanbox_f64(), + ); + Some(true) +} + +/// Invoke an instance-private getter on its lexical declaring class. Unlike +/// ordinary public accessor lookup, private names are not inherited and must +/// not be shadowed by a public string property with the same spelling. +pub(crate) unsafe fn class_private_instance_getter_value( + class_id: u32, + name: &str, + receiver: f64, +) -> Option { + let guard = CLASS_VTABLE_REGISTRY.read().ok()?; + let vtable = guard.as_ref()?.get(&class_id)?; + let &getter = vtable.getters.get(name)?; + if getter == 0 { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); + Some(f(receiver)) +} + +/// Invoke an instance-private setter on its lexical declaring class. +pub(crate) unsafe fn class_private_instance_setter_apply( + class_id: u32, + name: &str, + receiver: f64, + value: f64, +) -> bool { + let guard = match CLASS_VTABLE_REGISTRY.read() { + Ok(guard) => guard, + Err(_) => return false, + }; + let Some(vtable) = guard.as_ref().and_then(|registry| registry.get(&class_id)) else { + return false; + }; + let Some(&setter) = vtable.setters.get(name) else { + return false; + }; + if setter != 0 { + let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter); + let _ = f(receiver, value); + } + true +} + +pub(crate) unsafe fn call_private_static_method_for_owner( + owner_class_id: u32, + name: &str, + this_value: f64, + private_brand: f64, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let (func_ptr, param_count, has_rest) = CLASS_STATIC_METHODS + .read() + .ok()? + .as_ref()? + .get(&owner_class_id)? + .get(name) + .copied()?; + let scope = crate::gc::RuntimeHandleScope::new(); + let this_value = scope.root_nanbox_f64(this_value); + let private_brand = scope.root_nanbox_f64(private_brand); + let previous_this = crate::object::js_implicit_this_set(this_value.get_nanbox_f64()); + crate::object::static_private_owner_push(private_brand.get_nanbox_f64()); + crate::object::private_lexical_brand_push(private_brand.get_nanbox_f64()); + crate::object::static_this_arm_if_unarmed(this_value.get_nanbox_f64()); + let result = call_registered_static_method(func_ptr, args_ptr, args_len, param_count, has_rest); + crate::object::static_this_disarm(); + crate::object::private_lexical_brand_pop(); + crate::object::static_private_owner_pop(); + crate::object::js_implicit_this_set(previous_this); + Some(result) +} diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index e875a438cd..59ffe49062 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -2,6 +2,27 @@ use super::*; use std::collections::HashMap; use std::sync::RwLock; +const CLASS_LEXICAL_BINDING_KEY: &str = "#"; + +/// Read/write storage for the outer mutable binding introduced by a class +/// declaration. The class body's same-spelled inner binding is lowered +/// directly to its ClassRef and never reaches these helpers. +#[no_mangle] +pub extern "C" fn js_class_lexical_binding_get(class_ref: f64) -> f64 { + let Some(class_id) = class_ref_id(class_ref) else { + return class_ref; + }; + class_own_static_field_value(class_id, CLASS_LEXICAL_BINDING_KEY).unwrap_or(class_ref) +} + +#[no_mangle] +pub extern "C" fn js_class_lexical_binding_set(class_ref: f64, value: f64) -> f64 { + if let Some(class_id) = class_ref_id(class_ref) { + class_dynamic_prop_root_store(class_id, CLASS_LEXICAL_BINDING_KEY, value); + } + value +} + /// Register a static field value on a class so `Cls.field` (when `Cls` is /// accessed via dynamic dispatch — e.g. through an Any-typed local) finds /// the value via the runtime path. Codegen calls this at module init for @@ -24,6 +45,33 @@ pub unsafe extern "C" fn js_class_register_static_field( class_dynamic_prop_root_store(class_id, name, value); } +/// Read a computed instance-field key resolved at ClassDefinitionEvaluation. +/// Fresh class values carry the hidden slot on their heap class object; plain +/// class references use the class-id static side table. +#[no_mangle] +pub unsafe extern "C" fn js_class_computed_field_key( + receiver: f64, + class_id: u32, + name_ptr: *const u8, + name_len: usize, +) -> f64 { + if name_ptr.is_null() || name_len == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + if let Some(owner) = crate::object::private_evaluation_brand_value(receiver) { + let value = crate::object::js_object_get_own_field_or_undef(owner, name_ptr, name_len); + if value.to_bits() != crate::value::TAG_UNDEFINED { + return value; + } + } + let bytes = std::slice::from_raw_parts(name_ptr, name_len); + let Ok(name) = std::str::from_utf8(bytes) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + class_own_static_field_value(class_id, name) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) +} + crate::perry_thread_local! { /// Issue #838: JS-classic prototype method assignment. /// diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 6625220087..401b615274 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -396,6 +396,47 @@ unsafe fn resolve_proto_chain_field_inner( unsafe { super::super::field_get_set::own_data_field_by_name(decl_proto, key) } { if !value.is_undefined() { + // Declared methods are mirrored onto the template's shared + // reflective prototype. For a ClassExprFresh instance, + // substitute the method closure owned by this particular + // class evaluation; otherwise separate factory calls share + // a lexical private brand and cross-calls incorrectly pass. + if let Some(receiver) = receiver { + let key_ptr = crate::string::string_data(key); + let key_len = (*key).byte_len as usize; + if let Ok(name) = + std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) + { + let name = name.to_string(); + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_u64(value.bits()); + let receiver = scope.root_heap_word_u64(receiver.to_bits()); + if super::super::native_module::class_has_own_method(cid, &name) + && value.get_nanbox_u64() + == super::super::native_module::class_prototype_method_value_for_name( + cid, &name, + ) + .to_bits() + { + let receiver = f64::from_bits(receiver.get_heap_word_u64()); + if let Some(brand) = + super::super::private_evaluation_brand_value(receiver) + { + let brand_obj = crate::value::JSValue::from_bits(brand.to_bits()) + .as_pointer::(); + if !brand_obj.is_null() + && js_object_get_class_id(brand_obj) == cid + { + let method = super::super::native_module::class_evaluation_method_value_for_name( + cid, &name, brand, + ); + return Some(JSValue::from_bits(method.to_bits())); + } + } + } + return Some(JSValue::from_bits(value.get_nanbox_u64())); + } + } return Some(value); } } diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index de8d6d0bee..6830919e13 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -136,7 +136,7 @@ pub(crate) fn class_own_enumerable_field_names(class_id: u32) -> Vec { .map(|props| { props .keys() - .filter(|k| !k.starts_with('#')) + .filter(|k| !crate::object::is_internal_runtime_key(k)) // #7190: a key installed by `Object.defineProperty` without // `enumerable: true` shares this table with static fields // but is NOT enumerable. @@ -576,7 +576,7 @@ pub(crate) fn class_decl_prototype_object(class_id: u32) -> *mut ObjectHeader { }) } -fn class_decl_prototype_method_names(class_id: u32) -> Vec { +pub(crate) fn class_decl_prototype_method_names(class_id: u32) -> Vec { let mut names = Vec::new(); if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { if let Some(vtable) = registry.as_ref().and_then(|reg| reg.get(&class_id)) { @@ -692,14 +692,24 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; } - let parent_proto_bits = get_parent_class_id(class_id) - .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) - .and_then(|parent_id| { - let parent_proto = class_decl_prototype_value(parent_id); - let parent_bits = parent_proto.to_bits(); - ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) - }) - .or_else(global_object_prototype_bits); + let dynamic_parent = js_get_dynamic_parent_value(class_id); + let null_heritage = dynamic_parent.to_bits() == crate::value::TAG_NULL; + let parent_proto_bits = if null_heritage { + // A class extending null creates a prototype object whose + // [[Prototype]] is null, not Object.prototype. Record TAG_NULL + // explicitly so "no custom link" is not mistaken for the ordinary + // Object.prototype default. + Some(crate::value::TAG_NULL) + } else { + get_parent_class_id(class_id) + .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) + .and_then(|parent_id| { + let parent_proto = class_decl_prototype_value(parent_id); + let parent_bits = parent_proto.to_bits(); + ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) + }) + .or_else(global_object_prototype_bits) + }; if let Some(bits) = parent_proto_bits { super::super::prototype_chain::object_set_static_prototype(proto as usize, bits); } diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index eee3d873b9..decb9bcb65 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -192,6 +192,25 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu && crate::symbol::js_is_symbol(key_value) == 0 { if let Some(method_name) = metadata_key_to_string(key_value) { + let class_obj = extract_obj_ptr(obj_value); + if !class_obj.is_null() { + let class_id = super::js_object_get_class_id(class_obj); + if let Some((acc, attrs)) = + super::class_registry::class_dynamic_static_accessor_descriptor( + class_id, + &method_name, + obj_value, + ) + { + let undef = crate::value::TAG_UNDEFINED; + return build_accessor_descriptor( + f64::from_bits(if acc.get == 0 { undef } else { acc.get }), + f64::from_bits(if acc.set == 0 { undef } else { acc.set }), + attrs.enumerable(), + attrs.configurable(), + ); + } + } // #6943: `js_string_coerce` allocates for every non-heap-string // key and can run a user `toString` / `valueOf` for an object // key, so it can trigger a GC that **evacuates**. `obj` — the @@ -208,6 +227,7 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if !obj.is_null() && !key_str.is_null() && !own_key_present(obj, key_str) { let class_id = super::js_object_get_class_id(obj as *const ObjectHeader); if class_id != 0 + && !method_name.starts_with('#') && !super::class_registry::class_is_key_deleted(class_id, &method_name) && super::class_registry::class_has_own_static_method( class_id, @@ -223,29 +243,6 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu } } - // Private elements (`#x`) are stored on the static side / in a class - // instance's keys_array but are never reflectable own properties, so - // their descriptor is always undefined. (Plain `{"#fff": 1}` literals - // carry class_id 0 and are handled by the ordinary path below.) - { - let kjv = crate::JSValue::from_bits(key_value.to_bits()); - let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - if let Some(b) = crate::string::js_string_key_bytes(kjv, &mut buf) { - if b.first() == Some(&b'#') { - let is_class = class_ref_id(obj_value).is_some() || { - let obj = extract_obj_ptr(obj_value); - !obj.is_null() - && (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 - && crate::object::is_valid_obj_ptr(obj as *const u8) - && (*obj).class_id != 0 - }; - if is_class { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - } - } - } - // #2818: string primitives box to String objects whose own // properties are the index keys "0".."len-1" (writable:false, // enumerable:true, configurable:false) plus "length" @@ -365,6 +362,37 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if super::class_registry::class_is_key_deleted(class_id, &method_name) { return f64::from_bits(crate::value::TAG_UNDEFINED); } + // Private registry entries retain their source spelling, but + // a public computed static field named `"#x"` is a distinct + // String property and must remain reflectable. + if method_name.starts_with('#') { + if super::class_prototype_ref_id(obj_value).is_none() { + if let Some(v) = super::class_registry::class_own_static_field_value( + class_id, + &method_name, + ) { + return build_data_descriptor(v, true, true, true); + } + } + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + if super::class_prototype_ref_id(obj_value).is_none() { + if let Some((acc, attrs)) = + super::class_registry::class_dynamic_static_accessor_descriptor( + class_id, + &method_name, + obj_value, + ) + { + let undef = crate::value::TAG_UNDEFINED; + return build_accessor_descriptor( + f64::from_bits(if acc.get == 0 { undef } else { acc.get }), + f64::from_bits(if acc.set == 0 { undef } else { acc.set }), + attrs.enumerable(), + attrs.configurable(), + ); + } + } // `C.prototype` is a non-writable, non-enumerable, non-configurable // own data property of the class constructor (ECMA-262 // MakeConstructor). Only the constructor ref carries it — the @@ -423,14 +451,14 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu true, ); } - if method_name == "constructor" || class_has_own_method(class_id, &method_name) { + if super::class_prototype_ref_id(obj_value).is_some() + && (method_name == "constructor" + || class_has_own_method(class_id, &method_name)) + { let value = if method_name == "constructor" - && super::class_prototype_ref_id(obj_value).is_some() - && class_has_own_method(class_id, &method_name) + && !class_has_own_method(class_id, &method_name) { - class_prototype_method_value_for_name(class_id, &method_name) - } else if method_name == "constructor" { - obj_value + super::class_constructor_ref_value(class_id) } else { class_prototype_method_value_for_name(class_id, &method_name) }; @@ -698,6 +726,17 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if let Some(desc) = super::arguments_object_descriptor(obj, key_str) { return desc; } + if crate::array::is_array_subclass_value(obj_value) && key_rust.as_deref() == Some("length") + { + let length = crate::object::js_object_get_field_by_name(obj, key_str); + let frozen = + (*crate::object::gc_header_for(obj))._reserved & crate::gc::OBJ_FLAG_FROZEN != 0; + let writable = !frozen + && get_property_attrs(obj as usize, "length") + .map(|attrs| attrs.writable()) + .unwrap_or(true); + return build_data_descriptor(f64::from_bits(length.bits()), writable, false, false); + } if (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { let gc_header = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; @@ -940,7 +979,47 @@ pub(crate) unsafe fn handle_own_names_raw_array( pub(crate) unsafe fn symbol_own_property_descriptor(obj_value: f64, key_value: f64) -> f64 { let owner = crate::symbol::obj_key_from_f64(obj_value); let sym_key = crate::symbol::sym_key_from_f64(key_value); - if owner == 0 || sym_key == 0 { + if sym_key == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + // Computed Symbol class members live in the class registries rather than + // the generic per-object Symbol table. They are nevertheless own + // properties of the constructor/prototype and must reflect as method or + // accessor descriptors. + let class_owner = if let Some(cid) = super::class_ref_id(obj_value) { + Some((cid, super::class_prototype_ref_id(obj_value).is_none())) + } else if owner != 0 { + super::class_registry::class_id_for_decl_prototype_object(owner).map(|cid| (cid, false)) + } else { + None + }; + if let Some((cid, is_static)) = class_owner { + let display_name = crate::symbol::symbol_function_name(sym_key); + if let Some((get, set)) = + super::class_registry::class_own_symbol_accessor_ptrs(cid, sym_key, is_static) + { + return build_accessor_descriptor( + super::class_registry::class_accessor_function_value(get, false, &display_name), + super::class_registry::class_accessor_function_value(set, true, &display_name), + false, + true, + ); + } + if let Some((func_ptr, param_count, has_rest)) = + super::class_registry::class_own_symbol_method(cid, sym_key, is_static) + { + let value = super::build_symbol_bound_method_closure( + obj_value, + func_ptr, + param_count, + has_rest, + is_static, + &display_name, + ); + return build_data_descriptor(value, true, false, true); + } + } + if owner == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } let attrs = crate::symbol::get_symbol_property_attrs(owner, sym_key) @@ -1191,19 +1270,25 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { vtable.methods.keys().cloned().collect(); method_names.sort(); for name in method_names { - push_unique_name(&mut names, name); + if !name.starts_with('#') { + push_unique_name(&mut names, name); + } } let mut getter_names: Vec = vtable.getters.keys().cloned().collect(); getter_names.sort(); for name in getter_names { - push_unique_name(&mut names, name); + if !name.starts_with('#') { + push_unique_name(&mut names, name); + } } let mut setter_names: Vec = vtable.setters.keys().cloned().collect(); setter_names.sort(); for name in setter_names { - push_unique_name(&mut names, name.clone()); + if !name.starts_with('#') { + push_unique_name(&mut names, name); + } } } } @@ -1215,7 +1300,9 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { let mut method_names: Vec = map.keys().cloned().collect(); method_names.sort(); for name in method_names { - push_unique_name(&mut names, name); + if !name.starts_with('#') { + push_unique_name(&mut names, name); + } } } } @@ -1224,7 +1311,9 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { let mut accessor_names: Vec = map.keys().cloned().collect(); accessor_names.sort(); for name in accessor_names { - push_unique_name(&mut names, name); + if !name.starts_with('#') { + push_unique_name(&mut names, name); + } } } } @@ -1233,15 +1322,14 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { let mut prop_names: Vec = props.keys().cloned().collect(); prop_names.sort(); for name in prop_names { - push_unique_name(&mut names, name); + if !super::field_get_set::is_internal_runtime_key(&name) { + push_unique_name(&mut names, name); + } } } }); } - // Private elements (`#x`) live on the static side / prototype - // vtable under `#`-prefixed keys but are never reflectable own - // properties of `C` or `C.prototype`. - names.retain(|n| !n.starts_with('#')); + names.retain(|n| !super::field_get_set::is_internal_runtime_key(n)); sort_property_names_ecma(&mut names); let result = crate::array::js_array_alloc(names.len() as u32); for name in names { @@ -1407,9 +1495,8 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { None => j as u32, } }; - // Private elements (`#x`) live in a class instance's keys_array but are - // never reflectable own properties. Drop them for class instances - // (class_id != 0); plain `{"#fff": 1}` literals keep class_id 0. + // Drop only compiler/runtime storage keys. A user String key beginning + // with `#` is still an ordinary reflectable property. let hide_private = (*obj).class_id != 0; let hide_wasi_state = crate::wasi::is_wasi_import_object(obj) || crate::wasi::is_wasi_instance(f64::from_bits( @@ -1421,8 +1508,7 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { let key_val = crate::array::js_array_get(keys, pos(i)); if hide_private || hide_wasi_state { if let Some(b) = crate::string::js_string_key_bytes(key_val, &mut sso_buf) { - if b.first() == Some(&b'#') - || super::field_get_set::is_internal_runtime_key_bytes(b) + if super::field_get_set::is_internal_runtime_key_bytes(b) || (hide_wasi_state && b.starts_with(b"__wasi")) { continue; diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 40007be7a4..9e77c94d52 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -248,6 +248,7 @@ pub(crate) use accessors::{ ordinary_object_prototype_property_value, own_data_field_by_name, primitive_builtin_prototype_property, primitive_object_prototype_accessor, string_index_value, }; +pub(crate) use class_object_props::class_object_prototype_value; pub(crate) use crypto_key::{ crypto_key_property_value, CLASS_ID_BOXED_BIGINT, CLASS_ID_BOXED_BOOLEAN, CLASS_ID_BOXED_NUMBER, CLASS_ID_BOXED_STRING, CLASS_ID_BOXED_SYMBOL, @@ -276,13 +277,18 @@ pub use has_property::{js_in_operator, js_object_has_property}; #[cfg(test)] pub(crate) use ic_miss::primitive_proto_method_name_static; pub(crate) use ic_miss::{ - bind_primitive_proto_method_static, is_array_method_value_name, set_method_value_name, - stamp_private_evaluation_brand, timer_handle_method_name_static, + bind_primitive_proto_method_static, is_array_method_value_name, 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, private_member_call_by_name, private_member_get_by_name, + private_member_set_by_name, scan_private_lexical_brand_roots_mut, set_method_value_name, + stamp_private_evaluation_brand, take_private_method_call_hint, take_private_method_owner_hint, + timer_handle_method_name_static, }; pub use ic_miss::{ - js_object_get_field_by_name_f64, js_object_get_field_by_property_id_f64, - js_object_get_field_ic_miss, js_object_set_field_by_property_id, js_private_brand_check, - js_private_guard, PicCache, PIC_CACHE_WORDS, + js_class_field_add, js_object_get_field_by_name_f64, js_object_get_field_by_property_id_f64, + js_object_get_field_ic_miss, js_object_set_field_by_property_id, js_private_brand_add, + js_private_brand_check, js_private_field_add, js_private_guard, PicCache, PIC_CACHE_WORDS, }; #[cfg(test)] 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 bb44ee389f..a77ad959ed 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,18 +5,126 @@ use super::*; +const CLASS_EVALUATION_PROTOTYPE_KEY: &[u8] = b"#"; + +/// Materialize the distinct prototype object created by one evaluation of a +/// heap class expression/declaration. Template class ids still own dispatch, +/// 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 { + 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)) + }); + if let Some(existing) = existing.filter(|value| !value.is_undefined()) { + return f64::from_bits(existing.bits()); + } + + let class_id = class.with_mut_ptr::(|class| (*class).class_id); + let proto = scope.root_raw_mut_ptr(js_object_alloc(class_id, 0)); + + let constructor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + let constructor_key = scope.root_string_ptr(constructor_key); + let class_value = class + .with_mut_ptr::(|class| crate::value::js_nanbox_pointer(class as i64)); + proto.with_mut_ptr::(|proto| { + constructor_key.with_const_ptr::(|key| { + js_object_set_field_by_name(proto, key, class_value) + }); + set_builtin_property_attrs( + proto as usize, + "constructor".to_string(), + PropertyAttrs::new(true, false, true), + ); + }); + + for name in super::super::class_registry::class_decl_prototype_method_names(class_id) { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + 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, + ); + proto.with_mut_ptr::(|proto| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name(proto, key, method) + }); + set_builtin_property_attrs(proto as usize, name, PropertyAttrs::new(true, false, true)); + }); + } + + // 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()); + 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); + proto.with_mut_ptr::(|proto| { + super::super::prototype_chain::object_set_static_prototype( + proto as usize, + 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) + }) + }); + proto.with_mut_ptr::(|proto| crate::value::js_nanbox_pointer(proto as i64)) +} + /// #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 /// tsc/tslib decorator code can inspect and mutate method descriptors. -pub(super) unsafe fn class_object_prototype_value(obj: *const ObjectHeader) -> JSValue { - let class_id = (*obj).class_id; - let value = super::super::class_registry::class_decl_prototype_value(class_id); - if value.to_bits() == crate::value::TAG_UNDEFINED { - let value = super::super::class_prototype_ref_value(class_id); - return JSValue::from_bits(value.to_bits()); - } - JSValue::from_bits(value.to_bits()) +pub(crate) unsafe fn class_object_prototype_value(obj: *const ObjectHeader) -> JSValue { + JSValue::from_bits(class_evaluation_prototype_value(obj).to_bits()) } /// Resolve `.name` for an `OBJECT_TYPE_CLASS` heap object. An explicit diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index 6ff0c44d6a..5c4fb9df03 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -1269,7 +1269,7 @@ pub extern "C" fn js_object_keys(obj: *const ObjectHeader) -> *mut ArrayHeader { Ok(s) => s, Err(_) => continue, }; - if (hide_private && (key_str.starts_with('#') || is_internal_runtime_key(key_str))) + if (hide_private && is_internal_runtime_key(key_str)) || (hide_wasi_state && key_str.starts_with("__wasi")) { continue; @@ -1289,10 +1289,8 @@ pub extern "C" fn js_object_keys(obj: *const ObjectHeader) -> *mut ArrayHeader { } /// Get the values of an object as an array -/// True when `obj` is a class instance (`class_id != 0`) and `key_val` names a -/// private element (`#x`). Private elements physically live in the instance -/// keys_array but are never enumerable/reflectable properties. Plain object -/// literals keep `class_id == 0`, so `{"#fff": 1}` stays visible. +/// True when `key_val` names compiler/runtime-only private storage on a class +/// instance. Public String keys such as `"#x"` remain visible. pub(crate) unsafe fn instance_private_key_hidden( obj: *const ObjectHeader, key_val: crate::JSValue, @@ -1302,7 +1300,7 @@ pub(crate) unsafe fn instance_private_key_hidden( } let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; crate::string::js_string_key_bytes(key_val, &mut buf) - .map(|b| b.first() == Some(&b'#') || is_internal_runtime_key_bytes(b)) + .map(is_internal_runtime_key_bytes) .unwrap_or(false) } @@ -1328,7 +1326,11 @@ pub(crate) unsafe fn instance_private_key_hidden( pub(crate) fn is_internal_runtime_key_bytes(b: &[u8]) -> bool { b == crate::object::map_set_subclass::BACKING_KEY || b == crate::weakref::WEAK_ENTRIES_KEY + || b == crate::object::parent_static::CLASS_OBJECT_PARENT_KEY.as_bytes() + || b == b"__perry_ctor_caps" || b.starts_with(crate::node_stream::NATIVE_BASE_SUPER_PREFIX) + || b.starts_with(b"__perry_computed_field_key_") + || b.starts_with(b"# JSValue { + if let Some(value) = super::private_member_get_by_name(obj, key) { + return JSValue::from_bits(value.to_bits()); + } // #7341: the `.size` arm below calls two helpers that allocate, and every // arm AFTER it dereferences `obj` again. Shadow the parameter so that arm // can republish the post-collection address instead of leaving from-space @@ -1110,7 +1113,11 @@ pub extern "C" fn js_object_get_field_by_name( let value = class_prototype_method_value_for_name(class_id, name); return JSValue::from_bits(value.to_bits()); } - if name == "constructor" && class_id != 0 && is_class_id_registered(class_id) { + if name == "constructor" + && is_prototype_ref + && class_id != 0 + && is_class_id_registered(class_id) + { let value = if is_prototype_ref { super::super::class_constructor_ref_value(class_id) } else { @@ -1218,7 +1225,7 @@ pub extern "C" fn js_object_get_field_by_name( // ClassDefinitionEvaluation installs it per class). Skip the // chain walk so the #2059 own-name synthesis below answers // with THIS class's registered name instead of an ancestor's. - if name != "name" { + if !matches!(name, "name" | "length") { // Walk the class-object proto chain for an inherited static // DATA field. At EACH level the class's pinned // per-evaluation parent OBJECT is consulted BEFORE the @@ -1325,7 +1332,7 @@ pub extern "C" fn js_object_get_field_by_name( // subclass of a per-evaluation class object reported its // BASE's synthesized `.name` (bundled zod: // `z.string().constructor.name` gave "ZodType"). - if name != "name" { + if !matches!(name, "name" | "length") { if let Some(v) = super::super::class_registry::resolve_proto_chain_field(class_id, key) { @@ -1367,6 +1374,29 @@ pub extern "C" fn js_object_get_field_by_name( return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); } } + if name == "length" + && class_id != 0 + && !is_prototype_ref + && !super::super::class_registry::class_is_key_deleted(class_id, name) + { + if let Some(length) = + super::super::class_registry::class_length_for_id(class_id) + { + return JSValue::number(length as f64); + } + } + // A class constructor is also a Function object. Reify + // inherited Function.prototype methods for value reads + // (`const bind = C.bind`) just as the closure path does; + // the captured ClassRef is accepted by native method + // dispatch and by `js_function_bind`. + if !is_prototype_ref { + if let Some(method) = super::reified_function_method_name(name) { + let value = + crate::closure::reify_function_method_value(class_value, method); + return JSValue::from_bits(value.to_bits()); + } + } // No own static / inherited entry resolved the name. A class // constructor is a function, so a bare read of `.caller` or // `.arguments` hits the poison-pill %ThrowTypeError% accessor @@ -1382,6 +1412,13 @@ pub extern "C" fn js_object_get_field_by_name( ); } } + // The built-in constructor object's `constructor` value is + // inherited from Function.prototype. It is therefore only the + // fallback after own computed fields, static methods, and + // static accessors of the same name have had a chance to win. + if name == "constructor" && class_id != 0 && is_class_id_registered(class_id) { + return JSValue::from_bits(class_value.to_bits()); + } } return JSValue::undefined(); } 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 2ff1ae7b58..8ebdfc10f1 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 @@ -738,7 +738,7 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { let key_ptr = crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; if let Some(k) = unsafe { super::super::has_own_helpers::str_from_string_header(key_ptr) } { - if k.starts_with('#') { + if super::is_internal_runtime_key(k) { return nanbox_false; } } @@ -996,11 +996,12 @@ unsafe fn object_string_key_has_property( let class_id = (*obj_ptr).class_id; if class_id != 0 { - // `#name`-prefixed string keys on class instances are private elements — - // invisible to ordinary [[HasProperty]] (mirrors the slow-path arm). + // Compiler/runtime-only private storage keys are invisible to ordinary + // [[HasProperty]], while a public computed key such as `"#name"` is a + // normal String property. let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; if let Some(b) = crate::string::js_string_key_bytes(key_val, &mut sso) { - if b.first() == Some(&b'#') { + if super::is_internal_runtime_key_bytes(b) { return nanbox_false; } } diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs index 0f11788f02..d677f3c552 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -10,6 +10,9 @@ pub extern "C" fn js_object_get_field_by_name_f64( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> f64 { + if let Some(value) = private_member_get_by_name(obj, key) { + return value; + } if (obj as usize) > 0 && (obj as usize) < 0x10000 && !key.is_null() { if let Some(name) = unsafe { super::super::has_own_helpers::str_from_string_header(key) } { let class_id = obj as usize as u32; @@ -867,6 +870,51 @@ mod sso_tests_1781 { } } +crate::perry_thread_local! { + /// The ClassDefinitionEvaluation captured by the currently executing + /// method function. Each vtable call pushes one entry, including an + /// `undefined` delimiter for ordinary classes, so a nested call never + /// inherits its caller's private-name environment by accident. + static PRIVATE_LEXICAL_BRAND_STACK: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; +} + +pub(crate) fn private_lexical_brand_push(value: f64) { + PRIVATE_LEXICAL_BRAND_STACK.with(|stack| stack.borrow_mut().push(value.to_bits())); +} + +pub(crate) fn private_lexical_brand_pop() { + PRIVATE_LEXICAL_BRAND_STACK.with(|stack| { + stack.borrow_mut().pop(); + }); +} + +pub(crate) fn private_lexical_brand_stack_savepoint() -> usize { + PRIVATE_LEXICAL_BRAND_STACK.with(|stack| stack.borrow().len()) +} + +pub(crate) fn private_lexical_brand_stack_restore(depth: usize) { + PRIVATE_LEXICAL_BRAND_STACK.with(|stack| stack.borrow_mut().truncate(depth)); +} + +pub(crate) fn scan_private_lexical_brand_roots_mut( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, +) { + PRIVATE_LEXICAL_BRAND_STACK.with(|stack| { + for bits in stack.borrow_mut().iter_mut() { + visitor.visit_nanbox_u64_slot(bits); + } + }); +} + +fn current_private_lexical_brand(declaring_class_id: u32) -> Option { + PRIVATE_LEXICAL_BRAND_STACK.with(|stack| { + let bits = *stack.borrow().last()?; + let value = f64::from_bits(bits); + private_evaluation_brand(value, declaring_class_id).map(|_| bits) + }) +} + /// Stamp an instance constructed through a `ClassExprFresh` value with the /// identity of that particular class evaluation. The brand lives in the /// object's traced metadata record so it neither shifts user field slots nor @@ -896,6 +944,7 @@ fn private_evaluation_brand(value: f64, declaring_class_id: u32) -> Option if declaring_class_id == 0 { return None; } + let value = crate::proxy::private_element_receiver(value); if super::super::class_registry::is_class_object_value(value) { let object = JSValue::from_bits(value.to_bits()).as_pointer::(); if !object.is_null() && js_object_get_class_id(object) == declaring_class_id { @@ -922,96 +971,200 @@ fn private_evaluation_brand(value: f64, declaring_class_id: u32) -> Option .then_some(brand.to_bits()) } -/// If the lexical class evaluation can be recovered from `brand_owner`, -/// compare `obj` against that exact evaluation. `None` asks callers to retain -/// the existing template-class check for ordinary (single-evaluation) classes. -fn private_evaluation_brand_matches( - obj: f64, - brand_owner: f64, - declaring_class_id: u32, -) -> Option { - // A static method/accessor closes over the PrivateEnvironment of its class - // evaluation. Its visible `this` may be replaced by call/apply, so dispatch - // records the lexical owner separately from ambient IMPLICIT_THIS. - let brand_owner = if super::super::class_registry::is_class_object_value(brand_owner) { - let captured_owner = super::super::static_private_owner_current().unwrap_or(brand_owner); - if super::super::class_registry::is_class_object_value(captured_owner) - && private_evaluation_brand(captured_owner, declaring_class_id).is_some() +/// Return the fresh ClassDefinitionEvaluation object carried by a constructor +/// or instance. Unlike `private_evaluation_brand`, this does not require the +/// caller to know the compile-time template id; method dispatch uses it to +/// establish the callee's lexical private-name environment. +pub(crate) fn private_evaluation_brand_value(value: f64) -> Option { + let value = crate::proxy::private_element_receiver(value); + if super::super::class_registry::is_class_object_value(value) { + return Some(value); + } + let value = JSValue::from_bits(value.to_bits()); + if !value.is_pointer() { + return None; + } + let object = value.as_pointer::(); + let brand = unsafe { + if object.is_null() || !crate::object::object_is_shaped(object) || (*object).meta.is_null() { - captured_owner - } else { - brand_owner + return None; } - } else { - brand_owner + f64::from_bits((*(*object).meta).private_evaluation_brand) }; - let expected = private_evaluation_brand(brand_owner, declaring_class_id)?; - Some(private_evaluation_brand(obj, declaring_class_id) == Some(expected)) + super::super::class_registry::is_class_object_value(brand).then_some(brand) } -#[no_mangle] -pub extern "C" fn js_private_brand_check( - obj: f64, - brand_owner: f64, +include!("ic_miss/private_member_access.rs"); + +fn private_field_marker_key( declaring_class_id: u32, field_name_ptr: *const u8, field_name_len: u32, -) -> f64 { - let false_value = f64::from_bits(crate::value::TAG_FALSE); - let true_value = f64::from_bits(crate::value::TAG_TRUE); - if declaring_class_id == 0 || field_name_ptr.is_null() || field_name_len == 0 { - return false_value; - } - - let has_declaring_brand = - private_evaluation_brand_matches(obj, brand_owner, declaring_class_id) - .unwrap_or_else(|| unsafe { private_object_has_brand(obj, declaring_class_id) }); - if !has_declaring_brand { - return false_value; +) -> Option { + if field_name_ptr.is_null() || field_name_len == 0 { + return None; } + let field_name = unsafe { + std::str::from_utf8(std::slice::from_raw_parts( + field_name_ptr, + field_name_len as usize, + )) + .ok()? + }; + Some(format!( + "#" + )) +} - true_value +fn private_marker_is_present(storage: f64, marker: &str) -> bool { + crate::object::js_object_get_own_field_or_undef(storage, marker.as_ptr(), marker.len()) + .to_bits() + != crate::value::TAG_UNDEFINED } -/// Throw a `TypeError` with `msg` through Perry's exception machinery so a -/// surrounding `try { ... } catch (e) { ... }` catches it. Diverges. -fn throw_private_type_error(msg: &str) -> ! { - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(s); - let v = crate::value::JSValue::pointer(err as *const u8).bits(); - crate::exception::js_throw(f64::from_bits(v)) +fn private_instance_element_is_present( + storage: f64, + declaring_class_id: u32, + field_name_ptr: *const u8, + field_name_len: u32, + kind: u32, +) -> bool { + let marker = if kind == 0 { + private_field_marker_key(declaring_class_id, field_name_ptr, field_name_len) + } else { + Some(private_brand_key(declaring_class_id)) + }; + marker.is_some_and(|marker| private_marker_is_present(storage, &marker)) } -/// Brand check core shared with `js_private_brand_check`: does `obj` carry the -/// brand of `declaring_class_id` (it is an instance of that class or a -/// subclass)? Walks the class-id parent chain. -unsafe fn private_object_has_brand(obj: f64, declaring_class_id: u32) -> bool { +/// Install one class's instance-private brand on `obj`. +/// +/// A class contributes one brand regardless of how many private fields, +/// methods, or accessors it declares. Re-installing that brand on the same +/// object is the observable error required by PrivateFieldAdd and +/// PrivateMethodOrAccessorAdd (for example when a base constructor returns an +/// object that was already initialized by the derived class once). +#[no_mangle] +pub extern "C" fn js_private_brand_add(obj: f64, declaring_class_id: u32) -> f64 { if declaring_class_id == 0 { - return false; + return obj; } - let value = JSValue::from_bits(obj.to_bits()); + let storage = crate::proxy::private_element_receiver(obj); + let marker = private_brand_key(declaring_class_id); + if private_marker_is_present(storage, &marker) { + throw_private_type_error("Cannot initialize private elements twice on the same object"); + } + let value = JSValue::from_bits(storage.to_bits()); if !value.is_pointer() { - return false; + throw_private_type_error("Cannot initialize private elements on a non-object"); } - let obj_ptr = value.as_pointer::(); - if obj_ptr.is_null() { - return false; + let object = value.as_pointer::() as *mut ObjectHeader; + if object.is_null() || !crate::value::addr_class::is_plausible_heap_addr(object as usize) { + throw_private_type_error("Cannot initialize private elements on a non-object"); } - let obj_class_id = js_object_get_class_id(obj_ptr); - if obj_class_id == 0 { - return false; + // The marker-key allocation can evacuate both the receiver and any live + // value. Root first, then derive raw pointers only inside scoped handle + // accessors after the allocation. + let scope = crate::gc::RuntimeHandleScope::new(); + let object = scope.root_raw_mut_ptr(object); + let key = crate::string::js_string_from_bytes(marker.as_ptr(), marker.len() as u32); + let key = scope.root_string_ptr(key); + object.with_mut_ptr::(|object| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name(object, key, f64::from_bits(crate::value::TAG_TRUE)); + }); + }); + obj +} + +/// Define an instance private field without going through Proxy [[Set]]. The +/// corresponding class brand is installed once by `js_private_brand_add`. +#[no_mangle] +pub extern "C" fn js_private_field_add( + obj: f64, + declaring_class_id: u32, + field_key: f64, + value: f64, +) -> f64 { + // A short-string key may be materialized here, so root every GC-managed + // operand before asking for a StringHeader and use only refreshed handles + // afterwards. + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_nanbox_f64(obj); + let field_key = scope.root_nanbox_f64(field_key); + let value = scope.root_nanbox_f64(value); + let field_key_ptr = crate::value::js_get_string_pointer_unified(field_key.get_nanbox_f64()) + as *const crate::StringHeader; + if field_key_ptr.is_null() { + throw_private_type_error("Invalid private field name"); } - let mut cur = obj_class_id; - for _ in 0..32 { - if cur == declaring_class_id { - return true; - } - match super::super::class_registry::get_parent_class_id(cur) { - Some(parent) if parent != 0 && parent != cur => cur = parent, - _ => break, - } + let field_name_len = unsafe { (*field_key_ptr).byte_len }; + let field_name_ptr = crate::string::string_data(field_key_ptr); + let Some(marker) = private_field_marker_key(declaring_class_id, field_name_ptr, field_name_len) + else { + throw_private_type_error("Invalid private field name"); + }; + let field_name = unsafe { + std::str::from_utf8(std::slice::from_raw_parts( + field_name_ptr, + field_name_len as usize, + )) + } + .unwrap_or_else(|_| throw_private_type_error("Invalid private field name")); + let storage_name = format!("#"); + let storage = crate::proxy::private_element_receiver(obj.get_nanbox_f64()); + if private_marker_is_present(storage, &marker) { + throw_private_type_error("Cannot initialize a private field twice on the same object"); + } + let receiver = JSValue::from_bits(storage.to_bits()); + if !receiver.is_pointer() { + throw_private_type_error("Cannot initialize a private field on a non-object"); } - false + let object = receiver.as_pointer::() as *mut ObjectHeader; + if object.is_null() || !crate::value::addr_class::is_plausible_heap_addr(object as usize) { + throw_private_type_error("Cannot initialize a private field on a non-object"); + } + let object = scope.root_raw_mut_ptr(object); + let storage_key = + crate::string::js_string_from_bytes(storage_name.as_ptr(), storage_name.len() as u32); + let storage_key = scope.root_string_ptr(storage_key); + let marker_key = crate::string::js_string_from_bytes(marker.as_ptr(), marker.len() as u32); + let marker_key = scope.root_string_ptr(marker_key); + object.with_mut_ptr::(|object| { + storage_key.with_const_ptr::(|storage_key| { + js_object_set_field_by_name(object, storage_key, value.get_nanbox_f64()); + }); + }); + object.with_mut_ptr::(|object| { + marker_key.with_const_ptr::(|marker_key| { + js_object_set_field_by_name(object, marker_key, f64::from_bits(crate::value::TAG_TRUE)); + }); + }); + value.get_nanbox_f64() +} + +/// Define one public class field with DefineField/CreateDataProperty +/// semantics. In particular, inherited setters are bypassed and Proxy +/// receivers observe `defineProperty`, not `set`. +#[no_mangle] +pub extern "C" fn js_class_field_add(receiver: f64, key: f64, value: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let key = scope.root_nanbox_f64(key); + let value = scope.root_nanbox_f64(value); + // Use [[DefineOwnProperty]] for both ordinary objects and Proxies. The + // ordinary define path bypasses an inherited setter; the Proxy path invokes + // the receiver's `defineProperty` trap. A normal `[[Set]]` helper cannot + // provide both sides of DefineField semantics. + if !crate::proxy::create_data_property( + receiver.get_nanbox_f64(), + key.get_nanbox_f64(), + value.get_nanbox_f64(), + ) { + throw_private_type_error("Cannot define class field on receiver"); + } + value.get_nanbox_f64() } /// Brand + kind/op guard for a private member access `obj.#name`. Returns @@ -1054,6 +1207,11 @@ pub extern "C" fn js_private_guard( } let is_static = op >= 2; let read_write = op & 1; // 0=read, 1=write + if is_static && crate::proxy::js_proxy_is_proxy(obj) != 0 { + throw_private_type_error( + "Cannot access private member from an object whose class did not declare it", + ); + } let has_brand = private_evaluation_brand_matches(obj, brand_owner, declaring_class_id) .unwrap_or_else(|| { if is_static { @@ -1062,7 +1220,13 @@ pub extern "C" fn js_private_guard( // a subclass. super::super::class_ref_id(obj) == Some(declaring_class_id) } else { - unsafe { private_object_has_brand(obj, declaring_class_id) } + private_instance_element_is_present( + crate::proxy::private_element_receiver(obj), + declaring_class_id, + _field_name_ptr, + _field_name_len, + kind, + ) } }); if !has_brand { @@ -1070,6 +1234,18 @@ pub extern "C" fn js_private_guard( "Cannot access private member from an object whose class did not declare it", ); } + if !is_static { + let storage = crate::proxy::private_element_receiver(obj); + if !private_instance_element_is_present( + storage, + declaring_class_id, + _field_name_ptr, + _field_name_len, + kind, + ) { + throw_private_type_error("Cannot access private member before it has been initialized"); + } + } let op = read_write; // Kind/op legality, after the brand check (spec order). let illegal = matches!( @@ -1081,7 +1257,43 @@ pub extern "C" fn js_private_guard( if illegal { throw_private_type_error("Invalid private member operation for its kind"); } - obj + if kind != 0 { + let field_name = unsafe { + std::str::from_utf8(std::slice::from_raw_parts( + _field_name_ptr, + _field_name_len as usize, + )) + .unwrap_or("") + .to_string() + }; + PRIVATE_MEMBER_ACCESS_HINTS.with(|hints| { + hints.borrow_mut().push(PrivateMemberAccessHint { + class_id: declaring_class_id, + name: field_name.clone(), + kind, + is_static, + is_write: read_write != 0, + }); + }); + } + if kind == 1 && read_write == 0 { + let field_name = unsafe { + std::str::from_utf8(std::slice::from_raw_parts( + _field_name_ptr, + _field_name_len as usize, + )) + .unwrap_or("") + .to_string() + }; + PRIVATE_METHOD_OWNER_HINT.with(|hint| { + *hint.borrow_mut() = Some((declaring_class_id, field_name)); + }); + } + if is_static { + obj + } else { + crate::proxy::private_element_receiver(obj) + } } #[cfg(test)] 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 new file mode 100644 index 0000000000..b96fafb443 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs @@ -0,0 +1,316 @@ +fn private_brand_key(declaring_class_id: u32) -> String { + format!("#") +} + +crate::perry_thread_local! { + static PRIVATE_METHOD_OWNER_HINT: std::cell::RefCell> = + std::cell::RefCell::new(None); + static PRIVATE_MEMBER_ACCESS_HINTS: std::cell::RefCell> = + std::cell::RefCell::new(Vec::new()); +} + +#[derive(Clone)] +struct PrivateMemberAccessHint { + class_id: u32, + name: String, + kind: u32, + is_static: bool, + is_write: bool, +} + +pub(crate) fn private_member_access_hints_savepoint() -> usize { + PRIVATE_MEMBER_ACCESS_HINTS.with(|hints| hints.borrow().len()) +} + +pub(crate) fn private_member_access_hints_restore(depth: usize) { + PRIVATE_MEMBER_ACCESS_HINTS.with(|hints| hints.borrow_mut().truncate(depth)); +} + +pub(crate) fn take_private_method_owner_hint(method_name: &str) -> Option { + PRIVATE_METHOD_OWNER_HINT.with(|hint| { + let mut hint = hint.borrow_mut(); + match hint.as_ref() { + Some((class_id, name)) if name == method_name => { + let class_id = *class_id; + *hint = None; + Some(class_id) + } + _ => None, + } + }) +} + +fn private_member_storage_name(key: *const crate::StringHeader) -> Option { + let key = unsafe { super::super::has_own_helpers::str_from_string_header(key) }?; + private_member_storage_name_str(key).map(str::to_string) +} + +fn private_member_storage_name_str(key: &str) -> Option<&str> { + let rest = key.strip_prefix("#') +} + +fn take_private_member_access_hint(name: &str, is_write: bool) -> Option { + PRIVATE_MEMBER_ACCESS_HINTS.with(|hints| { + let mut hints = hints.borrow_mut(); + let index = hints + .iter() + .rposition(|hint| hint.name == name && hint.is_write == is_write)?; + Some(hints.remove(index)) + }) +} + +fn private_member_receiver(obj: *const ObjectHeader) -> f64 { + let bits = obj as u64; + if matches!(bits >> 48, 0x7FFD | 0x7FFE) { + f64::from_bits(bits) + } else { + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()) + } +} + +pub(crate) fn private_member_get_by_name( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> Option { + let name = private_member_storage_name(key)?; + let hint = take_private_member_access_hint(&name, false)?; + let receiver = private_member_receiver(obj); + unsafe { + match hint.kind { + 1 => { + if hint.is_static { + let _ = take_private_method_owner_hint(&name); + let brand = current_private_lexical_brand(hint.class_id) + .map(f64::from_bits) + .or_else(|| { + private_evaluation_brand(receiver, hint.class_id).map(f64::from_bits) + }) + .unwrap_or(receiver); + return Some( + super::super::native_module::class_private_static_method_value_for_name( + hint.class_id, + &name, + brand, + ), + ); + } + let stable_name = super::super::native_module::intern_class_method_name( + hint.class_id, + &name, + ); + Some(super::super::js_class_method_bind( + receiver, + stable_name.as_ptr(), + stable_name.len(), + )) + } + 2 | 4 if hint.is_static => { + super::super::class_registry::class_static_accessor_getter_value( + hint.class_id, + &name, + receiver, + ) + } + 2 | 4 => super::super::class_registry::class_private_instance_getter_value( + hint.class_id, + &name, + receiver, + ), + _ => None, + } + } +} + +/// Invoke a guarded private method from the fused `receiver.#method(args)` +/// lowering. The ordinary dynamic-method tower cannot resolve the internal +/// storage key, while the preceding guard has already validated its brand. +pub(crate) unsafe fn private_member_call_by_name( + receiver: f64, + storage_name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let (class_id, is_static, name) = take_private_method_call_hint(storage_name)?; + + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + if is_static { + return Some(super::super::class_registry::js_class_static_method_call( + receiver.get_nanbox_f64(), + name.as_ptr(), + name.len(), + args_ptr, + args_len, + )); + } + + let (func_ptr, param_count, has_synthetic_arguments, has_rest) = + super::super::class_registry::lookup_class_method_in_chain(class_id, name)?; + let receiver_value = receiver.get_nanbox_f64(); + let private_brand = current_private_lexical_brand(class_id) + .map(f64::from_bits) + .or_else(|| private_evaluation_brand_value(receiver_value)) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let this_bits = receiver_value.to_bits(); + let this = if (this_bits >> 48) == 0x7FFD { + (this_bits & crate::value::POINTER_MASK) as i64 + } else { + this_bits as i64 + }; + Some( + super::super::class_registry::call_vtable_method_with_private_brand( + func_ptr, + this, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + private_brand, + ), + ) +} + +pub(crate) fn take_private_method_call_hint(storage_name: &str) -> Option<(u32, bool, &str)> { + let name = private_member_storage_name_str(storage_name)?; + let hint = take_private_member_access_hint(name, false)?; + if hint.kind != 1 { + return None; + } + let _ = take_private_method_owner_hint(name); + Some((hint.class_id, hint.is_static, name)) +} + +pub(crate) fn private_member_set_by_name( + obj: *mut ObjectHeader, + key: *const crate::StringHeader, + value: f64, +) -> bool { + let Some(name) = private_member_storage_name(key) else { + return false; + }; + let Some(hint) = take_private_member_access_hint(&name, true) else { + return false; + }; + let receiver = private_member_receiver(obj); + let applied = unsafe { + if hint.is_static { + super::super::class_registry::class_static_accessor_setter_apply( + hint.class_id, + &name, + receiver, + value, + ) + } else { + super::super::class_registry::class_private_instance_setter_apply( + hint.class_id, + &name, + receiver, + value, + ) + } + }; + if !applied { + throw_private_type_error("Private setter is unavailable"); + } + true +} + +/// If the lexical class evaluation can be recovered from `brand_owner`, +/// compare `obj` against that exact evaluation. `None` asks callers to retain +/// the existing template-class check for ordinary (single-evaluation) classes. +fn private_evaluation_brand_matches( + obj: f64, + brand_owner: f64, + declaring_class_id: u32, +) -> Option { + if let Some(expected) = current_private_lexical_brand(declaring_class_id) { + let actual = private_evaluation_brand(obj, declaring_class_id); + return Some(actual == Some(expected)); + } + + let brand_owner = if super::super::class_registry::is_class_object_value(brand_owner) { + let captured_owner = super::super::static_private_owner_current().unwrap_or(brand_owner); + if super::super::class_registry::is_class_object_value(captured_owner) + && private_evaluation_brand(captured_owner, declaring_class_id).is_some() + { + captured_owner + } else { + brand_owner + } + } else { + brand_owner + }; + let expected = private_evaluation_brand(brand_owner, declaring_class_id)?; + Some(private_evaluation_brand(obj, declaring_class_id) == Some(expected)) +} + +#[no_mangle] +pub extern "C" fn js_private_brand_check( + obj: f64, + brand_owner: f64, + declaring_class_id: u32, + field_name_ptr: *const u8, + field_name_len: u32, + kind: u32, + is_static: u32, +) -> f64 { + let false_value = f64::from_bits(crate::value::TAG_FALSE); + let true_value = f64::from_bits(crate::value::TAG_TRUE); + if declaring_class_id == 0 || field_name_ptr.is_null() || field_name_len == 0 { + return false_value; + } + if is_static != 0 && crate::proxy::js_proxy_is_proxy(obj) != 0 { + return false_value; + } + + let has_declaring_brand = + private_evaluation_brand_matches(obj, brand_owner, declaring_class_id).unwrap_or_else( + || { + if is_static != 0 { + super::super::class_ref_id(obj) == Some(declaring_class_id) + } else { + private_instance_element_is_present( + crate::proxy::private_element_receiver(obj), + declaring_class_id, + field_name_ptr, + field_name_len, + kind, + ) + } + }, + ); + if !has_declaring_brand { + return false_value; + } + + if is_static == 0 { + let storage = crate::proxy::private_element_receiver(obj); + if !private_instance_element_is_present( + storage, + declaring_class_id, + field_name_ptr, + field_name_len, + kind, + ) { + return false_value; + } + } + + true_value +} + +/// Throw a `TypeError` with `msg` through Perry's exception machinery so a +/// surrounding `try { ... } catch (e) { ... }` catches it. Diverges. +fn throw_private_type_error(msg: &str) -> ! { + let scope = crate::gc::RuntimeHandleScope::new(); + let s = scope.root_string_ptr(crate::string::js_string_from_bytes( + msg.as_ptr(), + msg.len() as u32, + )); + let err = s.with_mut_ptr::(|s| crate::error::js_typeerror_new(s)); + let v = crate::value::JSValue::pointer(err as *const u8).bits(); + crate::exception::js_throw(f64::from_bits(v)) +} diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 84274e4285..3d9861e582 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -27,6 +27,7 @@ pub use attr_variants::{ }; pub use fast_paths::js_object_set_field_by_name_transition_fast; pub(crate) use fast_paths::try_existing_own_data_overwrite; +pub(crate) use tail::set_field_by_name_object_tail; pub(crate) use write_helpers::nm_field_set_override; use write_helpers::string_key_eq; @@ -39,6 +40,31 @@ pub extern "C" fn js_object_set_field_by_name( key: *const crate::StringHeader, value: f64, ) { + if super::private_member_set_by_name(obj, key, value) { + return; + } + // A heap class value is an exotic constructor object. Its own + // `prototype` property is non-writable, so both ordinary assignment and + // a computed static field whose PropertyKey resolves to "prototype" must + // fail instead of appending an ordinary shape slot. + let obj_bits = obj as u64; + let normalized_obj = if (obj_bits >> 48) == 0x7FFD { + (obj_bits & crate::value::POINTER_MASK) as *mut ObjectHeader + } else { + obj + }; + if !key.is_null() + && crate::value::addr_class::is_above_handle_band(normalized_obj as usize) + && crate::object::class_registry::is_class_object_ptr(normalized_obj.cast()) + { + unsafe { + let name_ptr = crate::string::string_data(key); + let name_len = (*key).byte_len as usize; + if std::slice::from_raw_parts(name_ptr, name_len) == b"prototype" { + crate::error::throw_immutable_write((*normalized_obj).class_id, "prototype"); + } + } + } // Aliased `process.env` writes must update the OS environment, not only the // materialized object's field bag. The helper declines internal cache // mirror writes so those can proceed through the ordinary setter below. @@ -551,6 +577,13 @@ pub extern "C" fn js_object_set_field_by_name( } } unsafe { + if string_key_eq(key, b"length") { + let receiver = crate::value::js_nanbox_pointer(obj as i64); + if crate::array::is_array_subclass_value(receiver) { + crate::array::array_object_set_length(receiver, value); + return; + } + } if (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 && string_key_eq(key, b"length") { let gc_header = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; diff --git a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs index ca4b2139ee..7d6410f002 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs @@ -38,6 +38,12 @@ pub(crate) unsafe fn try_existing_own_data_overwrite( if obj_gc.obj_type != crate::gc::GC_TYPE_OBJECT || obj_gc.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 || obj_gc._reserved & BLOCKING_FLAGS != 0 + // A per-evaluation class object can carry dynamic static accessors in + // the class registry while retaining an ordinary backing slot with the + // same key. Overwriting that slot directly bypasses the accessor + // setter, so class constructors must always take the full exotic + // `[[Set]]` path. + || crate::object::class_registry::is_class_object_ptr(obj.cast()) || (*obj).class_id == NATIVE_MODULE_CLASS_ID || crate::array::object_prototype_addr_matches(obj_addr) // URL's visible fields are live views over one backing URL. An own diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index d891ef3ee7..05d46c9a38 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -35,7 +35,7 @@ fn overflow_store_bits(value: f64, obj: *mut ObjectHeader, new_index: usize) -> /// of its NaN-box tag and vetted against the handle band, typed arrays, and /// the `arr.length` special case. Body moved verbatim. #[allow(unused_assignments)] -pub(super) fn set_field_by_name_object_tail( +pub(crate) fn set_field_by_name_object_tail( obj: *mut ObjectHeader, key: *const crate::StringHeader, value: f64, @@ -266,46 +266,28 @@ pub(super) fn set_field_by_name_object_tail( let plan_fast = plan_eligible && super::prop_plan::store_plan_check(obj_class_id, interned_key as usize); - // A per-evaluation class expression is a heap class object rather than - // the INT32 ClassRef handled in the entry-point prelude. Private static - // setter assignments (`this.#m = value`) still need to consult the - // template class's registered static accessor, while passing THIS - // evaluation's class object as the receiver. Private names are scoped - // here deliberately: an ordinary string property literally named - // "#m" remains a separate public data property. - // `js_string_intern` above can allocate and evacuate all three rooted - // operands. Refresh before the first class-object/private-name probe; - // every dereference and the user-visible setter call below is then - // dominated by the post-allocation reload. - let private_static = obj_handle.with_mut_ptr::(|obj| { - key_handle.with_const_ptr::(|key| { - if !crate::object::is_class_object_ptr(obj as *const u8) - || key.is_null() - || !crate::value::addr_class::is_above_handle_band(key as usize) - { - return None; + // A per-evaluation class object carries dynamically installed static + // accessors in the descriptor side table, not in its ordinary field + // slots. Invoke that setter before the generic instance-vtable and + // own-data paths below; otherwise `C.x = value` silently appends or + // overwrites a data field after `defineProperty(C, "x", { set })`. + if !plan_fast + && !key.is_null() + && crate::object::class_registry::is_class_object_ptr(obj.cast()) + { + let name_ptr = crate::string::string_data(key); + let name_len = (*key).byte_len as usize; + if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + let receiver = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + if super::class_registry::class_static_accessor_setter_apply( + obj_class_id, + name, + receiver, + value, + ) { + return; } - let name_ptr = crate::string::string_data(key); - let name_len = (*key).byte_len as usize; - let name = - std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)).ok()?; - name.starts_with('#').then(|| { - ( - (*obj).class_id, - name.to_string(), - crate::value::js_nanbox_pointer(obj as i64), - ) - }) - }) - }); - if let Some((class_id, name, receiver)) = private_static { - if super::class_registry::class_static_accessor_setter_apply( - class_id, - &name, - receiver, - value_handle.get_nanbox_f64(), - ) { - return; } } diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index fadf21b9f5..cf6eae179a 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -544,6 +544,7 @@ fn is_uncallable_builtin_super_parent(name: &str) -> bool { | "Uint16Array" | "Int32Array" | "Uint32Array" + | "Float16Array" | "Float32Array" | "Float64Array" | "BigInt64Array" @@ -581,6 +582,7 @@ fn is_uncallable_builtin_super_parent_class_id(class_id: u32) -> bool { "Uint16Array", "Int32Array", "Uint32Array", + "Float16Array", "Float32Array", "Float64Array", "BigInt64Array", @@ -610,6 +612,14 @@ pub unsafe extern "C" fn js_fetch_or_value_super( args_len: usize, ) -> f64 { let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + // `extends null` is valid at ClassDefinitionEvaluation time, but its + // derived constructor has no super-constructor to invoke. An explicit or + // synthesized `super()` therefore throws TypeError. + if crate::value::JSValue::from_bits(parent_val.to_bits()).is_null() { + crate::object::object_ops::throw_object_type_error( + b"Super constructor null is not a constructor", + ); + } let wasi_parent = super::super::native_module::bound_native_callable_module_and_method( parent_val, ) @@ -776,13 +786,23 @@ pub unsafe extern "C" fn js_fetch_or_value_super( _ => None, } }); - // #5657: a native builtin base that can't be called as a function (incl. - // ALIASED parents — `const AB = ArrayBuffer; class X extends AB {}` — which - // the codegen name guard can't see). No-op rather than throwing - // "X is not a function" / "Constructor X requires 'new'". + // A native builtin base that cannot be called as a function still has to + // perform its [[Construct]] work for `super()`. Construct it with the + // current subclass as newTarget so the result carries the builtin's real + // internal slots while inheriting from the subclass prototype. if let Some(name) = kind { if is_uncallable_builtin_super_parent(name) { - return undef; + let Some(obj) = subclass_this_object_ptr(this_box) else { + return undef; + }; + let cid = crate::object::js_object_get_class_id(obj); + if cid == 0 { + return undef; + } + let new_target = crate::object::class_constructor_ref_value(cid); + return crate::object::js_new_function_construct_with_new_target( + parent_val, args_ptr, args_len, new_target, + ); } } match kind { @@ -869,8 +889,15 @@ pub unsafe extern "C" fn js_fetch_or_value_super( ); if parent_cid != 0 { if let Some(obj) = subclass_this_object_ptr(this_box) { - super::super::class_constructors::run_class_constructor_on_this_flat( - parent_cid, obj as i64, args_ptr, args_len, + // A fresh class object's constructor environment + // is per evaluation. Replaying by class id alone + // consults the template-wide declaration snapshot + // and drops captured values such as a factory's + // `tag`. Use the class-object replay path so this + // exact parent's `__perry_ctor_caps` supplies its + // synthesized capture params. + super::super::class_constructors::replay_class_object_constructor( + parent_val, parent_cid, obj, args_ptr, args_len, ); return undef; } @@ -883,8 +910,8 @@ pub unsafe extern "C" fn js_fetch_or_value_super( let parent_cid = crate::object::js_object_get_class_id(p as *const _); if parent_cid != 0 { if let Some(obj) = subclass_this_object_ptr(this_box) { - super::super::class_constructors::run_class_constructor_on_this_flat( - parent_cid, obj as i64, args_ptr, args_len, + super::super::class_constructors::replay_class_object_constructor( + parent_val, parent_cid, obj, args_ptr, args_len, ); } } @@ -998,6 +1025,27 @@ pub(crate) extern "C" fn global_this_eval_thunk( let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); crate::value::js_nanbox_string(ptr as i64) } - _ => f64::from_bits(crate::value::TAG_UNDEFINED), + _ => { + #[cfg(feature = "dyn-eval")] + { + let body = body.to_string(); + let scope = crate::gc::RuntimeHandleScope::new(); + let global = scope.root_nanbox_f64(js_get_global_this()); + let lexical = scope.root_nanbox_f64(crate::dyn_eval::script_environment( + global.get_nanbox_f64(), + &[], + )); + crate::dyn_eval::eval_script_in( + &body, + global.get_nanbox_f64(), + global.get_nanbox_f64(), + lexical.get_nanbox_f64(), + ) + } + #[cfg(not(feature = "dyn-eval"))] + { + f64::from_bits(crate::value::TAG_UNDEFINED) + } + } } } diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs index 31da8fea3a..28026aff9a 100644 --- a/crates/perry-runtime/src/object/global_this/generator.rs +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -112,6 +112,11 @@ pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option } let obj_value = obj_h.get_nanbox_f64(); crate::closure::closure_set_dynamic_prop(closure_ptr, "prototype", obj_value); + super::super::set_builtin_property_attrs( + closure_ptr, + "prototype".to_string(), + super::super::PropertyAttrs::new(true, false, false), + ); Some(obj_h.get_nanbox_f64()) } diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index a9ac268f83..5fcee46700 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -521,6 +521,7 @@ pub(crate) fn global_builtin_constructor_class_id(name: &str) -> u32 { "WeakSet" => 0xFFFF002D, "RegExp" => 0xFFFF0021, "ArrayBuffer" => 0xFFFF0025, + "SharedArrayBuffer" => 0xFFFF002E, "DataView" => 0xFFFF002B, "Array" => 0xFFFF0024, "Object" => 0xFFFF0050, @@ -774,6 +775,7 @@ crate::perry_thread_local! { fn rhs_is_object_value(value: f64) -> bool { let bits = value.to_bits(); let jsval = crate::JSValue::from_bits(bits); + if jsval.is_null() || jsval.is_undefined() || jsval.is_bool() @@ -1204,6 +1206,22 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { let bits = value.to_bits(); let jsval = crate::JSValue::from_bits(bits); + // Native/exotic subclass instances (typed arrays, ArrayBuffers, boxed + // primitives, Dates, …) do not carry a Perry `ObjectHeader.class_id`. + // Their constructor records the distinct newTarget prototype in the + // prototype side table instead. Honor that chain for user class ids. + if is_class_id_registered(class_id) { + let addr = value_addr(value); + if addr != 0 && super::prototype_chain::object_static_prototype(addr).is_some() { + let constructor = super::class_constructor_ref_value(class_id); + return if ordinary_has_instance_prototype_walk(value, constructor) { + true_val + } else { + false_val + }; + } + } + // Special handling for Uint8Array/Buffer (class_id 0xFFFF0004) // Perry buffers are raw BufferHeader pointers bitcast to f64 (not NaN-boxed), // so the normal POINTER_TAG check doesn't work for them. @@ -1228,7 +1246,8 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { // marked in a side registry. They can arrive either NaN-boxed or as raw // buffer pointers, matching the Buffer/Uint8Array path above. const CLASS_ID_ARRAY_BUFFER: u32 = 0xFFFF0025; - if class_id == CLASS_ID_ARRAY_BUFFER { + const CLASS_ID_SHARED_ARRAY_BUFFER: u32 = 0xFFFF002E; + if class_id == CLASS_ID_ARRAY_BUFFER || class_id == CLASS_ID_SHARED_ARRAY_BUFFER { let addr = if jsval.is_pointer() { (bits & 0x0000_FFFF_FFFF_FFFF) as usize } else { @@ -1239,10 +1258,12 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { 0 } }; - if addr != 0 - && crate::buffer::is_registered_buffer(addr) - && crate::buffer::is_array_buffer(addr) - { + let matches_brand = if class_id == CLASS_ID_SHARED_ARRAY_BUFFER { + crate::buffer::is_shared_array_buffer(addr) + } else { + crate::buffer::is_array_buffer(addr) + }; + if addr != 0 && crate::buffer::is_registered_buffer(addr) && matches_brand { return true_val; } return false_val; diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 0456cb30fd..6a38ffe05b 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -246,17 +246,24 @@ pub(crate) use descriptor_state::{ PropertyAttrs, }; pub(crate) use field_get_set::FieldLookupCaches; -pub use this_binding::{ - js_implicit_this_get, js_implicit_this_get_sloppy, js_implicit_this_set, js_new_target_get, - js_new_target_set, js_static_this_arm_classref, js_static_this_arm_value, - js_static_this_resolve, +pub(crate) use field_get_set::{ + private_evaluation_brand_value, private_lexical_brand_pop, private_lexical_brand_push, + private_lexical_brand_stack_restore, private_lexical_brand_stack_savepoint, + private_member_access_hints_restore, private_member_access_hints_savepoint, + scan_private_lexical_brand_roots_mut, }; pub(crate) use this_binding::{ + derived_super_binding_stack_restore, derived_super_binding_stack_savepoint, scan_implicit_this_roots_mut, static_private_owner_current, static_private_owner_pop, static_private_owner_push, static_private_owner_stack_restore, static_private_owner_stack_savepoint, static_this_arm, static_this_arm_if_unarmed, static_this_disarm, IMPLICIT_THIS, }; +pub use this_binding::{ + js_implicit_this_get, js_implicit_this_get_sloppy, js_implicit_this_set, js_new_target_get, + js_new_target_set, js_static_this_arm_classref, js_static_this_arm_value, + js_static_this_resolve, +}; pub use to_string_tag::js_object_to_string; pub(crate) use to_string_tag::typed_array_to_string_tag_name; @@ -1723,7 +1730,7 @@ pub struct ObjectMeta { /// authoritative: no `property_descriptors` entry `(owner, key)` can /// exist for a key whose bit is clear, so the hot paths skip the /// side-table probe (and its per-call `String` build) entirely. POD — - /// the GC trace arm visits only `prototype`. + /// the GC trace arm visits the record's three child edges explicitly. pub attr_key_bits: u64, /// Same summary for accessor descriptors (`get`/`set` installs) — the /// `accessor_descriptors` table twin of `attr_key_bits`. diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index c92dda113c..047faf9c4c 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -1074,6 +1074,22 @@ pub unsafe extern "C-unwind" fn js_native_call_method( args_ptr: *const f64, args_len: usize, ) -> f64 { + if !method_name_ptr.is_null() && method_name_len > 0 { + let method_name_bytes = + std::slice::from_raw_parts(method_name_ptr as *const u8, method_name_len); + if method_name_bytes.starts_with(b"# { - let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if jsval.is_pointer() && crate::closure::is_closure_ptr(raw_ptr) { + if crate::object::value_is_callable(object) { return Some(crate::closure::js_function_bind(object, args_ptr, args_len)); } // #3662: a non-callable `this` (primitive or recognized plain @@ -499,6 +498,12 @@ pub(super) unsafe fn dispatch_common( // `fn.apply(this, arguments)` / `fn.call(this, x)`, so without these // arms ramda fails immediately on the first curried export. "call" => { + // Class constructors have no [[Call]] slot. `C.call(...)` must + // reject instead of treating the INT32-tagged ClassRef payload as + // a closure pointer in the generic Function.prototype path. + if super::class_ref_id(object).is_some() { + throw_fn_proto_not_callable("call"); + } // Proxy receiver (#3656): `p.call(thisArg, ...args)` routes through // the proxy `apply` trap (or, absent a trap, forwards to the target). if crate::proxy::js_proxy_is_proxy(object) == 1 { @@ -583,6 +588,9 @@ pub(super) unsafe fn dispatch_common( // but for the `Function.prototype.apply` path rather than the // dynamic-spread method-call codegen path. "apply" => { + if super::class_ref_id(object).is_some() { + throw_fn_proto_not_callable("apply"); + } // Proxy receiver (#3656): `p.apply(thisArg, argsArray)` routes // through the proxy `apply` trap (or forwards to the target). if crate::proxy::js_proxy_is_proxy(object) == 1 { 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 cc9a1dc626..d9b39c6a5a 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 @@ -59,6 +59,24 @@ pub(super) unsafe fn dispatch_primitive( args.as_ptr(), args.len(), )); + } else if class_id != 0 + && matches!( + method_name, + "bind" | "call" | "apply" | "isPrototypeOf" | "toString" + ) + && crate::object::class_registry::class_own_static_field_value(class_id, method_name) + .is_none() + { + // These are inherited Function/Object prototype operations, not + // static data members. Let `dispatch_common` handle them. Looking + // them up as a class property here reifies a bound method whose + // dispatch re-enters this same arm indefinitely (`C.call(...)` + // exhausted the native stack instead of throwing TypeError). + return match method_name { + "bind" => Some(crate::closure::js_function_bind(object, args_ptr, args_len)), + "call" | "apply" => super::proto_dispatch::throw_fn_proto_not_callable(method_name), + _ => None, + }; } else if class_id != 0 && !method_name_ptr.is_null() && method_name_len > 0 { // #5437: `C.viaFn()` where `viaFn` is a static DATA property holding a // callable (`C.viaFn = fn` / `static viaFn = fn`), NOT a registered diff --git a/crates/perry-runtime/src/object/native_call_method/string_methods.rs b/crates/perry-runtime/src/object/native_call_method/string_methods.rs index c0e0704704..cae30b6a76 100644 --- a/crates/perry-runtime/src/object/native_call_method/string_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/string_methods.rs @@ -28,8 +28,18 @@ pub(super) unsafe fn dispatch_string( // receivers continue to use the inline `js_string_*` paths in // `lower_string_method.rs`; this dispatch only catches fallthroughs // where codegen couldn't statically prove the type. - if jsval.is_string() || jsval.is_short_string() { - let s_ptr = crate::value::js_get_string_pointer_unified(object_handle.get_nanbox_f64()) + let string_receiver = if jsval.is_string() || jsval.is_short_string() { + Some(object_handle.get_nanbox_f64()) + } else if crate::builtins::boxed_primitive_to_string_tag(object_handle.get_nanbox_f64()) + == Some("String") + { + crate::builtins::boxed_primitive_payload(object_handle.get_nanbox_f64()) + .map(|(_, payload)| payload) + } else { + None + }; + if let Some(string_receiver) = string_receiver { + let s_ptr = crate::value::js_get_string_pointer_unified(string_receiver) as *const crate::StringHeader; if !s_ptr.is_null() { // NOTE: user-defined `String.prototype` methods on primitive string @@ -146,7 +156,7 @@ pub(super) unsafe fn dispatch_string( } return Some(f64::from_bits(JSValue::string_ptr(result).bits())); } - "toString" | "valueOf" => return Some(object_handle.get_nanbox_f64()), + "toString" | "valueOf" => return Some(string_receiver), // Issue #519 follow-up: hono's matcher.js does // `path2.match(matcher[0])` where `path2` is a string and // `matcher[0]` is a regex. The HIR optimistic diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 8daa5e478b..050d07dc74 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -9,8 +9,12 @@ use super::*; use std::cell::{Cell, RefCell}; +use std::collections::HashMap; use std::ptr::null_mut; -use std::sync::atomic::{AtomicPtr, Ordering}; +use std::sync::{ + atomic::{AtomicPtr, Ordering}, + OnceLock, RwLock, +}; mod async_hooks_exports; mod callable_export_arity_table; @@ -1268,8 +1272,9 @@ pub extern "C" fn js_class_method_bind( class_ref_id(instance).is_some() && class_prototype_ref_id(instance).is_none(); if !receiver_is_constructor_ref && bound_native_method_length(name).is_none() { if let Some(class_id) = class_id_from_method_receiver(instance) { - if let Some(owner) = - super::class_registry::method_owner_class_id(class_id, name) + let private_owner = super::take_private_method_owner_hint(name); + if let Some(owner) = private_owner + .or_else(|| super::class_registry::method_owner_class_id(class_id, name)) { // [[Get]] order: an OWN data property of this name // shadows the prototype method. The ubiquitous @@ -1282,7 +1287,8 @@ pub extern "C" fn js_class_method_bind( // prototype-ref receiver has no own-property bag, so this // check is naturally a no-op there. let recv_jsv = JSValue::from_bits(instance.to_bits()); - if recv_jsv.is_pointer() + if private_owner.is_none() + && recv_jsv.is_pointer() && !super::class_registry::is_registered_class_prototype_object( crate::value::js_nanbox_get_pointer(instance) as usize, ) @@ -1302,7 +1308,9 @@ pub extern "C" fn js_class_method_bind( } } } - let canonical = class_prototype_method_value_for_name(owner, name); + let canonical = private_evaluation_brand_value(instance) + .map(|brand| class_evaluation_method_value_for_name(owner, name, brand)) + .unwrap_or_else(|| class_prototype_method_value_for_name(owner, name)); if canonical.to_bits() != crate::value::TAG_UNDEFINED { return canonical; } @@ -1352,16 +1360,11 @@ pub(crate) fn test_take_bound_method_move() -> (usize, usize) { TEST_BOUND_METHOD_MOVE.with(|trace| trace.replace((0, 0))) } -/// Allocate a BOUND_METHOD closure binding `instance` as the receiver for the -/// named method, stamping its `.name`/`.length`. This is the raw builder used -/// by both `js_class_method_bind` (after its canonical-identity short-circuit) -/// and `class_prototype_method_value_for_name` (which caches one canonical per -/// `(class_id, name)`). Keeping it separate breaks the recursion that an -/// unconditional canonical lookup inside `js_class_method_bind` would create. -pub(crate) fn build_bound_method_closure( +fn build_bound_method_closure_with_private_brand( instance: f64, method_name_ptr: *const u8, method_name_len: usize, + private_brand: Option, ) -> f64 { // `js_closure_alloc` can collect before it returns, so keep the receiver // live across that allocation. The metadata installation below allocates a @@ -1373,9 +1376,10 @@ pub(crate) fn build_bound_method_closure( // method value at an immediately-following `typeof` check (#8036). let scope = crate::gc::RuntimeHandleScope::new(); let instance_handle = scope.root_nanbox_f64(instance); + let private_brand_handle = private_brand.map(|brand| scope.root_nanbox_f64(brand)); let closure_handle = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc( crate::closure::BOUND_METHOD_FUNC_PTR, - 3, + if private_brand_handle.is_some() { 4 } else { 3 }, )); // Capture-slot writes are scoped arguments to non-allocating stores, so // the address cannot go stale inside the call. Each value is read from its @@ -1385,6 +1389,9 @@ pub(crate) fn build_bound_method_closure( crate::closure::js_closure_set_capture_f64(closure, 0, instance_value); crate::closure::js_closure_set_capture_ptr(closure, 1, method_name_ptr as i64); crate::closure::js_closure_set_capture_ptr(closure, 2, method_name_len as i64); + if let Some(brand) = &private_brand_handle { + crate::closure::js_closure_set_capture_f64(closure, 3, brand.get_nanbox_f64()); + } }); #[cfg(test)] TEST_COLLECT_BOUND_METHOD_AFTER_CAPTURE_INIT.with(|armed| { @@ -1432,12 +1439,13 @@ pub(crate) fn build_bound_method_closure( }) } +include!("native_module/class_method_values.rs"); + /// #6173: sentinel "method name" installed in the name-capture slots (1, 2) of /// a BOUND_METHOD closure whose target is a SYMBOL-keyed class method. A /// symbol method has no string name to re-resolve at call time, so the /// closure instead carries the already-resolved dispatch data in two extra /// capture slots: -/// /// slot 0: receiver (NaN-boxed instance/prototype-ref, or the INT32 class /// ref for a static method) /// slot 1: `SYMBOL_BOUND_METHOD_NAME.as_ptr()` — the discriminant, compared @@ -1467,6 +1475,7 @@ pub(crate) fn build_symbol_bound_method_closure( param_count: u32, has_rest: bool, is_static: bool, + display_name: &str, ) -> f64 { // The allocation itself is a safepoint. Keep the receiver current before // storing it into the freshly allocated closure. @@ -1504,7 +1513,12 @@ pub(crate) fn build_symbol_bound_method_closure( }; closure_handle.with_mut_ptr::(|closure| { set_builtin_closure_length(closure as usize, spec_length); - crate::gc::runtime_write_barrier_root_heap_word(closure as u64); + }); + closure_handle.with_mut_ptr::(|closure| { + set_bound_native_closure_name(closure, display_name) + }); + closure_handle.with_mut_ptr::(|closure| { + crate::gc::runtime_write_barrier_root_heap_word(closure as u64) }); closure_handle.with_mut_ptr::(|closure| { crate::value::js_nanbox_pointer(closure as i64) @@ -1770,7 +1784,7 @@ pub fn class_prototype_method_value_for_name(class_id: u32, method_name: &str) - // `(class_id, method_name)` pair the program ever asks for, so the // total leak is bounded by the static set of decorated method // descriptors. The cache below short-circuits repeat queries. - let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); + let leaked = intern_class_method_name(class_id, method_name); let class_ref = class_prototype_ref_value(class_id); // Build the closure DIRECTLY (not via `js_class_method_bind`, whose // canonical short-circuit would call back into this function and recurse). 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 new file mode 100644 index 0000000000..00bde3ed62 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/class_method_values.rs @@ -0,0 +1,129 @@ +pub(crate) fn class_evaluation_method_value_for_name( + owner_class_id: u32, + method_name: &str, + evaluation_brand: f64, +) -> f64 { + let cache_key = format!("#"); + let cached = crate::object::js_object_get_own_field_or_undef( + evaluation_brand, + cache_key.as_ptr(), + cache_key.len(), + ); + if cached.to_bits() != crate::value::TAG_UNDEFINED { + return cached; + } + + let scope = crate::gc::RuntimeHandleScope::new(); + let brand = scope.root_nanbox_f64(evaluation_brand); + let leaked = intern_class_method_name(owner_class_id, method_name); + let method = build_bound_method_closure_with_private_brand( + class_prototype_ref_value(owner_class_id), + leaked.as_ptr(), + leaked.len(), + Some(brand.get_nanbox_f64()), + ); + let method = scope.root_nanbox_f64(method); + let key = crate::string::js_string_from_bytes(cache_key.as_ptr(), cache_key.len() as u32); + let key = scope.root_string_ptr(key); + let class_obj = JSValue::from_bits(brand.get_nanbox_f64().to_bits()) + .as_pointer::() as *mut ObjectHeader; + let class_obj = scope.root_raw_mut_ptr(class_obj); + class_obj.with_mut_ptr::(|class_obj| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name(class_obj, key, method.get_nanbox_f64()); + }); + }); + method.get_nanbox_f64() +} + +pub(crate) fn class_private_static_method_value_for_name( + owner_class_id: u32, + method_name: &str, + evaluation_brand: f64, +) -> f64 { + let cache_name = format!("#"); + if class_registry::is_class_object_value(evaluation_brand) { + let cached = crate::object::js_object_get_own_field_or_undef( + evaluation_brand, + cache_name.as_ptr(), + cache_name.len(), + ); + if cached.to_bits() != crate::value::TAG_UNDEFINED { + return cached; + } + + let scope = crate::gc::RuntimeHandleScope::new(); + let brand = scope.root_nanbox_f64(evaluation_brand); + let leaked = intern_class_method_name(owner_class_id, method_name); + let method = build_bound_method_closure_with_private_brand( + class_constructor_ref_value(owner_class_id), + leaked.as_ptr(), + leaked.len(), + Some(brand.get_nanbox_f64()), + ); + let method = scope.root_nanbox_f64(method); + let key = crate::string::js_string_from_bytes(cache_name.as_ptr(), cache_name.len() as u32); + let key = scope.root_string_ptr(key); + let class_obj = JSValue::from_bits(brand.get_nanbox_f64().to_bits()) + .as_pointer::() as *mut ObjectHeader; + let class_obj = scope.root_raw_mut_ptr(class_obj); + class_obj.with_mut_ptr::(|class_obj| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name(class_obj, key, method.get_nanbox_f64()); + }) + }); + return method.get_nanbox_f64(); + } + + if let Some(bits) = CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + cache + .borrow() + .get(&(owner_class_id, cache_name.clone())) + .copied() + }) { + return f64::from_bits(bits); + } + let leaked = intern_class_method_name(owner_class_id, method_name); + let method = build_bound_method_closure_with_private_brand( + class_constructor_ref_value(owner_class_id), + leaked.as_ptr(), + leaked.len(), + Some(evaluation_brand), + ); + class_prototype_method_value_cache_root_store(owner_class_id, cache_name, method.to_bits()); + method +} +static CLASS_METHOD_NAME_INTERNER: OnceLock>> = + OnceLock::new(); + +/// Stable storage for the method-name pointer captured by bound-method +/// closures. The key set is bounded by the program's declared class methods, +/// even when one class expression is evaluated arbitrarily many times. +pub(super) fn intern_class_method_name(class_id: u32, method_name: &str) -> &'static [u8] { + let interner = CLASS_METHOD_NAME_INTERNER.get_or_init(|| RwLock::new(HashMap::new())); + let key = (class_id, method_name.to_string()); + if let Ok(guard) = interner.read() { + if let Some(bytes) = guard.get(&key).copied() { + return bytes; + } + } + let mut guard = interner + .write() + .expect("class method name interner poisoned"); + if let Some(bytes) = guard.get(&key).copied() { + return bytes; + } + let bytes: &'static [u8] = method_name.as_bytes().to_vec().leak(); + guard.insert(key, bytes); + bytes +} + +/// Allocate a bound-method closure for the named method. Keeping this raw +/// builder separate avoids recursion through the canonical method cache. +pub(crate) fn build_bound_method_closure( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, +) -> f64 { + build_bound_method_closure_with_private_brand(instance, method_name_ptr, method_name_len, None) +} 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 a487fcad12..3aa6f2dd26 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -538,6 +538,48 @@ pub extern "C" fn js_object_define_property( return obj_value; } if let Some(name) = super::super::metadata_key_to_string(key_value) { + let has_get = desc_has_field(descriptor_value, b"get"); + let has_set = desc_has_field(descriptor_value, b"set"); + if super::super::class_prototype_ref_id(obj_value).is_none() && (has_get || has_set) + { + let descriptor_value = desc_handle.get_nanbox_f64(); + let get_field = desc_read_field(descriptor_value, b"get"); + let get_field = scope.root_nanbox_u64(get_field.bits()); + let set_field = desc_read_field(desc_handle.get_nanbox_f64(), b"set"); + let set_field = scope.root_nanbox_u64(set_field.bits()); + let get_bits = has_get.then(|| { + (get_field.get_nanbox_u64() != crate::value::TAG_UNDEFINED) + .then(|| get_field.get_nanbox_u64()) + .unwrap_or(0) + }); + let set_bits = has_set.then(|| { + (set_field.get_nanbox_u64() != crate::value::TAG_UNDEFINED) + .then(|| set_field.get_nanbox_u64()) + .unwrap_or(0) + }); + let class_value = f64::from_bits(obj_value_handle.get_heap_word_u64()); + let descriptor_value = desc_handle.get_nanbox_f64(); + let enumerable = desc_has_field(descriptor_value, b"enumerable") + .then(|| descriptor_enumerable(desc_handle.get_nanbox_f64())); + let descriptor_value = desc_handle.get_nanbox_f64(); + let configurable = + desc_has_field(descriptor_value, b"configurable").then(|| { + crate::value::js_is_truthy(f64::from_bits( + desc_read_field(desc_handle.get_nanbox_f64(), b"configurable") + .bits(), + )) != 0 + }); + super::super::class_registry::register_class_dynamic_static_accessor( + target_cid, + class_value, + &name, + get_bits, + set_bits, + enumerable, + configurable, + ); + return obj_value; + } let desc_ptr = extract_obj_ptr(descriptor_value); if !desc_ptr.is_null() { let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); diff --git a/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs b/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs index 431aefce77..8c4e6545b2 100644 --- a/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs +++ b/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs @@ -762,6 +762,7 @@ pub(crate) unsafe fn define_property_force_store_value( let scope = crate::gc::RuntimeHandleScope::new(); let obj_handle = scope.root_raw_mut_ptr(obj); let key_handle = scope.root_string_ptr(key_str); + let value_handle = scope.root_nanbox_f64(value); let mut obj = obj_handle.get_raw_mut_ptr::(); if obj.is_null() || (obj as usize) <= 0x10000 { return; @@ -771,8 +772,50 @@ pub(crate) unsafe fn define_property_force_store_value( let gc = gc_header_for(obj); let saved = (*gc)._reserved; (*gc)._reserved &= !immutability; - let key_str = key_handle.get_raw_const_ptr::(); - js_object_set_field_by_name(obj, key_str, value); + // `js_object_set_field_by_name` implements [[Set]], including inherited + // setter lookup. DefineProperty must write the receiver's own slot + // directly. Ensure the shape entry exists, then locate its parallel value + // slot and store by index (or through object-owned overflow storage). + obj_handle.with_mut_ptr::(|obj| { + key_handle.with_const_ptr::(|key_str| { + ensure_key_in_keys_array(obj, key_str) + }) + }); + let slot = obj_handle.with_mut_ptr::(|obj| { + key_handle.with_const_ptr::(|key_str| { + let keys = crate::object::object_keys_array(obj); + if keys.is_null() { + return None; + } + let count = crate::array::keys_array_len_capped_to_capacity(keys) as usize; + let (slots, slot_len) = crate::object::keys_array_dense_slots(keys); + for i in 0..count.min(slot_len) { + let stored = JSValue::from_bits((*slots.add(i)).to_bits()); + if crate::string::js_string_key_matches(stored, key_str) { + let live_slots = crate::object::object_live_slot_count(obj) as usize; + return Some((i, live_slots)); + } + } + None + }) + }); + if let Some((index, live_slots)) = slot { + obj_handle.with_mut_ptr::(|obj| { + if index < live_slots.max(crate::object::INLINE_SLOT_FLOOR) { + js_object_set_field( + obj, + index as u32, + JSValue::from_bits(value_handle.get_nanbox_f64().to_bits()), + ); + } else { + crate::object::overflow_set( + obj as usize, + index, + value_handle.get_nanbox_f64().to_bits(), + ); + } + }); + } // Re-fetch after a possible evacuation, then restore the immutability bits. obj = obj_handle.get_raw_mut_ptr::(); if !obj.is_null() && (obj as usize) > 0x10000 { 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 c6125dca3a..ab40384314 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -191,32 +191,35 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { if let Some(class_id) = super::super::class_ref_id(obj_value) { let present = super::super::has_own_helpers::str_from_string_header(key_str) .map(|key| { - if key.starts_with('#') { - // Private static elements are never reflectable own - // properties of the class constructor. + if super::super::field_get_set::is_internal_runtime_key(key) { false } else if super::super::class_registry::class_is_key_deleted(class_id, key) { false } else if matches!(key, "length" | "prototype") { true } else if key == "name" - && super::super::class_registry::lookup_static_method_in_chain(class_id, key) - .is_none() + && super::super::class_registry::lookup_static_method_in_chain( + class_id, key, + ) + .is_none() { super::super::class_registry::class_name_for_id(class_id).is_some() } else { - CLASS_DYNAMIC_PROPS.with(|m| { + let has_public_data = CLASS_DYNAMIC_PROPS.with(|m| { m.borrow() .get(&class_id) .is_some_and(|props| props.contains_key(key)) - }) || super::super::class_registry::lookup_static_method_in_chain(class_id, key) - .is_some() - // A static accessor (`static get x()`) is an own - // property of the constructor — own-only, mirroring - // getOwnPropertyDescriptor (class/definition/ - // {getters,setters}-prop-desc `staticX`). - || super::super::class_registry::class_own_static_accessor_ptrs(class_id, key) + }); + has_public_data + || (!key.starts_with('#') + && (super::super::class_registry::lookup_static_method_in_chain( + class_id, key, + ) .is_some() + || super::super::class_registry::class_own_static_accessor_ptrs( + class_id, key, + ) + .is_some())) } }) .unwrap_or(false); @@ -419,8 +422,7 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { // for them. Plain literals keep class_id 0. if (*obj).class_id != 0 { if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { - if key.starts_with('#') || super::super::field_get_set::is_internal_runtime_key(key) - { + if super::super::field_get_set::is_internal_runtime_key(key) { return f64::from_bits(TAG_FALSE); } } @@ -442,9 +444,10 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { if !super::super::class_registry::class_is_key_deleted(cid, key) && (key == "constructor" - || super::super::class_registry::class_own_accessor_ptrs(cid, key) - .is_some() - || super::super::native_module::class_has_own_method(cid, key)) + || (!key.starts_with('#') + && (super::super::class_registry::class_own_accessor_ptrs(cid, key) + .is_some() + || super::super::native_module::class_has_own_method(cid, key)))) { return f64::from_bits(TAG_TRUE); } @@ -563,11 +566,12 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6 if let Some(key_name) = super::super::has_own_helpers::str_from_string_header(key_str) { - let is_static_field = !key_name.starts_with('#') - && super::super::class_registry::class_own_static_field_value( - class_id, key_name, - ) - .is_some(); + let is_static_field = + !super::super::field_get_set::is_internal_runtime_key(key_name) + && super::super::class_registry::class_own_static_field_value( + class_id, key_name, + ) + .is_some(); return f64::from_bits(if is_static_field { TAG_TRUE } else { TAG_FALSE }); } } diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index 5f463af7f0..7a0f09f89c 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -285,6 +285,15 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { None }; let buffer_backed_prototype = |addr: usize| -> Option { + // A native ArrayBuffer/SharedArrayBuffer constructed with a distinct + // newTarget (subclassing / Reflect.construct) records that custom + // [[Prototype]] in the same side table as typed arrays. Honor it before + // falling back to the intrinsic buffer prototype. + if let Some(proto_bits) = super::super::prototype_chain::object_static_prototype(addr) { + if proto_bits != crate::value::TAG_NULL { + return Some(f64::from_bits(proto_bits)); + } + } let name = if crate::buffer::is_array_buffer(addr) { "ArrayBuffer" } else if crate::buffer::is_shared_array_buffer(addr) { @@ -338,6 +347,17 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { }; if top16 == 0x7FFE { let class_id = (bits & 0xFFFF_FFFF) as u32; + if super::super::class_prototype_ref_id(obj_value).is_none() { + // A class whose heritage is a runtime function value has no Perry + // parent class id. Its constructor's [[Prototype]] is that exact + // function object (not Function.prototype), as observed by + // Object.getPrototypeOf(D) and static `super` lookup. + let dynamic_parent = super::super::js_get_dynamic_parent_value(class_id); + let parent_value = crate::value::JSValue::from_bits(dynamic_parent.to_bits()); + if !parent_value.is_undefined() && !parent_value.is_null() { + return dynamic_parent; + } + } if let Some(parent_id) = get_parent_class_id(class_id) { // #8343 followup: `NATIVE_MODULE_CLASS_ID` (0xFFFFFFFE) is a // sentinel, not a real class. A prior `Object.create(proto)` whose diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index cae2a54b10..cb655f9d23 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -191,7 +191,14 @@ unsafe fn set_property_key_resolved(obj_value: f64, key: f64, value: f64) -> f64 // get side already passes the raw NaN-boxed bits into the by-name dispatch // (which has a dedicated 0x7FFE class-ref branch); mirror that on the set // side so static-accessor and prototype instance-setter dispatch run. - if super::class_ref_id(obj_value).is_some() { + if let Some(class_id) = super::class_ref_id(obj_value) { + if super::class_prototype_ref_id(obj_value).is_none() { + let name_ptr = crate::string::string_data(key_str); + let name_len = (*key_str).byte_len as usize; + if std::slice::from_raw_parts(name_ptr, name_len) == b"prototype" { + crate::error::throw_immutable_write(class_id, "prototype"); + } + } js_object_set_field_by_name(obj_value.to_bits() as *mut ObjectHeader, key_str, value); return value; } @@ -382,6 +389,24 @@ pub unsafe extern "C" fn js_super_accessor_get( } } } + let mut cid = parent_class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(result) = + crate::object::class_registry::class_dynamic_static_accessor_getter_value( + cid, key_name, receiver, + ) + { + return result; + } + match crate::object::get_parent_class_id(cid) { + Some(parent) if parent != 0 && parent != cid => { + cid = parent; + depth += 1; + } + _ => break, + } + } // (b) parent static data field (CLASS_DYNAMIC_PROPS), same walk. let mut cid = parent_class_id; let mut depth = 0usize; @@ -400,6 +425,33 @@ pub unsafe extern "C" fn js_super_accessor_get( } } } + // A function-valued superclass has no Perry class id, but its + // constructor value was captured by `js_register_class_parent_dynamic` + // under the child class id. Resolve `super.x` against that function's + // own properties with the child constructor as Receiver. + if let Some(child_id) = super::class_ref_id(receiver) { + let parent = crate::object::js_get_dynamic_parent_value(child_id); + let parent_handle = scope.root_heap_word_u64(parent.to_bits()); + let pv = crate::value::JSValue::from_bits(parent.to_bits()); + if !pv.is_undefined() && !pv.is_null() { + if let Some(key_name) = key_name.as_ref() { + if pv.is_pointer() { + let ptr = pv.as_pointer::() as usize; + if crate::closure::is_closure_ptr(ptr) { + let own = crate::closure::closure_get_dynamic_prop(ptr, key_name); + if own.to_bits() != crate::value::TAG_UNDEFINED { + return own; + } + } + } + } + return crate::proxy::js_reflect_get( + f64::from_bits(parent_handle.get_heap_word_u64()), + key_handle.get_nanbox_f64(), + f64::from_bits(receiver_handle.get_heap_word_u64()), + ); + } + } return f64::from_bits(crate::value::TAG_UNDEFINED); } if let Some(key_name) = key_name { @@ -441,6 +493,14 @@ pub unsafe extern "C" fn js_super_accessor_get( // class declaration (test262 super/prop-{dot,expr}-cls-val). Falls back to // the older table for synthetic-prototype sources that lack a decl entry. let mut proto = crate::object::class_decl_prototype_object(parent_class_id); + if proto.is_null() { + let materialized = + crate::object::class_registry::class_decl_prototype_value(parent_class_id); + if crate::value::JSValue::from_bits(materialized.to_bits()).is_pointer() { + proto = crate::value::JSValue::from_bits(materialized.to_bits()) + .as_pointer::() as *mut _; + } + } if proto.is_null() { proto = crate::object::class_prototype_object(parent_class_id); } diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 63b0b37244..fe0797c5fa 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -207,13 +207,15 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, instance_ov unsafe { if let Some(obj) = meta_capable_object(obj_ptr) { // `object_meta_ensure` allocates and may evacuate the owner. Keep - // the caller's pointer rooted and reload it before the semantic - // ShapeId transition below. + // both the caller's pointer and the prototype rooted, then reload + // them before the stores below. let scope = crate::gc::RuntimeHandleScope::new(); let obj_handle = scope.root_raw_mut_ptr(obj); + let proto_handle = scope.root_heap_word_u64(proto_bits); let (meta, obj) = obj_handle.across_mut::(|| { crate::object::object_meta_ensure(obj) }); + let proto_bits = proto_handle.get_heap_word_u64(); (*meta).prototype = proto_bits; if instance_override { (*meta).flags |= crate::object::OBJECT_META_FLAG_PROTO_OVERRIDE; diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index bd8b5cdc27..236c01946c 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -252,3 +252,70 @@ pub fn scan_implicit_this_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor< } }); } + +crate::perry_thread_local! { + /// Active inline derived-constructor `super()` binding cells. An arrow + /// created inside a constructor has its own codegen context, so it cannot + /// name the outer function's alloca directly; this stack gives it the + /// exact live binding cell without turning the state into a process-global + /// boolean. + static DERIVED_SUPER_BINDING_STACK: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; +} + +#[no_mangle] +pub extern "C" fn js_derived_super_scope_push(slot: *mut u8) { + DERIVED_SUPER_BINDING_STACK.with(|stack| stack.borrow_mut().push(slot as usize)); +} + +#[no_mangle] +pub extern "C" fn js_derived_super_scope_pop() { + DERIVED_SUPER_BINDING_STACK.with(|stack| { + stack.borrow_mut().pop(); + }); +} + +pub(crate) fn derived_super_binding_stack_savepoint() -> usize { + DERIVED_SUPER_BINDING_STACK.with(|stack| stack.borrow().len()) +} + +pub(crate) fn derived_super_binding_stack_restore(depth: usize) { + DERIVED_SUPER_BINDING_STACK.with(|stack| stack.borrow_mut().truncate(depth)); +} + +/// Bind the innermost derived constructor's `this` from a nested arrow. The +/// base constructor has already run when this is called; a duplicate therefore +/// throws at binding time, as required by EvaluateCall(super()). +#[no_mangle] +pub extern "C" fn js_derived_super_bind_current() -> f64 { + let slot = DERIVED_SUPER_BINDING_STACK.with(|stack| stack.borrow().last().copied()); + let Some(slot) = slot else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let slot = slot as *mut u8; + unsafe { + if slot.read() != 0 { + return crate::error::js_throw_reference_error_this_before_super(); + } + slot.write(1); + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// Throw when a separately-emitted arrow reads the active derived +/// constructor's lexical `this` before `super()` has initialized it. +/// Ordinary functions have no active binding stack entry, so the helper is a +/// cheap no-op for their `this` reads. +#[no_mangle] +pub extern "C" fn js_derived_this_check_current() -> f64 { + let slot = DERIVED_SUPER_BINDING_STACK.with(|stack| stack.borrow().last().copied()); + if let Some(slot) = slot { + let slot = slot as *const u8; + unsafe { + if slot.read() == 0 { + return crate::error::js_throw_reference_error_this_before_super(); + } + } + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} diff --git a/crates/perry-runtime/src/object/weakref_proto_thunks.rs b/crates/perry-runtime/src/object/weakref_proto_thunks.rs index ddd86f413c..05629d1b73 100644 --- a/crates/perry-runtime/src/object/weakref_proto_thunks.rs +++ b/crates/perry-runtime/src/object/weakref_proto_thunks.rs @@ -68,13 +68,13 @@ use crate::weakref::{ /// `obj` must be a valid, readable `ObjectHeader` pointer (the caller has /// already validated it as a live heap object). pub unsafe fn try_weak_method_dispatch( - obj: *const ObjectHeader, + _obj: *const ObjectHeader, receiver: f64, method_name: &str, args_ptr: *const f64, args_len: usize, ) -> Option { - let class_id = (*obj).class_id; + let class_id = weak_wrapper_class_id(receiver)?; if !matches!( class_id, CLASS_ID_WEAKMAP | CLASS_ID_WEAKSET | CLASS_ID_WEAKREF | CLASS_ID_FINALIZATION_REGISTRY @@ -163,6 +163,19 @@ pub fn weak_wrapper_class_id(receiver: f64) -> Option { ) { return Some(cid); } + // User subclasses keep their own class id but their registered parent + // chain terminates at the reserved WeakMap/WeakSet constructor ids. + const CLASS_ID_WEAKMAP_RESERVED: u32 = 0xFFFF_002C; + const CLASS_ID_WEAKSET_RESERVED: u32 = 0xFFFF_002D; + let mut current = cid; + for _ in 0..64 { + match crate::object::get_parent_class_id(current) { + Some(CLASS_ID_WEAKMAP_RESERVED) => return Some(CLASS_ID_WEAKMAP), + Some(CLASS_ID_WEAKSET_RESERVED) => return Some(CLASS_ID_WEAKSET), + Some(parent) => current = parent, + None => break, + } + } } None } diff --git a/crates/perry-runtime/src/promise/subclass.rs b/crates/perry-runtime/src/promise/subclass.rs index 985569858a..5ad228b020 100644 --- a/crates/perry-runtime/src/promise/subclass.rs +++ b/crates/perry-runtime/src/promise/subclass.rs @@ -114,17 +114,22 @@ pub(crate) fn subclass_backing_promise(value: f64) -> Option<*mut Promise> { /// synchronously per step 2. #[no_mangle] pub extern "C" fn js_promise_subclass_init(this: f64, executor: f64) -> f64 { - let obj = match unsafe { instance_object_ptr(this) } { - Some(o) => o, - None => return this, - }; + let scope = crate::gc::RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let executor = scope.root_nanbox_f64(executor); + if unsafe { instance_object_ptr(this.get_nanbox_f64()) }.is_none() { + return this.get_nanbox_f64(); + } // 27.2.3.1 step 2: a non-callable executor throws a TypeError, before any // promise is created. - if !super::spec_combinators::is_callable_value(executor) { + if !super::spec_combinators::is_callable_value(executor.get_nanbox_f64()) { let msg = b"Promise resolver is not a function"; - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(s); + let s = scope.root_string_ptr(crate::string::js_string_from_bytes( + msg.as_ptr(), + msg.len() as u32, + )); + let err = s.with_mut_ptr::(|s| crate::error::js_typeerror_new(s)); let v = f64::from_bits(JSValue::pointer(err as *const u8).bits()); crate::exception::js_throw(v); } @@ -132,27 +137,35 @@ pub extern "C" fn js_promise_subclass_init(this: f64, executor: f64) -> f64 { // Build the backing promise + resolving functions, run the executor. Keep a // raw root on the backing cell across the string-key allocation below (which // can GC) by stashing it immediately after the executor runs. - let promise = super::js_promise_new(); - let (resolve_closure, reject_closure) = super::combinators::make_resolving_functions(promise); - let resolve_f64 = crate::value::js_nanbox_pointer(resolve_closure as i64); - let reject_f64 = crate::value::js_nanbox_pointer(reject_closure as i64); + let promise = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + super::js_promise_new() as i64 + )); + let promise_ptr = crate::value::js_nanbox_get_pointer(promise.get_nanbox_f64()) as *mut Promise; + let (resolve_closure, reject_closure) = + super::combinators::make_resolving_functions(promise_ptr); + let resolve = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(resolve_closure as i64)); + let reject = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(reject_closure as i64)); // 27.2.3.1 step 10: run the executor; a throw rejects the promise via the // shared resolving `reject` (so the [[AlreadyResolved]] guard makes a later // resolve/reject a no-op). `js_native_call_value` accepts both POINTER_TAG // closures and raw-pointer-bits closures, so `executor` is passed as-is. - let args = [resolve_f64, reject_f64]; + let args = [resolve.get_nanbox_f64(), reject.get_nanbox_f64()]; if let Err(reason) = super::combinators::combinator_catch_js(|| unsafe { - crate::closure::js_native_call_value(executor, args.as_ptr(), args.len()) + crate::closure::js_native_call_value(executor.get_nanbox_f64(), args.as_ptr(), args.len()) }) { - crate::closure::js_closure_call1(reject_closure, reason); + let reject = crate::value::js_nanbox_get_pointer(reject.get_nanbox_f64()) + as *const crate::closure::ClosureHeader; + crate::closure::js_closure_call1(reject, reason); } // #7795: arm the probe gate before the field exists, so no reader can // observe a stashed backing cell while the flag still says "never". PROMISE_SUBCLASS_EVER.store(true, std::sync::atomic::Ordering::Relaxed); let key = crate::string::js_string_from_bytes(BACKING_KEY.as_ptr(), BACKING_KEY.len() as u32); - let backing_bits = JSValue::pointer(promise as *const u8).bits(); + let obj = unsafe { instance_object_ptr(this.get_nanbox_f64()) } + .expect("rooted Promise subclass receiver must remain an object"); + let backing_bits = promise.get_nanbox_f64().to_bits(); js_object_set_field_by_name(obj, key, f64::from_bits(backing_bits)); - this + this.get_nanbox_f64() } diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 1ca657a817..5aa7f0a66b 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -521,6 +521,34 @@ pub extern "C" fn js_proxy_is_proxy(value: f64) -> i32 { } } +/// Resolve the backing object used by Perry's private-element storage without +/// invoking any Proxy trap. Private names use the object's internal +/// [[PrivateElements]] list in ECMAScript; they are deliberately not ordinary +/// `[[Get]]`/`[[Set]]` operations. Perry's Proxy is a stable registry handle, +/// so its private storage lives on the backing target and all private-element +/// entry points consistently resolve through this helper. +pub(crate) fn private_element_receiver(mut value: f64) -> f64 { + for _ in 0..32 { + let Some(id) = lookup(value) else { + return value; + }; + let (target, revoked) = PROXIES.with(|p| { + p.borrow() + .get(id as usize) + .and_then(|entry| entry.as_ref()) + .map(|entry| (entry.target, entry.revoked)) + .unwrap_or((f64::from_bits(TAG_UNDEFINED), false)) + }); + if revoked { + revoked_return_with_message( + "Cannot access a private element on a proxy that has been revoked", + ); + } + value = target; + } + value +} + /// `IsArray`'s Proxy branch (ECMA-262 §7.2.2). If `value` is a live Proxy, /// returns `Some(target)` so the caller can recurse on the target; if the Proxy /// has been revoked, throws a `TypeError` (does not return). Returns `None` for @@ -968,6 +996,36 @@ pub extern "C" fn js_proxy_get(proxy_boxed: f64, key: f64) -> f64 { target_get(target, key) } +/// Resolve the ultimate target when a Proxy wraps a class constructor. Used +/// by method-call dispatch to bind a static method's visible `this` to the +/// Proxy receiver while retaining the target class as its lexical owner. +pub(crate) fn proxy_target_class_id(mut value: f64) -> Option { + let mut depth = 0usize; + while let Some(id) = lookup(value) { + value = PROXIES.with(|proxies| { + proxies + .borrow() + .get(id as usize) + .and_then(|entry| entry.as_ref()) + .map(|entry| entry.target) + .unwrap_or(f64::from_bits(TAG_UNDEFINED)) + }); + depth += 1; + if depth >= 32 { + return None; + } + } + if let Some(class_id) = crate::object::class_ref_id(value) { + return Some(class_id); + } + if crate::object::is_class_object_value(value) { + let raw = extract_pointer(value.to_bits()) as *const crate::ObjectHeader; + let class_id = crate::object::js_object_get_class_id(raw); + return (class_id != 0).then_some(class_id); + } + None +} + /// Extract a raw heap pointer (48-bit) from either a NaN-boxed value /// (POINTER_TAG / STRING_TAG) or a raw i64/f64-reinterpreted pointer /// (module-level globals store Arrays/Objects as raw I64s, not NaN-boxed). @@ -1024,10 +1082,23 @@ fn target_get_property_key(target: f64, property_key: f64) -> f64 { if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 { return unsafe { crate::symbol::js_object_get_symbol_property(target, property_key) }; } - let obj_ptr = extract_pointer(target.to_bits()) as *const crate::ObjectHeader; let key_ptr = crate::value::js_get_string_pointer_unified(property_key) as *const crate::StringHeader; - if obj_ptr.is_null() || key_ptr.is_null() { + if key_ptr.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + // Class constructors use Perry's INT32-tagged ClassRef representation, + // not a heap pointer. Preserve those bits exactly as the ordinary dynamic + // class-property path does; pointer extraction would turn the target into + // null and make `new Proxy(C, {}).staticMethod` read as `undefined`. + if crate::object::class_ref_id(target).is_some() { + return crate::object::js_object_get_field_by_name_f64( + target.to_bits() as *const crate::ObjectHeader, + key_ptr, + ); + } + let obj_ptr = extract_pointer(target.to_bits()) as *const crate::ObjectHeader; + if obj_ptr.is_null() { return f64::from_bits(TAG_UNDEFINED); } crate::object::js_object_get_field_by_name_f64(obj_ptr, key_ptr) @@ -1511,6 +1582,24 @@ unsafe fn build_create_data_descriptor(value: f64) -> f64 { ) } +/// Define a writable, enumerable, configurable own data property using the +/// receiver's `[[DefineOwnProperty]]`. This is the operation used by public +/// class fields: it bypasses inherited setters on ordinary objects and still +/// drives a Proxy's `defineProperty` trap. +pub(crate) fn create_data_property(receiver: f64, key: f64, value: f64) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let key = scope.root_nanbox_f64(key); + let value = scope.root_nanbox_f64(value); + let descriptor = unsafe { build_create_data_descriptor(value.get_nanbox_f64()) }; + let descriptor = scope.root_nanbox_f64(descriptor); + crate::value::js_is_truthy(js_reflect_define_property( + receiver.get_nanbox_f64(), + key.get_nanbox_f64(), + descriptor.get_nanbox_f64(), + )) != 0 +} + /// #5129: build a `{ value }`-only property descriptor — the `valueDesc` of /// OrdinarySetWithOwnDescriptor step 2.d.iii, used to update an existing /// writable data property on a Proxy receiver without disturbing its other @@ -2088,6 +2177,40 @@ pub extern "C" fn js_super_put_value_set( strict: i32, ) -> f64 { let receiver = normalize_accessor_receiver(receiver); + // Static-context super uses the parent CONSTRUCTOR as the lookup target, + // while retaining the child constructor as Receiver. A statically-known + // class parent is represented by a ClassRef; a function-valued parent is + // the value captured at class-definition time. The previous instance-only + // path looked at `Parent.prototype` and made valid static writes fail. + if let Some(child_id) = crate::object::class_ref_id(receiver) { + let target = if parent_class_id != 0 { + f64::from_bits(crate::value::INT32_TAG | parent_class_id as u64) + } else { + crate::object::js_get_dynamic_parent_value(child_id) + }; + let tv = crate::value::JSValue::from_bits(target.to_bits()); + if !tv.is_undefined() && !tv.is_null() { + return js_put_value_set(target, key, value, receiver, strict); + } + // A base class's constructor inherits from Function.prototype. Perry + // does not materialize that object for this path; when lookup misses, + // OrdinarySet creates the own property on Receiver. + return js_put_value_set(receiver, key, value, receiver, strict); + } + if parent_class_id == 0 && crate::object::is_class_object_value(receiver) { + let obj = crate::value::JSValue::from_bits(receiver.to_bits()) + .as_pointer::(); + let child_id = if obj.is_null() { + 0 + } else { + crate::object::js_object_get_class_id(obj) + }; + let dynamic_parent = crate::object::js_get_dynamic_parent_value(child_id); + if crate::value::JSValue::from_bits(dynamic_parent.to_bits()).is_undefined() { + return js_put_value_set(receiver, key, value, receiver, strict); + } + return js_put_value_set(dynamic_parent, key, value, receiver, strict); + } let receiver_parent_class_id = receiver_super_parent_class_id(receiver); if let Some(ok) = class_super_accessor_set(parent_class_id, key, value, receiver).or_else(|| { diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 932d227f37..87413c5249 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -32,6 +32,9 @@ unsafe fn write_fast_path_receiver_kind_ok( if class_id == crate::object::NATIVE_MODULE_CLASS_ID { return false; } + if crate::object::is_class_object_ptr(obj.cast()) { + return false; + } class_id != 0 || obj_flags & crate::gc::OBJ_FLAG_PLAIN_ORDINARY != 0 } diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index 4a30664ad0..d184854302 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -225,6 +225,16 @@ pub(crate) unsafe fn symbol_description_text( description_bytes_from_header((*sym_ptr).description).map(std::sync::Arc::from) } +/// SetFunctionName spelling for a Symbol property key: an undefined +/// description produces the empty string, otherwise `[description]`. +pub(crate) unsafe fn symbol_function_name(sym_key: usize) -> String { + let sym_ptr = sym_key as *const SymbolHeader; + match symbol_description_text(sym_ptr) { + Some(desc) => format!("[{}]", String::from_utf8_lossy(desc.as_ref())), + None => String::new(), + } +} + /// The raw payload bytes of a description `StringHeader`, WITHOUT UTF-8 /// validation. `str_from_header` validates and would drop a WTF-8 description /// on the floor (#7246). diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index ddc4e90e8c..83b79c635d 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -360,6 +360,7 @@ pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f6 param_count, has_rest, !is_proto_ref, + &crate::symbol::symbol_function_name(sym_key), ); } } @@ -636,6 +637,7 @@ pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f6 param_count, has_rest, false, + &crate::symbol::symbol_function_name(sym_key), ); } } diff --git a/crates/perry-runtime/src/weakref.rs b/crates/perry-runtime/src/weakref.rs index 53ff9ac38b..e080b82d81 100644 --- a/crates/perry-runtime/src/weakref.rs +++ b/crates/perry-runtime/src/weakref.rs @@ -14,7 +14,8 @@ use crate::array::{ ArrayHeader, }; use crate::object::{ - js_object_alloc_with_shape, js_object_get_field_by_name, js_object_set_field, ObjectHeader, + js_object_alloc_with_shape, js_object_get_field_by_name, js_object_set_field, + js_object_set_field_by_name, ObjectHeader, }; use crate::value::{ js_nanbox_get_pointer, JSValue, BIGINT_TAG, POINTER_MASK, POINTER_TAG, STRING_TAG, TAG_MASK, @@ -1390,6 +1391,8 @@ pub extern "C" fn js_weakmap_new() -> *mut ObjectHeader { obj } +include!("weakref/subclass.rs"); + /// `WeakMap ( [ iterable ] )`'s `AddEntriesFromIterable` step. `map` is the /// already-allocated (empty) WeakMap from `js_weakmap_new`; this only /// populates it. #5834: only fetches/validates the `set` adder when diff --git a/crates/perry-runtime/src/weakref/subclass.rs b/crates/perry-runtime/src/weakref/subclass.rs new file mode 100644 index 0000000000..45d5e82e37 --- /dev/null +++ b/crates/perry-runtime/src/weakref/subclass.rs @@ -0,0 +1,37 @@ +/// Initialize the WeakMap/WeakSet internal entry slot on an existing user +/// class instance, then consume the optional iterable through the ordinary +/// builtin algorithm. `kind`: 0 = WeakMap, 1 = WeakSet. +#[no_mangle] +pub extern "C" fn js_weak_collection_subclass_init(this: f64, kind: i32, iterable: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let iterable = scope.root_nanbox_f64(iterable); + let raw = crate::value::js_nanbox_get_pointer(this.get_nanbox_f64()) as usize; + let is_object = unsafe { + crate::value::addr_class::try_read_gc_header(raw) + .is_some_and(|header| header.obj_type == crate::gc::GC_TYPE_OBJECT) + }; + if !is_object { + return this.get_nanbox_f64(); + } + let object = scope.root_raw_mut_ptr( + crate::value::js_nanbox_get_pointer(this.get_nanbox_f64()) as *mut ObjectHeader, + ); + let entries = js_array_alloc(0); + let entries = scope.root_raw_mut_ptr(entries); + let key = crate::string::js_string_from_bytes(WEAK_ENTRIES_KEY.as_ptr(), 18); + object.with_mut_ptr::(|object| { + entries.with_mut_ptr::(|entries| { + js_object_set_field_by_name( + object, + key, + f64::from_bits(JSValue::array_ptr(entries).bits()), + ); + }) + }); + if kind == 0 { + js_weakmap_init_iterable(this.get_nanbox_f64(), iterable.get_nanbox_f64()) + } else { + js_weakset_init_iterable(this.get_nanbox_f64(), iterable.get_nanbox_f64()) + } +} diff --git a/crates/perry-transform/src/async_to_generator.rs b/crates/perry-transform/src/async_to_generator.rs index 21223e8f3f..edf43559ab 100644 --- a/crates/perry-transform/src/async_to_generator.rs +++ b/crates/perry-transform/src/async_to_generator.rs @@ -1951,6 +1951,7 @@ mod computed_and_field_async_tests { function: empty_fn(2, vec![Stmt::Expr(async_closure_with_await(70))]), is_static: false, kind: ClassComputedMemberKind::Method, + source_order: 0, }); module.classes.push(class); diff --git a/crates/perry/tests/issue_5579_indirect_eval_global_completion.rs b/crates/perry/tests/issue_5579_indirect_eval_global_completion.rs index 858fae3072..20ff021610 100644 --- a/crates/perry/tests/issue_5579_indirect_eval_global_completion.rs +++ b/crates/perry/tests/issue_5579_indirect_eval_global_completion.rs @@ -103,6 +103,27 @@ fn indirect_eval_completion_value_global_script() { ); } +/// A script-level `var` binding and its `globalThis` property are the same +/// binding. Assignments lowered after the declaration must therefore remain +/// visible to indirect eval, which resolves through the global environment. +const VAR_ASSIGNMENT_MIRRORS_GLOBAL: &str = r#" +var x = 1; +x = 7; +console.log("eval.x:", (0, eval)("x")); +console.log("DONE"); +"#; + +#[test] +fn script_var_assignment_remains_visible_to_indirect_eval() { + let (ok, out) = compile_and_run(VAR_ASSIGNMENT_MIRRORS_GLOBAL, /* global_script */ true); + assert!(ok, "binary did not exit cleanly\n{out}"); + assert!( + out.contains("eval.x: 7"), + "assignment to a script `var` must update its global property\n{out}" + ); + assert!(out.contains("DONE"), "program must complete\n{out}"); +} + /// `cptn-nrml-expr-obj.js` shape: the eval body reads a global object and the /// completion is that very object (identity preserved). const CPTN_OBJ: &str = r#" diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index b0156607e1..9b7bed7706 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -22,13 +22,17 @@ # # Regenerate: python3 scripts/addr_class_inventory.py --write-baseline +handle-floor | crates/perry-ext-events/src/lib.rs | 3 +handle-floor | crates/perry-ext-exponential-backoff/src/lib.rs | 1 +handle-floor | crates/perry-ext-fastify/src/server.rs | 1 +handle-floor | crates/perry-ext-http/src/agent.rs | 3 +handle-floor | crates/perry-ext-http/src/lib.rs | 2 handle-floor | crates/perry-runtime/src/array/alloc.rs | 2 handle-floor | crates/perry-runtime/src/array/concat_reverse.rs | 1 handle-floor | crates/perry-runtime/src/array/flat_clone.rs | 4 handle-floor | crates/perry-runtime/src/array/generic.rs | 4 handle-floor | crates/perry-runtime/src/array/header.rs | 3 handle-floor | crates/perry-runtime/src/array/indexing.rs | 4 -handle-floor | crates/perry-runtime/src/array/iter_methods.rs | 2 handle-floor | crates/perry-runtime/src/array/iter_object.rs | 1 handle-floor | crates/perry-runtime/src/array/iterator.rs | 2 handle-floor | crates/perry-runtime/src/array/push_pop.rs | 1 @@ -105,7 +109,7 @@ handle-floor | crates/perry-runtime/src/object/buffer_dispatch.rs | 2 handle-floor | crates/perry-runtime/src/object/dataview_proto_thunks.rs | 1 handle-floor | crates/perry-runtime/src/object/delete_rest.rs | 1 handle-floor | crates/perry-runtime/src/object/descriptor_state.rs | 1 -handle-floor | crates/perry-runtime/src/object/descriptors.rs | 2 +handle-floor | crates/perry-runtime/src/object/descriptors.rs | 1 handle-floor | crates/perry-runtime/src/object/field_get_set/accessors.rs | 2 handle-floor | crates/perry-runtime/src/object/field_get_set/enumeration.rs | 4 handle-floor | crates/perry-runtime/src/object/field_get_set/field_ops.rs | 3 @@ -126,7 +130,6 @@ handle-floor | crates/perry-runtime/src/object/native_call_method/collection_met handle-floor | crates/perry-runtime/src/object/native_call_method/common_methods.rs | 1 handle-floor | crates/perry-runtime/src/object/native_call_method/handle_methods.rs | 2 handle-floor | crates/perry-runtime/src/object/native_call_method/primitive_methods.rs | 1 -handle-floor | crates/perry-runtime/src/object/native_module.rs | 1 handle-floor | crates/perry-runtime/src/object/native_module/namespace_builders.rs | 1 handle-floor | crates/perry-runtime/src/object/native_module/web_locks.rs | 1 handle-floor | crates/perry-runtime/src/object/object_literal_ops.rs | 1 @@ -142,7 +145,7 @@ handle-floor | crates/perry-runtime/src/object/polymorphic_index.rs | 2 handle-floor | crates/perry-runtime/src/object/property_key.rs | 1 handle-floor | crates/perry-runtime/src/object/prototype_chain.rs | 2 handle-floor | crates/perry-runtime/src/object/prototype_helpers.rs | 1 -handle-floor | crates/perry-runtime/src/object/reflect_support.rs | 3 +handle-floor | crates/perry-runtime/src/object/reflect_support.rs | 2 handle-floor | crates/perry-runtime/src/object/to_string_tag.rs | 10 handle-floor | crates/perry-runtime/src/object/typed_array_define.rs | 1 handle-floor | crates/perry-runtime/src/object/typed_array_proto_thunks.rs | 1 @@ -191,9 +194,6 @@ handle-floor | crates/perry-runtime/src/value/equality.rs | 2 handle-floor | crates/perry-runtime/src/value/nanbox.rs | 1 handle-floor | crates/perry-runtime/src/value/to_string.rs | 3 handle-floor | crates/perry-runtime/src/weakref.rs | 2 -handle-floor | crates/perry-stdlib/src/axios.rs | 1 -handle-floor | crates/perry-stdlib/src/container/mod.rs | 1 -handle-floor | crates/perry-stdlib/src/container/types.rs | 1 handle-floor | crates/perry-stdlib/src/crypto/kdf.rs | 4 handle-floor | crates/perry-stdlib/src/crypto/keys.rs | 1 handle-floor | crates/perry-stdlib/src/crypto/random.rs | 1 @@ -203,7 +203,6 @@ handle-floor | crates/perry-stdlib/src/domain.rs | 1 handle-floor | crates/perry-stdlib/src/events.rs | 1 handle-floor | crates/perry-stdlib/src/exponential_backoff.rs | 1 handle-floor | crates/perry-stdlib/src/fetch/dispatch.rs | 4 -handle-floor | crates/perry-stdlib/src/fetch/mod.rs | 1 handle-floor | crates/perry-stdlib/src/jsonwebtoken.rs | 1 handle-floor | crates/perry-stdlib/src/querystring.rs | 5 handle-floor | crates/perry-stdlib/src/readline/mod.rs | 1 @@ -213,13 +212,11 @@ handle-floor | crates/perry-stdlib/src/streams/byob.rs | 1 handle-floor | crates/perry-stdlib/src/streams/subclass.rs | 1 handle-floor | crates/perry-stdlib/src/streams/transform.rs | 1 handle-floor | crates/perry-stdlib/src/string_decoder.rs | 5 -handle-floor | crates/perry-stdlib/src/tls.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/aes.rs | 4 handle-floor | crates/perry-stdlib/src/webcrypto/hmac.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/jwk.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/supports.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/util.rs | 5 -handle-floor | crates/perry-stdlib/src/worker_threads.rs | 1 handle-floor | crates/perry-stdlib/src/zlib.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/array/subclass.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/buffer/access.rs | 1 @@ -229,9 +226,9 @@ lone-valid-obj-ptr | crates/perry-runtime/src/error.rs | 3 lone-valid-obj-ptr | crates/perry-runtime/src/intl.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/intl/ctor_guard.rs | 2 lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/class_meta.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/construct.rs | 6 +lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/construct.rs | 5 lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/prototype_objects.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/object/descriptors.rs | 2 +lone-valid-obj-ptr | crates/perry-runtime/src/object/descriptors.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/accessors.rs | 2 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/enumeration.rs | 3 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/field_ops.rs | 1 @@ -271,19 +268,3 @@ lone-valid-obj-ptr | crates/perry-runtime/src/util_mime.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/util_style_text.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/value/dyn_index.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/value/to_string.rs | 1 - -# #7272: `crates/perry-ext-*` entered this gate's scope. These ten sites are -# not new code — they were invisible because the scan roots stopped at -# perry-runtime/perry-stdlib while the sibling gate -# (gc_store_site_inventory.py) had globbed the ext crates all along. -# -# Five of them are the HTTP server's, which #6826 moved out of -# crates/perry-stdlib/src/http.rs. This file used to carry an entry for that -# path, and once the file vanished the gate reported "baseline says 11, found -# 0 -- lower it to 0": an invitation to ratify a coverage loss as a fix. They -# are baselined here at their real counts instead. -handle-floor | crates/perry-ext-events/src/lib.rs | 3 -handle-floor | crates/perry-ext-exponential-backoff/src/lib.rs | 1 -handle-floor | crates/perry-ext-fastify/src/server.rs | 1 -handle-floor | crates/perry-ext-http/src/agent.rs | 3 -handle-floor | crates/perry-ext-http/src/lib.rs | 2 diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 446e3d9bdc..66c59229df 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -463,6 +463,21 @@ ], "_FRONTIER_README": "Identity-pinned ratchet over the perry-ui* crates and otherwise-unclassified core perry_thread_local! declarations (see the census docstring, 'The identity-pinned frontier'). Entries are debt baselines, not verdicts. A new uncovered holder fails until deliberately pinned, and a fixed holder fails until its stale entry is deleted. An optional scanner names cross-file coverage and must remain registered; deleting that registration invalidates the pin and fails the gate. Covered same-file holders must NOT be pinned.", "frontier": [ + { + "file": "crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs", + "name": "PRIVATE_METHOD_OWNER_HINT", + "why": "Only a compile-time class id and Rust-owned method-name String; no JS value or Perry heap address." + }, + { + "file": "crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs", + "name": "PRIVATE_MEMBER_ACCESS_HINTS", + "why": "Only compile-time class ids, Rust-owned Strings, and scalar flags; no JS value or Perry heap address." + }, + { + "file": "crates/perry-runtime/src/object/this_binding.rs", + "name": "DERIVED_SUPER_BINDING_STACK", + "why": "Pointers to one-byte LLVM alloca flags in active native constructor frames, not Perry heap objects. Native frames do not move; balanced push/pop and exception savepoint restoration prevent entries outliving their frames." + }, { "file": "crates/perry-runtime/src/array/element_shape.rs", "name": "ELEMENT_SHAPES" diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index ced7b1ba5b..f55dabd5d7 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -925 +922 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index 40becbb8ca..f9ec92b22a 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -89,7 +89,7 @@ 4 crates/perry-runtime/src/node_submodules/test.rs 33 crates/perry-runtime/src/object/alloc.rs 2 crates/perry-runtime/src/object/bigint_dispatch.rs -5 crates/perry-runtime/src/object/class_registry/construct.rs +3 crates/perry-runtime/src/object/class_registry/construct.rs 2 crates/perry-runtime/src/object/delete_rest.rs 12 crates/perry-runtime/src/object/descriptors.rs 5 crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -109,7 +109,7 @@ 26 crates/perry-runtime/src/object/native_module/callable_exports.rs 6 crates/perry-runtime/src/object/object_literal_ops.rs 2 crates/perry-runtime/src/object/object_ops/define_property.rs -3 crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs +2 crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs 4 crates/perry-runtime/src/object/object_ops/from_entries.rs 5 crates/perry-runtime/src/object/object_ops/keys_array.rs 6 crates/perry-runtime/src/object/polymorphic_index.rs diff --git a/test-files/test_issue_5893_private_brand_freshness.ts b/test-files/test_issue_5893_private_brand_freshness.ts index 3437d9b303..3cdcf91c89 100644 --- a/test-files/test_issue_5893_private_brand_freshness.ts +++ b/test-files/test_issue_5893_private_brand_freshness.ts @@ -212,3 +212,129 @@ function checkFreshStaticBrands(label: string, make: () => any): void { checkFreshStaticBrands("static expression", makeStaticClass); checkFreshStaticBrands("static declaration", makeStaticDeclarationClass); + +function makeOrderedStatics(label: string): any { + const events: string[] = []; + const key = (name: string): string => { + events.push("key-" + name); + return name; + }; + const C = class { + static #brand = 0; + + [key("method")](): void {} + + static [key("first")] = (events.push("init-first"), 1); + + static { + events.push("block"); + (this as any).fromBlock = label; + } + + static tail = (events.push("init-tail"), 2); + static missing; + }; + + check( + label + " computed/static order", + events.join(",") === + "key-method,key-first,init-first,block,init-tail" + ); + check(label + " static block this", C.fromBlock === label); + check( + label + " uninitialized static own", + Object.prototype.hasOwnProperty.call(C, "missing") && + C.missing === undefined + ); + return C; +} + +makeOrderedStatics("fresh order"); + +function makeDynamicAccessor(tag: string): any { + return class { + static #brand = 0; + + static install(): void { + Object.defineProperty(this, "dynamic", { + configurable: true, + enumerable: true, + get: () => tag, + set: (value: string) => { + tag = value; + }, + }); + } + + static readTag(): string { + return tag; + } + }; +} + +const accessorA = makeDynamicAccessor("a"); +const accessorB = makeDynamicAccessor("b"); +accessorA.install(); +accessorB.install(); +check("dynamic accessor evaluation A", accessorA.dynamic === "a"); +check("dynamic accessor evaluation B", accessorB.dynamic === "b"); +Object.defineProperty(accessorA, "dynamic", { + get: () => "fixed", +}); +const accessorDescriptor = Object.getOwnPropertyDescriptor( + accessorA, + "dynamic" +)!; +check( + "dynamic accessor retained halves", + typeof accessorDescriptor.set === "function" +); +check( + "dynamic accessor retained attrs", + accessorDescriptor.enumerable && accessorDescriptor.configurable +); +accessorA.dynamic = "changed"; +check("dynamic accessor retained setter", accessorA.readTag() === "changed"); +check("dynamic accessor sibling isolated", accessorB.dynamic === "b"); + +function makePrototypeParent(tag: string): any { + return class { + static #brand = 0; + + inherited(): string { + return tag; + } + }; +} + +function makePrototypeChild(parent: any): any { + return class extends parent { + static #brand = 0; + }; +} + +const prototypeParent = makePrototypeParent("parent"); +const prototypeChild = makePrototypeChild(prototypeParent); +check( + "fresh prototype parent link", + Object.getPrototypeOf(prototypeChild.prototype) === prototypeParent.prototype +); +check( + "fresh prototype inherited method", + new prototypeChild().inherited() === "parent" +); + +class HugeKeyBase {} +Object.defineProperty(HugeKeyBase.prototype, "9223372036854776000", { + value: "huge", +}); +class HugeKeyDerived extends HugeKeyBase { + read(): string { + return super[9223372036854775808]; + } +} +check("super huge numeric property key", new HugeKeyDerived().read() === "huge"); + +const boxedString = new String("payload"); +check("boxed String toString payload", boxedString.toString() === "payload"); +check("boxed String valueOf payload", boxedString.valueOf() === "payload");