Skip to content

mruby-regexp: document what a pattern still decides in the String overrides - #7067

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:string-regexp-dispatch-note
Aug 10, 2026
Merged

mruby-regexp: document what a pattern still decides in the String overrides#7067
matz merged 1 commit into
mruby:masterfrom
takumin:string-regexp-dispatch-note

Conversation

@takumin

@takumin takumin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The overrides in mrbgems/mruby-regexp/mrblib/string_regexp.rb take care not to let an
argument lie about its type: Regexp === pattern reads the real class, and
Regexp.__check_pattern makes the accept-or-reject decision in C so that no Ruby-side
helper can be swapped out under it. Having established what the object is, every override
then calls a method on it, and those methods are ordinary Ruby-visible methods on
Regexp.

r = Regexp.new("l+")
def r.match(*a); "PWNED"; end

"hello".sub(r) { |m| m.upcase }  # CRuby: "heLLo", mruby: NoMethodError (pre_match)
"hello"[r]                       # CRuby: "ll",    mruby: "P"
s = "hello"; s[r] = "X"; s       # CRuby: "heXo",  mruby: NoMethodError (begin)
"hello".slice!(r)                # CRuby: "ll",    mruby: NoMethodError (begin)

sub's block form reaches the pattern through md.pre_match, where md is whatever
pattern.match(self) returned; the redefined match's "PWNED" string has no
pre_match, so mruby raises NoMethodError instead of substituting garbage. sub! with
a block goes the same way, through sub.

String#[] answers "P" rather than raising: it hands the result of args[0].match(self)
to MatchData#[], and for a String receiver that is String#[] again with an index of 0,
so the answer is the first character of the string the redefined match returned.
String#slice is a second entry for the same method. []= and slice! fetch the match
the same way and then ask it for begin, which a String does not have, so those raise.

gsub and split do not consult match for their result, so a redefined match leaves
them alone. They reach the pattern through __byte_match instead, which is redefinable in
exactly the same way, and redefining that one gives them a NoMethodError on
__byte_begin. gsub! calls pattern.match(self) only for its truthiness, so a
redefined match returning any true value leaves it working; scan hands the whole job
to pattern.__scan, and the replacement-string forms of sub and gsub to
pattern.__sub_str and pattern.__gsub_str.

The surface is every method the overrides call on a pattern or on the MatchData that
pattern handed back:

  • on the pattern: match (from match, sub, sub!, gsub!, [], []= and slice!),
    match? (from match?), =~ (from =~), __byte_match (from gsub and split),
    __sub_str, __gsub_str and __scan.
  • on the MatchData: [], pre_match, post_match, begin, end, size, length,
    __byte_begin, __byte_end and __set_globals.

All of them are defined with mrb_define_method(); the __ prefix is a naming
convention, not a protection.

CRuby is open in one of these places and deliberately so: rb_str_match_m() dispatches
match to the pattern on purpose, which is why "hello".match(r) answers "PWNED" there
as well. Everywhere else it is closed. rb_str_sub_bang(), rb_str_subpat(),
rb_str_subpat_set() and rb_str_slice_bang() call rb_reg_search() directly, and
rb_str_match_m_p() and rb_str_match() search a real Regexp without asking it anything,
so String#match? and String#=~ are closed there while the overrides here dispatch. A
Regexp created with Regexp.new is not frozen in either implementation, so this needs no
unusual setup.

Why a comment rather than a fix

It takes deliberate sabotage to observe. Redefining Regexp#match on an instance you then
pass to String#sub or String#[] is not something a program does by accident, and
nothing here is reachable from ordinary input: an argument that is not a Regexp never
reaches these calls, because the type test in front of them cannot be steered. That
distinction is the point, and the existing comments do not draw it. They claim only that
the argument cannot pose as a Regexp, which is true, but placed in front of a search they
read as though accepting the argument settled everything the pattern can influence.

The alternative is to give the overrides a C entry point that takes a pattern and a string
and searches, without going through any Ruby-visible method on the pattern. That is a
larger change than it sounds: sub, gsub, split, scan, [], []= and slice!
would each need one, and the block forms have to stay in mrblib because they call back
into Ruby. It also buys nothing against any input the gem actually receives.

So this PR writes the position down instead:

  • a note at the top of the file listing what an accepted pattern still decides, where
    CRuby stands on each, and why the gem accepts it;
  • one added sentence in String#[], so its type-test comment stops reading as a claim
    about the search that follows it.

CodeRabbit raised the String#[] half of this on #7054 as a review comment, asking that
the regexp branch call the built-in Regexp#match directly. That is the C entry point
option applied to one method; it was left alone there on the grounds that the rest of the
file has the same shape and the question belongs to the whole gem. This is the answer to
that question.

Testing

Comment-only, so behaviour is unchanged. rake test passes (2008 tests, 0 failures, 0
crashes). Every result above was reproduced on this branch and against CRuby 4.0.6.

Summary by CodeRabbit

  • Documentation
    • Clarified how String regexp methods validate patterns and dispatch operations.
    • Documented how rewritten Regexp instance and singleton methods can influence results after type validation.
    • Expanded String#[] documentation to distinguish implementation selection from subsequent pattern behavior.
    • Added notes describing behavior differences compared with CRuby and clarifying how pattern and match-data operations are dispatched.

@takumin
takumin requested a review from matz as a code owner August 10, 2026 08:30
@coderabbitai

coderabbitai Bot commented Aug 10, 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: 6007f551-4620-4626-8fd2-090e9c6493ff

📥 Commits

Reviewing files that changed from the base of the PR and between d7406f4 and 8be3a94.

📒 Files selected for processing (1)
  • mrbgems/mruby-regexp/mrblib/string_regexp.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • mrbgems/mruby-regexp/mrblib/string_regexp.rb

📝 Walkthrough

Walkthrough

The change expands documentation for Regexp validation, Ruby-level method dispatch, singleton overrides, CRuby differences, and String#[] implementation selection in String regexp overrides.

Changes

Regexp Override Documentation

Layer / File(s) Summary
Validation and dispatch documentation
mrbgems/mruby-regexp/mrblib/string_regexp.rb
Comments document Regexp validation, post-validation method dispatch, singleton overrides, CRuby differences, and String#[] implementation selection.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • mruby/mruby#6994: Modifies related Regexp validation and dispatch behavior in String regexp overrides.
  • mruby/mruby#7001: Adds pattern-validation changes documented by this PR.
  • mruby/mruby#7054: Introduces the String#[] override behavior documented by this PR.

Suggested reviewers: matz

🚥 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 and concisely describes the main documentation change in the String regexp overrides.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

@takumin
takumin force-pushed the string-regexp-dispatch-note branch from d7406f4 to eb65911 Compare August 10, 2026 08:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mrbgems/mruby-regexp/mrblib/string_regexp.rb`:
- Around line 1-4: Revise the introductory validation and reachability comments
to cover only the overrides that actually call Regexp.__check_pattern. Document
that String#=~ directly rejects String, while String#[], String#[]=, and
String#slice! first use a Regexp === guard; also note that __check_pattern
accepts String patterns, which may reach later dispatch after compilation or
quoting.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ffd6ce1-3c17-49c9-8093-e7824365cf64

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad6907 and d7406f4.

📒 Files selected for processing (1)
  • mrbgems/mruby-regexp/mrblib/string_regexp.rb

Comment thread mrbgems/mruby-regexp/mrblib/string_regexp.rb Outdated
@takumin takumin changed the title Document what a pattern still decides in the String regexp overrides mruby-regexp: document what a pattern still decides in the String overrides Aug 10, 2026
…rrides

The overrides in `mrbgems/mruby-regexp/mrblib/string_regexp.rb` take care not
to let a pattern argument lie about its type, and the comments say so:
`Regexp.__check_pattern` makes the accept-or-reject decision in C, and `[]`,
`[]=` and `slice!` read the real class with `Regexp ===` before taking the
regexp path at all. What the comments leave unsaid is that the searches those
overrides then perform are ordinary sends to the pattern and to the MatchData
it hands back, so a singleton method on the pattern changes the answer:

```ruby
r = Regexp.new("l+")
def r.match(*a); "PWNED"; end

"hello".sub(r) { |m| m.upcase }  # CRuby: "heLLo", mruby: NoMethodError (pre_match)
"hello"[r]                       # CRuby: "ll",    mruby: "P"
s = "hello"; s[r] = "X"; s       # CRuby: "heXo",  mruby: NoMethodError (begin)
"hello".slice!(r)                # CRuby: "ll",    mruby: NoMethodError (begin)
```

`gsub` and `split` reach the pattern through `__byte_match` rather than
`match`, so the same rewrite there gives them a `NoMethodError` on
`__byte_begin`; `scan`, `sub` and `gsub` with a replacement string go through
`__scan`, `__sub_str` and `__gsub_str`. The `__` prefix is a naming
convention, not a protection.

CRuby dispatches `match` from `String#match` on purpose and answers `"PWNED"`
there too, but `rb_str_sub_bang()`, `rb_str_subpat()`, `rb_str_subpat_set()`
and `rb_str_slice_bang()` call `rb_reg_search()` directly, and `String#match?`
and `String#=~` search a real Regexp without asking it anything.

Reaching any of this takes rewriting a method on a Regexp instance and then
handing that instance to a String method. An argument that is not a Regexp
never gets as far as the searches, because the type test in front of them
cannot be steered. The gem accepts the wider surface rather than giving every
override a C entry point that searches without dispatching, so record that
decision in a note at the top of the file, and stop the type-test comment in
`String#[]` from reading as though accepting the argument settled everything
the pattern can still influence.
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