Skip to content

string.c: write MRB_STR_SINGLE_BYTE through the accessors it has - #7156

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:single-byte-flag-through-accessors
Aug 14, 2026
Merged

string.c: write MRB_STR_SINGLE_BYTE through the accessors it has#7156
matz merged 1 commit into
mruby:masterfrom
takumin:single-byte-flag-through-accessors

Conversation

@takumin

@takumin takumin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

#7150 gave MRB_STR_BINARY the two accessors it was missing and listed what it deliberately left behind. The first item on that list:

mrbgems/mruby-string-ext/src/string.c, in str_ascii_only_p(): flags |= MRB_STR_SINGLE_BYTE, where RSTR_SET_SINGLE_BYTE_FLAG() exists but compiles to nothing without MRB_UTF8_STRING, so swapping it in is a behavior question, not a spelling one.

This PR answers that question, and picks up two more writes the grep behind that list could not see.

The three sites

/* mrbgems/mruby-string-ext/src/string.c, str_ascii_only_p() */
mrb_str_ptr(str)->flags |= MRB_STR_SINGLE_BYTE;

/* src/string.c, str_escape() */
mrb_str_ptr(str)->flags |= src_sb_flag;
mrb_str_ptr(result)->flags |= sb_flag;

The grep in #7150 was for flags |= MRB_STR_SINGLE_BYTE, so it found the first and not the other two: those spell the value as a local holding MRB_STR_SINGLE_BYTE or zero, which is the same write with the constant moved a few lines up.

MRB_STR_SINGLE_BYTE is not short of accessors the way MRB_STR_BINARY was. It already has all four, so nothing is added to include/mruby/string.h here; the three sites simply start using what is there.

str_ascii_only_p(): what the behavior question comes to

The site sits outside the #ifdef MRB_UTF8_STRING block that gives the flag its meaning, so the raw write also runs in a build without that define. What it writes there is a bit nothing on a string reads:

  • RSTR_SINGLE_BYTE_P() is TRUE for every string in that build, so no reader of the flag consults the bit.
  • The one other flag at bit 5, MRB_FL_OBJ_SHAPED, is only ever read or cleared through a struct RObject* (src/variable.c), never on a string.

So the behavior question comes to this: the MRB_UTF8_STRING build keeps writing exactly what it wrote before, and the build without it stops making a write no one can observe. That is the whole of the difference.

The accessor used is RSTR_SET_ASCII_FLAG(), not RSTR_SET_SINGLE_BYTE_FLAG() directly. They are the same macro, but the first is the name this codebase uses at the moment it knows the bytes in hand are ASCII, including twice in this very file (int_chr_binary(), int_chr_utf8()) and in src/object.c, src/symbol.c and mruby-time. str_ascii_only_p() has just walked the bytes and proved precisely that. It also passes the struct RString* it already holds as s, matching the two reads above it, rather than calling mrb_str_ptr(str) a second time.

str_escape(): two locals that carry a bit

The two locals exist so that the walk can record what it saw and the write can happen once at the end. Carrying the flag bit itself is what let that write be an unconditional |=; with the accessor, a plain truth value does the same job:

-  uint32_t sb_flag = MRB_STR_SINGLE_BYTE;      /* what `result` comes out as */
-  uint32_t src_sb_flag = MRB_STR_SINGLE_BYTE;  /* what the walk found `str` to be */
+  mrb_bool sb_flag = TRUE;      /* whether `result` comes out single byte */
+  mrb_bool src_sb_flag = TRUE;  /* whether the walk found `str` single byte */

Both stay under the #ifdef they are already in. Only a build that reads whole characters out of str has an answer to record at all.

Generated code

gcc -S -O3 over both changed files, with MRB_UTF8_STRING and without, so all four combinations:

file build assembly .text
src/string.c MRB_UTF8_STRING differs inside str_escape() only 59986 -> 59842 (-144)
src/string.c without identical unchanged
mruby-string-ext MRB_UTF8_STRING identical unchanged
mruby-string-ext without differs inside str_ascii_only_p() only 21340 -> 21308 (-32)

Both moves shrink. In str_escape() the flags word now takes a constant under a branch instead of having a runtime value inserted into the bitfield; in str_ascii_only_p() the insert disappears with the write. Every differing line falls inside those two functions, and no other function in either file changes.

Testing

MRUBY_CONFIG=ci/gcc-clang rake -m test, all builds green, 0 KO, 0 crash, 0 warnings:

  • full-core gembox with MRB_UTF8_STRING: 2286 tests, 2276 OK, 10 skip.
  • default gembox, where mruby-encoding is absent and strings index by byte: 2070 tests, 2042 OK, 28 skip.

No test accompanies the change. Neither String#ascii_only? nor String#inspect gives a different answer anywhere, so there is nothing new to pin.

Still left out

The other item on #7150's list, RSTR_SET_EMBED_LEN() written out by hand in mrbgems/mruby-sprintf/src/sprintf.c, is untouched. It is a different flag family, and folding it into RSTR_SET_LEN() next to a branch that sets the heap length is not a rename.

After this change, grep -rn "MRB_STR_SINGLE_BYTE" --include='*.c' src mrbgems finds one line, and it is a comment.

`include/mruby/string.h` names a way in and out of each string flag it
defines, and `MRB_STR_SINGLE_BYTE` has all four: a predicate, a set, an
unset and a copy. Three writes reach past them and name the bit:

    mrb_str_ptr(str)->flags |= MRB_STR_SINGLE_BYTE;  /* str_ascii_only_p */
    mrb_str_ptr(str)->flags |= src_sb_flag;          /* str_escape */
    mrb_str_ptr(result)->flags |= sb_flag;           /* str_escape */

The first is in `mruby-string-ext`, outside the `MRB_UTF8_STRING` guard
the flag's meaning sits behind, so it runs in a build without that
define as well, and there it sets a bit nothing on a string reads:
`RSTR_SINGLE_BYTE_P()` is `TRUE` for every string in that build, and
the one other flag at bit 5, `MRB_FL_OBJ_SHAPED`, is read through a
`struct RObject*` and never on a string. `RSTR_SET_ASCII_FLAG()` is
what the same file calls a few hundred lines above once it knows the
bytes in hand are ASCII, and it leaves the `MRB_UTF8_STRING` build
writing what it wrote before while dropping the write from the build
that has no reader for it. The `struct RString*` is already in hand as
`s`, so the accessor takes that instead of reaching through `str` a
second time.

The other two are in `str_escape()`, which carries the bit in
`uint32_t` locals so that the write at the end can be one `|=` of a
value that is either `MRB_STR_SINGLE_BYTE` or zero. The locals become
`mrb_bool` and the accessor does the writing. Both stay under the
`#ifdef` they are already in, since only a build that reads whole
characters out of `str` has an answer to record.

No answer any of the three gives changes, so no test comes with this.
`gcc -S -O3` over both files, with `MRB_UTF8_STRING` and without, moves
in two of the four combinations, and in no function other than the two
above:

  * `str_escape()` with the define: the flags word takes a constant
    under a branch instead of a runtime value inserted into the
    bitfield, and `.text` for `src/string.c` loses 144 bytes.
  * `str_ascii_only_p()` without it: the insert goes, and `.text` for
    the gem loses 32 bytes.

The other two combinations are identical, byte for byte, and nothing
grows.
@takumin
takumin requested a review from matz as a code owner August 14, 2026 06:38
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@takumin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 seconds

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 97b7420a-4aba-4e43-970b-0c40f51e35fb

📥 Commits

Reviewing files that changed from the base of the PR and between 84a4186 and 0bffedb.

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

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 ddde773 into mruby:master Aug 14, 2026
20 of 21 checks passed
@takumin
takumin deleted the single-byte-flag-through-accessors branch August 14, 2026 06:49
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