From 371d5715ead4cd13aca2dca0b7838b6e5a3268c1 Mon Sep 17 00:00:00 2001 From: Ralph Date: Thu, 18 Jun 2026 01:02:29 -0700 Subject: [PATCH] fix(codegen): apply ctor return-override on standalone-symbol new path (#5345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `new Derived()` for a class with its own constructor takes the shared `_constructor` symbol-call path (the default since the [#bloat] change; `force_ctor_call`). That path called the standalone constructor but discarded its return value and yielded the freshly-allocated `this`, so ECMAScript constructor return-override semantics were never applied: - a derived ctor returning a non-object primitive did NOT throw the required TypeError (`new Derived(){ super(); return 0 }` silently returned the instance), and - a ctor returning an object did NOT override `this`. The standalone symbol already returns the body's explicit `return ` (or NaN-boxed `undefined` on fall-through), so the fix is to apply `js_ctor_return_override(this, ret, is_derived)` at the construction site — exactly as the inline `new` path already does — and return its result. `is_derived` mirrors the inline path's heritage check. test262 language/statements/class/subclass: +10 cases (parity 31.2% → 40.4%), 0 regressions — the full `derived-class-return-override-with-*` cluster (boolean/null/number/string/symbol/object), the `-catch`/ `-catch-super`/`-catch-super-arrow` uncatchable-TypeError variants, and `class-definition-null-proto-contains-return-override`. The language/statements/class/definition dir is unchanged (no regressions). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-codegen/src/lower_call/new.rs | 36 +++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 7614462e3..a11ba25d4 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -172,19 +172,28 @@ fn local_constructor_symbol_exists(ctx: &FnCtx<'_>, class: &perry_hir::Class) -> .contains_key(&(class.name.clone(), ctor_method_name)) } +/// Construct via the shared standalone `_constructor` symbol and apply +/// ECMAScript constructor return-override semantics to its result. Returns the +/// construction value: the freshly-allocated `this` (`obj_box`) for an implicit +/// or `undefined` return, an explicitly-returned object, or — for a derived +/// constructor returning a primitive — a `js_ctor_return_override` call that +/// throws a TypeError at runtime. The standalone symbol returns the ctor body's +/// explicit `return ` (or NaN-boxed `undefined` on fall-through), so the +/// override must be applied HERE at the construction site rather than discarding +/// the symbol's result. Refs class/subclass/derived-class-return-override-*. fn call_local_constructor_symbol( ctx: &mut FnCtx<'_>, class: &perry_hir::Class, obj_box: &str, lowered_args: &[String], -) { +) -> String { let ctor_method_name = format!("{}_constructor", class.name); let Some(ctor_name) = ctx .methods .get(&(class.name.clone(), ctor_method_name)) .cloned() else { - return; + return obj_box.to_string(); }; // The standalone `_constructor` symbol's signature is the class's // OWN ctor params, OR — when the class has no own ctor — the closest @@ -243,7 +252,24 @@ fn call_local_constructor_symbol( for arg in &ctor_values { ctor_args.push((DOUBLE, arg.as_str())); } - let _ = ctx.block().call(DOUBLE, &ctor_name, &ctor_args); + let ret_val = ctx.block().call(DOUBLE, &ctor_name, &ctor_args); + // Apply the spec return-override on the symbol's result. A class is + // "derived" (subject to the stricter rules — a returned primitive is a + // TypeError) if it has ANY heritage, matching the inline path's check. + let is_derived = class.extends.is_some() + || class.extends_name.is_some() + || class.native_extends.is_some() + || class.extends_expr.is_some(); + let is_derived_str = if is_derived { "1" } else { "0" }; + ctx.block().call( + DOUBLE, + "js_ctor_return_override", + &[ + (DOUBLE, obj_box), + (DOUBLE, &ret_val), + (I32, is_derived_str), + ], + ) } /// Lower `new ClassName(args…)` — Phase C.1. @@ -871,8 +897,8 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> || ctor_alias_collision || force_ctor_call { - call_local_constructor_symbol(ctx, class, &obj_box, &lowered_args); - return Ok(obj_box); + let result = call_local_constructor_symbol(ctx, class, &obj_box, &lowered_args); + return Ok(result); } // Allocate a `this` slot and store the new object there. The