Skip to content

Share the UTF-8 decoder between core, mruby-regexp and mruby-string-ext - #7128

Merged
matz merged 3 commits into
mruby:masterfrom
takumin:utf8-share-decoder
Aug 12, 2026
Merged

Share the UTF-8 decoder between core, mruby-regexp and mruby-string-ext#7128
matz merged 3 commits into
mruby:masterfrom
takumin:utf8-share-decoder

Conversation

@takumin

@takumin takumin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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 and utf8code() in mruby-string-ext each measure the character with mrb_utf8len() and then fold the same masks over the same bytes (the 0xff >> (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#ord and String#codepoints raise ArgumentError.

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; malformed and redundant are distinct errors), not the RFC 3629 reading mrb_utf8len() enforces.

What core gains

uint32_t mrb_utf8_decode(const char *p, const char *e, mrb_int *lenp);

The engine's body, moved beside the mrb_utf8len() it reads through, behind the same MRB_UTF8_STRING || MRB_UTF8_SCAN guard 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

  • mruby-regexp deletes 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 keeps mrb_re_decode_char(), which now narrows the length back to the engine's int at the same seam where mrb_re_charlen() already does. re_utf8.c keeps the case folding and the \w test.
  • mruby-string-ext's utf8code() keeps only what is String's to decide, the ArgumentError. Its callers, String#ord and String#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 (nm puts 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 test on the default gembox, where the engine reads UTF-8 without MRB_UTF8_STRING: 2037 tests, all green on each commit
  • MRUBY_CONFIG=ci/gcc-clang rake test (full-core: MRB_UTF8_STRING everywhere, MRB_REGEXP_UNICODE_CASE on full-debug, and the C++ cxx_abi build): all green
  • MRUBY_CONFIG=minimal rake builds, where neither define exists and the decoder compiles out with the rest of the scan
  • prek run --all-files passes, except that markdownlint could not install locally (npm engine mismatch); no Markdown is touched here

Summary by CodeRabbit

  • New Features
    • Improved UTF-8 decoding across strings and regular expressions.
    • Added consistent handling for valid, invalid, and truncated UTF-8 sequences.
  • Bug Fixes
    • Improved UTF-8 character length reporting and error handling in string operations.
    • Standardized Unicode character decoding during regular-expression processing.

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.
@coderabbitai

coderabbitai Bot commented Aug 12, 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: 7a8adfa1-156e-47a2-b39d-d67e84caa04f

📥 Commits

Reviewing files that changed from the base of the PR and between ecd891b and 1aa9e67.

📒 Files selected for processing (7)
  • include/mruby/internal.h
  • mrbgems/mruby-regexp/include/re_internal.h
  • mrbgems/mruby-regexp/mrbgem.rake
  • mrbgems/mruby-regexp/src/re_compile.c
  • mrbgems/mruby-regexp/src/re_utf8.c
  • mrbgems/mruby-string-ext/src/string.c
  • src/string.c

📝 Walkthrough

Walkthrough

The 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.

Changes

UTF-8 decoder migration

Layer / File(s) Summary
Shared UTF-8 decoder
include/mruby/internal.h, src/string.c
Declares and implements mrb_utf8_decode. The helper reports decoded codepoints and consumed byte lengths, and handles ASCII, invalid, and truncated sequences.
Regexp decoder migration
mrbgems/mruby-regexp/include/re_internal.h, mrbgems/mruby-regexp/src/re_utf8.c, mrbgems/mruby-regexp/src/re_compile.c, mrbgems/mruby-regexp/mrbgem.rake
Removes the regexp-specific decoder and updates regexp character parsing and compilation to use mrb_utf8_decode.
String extension decoder migration
mrbgems/mruby-string-ext/src/string.c
Updates utf8code to use mrb_utf8_decode while preserving length reporting and ArgumentError handling.

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

Possibly related PRs

  • mruby/mruby#7094: Replaces private regexp UTF-8 helpers with shared core APIs.
  • mruby/mruby#7105: Modifies UTF-8 decoding validation in the string extension.
  • mruby/mruby#7123: Replaces duplicated UTF-8 decoding logic with shared core APIs.

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 sharing the UTF-8 decoder across core, mruby-regexp, and mruby-string-ext.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 e6e2454 into mruby:master Aug 12, 2026
21 checks passed
@takumin
takumin deleted the utf8-share-decoder branch August 13, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants