From 7438577b828144e4ea0a1c3286246b723a57aa42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 13 Jun 2026 08:13:39 +0200 Subject: [PATCH] fix(class): new.target reflects the actual constructed class, not the enclosing one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HIR hardcoded `new.target` inside a class constructor to a literal `{ name: }` (expr_misc.rs / expr_member.rs / lower_expr.rs). So `new.target` was always the class whose BODY runs — for `new Derived()` that inlines `Base`'s constructor via super(), it was `Base`, not `Derived` — and `new.target === SomeClass` was always false (a fresh object never equals the class ref), breaking the abstract-base-class guard idiom `if (new.target === Abstract) throw`. Lower `new.target` (and `new.target.name` / `.prototype` / `?.` member reads) to the real `Expr::NewTarget`. Codegen resolves it to the active constructor's LEAF class ref (`INT32_TAG | class_id`, the same value `Expr::ClassRef` produces) via a `new_target_stack` slot pushed around the inlined constructor body — in both the ordinary inline-`new` path (lower_call/new.rs) and the scalar-replacement path for a non-escaping `const c = new C()` (stmt/let_stmt.rs). Using the codegen slot rather than the runtime cell keeps a non-constructor method called from the ctor body — compiled separately, empty new_target_stack — correctly reading `undefined`. Now matches Node: `new Derived()` new.target is the leaf class across no-own-ctor / explicit-super / field-init-bearing / multi-level chains; `new.target === C`, `.name`, `.prototype` all correct; abstract guards fire; nested and scalar-replaced construction restore the outer target. Surfaced while verifying #2768 (whose functional acceptance criteria — newTarget honored, array-like args, constructor/proxy validation — all pass on current main). --- crates/perry-codegen/src/lower_call/new.rs | 24 ++++++++++++++++++ crates/perry-codegen/src/stmt/let_stmt.rs | 21 ++++++++++++++++ crates/perry-hir/src/lower/expr_member.rs | 21 +++++++--------- crates/perry-hir/src/lower/expr_misc.rs | 29 ++++++++++------------ crates/perry-hir/src/lower/lower_expr.rs | 18 ++++++++------ 5 files changed, 78 insertions(+), 35 deletions(-) diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 02e391cccb..2a721cd5d3 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -883,6 +883,29 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> ctx.this_stack.push(this_slot); ctx.class_stack.push(class_name.to_string()); + // #2768/new.target: `new C()` is fully inlined here, so the runtime + // `js_new_target_*` cell is never set on this path. Bind `new.target` + // inside the (own or inherited-via-super) constructor body to THIS leaf + // class's ref via a `new_target_stack` slot. Using the codegen slot + // rather than the runtime cell keeps a non-constructor method called from + // the ctor body — compiled as a separate function whose `new_target_stack` + // is empty — correctly reading `undefined`. A class ref is + // `INT32_TAG | class_id`, the same value `Expr::ClassRef` produces, so + // `new.target === C`, `new.target.name`, and `new.target.prototype` all + // work. Falls back to `undefined` if the class id is somehow unresolved. + let new_target_bits = ctx + .class_ids + .get(class_name) + .map(|&cid| crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF)) + .unwrap_or(crate::nanbox::TAG_UNDEFINED); + let new_target_slot = ctx.func.alloca_entry(DOUBLE); + ctx.block().store( + DOUBLE, + &double_literal(f64::from_bits(new_target_bits)), + &new_target_slot, + ); + ctx.new_target_stack.push(new_target_slot); + // Set up the inline-constructor return target. An explicit `return` // inside the (about-to-be-inlined) ctor body must apply spec // return-override semantics and yield the `new` expression's value — @@ -1457,6 +1480,7 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> obj_box }; + ctx.new_target_stack.pop(); ctx.this_stack.pop(); ctx.class_stack.pop(); Ok(final_box) diff --git a/crates/perry-codegen/src/stmt/let_stmt.rs b/crates/perry-codegen/src/stmt/let_stmt.rs index 419dedf540..63dbeea686 100644 --- a/crates/perry-codegen/src/stmt/let_stmt.rs +++ b/crates/perry-codegen/src/stmt/let_stmt.rs @@ -571,6 +571,26 @@ pub(crate) fn lower_let( let dummy_this = ctx.func.alloca_entry(DOUBLE); ctx.this_stack.push(dummy_this); + // #2768/new.target: scalar replacement inlines the (own or + // inherited) constructor here without going through + // `lower_new`, so mirror its `new_target_stack` setup — bind + // `new.target` in the inlined body to this leaf class's ref + // (`INT32_TAG | class_id`). Without this a `new.target` read in + // the ctor (notably `const t = new.target`) fell through to the + // runtime cell, which this path never sets, yielding undefined. + let new_target_bits = ctx + .class_ids + .get(class_name) + .map(|&cid| crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF)) + .unwrap_or(crate::nanbox::TAG_UNDEFINED); + let new_target_slot = ctx.func.alloca_entry(DOUBLE); + ctx.block().store( + DOUBLE, + &crate::nanbox::double_literal(f64::from_bits(new_target_bits)), + &new_target_slot, + ); + ctx.new_target_stack.push(new_target_slot); + // Stage field initializers around any parent body chain. // Refs #420: leaf field inits may reference state set by // parent body (e.g. drizzle's @@ -690,6 +710,7 @@ pub(crate) fn lower_let( )?; } + ctx.new_target_stack.pop(); ctx.this_stack.pop(); ctx.class_stack.pop(); ctx.scalar_ctor_target.pop(); diff --git a/crates/perry-hir/src/lower/expr_member.rs b/crates/perry-hir/src/lower/expr_member.rs index 30ca2f5db1..7d09bb8880 100644 --- a/crates/perry-hir/src/lower/expr_member.rs +++ b/crates/perry-hir/src/lower/expr_member.rs @@ -210,18 +210,15 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re if matches!(mp.kind, ast::MetaPropKind::NewTarget) { if let ast::MemberProp::Ident(prop_ident) = &member.prop { let prop_name = prop_ident.sym.as_ref(); - if let Some(class_name) = ctx.in_constructor_class.clone() { - return Ok(match prop_name { - "name" => Expr::String(class_name), - // Other props on a class reference (`prototype`, - // arbitrary) — undefined is the safe fallback; - // adding `prototype` would need a real class - // reference, not in scope for #449. - _ => Expr::Undefined, - }); - } - // Outside a constructor: `new.target` is undefined and - // ordinary functions resolve it dynamically at runtime. + // #2768: read the property off the RUNTIME `new.target`, which + // codegen resolves to the active constructor's leaf class ref + // (`INT32_TAG | class_id`). `.name` / `.prototype` / + // `=== SomeClass` then all reflect the actual constructed + // class. The old fold returned the *enclosing* class name + // string (wrong leaf for `super()`-inlined bodies) and made + // `new.target.prototype` undefined. Outside a constructor + // `new.target` is `undefined`, so the runtime read yields + // `undefined.` semantics via the same PropertyGet. return Ok(Expr::PropertyGet { object: Box::new(Expr::NewTarget), property: prop_name.to_string(), diff --git a/crates/perry-hir/src/lower/expr_misc.rs b/crates/perry-hir/src/lower/expr_misc.rs index 0f9cf16dc7..697c55d9c7 100644 --- a/crates/perry-hir/src/lower/expr_misc.rs +++ b/crates/perry-hir/src/lower/expr_misc.rs @@ -296,22 +296,19 @@ pub(super) fn lower_meta_prop( ])) } ast::MetaPropKind::NewTarget => { - // Inside a class constructor, `new.target` evaluates to the - // class itself. We approximate this with a small object - // literal `{ name: }` so: - // - `new.target ? a : b` is truthy → takes the `a` branch - // - `new.target.name` returns the class name string - // Outside a class constructor, ordinary function bodies read it - // dynamically from the constructor-call slot. Arrow closures can - // capture that value lexically during closure creation. - if let Some(class_name) = ctx.in_constructor_class.clone() { - Ok(Expr::Object(vec![( - "name".to_string(), - Expr::String(class_name), - )])) - } else { - Ok(Expr::NewTarget) - } + // `new.target` always lowers to the runtime meta-property read + // (#2768). Codegen resolves it to the active constructor's leaf + // class ref: for an inlined `new C()` via a `new_target_stack` + // slot holding `C`'s class ref, and for dynamic dispatch + // (`Reflect.construct`, imported classes) via the `js_new_target_*` + // cell the construct path sets. The previous in-constructor + // approximation hardcoded `{ name: }`, which made + // `new.target` the class whose BODY runs (a base class via super()) + // rather than the actual constructed class, and broke + // `new.target === C` identity (a fresh object never equals the + // class ref). `ctx.in_constructor_class` is no longer consulted + // here. + Ok(Expr::NewTarget) } } } diff --git a/crates/perry-hir/src/lower/lower_expr.rs b/crates/perry-hir/src/lower/lower_expr.rs index f85a039d89..a35cc17ba0 100644 --- a/crates/perry-hir/src/lower/lower_expr.rs +++ b/crates/perry-hir/src/lower/lower_expr.rs @@ -1661,13 +1661,17 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result< if matches!(mp.kind, ast::MetaPropKind::NewTarget) { if let ast::MemberProp::Ident(prop_ident) = &member.prop { let prop_name = prop_ident.sym.as_ref(); - if let Some(class_name) = ctx.in_constructor_class.clone() { - return Ok(match prop_name { - "name" => Expr::String(class_name), - _ => Expr::Undefined, - }); - } - return Ok(Expr::Undefined); + // #2768: `new.target?.` reads off the + // runtime new.target (a leaf class ref inside a + // constructor, `undefined` outside). Inside a + // ctor it's non-null so `?.` resolves the + // property; outside it yields undefined. The old + // fold hardcoded the enclosing class name (wrong + // leaf) and undefined for `.prototype`. + return Ok(Expr::PropertyGet { + object: Box::new(Expr::NewTarget), + property: prop_name.to_string(), + }); } } }