From 68358429dc0ebf70fa36c17bf86229f437c75236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 16 Jul 2026 00:31:04 +0200 Subject: [PATCH] fix(codegen): set new.target for a cross-module imported constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `new C()` where `C`'s constructor lives in another module calls the imported `_constructor` symbol, which is compiled in its source module and reads `new.target` from the runtime cell — NOT this module's codegen `new_target_stack` slot. The imported-ctor call sites never set that cell, so an ancestor constructor reading `new.target` (e.g. `this.type = new.target.type`) saw a stale/undefined value; with an unguarded read it threw `Cannot read properties of undefined`. Bind the runtime new.target cell to the leaf class ref around the imported-ctor call and restore it after, mirroring the local standalone-symbol path. --- crates/perry-codegen/src/lower_call/new.rs | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 306b7691dc..d7831f6bf8 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -1762,7 +1762,24 @@ fn lower_new_impl( // to match the symbol's real signature (see codegen/mod.rs). ctx.pending_declares .push((ctor.symbol.clone(), DOUBLE, ctor_param_types)); + // new.target cross-module: the imported ctor symbol is compiled + // in its SOURCE module and reads `new.target` from the runtime + // cell, NOT this module's codegen `new_target_stack` slot. Bind + // the cell to the LEAF class ref around the call so an ancestor + // ctor (e.g. Auth.js `AuthError`'s `this.type = new.target.type`) + // sees the class being constructed instead of a stale/undefined + // value. Without this, `new CredentialsSignin()` from another + // chunk threw `Cannot read properties of undefined (reading + // 'type')`, or silently set `type = undefined` → the auth error + // was mis-categorized and the login redirect fell back to + // `?error=Configuration`. + let nt_prev = ctx.block().call(DOUBLE, "js_new_target_get", &[]); + let nt_ref = double_literal(f64::from_bits(new_target_bits)); + ctx.block() + .call(DOUBLE, "js_new_target_set", &[(DOUBLE, &nt_ref)]); let _ = ctx.block().call(DOUBLE, &ctor.symbol, &ctor_args); + ctx.block() + .call(DOUBLE, "js_new_target_set", &[(DOUBLE, &nt_prev)]); } else if let Some(ctor) = ctx.imported_class_ctors.get(class_name).cloned() { // Pad missing optional args with TAG_UNDEFINED so the constructor // doesn't read garbage from stale registers, and pack the rest @@ -1788,7 +1805,16 @@ fn lower_new_impl( // ("value is not a function" on `new Chalk(...).red(...)`). ctx.pending_declares .push((ctor.symbol.clone(), DOUBLE, ctor_param_types)); + // new.target cross-module: bind the runtime cell to the leaf + // class ref around the imported ctor call (see the ANCESTOR arm + // above for why). This is the direct `new ImportedClass()` case. + let nt_prev = ctx.block().call(DOUBLE, "js_new_target_get", &[]); + let nt_ref = double_literal(f64::from_bits(new_target_bits)); + ctx.block() + .call(DOUBLE, "js_new_target_set", &[(DOUBLE, &nt_ref)]); let ctor_ret = ctx.block().call(DOUBLE, &ctor.symbol, &ctor_args); + ctx.block() + .call(DOUBLE, "js_new_target_set", &[(DOUBLE, &nt_prev)]); ctx.block().store(DOUBLE, &ctor_ret, &ctor_result_slot); found_inherited_ctor = true; }