mruby-regexp: split a character class range at the ASCII boundary - #7052
Merged
Conversation
`compile_charclass()` stores a range with both bounds below 128 in the class
bitmap and every other range in the codepoint list. A range that starts in
ASCII and ends above it went to the codepoint list whole. `class_match()`
answers a codepoint below 128 from the bitmap alone and never reads the
codepoint list, so the ASCII half of such a range matched nothing.
```ruby
/[a-\u0100]/.match?("a") # CRuby: true, mruby: false
/[0-\u0100]/.match?("5") # CRuby: true, mruby: false
```
Negation is applied at match time against the same bitmap, so a negated form
accepted every ASCII character it was written to reject. Nothing raises.
```ruby
/[^a-\u0100]/.match?("a") # CRuby: false, mruby: true
```
Split the range at the boundary: the bitmap takes `cp` through 127 and the
codepoint list 128 through `hi`. A range that stays on one side reaches
exactly one of the two calls and is stored as before, so only the straddling
case changes. The `cp <= hi` guard moves out to cover both calls, which
leaves a descending range storing nothing, as before.
With the ASCII half in the bitmap, the `/i` fold at the end of the same
function reaches it, so `/[a-\u0100]/i` now matches `"A"` too. Non-ASCII case
folding stays unapplied, so `/[\u0100]/i` still does not match `"\u0101"`.
This also restores `class_add_range()`'s documented contract that both of its
bounds are at least 128; the straddling case was the one caller that broke it.
`compute_first_set()` needs no change: `first_set_walk()` bails out on a class
whose codepoint list is non-empty, which a split range still leaves non-empty,
so the first-byte skip stays disabled for these patterns exactly as it was.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughRegexp compilation now splits character-class ranges that cross the ASCII/non-ASCII boundary. Tests cover matching, negation, case-insensitive matching, quantification, and same-side ranges. ChangesRegexp character-class boundary handling
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
This was referenced Aug 9, 2026
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
compile_charclass()stores a range in one of two places. Both bounds below128 go into the class bitmap through
class_set_range(); anything else goesinto the codepoint list through
class_add_range(). A range that starts inASCII and ends above it took the second path with the span left whole.
class_match()answers a codepoint below 128 from the bitmap alone and neverreads the codepoint list, so the ASCII half of such a range is unreachable.
The upper half still answers, so the class reads as half alive rather than
broken.
A negated class is worse. Negation is applied at match time against the same
bitmap, so a class that matches nothing below 128 makes its negated form match
everything below 128, including the characters it was written to reject.
Nothing raises.
Fix
Split the range at the boundary instead of picking one side for the whole
span. The bitmap takes
cpthrough 127 and the codepoint list takes 128through
hi.A range that stays on one side reaches exactly one of the two calls and is
stored where it was before:
[a-c]sets bitmap bits only,[\u0100-\u0110]appends a codepoint range only. Only the straddling case changes, and it now
occupies both.
The
cp <= higuard moves out to cover both calls. It used to guard thecodepoint path alone, while the ASCII path relied on
class_set_range()'sloop not running for an inverted range. Neither call is reached now when
cp > hi, so a descending range still stores nothing.This also restores
class_add_range()'s documented contract that both of itsbounds are at least 128. The straddling case was the one caller that broke it.
compute_first_set()needs no change.first_set_walk()bails out on anRE_CLASSwhose codepoint list is non-empty, which a split range still leavesnon-empty, so the first-byte skip stays disabled for these patterns exactly as
it was.
Scope
Non-ASCII case folding is unchanged and still out of scope:
/[\u0100]/idoesnot match
"\u0101". What this fixes is the ASCII half of a mixed range,which the bitmap already knew how to hold.
#7049 folds
/iinto that same bitmap at the end ofcompile_charclass(), sothe two compose: with the ASCII half now stored where the fold walks,
/[a-\u0100]/imatches"A"and/[^a-\u0100]/irejects it, both of whichagree with CRuby. That fold could not reach a straddling range on its own,
because the range never reached the bitmap.
Tests
mrbgems/mruby-regexp/test/regexp.rb, a newRegexp - character class range across the ASCII boundaryblock next toRegexp - character class: the ASCII half of[a-\u0100], both of itsboundaries (U+0060 below and U+0101 above), the non-ASCII half, a quantified
form, all four negated cases, the two
/iforms, and two single-sided rangesas regression guards.
It carries no
__ENCODING__guard, unlike the other multibyte blocks in thefile. It reads only match results, never a character offset, so it runs on a
build without
MRB_UTF8_STRING, which is what CI builds. With the guard theblock would never execute in CI.
Verified on
x86_64-linux:rake test: 1976 total, 1958 OK, 0 KO, 0 crash, and bintest 105 OK.MRB_INT32build with clang and-Wall -Wextra: 1878 total, 1860 OK,0 KO, 0 crash, and no new warning from any
mruby-regexpfile.Summary by CodeRabbit
Bug Fixes
Tests