You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
mruby-regexp's String#split override converts a non-Integer limit by asking the
argument for to_int:
# mrbgems/mruby-regexp/mrblib/string_regexp.rb:137-144iflimit.respond_to?(:to_int)limit=limit.to_intunlesslimit.is_a?(Integer)raiseTypeError,"no implicit conversion of #{limit.class} to Integer)"endelselimit=limit.__to_intend
This is the only place in mruby's own Ruby code that dispatches to_int as an implicit
conversion. Nothing in core does it, and the C implementation this override replaces does
not either:
o=Object.newdefo.to_int2end"a,b,c".split(",",o)# ["a", "b,c"] <- the override"a,b,c".__split(",",o)# TypeError <- the core C split it aliased awayArray.new(o)# TypeError[1,2,3][o]# TypeError"ab" * o# TypeError
CRuby accepts the object in every one of those. mruby accepts it in exactly one, and that
one is a gem override.
Why this is worth asking about rather than just fixing
The gem is more permissive than the core it sits on, which is the situation mruby/mruby#6994 settled the other way for the pattern argument. String#match resolves
its pattern with a plain type check rather than rb_check_string_type's to_str protocol,
on the grounds that mruby has no implicit String conversion in core at all, and matching
CRuby there would make the gem more permissive than its host. That decision is pinned with a
test (599856dfe).
The to_int dispatch predates that decision. It arrived in b61e44b35
("mruby-regexp: coerce String#split limit argument", 2026-06-25), whose stated purpose was to
stop a non-Integer limit such as nil from reaching limit > 0 and failing with an
unrelated comparison error. Normalizing nil is the part that fixed a real bug; accepting to_int came along with it. The to_str policy was written afterwards, so nothing was
inconsistent at the time.
Symptom 1: the guard can be redefined, and only here
respond_to? is redefinable, so an argument that claims to_int without defining it gets
past the guard and fails inside the conversion:
classLiardefrespond_to?(name,include_all=false)trueendend"a,b,c".split(/,/,Liar.new)# CRuby: TypeError (no implicit conversion of Liar into Integer)# mruby: NoMethodError (undefined method 'to_int' for Liar)
respond_to_missing? reaches the same place, so defining that alone is enough.
CRuby raises TypeError however respond_to? answered, because rb_check_funcall falls
back to method_missing and treats its absence as "not convertible". Note that the
divergence runs one way only: an argument that definesto_int and denies it raises in
both, since CRuby honours a redefined respond_to? too.
Core mruby has no such hole, because it never asks:
So this is not a core limitation the gem inherits. It is the cost of the gem implementing a
conversion protocol its host does not have.
Symptom 2: the error message has no CRuby form to match
Having its own conversion means having its own message for a to_int that returns the wrong
type, and CRuby's two forms do not fit it:
no implicit conversion of X into Integer is what CRuby says when there is no to_int.
can't convert X to Integer (X#to_int gives Y) is what it says when to_int returns the
wrong type, which is this branch.
The override raises no implicit conversion of Float to Integer), which borrows the first
form, fills it with the class the conversion returned rather than the argument's, writes to where CRuby writes into, and closes a paren it never opened. The stray paren is a
separate typo and #7004 drops it; the rest is what this issue is about.
Adopting the second form needs the argument's real class, and the only way to read that from
mrblib is Object#class, which is redefinable exactly like the is_a? beside it:
classMaskeddefto_int;"x";enddefclass;42;endend# with CRuby's wording, from mrblib: can't convert 42 to Integer (42#to_int gives String)
If the to_int dispatch goes, so does this branch and the question with it: the message
becomes mrb_ensure_integer_type()'s, the same one every other index in mruby produces.
The three options, none of which this draft picks
Drop the dispatch.limit = limit.__to_int for every non-Integer, which is mrb_ensure_integer_type() and asks the object nothing. Both symptoms go, the gem stops
being the only to_int caller in the tree, and it lines up with the to_str decision.
Costs the to_int support outright, and deletes four lines of String#split with regexp limit. Moves away from CRuby.
Keep it and close the hole in C. Measured rather than guessed; see the section below.
It closes this hole and opens a different one, so it does not reach CRuby either.
Keep it as it is. The hole needs a redefined respond_to? to reach, which is not
something ordinary code does.
Option 1 is the smallest change and the only one that needs no new C. Option 2 does not
finish the job. Which is wanted is a policy question about how far the gem should go beyond
its host, which is why this is filed as a question.
Option 2 was built and measured, and it does not get there
The obvious shape is a C helper that routes the limit through the core conversion machinery,
which is exempt from the "no VM callback" rule precisely because the dispatch is the
specification for an implicit-conversion protocol:
staticmrb_valueregexp_to_int(mrb_state*mrb, mrb_valueself)
{
mrb_valueval;
mrb_get_args(mrb, "o", &val);
if (mrb_integer_p(val)) returnval;
if (mrb_float_p(val)) returnmrb_ensure_integer_type(mrb, val);
returnmrb_convert_type(mrb, val, MRB_TT_INTEGER, "Integer", "to_int");
}
That does close the hole. mrb_respond_to() calls mrb_obj_respond_to(), which runs mrb_method_search_vm() against the method table, so an argument cannot claim a to_int
it does not define. The redefined-class problem in the message goes with it, since the
type name no longer comes from the object.
It closes it by not calling respond_to? at all, which is the new divergence. CRuby's rb_check_funcalldoes honour a redefined respond_to?, so an object that defines to_int and denies it raises there. Searching the method table converts it instead:
classRespondDenierdefto_int2enddefrespond_to?(name,include_all=false)falseendend"a,b,c".split(/,/,RespondDenier.new)# CRuby: TypeError# mruby today: TypeError <- agrees# with the C helper: ["a", "b,c"] <- no longer agrees
So the C helper trades one disagreement for another, and the one it introduces is a case
that agrees today. The messages do not land on CRuby's either, because %v renders the
value rather than its class:
with the C helper: TypeError: #<Object:0x...> cannot be converted to Integer by #to_int
CRuby: TypeError: can't convert Object to Integer (Object#to_int gives Float)
with the C helper: TypeError: can't convert nil into "Integer"
CRuby: TypeError: no implicit conversion from nil to integer
Where each implementation stands against CRuby, on the two arguments that separate them:
respond_to? claims a missing to_int
respond_to? denies a real to_int
today (mrblib)
NoMethodError
TypeError
option 2 (C helper)
TypeError
converts, no error
CRuby 4.0.6
TypeError
TypeError
Neither column is CRuby's. Getting both needs what rb_check_funcall does, which is to call
a redefined respond_to?, then fall back to method_missing and read its absence as "not
convertible". mruby has no such function: convert_type() (src/object.c) asks mrb_respond_to() and dispatches, with no method_missing step. So a genuinely
CRuby-compatible to_int here is not a mruby-regexp change at all; it needs the protocol
itself in core, which is a much larger question than one gem's split.
Measured on 65812128d against CRuby 4.0.6. The full suite passes with the C helper applied
(1941 / 1923 OK / 0 KO, 105 bintests), which is the point: nothing in the tree currently
covers either behaviour.
Relation to other issues
mruby-regexp: read the real type of String#split's limit #7004 is the is_a? guard on the line above this block: the same shape of problem, but a
fix rather than a question, since it lets an argument pose as an Integer and drive the
split unconverted. It changes only that line and deliberately leaves this block alone. The
two do not conflict, and if option 1 is taken here, the block it preserves disappears
entirely.
mruby-regexp: type-check the sub, gsub, scan, split and =~ pattern #7001 closed the redefinable-guard hole for the pattern argument of sub, gsub, scan and split. This is the same class of problem one argument over, but the fix shape
does not carry across: the pattern check moved to C because it needs no dispatch, and this
one cannot.
Environment / how to reproduce
mruby-regexp is part of default.gembox (via stdlib.gembox), so a plain build is
enough to reproduce:
$ rake
$ ./build/host/bin/mruby -e 'class L; def respond_to?(n, a = false); true; end; end; "a,b".split(/,/, L.new)'-e:1: undefined method 'to_int' for L (NoMethodError)
$ ./build/host/bin/mruby -e 'o = Object.new; def o.to_int; 2; end; p "a,b,c".split(",", o); begin; "a,b,c".__split(",", o); rescue => e; p e; end'["a", "b,c"]#<TypeError: Object cannot be converted to Integer>
Checked on master (6581212, 2026-08-03), compared against CRuby 4.0.6.
mruby-regexp'sString#splitoverride converts a non-Integerlimitby asking theargument for
to_int:This is the only place in mruby's own Ruby code that dispatches
to_intas an implicitconversion. Nothing in core does it, and the C implementation this override replaces does
not either:
CRuby accepts the object in every one of those. mruby accepts it in exactly one, and that
one is a gem override.
Why this is worth asking about rather than just fixing
The gem is more permissive than the core it sits on, which is the situation
mruby/mruby#6994settled the other way for the pattern argument.String#matchresolvesits pattern with a plain type check rather than
rb_check_string_type'sto_strprotocol,on the grounds that mruby has no implicit String conversion in core at all, and matching
CRuby there would make the gem more permissive than its host. That decision is pinned with a
test (
599856dfe).The
to_intdispatch predates that decision. It arrived inb61e44b35("mruby-regexp: coerce String#split limit argument", 2026-06-25), whose stated purpose was to
stop a non-Integer limit such as
nilfrom reachinglimit > 0and failing with anunrelated comparison error. Normalizing
nilis the part that fixed a real bug; acceptingto_intcame along with it. Theto_strpolicy was written afterwards, so nothing wasinconsistent at the time.
Symptom 1: the guard can be redefined, and only here
respond_to?is redefinable, so an argument that claimsto_intwithout defining it getspast the guard and fails inside the conversion:
respond_to_missing?reaches the same place, so defining that alone is enough.CRuby raises
TypeErrorhoweverrespond_to?answered, becauserb_check_funcallfallsback to
method_missingand treats its absence as "not convertible". Note that thedivergence runs one way only: an argument that defines
to_intand denies it raises inboth, since CRuby honours a redefined
respond_to?too.Core mruby has no such hole, because it never asks:
So this is not a core limitation the gem inherits. It is the cost of the gem implementing a
conversion protocol its host does not have.
Symptom 2: the error message has no CRuby form to match
Having its own conversion means having its own message for a
to_intthat returns the wrongtype, and CRuby's two forms do not fit it:
no implicit conversion of X into Integeris what CRuby says when there is noto_int.can't convert X to Integer (X#to_int gives Y)is what it says whento_intreturns thewrong type, which is this branch.
The override raises
no implicit conversion of Float to Integer), which borrows the firstform, fills it with the class the conversion returned rather than the argument's, writes
towhere CRuby writesinto, and closes a paren it never opened. The stray paren is aseparate typo and #7004 drops it; the rest is what this issue is about.
Adopting the second form needs the argument's real class, and the only way to read that from
mrblib is
Object#class, which is redefinable exactly like theis_a?beside it:If the
to_intdispatch goes, so does this branch and the question with it: the messagebecomes
mrb_ensure_integer_type()'s, the same one every other index in mruby produces.The three options, none of which this draft picks
limit = limit.__to_intfor every non-Integer, which ismrb_ensure_integer_type()and asks the object nothing. Both symptoms go, the gem stopsbeing the only
to_intcaller in the tree, and it lines up with theto_strdecision.Costs the
to_intsupport outright, and deletes four lines ofString#split with regexp limit. Moves away from CRuby.It closes this hole and opens a different one, so it does not reach CRuby either.
respond_to?to reach, which is notsomething ordinary code does.
Option 1 is the smallest change and the only one that needs no new C. Option 2 does not
finish the job. Which is wanted is a policy question about how far the gem should go beyond
its host, which is why this is filed as a question.
Option 2 was built and measured, and it does not get there
The obvious shape is a C helper that routes the limit through the core conversion machinery,
which is exempt from the "no VM callback" rule precisely because the dispatch is the
specification for an implicit-conversion protocol:
That does close the hole.
mrb_respond_to()callsmrb_obj_respond_to(), which runsmrb_method_search_vm()against the method table, so an argument cannot claim ato_intit does not define. The redefined-
classproblem in the message goes with it, since thetype name no longer comes from the object.
It closes it by not calling
respond_to?at all, which is the new divergence. CRuby'srb_check_funcalldoes honour a redefinedrespond_to?, so an object that definesto_intand denies it raises there. Searching the method table converts it instead:So the C helper trades one disagreement for another, and the one it introduces is a case
that agrees today. The messages do not land on CRuby's either, because
%vrenders thevalue rather than its class:
Where each implementation stands against CRuby, on the two arguments that separate them:
respond_to?claims a missingto_intrespond_to?denies a realto_intNoMethodErrorTypeErrorTypeErrorTypeErrorTypeErrorNeither column is CRuby's. Getting both needs what
rb_check_funcalldoes, which is to calla redefined
respond_to?, then fall back tomethod_missingand read its absence as "notconvertible". mruby has no such function:
convert_type()(src/object.c) asksmrb_respond_to()and dispatches, with nomethod_missingstep. So a genuinelyCRuby-compatible
to_inthere is not amruby-regexpchange at all; it needs the protocolitself in core, which is a much larger question than one gem's
split.Measured on
65812128dagainst CRuby 4.0.6. The full suite passes with the C helper applied(1941 / 1923 OK / 0 KO, 105 bintests), which is the point: nothing in the tree currently
covers either behaviour.
Relation to other issues
String#split's limit #7004 is theis_a?guard on the line above this block: the same shape of problem, but afix rather than a question, since it lets an argument pose as an Integer and drive the
split unconverted. It changes only that line and deliberately leaves this block alone. The
two do not conflict, and if option 1 is taken here, the block it preserves disappears
entirely.
String#matchand#match?#6994 and599856dfeare the precedent for the policy question, decided in the directionof option 1 for
to_str.sub,gsub,scan,splitand=~pattern #7001 closed the redefinable-guard hole for the pattern argument ofsub,gsub,scanandsplit. This is the same class of problem one argument over, but the fix shapedoes not carry across: the pattern check moved to C because it needs no dispatch, and this
one cannot.
Environment / how to reproduce
mruby-regexpis part ofdefault.gembox(viastdlib.gembox), so a plain build isenough to reproduce:
Checked on master (6581212, 2026-08-03), compared against CRuby 4.0.6.