Skip to content

fix(codegen): forward rest-param spread through native method overrides and super method calls#5529

Merged
proggeramlug merged 2 commits into
mainfrom
fix/super-method-rest-spread
Jun 22, 2026
Merged

fix(codegen): forward rest-param spread through native method overrides and super method calls#5529
proggeramlug merged 2 commits into
mainfrom
fix/super-method-rest-spread

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Problem

A subclass that overrides a method and forwards a rest param via super.method(event, ...args) delivered the spread as a single array argument when the resolved super target was a native / bound base (e.g. EventEmitter):

class M extends EventEmitter {
  constructor() { super() }
  emit(event, ...args) { return super.emit(event, ...args) }
}
const m = new M()
m.on("evt", (...p) => console.log(p))
m.emit("evt", "A", "B")   // delivered ["A","B"], undefined  ❌  (should deliver "A", "B")

This breaks the common emit / event-forwarding override pattern.

Root cause — two distinct latent bugs

  1. Native-override branch (lower_call/property_get.rs, lower_call/method_override.rs): a method with a rest param rest-bundles its args into [this, event, [rest]] for the static ABI. The own-method-override runtime branch (which invokes the native base method installed on the instance) was handed that bundled list and forwarded the rest array as one positional arg. Fixed by giving the override branch the flat, un-bundled user args while the static branch keeps the bundled form.

  2. Static super-method dispatch (perry-hir/.../expr_call/mod.rs): super.method(...args) lowered to Expr::SuperMethodCall { args: Vec<Expr> }, discarding the spread marker (only the constructor SuperCallSpread path preserved it) — so even a fixed-arity JS parent received the spread as one array. Added a SuperMethodCallSpread HIR variant + a js_super_method_call_dynamic_apply runtime helper that flattens the codegen-built args array (regular + spread-expanded) before dispatch.

Verification

  • The repro now delivers "A", "B"; zero-arg fires; super.emit(e) works.
  • Regression matrix (all correct): native super spread, JS-parent super fixed-arity, JS-parent super spread, super no-arg, mixed positional+spread super, plain this.method override of a rest method, non-rest override.
  • cargo test -p perry-runtime --lib --test-threads=1: 1067 passed, 0 failed. cargo check clean.

Repro: test-files/test_super_rest_spread_native_override.ts.

Summary by CodeRabbit

  • New Features

    • Added support for super.method(...spread) so spread arguments are correctly flattened and forwarded during super method dispatch.
  • Bug Fixes

    • Improved super handling for spread calls: this-usage detection and override dispatch now receive the original argument shape (avoids rest being bundled incorrectly), producing correct argument counts for native/event-style methods.
  • Tests

    • Added a regression test covering super.<method>(event, ...rest) forwarding across instance overrides and static dispatch paths, ensuring flat argument delivery.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e4edf032-de63-4db3-b634-9c370db7b5fa

📥 Commits

Reviewing files that changed from the base of the PR and between 0f160fb and b554061.

📒 Files selected for processing (2)
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry-hir/src/stable_hash/expr.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-codegen/src/expr/super_method.rs

📝 Walkthrough

Walkthrough

Adds a new HIR variant Expr::SuperMethodCallSpread for super.method(...spread) calls, wires it through analysis, walkers, stable-hash, and HIR lowering. A new runtime FFI function js_super_method_call_dynamic_apply flattens a NaN-boxed args array and forwards it to the existing dynamic super-method dispatch. The codegen override-check is refactored to pass un-rest-bundled arguments, and regression tests verify correct argument flattening.

Changes

super.method(...spread) end-to-end implementation

Layer / File(s) Summary
HIR variant definition and analysis infrastructure
crates/perry-hir/src/ir/expr.rs, crates/perry-hir/src/analysis/uses_this.rs, crates/perry-hir/src/stable_hash/expr.rs, crates/perry-hir/src/walker/expr_mut.rs, crates/perry-hir/src/walker/expr_ref.rs
Defines Expr::SuperMethodCallSpread { method: String, args: Vec<CallArg> } and registers it in uses_this_expr (returns true), stable-hash (tag 12509), and both mutable and immutable expression walkers.
HIR call lowering: emit SuperMethodCallSpread
crates/perry-hir/src/lower/expr_call/mod.rs
In lower_call_inner, both SuperProp::Ident and string-literal computed property forms now emit Expr::SuperMethodCallSpread when spread arguments are present, falling back to Expr::SuperMethodCall otherwise.
Runtime FFI helper: js_super_method_call_dynamic_apply
crates/perry-runtime/src/object/class_constructors.rs, crates/perry-codegen/src/runtime_decls/strings.rs
Adds js_super_method_call_dynamic_apply which extracts a NaN-boxed array into a flat Vec<f64> and forwards to js_super_method_call_dynamic, with keepalive anchor and Phase D runtime symbol declaration.
Codegen lowering for SuperMethodCallSpread
crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/collectors/this_as_value.rs, crates/perry-codegen/src/expr/super_method.rs
Routes Expr::SuperMethodCallSpread through super_method::lower, implements lowering that resolves class id and this_box, builds a flattened args array (plain args via js_array_push_f64, spread args via js_array_like_to_array/js_array_concat), and dispatches to js_super_method_call_dynamic_apply. Marks the variant as this-as-value in the collector.
Method override check: separate this_box and un-rest-bundled args
crates/perry-codegen/src/lower_call/method_override.rs, crates/perry-codegen/src/lower_call/property_get.rs
Refactors emit_own_method_override_check to accept explicit this_box and override_user_args parameters instead of deriving both from lowered_args. Updates the call site to pass fallback_user_args so native/arrow/bound overrides receive flat positional args rather than a rest-bundled slice.
Regression tests
test-files/test_super_rest_spread_native_override.ts
Adds tests for super.emit(event, ...args) forwarding into native EventEmitter (one arg, two args, zero args), rest spread through a fixed-arity JS parent, and an instance-level override of a rest-parameter method verifying flattened argument count.

Sequence Diagram(s)

sequenceDiagram
  participant HIR as HIR Lowering
  participant Codegen as Codegen (lower_expr)
  participant SuperMethod as super_method::lower
  participant Runtime as js_super_method_call_dynamic_apply
  participant Dynamic as js_super_method_call_dynamic

  HIR->>Codegen: Expr::SuperMethodCallSpread { method, args }
  Codegen->>SuperMethod: route via super_method::lower
  SuperMethod->>SuperMethod: resolve class id, materialize this_box
  SuperMethod->>SuperMethod: build args_array (push plain, concat spread)
  SuperMethod->>Runtime: cid, name_ptr, name_len, this_box, args_array
  Runtime->>Runtime: flatten NaN-boxed array to Vec<f64>
  Runtime->>Dynamic: cid, name_ptr, name_len, this_value, args_ptr, args_len
  Dynamic-->>Runtime: result f64
  Runtime-->>SuperMethod: result f64
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PerryTS/perry#5395: Modifies crates/perry-codegen/src/expr/super_method.rs and the super-method lowering/dispatch path, directly overlapping with the codegen layer changed in this PR.

Poem

A rabbit hops through the class tree with glee,
super.emit(...rest) now spreads wild and free!
No bundled arrays to tangle the call,
Flat arguments fly to the listener's hall.
🐇✨ Each arg lands just right, one by one —
The spread is now done, and the tests pass: well run!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: fixing rest-param spread forwarding through native method overrides and super method calls.
Description check ✅ Passed The description thoroughly covers problem statement, root causes, and verification. All required template sections are present and well-populated with concrete details and test results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/super-method-rest-spread

Comment @coderabbitai help to get the list of available commands and usage tips.

…super calls

When a class method with a rest parameter is invoked and the receiver has an
instance-level (own-property) override of that method — as happens for an
EventEmitter subclass whose native emit/on surface is installed on the
instance — the override branch of the runtime override check was handed the
STATIC ABI's rest-bundled argument list. It then forwarded the rest array as a
single positional argument to the dynamic override (js_native_call_value), so
super.emit(event, ...args) delivered [payload] to listeners instead of payload
(and lost trailing args entirely for the multi-arg case).

Fix the override branch to receive the flat, un-rest-bundled user arguments;
the static branch keeps the bundled arguments unchanged.

Separately, the SuperMethodCall lowering discarded the spread marker entirely,
so the static super-dispatch path passed a ...rest spread as one array argument
even for an ordinary fixed-arity parent. Add a SuperMethodCallSpread HIR variant
(mirroring the existing constructor SuperCallSpread) plus a runtime apply helper
that flattens the codegen-built args array (regular + spread-expanded) before
forwarding through the shared super-method dispatch.

Adds a standalone regression fixture covering 0/1/2-arg native-override
forwarding, fixed-arity JS-parent spread, and rest-param instance overrides.
@proggeramlug proggeramlug force-pushed the fix/super-method-rest-spread branch from 6fc45e8 to 0f160fb Compare June 21, 2026 22:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-hir/src/stable_hash/expr.rs`:
- Line 100: The tag function call in the Expr::SuperMethodCallSpread variant
uses tag value 12241, which is already used by the Expr::RegisterClassCaptures
variant, causing a stable-hash collision. Replace the tag value 12241 in the
SuperMethodCallSpread match arm with a unique tag number that is not already
used by any other Expr variant in this stable hash implementation. Verify the
new tag value does not conflict with any other tags used in the file before
committing the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d51461f-9fdb-4584-961d-cef0144e4a85

📥 Commits

Reviewing files that changed from the base of the PR and between d032369 and 0f160fb.

📒 Files selected for processing (14)
  • crates/perry-codegen/src/collectors/this_as_value.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry-codegen/src/lower_call/method_override.rs
  • crates/perry-codegen/src/lower_call/property_get.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/analysis/uses_this.rs
  • crates/perry-hir/src/ir/expr.rs
  • crates/perry-hir/src/lower/expr_call/mod.rs
  • crates/perry-hir/src/stable_hash/expr.rs
  • crates/perry-hir/src/walker/expr_mut.rs
  • crates/perry-hir/src/walker/expr_ref.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • test-files/test_super_rest_spread_native_override.ts

Comment thread crates/perry-hir/src/stable_hash/expr.rs Outdated
The new Expr::SuperMethodCallSpread variant reused tag 12241, already
held by Expr::RegisterClassCaptures, tripping the
expr_variant_stable_hash_tags_are_unique guard and risking incorrect
incremental-cache reuse. Give it 12509 (next free tag after 12508).

Also applies cargo fmt to super_method.rs (CI fmt gate). Resolves the
CodeRabbit review comment on expr.rs:100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug merged commit 3c34fee into main Jun 22, 2026
14 of 15 checks passed
@proggeramlug proggeramlug deleted the fix/super-method-rest-spread branch June 22, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant