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; }