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(), + }); } } }