Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/perry-codegen/src/collectors/this_as_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ pub fn expr_uses_this_as_value(e: &perry_hir::Expr, fields: &HashSet<String>) ->
} => true,
Expr::SuperCall(_)
| Expr::SuperMethodCall { .. }
| Expr::SuperMethodCallSpread { .. }
| Expr::SuperPropertySet { .. }
| Expr::ObjectSuperPropertyGet { .. }
| Expr::ObjectSuperPropertySet { .. }
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
| Expr::SetNewFromArray(..) => logical_collections::lower(ctx, expr),
Expr::StaticMethodCall { .. } => static_method::lower(ctx, expr),
Expr::SuperMethodCall { .. }
| Expr::SuperMethodCallSpread { .. }
| Expr::SuperPropertyGet { .. }
| Expr::SuperPropertySet { .. }
| Expr::ObjectSuperPropertyGet { .. }
Expand Down
81 changes: 81 additions & 0 deletions crates/perry-codegen/src/expr/super_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,87 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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(&current_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.<prop> 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
Expand Down
25 changes: 18 additions & 7 deletions crates/perry-codegen/src/lower_call/method_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))]);
Expand All @@ -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)]);
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-codegen/src/lower_call/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)));
}

Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// `<func>.prototype = <obj>`. Binds an object as the function's
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/analysis/uses_this.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. }
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-hir/src/ir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,19 @@ pub enum Expr {
args: Vec<Expr>,
},

/// `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<CallArg>,
},

// Super property read (value form). super.<prop>. Resolved at
// codegen by walking the parent class's method table (issue #774).
SuperPropertyGet {
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-hir/src/lower/expr_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ fn lower_call_inner(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Result<E
args,
});
}
if let Some(spread_args) = spread_args.clone() {
return Ok(Expr::SuperMethodCallSpread {
method: ident.sym.to_string(),
args: spread_args,
});
}
return Ok(Expr::SuperMethodCall {
method: ident.sym.to_string(),
args,
Expand All @@ -400,6 +406,12 @@ fn lower_call_inner(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Result<E
// receiver binding (test262 super/prop-expr-cls-ref-this).
if let ast::Expr::Lit(ast::Lit::Str(s)) = computed.expr.as_ref() {
if let Some(method) = s.value.as_str() {
if let Some(spread_args) = spread_args.clone() {
return Ok(Expr::SuperMethodCallSpread {
method: method.to_string(),
args: spread_args,
});
}
return Ok(Expr::SuperMethodCall {
method: method.to_string(),
args,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/stable_hash/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl SH for Expr {
Expr::This => 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); }
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/walker/expr_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/walker/expr_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
56 changes: 56 additions & 0 deletions crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64> = 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
Expand Down
Loading
Loading