diff --git a/crates/perry-codegen/src/collectors/this_as_value.rs b/crates/perry-codegen/src/collectors/this_as_value.rs index 08ff783f7..94d2870ec 100644 --- a/crates/perry-codegen/src/collectors/this_as_value.rs +++ b/crates/perry-codegen/src/collectors/this_as_value.rs @@ -219,6 +219,7 @@ pub fn expr_uses_this_as_value(e: &perry_hir::Expr, fields: &HashSet) -> } => true, Expr::SuperCall(_) | Expr::SuperMethodCall { .. } + | Expr::SuperMethodCallSpread { .. } | Expr::SuperPropertySet { .. } | Expr::ObjectSuperPropertyGet { .. } | Expr::ObjectSuperPropertySet { .. } diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 429ad66e3..8a3e42639 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1551,6 +1551,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { | Expr::SetNewFromArray(..) => logical_collections::lower(ctx, expr), Expr::StaticMethodCall { .. } => static_method::lower(ctx, expr), Expr::SuperMethodCall { .. } + | Expr::SuperMethodCallSpread { .. } | Expr::SuperPropertyGet { .. } | Expr::SuperPropertySet { .. } | Expr::ObjectSuperPropertyGet { .. } diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index 570f87b56..d68322165 100644 --- a/crates/perry-codegen/src/expr/super_method.rs +++ b/crates/perry-codegen/src/expr/super_method.rs @@ -144,6 +144,87 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Ok(ctx.block().call(DOUBLE, &fn_name, &arg_slices)) } + // -------- super.method(...spread) -------- + // The plain `SuperMethodCall` arm passes each argument positionally, so + // a `...rest` spread would be delivered as ONE array argument. When the + // resolved super target is a NATIVE base (EventEmitter.prototype.emit + // and friends) forwarding a rest param via `super.emit(event, ...args)` + // delivered `[payload]` to the listener instead of `payload`. Flatten + // every argument (regular + spread-expanded) into a single JS array, + // then dispatch through `js_super_method_call_dynamic_apply` — the + // runtime helper materialises the array into a flat f64 buffer and + // forwards to the same parent-chain resolution used by the non-spread + // dynamic path (which handles both native-prototype and user-class + // JS parents). + Expr::SuperMethodCallSpread { method, args } => { + use perry_hir::CallArg; + let Some(current_class_name) = ctx.class_stack.last().cloned() else { + for a in args { + match a { + CallArg::Expr(e) | CallArg::Spread(e) => { + let _ = lower_expr(ctx, e)?; + } + } + } + return Ok(double_literal(0.0)); + }; + let cid = ctx.class_ids.get(¤t_class_name).copied().unwrap_or(0); + if cid == 0 { + for a in args { + match a { + CallArg::Expr(e) | CallArg::Spread(e) => { + let _ = lower_expr(ctx, e)?; + } + } + } + return Ok(double_literal(0.0)); + } + let this_box = match ctx.this_stack.last().cloned() { + Some(slot) => ctx.block().load(DOUBLE, &slot), + None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), + }; + // Build a single args array containing every argument in source + // order, expanding spreads via array-like-to-array + concat (the + // same machinery the CallSpread method-apply path uses). + let mut acc_handle = ctx.block().call(I64, "js_array_alloc", &[(I32, "0")]); + for a in args { + match a { + CallArg::Expr(e) => { + let v = lower_expr(ctx, e)?; + acc_handle = ctx.block().call( + I64, + "js_array_push_f64", + &[(I64, &acc_handle), (DOUBLE, &v)], + ); + } + CallArg::Spread(e) => { + let part_box = lower_expr(ctx, e)?; + let part_handle = + ctx.block() + .call(I64, "js_array_like_to_array", &[(DOUBLE, &part_box)]); + acc_handle = ctx.block().call( + I64, + "js_array_concat", + &[(I64, &acc_handle), (I64, &part_handle)], + ); + } + } + } + let args_array = nanbox_pointer_inline(ctx.block(), &acc_handle); + let name_global = emit_string_literal_global(ctx, method); + Ok(ctx.block().call( + DOUBLE, + "js_super_method_call_dynamic_apply", + &[ + (I32, &cid.to_string()), + (PTR, &name_global), + (I64, &method.len().to_string()), + (DOUBLE, &this_box), + (DOUBLE, &args_array), + ], + )) + } + // -------- super. as a value (issue #774) -------- // Walk the parent-class chain. If a parent declares a method // with the requested name, materialize it as a closure value diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index ec4dc13b7..9a4a25af6 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -16,13 +16,23 @@ use crate::types::{DOUBLE, I32, I64}; /// `this.method = X`), invoke the stored closure via `js_native_call_value`; /// otherwise call the static method body directly. Returns the LLVM register /// holding the unified result (phi over the two branches). +/// `override_user_args` are the FLAT (un-rest-bundled) user arguments — i.e. +/// the source-level call arguments WITHOUT the leading `this` and WITHOUT the +/// trailing rest array the static ABI bundles. The override branch dispatches a +/// dynamic value (an arrow / bound function / native method) via +/// `js_native_call_value`, which performs its own arity/rest handling from a +/// flat positional buffer — so it must receive the spread-out args, not the +/// rest array as one positional. (`super.emit(event, ...args)` forwarding to a +/// native EventEmitter override otherwise delivered `[payload]` to listeners.) +/// The static branch keeps `fallback_arg_slices` (rest-bundled) unchanged. pub(super) fn emit_own_method_override_check( ctx: &mut FnCtx<'_>, recv_box: &str, property: &str, fallback_fn: &str, fallback_arg_slices: &[(crate::types::LlvmType, &str)], - lowered_args: &[String], + this_box: &str, + override_user_args: &[String], ) -> String { // Intern the property name so we can pass (ptr, len) directly to the // override probe — saves an allocation vs synthesizing a StringHeader. @@ -66,12 +76,12 @@ pub(super) fn emit_own_method_override_check( // `IMPLICIT_THIS` to the receiver around the call so non-arrow // function bodies see the right `this` (issue #632 / #519 pattern). ctx.current_block = override_idx; - let user_arg_count = lowered_args.len().saturating_sub(1); + let user_arg_count = override_user_args.len(); let (args_ptr, args_len) = if user_arg_count == 0 { ("null".to_string(), "0".to_string()) } else { let buf_reg = ctx.func.alloca_entry_array(DOUBLE, user_arg_count); - for (i, a_val) in lowered_args.iter().skip(1).enumerate() { + for (i, a_val) in override_user_args.iter().enumerate() { let slot = ctx .block() .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); @@ -84,10 +94,11 @@ pub(super) fn emit_own_method_override_check( )); (ptr_reg, user_arg_count.to_string()) }; - let recv_for_this = lowered_args - .first() - .cloned() - .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + let recv_for_this = if this_box.is_empty() { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + } else { + this_box.to_string() + }; let prev_this = ctx .block() .call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &recv_for_this)]); diff --git a/crates/perry-codegen/src/lower_call/property_get.rs b/crates/perry-codegen/src/lower_call/property_get.rs index 0e46e25f8..34968ef0d 100644 --- a/crates/perry-codegen/src/lower_call/property_get.rs +++ b/crates/perry-codegen/src/lower_call/property_get.rs @@ -2058,13 +2058,22 @@ pub fn try_lower_property_get_method_call( // class). Hono's SmartRouter rebinds `this.match` on the // first call so subsequent calls go through the bound // fast-path closure instead of the original method. + // The override branch dispatches a dynamic value (arrow / bound + // / native method) via `js_native_call_value`, which does its + // own arity/rest handling from a FLAT positional buffer. Pass + // the un-rest-bundled user args (`fallback_user_args`) — not the + // rest-bundled `lowered_args[1..]`, which would deliver the rest + // array as one positional argument and break a native override + // such as `super.emit(event, ...args)` forwarding to + // EventEmitter (#620 / rest-spread-to-native-override). return Ok(Some(emit_own_method_override_check( ctx, &recv_box, property, &fallback_fn, &arg_slices, - &lowered_args, + &recv_box, + &fallback_user_args, ))); } diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index f21bf5947..1b626ac94 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1167,6 +1167,13 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { DOUBLE, &[I32, PTR, I64, DOUBLE, PTR, I64], ); + // `super.method(...spread)` — flatten the (codegen-built) args array into a + // flat f64 buffer and forward to `js_super_method_call_dynamic`. + module.declare_function( + "js_super_method_call_dynamic_apply", + DOUBLE, + &[I32, PTR, I64, DOUBLE, DOUBLE], + ); module.declare_function("js_array_push_spread_any", I64, &[I64, DOUBLE]); // Issue #711 part 2: prototype-based class declaration via // `.prototype = `. Binds an object as the function's diff --git a/crates/perry-hir/src/analysis/uses_this.rs b/crates/perry-hir/src/analysis/uses_this.rs index 898e3786e..4c1e0bb42 100644 --- a/crates/perry-hir/src/analysis/uses_this.rs +++ b/crates/perry-hir/src/analysis/uses_this.rs @@ -11,6 +11,7 @@ pub(crate) fn uses_this_expr(expr: &Expr) -> bool { Expr::SuperCall(_) | Expr::SuperCallSpread(_) | Expr::SuperMethodCall { .. } + | Expr::SuperMethodCallSpread { .. } | Expr::SuperPropertyGet { .. } | Expr::SuperPropertySet { .. } | Expr::ObjectSuperPropertyGet { .. } diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index ca82680d1..25b0094d2 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -573,6 +573,19 @@ pub enum Expr { args: Vec, }, + /// `super.method(...spread)` with one or more spread arguments. Mirrors + /// `SuperCallSpread` for the method-call shape: the plain `SuperMethodCall` + /// drops the spread marker and would pass the spread operand (an array) as + /// ONE positional argument, so a `super.emit(event, ...args)` forwarding a + /// rest param to a native base (EventEmitter) delivered `[payload]` instead + /// of `payload`. Codegen flattens every arg (regular + spread-expanded) + /// into a single args array and dispatches through the runtime super + /// helper, which already takes an args buffer. + SuperMethodCallSpread { + method: String, + args: Vec, + }, + // Super property read (value form). super.. Resolved at // codegen by walking the parent class's method table (issue #774). SuperPropertyGet { diff --git a/crates/perry-hir/src/lower/expr_call/mod.rs b/crates/perry-hir/src/lower/expr_call/mod.rs index d78a6919c..99dcd093a 100644 --- a/crates/perry-hir/src/lower/expr_call/mod.rs +++ b/crates/perry-hir/src/lower/expr_call/mod.rs @@ -378,6 +378,12 @@ fn lower_call_inner(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Result Result tag(h, 50), Expr::SuperCall(args) => { tag(h, 51); args.hash(h); } Expr::SuperMethodCall { method, args } => { tag(h, 52); method.hash(h); args.hash(h); } + Expr::SuperMethodCallSpread { method, args } => { tag(h, 12509); method.hash(h); for a in args { match a { CallArg::Expr(e) | CallArg::Spread(e) => e.hash(h), } } } Expr::SuperPropertyGet { property } => { tag(h, 461); property.hash(h); } Expr::SuperPropertySet { parent_class_id, parent_class_name, key, value } => { tag(h, 12238); parent_class_id.hash(h); parent_class_name.hash(h); key.as_ref().hash(h); value.as_ref().hash(h); } Expr::ObjectSuperPropertyGet { home, key, receiver } => { tag(h, 12231); home.as_ref().hash(h); key.as_ref().hash(h); receiver.as_ref().hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index 9cfceb9af..dddbeecfc 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -861,7 +861,7 @@ where } } } - Expr::SuperCallSpread(args) => { + Expr::SuperCallSpread(args) | Expr::SuperMethodCallSpread { args, .. } => { for a in args { match a { CallArg::Expr(e) | CallArg::Spread(e) => f(e), diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index 9816f1e34..246a3802f 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -858,7 +858,7 @@ where } } } - Expr::SuperCallSpread(args) => { + Expr::SuperCallSpread(args) | Expr::SuperMethodCallSpread { args, .. } => { for a in args { match a { CallArg::Expr(e) | CallArg::Spread(e) => f(e), diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 3f9302782..70a6ba5d2 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -350,6 +350,62 @@ static KEEP_JS_SUPER_METHOD_CALL_DYNAMIC: unsafe extern "C" fn( usize, ) -> f64 = js_super_method_call_dynamic; +/// `super.method(...spread)` dispatch where the argument count is dynamic. +/// Codegen flattens every argument (regular args plus every spread-expanded +/// element) into a single JS array `args_array`, then routes here. We +/// materialise that array into a contiguous flat `f64` buffer and forward to +/// `js_super_method_call_dynamic`, so a `super.emit(event, ...args)` forwarding +/// a rest param to a native base (EventEmitter) delivers the spread elements as +/// individual arguments instead of one array. Without this the plain +/// `SuperMethodCall` lowering passed the spread operand as ONE positional arg. +/// +/// # Safety +/// `name_ptr` must be valid for `name_len` bytes. `args_array` is a NaN-boxed +/// array pointer (or any non-array value, treated as zero args). +#[no_mangle] +pub unsafe extern "C" fn js_super_method_call_dynamic_apply( + child_class_id: u32, + name_ptr: *const u8, + name_len: usize, + this_value: f64, + args_array: f64, +) -> f64 { + let arr = + (args_array.to_bits() & crate::value::POINTER_MASK) as *const crate::array::ArrayHeader; + let n = if arr.is_null() { + 0usize + } else { + crate::array::js_array_length(arr) as usize + }; + let mut flat: Vec = Vec::with_capacity(n); + for i in 0..n { + flat.push(crate::array::js_array_get_f64(arr, i as u32)); + } + let (args_ptr, args_len) = if flat.is_empty() { + (std::ptr::null(), 0usize) + } else { + (flat.as_ptr(), flat.len()) + }; + js_super_method_call_dynamic( + child_class_id, + name_ptr, + name_len, + this_value, + args_ptr, + args_len, + ) +} + +/// Keepalive anchor (generated-code-only callee). +#[used] +static KEEP_JS_SUPER_METHOD_CALL_DYNAMIC_APPLY: unsafe extern "C" fn( + u32, + *const u8, + usize, + f64, + f64, +) -> f64 = js_super_method_call_dynamic_apply; + /// Run the constructor of class `parent_cid` (or its nearest ctor-bearing /// ancestor) on the EXISTING `this`, taking arguments from a flat f64 buffer — /// the codegen `super()` ABI. Returns `true` when a constructor was found and diff --git a/test-files/test_super_rest_spread_native_override.ts b/test-files/test_super_rest_spread_native_override.ts new file mode 100644 index 000000000..06773b9b0 --- /dev/null +++ b/test-files/test_super_rest_spread_native_override.ts @@ -0,0 +1,70 @@ +// Regression: `super.(event, ...rest)` forwarding a rest parameter +// to a NATIVE base method (EventEmitter.prototype.emit) must spread the rest +// elements as individual arguments — not deliver the rest array as one arg. +// +// Two latent codegen bugs were involved: +// 1. The own-method-override runtime check passed the STATIC ABI's +// rest-bundled args to the dynamic-override branch (`js_native_call_value`), +// so the native override received `[event, [payload]]` and listeners saw +// `[payload]` instead of `payload`. +// 2. `SuperMethodCall` dropped the spread marker, so the static dispatch path +// passed the rest array as a single positional argument. +import { EventEmitter as EE } from "events"; + +let M: any; +M = class M extends EE { + constructor() { + super(); + } + emit(event: any, ...args: any[]) { + return super.emit(event, ...args); + } +}; + +const m: any = new M(); + +let one: any = null; +m.on("one", (p: string) => { + one = p; +}); +m.emit("one", "PAYLOAD"); +console.log("one:", JSON.stringify(one)); // expect "PAYLOAD" + +let a: any = null; +let b: any = null; +m.on("two", (x: string, y: string) => { + a = x; + b = y; +}); +m.emit("two", "A", "B"); +console.log("two:", JSON.stringify(a), JSON.stringify(b)); // expect "A" "B" + +let zero = "no"; +m.on("zero", () => { + zero = "yes"; +}); +m.emit("zero"); +console.log("zero:", zero); // expect yes + +// Spread forwarding through a fixed-arity JS parent (static dispatch path). +class Base { + recv(x: any, y: any) { + return JSON.stringify(x) + "|" + JSON.stringify(y); + } +} +class Child extends Base { + recv(...rest: any[]) { + return super.recv(...(rest as [any, any])); + } +} +console.log("fixed-arity:", new Child().recv("X", "Y")); // expect "X"|"Y" + +// Instance-level override of a rest-param method must receive flat args. +class Collector { + collect(...items: any[]) { + return "orig:" + items.length; + } +} +const c: any = new Collector(); +c.collect = (...items: any[]) => "ovr:" + items.length; +console.log("override-rest:", c.collect("a", "b", "c")); // expect ovr:3