lib: collapse the manual rt_ensure append workarounds onto the fixed builtin (#34 follow-up)

shlex.appendstr, getopt.appendoption, bytes.appendslice and
strings.appendstr existed only because the append builtin stored the
first 8 bytes of the element; each carried its own @symbol("rt_ensure")
bind and a grow-then-store-through-*T body, with comments promising to
"collapse in one go when the append builtin is fixed". The previous
commit fixed the builtin; this removes all four helpers and their
rt_ensure binds and spells every call site as plain append().

Bonus correctness: getopt's appendoption passed a hardcoded membsz of
24, stale since the str 24B redesign made option {rune, str} 32B — the
manual growth under-allocated past 6 options while &opts.ptr[i] strode
32 (latent OOB). The builtin derives membsz from the type table
(probe: MOVQ $32, SI), closing that drift by construction.
This commit is contained in:
2026-06-04 02:22:25 +09:00
parent faade48513
commit 70fa9e2264
9 changed files with 66 additions and 354 deletions

View File

@@ -30,7 +30,7 @@
//
// - Drop nomem: ww os.alloc has no recoverable failure path (OOM
// yields a poisonous pointer that faults on deref; see lib/os.ww
// comment). Same precedent as strings.dup, getopt.appendoption.
// comment). Same precedent as strings.dup.
// Hare's (...|syntaxerr|nomem) collapses to (...|syntaxerr).
//
// - Byte-wise iteration via i32 cursor instead of Hare's
@@ -55,15 +55,6 @@
// "write one byte" path goes through `io.write(s, buf[0:1])`
// against a stack `[N]u8`. Same shape as fmt's rune-arm.
//
// - `[]str` grown via direct rt_ensure rather than the `append`
// builtin: cgen lowers `append(slice, element)` to a single MOVQ
// of the first 8 bytes, which drops the `len` half of a 16B str
// element. [[appendstr]] writes both halves through `*str`.
// Same gap, same workaround, same comment shape as
// [[getopt.appendoption]] (lib/getopt/getopt.ww:96-106) — kept
// greppable across modules. When the append-builtin is fixed,
// all three (getopt, shlex, anywhere else) collapse in one go.
//
// - Internal [[dupstr]] inlined rather than `use strings;`: keeps
// the dep surface small for a 6-line helper. shlex doesn't reach
// for any other strings:: routine, and callers free split()'s
@@ -94,13 +85,6 @@ import memio;
import os;
import strings;
// rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. We bind it directly because the builtin's
// expansion stores only 8 bytes of the new element (cgen emits a
// single MOVQ), losing the `len` half of a `str` (16B). No public
// stdlib facade exposes it, hence the direct @symbol.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// syntaxerr — the input wasn't a valid shell-tokenizable string
// (unterminated quote / bare trailing backslash). Mirrors Hare's
// shlex::syntaxerr; a `!void` so it can ride a tagged-union return
@@ -127,18 +111,6 @@ fn dupstr(s: str) str = {
return strings.frombytes(buf);
};
// appendstr — grow `*slice` by one and store `item` (16B). Bypasses
// the `append` builtin's first-8B-only store gap; mirror of
// [[getopt.appendoption]] for the `[]option` case.
fn appendstr(slice: *[]str, item: str) void = {
let newlen: i32 = slice.len + 1;
slice.len = newlen;
rtensure(slice: *void, size(str): u64);
let dst: *str = &slice.ptr[newlen - 1];
dst.ptr = item.ptr;
dst.len = item.len;
};
// freepartial — drop a partially built [[split]] result on the
// syntaxerr path. Hare uses a `defer if (!ok)` guard; ww has no defer,
// so we hand-roll the cleanup at every error return. Mirror of
@@ -271,7 +243,7 @@ export fn split(in: str) ([]str | syntaxerr) = {
if (!first) {
let view: str = memio.string(&st);
let owned: str = dupstr(view);
appendstr(&slice, owned);
append(slice, owned);
memio.reset(&st);
};
dirty = false;
@@ -315,7 +287,7 @@ export fn split(in: str) ([]str | syntaxerr) = {
if (dirty) {
let view: str = memio.string(&st);
let owned: str = dupstr(view);
appendstr(&slice, owned);
append(slice, owned);
};
let _c = io.close(snk);