mruby-regexp: make a multibyte literal one atom - #7056
Merged
Conversation
📝 WalkthroughWalkthroughThe regex compiler now emits multibyte UTF-8 characters as single atoms. Regression tests cover quantifiers, optional matches, non-greedy matches, and scanning behavior. ChangesUTF-8 regex atom handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
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 |
There was a problem hiding this comment.
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 938-944: Ensure the atom-building logic around mrb_re_utf8_charlen
validates every required UTF-8 continuation byte before consuming it, so invalid
or truncated sequences fall back to a single lead byte and do not absorb
following pattern characters. Update mrb_re_utf8_charlen or add pre-loop
validation, preserving valid multibyte decoding, and add regression coverage for
invalid-continuation and truncated sequences.
🪄 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: 4072ebc2-153e-4ae3-ae71-a7ae9cc19954
📒 Files selected for processing (2)
mrbgems/mruby-regexp/src/re_compile.cmrbgems/mruby-regexp/test/regexp.rb
`compile_atom()` emits a literal one byte at a time. For a character above
127 that means the lead byte and each continuation byte become atoms of their
own, and `compile_quantified()` binds a quantifier to the last atom emitted.
`/Ā+/` therefore compiled as `\xC4(\x80)+`.
```ruby
/Ā+/.match("ĀĀ")[0] # CRuby: "ĀĀ", mruby: "Ā"
/Ā{2}/.match?("ĀĀ") # CRuby: true, mruby: false
"ĀĀxĀ".scan(/Ā+/).size # CRuby: 2, mruby: 3
```
The repetition forms go the same way, since `{n,m}` copies that same last
atom, and so do three and four byte characters.
Emit every byte of the character in one go instead. The atom then spans the
whole character, `emit_atom_copy()` copies all of its bytes for `{n,m}`, and
the `RE_SPLIT` that `insert_inst()` puts in front of it for `*` and `?` covers
all of them too.
The byte count comes from `mrb_re_utf8_charlen()`, which derived it from the
lead byte alone. That is too loose to build an atom from. For the pattern bytes
`C4 78 2B` it reports 2, so `x` joins the atom and `+` repeats `\xC4x`; for
`C4 2B` it reports 2 as well, and the `+` itself is swallowed and stops being a
quantifier at all. Check the continuation bytes there, so a sequence that never
completes keeps reporting a length of 1 and its bytes stay atoms of their own,
which is how such a pattern reads as bytes.
`mrb_re_utf8_decode()` takes its length from the same function, so the string
being matched is read the same way: `.` takes an incomplete lead byte alone
instead of pairing it with whatever follows.
Nothing downstream changes. `compute_fixed_len()` counts each `RE_CHAR` as one
byte and totals the same, `first_set_walk()` already gives up on a lead byte
above 127, and the literal prefix scan reads whatever `RE_CHAR` run it finds.
takumin
force-pushed
the
regexp-multibyte-literal-quantifier
branch
from
August 9, 2026 16:20
ca96faa to
bcacba1
Compare
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_atom()emits a literal one byte at a time(
re_compile.c:931). For a character above 127 that means the lead byte andeach continuation byte become atoms of their own, and
compile_quantified()binds a quantifier to the last atom emitted.
/Ā+/therefore compiled as\xC4(\x80)+: the lead byte once, then the continuation byte repeated.Three and four byte characters go the same way, and a quantified literal
after another atom is affected too.
Scanning turns one run into one match per character, which is the form most
likely to be noticed as wrong output rather than as a missing match.
Nothing raises in any of these.
Fix
Emit every byte of the character in one go, so the atom spans the whole
character.
That is all it takes, because the quantifier machinery already works on a
span rather than on a single instruction:
emit_atom_copy()copies the whole[start, code_len)range for{n,m}, and theRE_SPLITthatinsert_inst()puts in front of the atom for*and?covers the range aswell. A group, which is many instructions, has always relied on this; a
multibyte literal was the one atom that did not produce a span.
Where the character ends comes from
mrb_re_utf8_charlen(), which derived itfrom the lead byte alone and never looked at the bytes that follow. That is too
loose to build an atom from. For the pattern bytes
C4 78 2Bit reports 2, soxjoins the atom and+repeats\xC4x; forC4 2Bit reports 2 as well,and the
+itself is swallowed and stops being a quantifier at all.So this checks the continuation bytes there as well. A sequence that never
completes keeps reporting a length of 1, its bytes stay atoms of their own, and
such a pattern reads as the bytes it is made of.
mrb_re_utf8_decode()takesits length from the same function, so the string being matched is read the same
way:
.takes an incomplete lead byte alone instead of pairing it withwhatever follows.
Nothing downstream changes.
compute_fixed_len()counts eachRE_CHARas onebyte and reaches the same total,
first_set_walk()already returns early on alead byte above 127, and the literal prefix scan reads whatever run of
RE_CHARit finds.Tests
mrbgems/mruby-regexp/test/regexp.rb, two new blocks beforeRegexp - multibyte (UTF-8) match extraction.Regexp - quantifier on a multibyte literal:+,*,{n}and{n,m}ona two byte literal, the three and four byte cases, a quantified literal
preceded by another atom, a non-greedy form,
scan, and an absent optionalliteral. The assertions compare
bytesize, since a match that stops onecharacter early is still a match and only the length tells the two apart.
Regexp - quantifier on an invalid multibyte literal: a lead byte followed byan ASCII byte, a lead byte followed by the quantifier itself, a sequence cut
short by the end of the pattern, a valid character right after a stray lead
byte, and
.over a stray lead byte on the subject side.Verified on
x86_64-linux:one to four bytes, eleven quantifier forms, each plain, preceded by another
atom and preceded by a lookbehind, over four subject lengths, plus five stray
lead bytes with five followers each. 234 cases disagreed before this change
and none do after. Patterns that are not valid UTF-8 are compared against a
binary CRuby regexp, since CRuby rejects them outright while mruby has no
encodings.
rake test: 1977 total, 1959 OK, 0 KO, 0 crash, and bintest 105 OK.MRB_INT32build with clang and-Wall -Wextra: 2046 total, 2036 OK,0 KO, 0 crash, and no new warning from any
mruby-regexpfile (regexp.calready emits four
-Wunused-parameter).Summary by CodeRabbit
Bug Fixes
Tests