string.c: keep one answer about a string's bytes, not three flags - #7158
Conversation
`MRB_STR_BINARY` is the whole of the answer to how a string's bytes are
read: a string carrying it is read by byte, a string without it is read
the way the build reads strings by default. The macros around it are
named after the bit rather than after the question, so every write names
one value of the field instead of the field itself:
RSTR_SET_BINARY_FLAG(s);
RSTR_UNSET_BINARY_FLAG(s);
RSTR_COPY_BINARY_FLAG(dst, src);
`include/mruby/string.h` now names the field and the values it takes:
#define MRB_STR_ENCODING_DEFAULT 0
#define MRB_STR_ENCODING_BINARY 1
#ifdef MRB_UTF8_STRING
# define MRB_STR_ENCODING_UTF8 MRB_STR_ENCODING_DEFAULT
#endif
#define RSTR_ENCODING(s)
#define RSTR_ENCODING_SET(s, e)
#define RSTR_ENC_COPY(dst, src)
and the 22 writes and copies go through them. `RSTR_BINARY_P()` stays,
since whether a string is read by byte is a question about the encoding
and not about the shape it is kept in. It reads the field now and
answers 1 rather than 16, which is the narrowing `re_binary_string_p()`
was doing on the way out.
Index 0 is the build's default because a fresh string is zero filled:
the strings the parser hands over, and the ones numbers and times spell
themselves with, then say what they are without a store on the path
every string is made on. `__ENCODING__` names index 0 for a given build,
so it is UTF-8 where `MRB_UTF8_STRING` is set and ASCII-8BIT where it is
not.
Binary cannot share index 0 with the default in a build that indexes by
byte anyway. `String#b` and `Integer#chr` mark strings there too, and
`mruby-regexp` reads the mark through `re_binary_string_p()` to decide
whether to walk the subject a byte at a time, so folding the two
together would change what that build answers.
`MRB_STR_ENCODING_UTF8` is defined only where the build carries UTF-8. A
build without it has no UTF-8 string to name, so naming one should stop
the compiler rather than quietly write the default. The only place that
names it is `String#force_encoding`, in the gem that sets the define.
Every write is bit for bit the write it replaces, so no answer changes
and no test comes with this. Building full-core with `mruby-encoding`
and without it, every object file is identical instruction for
instruction to the one master builds, and `.text` over the whole build
is unchanged in both.
Reading a string's bytes as the encoding it carries comes back with one
of four answers: not asked yet, nothing but ASCII, read whole and sound,
read and found broken. `include/mruby/string.h` kept them as three
flags, `MRB_STR_SINGLE_BYTE`, `MRB_STR_VALID_ENC` and
`MRB_STR_BROKEN_ENC`, each with a predicate, a set, an unset and a copy
of its own. No writer sets two of them, so the answers are exclusive
already, and what the flags leave to the readers is the map from
combinations back to answers: `mrb_str_valid_encoding_p()` answered TRUE
off `MRB_STR_SINGLE_BYTE` and then wrote `MRB_STR_VALID_ENC` beside it
because the first implies the second, and `str_replace()` wrote three
copies in a row so that a copy would stand where its source stood.
The answer gets a name of its own, spelled as CRuby spells it in
`ENC_CODERANGE_UNKNOWN` and the rest:
#define MRB_STR_CODERANGE_UNKNOWN 0
#define MRB_STR_CODERANGE_7BIT 1
#define MRB_STR_CODERANGE_VALID 2
#define MRB_STR_CODERANGE_BROKEN 3
RSTR_CODERANGE(s)
RSTR_CODERANGE_SET(s, cr)
RSTR_ENC_CR_COPY(dst, src)
RSTR_ENC_CR_COPY_FOR_SUBSTR(dst, src)
The three bits stay exactly where they are and the accessors are written
over them, so what changes here is the vocabulary and not the layout.
UNKNOWN is 0 because a fresh string is filled with zeroes and has been
asked nothing.
The encoding travels with the answer in one macro rather than two, since
a copy that keeps one and drops the other is how flags went missing on
copy paths before. There are two of them because a subrange is not an
exact copy: a cut can leave a character in pieces, and it can cut away
the piece that spelled none, so a subrange inherits neither soundness
nor brokenness. Nothing but ASCII survives being cut anywhere, so 7BIT
is the one answer `mrb_str_byte_subseq()` carries over. CRuby splits the
same two ways, in `rb_enc_cr_str_exact_copy()` and
`rb_enc_cr_str_copy_for_substr()`.
`RSTR_SET_ASCII_FLAG()` goes with the flag it aliased, and its 11
callers in `src/symbol.c`, `src/object.c`, `src/numeric.c`,
`mruby-time` and `mruby-string-ext` say `MRB_STR_CODERANGE_7BIT`
instead, so there is one word for the answer rather than two.
`RSTR_CODERANGE_SET()` writes all three bits where the old sets wrote
one, so each converted write is a place where clearing the other two has
to lose nothing. Three are worth naming:
* `mrb_str_valid_encoding_p()` no longer writes "sound" onto a string
that is already nothing but ASCII, since 7BIT says that too. It is
the one write that goes rather than moves.
* `mrb_str_modify_keep_ascii()` and `mrb_str_modify()` are where every
in-place write passes and where the answer stops holding. As a pair
they took soundness and brokenness away and then took ASCII away as
well; now the first keeps 7BIT and writes UNKNOWN over anything
else, and the second writes UNKNOWN.
* `String#*` still refuses to call a nought times repetition broken.
The receiver's answer is copied and then taken back off the empty
result, which is where it stood before.
A build without `MRB_UTF8_STRING` reads every byte as a character and
asks the bytes nothing, so `RSTR_CODERANGE()` there is 7BIT and
`RSTR_CODERANGE_SET()` records nothing, which is what
`RSTR_SINGLE_BYTE_P()` and `RSTR_VALID_ENC_P()` being TRUE said. The two
copies stay outside the define, so the encoding still travels there.
No answer changes, so no test comes with this. Building full-core with
`mruby-encoding` and without it, the byte indexed build is identical
instruction for instruction to what master builds. The UTF-8 build loses
1,424 bytes of `.text` over the whole build: `src/string.o` loses 1,616,
and 4 files gain between 16 and 80 bytes where a one bit set became a
three bit write. The largest single movers inside `src/string.o` are
`mrb_str_resize()` and `mrb_str_to_s()`, which are the inliner
rearranging around a smaller `str_replace()` rather than code that went
away where it stood.
|
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 (11)
📝 WalkthroughWalkthroughThe string metadata API now separates encoding from coderange. Core string operations and related gems use the new accessors, setters, and copy helpers. ASCII, UTF-8, binary, and broken states use the unified coderange model. ChangesString metadata migration
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to This change consolidates string encoding state behind shared accessors without an identified current-head correctness or integration issue; no actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
b938640 to
f51a52e
Compare
Stacked on #7157, which is the first commit here. The second commit is this PR's own change, and the diff to read is
git diff <first commit>..HEAD. Merging #7157 first leaves this one a single commit.Reading a string's bytes as the encoding it carries comes back with one of four answers: not asked yet, nothing but ASCII, read whole and sound, read and found broken.
include/mruby/string.hkeeps them as three flags,MRB_STR_SINGLE_BYTE,MRB_STR_VALID_ENCandMRB_STR_BROKEN_ENC, each with a predicate, a set, an unset and a copy of its own.No writer sets two of them, so the answers are exclusive already. What the three flags leave to their readers is the map from combinations back to answers:
This PR gives the answer a name of its own. The three bits stay exactly where they are and the accessors are written over them, so what changes is the vocabulary and not the layout.
What goes into
include/mruby/string.hThe four answers are the set CRuby closes on in
ENC_CODERANGE_UNKNOWN,_7BIT,_VALIDand_BROKEN, and the names follow. UNKNOWN is 0 for the same reason it is 0 there: a fresh string is filled with zeroes and has been asked nothing.The values are small integers rather than the flag positions CRuby uses, because here they are not positions of anything: they sit over three bits that are not adjacent, and
MRB_STR_SINGLE_BYTE|MRB_STR_VALID_ENCis not the bit pattern of any answer. A build withoutMRB_UTF8_STRINGreads every byte as a character and asks the bytes nothing, which is where 7BIT stands, soRSTR_CODERANGE()is that constant there andRSTR_CODERANGE_SET()records nothing. That is whatRSTR_SINGLE_BYTE_P()andRSTR_VALID_ENC_P()beingTRUEsaid before.Two copies, and why they carry the encoding too
The encoding travels with the answer in one macro rather than beside it in another, since a copy that keeps one and drops the other is how flags went missing on copy paths before (f71bd22 and the whole of #7136 are that shape: a path that copied the bytes and left the flag behind on the original). The two copies stay outside the
#ifdef, so a byte indexed build still carries the encoding across.There are two because a subrange is not an exact copy. A cut can leave a character in pieces, and it can cut away the piece that spelled none, so a subrange inherits neither soundness nor brokenness. Nothing but ASCII survives being cut anywhere, so 7BIT is the one answer
mrb_str_byte_subseq()carries over. CRuby splits the same two ways, inrb_enc_cr_str_exact_copy()andrb_enc_cr_str_copy_for_substr(); the second of those also walks the subrange to promote VALID to 7BIT, which is not what this code does today and is not added here.The sites
RSTR_SINGLE_BYTE_P14,RSTR_VALID_ENC_P1,RSTR_BROKEN_ENC_P1mrb_str_byte_subseq(),str_replace(),String#*RSTR_SET_ASCII_FLAGsrc/object.c(4),mruby-string-ext(3),src/symbol.c(2),src/numeric.c(2),mruby-time(1)RSTR_SET_ASCII_FLAG()was an alias forRSTR_SET_SINGLE_BYTE_FLAG(), and it goes with the flag it aliased. Its callers sayRSTR_CODERANGE_SET(s, MRB_STR_CODERANGE_7BIT), so there is one word for the answer rather than two.Writes where one flag became three bits
RSTR_CODERANGE_SET()writes all three bits where the old sets wrote one, so every converted write is a place where clearing the other two has to lose nothing. It does, because no writer sets two of them and 7BIT is the one answer that implies another. Three are worth naming:mrb_str_valid_encoding_p()no longer writes "sound" onto a string that is already nothing but ASCII. It is the one write that goes rather than moves.mrb_str_modify_keep_ascii()andmrb_str_modify()are where every in-place write passes and where the answer stops holding. As a pair they took soundness and brokenness away and then took ASCII away as well; now the first keeps 7BIT and writes UNKNOWN over anything else, and the second writes UNKNOWN.str_modify_cat(), which reaches neither when it appends into a shared buffer, writes UNKNOWN in one go instead of clearing three bits.String#*still refuses to call a nought times repetition broken. The receiver's answer is copied and then taken back off the empty result, which is where it stood before.Generated code
Two full-core builds, with
mruby-encodingand with the gem removed from the box, gcc-O3, measured against the tree with #7157 alone:.textover the whole buildmruby-encodingMRB_UTF8_STRINGWhere the UTF-8 build moves:
.textsrc/string.omruby-string-extsrc/symbol.osrc/object.omruby-timeSix objects change at all, and
src/numeric.ochanges without growing. The four that grow are the files whose only change isRSTR_SET_ASCII_FLAG()becoming a write of one answer: a bit set turns into a read, a mask and an or. Insidesrc/string.othe sites this PR touches move by tens of bytes in both directions (mrb_str_times()+68,mrb_str_char_len()+64,mrb_str_modify_keep_ascii()+64,mrb_str_valid_encoding_p()-41), and the largest single movers are functions this PR does not touch at all:mrb_str_resize()1057 -> 208 andmrb_str_to_s()665 -> 110, where the inliner rearranges around astr_replace()that is now four macro expansions shorter.Testing
rake -m teston both builds above, plus the C++ ABI build, all green:MRB_UTF8_STRING: 2291 tests, 2281 OK, 10 skip, 0 KO, 0 crash, 0 warnings.mruby-encoding, where strings index by byte: 2229 tests, 2200 OK, 29 skip, 0 KO, 0 crash, 0 warnings.enable_cxx_abi: 2291 tests, 2281 OK, 10 skip.No test accompanies the change. No string answers anything different, so there is nothing new to pin.
Not in this PR
The three bits are not folded into a two bit field, and none of them moves.
MRB_STR_SINGLE_BYTE,MRB_STR_VALID_ENCandMRB_STR_BROKEN_ENCkeep their values and are now named only inside the two accessors.Summary by CodeRabbit