Skip to content

mruby-regexp: write the disabled flags in Regexp#to_s - #7062

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-to-s-negated-flags
Aug 10, 2026
Merged

mruby-regexp: write the disabled flags in Regexp#to_s#7062
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-to-s-negated-flags

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Regexp#to_s writes only the flags that are on (regexp.c:523-525), so the
(?...) form it produces says nothing about the ones that are off. CRuby names
those after a -, and that is what keeps the form meaningful once it is
interpolated into another pattern: without it, the enclosing pattern's flags
reach the embedded source. Regexp#inspect (regexp.c:541-543) repeats the
same run of tests and writes the letters in the i, m, x order, where Ruby
writes m, i, x.

/a/.to_s       # CRuby: "(?-mix:a)", mruby: "(?:a)"
/a/i.to_s      # CRuby: "(?i-mx:a)", mruby: "(?i:a)"
/a/im.to_s     # CRuby: "(?mi-x:a)", mruby: "(?im:a)"
/a/im.inspect  # CRuby: "/a/mi",     mruby: "/a/im"

Nothing raises. The missing part turns into a different pattern only once the
result is used, which is where it is hardest to attribute:

/#{/a/}b/i.match?("Ab")   # CRuby: false, mruby: true
/#{/a/}b/i.match?("aB")   # CRuby: true,  mruby: true

Interpolation goes through to_s: the compiler concatenates the parts of an
interpolated literal with OP_STRCAT and hands the result to Regexp.compile,
so /#{/a/}b/i compiles the source (?:a)b under Regexp::IGNORECASE and the
a is matched case-insensitively. Regexp.new(re.to_s) loses the flags the
same way.

Fix

Emit the flags that are off after a -, dropping that run only when none of
them are, and take the letters of both forms from one shared table so the two
orders cannot drift apart again.

The new form has to recompile, and parse_inline_flags() rejected it. Every
to_s of a pattern that is not extended now carries a -x, while the x
branch (re_compile.c:607-610) raised inline extended mode (?x) is not supported before it consulted negate, so it rejected an x in the disabled
run exactly like an enabled one. With the to_s change alone, every
interpolation of a Regexp would start raising RegexpError.

So a disabled x is accepted and dropped. That is exact whenever the enclosing
pattern is not extended, since the flag is already off there, which is the case
for every string to_s produces for such a pattern. Inside a pattern that is
itself extended it is not exact: preprocess_pattern() strips the whitespace
before the parser runs, so a scoped -x cannot bring it back.

Regexp.new("(?-x:a b)", Regexp::EXTENDED).match?("ab")   # CRuby: false, mruby: true
Regexp.new("(?-x:a b)", Regexp::EXTENDED).match?("a b")  # CRuby: true,  mruby: false

An enabled x keeps raising, as it must: extended mode is applied to the whole
pattern before it is parsed and cannot be scoped inline at all. Both limits are
now in the gem's Limitations section, which said nothing about inline extended
mode before.

This does not repair the round trip for an extended Regexp, which was already
broken: /a/x.to_s was "(?x:a)" and is now "(?x-mi:a)", and neither
recompiles.

Tests

mrbgems/mruby-regexp/test/regexp.rb.

Regexp#to_s gains the disabled run in all four existing assertions, plus the
all-flags case where the - run is dropped, a round trip through
Regexp.new, and a case pinning that the flags do not leak in either
direction. Regexp#inspect gains two multi-flag cases, which are what the
order change is visible in. A new Regexp#to_s - interpolation block covers
the embedded Regexp keeping its own flags and picking up none of the outer
ones.

Regexp - inline options (?i) / (?i:...) gains the disabled x in both the
scoped and the toggle position, an enabled x in the scoped form next to the
toggle form already asserted there, and the inexact case inside an extended
pattern. Regexp extended mode (x flag) has its to_s expectation updated.

Verified on x86_64-linux:

  • A differential sweep of 736 cases against CRuby 4.0.6: four pattern sources
    over the eight flag combinations, each checked for to_s, inspect, six
    subjects, and a round trip through Regexp.new(re.to_s); every pair of
    inner and outer flag combinations for an interpolated Regexp; and 24
    hand-written inline flag runs, on and off, scoped and toggled, over the same
    eight combinations. 366 cases disagreed before this change and 268 after,
    with no case disagreeing that did not disagree before.
  • All 268 remaining are outside what this changes: 260 involve inline extended
    mode, which is the limitation above, and 8 are (?-:a b), an empty disabled
    run that mruby rejects as undefined (?...) sequence and CRuby accepts.
    That last one is an unrelated pre-existing gap and is left as is: to_s
    always names at least one flag, so it never produces that form.
  • rake test: 1988 total, 1969 OK, 0 KO, 0 crash, and bintest 105 OK.
  • An MRB_INT32 build with clang and -Wall -Wextra: 1890 total, 1879 OK,
    0 KO, 0 crash, bintest 105 OK, and no new warning from any mruby-regexp
    file (regexp.c already emits four -Wunused-parameter).

Summary by CodeRabbit

  • New Features

    • Regular expressions now accept inline disabling of extended mode (?-x).
    • Regular expression string and inspection output consistently displays enabled and disabled flags in normalized order.
    • Extended-mode flags are preserved in serialized regular expressions.
  • Bug Fixes

    • Improved handling and reporting of inline regular-expression options.
    • Added clearer behavior for unsupported inline extended-mode constructs.
  • Documentation

    • Documented limitations affecting inline extended-mode syntax.

`Regexp#to_s` named only the flags that were on, so the form it produced
did not carry the ones that were off. Interpolating it into another
pattern therefore let the enclosing flags reach the embedded source, which
is the reason CRuby spells them out. Both `to_s` and `Regexp#inspect` also
wrote the letters in the `i`, `m`, `x` order rather than Ruby's `m`, `i`,
`x`.

```ruby
/a/i.to_s                # CRuby: "(?i-mx:a)", mruby: "(?i:a)"
/a/im.inspect            # CRuby: "/a/mi",     mruby: "/a/im"
/#{/a/}b/i.match?("Ab")  # CRuby: false,       mruby: true
```

`to_s` now emits the flags that are off after a `-`, and drops that run
only when none of them are. The letters of both forms come from one shared
table, so the two orders cannot drift apart again.

The new form has to recompile, and `parse_inline_flags()` rejected it:
every `to_s` of a pattern that is not extended now carries a `-x`, and an
`x` raised `inline extended mode (?x) is not supported` wherever it stood.
A disabled `x` is accepted and dropped instead. That is exact when the
enclosing pattern is not extended, since the flag is already off there.
Inside a pattern that is extended it is not, because `preprocess_pattern()`
removes the whitespace before the parser runs and a scoped `-x` cannot
bring it back: `Regexp.new("(?-x:a b)", Regexp::EXTENDED)` matches "ab"
where CRuby matches "a b". An enabled `x` keeps raising. The gem's
Limitations section now records both.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 23:00
@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: f9708ddc-b1b0-4881-9a18-2987404e38fb

📥 Commits

Reviewing files that changed from the base of the PR and between 1f3f28b and d72dd0b.

📒 Files selected for processing (4)
  • mrbgems/mruby-regexp/README.md
  • mrbgems/mruby-regexp/src/re_compile.c
  • mrbgems/mruby-regexp/src/regexp.c
  • mrbgems/mruby-regexp/test/regexp.rb

📝 Walkthrough

Walkthrough

Regexp inline option parsing now accepts -x while rejecting x. Regexp#to_s and Regexp#inspect use canonical flag ordering, and to_s serializes disabled flags. Documentation and tests cover these behaviors.

Changes

Regexp option handling and serialization

Layer / File(s) Summary
Inline extended-mode option handling
mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/README.md, mrbgems/mruby-regexp/test/regexp.rb
Inline (?x) continues to raise RegexpError. Inline ?-x is accepted and ignored. Documentation and tests cover option combinations and extended-mode behavior.
Canonical flag serialization
mrbgems/mruby-regexp/src/regexp.c, mrbgems/mruby-regexp/test/regexp.rb
Regexp#to_s and Regexp#inspect emit enabled flags in m, i, x order. to_s also emits disabled flags after -. Tests cover ordering, interpolation, recompilation, and extended-mode output.

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

Possibly related PRs

  • mruby/mruby#7046: Both changes update inline regexp option handling and related tests.
  • mruby/mruby#7055: Both changes update regexp preprocessing and inline-option parsing.

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 primary change: writing disabled flags in Regexp#to_s.
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 cefca98 into mruby:master Aug 10, 2026
21 checks passed
@takumin
takumin deleted the regexp-to-s-negated-flags branch August 10, 2026 07:54
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