Skip to content

respondsToConversion asks an overridden #respond_to? with one argument; MRI passes two #859

Open

Description

@tannevaled

respondsToConversion (internal/vm/vm.go) implements MRI's
check_funcall_respond_to — consult an overridden #respond_to? before
sending a conversion method — but calls it with one argument where MRI calls it
with two:

MRI  : respond_to?(:to_a, true)
rbgo : respond_to?(:to_a)        → the receiver's own default fills in false

Measured with this, on /usr/bin/ruby and on rbgo built from
splat-array-subclass:

class Refuser
  def respond_to?(name, include_private = false)
    STDERR.puts "  respond_to?(#{name.inspect}, #{include_private.inspect})"
    false
  end
  def to_a; [:wrong]; end
end
a = *Refuser.new
MRI  :   respond_to?(:to_a, true)
rbgo :   respond_to?(:to_a, false)

Why it is not only cosmetic

The second argument is include_private. An object that overrides
#respond_to? and honours that flag — the common shape being
def respond_to?(name, include_private = false); include_private && …; end, or
anything delegating to super — will answer false to rbgo where it answers
true to MRI. rbgo then wraps the object instead of converting it, silently,
with no exception to notice.

It is narrower than it first looks, and that is worth recording: a private
conversion method on an object with the DEFAULT #respond_to? is reached
correctly today, because respondsToConversion falls through to a method-table
lookup that finds private methods anyway. Verified — *SplatPriv.new with a
private #to_a gives [7, 8] on both engines. The divergence needs an
overridden #respond_to? to show up.

What matching it takes

vm_respond_to (vm_method.c) does not simply pass two arguments; it reads the
arity of the custom #respond_to? first:

  • arity 1 → send one argument;
  • arity 2 (or variadic) → send (mid, priv);
  • arity > 2 → ArgumentError: respond_to? must accept 1 or 2 arguments (requires N).

Without that rule, passing two arguments unconditionally would raise
ArgumentError on every object whose #respond_to? takes exactly one — which
is a perfectly ordinary definition, and would be a regression far wider than the
bug.

Blast radius, which is why it was not folded into the splat fix

respondsToConversion is the single gate for every conversion site:
to_ary, to_a, to_str, to_sym, to_hash. A change there wants its own
measurement rather than a line inside a splat fix.

Note that no spec in the gate can see this. ruby/spec asserts it —
x.should_receive(:respond_to?).with(:to_ary, true) in
language/variables_spec.rb:75 — but our minimal MSpec shim's .with does not
enforce the argument list, so the example passes either way. Whoever fixes this
should tighten .with in the shim in the same change, or the fix is unwitnessed.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions