Skip to content

mruby-regexp: apply /i to character classes - #7049

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

mruby-regexp: apply /i to character classes#7049
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-ignorecase-charclass

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

RE_FLAG_IGNORECASE is folded in only where compile_atom() emits a single
literal character. compile_charclass() never reads the flag, so every [...]
ignores /i.

/[abc]/i.match?("A")        # CRuby: true,  mruby: false
/[a-c]/i.match?("A")        # CRuby: true,  mruby: false
/[A-C]/i.match?("a")        # CRuby: true,  mruby: false
/[a-c]+/i.match?("AB")      # CRuby: true,  mruby: false

The inline and constructor forms of the flag reach the same code, so they fail
the same way.

/(?i)[a-c]/.match?("A")                              # CRuby: true, mruby: false
Regexp.new("[a-c]", Regexp::IGNORECASE).match?("A")  # CRuby: true, mruby: false

POSIX bracket classes are affected too, since they are merged into the same
bitmap.

/[[:lower:]]/i.match?("A")  # CRuby: true,  mruby: false
/[[:upper:]]/i.match?("a")  # CRuby: true,  mruby: false

A negated class is worse: it matches what it must reject, so a /i regexp
used for validation accepts input it should refuse. Nothing raises.

/[^a-c]/i.match?("A")       # CRuby: false, mruby: true

Fix

Fold the ASCII letters into the class bitmap at the end of
compile_charclass(), after the parse loop and before the class is committed
to an RE_CLASS or RE_NCLASS. That is the only point at which a [...] is
complete: the loop merges POSIX bracket bits, shorthands, ASCII ranges and
single literals into one bitmap, so a single pass there covers all four forms
and [[:lower:]] under /i picks up uppercase.

Negation is applied at match time against that same bitmap, so folding the
positive set fixes [^a-c] at the same time. class_add_shorthand() and
posix_class_bits() both run before the class is complete and would miss
plain literals and a-c ranges, so neither is the right place.

class_get_bit() is added alongside class_set_bit(). re_compile.c had no
bit-level reader for re_charclass::bitmap: first_set_walk() ORs whole
bytes, and the one bit test in the gem lives in class_match(), which is
static in another translation unit and takes a codepoint rather than a bit
index.

The fold has to happen at compile time rather than in the matcher for three
reasons. class_match() receives only a re_charclass and has no access to
any flags. pat->flags is the whole-pattern option set and so cannot see
inline (?i:...) scoping. And compute_first_set() derives pat->first_bytes
from the bitmap at compile time, which the matcher then uses to skip input; a
match time fold would leave that skip narrower than the class, and /[a-c]/i
would still fail on "A".

Reading c->flags is what makes the inline forms work: compile_atom()
already saves, replaces and restores it around (?i) and (?i:...), so the
class sees the scoped value with no further change.

The existing IGNORECASE blocks in compile_atom() are already correct for a
single literal and are left alone.

Scope

Non-ASCII case folding is not addressed. The codepoint range list and
cc->utf8_any are untouched, and class_add_range() and
class_add_codepoint() are unchanged.

Tests

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

  • Regexp - case insensitive character class, a new block next to
    Regexp - case insensitive: the four positive forms, the
    Regexp::IGNORECASE constructor form, and both negated forms. It also
    guards the fold against widening the class past the ASCII letters, since
    [ and { are 32 apart but are not a case pair.
  • Regexp - POSIX bracket classes: [[:upper:]] and [[:lower:]] under
    /i, next to the existing [[:upper:]] assertion.
  • Regexp - inline options (?i) / (?i:...): (?i)[a-c], (?i:[a-c]), and
    that the option does not leak past the closing paren.

Verified on x86_64-linux:

  • Every reproduction above now matches CRuby 4.0.6.
  • rake test: 1968 total, 1950 OK, 0 KO, 0 crash, and bintest 105 OK.
  • An MRB_INT32 build with clang and -Wall -Wextra: 1870 total, 1852 OK,
    0 KO, 0 crash, and no new warning from any mruby-regexp file.

Summary by CodeRabbit

  • Bug Fixes

    • Improved case-insensitive regular expression matching for ASCII letters in character classes.
    • Added support for case-insensitive matching in ranges, shorthand classes, POSIX classes, and negated classes.
    • Ensured inline case-insensitive options remain limited to their intended scope.
    • Preserved existing behavior for non-ASCII character ranges.
  • Tests

    • Added coverage for case-insensitive character classes, negation, POSIX classes, and ASCII boundary cases.

`RE_FLAG_IGNORECASE` was folded in only where `compile_atom()` emits a single
literal character. `compile_charclass()` never read the flag, so every `[...]`
ignored `/i`.

```ruby
/[abc]/i.match?("A")        # CRuby: true,  mruby: false
/[a-c]/i.match?("A")        # CRuby: true,  mruby: false
/[a-c]+/i.match?("AB")      # CRuby: true,  mruby: false
/(?i)[a-c]/.match?("A")     # CRuby: true,  mruby: false
/[[:lower:]]/i.match?("A")  # CRuby: true,  mruby: false

# A negated class is worse: it matches what it must reject.
/[^a-c]/i.match?("A")       # CRuby: false, mruby: true
```

The negated form is a false positive, so a `/i` regexp used for validation
silently accepts input it should reject.

Fold the ASCII letters into the class bitmap at the end of
`compile_charclass()`, once the parse loop has merged POSIX brackets,
shorthands, ranges and single literals into it. One pass there covers all four
forms. Negation is applied at match time against that same bitmap via
`RE_NCLASS`, so folding the positive set fixes the negated form as well.

`class_get_bit()` is added next to `class_set_bit()` because `re_compile.c` had
no bit-level reader for `re_charclass::bitmap`; the equivalent test in
`class_match()` is `static` in another translation unit and takes a codepoint
rather than a bit index.

Folding at compile time also keeps `compute_first_set()` in step: it derives
`pat->first_bytes` from the same bitmap to skip input, so a match time fold
would leave that skip narrower than the class and `/[a-c]/i` would still fail
on `"A"`. Reading `c->flags` rather than `pat->flags` is what makes the
inline-scoped `(?i)` and `(?i:...)` forms work.

Non-ASCII case folding stays out of scope, so the codepoint range list is
untouched.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 14:15
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The regexp compiler now folds ASCII letter pairs in case-insensitive character classes. The change covers literals, ranges, shorthands, POSIX classes, and negated classes. Tests cover ASCII boundaries and inline option scope.

Changes

ASCII character-class case folding

Layer / File(s) Summary
Character-class folding and validation
mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/test/regexp.rb
The compiler adds ASCII bitmap lookup and folds uppercase/lowercase pairs for /i character classes. Tests cover POSIX classes, ranges, negation, Regexp::IGNORECASE, ASCII boundaries, and scoped inline options.

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

Suggested labels: mrbgems

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 applying the /i flag to character classes, which is the main change.
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.

@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/src/re_compile.c`:
- Around line 476-488: Extend the IGNORECASE handling around the class-building
logic to case-fold Unicode codepoints in the inclusive range list, not just
ASCII bitmap entries. Merge the folded Unicode set into the class representation
so ranges such as [a-\u0100] include both case variants, including under
RE_NCLASS negation. Add positive and negated mixed-range tests covering these
cases.
🪄 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: 1a4f2271-2878-43ee-b820-4551e25eade3

📥 Commits

Reviewing files that changed from the base of the PR and between 9360b3f and 9660f39.

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

Comment thread mrbgems/mruby-regexp/src/re_compile.c
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