mruby-regexp: fix MatchData#[] for a negative index and an unknown name - #7000
Merged
Conversation
`MatchData#[]` rejected every negative index, so counting back from the last
group answered nil:
```ruby
md = /(a)(b)/.match("ab")
md[-1] # CRuby: "b", mruby: nil
md[-2] # CRuby: "a", mruby: nil
```
Add the group count to a negative index in the numeric branch, and keep only
the upper bound at `found:`. The name branch reaches that label with a group
number that is already non-negative, so it is unaffected.
The lower bound follows CRuby's `rb_reg_nth_match()`, which discards the
adjusted index unless it is positive rather than merely non-negative. A
negative index therefore never reaches group 0, so `md[-3]` above stays nil
instead of answering the whole match. An out-of-range negative index is nil
like an out-of-range positive one; `IndexError` is not involved.
A name that resolves to no capture group answered nil, so a typo in a named
capture failed somewhere later instead of at the call:
```ruby
/(?<x>a)/.match("a")[:zz] # CRuby: IndexError, mruby: nil
/(?<x>a)/.match("a")["zz"] # CRuby: IndexError, mruby: nil
/(a)/.match("a")[:zz] # CRuby: IndexError, mruby: nil
```
The last row is not an oversight: CRuby raises even when the pattern declares
no named group at all, because the argument still names nothing. Out-of-range
indices keep answering nil, which is what CRuby does for them; only names
raise.
Format the name with `%l` rather than `%v`. `%v` runs the value through
`mrb_obj_as_string()`, which dispatches `to_s`, so a redefined `Symbol#to_s`
could pick the text of an argument check. `%l` copies the bytes and leaves
`matchdata_aref()` free of any call back into the VM.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesMatchData indexing behavior
Estimated code review effort: 3 (Moderate) | ~15–30 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MatchData#[]resolves its argument in two branches, and both answernilforan argument CRuby handles differently: a negative index is rejected outright,
and a name that resolves to no group is treated as a failed match.
Out-of-range positive indices already answer
nilcorrectly and are unchanged.A negative index
matchdata_aref()testedidx < 0at the point where it reads the capturetable and gave up there, so counting back from the last group never worked.
Adding the group count in the numeric branch is the whole fix; the name branch
reaches the same label with a group number that is already non-negative, so
only the upper bound has to stay.
The lower bound follows CRuby's
rb_reg_nth_match(), which discards theadjusted index unless it is positive rather than merely non-negative:
Two consequences are worth stating, because both look like bugs until you read
that:
Both are pinned in the tests.
An unknown group name
A
StringorSymbolthat names no capture group answerednil, which isindistinguishable from a group that matched nothing, so a typo in a named
capture surfaced somewhere far from the mistake. CRuby raises
IndexError, andit does so even when the pattern declares no named group at all:
The message is formatted with
%lrather than%v.%vruns the valuethrough
mrb_obj_as_string(), which dispatchesto_s, so a redefinedSymbol#to_scould choose the text of an argument check.%lcopies the bytesdirectly, which also keeps
matchdata_aref()free of any call back into theVM.
Deliberately not in this change
md[start, length]andmd[range]forms, which CRuby also accepts.[]is defined here withMRB_ARGS_REQ(1), and adding the extra forms is alarger piece of work.
TypeErrormessage for an argument that is neither an index nor a name.md[nil]saysnil cannot be converted to Integerwhere CRuby saysno implicit conversion from nil to integer; that phrasing comes frommrb_as_int()and is mruby-wide, so it is not this gem's to change.MatchData#beginand#end, which take their argument as"i"and soreject a group name outright, and answer
nilwhere CRuby raises. They arethe same class of bug in the neighbouring methods, but their rules differ
(they raise for every index they cannot use, including a negative one), so
they are better handled on their own.
One more heads-up for whoever reviews this: the name lookup a few lines above
compares lengths after truncating the requested length to
uint16_twhile thememcmp()next to it uses the untruncated length, which reads out of boundsfor a name longer than 65535 bytes. That is a memory-safety bug rather than a
compatibility one, it is reachable from plain Ruby, and it is not touched here.
I will send it as a separate one-line change so it can be reviewed and
backported on its own.
Testing
Two
assertblocks inmrbgems/mruby-regexp/test/regexp.rb, one per branch,covering the CRuby rules above including the two counter-intuitive ones. Every
row in this description was checked against CRuby 4.0.6 rather than assumed.
rake testis green on a default host build:bintestis 105 OK, 0 KO. No existing test changed.The two commits are independent and each passes the suite on its own, so
either can be dropped if only one of the two behaviours is wanted.
Summary by CodeRabbit
Bug Fixes
MatchData#[]behavior for negative capture indices.IndexErrorinstead of returningnil.Tests