string.c: drop a case walk's check that nothing reaches - #7191
Conversation
`mrb_str_case_convert_unicode()` asks twice whether the string it was handed holds anything to walk: ```c if (RSTR_BINARY_P(s) || str_ascii_p(s)) return -1; str_modify_keep_cr(mrb, s); if (RSTR_LEN(s) == 0 || RSTR_PTR(s) == NULL) return -1; ``` The second question has no answer left to give. `str_ascii_p()` answers FALSE only out of its scan, and the scan hands back a pointer other than `e` only where it read a byte with the high bit set. A string that gets past the first line therefore has a length above zero and a pointer that was read through, and `str_modify_keep_cr()` changes neither: it unshares the buffer, which allocates, and clears a coderange that says broken. An empty string leaves through the first line rather than the second. `search_nonascii(p, e)` does not enter its loop when `p == e`, so it hands back `e`, `str_ascii_p()` records the string as holding nothing but ASCII and answers TRUE, and the `-1` sends the caller back to the byte loop it has of its own, where the same question waits: ```c if (len == 0 || p == NULL) return mrb_nil_value(); ``` The line arrived with the function, in dcddbd4, where those loops are what it was written beside. Nothing tells the compiler what `str_ascii_p()` answers for an empty string, so `string.o` carries the check: one test where the string is embedded and two where it is on the heap, all three jumping to the `return -1` that the first line has already reached. ``` a51b: a9 f0 01 00 00 test $0x1f0,%eax # embedded length a520: 74 86 je a4a8 # to return -1 ... a88d: 4c 8b 71 10 mov 0x10(%rcx),%r14 # heap length a891: 4d 85 f6 test %r14,%r14 a894: 0f 84 0e fc ff ff je a4a8 a89a: 48 8b 69 20 mov 0x20(%rcx),%rbp # heap pointer a8a3: 48 85 ed test %rbp,%rbp a8a6: 0f 84 fc fb ff ff je a4a8 ``` Dropping it takes 32 bytes of `.text` out of each build that compiles the walk, and leaves a build whose strings index by byte where it was, since the walk is behind `MRB_UTF8_STRING` and is not compiled there at all.
|
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)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughThe Unicode case-conversion path no longer returns early for empty strings or null pointers after binary and ASCII checks. It proceeds directly to ChangesUnicode case conversion
Estimated code review effort: 1 (Trivial) | ~2 minutes Merge Risk: ⚪ Minimal · up to This change removes a redundant case-conversion check without changing behavior; the reported tests and sanitizer runs remain green, so no actionable merge-blocking risk remains. Possibly related PRs
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 |
mrb_str_case_convert_unicode(), the walkString#upcase,#downcase,#capitalizeand#swapcasereach for where a string holds a character the case tables speak about, asks twice whether there is anything to walk:The second question has no answer left to give, and this drops it.
Why the first line has already answered
str_ascii_p()answers FALSE in one place only, out of its scan:and
search_nonascii()hands back something other thaneonly where it read a byte with the high bit set. So a string that gets past the first line has a length above zero and a pointer that was read through, which is both halves of the check below it.str_modify_keep_cr()changes neither: it unshares the buffer, which allocates, and clears a coderange that says broken.An empty string leaves through the first line rather than the second.
search_nonascii(p, e)does not enter its loop whenp == e, so it hands backe,str_ascii_p()records the string as holding nothing but ASCII and answers TRUE, and the-1sends the caller back to the byte loop it has of its own, where the same question waits:That is where the line came from: it arrived with the function, in dcddbd4, written beside those loops.
Generated code
gcc -O3,ci/gcc-clangbintest, against the same object built from master. The check is emitted, since nothing tells the compiler whatstr_ascii_p()answers: one test where the string is embedded and two where it is on the heap, all three jumping to thereturn -1ata4a8that the first line has already reached.master, on return from
str_modify_keep_cr():this branch, where the same two paths walk straight into the conversion:
Size
.text, master then this branch:string.obin/mruby-110-112-32-32-32-32The byte-indexed build is unchanged byte for byte: the walk is behind
MRB_UTF8_STRINGand is not compiled there at all.Testing
rake -m test, all green with 0 KO, 0 crash, and no new warnings:build_config/asan.rbBehaviour is compared directly as well. 1160 case conversions, over 29 receivers spanning empty, ASCII, multi-byte, title case, one-to-many (
"ß","fi"), astral, broken and truncated UTF-8, each in five forms (literal,dup,+ "",[0, length],* 1) and throughupcase,downcase,capitalize,swapcaseand their!forms, with the length,valid_encoding?andbytesizeread afterwards: the output is identical to master byte for byte, and identical again underaddress,undefinedwith no report.Relation to #7190
None, in either direction. #7190 adds an
#elif defined(MRB_UTF8_STRING)arm after this function's closing brace, three lines below the line dropped here, and leaves the line itself alone. The two merge cleanly whichever lands first; this branch is on master.Environment
Versions, and the compile line of every build named above
build_config/asan.rbpicksg++forcxx_abiWhat each build compiles
src/string.cwith,-MMD -c, the-Ipaths and-ostripped:-g -O3is what thegcctoolchain sets.full-debugand the sanitizer build then append-g3 -O0throughenable_debug(), so those two are-O0, not-O3.cxx_abiis the C compiler driven as C++ with-x c++ -std=gnu++03, andg++links it.Summary by CodeRabbit