From fcde2a2703412fb4d43cc141313533724476256d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 5 Jul 2026 11:43:37 +0000 Subject: [PATCH] fix(codegen,runtime): super(...spread) dropped spread args and skipped custom iterators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `super(...args)` with spread arguments in a derived-class constructor lost the spread contents at two independent points, so a parent constructor invoked via the dynamic `super(...spread)` apply path saw zero (or truncated) arguments: 1. Runtime arg forwarding (`js_super_construct_apply`) truncated the materialized spread-args array to the parent ctor's DECLARED param count and always passed `has_synthetic_arguments = has_rest = false` to `call_vtable_method`. A pass-through parent ctor (`constructor() { ... arguments ... }`, tsc's emitted pattern, or an implicit derived `constructor(...args){ super(...args) }`) declares zero params but reads `arguments`, so every spread element was dropped and `arguments.length` read 0. Fix: register each ctor's `(has_synthetic_arguments, has_rest)` shape (new `js_register_class_constructor_flags` FFI + `CLASS_CONSTRUCTOR_FLAGS` side table, emitted by codegen next to the existing ctor registration) and, when the parent has a synthesized `arguments` slot or a trailing user rest param, forward ALL spread args flat and let `call_vtable_method` pack the trailing slot (from index 0 for `arguments`, from the rest position for a rest param) — mirroring the non-spread inline super path and `replay_registered_class_constructor`. Ctors with capture params keep the original fixed-arity path (caps must not be swallowed into `arguments`). 2. Codegen (`this_super_call.rs`) built the spread-args array with `js_array_push_spread_any`, which only handles real arrays and array-like (`.length`) objects. A plain iterable with a custom `[Symbol.iterator]` (`super(...iter)`) has no `.length`, so it contributed zero args, and a throwing iterator step/value never propagated its error. Fix: route spread operands through `js_array_spread_append` -> `array_from_spread_value`, the same full iterator-protocol helper array/call spreads already use — it drives custom iterators, spreads the arguments object, and propagates abrupt completions. This is a strict superset of the old helper's coverage. Test262 language/expressions/super: +19 cases (32 -> 46 pass) — the entire `call-spread-*` cluster (array-literal, expr, custom-iterator, and all 12 `call-spread-err-*` iterator-error-propagation cases) plus 4 object/method-definition generator/name super-prop cases that shared the spread-append path. Zero regressions across the super / object / property-accessors / delete / for-of / function slice (1032 -> 1051 pass) and no super/spread regressions in the language/statements/class corpus. --- .../perry-codegen/src/codegen/string_pool.rs | 34 +++++++ .../perry-codegen/src/expr/this_super_call.rs | 16 ++- .../runtime_decls/stdlib_ffi/language_core.rs | 8 ++ .../src/object/class_constructors.rs | 99 +++++++++++++++++-- 4 files changed, 143 insertions(+), 14 deletions(-) diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs index aecbe3895c..7b1cc36bc5 100644 --- a/crates/perry-codegen/src/codegen/string_pool.rs +++ b/crates/perry-codegen/src/codegen/string_pool.rs @@ -538,6 +538,11 @@ pub(super) fn emit_string_pool( // crash (Next.js `new c.AppPageRouteModule({...})`). Mirrors the // closure-rest registration but keyed by the `_constructor` symbol. let mut ctor_rest_regs: Vec<(String, usize)> = Vec::new(); + // Per-class-id ctor synth/rest flags (has_synthetic_arguments, has_rest) so + // the `super(...spread)` runtime apply path packs a pass-through parent + // ctor's `arguments` / rest slot correctly (a zero-declared-param parent + // that reads `arguments`, e.g. tsc's emitted pass-through ctor). + let mut ctor_flag_regs: Vec<(u32, bool, bool)> = Vec::new(); for (class_name, class) in classes.iter() { // Refs #486: skip alias keys (class_table now contains both the // canonical name and self-binding aliases like `_X` from @@ -662,6 +667,20 @@ pub(super) fn emit_string_pool( { ctor_rest_regs.push((ctor_symbol.clone(), rest_idx)); } + // Record the ctor's trailing-param shape so the `super(...spread)` + // apply path forwards the flat spread args and packs the trailing slot: + // a synthesized `arguments` slot receives ALL args (from index 0), a + // user rest param only the args from the rest position onward. + { + let last = class.constructor.as_ref().and_then(|c| c.params.last()); + let ctor_has_synth = last.map(|p| p.arguments_object.is_some()).unwrap_or(false); + let ctor_has_rest = last + .map(|p| p.is_rest && p.arguments_object.is_none()) + .unwrap_or(false); + if ctor_has_synth || ctor_has_rest { + ctor_flag_regs.push((cid, ctor_has_synth, ctor_has_rest)); + } + } ctor_triples.push((cid, ctor_symbol, ctor_params)); } method_triples.sort_unstable(); @@ -785,6 +804,21 @@ pub(super) fn emit_string_pool( &[(PTR, &func_ref), (I32, &rest_idx.to_string())], ); } + // Register ctor synth/rest flags so `super(...spread)` packs the parent + // ctor's trailing `arguments` / rest slot correctly. + ctor_flag_regs.sort_unstable(); + for (cid, has_synth, has_rest) in ctor_flag_regs { + chunker.roll_if_full(); + let blk = chunker.current_block(); + blk.call_void( + "js_register_class_constructor_flags", + &[ + (I64, &cid.to_string()), + (I64, if has_synth { "1" } else { "0" }), + (I64, if has_rest { "1" } else { "0" }), + ], + ); + } // Refs #618 / #420: register every class id with the runtime so // `js_value_typeof` can distinguish a class ref (NaN-boxed INT32 with diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 52e9efc3d6..ba46f6f212 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -151,13 +151,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ); } perry_hir::CallArg::Spread(e) => { - // `js_array_push_spread_any` also handles the - // arguments OBJECT (array-like, not ArrayHeader) — - // the `super(...arguments)` source. + // Route every spread operand through the full iterator + // protocol (`js_array_spread_append` -> `array_from_ + // spread_value`): it drives a custom `[Symbol.iterator]` + // (`super(...iter)`), spreads the arguments OBJECT + // (`super(...arguments)`), arrays, sets/maps, typed + // arrays, and strings, AND propagates an abrupt + // completion from a throwing iterator step/value — the + // `call-spread-*-iter` / `call-spread-err-*` cases. The + // old `js_array_push_spread_any` only handled arrays and + // array-like (`.length`) objects, so a plain iterable + // (no `.length`) contributed zero args. let v = lower_expr(ctx, e)?; arr = ctx.block().call( I64, - "js_array_push_spread_any", + "js_array_spread_append", &[(I64, &arr), (DOUBLE, &v)], ); } diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs index 6c431a8dea..4bdde80f4f 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs @@ -342,6 +342,14 @@ pub(crate) fn declare_core(module: &mut LlModule) { // #1787: register a class's standalone constructor so `new // ()` can replay it on a dynamically-allocated instance. module.declare_function("js_register_class_constructor", VOID, &[I64, I64, I64]); + // Constructor synth/rest flags: (class_id, has_synthetic_arguments, + // has_rest) — consulted by the `super(...spread)` apply path so it packs a + // pass-through parent ctor's `arguments` / rest slot correctly. + module.declare_function( + "js_register_class_constructor_flags", + VOID, + &[I64, I64, I64], + ); // #1788: register a class STATIC method + dispatch an inherited static // method on a class value (subclass extends a class-expression value). module.declare_function( diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index a8a3b6fbb9..9e63df2c34 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -65,6 +65,53 @@ fn lookup_class_constructor(class_id: u32) -> Option<(usize, u32)> { .copied() } +/// Per-class-id flags for a registered standalone constructor: whether its +/// trailing param is the HIR-synthesized `arguments` slot and/or a user rest +/// param (`constructor(a, ...rest)`). The `arguments` slot must receive ALL +/// call args (packed from index 0) whereas a user rest slot receives only the +/// args from the rest position onward — the same distinction `call_vtable_ +/// method` draws via `has_synthetic_arguments` / `has_rest`. Registered by +/// codegen (`js_register_class_constructor_flags`) alongside the ctor itself so +/// the `super(...spread)` apply path (`js_super_construct_apply`) can forward +/// the flat spread args and let `call_vtable_method` pack the trailing slot +/// correctly. Absent entry ⇒ neither flag (a plain fixed-arity ctor). +static CLASS_CONSTRUCTOR_FLAGS: RwLock>> = RwLock::new(None); + +/// Codegen FFI: record `(has_synthetic_arguments, has_rest)` for a class ctor. +/// See [`CLASS_CONSTRUCTOR_FLAGS`]. +#[no_mangle] +pub extern "C" fn js_register_class_constructor_flags( + class_id: i64, + has_synthetic_arguments: i64, + has_rest: i64, +) { + if class_id == 0 { + return; + } + let mut guard = CLASS_CONSTRUCTOR_FLAGS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert( + class_id as u32, + (has_synthetic_arguments != 0, has_rest != 0), + ); +} + +/// Keepalive anchor (generated-code-only callee). +#[used] +static KEEP_JS_REGISTER_CLASS_CONSTRUCTOR_FLAGS: extern "C" fn(i64, i64, i64) = + js_register_class_constructor_flags; + +/// Look up a class ctor's `(has_synthetic_arguments, has_rest)` flags. +fn lookup_class_constructor_flags(class_id: u32) -> (bool, bool) { + CLASS_CONSTRUCTOR_FLAGS + .read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&class_id).copied())) + .unwrap_or((false, false)) +} + thread_local! { /// Decl-site snapshots of a function-nested class DECLARATION's captured /// outer locals, keyed by class_id. Filled by the codegen-emitted @@ -350,16 +397,48 @@ pub unsafe extern "C" fn js_super_construct_apply( } else { crate::array::js_array_length(arr) } as usize; - let mut final_args: Vec = Vec::with_capacity(total_params as usize); - for i in 0..user_params { - if i < n { + // Flatten every SPREAD-expanded arg into a contiguous f64 buffer and + // let `call_vtable_method` pack the trailing param. When the parent + // ctor uses `arguments` (a zero-declared-param pass-through ctor: + // `super(...[3,4,5])` → parent reads `arguments.length`), the + // synthesized `arguments` slot must hold ALL args (packed from index + // 0); a user rest param (`constructor(a, ...rest)`) holds only the + // args from the rest position onward. The old truncation to + // `user_params` (and `has_synth=false`/`has_rest=false`) dropped the + // extra spread args and left `arguments.length == 0`. `caps` (the + // decl-site capture snapshot for a function-nested class) are + // appended AFTER the user args, mirroring the fixed-arity super path. + // `call_vtable_method`'s synth/rest packing bundles the trailing + // param from the flat arg buffer — but it packs from index 0 for + // synthesized `arguments` and would swallow any trailing capture + // params. Function-nested capturing ctors never combine caps with a + // synth/rest trailing param in practice, so only take the flat- + // forward path when there are no caps; otherwise keep the original + // fixed-arity truncation (caps appended after user args). + let (mut has_synth, mut has_rest_flag) = lookup_class_constructor_flags(cur); + if !caps.is_empty() { + has_synth = false; + has_rest_flag = false; + } + let mut final_args: Vec = Vec::with_capacity(user_params.max(n) + caps.len()); + if has_synth || has_rest_flag { + // Forward all n spread args flat; call_vtable_method packs the + // trailing synthesized-`arguments` / rest slot (from index 0 for + // `arguments`, from the rest position for a user rest param). + for i in 0..n { final_args.push(crate::array::js_array_get_f64(arr, i as u32)); - } else { - final_args.push(undef); } - } - for bits in &caps { - final_args.push(f64::from_bits(*bits)); + } else { + for i in 0..user_params { + if i < n { + final_args.push(crate::array::js_array_get_f64(arr, i as u32)); + } else { + final_args.push(undef); + } + } + for bits in &caps { + final_args.push(f64::from_bits(*bits)); + } } let _ = call_vtable_method( ctor_ptr, @@ -367,8 +446,8 @@ pub unsafe extern "C" fn js_super_construct_apply( final_args.as_ptr(), final_args.len(), total_params, - false, - false, + has_synth, + has_rest_flag, ); return undef; }