Skip to content

A &block-pass that is not a Proc raises TypeError, with MRI's two messages - #906

Merged
tannevaled merged 1 commit into
mainfrom
toproc-typeerror
Nov 7, 2026
Merged

tannevaled merged 1 commit into
mainfrom
toproc-typeerror

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Closes another of the language/send_spec.rb failures from #799: pass=74 fail=4 → pass=75 fail=3.

class Specs; def makeproc(&b) = b; end
Specs.new.makeproc(&Object.new)

# rbgo: NoMethodError: undefined method 'to_proc' for an instance of Object
# MRI:  TypeError: no implicit conversion of Object into Proc

toBlock reached to_proc through vm.send unconditionally, so an object with no to_proc surfaced the dispatch failure instead of a conversion failure.

⛔ The class was never NoMethodError, so this is not only a 4.0 move

engine &Object.new
MRI 2.6.10 TypeError: wrong argument type Object (expected Proc)
MRI 4.0.7 TypeError: no implicit conversion of Object into Proc
rbgo NoMethodError: undefined method 'to_proc'

2.6 already answered TypeError. What 4.0 changed is the wording. So the wrong class was a plain defect of long standing, and only the two messages belong to the version move — I had this filed as "a 4.0 behaviour" and that was half wrong.

Bibliography

vm_to_proc looks the method up first and branches, which is why there are two messages:

me = rb_callable_method_entry_with_refinements(CLASS_OF(proc), idTo_proc, NULL);
if (me) { b = rb_vm_call0(GET_EC(), proc, idTo_proc, 0, NULL, me, RB_NO_KEYWORDS); }
else    { /* NOTE: calling method_missing */
          b = rb_check_convert_type_with_id(proc, T_DATA, "Proc", idTo_proc); }

rb_raise(rb_eTypeError, "can't convert %"PRIsVALUE" to Proc "
                        "(%"PRIsVALUE"#to_proc gives %"PRIsVALUE")", …)
rb_raise(rb_eTypeError, "no implicit conversion of %s into Proc", …)

⛔ The predicate is a method ENTRY, not respond_to?

The obvious spelling is respondsToDynamic(v, "to_proc"). It would have broken three cases that already worked — measured on 4.0.7 before choosing, not after a test went red:

MRI 4.0.7 respond_to?
a private to_proc used → :priv false
to_proc via method_missing + respond_to_missing? used → :mm true
to_proc via method_missing alone used → :mmonly false

MRI uses rb_callable_method_entry (any visibility) and, failing that, rb_check_funcall, which calls method_missing when it is overridden and gives up when it is still BasicObject's. The guard mirrors exactly that: findMethod, else a method_missing whose owner is no longer cBasicObject. All three are now controls in the test, so narrowing the predicate later fails loudly.

An upstream report I did not file

ruby/spec expects can't convert X into Proc; MRI 4.0.9 formats to Proc. Corpus and reference disagree, and both readings were verified — the C source read verbatim, the 4.0.7 binary run. The conclusion would still have been wrong:

# CRuby < 4.1 has inconsistent coercion errors:
# https://bugs.ruby-lang.org/issues/21864
# This matcher ignores the message on CRuby < 4.1
if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1"
  message = nil

A deliberate waiver, not an oversight: CRuby 4.0's wording is known wrong, fixed in 4.1, and the message check is skipped for CRuby only. rbgo reports RUBY_ENGINE == "ruby" and RUBY_VERSION == "4.0.9", so the waiver covers it — which spec_helper.rb:350 already records. Implementing 4.0.9's exact wording is therefore correct and there is nothing to report upstream.

Measured

cmd/oracle against /opt/homebrew/opt/ruby@4.0/bin/ruby (ruby 4.0.7 … +PRISM [arm64-darwin25]): both probes AGREE, byte for byte across nine rows, where the control differed on three.

10 table rows in internal/vm/block_pass_to_proc_test.go, three of them the respond_to? controls, plus &nil, &:symbol and &proc as pass-through controls.

Refs #799

🤖 Generated with Claude Code

…sages

language/send_spec.rb: pass=74 fail=4 -> pass=75 fail=3.

    S.makeproc(&Object.new)
    # rbgo: NoMethodError: undefined method 'to_proc' for an instance of Object
    # MRI:  TypeError: no implicit conversion of Object into Proc

toBlock reached to_proc through vm.send unconditionally, so an object with no
to_proc at all surfaced the dispatch failure instead of a conversion failure.

## ⛔ The class was never NoMethodError, so this is not only a 4.0 move

Measured on three engines:

    MRI 2.6.10  TypeError: wrong argument type Object (expected Proc)
    MRI 4.0.7   TypeError: no implicit conversion of Object into Proc
    rbgo        NoMethodError: undefined method 'to_proc' ...

2.6 already answered TypeError. What 4.0 changed is the WORDING. The wrong
class was a plain defect; only the two messages belong to #799.

## Bibliography

vm_to_proc (vm_args.c at v4.0.9) looks the method up FIRST and branches, which
is why there are two messages and not one:

    me = rb_callable_method_entry_with_refinements(CLASS_OF(proc), idTo_proc, NULL);
    if (me) { b = rb_vm_call0(GET_EC(), proc, idTo_proc, 0, NULL, me, RB_NO_KEYWORDS); }
    else    { /* NOTE: calling method_missing */
              b = rb_check_convert_type_with_id(proc, T_DATA, "Proc", idTo_proc); }

    rb_raise(rb_eTypeError, "can't convert %"PRIsVALUE" to Proc "
                            "(%"PRIsVALUE"#to_proc gives %"PRIsVALUE")", ...)
    rb_raise(rb_eTypeError, "no implicit conversion of %s into Proc", ...)

## ⛔ The predicate is a METHOD ENTRY, not respond_to?

The obvious spelling is `respondsToDynamic(v, "to_proc")`. It would have broken
three cases that ALREADY WORKED, measured on 4.0.7 before choosing:

    a PRIVATE to_proc                                 -> used, :priv
    to_proc via method_missing + respond_to_missing?  -> used, :mm
    to_proc via method_missing ALONE                  -> used, :mmonly

respond_to? is false for the first and the third. MRI uses
rb_callable_method_entry (any visibility) and, failing that, rb_check_funcall,
which calls method_missing when it is overridden and gives up when it is still
BasicObject's. The guard mirrors that: findMethod, else a method_missing whose
owner is no longer cBasicObject. All three are controls in the test.

## An upstream report I did NOT file

ruby/spec expects "can't convert X into Proc" where MRI 4.0.9 formats "to Proc",
so the corpus and the reference disagree. That is deliberate, not a bug:

    # CRuby < 4.1 has inconsistent coercion errors:
    # https://bugs.ruby-lang.org/issues/21864
    # This matcher ignores the message on CRuby < 4.1
    if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1"
      message = nil

CRuby 4.0's wording is known-wrong and fixed in 4.1; the check is waived for
CRuby only. rbgo reports RUBY_ENGINE "ruby" and RUBY_VERSION "4.0.9", so the
waiver covers it -- as spec_helper.rb:350 already records. Implementing 4.0.9's
exact wording is therefore right, and there is nothing to report upstream.

## Measured

cmd/oracle against /opt/homebrew/opt/ruby@4.0/bin/ruby: both probes AGREE byte
for byte (9 rows). 10 table rows, 3 of them the respond_to? controls.

Refs #799

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled

Copy link
Copy Markdown
Contributor Author

The corpus sweep

PROVENANCE
  control   rbgo-ctl906 — built from 0b8ea8ff3658   (main, with #904)
  treatment rbgo-trt906 — built from d52241bdaf4b   (this branch)
MOVE none declared: both engines read the same corpus, so every delta below is the ENGINE

file                                                     control   treat  delta
language/send_spec.rb                                         74      75     +1

2206 file(s): control 23699 passing, treatment 23700 passing, +1
1 file(s) moved, 0 regressed

-dir core,language -move none -timeout 5m, every file printed. Clean on every channel specdelta reports:

control result
KILLED on timeout 0 — no count was decided by the clock
regressed 0
example(s) WENT RED 0
UNSTABLE 0
fewer detail lines than own fail+error tally 0

One file moved and it is the one this PR is about. +1 over 2206 files.

Why that is the expected shape, and not a disappointment

The sibling example — "raises TypeError if to_proc doesn't return a Proc" — was already passing before this change, with a message that did not match MRI at all:

rbgo (before):  TypeError: no implicit conversion of RawToProc into Proc
MRI 4.0.7:      TypeError: can't convert RawToProc to Proc (RawToProc#to_proc gives Integer)

It passed because raise_consistent_error waives the message for CRuby below 4.1 and rbgo reports RUBY_ENGINE == "ruby", RUBY_VERSION == "4.0.9". So the corpus could not see that message being wrong, and fixing it cannot show up as a +1.

That is worth stating rather than quietly banking the +1: this PR corrects two messages and the corpus can only count one of them. The second is covered by the table rows and by cmd/oracle agreeing byte for byte, not by the sweep.

The three rows the sweep also cannot see

A private to_proc, and a to_proc reached through method_missing with and without respond_to_missing?, all worked before and after. No spec example exercises them, and respond_to? — the obvious predicate — is false for two of the three. They are controls in internal/vm/block_pass_to_proc_test.go precisely because nothing else in the repository would have caught narrowing the predicate.

CI: 26/26 green.

🤖 Generated with Claude Code

@tannevaled
tannevaled merged commit ac87a75 into main Nov 7, 2026
26 checks passed
@tannevaled
tannevaled deleted the toproc-typeerror branch November 7, 2026 10:42
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