lib: drop _unsafe convention; rename fromutf8_unsafe → frombytes; strings α-batch (concat/join/lpad/rpad)

CLAUDE.md rule 9 amended with the explicit carve-out: ww is C/Plan-9-
lineage — no GC, no "safe" baseline to be unsafe relative to — so the
Hare `_unsafe` suffix flags an axis ww doesn't have. The convention
is dropped wholesale in lib/.

Concrete changes:
- lib/strings: `fromutf8_unsafe` → `frombytes` (pure reinterpret). The
  validating sibling `fromutf8` is deleted entirely (28 lines, plus its
  84-line fromutf8_cases test). Callers that need validation write the
  two lines inline at the IO source: `utf8.validate(b)?;
  let s = strings.frombytes(b);`. `fromutf8` name reserved for a future
  true validating helper.
- lib/strings α-batch: concat/join/lpad/rpad migrate from
  `rt.malloc(N): *u8` to `alloc([], N)!` + `buf.len = N;` +
  `return frombytes(buf);`. Same dup-pilot pattern (4c07ef0). Task #41.
- lib/memio header comment trimmed: drops a stale reference to
  "lib has no fromutf8 today"; cites the rule-9 carve-out instead.
- Caller renames across selfhost combined.ww files (auto-regen) +
  cgenutil.ww comment ref.

Rule-11 disclosure on the bundle: the rename and the α-batch are
nominally separable concerns (symbol-naming policy vs amalloc→
alloc-slice migration), but they touch the same 4 functions in
lib/strings/strings.ww — the α-batch's first emission of `frombytes`
postdates the rename. The α-batch was applied on top of the rename
sweep mid-flight by the pre-commit reviewer; splitting them back
out is fiddly text surgery for marginal bisect value. The rename is
the primary concern; α-batch is one entry in #8's sized-slice
migration.

Verified: make test 132/132, 995_self_rebuild byte-identity holds.
Closes #42; advances #41.
This commit is contained in:
2026-05-21 00:35:14 +09:00
parent 4c07ef0552
commit 47918d3ced
8 changed files with 109 additions and 340 deletions

View File

@@ -845,110 +845,20 @@ fn streq(a: str, b: str) bool = {
};
};
// ---- toutf8 / fromutf8_unsafe roundtrip -------------------------------
// ref/hare/strings/utf8.ha:31.
// ---- toutf8 / frombytes roundtrip -------------------------------
// ref/hare/strings/utf8.ha:31. Validation-half coverage lives in
// lib/encoding/utf8 (utf8test) per CLAUDE.md rule 9 carve-out.
@test fn utf8_roundtrip_cases() void = {
let s: str = "hello";
let b: []u8 = strings.toutf8(s);
if (b.len != 5) { fail(); };
if (b[0] != 104u8) { fail(); }; // 'h'
let r: str = strings.fromutf8_unsafe(b);
let r: str = strings.frombytes(b);
if (!streq(r, "hello")) { fail(); };
if (r.ptr != s.ptr) { fail(); }; // borrowed, not copied
};
// ---- fromutf8 ---------------------------------------------------------
// ref/hare/strings/utf8.ha:22 + @test fn utf8 (line 31). Validating
// sibling of fromutf8_unsafe — walks via [[utf8.validate]] and returns
// `utf8.invalid` on any malformed byte sequence (#7).
@test fn fromutf8_cases() void = {
// Hare vector — "hello world" round-trips.
signalled = 1900;
let hb: [11]u8;
hb[0] = 0x68u8; hb[1] = 0x65u8; hb[2] = 0x6Cu8; hb[3] = 0x6Cu8;
hb[4] = 0x6Fu8; hb[5] = 0x20u8; hb[6] = 0x77u8; hb[7] = 0x6Fu8;
hb[8] = 0x72u8; hb[9] = 0x6Cu8; hb[10] = 0x64u8;
match (strings.fromutf8(hb[0:11])) {
case let s: str => { if (!streq(s, "hello world")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Empty input — Hare's `fromutf8([])! == ""`.
signalled = 1901;
let eb: [1]u8;
match (strings.fromutf8(eb[0:0])) {
case let s: str => { if (s.len != 0) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// ASCII-only borrowed view: ptr aliases input.
signalled = 1902;
let s: str = "abc";
let bs: []u8 = strings.toutf8(s);
match (strings.fromutf8(bs)) {
case let r: str => {
if (!streq(r, "abc")) { fail(); };
if (r.ptr != s.ptr) { fail(); };
};
case let e: utf8.invalid => { fail(); };
};
// Multibyte Japanese — full UTF-8 round-trip.
signalled = 1903;
let jb: [15]u8;
jb[0] = 0xE3u8; jb[1] = 0x81u8; jb[2] = 0x93u8; // こ
jb[3] = 0xE3u8; jb[4] = 0x82u8; jb[5] = 0x93u8; // ん
jb[6] = 0xE3u8; jb[7] = 0x81u8; jb[8] = 0xABu8; // に
jb[9] = 0xE3u8; jb[10] = 0x81u8; jb[11] = 0xA1u8; // ち
jb[12] = 0xE3u8; jb[13] = 0x81u8; jb[14] = 0xAFu8; // は
match (strings.fromutf8(jb[0:15])) {
case let s: str => { if (!streq(s, "こんにちは")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Multibyte French — mixed-width "héllo".
signalled = 1904;
let fb: [6]u8;
fb[0] = 0x68u8; // h
fb[1] = 0xC3u8; fb[2] = 0xA9u8; // é
fb[3] = 0x6Cu8; fb[4] = 0x6Cu8; fb[5] = 0x6Fu8; // llo
match (strings.fromutf8(fb[0:6])) {
case let s: str => { if (!streq(s, "héllo")) { fail(); }; };
case let e: utf8.invalid => { fail(); };
};
// Lone 0x80 continuation byte — invalid.
signalled = 1905;
let c: [1]u8;
c[0] = 0x80u8;
match (strings.fromutf8(c[0:1])) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
// Overlong 2-byte encoding of NUL (0xC0 0x80) — invalid per
// ref/hare/encoding/utf8/decodetable.ha (state 0 rejects 0xC0).
signalled = 1906;
let ov: [2]u8;
ov[0] = 0xC0u8; ov[1] = 0x80u8;
match (strings.fromutf8(ov[0:2])) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
// Truncated multibyte — 0xE3 0x81 (2 of 3 bytes for U+3053) is
// rejected as invalid by [[utf8.validate]] (state != 0 at EOI).
signalled = 1907;
let tr: [2]u8;
tr[0] = 0xE3u8; tr[1] = 0x81u8;
match (strings.fromutf8(tr[0:2])) {
case let r: str => { fail(); };
case let e: utf8.invalid => void;
};
};
// ---- iter / next ------------------------------------------------------
// ref/hare/strings/iter.ha:84-108. Hare's @test fn iter() uses prev +
// riter heavily; both are deferred (no `utf8.prev`). Rebuild forward-
@@ -1797,7 +1707,6 @@ export fn main() i32 = {
signalled = 40; sub_cases();
signalled = 41; bytesub_cases();
signalled = 15; utf8_roundtrip_cases();
signalled = 43; fromutf8_cases();
signalled = 16; iter_empty_cases();
signalled = 17; iter_ascii_cases();
signalled = 18; iter_twobyte_cases();