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

@@ -93,6 +93,7 @@
package getopt;
import encoding.utf8;
import os;
import strings;
@@ -311,7 +312,19 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
} else {
// PARAM: glued value, or next argv slot.
if (bi + 1 < arg.len) {
let v: str = strings.bytesub(arg, bi + 1, arg.len);
// strings.bytesub now validates rune
// boundaries (#7). `bi` is the byte
// offset of the just-matched ASCII flag
// char, so `bi + 1` cannot land on a
// continuation byte in well-formed argv;
// abort spells the well-formedness
// precondition as Hare's `!` does.
let v: str;
match (strings.bytesub(arg, bi + 1, arg.len)) {
case let s: str => v = s;
case utf8.invalid =>
abort("getopt: malformed argv UTF-8");
};
appendoption(&opts, r, v);
advanced = true;
} else {