Skip to content

Handle mixed and omitted forwarded arguments - #434

Merged
mame merged 1 commit into
ruby:masterfrom
pvcresin:fix_forwarding_arguments_extra
Sep 1, 2026
Merged

Handle mixed and omitted forwarded arguments#434
mame merged 1 commit into
ruby:masterfrom
pvcresin:fix_forwarding_arguments_extra

Conversation

@pvcresin

@pvcresin pvcresin commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Summary

This updates forwarding argument handling for omitted and mixed ... calls.

The main issue was that omitted forwarded arguments were still represented by placeholder values early enough to participate in method-call resolution. That had two effects:

  • omitted ... calls could produce false arity diagnostics before the forwarding site was activated by actual arguments
  • mixed forms such as foo(x, ...) and super(x, ...) were hard to model correctly, because the forwarded remainder was not clearly separated from explicit leading arguments

Approach

This keeps the existing ForwardingArguments model, but refines how forwarded arguments are materialized at call sites.

There are now two distinct forwarding modes:

  • bare super continues to forward the full argument set
  • omitted ... calls materialize only the forwarded remainder, and only after activation from actual arguments

With that split, explicit leading arguments can still be prepended at the call site, while omitted placeholders no longer leak into method-call resolution too early.

This is intended to be a local refinement of forwarding behavior, not a redesign of the overall forwarding model.

Implementation Notes

A few parts of the diff are larger than they may look at first glance:

  • @forwarding_arguments = :rest is used to distinguish omitted ... forwarding from the existing full-forwarding behavior used by bare super
  • ast/method.rb now prepares dedicated forwarding placeholders for optional/rest/keyword/block forwarding, instead of reusing the method's formal argument vertices directly
  • ForwardingActualArguments exists to represent forwarded actual arguments that may still be omitted until activation, and to normalize them before method-call resolution

That extra structure is mainly there to avoid encoding the behavior as scattered special cases at call sites.

Changes

  • distinguish omitted ... calls from bare super forwarding
  • delay omitted forwarded arguments until activation so placeholder values do not trigger false arity diagnostics
  • preserve the existing full-argument forwarding behavior for bare super
  • support mixed forwarding forms such as foo(x, ...) and super(x, ...)
  • add scenario coverage for mixed forwarding and super(...)
  • move the extra positional forwarding case out of known issues into the regular scenario suite

Notes

The intent here is to keep the current forwarding design intact as much as possible, while making omitted forwarding behave like "forward the remaining arguments" at the call site.

In particular, this change tries to make the mixed cases work without changing the overall role of ForwardingArguments in method definition and argument propagation.

@mame

mame commented Apr 28, 2026

Copy link
Copy Markdown
Member

Thanks! Overall it looks good to me, but it's a fairly large change, so I'd like a bit more time to review it properly. Please give me a few days. I'll try to get to it within this week.

Differentiate `...` calls from bare `super` forwarding so we can drop omitted placeholders without breaking full-argument forwarding.

Also add scenarios for mixed forwarding, `super(...)`, and the no-diagnostic case, and move the extra-positional forwarding case out of known issues.

Co-authored-by: Codex <codex@openai.com>
@mame
mame force-pushed the fix_forwarding_arguments_extra branch from ca95ac7 to a78023d Compare September 1, 2026 09:18
@mame

mame commented Sep 1, 2026

Copy link
Copy Markdown
Member

Sorry for sitting on this for so long! I have to admit I couldn't get my head around the whole thing, but overall it looks good.

One thing though: opt_keyword_pairs = @opt_keyword_pairs.reject {|_name, vtx| vtx.types.empty? } is problematic because it inspects vtx.types here. It is an implicit rule in TypeProf that install should, in principle, only build the graph; if it makes decisions based on the values flowing through the graph, the result becomes inconsistent once more types arrive later. Concretely, with this code:

class B
  def foo(a, b: 1) = [a, b]
end
class C < B
  def foo(a, b: 2) = super
end
C.new.foo(1, b: "s")
master:  B#foo: (Integer, ?b: Integer | String) -> [Integer, Integer | String]
PR:      B#foo: (Integer, ?b: Integer)          -> [Integer, Integer]   # String does not get through

I have a fix for this locally, so let me merge this first and then send that as a separate PR.

Again, I'm really sorry for saying I'd look at it within the week and then leaving it for four months!

@mame
mame merged commit 5ff15c2 into ruby:master Sep 1, 2026
6 checks passed
mame added a commit that referenced this pull request Sep 1, 2026
Follow-up to #434.

`build_keyword_args` dropped an optional keyword whose vertex was empty, but it
runs during install, when every vertex is still empty and no dependency edge is
registered, so the keyword was never forwarded at all. The reject was there to
keep the named keywords out of the `**rest` hash; do that in a box instead,
which also keeps the rest's own fields.

    class B
      def foo(a, b: 1) = [a, b]
    end
    class C < B
      def foo(a, b: 2) = super
    end
    C.new.foo(1, b: "s")   # B#foo's b: Integer -> Integer | String
mame added a commit that referenced this pull request Sep 1, 2026
Follow-up to #434.

`bar(*, ...)` is a syntax error, so a forwarding call site never has an
anonymous rest to install, and the branch still tested for `DummyNilNode`, which
ff8a516 replaced with `nil`.
mame added a commit that referenced this pull request Sep 1, 2026
Follow-up to #434.

`build_keyword_args` dropped an optional keyword whose vertex was empty, but it
runs during install, when every vertex is still empty and no dependency edge is
registered, so the keyword was never forwarded at all. The reject was there to
keep the named keywords out of the `**rest` hash; do that in a box instead,
which also keeps the rest's own fields.

    class B
      def foo(a, b: 1) = [a, b]
    end
    class C < B
      def foo(a, b: 2) = super
    end
    C.new.foo(1, b: "s")   # B#foo's b: Integer -> Integer | String
mame added a commit that referenced this pull request Sep 1, 2026
Follow-up to #434.

`bar(*, ...)` is a syntax error, so a forwarding call site never has an
anonymous rest to install, and the branch still tested for `DummyNilNode`, which
ff8a516 replaced with `nil`.
@pvcresin
pvcresin deleted the fix_forwarding_arguments_extra branch September 1, 2026 09:30
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.

2 participants