string.h: fold the coderange's three bits into a two-bit field - #7170
Conversation
The coderange was three separate bits, one per answer, read in an order the reader had to fix: broken first, then nothing but ASCII, then sound. A bit per answer can spell states no writer makes, so that order was there only to give those a reading, and "not asked yet" was spelled by all three being clear rather than by a value of its own. The four answers are exclusive and the set is closed, the same set CRuby names with `ENC_CODERANGE_*`, so two bits hold every one of them and can spell nothing besides. `RSTR_CODERANGE` becomes a read of that field and `RSTR_CODERANGE_SET` a write of it, with `MRB_STR_CODERANGE_UNKNOWN` still 0 so the zeroed flags of a fresh string already say it. The write masks the answer to the field's width the way `RSTR_ENCODING_SET()` masks an index, and for a sharper reason: the bits beside this field are the embedded length rather than free ones, so a fifth answer written unmasked would not merely read back wrong, it would lengthen the string. The field goes in bits 4-5, the lowest pair the flags word has free, and `MRB_STR_SINGLE_BYTE`, `MRB_STR_VALID_ENC`, and `MRB_STR_BROKEN_ENC` go away, leaving bits 11-12 free. The accessors were already the only code reading or writing those bits, so the change stops at this header. What is built comes out smaller. Six objects change in each build that carries the field and `bin/mruby` loses about a kilobyte of `.text`, since a pair this low in the word takes a one byte immediate where bits 11 and 12 took a four byte one. A build without `MRB_UTF8_STRING` keeps no field at all and comes out identical, object for object.
|
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 (1)
📝 WalkthroughWalkthroughThe string header replaces three independent coderange flags with a two-bit field at bits 4–5. It adds field constants and updates coderange extraction and assignment macros with shifting and bounds masking. ChangesString coderange representation
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This PR changes how string coderange state is packed in the public header without changing the supported answers or observed behavior; no actionable merge-blocking risk remains beyond normal checks and review. 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 |
RStringkeeps what reading its bytes as the encoding they carry came back with, and it keeps that answer in three separate bits, one per answer:The four answers are exclusive, so three bits can spell states no writer makes, and the order above is there only to decide what such a state would read as. "Not asked yet" has no value of its own either: it is the three bits all being clear, which the reader has to know to spell as
MRB_STR_CODERANGE_UNKNOWN.This PR gives the answer bits of its own. The set is closed at four, the same set CRuby names with
ENC_CODERANGE_*, so two bits hold every one of them and can spell nothing besides.What goes into
include/mruby/string.hThe three constants take the shape
MRB_STR_EMBED_LEN_SHIFT/_BITS/_MASKandMRB_STR_ENCODING_SHIFT/_BITS/_MASKalready use for the other two fields in the word.MRB_STR_CODERANGE_UNKNOWNstays 0, so the zeroed flags of a freshly allocated string already say it and the path every string is made on still stores nothing.The three bits go with the answers they held. Since f51a52e put the last readers and writers on the accessors, every mention of them on master is inside this header, and nothing outside it changes:
Where the field sits
Bits 4-5 are the lowest pair the flags word has free. Bit 4 came free with 839b164, and bit 5 is the first of the three this PR folds.
MRB_STR_TYPE_MASKMRB_STR_TYPE_MASKMRB_STR_CODERANGE_MASKMRB_STR_SINGLE_BYTEMRB_STR_CODERANGE_MASKMRB_STR_EMBED_LEN_MASKMRB_STR_EMBED_LEN_MASKMRB_STR_VALID_ENCMRB_STR_BROKEN_ENCMRB_STR_ENCODING_MASKMRB_STR_ENCODING_MASKThree bits go in and two come out, so bits 11-12 come free as a pair. Where the field finally sits is for whenever the word is laid out afresh, as is where the encoding index finally sits: neither move is in this PR.
The write is masked to the field
RSTR_ENCODING_SET()masks its index to the width of its field as of f90657d. This write does the same, and here the reason is sharper: the bits above this field are the embedded length rather than free ones.A fifth answer is a mistake either way, and widening
MRB_STR_CODERANGE_BITSis what carrying one would need. What the mask decides is whether the mistake stays a wrong answer instead of becoming a longer string.The three bits leave a public header
include/mruby/string.his a public header, so an out of tree gem that names one of the three stops compiling. That is the point, and it matters more here than it did forMRB_STR_BINARYin 839b164: those values are not merely unread now, they name parts of a field that reads back as a different answer.s->flags |= 32(MRB_STR_SINGLE_BYTE)MRB_STR_CODERANGE_VALIDs->flags |= 2048(MRB_STR_VALID_ENC)MRB_STR_CODERANGE_UNKNOWNs->flags |= 4096(MRB_STR_BROKEN_ENC)An alias would keep every one of those compiling while it stopped meaning what it meant, which is the failure this shape is meant to prevent. A compile error names the lines to change, and what to change them to is the accessors that have been there since f51a52e:
s->flags & MRB_STR_SINGLE_BYTERSTR_CODERANGE(s) == MRB_STR_CODERANGE_7BITs->flags & MRB_STR_VALID_ENCRSTR_CODERANGE(s) == MRB_STR_CODERANGE_VALIDs->flags & MRB_STR_BROKEN_ENCRSTR_CODERANGE(s) == MRB_STR_CODERANGE_BROKENs->flags |= MRB_STR_SINGLE_BYTERSTR_CODERANGE_SET(s, MRB_STR_CODERANGE_7BIT)s->flags &= ~MRB_STR_SINGLE_BYTERSTR_CODERANGE_SET(s, MRB_STR_CODERANGE_UNKNOWN)Names in this family have left the header before:
MRB_STR_BINARYin 839b164,RSTR_SET_BINARY_FLAG()and its companions in 4848467, the unused unset macros in c18cd60, and the ASCII flag renamed toMRB_STR_SINGLE_BYTEin 57fd0ed.Builds that index by byte
A build without
MRB_UTF8_STRINGkeepsRSTR_CODERANGE()atMRB_STR_CODERANGE_7BITandRSTR_CODERANGE_SET()a no-op, so it carries no field at all, exactly as before.RSTR_ENC_CR_COPY()andRSTR_ENC_CR_COPY_FOR_SUBSTR()still copy the encoding there, whichstr_replace()needs in every build.Generated code
gcc -O3, against the same objects built from master. Each of the four buildsci/gcc-clangmakes:.textThe objects that differ are the ones that name the accessors:
src/string.o,src/object.o,src/symbol.o,src/numeric.o,mruby-string-ext, andmruby-time. Comparison isobjdump -dover every.oin the build; every other object is identical instruction for instruction, and inbyte-string, where the field does not exist, so is every object there is.The shrinking is the immediate. On x86-64 a mask over bits 4 and 5 fits one byte and one that reaches bit 11 or 12 does not. Where the compiler has the flags word shifted down to work on, as in
mrb_str_byte_subseq()asking whether the source is 7BIT and writing the answer, the instruction goes from six bytes to three:Where it works on the word in place instead, the mask is shifted up past a byte either way and the instruction keeps its width, which is why some of the six differ in what they hold without changing size.
This gives back what 839b164 took on when it put the encoding index in bits 13 and 14, for the field that is read and written far more often.
Testing
rake -m testoverci/gcc-clang, all four builds green, 0 KO, 0 crash, 0 warnings:byte-stringis the build withoutmruby-encoding, where strings index by byte and no field is carried. The other three are what pin the fold: every path that records an answer or copies one runs there,String#valid_encoding?andString#scrubfor the recording,String#*andString#replacefor the whole copy, byte slicing for the copy that carries 7BIT alone.No test accompanies the change. No string answers anything different, so there is nothing new to pin.
Not in this PR
The encoding index does not move, the field does not go to its final position, and nothing new writes a coderange.
Summary by CodeRabbit