From 61cb2621597c80f2c35467cc649dd31e60f1e7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 15 Jun 2026 09:51:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(codegen):=20Next.js=20wall=2046=20=E2=80=94?= =?UTF-8?q?=20derived=20field-init=20clobbered=20inherited=20captures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit class X extends _mod.default read the parent's relative-require captures (this.__perry_cap_*) as undefined from inherited methods. The captures were correct after super() but clobbered during the DERIVED class's post-super field-init phase: __perry_cap_* fields have init:None, so apply_field_initializers_recursive emitted this.field=undefined for them, re-initializing the inherited caps super() had just filled. FIX: skip __perry_cap_* in the field-initializer loop — they are populated exclusively by the ctor capture-param assignments, so a field-init undefined-write is always wrong. --- crates/perry-codegen/src/lower_call/new.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 361225272..a0c74adb5 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -1860,6 +1860,22 @@ pub(crate) fn apply_field_initializers_recursive( let mut init_pairs: Vec<(String, Expr)> = Vec::new(); let mut init_pairs_computed: Vec<(Expr, Expr)> = Vec::new(); for field in &class_fields { + // Wall 46: synthesized capture fields (`__perry_cap_*`) are populated + // EXCLUSIVELY by the constructor's capture-param assignments — for a + // class constructed directly, by its own ctor; for a subclass of an + // (inherited) dynamic parent, by super()'s parent-ctor run. They carry + // `init: None`, so the default `Expr::Undefined` write below would + // re-initialize them to `undefined` during the derived field-init + // phase (which runs AFTER super()), CLOBBERING the real captured value + // super already stored. That is the Next.js `NextNodeServer extends + // _baseserver.default` failure: base-server's `_iserror`/`_utils`/ + // `_log` read `undefined` in inherited methods. Field-init must never + // touch these — skip them so the ctor param assignment is the sole + // writer (verified: captures are correct at the parent ctor end and + // only vanish during the derived ctor's post-super field-init). + if field.key_expr.is_none() && field.name.starts_with("__perry_cap_") { + continue; + } let init = match &field.init { Some(e) => e.clone(), None => Expr::Undefined,