lib: collapse the parallel vstream scaffold onto the single Hare io surface (#94 fold-eFinal)

The Option-C parallel _v vstream API was scaffolding to bring the io stack up alongside the old surface; carrying both permanently is a rule-9 divergence from ref/hare, which has exactly one io surface. Collapse onto that surface (stream = *vtable, ref/hare/io/stream.ha) and rename the _v symbols to their Hare names (io vstream->stream, fmt vfprint->fprint, bufio/memio/log surfaces, log.new). Deletes the 4 lib/*/vstream.ww scaffold files; regenerates w6c/wwdump combined.ww. cstage and wwstage stay byte-identical and combined_ww_fresh holds; all 220 tests pass.
This commit is contained in:
2026-05-30 03:24:23 +09:00
parent 80184a3acf
commit 7d39f6d623
31 changed files with 2371 additions and 4834 deletions

View File

@@ -7,7 +7,7 @@
// shlex.syntaxerr — !void; bad shell syntax
// shlex.strerror(syntaxerr) — "Invalid shell syntax"
// shlex.split(in: str) — tokenize; returns ([]str | syntaxerr)
// shlex.quote(*io.stream, s) — write `s` shell-quoted to a sink
// shlex.quote(io.stream, s) — write `s` shell-quoted to a sink
// shlex.quotestr(s: str) str — quote into a fresh str
//
// Owning model: split() returns a fresh `[]str` of strings.dup'd
@@ -39,19 +39,16 @@
// / scan_backslash work on bytes; an escaped multibyte sequence
// round-trips as the byte sequence it appears in the input.
//
// - memio glue uses ww's caller-supplies-state shape:
// let mst: memio.state; let s: io.stream;
// memio.dynamic(&mst, &s);
// ... io.write(&s, ...) ...
// let view: str = memio.string(&mst); // borrowed
// instead of Hare's `let s = memio::dynamic()` return-by-value.
// Locked in by lib/memio's commit until cgen ships >24B return-
// by-value (graduates "in one go" per lib/CLAUDE.md).
// - memio glue uses the value-return shape (#94 fold-eFinal):
// let st: memio.stream = memio.dynamic();
// ... io.write(&st.vt, ...) ...
// let view: str = memio.string(&st); // borrowed
// mirroring Hare's `let s = memio::dynamic()` return-by-value.
// `&st.vt` is the [[io.stream]] (= `*io.vtable`) the dispatchers
// take.
//
// - quote() takes `*io.stream` (not Hare's `io::handle`) — that's
// already the ww-divergent shape lib/io ships. Returns
// `(i32 | io.closed)` instead of Hare's wider io::error: lib/io's
// stream vtable carries only `closed` on the write arm.
// - quote() takes `io.stream` (not Hare's `io::handle`) — that's
// the ww-divergent shape lib/io ships until io fold-2 (#5).
//
// - quote() emits raw bytes directly into the sink; without
// [[memio.appendrune]] / [[memio.concat]] in lib/memio yet, the
@@ -86,11 +83,9 @@
// Caller layout — quote into a fixed buffer:
//
// let buf: [128]u8;
// let mst: memio.state;
// let s: io.stream;
// memio.fixed(&mst, &s, buf[0:128]);
// shlex.quote(&s, "hello world"); // writes 'hello world'
// let view: str = memio.string(&mst);
// let st: memio.stream = memio.fixed(buf[0:128]);
// shlex.quote(&st.vt, "hello world"); // writes 'hello world'
// let view: str = memio.string(&st);
package shlex;
@@ -167,11 +162,11 @@ fn freepartial(slice: []str) void = {
// ship an appendbyte/appendrune in the ww port yet, so this is the
// uniform write-one-byte primitive across the module.
//
// The dynamic memio sink never returns `io.closed`; the [[split]] +
// [[quotestr]] callers ignore the error arm because their stream is
// always memio.dynamic. [[quote]] (caller-supplied sink) propagates
// it through its `(i32 | io.closed)` return.
fn writebyte(s: *io.stream, b: u8) (i32 | io.closed) = {
// The dynamic memio sink never errors; the [[split]] + [[quotestr]]
// callers ignore the error arm because their stream is always
// memio.dynamic. [[quote]] (caller-supplied sink) propagates it through
// its `(size | io.error)` return.
fn writebyte(s: io.stream, b: u8) (size | io.error) = {
let buf: [1]u8;
buf[0] = b;
return io.write(s, buf[0:1]);
@@ -182,7 +177,7 @@ fn writebyte(s: *io.stream, b: u8) (i32 | io.closed) = {
// newline silently. A trailing bare backslash (`*pos == in.len`) is
// `syntaxerr`. Otherwise the next byte is appended verbatim. Mirrors
// Hare's scan_backslash (split.ha:86).
fn scan_backslash(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
fn scan_backslash(out: io.stream, in: str, pos: *i32) (void | syntaxerr) = {
if (*pos >= in.len) { let e: syntaxerr; return e; };
let r: u8 = in[*pos];
*pos += 1;
@@ -194,7 +189,7 @@ fn scan_backslash(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
// scan_double — consume a `"..."` group at `*pos`. Closes on '"';
// '\<c>' inside processed by [[scan_backslash]]; unterminated → err.
// Mirrors Hare's scan_double (split.ha:110).
fn scan_double(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
fn scan_double(out: io.stream, in: str, pos: *i32) (void | syntaxerr) = {
let loop: bool = true;
for (loop) {
if (*pos >= in.len) { let e: syntaxerr; return e; };
@@ -217,7 +212,7 @@ fn scan_double(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
// scan_single — consume a `'...'` group at `*pos`. Closes on "'";
// no escapes inside (POSIX: single-quoted text is fully literal);
// unterminated → err. Mirrors Hare's scan_single (split.ha:135).
fn scan_single(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
fn scan_single(out: io.stream, in: str, pos: *i32) (void | syntaxerr) = {
let loop: bool = true;
for (loop) {
if (*pos >= in.len) { let e: syntaxerr; return e; };
@@ -242,9 +237,8 @@ fn scan_single(out: *io.stream, in: str, pos: *i32) (void | syntaxerr) = {
// signal for "an empty quote group was the only content of this
// token", which still pushes a literal "" (e.g. `cmd ''`).
export fn split(in: str) ([]str | syntaxerr) = {
let mst: memio.state;
let snk: io.stream;
memio.dynamic(&mst, &snk);
let st: memio.stream = memio.dynamic();
let snk: io.stream = &st.vt;
let slice: []str;
slice.ptr = nil: *str;
@@ -275,56 +269,56 @@ export fn split(in: str) ([]str | syntaxerr) = {
};
};
if (!first) {
let view: str = memio.string(&mst);
let view: str = memio.string(&st);
let owned: str = dupstr(view);
appendstr(&slice, owned);
memio.reset(&mst);
memio.reset(&st);
};
dirty = false;
} else { if (r == 92u8) { // '\\'
let er = scan_backslash(&snk, in, &pos);
let er = scan_backslash(snk, in, &pos);
match (er) {
case let e: syntaxerr => {
let _c = io.close(&snk);
let _c = io.close(snk);
freepartial(slice);
return e;
};
case void => {};
};
} else { if (r == 34u8) { // '"'
let er = scan_double(&snk, in, &pos);
let er = scan_double(snk, in, &pos);
match (er) {
case let e: syntaxerr => {
let _c = io.close(&snk);
let _c = io.close(snk);
freepartial(slice);
return e;
};
case void => {};
};
} else { if (r == 39u8) { // "'"
let er = scan_single(&snk, in, &pos);
let er = scan_single(snk, in, &pos);
match (er) {
case let e: syntaxerr => {
let _c = io.close(&snk);
let _c = io.close(snk);
freepartial(slice);
return e;
};
case void => {};
};
} else {
let _w = writebyte(&snk, r);
let _w = writebyte(snk, r);
}; }; }; };
if (first) { first = false; };
};
if (dirty) {
let view: str = memio.string(&mst);
let view: str = memio.string(&st);
let owned: str = dupstr(view);
appendstr(&slice, owned);
};
let _c = io.close(&snk);
let _c = io.close(snk);
return slice;
};
@@ -355,10 +349,8 @@ fn issafe(s: str) bool = {
};
// quote — emit `s` shell-quoted to `sink`. Returns total bytes written,
// or `io.closed` if the sink rejects mid-write. Mirrors Hare's
// shlex::quote (escape.ha:25), modulo the narrower `io.closed`-only
// error set on lib/io's stream vtable (Hare's `io::error` would carry
// underread/etc., which lib/io doesn't yet model).
// or `io.error` if the sink rejects mid-write. Mirrors Hare's
// shlex::quote (escape.ha:25).
//
// Strategy:
// - empty `s` → `''`
@@ -366,14 +358,14 @@ fn issafe(s: str) bool = {
// - otherwise → wrap in single quotes; an embedded `'`
// becomes `'"'"'` (close, double-quoted
// literal `'`, reopen).
export fn quote(sink: *io.stream, s: str) (i32 | io.closed) = {
export fn quote(sink: io.stream, s: str) (size | io.error) = {
if (s.len == 0) {
let buf: [2]u8;
buf[0] = 39u8; buf[1] = 39u8;
let r = io.write(sink, buf[0:2]);
match (r) {
case let n: i32 => return n;
case let c: io.closed => return c;
case let n: size => return n;
case let e: io.error => return e;
};
};
if (issafe(s)) {
@@ -382,16 +374,16 @@ export fn quote(sink: *io.stream, s: str) (i32 | io.closed) = {
raw.len = s.len;
let r = io.write(sink, raw);
match (r) {
case let n: i32 => return n;
case let c: io.closed => return c;
case let n: size => return n;
case let e: io.error => return e;
};
};
let total: i32 = 0;
let total: size = 0;
let r1 = writebyte(sink, 39u8); // "'"
match (r1) {
case let n: i32 => { total += n; };
case let c: io.closed => return c;
case let n: size => { total += n; };
case let e: io.error => return e;
};
let i: i32 = 0;
for (i < s.len) {
@@ -402,22 +394,22 @@ export fn quote(sink: *io.stream, s: str) (i32 | io.closed) = {
pat[3] = 34u8; pat[4] = 39u8;
let rr = io.write(sink, pat[0:5]);
match (rr) {
case let n: i32 => { total += n; };
case let c2: io.closed => return c2;
case let n: size => { total += n; };
case let e2: io.error => return e2;
};
} else {
let rr = writebyte(sink, c);
match (rr) {
case let n: i32 => { total += n; };
case let c2: io.closed => return c2;
case let n: size => { total += n; };
case let e2: io.error => return e2;
};
};
i += 1;
};
let r2 = writebyte(sink, 39u8); // "'"
match (r2) {
case let n: i32 => { total += n; };
case let c: io.closed => return c;
case let n: size => { total += n; };
case let e: io.error => return e;
};
return total;
};
@@ -427,19 +419,18 @@ export fn quote(sink: *io.stream, s: str) (i32 | io.closed) = {
// `os.free(r.ptr, r.len: u64)`. Mirrors Hare's shlex::quotestr
// (escape.ha:50).
//
// memio.dynamic's writes never return io.closed, so the `match` on
// `quote(...)` discards the error arm — it can't reach this caller's
// sink. quote()'s `(i32 | io.closed)` shape is preserved for the
// caller-supplied-sink path.
// memio.dynamic's writes never error, so the `match` on `quote(...)`
// discards the error arm — it can't reach this caller's sink. quote()'s
// `(size | io.error)` shape is preserved for the caller-supplied-sink
// path.
export fn quotestr(s: str) str = {
let mst: memio.state;
let snk: io.stream;
memio.dynamic(&mst, &snk);
let st: memio.stream = memio.dynamic();
let snk: io.stream = &st.vt;
let _r = quote(&snk, s);
let view: str = memio.string(&mst);
let _r = quote(snk, s);
let view: str = memio.string(&st);
let owned: str = dupstr(view);
let _c = io.close(&snk);
let _c = io.close(snk);
return owned;
};

View File

@@ -108,22 +108,21 @@ fn checkempty(in: str) void = {
};
fn checkquote(in: str, expected: str) void = {
let mst: memio.state;
let snk: io.stream;
memio.dynamic(&mst, &snk);
let st: memio.stream = memio.dynamic();
let snk: io.stream = &st.vt;
let r = shlex.quote(&snk, in);
let n: i32 = 0;
let r = shlex.quote(snk, in);
let n: size = 0;
match (r) {
case let v: i32 => { n = v; };
case io.closed => { fail(); };
case let v: size => { n = v; };
case let _e: io.error => { fail(); };
};
if (n != expected.len) { fail(); };
if (n != expected.len: size) { fail(); };
let view: str = memio.string(&mst);
let view: str = memio.string(&st);
if (!streq(view, expected)) { fail(); };
let _c = io.close(&snk);
let _c: (void | io.error) = io.close(snk);
};
// ---- split: Hare's @test fn split() table --------------------------