Skip to content

mruby-regexp: add Symbol#match, #match? and #=~ - #6993

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:symbol-match
Aug 2, 2026
Merged

mruby-regexp: add Symbol#match, #match? and #=~#6993
matz merged 1 commit into
mruby:masterfrom
takumin:symbol-match

Conversation

@takumin

@takumin takumin commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

CRuby defines match, match? and =~ on Symbol so that a symbol can be
matched against a regexp without spelling out the to_s; mruby-regexp
provided none of them, so any regexp use on a symbol raised NoMethodError.

:abc.match?(/b/)   # CRuby: true,  mruby: NoMethodError
:abc =~ /b/        # CRuby: 1,     mruby: NoMethodError

CRuby implements all three as the String method applied to the symbol's name
(rb_sym2str then rb_str_match_m / rb_str_match), so they delegate to
to_s rather than repeat the pattern handling. That inherits the String
pattern compilation, the pos argument, the block form, and the TypeError
for a String argument to =~. $~ and $1-$9 are set by the engine
itself, so delegating does not lose them.

Symbol#match needs the block to survive the delegation, which #6991 made
String#match do.

Delegating also inherits one difference from CRuby that is worth naming: for
an argument that is neither a Regexp nor a String, String#=~ dispatches
re =~ self, so :a =~ nil raises NoMethodError where CRuby returns nil.
Fixing that belongs to String#=~, not to the Symbol wrapper.

The argument type of match and match? is inherited the same way, and
there it interacts with #6994. String#match currently hands self to
anything that is not a String, so once Symbol#match exists the receiver
and the argument swap places and a symbol pattern quietly matches instead of
raising:

:abc.match(:b)   # without #6994: nil       (runs :b.to_s.match("abc"))
                 # with #6994:    TypeError (CRuby raises it too)
:abc.match?(:b)  # likewise false, then TypeError

#6994 adds that check in one helper on String, which both receivers go
through, so this PR needs no code for it. Merging #6994 first closes the
window; merging this one first opens it for as long as the two are apart.
The tests here therefore assert nothing about the argument type, and the
rows above are covered by #6994's tests instead.

This covers the symbol-on-the-left direction only. The Regexp side still
takes strings only, and rejects symbols in every entry point rather than just
in #===:

/l/ =~ :hello        # TypeError (CRuby: 2)
/l/.match(:hello)    # TypeError (CRuby: MatchData)
/l/.match?(:hello)   # TypeError (CRuby: true)
/l/ === :hello       # false     (CRuby: true)
[:to_s, :abc].grep(/^to_/)  # [] -- Enumerable#grep goes through Regexp#===

sym[/re/] is a third gap: mruby-symbol-ext already delegates Symbol#[]
to String#slice, but this gem does not implement the regexp form of
String#[] / #slice, so "hello"[/l+/] does not work either. Both are
fixes on the String and Regexp side, so they are left alone here; the README
Limitations section and the comment in symbol_regexp.rb now spell them out.

rake test passes.

Summary by CodeRabbit

  • New Features
    • Added regular-expression matching methods to symbols: match, match?, and =~.
    • Supports matching against symbol text, including positions, offsets, blocks, and multibyte symbols.
  • Documentation
    • Documented supported symbol regular-expression behavior and current limitations.
  • Tests
    • Added comprehensive coverage for symbol matching, match state, edge cases, and invalid operations.

CRuby defines `match`, `match?` and `=~` on Symbol so that a symbol can be
matched against a regexp without spelling out the `to_s`; mruby-regexp
provided none of them, so any regexp use on a symbol raised NoMethodError.

```ruby
:abc.match?(/b/)   # CRuby: true,  mruby: NoMethodError
:abc =~ /b/        # CRuby: 1,     mruby: NoMethodError
```

CRuby implements all three as the String method applied to the symbol's name
(`rb_sym2str` then `rb_str_match_m` / `rb_str_match`), so they delegate to
`to_s` rather than repeat the pattern handling. That inherits the String
pattern compilation, the `pos` argument, the block form, and the `TypeError`
for a String argument to `=~`. `$~` and `$1`-`$9` are set by the engine
itself, so delegating does not lose them.

`Symbol#match` needs the block to survive the delegation, which mruby#6991 made
`String#match` do.

Delegating also inherits one difference from CRuby that is worth naming: for
an argument that is neither a Regexp nor a String, `String#=~` dispatches
`re =~ self`, so `:a =~ nil` raises NoMethodError where CRuby returns nil.
Fixing that belongs to `String#=~`, not to the Symbol wrapper.

This covers the symbol-on-the-left direction only. The Regexp side still
takes strings only, and rejects symbols in every entry point rather than just
in `#===`:

```ruby
/l/ =~ :hello        # TypeError (CRuby: 2)
/l/.match(:hello)    # TypeError (CRuby: MatchData)
/l/.match?(:hello)   # TypeError (CRuby: true)
/l/ === :hello       # false     (CRuby: true)
[:to_s, :abc].grep(/^to_/)  # [] -- Enumerable#grep goes through Regexp#===
```

`sym[/re/]` is a third gap: `mruby-symbol-ext` already delegates `Symbol#[]`
to `String#slice`, but this gem does not implement the regexp form of
`String#[]` / `#slice`, so `"hello"[/l+/]` does not work either. Both are
fixes on the String and Regexp side, so they are left alone here; the README
Limitations section and the comment in symbol_regexp.rb now spell them out.
@takumin
takumin requested a review from matz as a code owner August 2, 2026 14:42
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds Symbol#match, Symbol#match?, and Symbol#=~. These methods delegate to the symbol’s string representation. Tests cover matching behavior, and the README documents supported and unsupported Symbol/Regexp operations.

Changes

Symbol regexp matching

Layer / File(s) Summary
Add and validate Symbol regexp methods
mrbgems/mruby-regexp/mrblib/symbol_regexp.rb, mrbgems/mruby-regexp/test/symbol_regexp.rb, mrbgems/mruby-regexp/README.md
Symbol delegates match, match?, and =~ to its string representation. Tests cover positions, blocks, match state, operators, errors, and UTF-8 offsets. The README documents supported and unsupported operand directions.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Symbol
  participant String
  participant Regexp
  Symbol->>String: convert symbol to string
  String->>Regexp: perform match operation
  Regexp-->>String: return match result
  String-->>Symbol: return delegated result
Loading

Possibly related PRs

  • mruby/mruby#6991: Both changes forward regexp match blocks for Symbol and String.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the three Symbol methods added by the 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 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

A note on merge order with #6994, which adds a TypeError for a non-Regexp
pattern in String#match and #match?.

The two PRs touch disjoint files, so they merge in either order without a
conflict. The order still matters for behaviour. This PR defines
Symbol#match, which gives a symbol pattern a receiver to dispatch to, so
"abc".match(:b) starts resolving to :b.to_s.match("abc") and quietly
returns nil where it used to raise NoMethodError:

"abc".match(:b)  # before:            NoMethodError
                 # after this PR:     nil
                 # after #6994:       TypeError

Merging #6994 first avoids that window. Nothing here needs to change either
way, so this is only about which one goes in first.

matz pushed a commit that referenced this pull request Aug 2, 2026
`String#match` and `#match?` converted a `String` argument to a `Regexp`
and handed `self` straight to anything else, so a pattern of the wrong
type reached a method call it does not respond to.  The failure named the
argument as the receiver, which says nothing about the pattern being
wrong.

```ruby
"abc".match(:b)   # CRuby: TypeError, mruby: NoMethodError for :b
"abc".match(nil)  # CRuby: TypeError, mruby: NoMethodError for nil
```

Resolve the pattern in one helper so both methods reject the same set,
and follow CRuby in naming `nil`, `true` and `false` by value and
everything else by class.  The check runs before `pos` is inspected,
matching CRuby's order.  The helper is private, so the gem does not grow
`String`'s public API with a method only `match` and `match?` call.

```ruby
"abc".match(:b)       # TypeError: wrong argument type Symbol (expected Regexp)
"abc".match(nil, 99)  # TypeError on the pattern, not an error about pos
```

CRuby also accepts an object that defines `to_str`, converting it through
`rb_check_string_type` before the type check.  mruby has no implicit
`String` conversion in core, so `"a" + obj` and `"abc".index(obj)`
already raise `TypeError` for such an object; honouring `to_str` in
`match` alone would make the gem more permissive than the core it sits
on.  Rejecting `to_str` objects therefore stays an intentional
difference from CRuby.

`String#=~` deliberately keeps its current behaviour.  CRuby's
`rb_str_match` special-cases `String` only and dispatches everything else
as `other =~ self`.

```ruby
"abc" =~ 1    # CRuby: NoMethodError, mruby: NoMethodError
"abc" =~ nil  # CRuby: nil,           mruby: NoMethodError
```

A blanket type check would turn both into `TypeError`, losing the case
mruby already matches and freezing the other in a state it can never grow
out of.  `Object#=~` was removed in Ruby 3.2, so `nil` is the only
receiver left to dispatch to, and the NoMethodError becomes CRuby's `nil`
the moment core gains `NilClass#=~`.  That is a core fix rather than one
for this gem.

This becomes more urgent once `Symbol#match` exists (#6993).
`Symbol#match` and `#match?` delegate to `to_s`, so without the check the
receiver and the argument swap places and the call quietly succeeds.

```ruby
"abc".match(:b)  # without the check: :b.to_s.match("abc") -> nil
                 # with the check:    TypeError
```
@matz
matz merged commit 121acd1 into mruby:master Aug 2, 2026
21 checks passed
@takumin
takumin deleted the symbol-match branch August 2, 2026 22:54
takumin added a commit to takumin/mruby that referenced this pull request Aug 2, 2026
CRuby matches a Symbol against its name wherever a Regexp is given a subject,
through `reg_operand()`. mruby-regexp took strings only, so all four entry
points refused a symbol:

```ruby
/a/ =~ :ab       # CRuby: 0,         mruby: TypeError
/a/.match(:ab)   # CRuby: MatchData, mruby: TypeError
/a/.match?(:ab)  # CRuby: true,      mruby: TypeError
/a/ === :ab      # CRuby: true,      mruby: false
```

`#===` is the one that matters most. It answers false instead of raising, so a
`case` over symbols quietly falls through to `else` and `Enumerable#grep`
returns an empty array, with nothing to show that the pattern was never given
a chance to run.

Convert the operand with `mrb_sym_str()` in one helper and call it from
`regexp_match()`, `regexp_match_p()` and `regexp_match_op()` in place of
`mrb_ensure_string_type()`. `regexp_case_match()` uses the same helper behind
a type test, because `#===` has to keep answering false for a type it cannot
match rather than start raising. `__byte_match` is left alone: it is internal,
is only reached from mrblib with `self` as the subject, and its argument spec
already pins the type.

For a symbol too long for the inline representation, `mrb_sym_str()` returns
an `mrb_str_new_static()` string sharing the symbol table's buffer. Mutating
the `MatchData#string` that comes out of one is safe, because `str_modify()`
copies an `RSTR_NOFREE` buffer before writing, but a test pins that.

This is the Regexp-side half of the symbol support mruby#6993 adds on the Symbol
side, and neither closes the other. mruby#6993 delegates `Symbol#match` and friends
through `to_s`, so it never reaches the C code changed here; the two are
independent and can land in either order. If mruby#6993 lands first, its
"Symbols only on the left of a match" entry in the gem README becomes stale
and should be dropped as part of the merge. `sym[/re/]` stays a third gap,
waiting on the regexp form of `String#[]`.
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