Skip to content

string.h: name what makes a character index a byte index - #7178

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:string-single-byte-predicate
Aug 15, 2026
Merged

string.h: name what makes a character index a byte index#7178
matz merged 1 commit into
mruby:masterfrom
takumin:string-single-byte-predicate

Conversation

@takumin

@takumin takumin commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Ten places ask the same question of a string and spell it out each time:

if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {

The two halves arrive from different sides, but they say one thing to every reader of them: a character index into this string is already a byte index. mrb_str_char_to_byte() returns the index it was handed, mrb_str_byte_to_char() likewise, mrb_str_check_byte_pos() has no boundary left to check, and mrb_str_rindex_m() hands the whole search to mrb_str_byterindex_m().

This PR gives that question a name.

What goes into include/mruby/string.h

#define RSTR_SINGLE_BYTE_P(s) \
  (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s))

It sits next to RSTR_BINARY_P, which is the other predicate derived from a field rather than read straight out of one.

This is not MRB_STR_SINGLE_BYTE coming back. That was a stored flag, one bit a writer had to set and keep true; this is derived from what the string already carries, so nothing stores it and nothing can leave it stale. RSTR_CODERANGE() folds to MRB_STR_CODERANGE_7BIT on a build without MRB_UTF8_STRING, where every byte is a character, so the predicate folds to a constant there as it did before.

The call sites are all of them

Ten on master, and no eleventh:

$ git grep -nE 'CODERANGE_7BIT.*RSTR_BINARY_P|RSTR_BINARY_P.*CODERANGE_7BIT'
mrbgems/mruby-string-ext/src/string.c:1036:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {
mrbgems/mruby-string-ext/src/string.c:1126:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {
mrbgems/mruby-string-ext/src/string.c:1169:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {
mrbgems/mruby-string-ext/src/string.c:1204:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {
mrbgems/mruby-string-ext/src/string.c:1802:  if (RSTR_CODERANGE(s) != MRB_STR_CODERANGE_7BIT && !RSTR_BINARY_P(s)) {
src/string.c:731:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {
src/string.c:772:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {
src/string.c:2128:    if (!RSTR_BINARY_P(s) && RSTR_CODERANGE(s) != MRB_STR_CODERANGE_7BIT &&
src/string.c:2440:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) return;
src/string.c:2794:  if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s)) {

and after, one definition and the same ten:

$ git grep -c RSTR_SINGLE_BYTE_P
include/mruby/string.h:1
mrbgems/mruby-string-ext/src/string.c:5
src/string.c:5

$ git grep -nE 'CODERANGE_7BIT.*RSTR_BINARY_P|RSTR_BINARY_P.*CODERANGE_7BIT'
include/mruby/string.h:189:  (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BIT || RSTR_BINARY_P(s))

Eight were the positive form above. Two were the negation, and one of those spells the halves in the other order:

/* src/string.c, mrb_str_chomp_bang() */
-    if (!RSTR_BINARY_P(s) && RSTR_CODERANGE(s) != MRB_STR_CODERANGE_7BIT &&
-        mrb_utf8_char_head(p, pp, p + len) != pp) {
+    if (!RSTR_SINGLE_BYTE_P(s) && mrb_utf8_char_head(p, pp, p + len) != pp) {

/* mrbgems/mruby-string-ext/src/string.c, str_chars_ary() */
-  if (RSTR_CODERANGE(s) != MRB_STR_CODERANGE_7BIT && !RSTR_BINARY_P(s)) {
+  if (!RSTR_SINGLE_BYTE_P(s)) {

str_chars_ary() had !A && !B with the halves in the macro's order, so !(A || B) is what it spelled. mrb_str_chomp_bang() had !B && !A, and that is the one place where the macro asks for something other than what the source said.

The one thing that changes in the object code

Nine call sites expand to the instructions they had. mrb_str_chomp_bang() tests the two halves in the other order, master above and this branch below, from bintest:

    5986:  mov    %r14d,%eax
    5989:  and    $0x1800,%eax        <- the encoding index, for RSTR_BINARY_P
    598e:  cmp    $0x800,%eax
    5993:  je     59c8 <mrb_str_chomp_bang+0x268>
    5995:  mov    %r14d,%eax
    5998:  and    $0x600,%eax         <- the coderange
    599d:  cmp    $0x200,%eax
    59a2:  je     59c8 <mrb_str_chomp_bang+0x268>

    5986:  mov    %r14d,%eax
    5989:  and    $0x600,%eax         <- the coderange first
    598e:  cmp    $0x200,%eax
    5993:  je     59c8 <mrb_str_chomp_bang+0x268>
    5995:  mov    %r14d,%eax
    5998:  and    $0x1800,%eax
    599d:  cmp    $0x800,%eax
    59a2:  je     59c8 <mrb_str_chomp_bang+0x268>

Same instructions, same count, the two tests swapped. Both are pure reads of one word already in a register, so neither order can raise, and the second test is reached only when the first says no in either order. That is the whole of the difference between master and this branch in the object code, over every build measured below.

Generated code

gcc 13.3.0 -O3, x86-64, against the same objects built from master. .text of bin/mruby:

build master this PR
full-debug 2636642 2636642 (±0)
bintest 1820341 1820341 (±0)
cxx_abi 1850166 1850166 (±0)
byte-string 1795973 1795973 (±0)
build_config/default.rb 1722135 1722135 (±0)
32-bit, full-core, i686-linux-gnu-gcc 1994304 1994304 (±0)

Objects differing, comparing objdump -d over every .o in the build:

build objects differing of which differ in size
full-debug 291 1 none
bintest 301 1 none
cxx_abi 291 1 none
byte-string 288 0 none
build_config/default.rb 270 0 none
32-bit 291 1 none

The one object is src/string.o, and the swap above is what differs in it. A build that indexes by byte carries no coderange, so RSTR_SINGLE_BYTE_P() folds to true there and both orders fold with it: byte-string and default.rb come out identical to master, object by object.

Testing

rake -m test, all green, 0 KO, 0 crash, 0 warnings, and the same counts as master:

build Total OK Skip
full-debug 2303 2300 3
bintest 2304 2293 11
bintest, the binary tests 117 117 0
cxx_abi 2304 2293 11
byte-string 2240 2194 46
build_config/default.rb 2086 2040 46
32-bit, full-core, i686-linux-gnu-gcc 2284 2272 12
the same, with enable_debug 2284 2280 4

build_config/host-m32.rb needs a multilib toolchain, so the 32-bit build above is full-core with the compiler set to i686-linux-gnu-gcc instead.

No test accompanies the change. Nine call sites are the expression they were and the tenth is the same two tests in the other order, so no string answers anything different and there is nothing new to pin.

Not in this PR

The macro is put where the ten call sites already asked the question, and nowhere else: no path that walks a string gains a shortcut it did not have. The write side of these two fields is untouched, and so is what either field means.

Summary by CodeRabbit

  • Refactor
    • Introduced a shared predicate for "one character per byte" and routed the ten places that spelled it out through it, across string indexing, reverse searching, line trimming, character enumeration, and scrubbing.

Nine places asked the same question of a string and spelled it out each
time: whether its coderange stands at 7BIT, or its bytes are read as
bytes. Both answers mean the same thing to the caller, which is that a
character index into the string is already a byte index. `mrb_str_char_to_byte`
returns the index it was handed, `mrb_str_byte_to_char` likewise,
`mrb_str_check_byte_pos` has no boundary to check, and `mrb_str_rindex_m`
hands the whole search to `mrb_str_byterindex_m`.

`RSTR_SINGLE_BYTE_P` gives that question a name. It is derived from what
the string carries rather than carried alongside it, so it is not the
`MRB_STR_SINGLE_BYTE` flag coming back: nothing stores it and nothing has
to keep it up to date. The expansion is what the call sites spelled, so
nothing changes in what is generated.
@coderabbitai

coderabbitai Bot commented Aug 15, 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: e0213ed8-78f2-4810-a99d-a092bd94e927

📥 Commits

Reviewing files that changed from the base of the PR and between 406205f and 33723b6.

📒 Files selected for processing (3)
  • include/mruby/string.h
  • mrbgems/mruby-string-ext/src/string.c
  • src/string.c

📝 Walkthrough

Walkthrough

The change adds RSTR_SINGLE_BYTE_P and replaces repeated ASCII-or-binary checks in core string and string-extension operations.

Changes

Single-byte string handling

Layer / File(s) Summary
Single-byte predicate contract
include/mruby/string.h
Adds RSTR_SINGLE_BYTE_P for ASCII-only and binary strings.
String extension adoption
mrbgems/mruby-string-ext/src/string.c
Updates ord, scrubbing, codepoint, and character operations to use the predicate.
Core string operation adoption
src/string.c
Updates character/byte conversion, chomp!, byte-position validation, and rindex to use the predicate.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 33723

This change names an existing single-byte string condition and replaces equivalent inline checks without changing generated size or tested behavior; no actionable merge-blocking risk remains after normal checks and review.

Possibly related PRs

  • mruby/mruby#7158: Shares the string encoding and coderange refactoring used by this change.
  • mruby/mruby#7157: Modifies the same string encoding macros and byte-oriented checks.
  • mruby/mruby#7106: Updates related UTF-8 handling in string-extension operations.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: defining what makes character indexes equivalent to byte indexes in string handling.
✨ 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.

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