Skip to content

mruby-regexp: support (?#...) comment groups - #7055

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-comment-groups
Aug 9, 2026
Merged

mruby-regexp: support (?#...) comment groups#7055
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-comment-groups

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

(?#...) is a comment group: Ruby drops it from the pattern and compiles what is
left. The (? dispatch in compile_atom() recognises (?:, (?=, (?!,
(?<=, (?<!, (?<name> and the inline options, but has no # branch, so a
pattern holding a comment group falls through to undefined (?...) sequence and
does not compile at all.

/a(?#note)b/.match?("ab")
# CRuby: true
# mruby: RegexpError (undefined (?...) sequence: /a(?#note)b/)

Regexp.new("a(?#x\\)y)b").match?("ab")
# CRuby: true
# mruby: RegexpError (undefined (?...) sequence: /a(?#x\)y)b/)

Regexp.new("(a(?#c)b)").match("ab").to_a
# CRuby: ["ab", "ab"]
# mruby: RegexpError (undefined (?...) sequence: /(a(?#c)b)/)

The group is not an atom, so a quantifier written after it repeats what came
before it:

Regexp.new("a(?#x)*") =~ "aaa"
# CRuby: 0
# mruby: RegexpError (undefined (?...) sequence: /a(?#x)*/)

Under /x the same pattern fails for a second reason. Extended mode is
preprocessed over the whole pattern before the parser runs, and its # branch
skips to the end of the line, so a (?#note) b loses #note) b and leaves a
dangling (? at the end of the stripped buffer. A fix confined to
compile_atom() would leave this half broken, and comment groups written
alongside /x are where they are most common.

Regexp.new("a (?#note) b", Regexp::EXTENDED).match?("ab")
# CRuby: true
# mruby: RegexpError (target of repeat operator is not specified: /a (?#note) b/)

Fix

Remove the group in the preprocessing pass rather than in the parser.

That is what lets a(?#x)* compile as a*: the group has to be gone before the
quantifier is parsed. An atom is too late. compile_quantified() returns early
when compile_atom() emitted no instruction, so a comment group handled there
would leave the * with no target, and an atom that did emit one would change
what the pattern matches.

strip_extended() becomes preprocess_pattern() and takes the /x behaviour
as a flag. Comment group removal runs unconditionally, whitespace and #
line-comment stripping only when RE_FLAG_EXTENDED is set. mrb_re_compile()
now enters the pass when either the flag is set or has_comment_group() finds
(?# in the pattern, so an ordinary pattern without one still skips the pass
and its mrb_malloc(). That gate is a memchr() scan for (.

The new branch sits after the backslash pass-through and after the character
class branch, so the two ways of writing those bytes without meaning a comment
group keep the behaviour they have now:

Regexp.new("a[(?#c)]b").match?("a#b")  # both: true, class member
Regexp.new("a\\(?#note)b")             # both: RegexpError, escaped '('

The group ends at the first ) not preceded by a backslash, and it does not
nest, so x(?#a(?#b))y closes at the first ) and reports the second as
unmatched, as CRuby does.

An unterminated group is copied through the pass instead of being dropped,
which is what the new # branch in compile_atom() is for: reaching it means
the group was never closed, and it raises rather than letting the rest of the
pattern be swallowed silently.

Regexp.new("a(?#note")
# CRuby: RegexpError (end pattern in group)
# this PR: RegexpError (unterminated comment group)

Error messages still quote the pattern as written, since compile_error()
already quotes c->orig rather than the preprocessed buffer.

Scope

Nothing outside the pattern compiler changes. The literal form /a(?#note)b/
already passed through the parser without complaint, which is why the
reproduction raises RegexpError rather than a parse-time error, so
mruby-compiler needs no change. regexp.c is not involved either, since the
failure happens during compilation before any Regexp method runs.

(?~...) and (?(...) remain unimplemented and still raise from the same
catch-all.

Tests

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

  • Regexp - comment groups (?#...), a new block next to
    Regexp - inline options (?i) / (?i:...), the nearest coverage of the same
    dispatch chain: the literal and constructor forms, leading, trailing and
    empty comments, a newline inside one, a comment inside a capture group, the
    quantifier target case, \) inside a comment and the escaped backslash that
    ends one early, the non-nesting case, the unterminated case with its message,
    the character class member, and the escaped (.
  • Regexp extended mode (x flag): a comment group under /x, one followed by
    an ordinary # line comment on the same line, and the unterminated case.

Verified on x86_64-linux:

  • Every reproduction above now agrees with CRuby 4.0.6, error message wording
    aside.
  • rake test: 1976 total, 1958 OK, 0 KO, 0 crash, and bintest 105 OK.
  • An MRB_INT32 build with clang and -Wall -Wextra: 1878 total, 1860 OK,
    0 KO, 0 crash, and no new warning from any mruby-regexp file.
  • build_config/clang-asan.rb: 2153 total, 2143 OK, 0 KO, 0 crash, with no
    leak or invalid access reported.

Summary by CodeRabbit

  • New Features

    • Added support for (?#...) comment groups in regular expressions.
    • Documented regular-expression comment-group syntax.
    • Comment groups now work with extended-mode patterns and character classes.
  • Bug Fixes

    • Unterminated comment groups now produce a specific parsing error.
    • Improved handling of escaped syntax, nesting, and quantifier interactions.

`(?#...)` is a comment: Ruby drops it from the pattern and compiles what is
left. The `(?` dispatch in `compile_atom()` has no `#` branch, so a pattern
holding one does not compile at all.

```ruby
/a(?#note)b/.match?("ab")
# CRuby: true
# mruby: RegexpError (undefined (?...) sequence: /a(?#note)b/)

Regexp.new("a (?#note) b", Regexp::EXTENDED).match?("ab")
# CRuby: true
# mruby: RegexpError (target of repeat operator is not specified: /a (?#note) b/)
```

Under `/x` the failure has a second cause. Extended mode is preprocessed over
the whole pattern before the parser runs, and its `#` branch skips to the end
of the line, so `a (?#note) b` loses `#note) b` and leaves a dangling `(?`
behind.

Remove the group in that preprocessing pass rather than in `compile_atom()`.
A comment group is not an atom: CRuby compiles `a(?#x)*` as `a*`, so the group
has to be gone before the quantifier is parsed, and an atom emitting no
instruction leaves the `*` with no target. `strip_extended()` becomes
`preprocess_pattern()`, taking the `/x` behaviour as a flag, and now runs for
any pattern that holds `(?#`. `has_comment_group()` gates that widening on a
`memchr()` scan, so an ordinary pattern still skips the pass and its
`mrb_malloc()`.

The removal sits after the backslash pass-through and after the character
class branch, so `a\(?#note)b` and `a[(?#c)]b` keep the meaning they have
now. The group ends at the first `)` not preceded by a backslash and does not
nest, as in CRuby. An unterminated group is copied through instead of being
dropped, so the new `#` branch in `compile_atom()` raises on it rather than
letting the rest of the pattern be swallowed silently.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 15:34
@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: 4852b9b3-9a76-4aed-a891-3970e57d4280

📥 Commits

Reviewing files that changed from the base of the PR and between 9233195 and 8488ab3.

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

📝 Walkthrough

Walkthrough

The regexp compiler now supports (?#...) comment groups. It preserves the original pattern, preprocesses comment groups before parsing, reports unterminated groups, and applies extended-mode stripping when enabled. Documentation and regression tests cover the new syntax.

Changes

Regexp comment groups

Layer / File(s) Summary
Comment-group preprocessing
mrbgems/mruby-regexp/README.md, mrbgems/mruby-regexp/src/re_compile.c
The compiler detects and removes terminated (?#...) groups while preserving escapes and the original pattern for error reporting. The documentation lists the new syntax.
Parser and compilation integration
mrbgems/mruby-regexp/src/re_compile.c
Compilation preprocesses patterns that use comment groups or extended mode. Unterminated comment groups produce a specific parser error.
Regexp behavior validation
mrbgems/mruby-regexp/test/regexp.rb
Tests cover placement, escaping, non-nesting, character classes, quantifiers, extended mode, and unterminated groups.

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

Sequence Diagram(s)

sequenceDiagram
  participant RegexpCompiler
  participant preprocess_pattern
  participant RegexpParser
  RegexpCompiler->>preprocess_pattern: preprocess comment groups and extended-mode syntax
  preprocess_pattern-->>RegexpCompiler: return parser input
  RegexpCompiler->>RegexpParser: parse preprocessed pattern
  RegexpParser-->>RegexpCompiler: return regexp or parsing error
Loading

Possibly related PRs

  • mruby/mruby#7031: Both changes preserve the original regexp pattern during preprocessing and error 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 describes the primary change: adding (?#...) comment-group support to mruby-regexp.
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.

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