fix(ast): anonymous rest param def f(*) no longer compiles to arity 0#335
Conversation
`def f(*)` is the Ruby 2.0+ anonymous forwarding form — the
method should accept arbitrary positional args and silently drop
them. Prism reports this as `RestParameterNode { name: None }`.
Before this fix, the translator at ast.rs:2111 did
`rp.name().map(|n| cid_to_string(n))`, so an unnamed rest
yielded `rest = None` and the rest slot was dropped entirely.
Result: `def f(*); end` compiled with arity 0 and rejected any
positional arg with `ArgumentError: wrong number of arguments
(given N, expected 0)`. Named forms `def f(*x)` were unaffected.
Fix mirrors the existing anonymous-block-param pattern at
~ast.rs:2109: fall back to a reserved sentinel `*` (invalid as
a user identifier) so the binder still allocates a sink slot.
CRuby surfaces the same anonymous rest as `:*` in
`Method#parameters` (`[[:rest, :*]]`), so the sentinel passes
through introspection unchanged.
Discovered by TRY_RUNS pass-11 probe — sinatra-4's
`Mustermann.new(path, **opts)` at sinatra/base.rb:1818 hit this
via the probe's `module Mustermann; def self.new(*); ...; end`
stub. (Layer #13.)
Tests: `tests/diff/anonymous_rest_param.rb` covers 6 shapes —
zero/many positional, required + rest, anonymous `**` and `&`
(already worked, kept as regression pins), Method#parameters /
#arity introspection, and the `def self.new(*)` singleton form
that triggered the probe.
gapscan PR diffBoth binaries produced identical histograms across the 10 canonical scan targets. (If the classifier changed for node classes that none of these targets exercise, this view won't catch it — the data-file diff would.) See docs/gap-reports/ for the dataset and methodology. |
There was a problem hiding this comment.
Pull request overview
Fixes rubyrs’ AST translation for anonymous rest parameters so def f(*) no longer drops the rest slot (which previously caused methods to compile as arity 0 and reject positional arguments). This aligns runtime call acceptance and Method#parameters / Method#arity introspection with CRuby.
Changes:
- Translate
RestParameterNode { name: None }to a reserved sentinel name"*"so the compiler/binder still allocates a rest slot. - Add a new CRuby differential fixture covering anonymous
*behavior, arity/parameters introspection, and singletondef self.new(*). - Register the new fixture in the
diff_crubytest runner.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| crates/rubyrs/src/ast.rs | Preserves anonymous rest params by mapping missing rest names to "*" (mirrors existing anonymous block-param handling). |
| crates/rubyrs/tests/diff/anonymous_rest_param.rb | New differential fixture exercising anonymous * acceptance, introspection parity, plus regression pins for ** and &. |
| crates/rubyrs/tests/diff_cruby.rs | Adds the new fixture to the diff test suite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Three passes condensed: layers #11/#12/#13/#14 closed across PRs #209/#324/#335/#337 between 2026-05-28 and 2026-06-01. Highlights: - Pass-10: PR #324 closed `Module#module_function` (Cat H). - Pass-11: PR #335 closed anonymous `def f(*)` → arity 0 (Cat I real bug, uncovered by Mustermann stub). - Pass-12: PR #337 closed `Class#inherited` callback never fired (Cat H, surfaced by `class App < Sinatra::Base`). - sinatra/base.rb now loads fully (REACHED-END) for the first time since the probe started. Next wall (open): #15 — `Kernel#caller`, surfaced by sinatra/base.rb:1913 `cleaned_caller` during base.rb's own load (Sinatra::Application < Base triggers the just-fixed inherited hook, which calls caller_files). Pre-existing follow-up uncovered: `def h(a, *, b)` Method#parameters renders `:opt` where CRuby renders `:rest` and arity -2 vs CRuby's -3. Separate post-rest required-param rendering bug, hidden by #13 until that fix landed.
Summary
def f(*)(Ruby 2.0+ anonymous forwarding form) used to compile with arity 0 — translator dropped the rest slot when Prism reportsRestParameterNode { name: None }.ast.rs:2111mirrors the existing anonymous block-param pattern: fall back to sentinel*so the binder still allocates a sink slot. CRuby reports the same anonymous rest as:*inMethod#parameters, so introspection stays byte-identical.Mustermann.new(path, **opts)atsinatra/base.rb:1818hit this via the probe'sdef self.new(*)Mustermann stub. (Layer Tier 2 scans — Bundler, Tilt, stdlib slice (n=10 dataset) #13.)Scope check
def f(*x)already worked — unaffected.**and&already worked — added as regression pins in the fixture.def h(a, *, b)) is not addressed here — it's a separate bug that was previously hidden behind this one. Tracked as a separate follow-up.Test plan
cargo test --test diff_cruby anonymous_rest_param— new fixture covers 6 shapes (zero/many positional, required + rest, anonymous**/®ression pins,Method#parameters/#arityintrospection,def self.new(*)singleton form).cargo test --test diff_cruby— 307/307 green (304 → 307 after recent additions; this PR adds 1).cargo test— full suite green.Mustermann.new(...)call site no longer trips this gap; the next layer (Spike L3-A: rb_raise (longjmp across Rust stack) #14) is now waiting behind it.