Share the UTF-8 decoder between core, mruby-regexp and mruby-string-ext - #7128
Merged
Conversation
The tree assembles a codepoint from UTF-8 bytes in two places, and both are the same function: mruby-regexp's `mrb_re_utf8_decode()` and mruby-string-ext's `utf8code()` each measure the character with `mrb_utf8len()` and then fold the same masks over the same bytes. Put the assembly in core once, as `mrb_utf8_decode()`, behind the same guard as the scanner it reads through and declared beside it. The body is mruby-regexp's, with the length widened to `mrb_int` to match what `mrb_utf8len()` returns. A sequence the scanner rejects comes back as its lead byte over one byte, so a value of 0x80 or above beside a length of 1 marks an invalid sequence, and whether that is an error stays the caller's question: the regexp engine matches it as a byte where String raises. Checked against both current implementations over 21,233,664 cases (every lead pair, nine tail bytes in each of the last two positions, every available length) with no difference in the code point, the byte length, or the error condition.
Core now carries the engine's decoder as `mrb_utf8_decode()`, so drop the local copy and call it: the compiler directly, where it reads a class atom or folds a character the pattern spells out, and the executor through `mrb_re_decode_char()`. The wrapper narrows the length back to the engine's `int` at the same seam where `mrb_re_charlen()` already does; the contract is unchanged, an invalid sequence still coming back as its lead byte over one byte.
`utf8code()` measured the character with `mrb_utf8len()` and then assembled the codepoint itself, folding the same masks core's `mrb_utf8_decode()` folds. Call that instead and keep only what is String's to decide: a sequence the decoder hands back as its lead byte is an `ArgumentError` here, where the regexp engine matches it as a byte.
|
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 (7)
📝 WalkthroughWalkthroughThe change adds a shared UTF-8 decoder to mruby and documents its behavior. Regexp and string extension code now use the shared decoder instead of local decoding logic. The regexp-specific decoder declaration and implementation were removed. ChangesUTF-8 decoder migration
Estimated code review effort: 3 (Moderate) | ~20 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 |
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.
The tree assembles a codepoint from UTF-8 bytes in two places, and the two are the same function:
mrb_re_utf8_decode()in mruby-regexp andutf8code()in mruby-string-ext each measure the character withmrb_utf8len()and then fold the same masks over the same bytes (the0xff >> (len + 1)one spells is the 0x1f, 0x0f, 0x07 the other names). They differ only in what an invalid sequence becomes: the engine matches the lead byte as a byte,String#ordandString#codepointsraiseArgumentError.This is the decoder counterpart of #7094 (the encoder) and #7109 (the scanner and character heads): core gains the one missing piece and both gems call it. mruby-pack's
unpack("U")decoder is left alone deliberately, since it implements CRuby's older, laxer dialect (surrogates pass;malformedandredundantare distinct errors), not the RFC 3629 readingmrb_utf8len()enforces.What core gains
The engine's body, moved beside the
mrb_utf8len()it reads through, behind the sameMRB_UTF8_STRING || MRB_UTF8_SCANguard and declared next to it in internal.h. A sequence the scanner rejects comes back as its lead byte over one byte, so a value of 0x80 or above beside a length of 1 marks an invalid sequence, and whether that is an error stays the caller's question.What the gems drop
mrb_re_utf8_decode(). The compiler calls core directly where it reads a class atom or folds a character the pattern spells out; the executor keepsmrb_re_decode_char(), which now narrows the length back to the engine'sintat the same seam wheremrb_re_charlen()already does.re_utf8.ckeeps the case folding and the\wtest.utf8code()keeps only what is String's to decide, theArgumentError. Its callers,String#ordandString#codepoints, are untouched.Equivalence
Checked by brute force rather than argued: both current implementations against the new function over 21,233,664 cases (both lead bytes exhaustively, nine representative tail bytes in each of the last two positions, every available length from 1 to 4), with no difference in the codepoint, the byte length, or the error condition.
Size
text grows slightly instead of shrinking: 1,721,815 → 1,722,415 on the default gembox, 1,812,452 → 1,812,996 on a full-core host build. The decoder now lives in the TU whose scanner it calls, so the compiler inlines
mrb_utf8len()into it (nmputs the new function at 494 bytes where the engine's was 163); the bytes buy back the call and drop the duplicate source.Verified
rake teston the default gembox, where the engine reads UTF-8 withoutMRB_UTF8_STRING: 2037 tests, all green on each commitMRUBY_CONFIG=ci/gcc-clang rake test(full-core:MRB_UTF8_STRINGeverywhere,MRB_REGEXP_UNICODE_CASEon full-debug, and the C++ cxx_abi build): all greenMRUBY_CONFIG=minimal rakebuilds, where neither define exists and the decoder compiles out with the rest of the scanprek run --all-filespasses, except thatmarkdownlintcould not install locally (npm engine mismatch); no Markdown is touched hereSummary by CodeRabbit