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

@@ -386,28 +386,6 @@ export fn remaining_tokens(s: *tokenizer) []u8 = {
return s.in;
};
// 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/.cap fields of a []u8 element (24B).
// Mirrors the same workaround in lib/shlex.shlex (appendstr, 16B) and
// lib/getopt.getopt (appendoption, 24B); collapses in one go when the
// append builtin learns to store the full element width.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// appendslice — grow `*slice` by one and store `item` (24B). Mirror
// of [[shlex.appendstr]] / [[getopt.appendoption]]. Bypasses the
// `append` builtin's first-8B-only-store gap for a slice-element.
fn appendslice(slice: *[][]u8, item: []u8) void = {
let newlen: i32 = slice.len + 1;
slice.len = newlen;
rtensure(slice: *void, 24u64);
let dst: *[]u8 = &slice.ptr[newlen - 1];
dst.ptr = item.ptr;
dst.len = item.len;
dst.cap = item.cap;
};
// splitn — split `in` on any byte in `delim`, returning up to `n`
// tokens via forward iteration. The trailing slot (when more than
// `n - 1` tokens exist) holds the unconsumed remainder.
@@ -432,7 +410,7 @@ export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: []u8 => { appendslice(&toks, s); };
case let s: []u8 => { append(toks, s); };
case done => { return toks; };
};
i += 1;
@@ -441,7 +419,7 @@ export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
case done => void;
case let pk: []u8 => {
let r: []u8 = remaining_tokens(&tok);
appendslice(&toks, r);
append(toks, r);
};
};
return toks;
@@ -470,7 +448,7 @@ export fn rsplitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: []u8 => { appendslice(&toks, s); };
case let s: []u8 => { append(toks, s); };
case done => { return toks; };
};
i += 1;
@@ -479,7 +457,7 @@ export fn rsplitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
case done => void;
case let pk: []u8 => {
let r: []u8 = remaining_tokens(&tok);
appendslice(&toks, r);
append(toks, r);
};
};

View File

@@ -97,14 +97,6 @@ import encoding.utf8;
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 `value: str` half of an [[option]].
// [[appendoption]] grows manually and stores both fields via *option.
// No public stdlib facade exposes rt_ensure, hence the direct @symbol.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// helpkind — which slot of [[help]] is meaningful. Hare's getopt
// also has a subcmd_help variant; not shipped here (see file header).
export type helpkind = enum i32 {
@@ -205,18 +197,6 @@ export type command = struct {
helpcap: i32,
};
// appendoption — grow `*opts` by one slot and store `(flag, value)`.
// Avoids the `append(opts, pair)` builtin: cgen lowers that to a
// MOVQ-of-the-first-8-bytes, which drops the `value: str` half.
fn appendoption(opts: *[]option, flag: rune, value: str) void = {
let newlen: i32 = opts.len + 1;
opts.len = newlen;
rtensure(opts: *void, 24u64);
let dst: *option = &opts.ptr[newlen - 1];
dst.flag = flag;
dst.value = value;
};
// findflag — lookup the kind of `r` in `hs`. Returns void if absent.
//
// Reads go through `&hs[i]` rather than `hs[i].field` directly: the
@@ -307,7 +287,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
};
case let k: helpkind => {
if (k == helpkind.FLAG) {
appendoption(&opts, r, "");
append(opts, option { flag = r, value = "" });
bi += 1;
} else {
// PARAM: glued value, or next argv slot.
@@ -325,7 +305,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
case utf8.invalid =>
abort("getopt: malformed argv UTF-8");
};
appendoption(&opts, r, v);
append(opts, option { flag = r, value = v });
advanced = true;
} else {
if (i + 1 >= argv.len) {
@@ -340,7 +320,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
return e;
};
i += 1;
appendoption(&opts, r, argv[i]);
append(opts, option { flag = r, value = argv[i] });
advanced = true;
};
};

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);

View File

@@ -88,7 +88,7 @@ export fn dup(s: str) str = {
// so the only nomem propagation point is the initial slice alloc.
// With no inner failure path, the rollback is structurally a no-op
// and is omitted; it returns once dup graduates to `(str | nomem)`
// (#46). The pre-allocated slice has `cap == s.len`, so appendstr's
// (#46). The pre-allocated slice has `cap == s.len`, so append's
// rt_ensure call never reaches the grow branch.
//
// Empty input bypasses the alloc: rt_malloc(0) is an mmap of 0 bytes
@@ -107,7 +107,7 @@ export fn dupall(s: []str) ([]str | nomem) = {
let newsl: []str = alloc([], s.len)?;
let i: i32 = 0;
for (i < s.len) {
appendstr(&newsl, dup(s[i]));
append(newsl, dup(s[i]));
i += 1;
};
return newsl;
@@ -733,24 +733,6 @@ export fn rcut(in: str, delim: str) (str, str) = {
return (frombytes(a), frombytes(b));
};
// rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. Direct bind for the same reason as
// lib/shlex.shlex (appendstr, 16B): the builtin's expansion stores
// only 8B of the new element, losing the `.len` half of a `str`.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// appendstr — grow `*slice` by one and store `item` (16B). Mirror of
// lib/shlex.shlex appendstr. Collapses when the append builtin learns
// to store the full element width.
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;
};
// splitn — split `in` on any byte in `delim`, returning up to `n`
// tokens via forward iteration. The trailing slot (when more than
// `n - 1` tokens exist) holds the unconsumed remainder. Strings
@@ -773,7 +755,7 @@ export fn splitn(in: str, delim: str, n: i32) []str = {
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: str => { appendstr(&toks, s); };
case let s: str => { append(toks, s); };
case bytes.done => { return toks; };
};
i += 1;
@@ -782,7 +764,7 @@ export fn splitn(in: str, delim: str, n: i32) []str = {
case bytes.done => void;
case let pk: str => {
let r: str = remaining_tokens(&tok);
appendstr(&toks, r);
append(toks, r);
};
};
return toks;
@@ -807,7 +789,7 @@ export fn rsplitn(in: str, delim: str, n: i32) []str = {
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: str => { appendstr(&toks, s); };
case let s: str => { append(toks, s); };
case bytes.done => { return toks; };
};
i += 1;
@@ -816,7 +798,7 @@ export fn rsplitn(in: str, delim: str, n: i32) []str = {
case bytes.done => void;
case let pk: str => {
let r: str = remaining_tokens(&tok);
appendstr(&toks, r);
append(toks, r);
};
};