Skip to content

Add NilClass#=~ - #6999

Merged
matz merged 2 commits into
mruby:masterfrom
takumin:nilclass-match-op
Aug 3, 2026
Merged

Add NilClass#=~#6999
matz merged 2 commits into
mruby:masterfrom
takumin:nilclass-match-op

Conversation

@takumin

@takumin takumin commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

String#=~ dispatches an argument that is neither a Regexp nor a String as
other =~ self, which is what CRuby's rb_str_match does too. CRuby has a defined
answer for nil because NilClass#=~ exists and returns nil; mruby had no =~ on
NilClass, so the dispatch raised NoMethodError. Kernel#!~ (mrblib/kernel.rb:38)
is !(self =~ y), so it inherited the same hole: !~ is listed among nil's methods,
and calling it raised.

"abc" =~ nil   # CRuby: nil,  mruby before: NoMethodError
nil !~ /a/     # CRuby: true, mruby before: NoMethodError
:a =~ nil      # CRuby: nil,  mruby before: NoMethodError (via Symbol#=~ -> String#=~)

nil is the only gap. Object#=~ was removed in Ruby 3.2, so every other receiver
without its own =~ raises NoMethodError in CRuby as well, and mruby already agrees.

"abc" =~ 1                  # CRuby: NoMethodError, mruby: NoMethodError
"abc" =~ Object.new         # CRuby: NoMethodError, mruby: NoMethodError
Object.new.respond_to?(:=~) # CRuby: false
nil.respond_to?(:=~)        # CRuby: true

Why the core and not an mrbgem

=~ is not an ISO method of NilClass (ISO 15.2.4.3 lists &, |, ^, nil? and
to_s), so CONTRIBUTING.md asks for a reason to add it to the core. mruby-object-ext
would otherwise be the natural home: it already extends NilClass with to_a, to_h,
to_i and to_f in a ROM table of its own.

The reason is Kernel#!~. It lives in core mrblib and is therefore present in every
build, including one that carries no gem at all, so nil !~ x is a hole no gem can
close. CRuby also answers nil.respond_to?(:=~) with true without loading anything;
making that depend on which gembox a build picks would be a worse surprise than the
space the method takes. That space is 48 bytes of object.o on x86_64: the method goes
into the existing NilClass ROM table, so it adds one entry and one function, and no
irep.

Declaring the entry MRB_ARGS_REQ(1) gives the arity check without a mrb_get_args()
call, the same way false_and() does.

What is in the PR

  • src/object.c: nil_match() and its ROM table entry.
  • test/t/nil.rb: the core cases, which need no Regexp: any argument answers nil,
    nil.respond_to?(:=~), the !~ path, and the arity check.
  • mrbgems/mruby-regexp/test/: the cases that do need Regexp, in the gem that owns the
    dispatch: "abc" =~ nil, "abc" !~ nil, :hello =~ nil, and the NoMethodError
    rows above, which pin that this change does not turn =~ into a blanket type check.
  • The header comment of mrbgems/mruby-regexp/mrblib/symbol_regexp.rb named this gap as
    behaviour inherited from String#=~. With NilClass#=~ in place the argument handling
    of Symbol#=~ agrees with CRuby on every argument type, so the comment is rewritten.

=~ deliberately keeps dispatching rather than type-checking its argument, unlike
match and match? (#6988): a blanket TypeError would break the nil and the
NoMethodError rows above, both of which CRuby answers the way mruby now does.

Compared against CRuby 4.0.6. rake test passes, and prek run --all-files is clean.

Summary by CodeRabbit

  • New Features

    • Added support for matching operations on nil, returning nil for all operands.
    • Added support for nil !~ expressions.
  • Bug Fixes

    • Improved matching behavior for strings and symbols with non-regular-expression operands.
    • Unsupported operands now raise the appropriate errors, while nil operands return expected results.
  • Tests

    • Expanded coverage for matching, negated matching, operand delegation, and invalid arguments.

takumin added 2 commits August 3, 2026 10:49
`String#=~` dispatches an argument that is neither a Regexp nor a String
as `other =~ self`, which is what CRuby's `rb_str_match` does too. CRuby
has a defined answer for `nil` because `NilClass#=~` exists and returns
nil; mruby had no `=~` on NilClass, so the dispatch raised NoMethodError
for `"abc" =~ nil`. `Kernel#!~` is `!(self =~ y)`, so `nil !~ /a/` raised
as well even though `!~` is listed among nil's methods.

```ruby
"abc" =~ nil   # CRuby: nil,  mruby: NoMethodError
nil !~ /a/     # CRuby: true, mruby: NoMethodError
```

`nil` is the only gap. `Object#=~` was removed in Ruby 3.2, so every
other receiver without its own `=~` raises NoMethodError in CRuby too,
and mruby already agrees.

`=~` is not an ISO method of NilClass, so this is a core addition that
CONTRIBUTING.md asks for a reason to make instead of reaching for an
mrbgem. `mruby-object-ext` would otherwise be the natural home, since it
already extends NilClass with `to_a`, `to_h`, `to_i` and `to_f`. The
reason is `Kernel#!~`: it lives in core mrblib, so it is present even in
a build that carries no gem at all, and `nil !~ x` is a hole no gem can
close. CRuby also answers `nil.respond_to?(:=~)` with true without
loading anything, and letting that depend on the gembox a build picks
would cost more in surprise than the method costs in space.

The method goes in the ROM table next to the other NilClass methods
rather than in mrblib, and declaring it `MRB_ARGS_REQ(1)` gives the
arity check without a `mrb_get_args()` call, as `false_and()` does. The
entry and the function add 48 bytes to `object.o` on x86_64.
`NilClass#=~` gives the gem's `=~` overrides a defined answer for a nil
argument, so record it where the dispatch actually happens: a nil
argument returns nil and `!~` returns true, while an argument without
its own `=~` still raises NoMethodError. These cases need Regexp, so
they belong here rather than in the core NilClass test.

The `symbol_regexp.rb` header comment claimed `:a =~ nil` raises
NoMethodError where CRuby returns nil. That was the last difference the
comment listed, and it is now gone: with `Object#=~` removed in Ruby 3.2
the remaining behaviour matches CRuby on every argument type.
@coderabbitai

coderabbitai Bot commented Aug 3, 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: efe48218-b970-4e35-9df6-4a6dd69fe1a8

📥 Commits

Reviewing files that changed from the base of the PR and between 2fdad90 and 30dee92.

📒 Files selected for processing (5)
  • mrbgems/mruby-regexp/mrblib/symbol_regexp.rb
  • mrbgems/mruby-regexp/test/regexp.rb
  • mrbgems/mruby-regexp/test/symbol_regexp.rb
  • src/object.c
  • test/t/nil.rb

📝 Walkthrough

Walkthrough

The match operator now supports NilClass#=~. Tests and documentation cover nil results, right-hand-operand dispatch, TypeError, NoMethodError, negation, and argument validation.

Changes

Match operator behavior

Layer / File(s) Summary
NilClass match operator
src/object.c, test/t/nil.rb
Adds NilClass#=~ with one required argument. The method returns nil, and tests cover availability, !~, and missing arguments.
Operand dispatch coverage
mrbgems/mruby-regexp/test/regexp.rb, mrbgems/mruby-regexp/test/symbol_regexp.rb, mrbgems/mruby-regexp/mrblib/symbol_regexp.rb
Covers right-hand-operand dispatch for String#=~ and Symbol#=~, including nil results and unsupported operands. Updates the Symbol#=~ documentation.

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

Possibly related PRs

  • mruby/mruby#6988: Covers related String#=~ operand dispatch and TypeError behavior.
  • mruby/mruby#6993: Covers related Symbol#=~ delegation and operand handling.

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 identifies the main change: adding NilClass#=~.
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.

@matz
matz merged commit 810f083 into mruby:master Aug 3, 2026
21 checks passed
@takumin
takumin deleted the nilclass-match-op branch August 3, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants