mruby-regexp: set $&, $`, $' and $+ after a match - #7025
Merged
Conversation
`create_matchdata()` assigns `$~` and `$1` to `$9`, but leaves the four remaining match globals untouched, so they read as `nil` after a match that already holds everything they describe. ```ruby /b(c)/ =~ "abc" $& # CRuby: "bc", mruby: nil $` # CRuby: "a", mruby: nil $' # CRuby: "", mruby: nil $+ # CRuby: "c", mruby: nil ``` Nothing below `regexp.c` treats these names specially. `PM_BACK_REFERENCE_READ_NODE` emits a plain `OP_GETGV` on the reference's own name, so a read of `$&` resolves through the ordinary global table just as `$1` does. They are simply never assigned. Assign them next to the `$1` to `$9` loop, from the same capture array and through the same `re_byte_substr()` helper. `$&`, `` $` `` and `$'` follow from the whole match offsets. The latter two are the expressions `matchdata_pre()` and `matchdata_post()` evaluate, but those are instance methods that need a receiver, so routing the globals through them would mean a method dispatch from C for what one `re_byte_substr()` call does. `$+` is the last group that actually participated, which is not `captures[(num_captures-1)*2]`, because the last group in the pattern is frequently the one that did not match: ```ruby /(a)|(b)/ =~ "a" $+ # "a", from group 1, since group 2 never participated ``` so it needs a backwards scan. A pattern with no groups leaves that scan with nothing to find and yields `nil`, which is what CRuby reports. Clear the four in `clear_match_globals()` alongside `$~` and the `nth_syms` loop. Without that a failed match leaves a stale `$&` visible, which is the same defect in the other direction. `create_matchdata()` is the only point at which a successful match is known together with both the subject and the capture array, and its four call sites inherit the assignment exactly as they inherit `$~` today. `regexp_match_p()` deliberately sets no match globals and keeps setting none. `re_byte_substr()` copies through `mrb_str_new()`, so the four values describe the subject as it was at match time and no later mutation of it can invalidate them. The cost is four `mrb_gv_set()` calls per match on top of the nine the `$1` to `$9` loop already issues, and four strings retained until the next match or the next clear. `` $` `` and `$'` are the ones that matter there, since between them they can pin a copy of nearly the whole subject. The four symbols are interned in `ensure_nth_syms()`, renamed to `ensure_match_syms()` now that it covers more than `$1` to `$9`, so they stay out of the per-match path. Assigning `$~` directly still updates none of them, because they are set only from inside `create_matchdata()`. That is the existing behaviour of `$1` to `$9`, which these four now share rather than diverge from.
|
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)
📝 WalkthroughWalkthroughRegexp matching now supports ChangesRegexp match global variables
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant RegexpMatching
participant MatchData
participant MatchGlobals
RegexpMatching->>MatchData: create match data
MatchData->>MatchGlobals: assign $&, $``, and $'
MatchData->>MatchGlobals: assign the last participating capture to $+
RegexpMatching->>MatchGlobals: clear match globals after a failed match
Possibly related PRs
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 9, 2026
This was referenced Aug 19, 2026
Merged
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.
create_matchdata()assigns$~and$1to$9, but leaves the four remainingmatch globals untouched. They read as
nilafter a match that already holdseverything they describe.
Cause
Nothing below
regexp.ctreats these names specially.PM_BACK_REFERENCE_READ_NODEincodegen.cemits a plainOP_GETGVon thereference's own name, the same opcode
PM_GLOBAL_VARIABLE_READ_NODEemits, so aread of
$&resolves through the ordinary global table just as$1does. Thecompiler side already works; the four are simply never assigned.
Change
Assign them in
create_matchdata(), next to the$1to$9loop, from the samecapture array and through the same
re_byte_substr()helper. That is the onlypoint at which a successful match is known together with both the subject and the
capture array, so its four call sites (
exec_match(),regexp_gsub_str(),regexp_sub_str()andregexp_scan()) inherit the assignment exactly as theyinherit
$~today.$&,$`and$'follow from the whole match offsets. The latter two arethe expressions
matchdata_pre()andmatchdata_post()evaluate, but those areinstance methods that need a receiver, so routing the globals through them would
mean a method dispatch from C for what one
re_byte_substr()call does.$+is the last group that actually participated, which is notcaptures[(num_captures-1)*2], because the last group in the pattern isfrequently the one that did not match:
so it needs a backwards scan from
md->num_captures - 1down to group 1. Apattern with no groups leaves that scan with nothing to find and yields
nil,which is what CRuby reports.
clear_match_globals()sets the four tonilalongside$~and thenth_symsloop. Without that a failed match leaves a stale
$&visible, which is the samedefect in the other direction and is what the existing test for
$1guardsagainst.
ensure_nth_syms()interns the four and is renamed toensure_match_syms(), nowthat it covers more than
$1to$9. Both callers already invoke it, so no newcall site is needed and the interning stays out of the per-match path.
regexp_match_p()deliberately sets no match globals, and the existingRegexp#match? - does not update last matchtest asserts that. It keeps settingnone.
Cost and lifetime
re_byte_substr()allocates throughmrb_str_new()and therefore copies thebytes, so the four values describe the subject as it was at match time and no
later mutation of it can invalidate them.
The cost is four
mrb_gv_set()calls per match on top of the nine the$1to$9loop already issues, and four more strings retained until the next match orthe next clear.
$`and$'are the ones that matter there, since betweenthem they can pin a copy of nearly the whole subject. Globals are a direct GC
root, so
mrb_gv_set()needs no write barrier and none was added.In
regexp_gsub_str()andregexp_scan()the loop'smrb_gc_arena_restore()runs before the trailing
create_matchdata()call, so the new allocations aremade after the arena is already unwound.
Known limit
Assigning
$~directly still updates none of the four, because they are set onlyfrom inside
create_matchdata(). That is the existing behaviour of$1to$9,which these four now share rather than diverge from; closing it would mean
deriving all of them from
$~at read time, which is a separate change.Testing
rake testpasses: 1950 tests, 0 failures, 0 crashes. It also passes withMRB_UTF8_STRINGenabled (1970 tests, 0 failures), where the four report thesame multibyte substrings as CRuby:
Behaviour checked against CRuby 4.0.6.
Summary by CodeRabbit
New Features
Bug Fixes