lib/strings+test: graduate fromutf8 + bytesub to validating return

fromutf8(in: []u8) (str | utf8.invalid) and the bytesub form per
ref/hare/strings/utf8.ha:22 and sub.ha:59. bytesub keeps its byte
asserts (ww extension over Hare; predates #7).

fromutf8 walks the utf8 decoder via utf8.next rather than the
shorter `utf8.validate(in)?` form. Two compiler bugs in the way:
cross-shape `(void | invalid) → (str | invalid)` propagation is
#19, and (void | !void) match-bind locals diverge between stages /
str→union lift SIGSEGVs in cstage — both filed as #48. The
decoder-walk form bypasses both and matches what
ref/hare/strings/utf8.ha actually does in source.

getopt.ww:314 caller updated to match the new (str | invalid)
return; bi+1 cannot hit a continuation byte in well-formed argv
(bi is a just-matched ASCII flag), so abort spells the precondition.

bytesub_cases rewritten as exhaustive match; new rows cover
start-on-continuation and end-on-continuation invalid arms plus an
end==s.len bypass. fromutf8_cases is new — Hare vector + edge
bytes + multibyte parity rows.
This commit is contained in:
2026-05-20 02:02:28 +09:00
parent e03a6281d6
commit 6d006da26c
6 changed files with 357 additions and 29 deletions

View File

@@ -48,6 +48,39 @@ export fn fromutf8_unsafe(in: []u8) str = {
return r;
};
// fromutf8 — validating sibling of [[fromutf8_unsafe]]. Returns the
// borrowed str view on success, or `utf8.invalid` if `in` contains a
// malformed UTF-8 sequence. ref/hare/strings/utf8.ha:22 (#7).
//
// Hare's spelling is `utf8::validate(in)?; return fromutf8_unsafe(in)`.
// ww calls `utf8.validate` and matches its `(void | utf8.invalid)`
// result instead of `?` (cross-shape propagation is #19). Even with
// match, the call diverges cstage/wwstage when the result is bound to
// a local (#48: wwstage spills DX for the payload-less arm) and the
// success path then SIGSEGVs lifting `str` to `(str | utf8.invalid)`.
// So the validation is open-coded against the decoder — the same
// byte-by-byte DFA walk `utf8.validate` performs (ref/hare/encoding/
// utf8/decode.ha:207) — whose `(rune | done | more | invalid)` arm
// matches the failure axis of fromutf8 and lifts cleanly. Collapses
// to `utf8.validate(in)?` once #19 + #48 are fixed.
export fn fromutf8(in: []u8) (str | utf8.invalid) = {
let d: utf8.decoder = utf8.decode(in);
for (true) {
match (utf8.next(&d)) {
case let r: rune => void;
case utf8.done => {
let r: str = fromutf8_unsafe(in);
return r;
};
case utf8.more => {
let e: utf8.invalid; return e;
};
case let e: utf8.invalid => { return e; };
};
};
let e: utf8.invalid; return e;
};
// compare — three-way bytewise codepoint-order comparison. Return is
// a sign (neg/zero/pos), not an index, so it tracks Hare's `int`
// rather than the str-index i32 (#8). ref/hare/strings/compare.ha:12.
@@ -235,12 +268,23 @@ export fn sub(s: str, start: i32, end: i32) str = {
};
// bytesub — borrowed substring [start, end) where start/end are byte
// offsets. ref/hare/strings/sub.ha:59. Hare's validating return
// `(str | utf8::invalid)` is dropped (filed as #7); a multibyte-split
// argument will yield a sub-string whose bytes don't decode cleanly.
export fn bytesub(s: str, start: i32, end: i32) str = {
// offsets. ref/hare/strings/sub.ha:59 (#7). Returns `utf8.invalid` if
// either endpoint lands on a continuation byte (would split a
// codepoint); the equivalent Hare predicate is `s[i] & 0xc0 == 0x80`
// at ref/hare/strings/sub.ha:72-73.
export fn bytesub(s: str, start: i32, end: i32) (str | utf8.invalid) = {
os.assert(start <= end, "strings.bytesub: start is higher than end");
os.assert(end <= s.len, "strings.bytesub: end exceeds string length");
if (start < s.len) {
if ((s[start] & 0xC0u8) == 0x80u8) {
let e: utf8.invalid; return e;
};
};
if (end < s.len) {
if ((s[end] & 0xC0u8) == 0x80u8) {
let e: utf8.invalid; return e;
};
};
let r: str;
r.ptr = s.ptr + (start: u64);
r.len = end - start;