Skip to content

mruby-regexp: fix MatchData#begin / #end for a group name and an out-of-range index - #7048

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:matchdata-begin-end-group-name
Aug 9, 2026
Merged

mruby-regexp: fix MatchData#begin / #end for a group name and an out-of-range index#7048
matz merged 1 commit into
mruby:masterfrom
takumin:matchdata-begin-end-group-name

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

MatchData#begin and #end take their argument with mrb_get_args(mrb, "i", &idx),
so a group name is a TypeError before the method body runs, and an index that names
no group returns nil instead of raising.

md = /(?<_x>a)/.match("a")
md.begin(:_x)  # CRuby: 0,  mruby: TypeError (Symbol cannot be converted to Integer)
md.begin("_x") # CRuby: 0,  mruby: TypeError (String cannot be converted to Integer)
md.begin(:zz)  # CRuby: IndexError (undefined group name reference: zz)
               # mruby: TypeError (Symbol cannot be converted to Integer)

md = /(a)(b)/.match("ab")
md.begin(3)    # CRuby: IndexError (index 3 out of matches),  mruby: nil
md.begin(-1)   # CRuby: IndexError (index -1 out of matches), mruby: nil
md.end(-1)     # CRuby: IndexError (index -1 out of matches), mruby: nil

Both halves matter for the same reason: begin and end return an offset, and nil
is not one. Arithmetic on the result of a mistyped index fails somewhere else entirely,
while CRuby stops at the call.

What changes

matchdata_begin() and matchdata_end() take their argument as "o" and resolve it
before reading the capture:

  • a String or Symbol resolves through the pattern's named-capture table, raising
    IndexError with undefined group name reference: NAME when it is not there
  • an Integer outside 0...num_captures, negative included, raises IndexError with
    index N out of matches

This is deliberately stricter than MatchData#[], and CRuby is stricter here too:
[] returns nil for an index out of range because a group that did not participate
is also nil, whereas begin has no such value to return. []'s rules are untouched,
including the negative index it normalizes and the nil it returns out of range.

A group that exists but did not participate is still nil, which is the one result
that must not become a raise:

/(a)|(b)/.match("a").begin(2)   # both: nil

The name lookup was the loop matchdata_aref() already ran. It moves into a shared
matchdata_name_to_group() and gains begin and end as callers. The
RE_NAME_LEN_FITS() bound travels with it rather than staying behind: it is what keeps
the (uint32_t)name_len cast in the loop lossless, so begin and end get the
over-long name rejected for free. The IndexError for an unknown name moves with it as
well, since all three methods want the same message.

__byte_begin and __byte_end stay on "i" and keep returning nil. They are private,
are called only as md.__byte_begin(0) from String#gsub and #split in
mrblib/string_regexp.rb, and should not pay for the lookup on every iteration.

The bindings do not change: begin and end were already MRB_ARGS_REQ(1), which is
correct for an argument that is now an object rather than an integer.

Verification

Before:

$ ./build/host/bin/mruby -e 'p /(?<_x>a)/.match("a").begin(:_x)'
-e:1:in begin: Symbol cannot be converted to Integer (TypeError)
$ ./build/host/bin/mruby -e 'p /(a)(b)/.match("ab").begin(-1)'
nil

After, matching CRuby 4.0.6 on every line of the table above:

$ ./build/host/bin/mruby -e 'p /(?<_x>a)/.match("a").begin(:_x)'
0
$ ./build/host/bin/mruby -e 'p /(a)(b)/.match("ab").begin(-1)'
-e:1:in begin: index -1 out of matches (IndexError)

Tests

mrbgems/mruby-regexp/test/regexp.rb gains three blocks: the name cases beside the
existing MatchData#begin / #end assertion, the out-of-range cases including the
non-participating group that must stay nil, and the over-long name beside the
MatchData#[] regression test for the same bound, now that the lookup is shared.

rake test passes: 1970 assertions, 0 failures, no new compiler warnings.

Summary by CodeRabbit

  • New Features

    • MatchData#begin and MatchData#end now support named capture groups using strings or symbols.
    • Named captures can be resolved consistently across match data access methods.
  • Bug Fixes

    • Invalid capture names and indexes now raise IndexError.
    • Unmatched but valid capture groups continue to return nil.
    • Added support for correctly handling very long unknown capture names.

…out-of-range index

`matchdata_begin()` and `matchdata_end()` took their argument with
`mrb_get_args(mrb, "i", &idx)`, so a group name was a `TypeError` before the
method body ran, and an index that named no group returned `nil` instead of
raising.

```ruby
md = /(?<_x>a)/.match("a")
md.begin(:_x)  # CRuby: 0,  mruby: TypeError (Symbol cannot be converted to Integer)
md.begin("_x") # CRuby: 0,  mruby: TypeError (String cannot be converted to Integer)
md.begin(:zz)  # CRuby: IndexError (undefined group name reference: zz)
               # mruby: TypeError (Symbol cannot be converted to Integer)

md = /(a)(b)/.match("ab")
md.begin(3)    # CRuby: IndexError (index 3 out of matches),  mruby: nil
md.begin(-1)   # CRuby: IndexError (index -1 out of matches), mruby: nil
md.end(-1)     # CRuby: IndexError (index -1 out of matches), mruby: nil
```

`begin` and `end` return an offset, and `nil` is not one, so an argument they
cannot use is an error rather than a missing result. That is stricter than
`MatchData#[]` on purpose: `[]` has `nil` to return for a group that did not
participate and reuses it for an index out of range, while `begin` has no such
value to return.

Both now take their argument as `"o"`. A String or Symbol resolves through the
pattern's named-capture table, and an Integer outside `0...num_captures`,
negative included, raises `IndexError`. A group that exists but did not
participate still returns `nil`:

```ruby
/(a)|(b)/.match("a").begin(2)   # both: nil
```

The name lookup is the loop `matchdata_aref()` already ran, so it moves into a
shared `matchdata_name_to_group()`, together with the `RE_NAME_LEN_FITS()` bound
that keeps its `memcmp()` from being handed a length larger than what was
measured, and with the `IndexError` all three methods want. Nothing about
`MatchData#[]` changes: its negative-index rule and its `nil` for an index out
of range are its own and stay where they are.

`__byte_begin` and `__byte_end` stay on `"i"`. They are private helpers for
`String#gsub` and `#split`, are only ever called with the literal `0`, and
should not pay for the lookup.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 14:14
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 76628c14-632b-43c5-9502-4a370bbf9136

📥 Commits

Reviewing files that changed from the base of the PR and between 9360b3f and bbeaf85.

📒 Files selected for processing (2)
  • mrbgems/mruby-regexp/src/regexp.c
  • mrbgems/mruby-regexp/test/regexp.rb

📝 Walkthrough

Walkthrough

MatchData now shares named-capture resolution across [], begin, and end. The latter methods accept string and symbol names, raise IndexError for invalid arguments, and return nil for valid unmatched groups. Tests cover normal and oversized names.

Changes

MatchData named capture access

Layer / File(s) Summary
Shared capture-name resolution
mrbgems/mruby-regexp/src/regexp.c
Centralizes string and symbol capture-name lookup and reuses it from MatchData#[]. Undefined names raise IndexError.
begin and end argument support
mrbgems/mruby-regexp/src/regexp.c, mrbgems/mruby-regexp/test/regexp.rb
MatchData#begin and #end accept named captures and validate integer indices. Tests cover string and symbol names, invalid names and indices, unmatched groups, and oversized names.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: mrbgems

Suggested reviewers: matz, nattzn

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes to MatchData#begin and #end for named groups and out-of-range indexes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants