Skip to content

mruby-regexp: close a class range at the first codepoint of a \u{...} list - #7263

Merged
matz merged 2 commits into
mruby:masterfrom
takumin:regexp-class-range-bounds
Aug 18, 2026
Merged

mruby-regexp: close a class range at the first codepoint of a \u{...} list#7263
matz merged 2 commits into
mruby:masterfrom
takumin:regexp-class-range-bounds

Conversation

@takumin

@takumin takumin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Two defects at the range step of compile_charclass in mrbgems/mruby-regexp/src/re_compile.c, fixed together because the second is not fully observable without the first: with the range end read from the wrong codepoint, [b-\u{61 63}] compiles to b-c and no reversed range is ever seen there.

A reversed range was dropped without a word. The emission was guarded by cp <= hi with no else, so a positive class silently lacked the span and a negated one admitted every character. CRuby raises for either.

Regexp.new("[b-a]")            # CRuby: RegexpError (empty range in char class), mruby before: compiles and matches nothing, after: RegexpError
Regexp.new("[^b-a]") =~ "x"    # CRuby: RegexpError, mruby before: 0, after: RegexpError
Regexp.new("[\\u{62}-\\u{61}]")  # CRuby: RegexpError, mruby before: compiles, after: RegexpError
Regexp.new("[\\u{62 63}-a]")     # CRuby: RegexpError, mruby before: compiles to `b`, after: RegexpError

A \u{...} list closing a range was bound at its last codepoint. read_class_atom handed every list over the same way, all but the last codepoint as members and the last returned, which is right before a - ([\u{61 62}-z] is a plus b-z) and wrong after one, where CRuby closes the range with the codepoint next to the - and takes the rest as members. The class held a different set in either direction.

/[a-\u{63 7a}]/ =~ "m"   # CRuby: nil, mruby before: 0 (`a-z` plus `c`), after: nil (`a-c` plus `z`)
/[a-\u{7a 41}]/ =~ "a"   # CRuby: 0, mruby before: nil (`a` down to `A`, dropped, and `z` alone), after: 0 (`a-z` plus `A`)
/[\u{3042}-\u{3044 3046}]/ =~ "ぅ"   # CRuby: nil, mruby before: 0, after: nil
Regexp.new("[b-\\u{61 63}]")   # CRuby: RegexpError (empty range in char class), mruby before: `a-c`, after: RegexpError

Fix

  • After the byte-versus-character check on the two ends, cp > hi raises RegexpError with CRuby's wording, empty range in char class: /.../. The comparison is sound where it runs: that check leaves no pair of a byte and a character above ASCII, and ASCII sits below either kind.
  • read_class_atom takes a closes_range flag. For a \u{...} list that closes a range the first codepoint is returned as the end and the rest join the class as members; a list that does not keeps the old order (last codepoint returned). On a build reading a String by byte the end is still the last byte of that codepoint's spelling, as for any \u in a class there.
  • README: the class paragraph on \u{...} lists now states both sides of the - and that a reversed range raises.

Both [a-\u{61}] and [\u{61}-\u{61}] (a range of one) still compile, [-a] and [a-] still take - as a member, and the byte-versus-character rule for \x ends is untouched.

Testing

  • regexp_syntax.rb: new block reversed character class range ([b-a], [^b-a] with the exact message, [xz-ay], and the non-cases [a-a], [-a], [a-]).
  • regexp_utf8.rb: new blocks reversed character class range through \u and a \u list closing a character class range; the ASCII \u rows run on every build, the rows with codepoints above ASCII ([\u{3044}-\u{3042}], [\u{100}-\u{FF}], [\u{3042}-\u{3044 3046}], [\u{3044}-\u{3046 3042}]) under __ENCODING__ == "UTF-8", since on a byte-read build the ends are the last bytes of the spellings and their order is not the codepoints' order. The two existing list blocks gain a HI-side row [a-\u{63 7a}].
  • Every value in the tests and above was checked against CRuby 4.0.6; a probe of 41 class-range patterns gives byte-identical output (including error messages) from bin/mruby and ruby.

Full suite green at every commit (MRUBY_CONFIG=ci/gcc-clang rake -m test at both commits, default rake -m test at the tip after rm -rf build/host):

Build Total KO Crash
full-debug 2353 0 0
bintest 2353 (+123 bintest) 0 0
cxx_abi 2353 0 0
byte-string 2283 0 0
ascii-case 2350 0 0
default (rake -m test) 2129 (+112 bintest) 0 0

At the first commit the totals are one lower on each of the five builds (2352 / 2352 / 2352 / 2282 / 2349), KO 0, Crash 0.

Environment

Machine, toolchain, and the compile line of every build
Item Value
OS Ubuntu 24.04.4 LTS
Kernel 7.0.0-29-generic
CPU AMD Ryzen 9 5950X 16-Core Processor
C compiler gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
binutils GNU ld (GNU Binutils) 2.47.20260726
rake rake, version 13.3.1
CRuby (reference) ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]

Actual compile line of src/string.c in each build_config/ci/gcc-clang.rb build (-MMD -c, -I, and -o dropped). full-debug is -O0 because enable_debug appends -g3 -O0 after the toolchain's -g -O3; cxx_abi compiles C as C++ with gcc -x c++ -std=gnu++03, g++ only links.

# full-debug
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -g3 -O0 -DMRB_GC_STRESS -DMRB_USE_DEBUG_HOOK -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c
# bintest
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_GC_FIXED_ARENA -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -DMRB_USE_DEBUG_HOOK src/string.c
# cxx_abi
gcc -g -O3 -Wall -Wundef -Wwrite-strings -x c++ -std=gnu++03 -DMRB_GC_FIXED_ARENA -DMRB_USE_CXX_EXCEPTION -DMRB_USE_CXX_ABI -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c
# byte-string
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c
# ascii-case
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CASE -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c

Summary by CodeRabbit

  • Bug Fixes

    • Improved regular expression character classes using \u{...} codepoint lists.
    • Reversed ranges now correctly raise RegexpError instead of being silently ignored.
    • Valid ranges and mixed codepoint-list patterns now produce the expected matches, including negated classes.
  • Documentation

    • Clarified how codepoint lists and range boundaries are interpreted in character classes.

`compile_charclass` guarded the range emission with `cp <= hi` and had
no `else`, so a range written backwards was dropped without a word. A
positive class then silently lacked the span, and a negated one admitted
every character. CRuby raises `RegexpError` for either:

```ruby
Regexp.new("[b-a]")           # CRuby: RegexpError (empty range in char class), mruby: compiles, matches nothing
Regexp.new("[^b-a]") =~ "x"   # CRuby: RegexpError, mruby: 0
Regexp.new("[\\u{62}-\\u{61}]")  # CRuby: RegexpError, mruby: compiles
Regexp.new("[\\u{62 63}-a]")     # CRuby: RegexpError, mruby: compiles to `b`
```

The bare `[b-a]` is old behavior, but `\u` made such a range easy to
write without the letters showing it, and the loss is the same. Raise
"empty range in char class" with CRuby's wording once both ends are
read. The comparison is sound where it runs: the byte-versus-character
check just before it leaves no pair of a byte and a character above
ASCII, and ASCII sits below either kind, so the two numbers order the
ends.
…}` list

`read_class_atom` handed every `\u{...}` list over the same way: all but
the last codepoint joined the class as members and the last was
returned, so it could open a range. That is right before a `-`, where
`[\u{61 62}-z]` is `a` plus `b-z`, and wrong after one, where CRuby
closes the range with the codepoint next to the `-`, the first of the
list, and takes the rest as members. The class held a different set in
either direction:

```ruby
/[a-\u{63 7a}]/ =~ "m"   # CRuby: nil, mruby: 0 (`a-z` plus `c` instead of `a-c` plus `z`)
/[a-\u{7a 41}]/ =~ "a"   # CRuby: 0, mruby: nil (`a` down to `A`, dropped, and `z` alone)
Regexp.new("[b-\\u{61 63}]")  # CRuby: RegexpError (empty range in char class), mruby: `a-c`
```

`read_class_atom` now takes whether the atom closes a range. For a list
that does, the first codepoint is returned as the end and the rest join
the class; a list that does not keeps the old order. `[a-\u{7a 41}]` is
`a-z` plus `A`, and `[b-\u{61 63}]` is reported as the empty range it
is. On a build reading a String by byte the end is still the last byte
of the codepoint's spelling, as it is for any `\u` in a class there.
@takumin
takumin requested a review from matz as a code owner August 18, 2026 10:09
@coderabbitai

coderabbitai Bot commented Aug 18, 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: ab0ed30f-5df1-44bd-9d5c-aa42d25ea29e

📥 Commits

Reviewing files that changed from the base of the PR and between 885e215 and ba629b4.

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

Included review availability: Your plan includes up to 8 reviews per rolling hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

Changes

Regexp character-class range handling

Layer / File(s) Summary
Unicode-list range parsing and validation
mrbgems/mruby-regexp/src/re_compile.c
The compiler treats adjacent \u{...} codepoints as range endpoints when required, keeps other codepoints as class members, and raises RegexpError for reversed ranges.
Regression coverage and documentation
mrbgems/mruby-regexp/test/regexp_syntax.rb, mrbgems/mruby-regexp/test/regexp_utf8.rb, mrbgems/mruby-regexp/README.md
Tests cover list membership, ASCII and non-ASCII ranges, reversed ranges, negated classes, and edge hyphens. Documentation describes the updated behavior.

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

Merge Risk: ⚪ Minimal · up to ba629

This localized change corrects reversed character-class ranges and Unicode range bounds, with regression coverage and passing test suites; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Pattern
  participant RegexpCompiler
  participant CharacterClass
  Pattern->>RegexpCompiler: provide character-class pattern
  RegexpCompiler->>CharacterClass: parse Unicode-list members and range endpoints
  RegexpCompiler->>RegexpCompiler: validate range direction
  RegexpCompiler-->>Pattern: compile class or raise RegexpError
Loading

Possibly related PRs

  • mruby/mruby#7052: Both changes update character-class range handling in re_compile.c, but address different parsing behavior.
  • mruby/mruby#7074: This change extends the Unicode-list character-class parsing implemented in the related PR.
  • mruby/mruby#7207: Both changes update character-class range handling and UTF-8 tests.

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 describes the primary documented behavior change for closing ranges with the first codepoint of a \\u{...} list.
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 5b51131 into mruby:master Aug 18, 2026
21 checks passed
@takumin
takumin deleted the regexp-class-range-bounds branch August 18, 2026 13:04
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