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

@@ -1769,44 +1769,15 @@ export fn toutf8(s: str) []u8 = {
return r;
};
// fromutf8_unsafe — borrowed str view of `in`. Does not validate.
// ref/hare/strings/utf8.ha:10.
export fn fromutf8_unsafe(in: []u8) str = {
// frombytes — borrowed str view of `in`. Pure reinterpret per
// CLAUDE.md rule 9 carve-out; ref/hare/strings/utf8.ha:10.
export fn frombytes(in: []u8) str = {
let r: str;
r.ptr = in.ptr;
r.len = in.len;
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 open-codes the same byte-by-byte DFA walk (ref/hare/encoding/utf8/
// decode.ha:207) because the cross-shape `(void | invalid) →
// (str | invalid)` propagation `?` needs is #19. The decoder-walk
// form's `(rune | done | more | invalid)` arm matches the failure
// axis of fromutf8 and lifts cleanly. Collapses to `utf8.validate(in)?`
// once #19 lands.
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.
@@ -1832,7 +1803,7 @@ export fn dup(s: str) str = {
let i: i32 = 0;
for (i < s.len) { buf[i] = s[i]; i += 1; };
buf.len = s.len;
return fromutf8_unsafe(buf);
return frombytes(buf);
};
// dupall — fresh `[]str` whose elements are independent copies of
@@ -1905,7 +1876,7 @@ export fn concat(strs: str...) str = {
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = rt.malloc(total: u64): *u8;
let buf: []u8 = alloc([], total: u64)!;
let off: i32 = 0;
i = 0;
for (i < strs.len) {
@@ -1917,9 +1888,8 @@ export fn concat(strs: str...) str = {
off += strs[i].len;
i += 1;
};
r.ptr = buf;
r.len = total;
return r;
buf.len = total;
return frombytes(buf);
};
// join — fresh allocation with `delim` placed between each element of
@@ -1938,7 +1908,7 @@ export fn join(delim: str, strs: str...) str = {
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = rt.malloc(total: u64): *u8;
let buf: []u8 = alloc([], total: u64)!;
let off: i32 = 0;
i = 0;
for (i < strs.len) {
@@ -1958,9 +1928,8 @@ export fn join(delim: str, strs: str...) str = {
};
i += 1;
};
r.ptr = buf;
r.len = total;
return r;
buf.len = total;
return frombytes(buf);
};
// utf8bytelenbounded — walk `it` forward `end` runes and return the
@@ -2247,7 +2216,7 @@ let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8];
// the working field-assign path.
export fn ltrim(input: str, trim: rune...) str = {
if (trim.len == 0) {
return fromutf8_unsafe(bytes.ltrim(toutf8(input), whitespace[0:4]...));
return frombytes(bytes.ltrim(toutf8(input), whitespace[0:4]...));
};
let it: iterator = iter(input);
for (true) {
@@ -2279,7 +2248,7 @@ export fn ltrim(input: str, trim: rune...) str = {
// ref/hare/strings/trim.ha:32.
export fn rtrim(input: str, trim: rune...) str = {
if (trim.len == 0) {
return fromutf8_unsafe(bytes.rtrim(toutf8(input), whitespace[0:4]...));
return frombytes(bytes.rtrim(toutf8(input), whitespace[0:4]...));
};
let it: iterator = riter(input);
for (true) {
@@ -2392,7 +2361,7 @@ export fn iterstr(it: *iterator) str = {
} else {
r = it.src[it.offs:it.src.len];
};
return fromutf8_unsafe(r);
return frombytes(r);
};
// slice — borrowed substring between two iterator positions.
@@ -2407,7 +2376,7 @@ export fn slice(begin: *iterator, end: *iterator) str = {
let e: utf8.decoder;
e.src = end.src;
e.offs = end.offs;
return fromutf8_unsafe(utf8.slice(&b, &e));
return frombytes(utf8.slice(&b, &e));
};
// position — byte-wise offset of the iterator in its source.
@@ -2458,7 +2427,7 @@ export fn rtokenize(s: str, delim: str) tokenizer = {
export fn next_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.next_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case let v: []u8 => return frombytes(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
@@ -2468,7 +2437,7 @@ export fn next_token(s: *tokenizer) (str | bytes.done) = {
export fn peek_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.peek_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case let v: []u8 => return frombytes(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
@@ -2477,7 +2446,7 @@ export fn peek_token(s: *tokenizer) (str | bytes.done) = {
// cursor. ref/hare/strings/tokenize.ha:79.
export fn remaining_tokens(s: *tokenizer) str = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
return fromutf8_unsafe(bytes.remaining_tokens(b));
return frombytes(bytes.remaining_tokens(b));
};
// rt_ensure is the runtime slice-growth helper invoked by the
@@ -2615,7 +2584,7 @@ export fn lpad(s: str, p: rune, maxlen: i32) str = {
if (s.len >= maxlen) { return dup(s); };
let scratch: [4]u8;
let pad: []u8 = runebytes(scratch[0:4], p);
let buf: *u8 = rt.malloc(maxlen: u64): *u8;
let buf: []u8 = alloc([], maxlen: u64)!;
let padwrite: i32 = (maxlen - s.len) * pad.len;
if (padwrite > maxlen) { padwrite = maxlen; };
let off: i32 = 0;
@@ -2630,10 +2599,8 @@ export fn lpad(s: str, p: rune, maxlen: i32) str = {
buf[off + k] = s[k];
k += 1;
};
let r: str;
r.ptr = buf;
r.len = maxlen;
return r;
buf.len = maxlen;
return frombytes(buf);
};
// replace — fresh allocation of `s` with every non-overlapping
@@ -2689,7 +2656,7 @@ export fn replace(s: str, needle: str, target: str) (str | nomem) = {
};
};
res.len = total;
return fromutf8_unsafe(res);
return frombytes(res);
};
// rpad — right-pad `s` with `p` rune until the result reaches `maxlen`
@@ -2698,7 +2665,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = {
if (s.len >= maxlen) { return dup(s); };
let scratch: [4]u8;
let pad: []u8 = runebytes(scratch[0:4], p);
let buf: *u8 = rt.malloc(maxlen: u64): *u8;
let buf: []u8 = alloc([], maxlen: u64)!;
let k: i32 = 0;
for (k < s.len) {
buf[k] = s[k];
@@ -2710,10 +2677,8 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = {
buf[s.len + i] = pad.ptr[i % pad.len];
i += 1;
};
let r: str;
r.ptr = buf;
r.len = maxlen;
return r;
buf.len = maxlen;
return frombytes(buf);
};
// strconv — number↔string conversions.