Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions crates/perry-codegen/src/stmt/let_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 9 additions & 12 deletions crates/perry-hir/src/lower/expr_member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<prop>` semantics via the same PropertyGet.
return Ok(Expr::PropertyGet {
object: Box::new(Expr::NewTarget),
property: prop_name.to_string(),
Expand Down
29 changes: 13 additions & 16 deletions crates/perry-hir/src/lower/expr_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <class_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: <enclosing-class> }`, 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)
}
}
}
Expand Down
18 changes: 11 additions & 7 deletions crates/perry-hir/src/lower/lower_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?.<prop>` 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(),
});
}
}
}
Expand Down