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

@@ -1,30 +1,51 @@
// memio — in-memory io stream.
// memio — in-memory io stream. Project #94 fold-eFinal.
//
// Hare's memio:: surface, drop underscores. Two flavours behind a
// single [[io.stream]]:
// single [[io.stream]] (= `*io.vtable`):
//
// fixed caller owns the buffer, writes stop when full.
// dynamic memio owns the buffer, writes grow it; close frees.
//
// Call shape divergence from Hare: the caller supplies both the
// memio `state` and the `io.stream` slot, by pointer. ww cgen does
// not yet implement &x.field or 32B-struct return-by-value, so the
// Hare `let s = memio::fixed(buf)` shape isn't reachable; collapse
// to a single returned struct when those land (lib/CLAUDE.md
// "graduate in one go").
// Hare's memio::fixed/dynamic/dynamic_from return a `stream` whose
// FIRST field IS the `io::stream` (= `*vtable`). ww mirrors that
// intrusively: `stream`'s first field is `vt: io.vtable` (the vtable
// embedded INLINE) so a stack `stream` is castable to
// `io.stream = *vtable` via `&s.vt` — and the callbacks recover the
// outer `stream` by casting the dispatch arg back to `*stream`. Same
// intrusive shape as lib/bufio + lib/log over their embedded vtables.
//
// let mem: memio.state;
// let s: io.stream;
// memio.fixed(&mem, &s, buf);
// io.write(&s, bytes);
// let s: memio.stream = memio.fixed(buf);
// io.write(&s.vt, bytes);
// let view: str = memio.string(&s);
//
// Subset of Hare's surface: io.stream's variants are {eof, closed},
// so memio drops Hare's NONBLOCK flag (would need an `again` variant
// in lib/io). string()'s utf8-validating constructor is omitted per
// CLAUDE.md rule 9 carve-out. Hare's seek / copy callbacks are
// likewise absent: lib/io's stream vtable has only read/write/close
// slots, so memio can't wire a seeker or copier even if we wanted
// to. All three come back when their dependencies do.
// Constructors return the `stream` BY VALUE (Hare shape,
// ref/hare/memio/stream.ha:46,58,64): build the struct in a local,
// field-assign every slot, and `return r;` (the proven sret round-trip
// pinned by test/wcc/925 + 776). NO heap, NO `nomem`: the alloc that
// forced an earlier heap return is gone, so the constructor cannot
// fail. Caller owns the returned `stream` (stack ownership, no-GC) and
// passes `&s.vt` to the io dispatchers — exactly Hare's `&s` into
// io::write.
//
// Subset of Hare's surface: io's variants are {eof, error}, so memio
// drops Hare's NONBLOCK flag (would need an `again` variant in lib/io).
// string()'s utf8-validating constructor is omitted per CLAUDE.md
// rule 9 carve-out — see [[string]]. Hare's seek / copy callbacks are
// likewise absent: io's stream vtable has only read/write/close slots,
// so memio can't wire a seeker or copier yet (io fold-2, #5).
//
// Cast workaround per #206-payoff (ken: KEEP the explicit casts; they
// are cgen-neutral and sidestep the #214 over-acceptance surface). The
// `(&fn_name): *io.<role>` cast at each store site is the Hare-faithful
// minimum-touch route — the #206 cast-drop is a separate deferred
// payoff gated on #214.
//
// ptr/len/cap kept flat (no `buf: []u8`) per a historical cgen note:
// chained-dot writes through a state pointer into a slice subfield
// miscompile silently (#195 family); the flat shape sidesteps it.
// dynamicfrom uses the slice's `cap` (NOT `len`) to track the
// allocated-capacity-to-free on close — passing a half-filled append
// slice with len < cap and using only `buf.len` would under-free.
package memio;
@@ -32,106 +53,79 @@ import io;
import os;
import rt;
// state — memio's per-stream bookkeeping. The caller owns the slot
// and passes its address into a constructor. `ptr/len/cap` are the
// slice fields kept flat to dodge a chained-dot write through the
// state pointer (cgen doesn't store into `m.buf.ptr` reliably).
export type state = struct {
// stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at
// offset 0 for the intrusive stream→io.stream cast (`&s.vt`) and the
// callbacks' reverse `s: *stream` cast. Unified across fixed/dynamic
// (Hare keeps a single `stream` over per-mode vtable singletons; ww
// wires the per-mode callbacks post-construction instead). ptr/len/cap
// flat per the header note.
export type stream = struct {
vt: io.vtable,
ptr: *u8,
len: i32,
cap: i32,
pos: i32,
};
// fixed — wire `s` over a caller-supplied buffer. Writes never grow;
// they return 0 once `pos` reaches the end of the buffer.
export fn fixed(m: *state, s: *io.stream, buf: []u8) void = {
m.ptr = buf.ptr;
m.len = buf.len;
m.cap = buf.len;
m.pos = 0;
s.ctx = m: *void;
s.read = readfn;
s.write = fixedwrite;
s.close = closenoop;
// fixed — wire a stream over a caller-supplied buffer. Writes never
// grow; they return 0 once `pos` reaches the end of the buffer (Hare
// returns `nomem` here; ww surfaces 0 — graduating to Hare's `nomem`
// return needs the widen-from-bare-nomem path that #173-family work
// gates).
//
// Mirrors ref/hare/memio/stream.ha:46.
export fn fixed(buf: []u8) stream = {
let r: stream;
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&fixedwrite): *io.writer;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.len;
r.pos = 0;
return r;
};
// dynamic — wire `s` with no initial buffer. Writes grow the backing
// allocation; [[io.close]] frees it.
export fn dynamic(m: *state, s: *io.stream) void = {
m.ptr = nil;
m.len = 0;
m.cap = 0;
m.pos = 0;
s.ctx = m: *void;
s.read = readfn;
s.write = dynamicwrite;
s.close = dynamicclose;
// dynamic — wire a stream with no initial buffer. Writes grow the
// backing allocation; [[io.close]] frees it.
//
// Mirrors ref/hare/memio/stream.ha:58.
export fn dynamic() stream = {
let r: stream;
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&dynamicwrite): *io.writer;
r.vt.closer = (&dynamicclose): *io.closer;
r.ptr = nil;
r.len = 0;
r.cap = 0;
r.pos = 0;
return r;
};
// dynamicfrom — like [[dynamic]] but seeded with an existing slice.
// Ownership of the slice transfers to the stream; [[io.close]] frees
// it. The slice must come from the runtime allocator: close calls
// [[os.free]] with `m.cap` bytes, which is taken from `buf.cap` (the
// slice's allocated capacity), not its logical length. Passing a
// half-filled append slice (len < cap) and using only `buf.len` here
// would under-free on close.
export fn dynamicfrom(m: *state, s: *io.stream, buf: []u8) void = {
m.ptr = buf.ptr;
m.len = buf.len;
m.cap = buf.cap;
m.pos = 0;
s.ctx = m: *void;
s.read = readfn;
s.write = dynamicwrite;
s.close = dynamicclose;
};
// buffer — borrowed view of bytes written so far (buf[..pos]).
// Seek to the end before calling if the full buffer is wanted.
export fn buffer(m: *state) []u8 = {
let r: []u8;
r.ptr = m.ptr;
r.len = m.pos;
// Ownership transfers; close frees `cap` bytes from the slice's
// allocated capacity (NOT logical length).
//
// Mirrors ref/hare/memio/stream.ha:64.
export fn dynamicfrom(buf: []u8) stream = {
let r: stream;
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&dynamicwrite): *io.writer;
r.vt.closer = (&dynamicclose): *io.closer;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.cap;
r.pos = 0;
return r;
};
// string — bytes written so far, as a str view. Hare returns
// (str | utf8::invalid); ww doesn't ship utf8 validation yet, so
// this returns the unchecked view.
export fn string(m: *state) str = {
let r: str;
r.ptr = m.ptr;
r.len = m.pos;
return r;
};
// ---- vtable callbacks ----------------------------------------------------
// reset — rewind the cursor and truncate the logical content to 0.
// Backing storage is preserved; subsequent writes (dynamic) re-fill
// from the start without reallocation.
export fn reset(m: *state) void = {
m.pos = 0;
m.len = 0;
};
// borrowedread — return an `amt`-byte view starting at `pos` without
// copying, advancing the cursor. eof if fewer bytes are available.
export fn borrowedread(m: *state, amt: i32) ([]u8 | io.eof) = {
if (m.len - m.pos < amt) {
let e: io.eof;
return e;
};
let r: []u8;
r.ptr = m.ptr + (m.pos: u64);
r.len = amt;
m.pos += amt;
return r;
};
// ---- vtable callbacks ------------------------------------------------
fn readfn(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = {
let m: *state = s.ctx: *state;
// readfn — recover the stream from the io.stream's `*vtable` via the
// intrusive offset-0 cast. Single fn over the common header (Hare's
// single `read` at ref/hare/memio/stream.ha:103); fixed and dynamic
// share it because the read path is buffer-flavour-agnostic.
fn readfn(s: io.stream, buf: []u8) (size | io.eof | io.error) = {
let m: *stream = s: *stream;
if (m.pos >= m.len) {
let e: io.eof;
return e;
@@ -145,12 +139,12 @@ fn readfn(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = {
i += 1;
};
m.pos += n;
return n;
return n: size;
};
fn fixedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
let m: *state = s.ctx: *state;
if (m.pos >= m.len) { return 0; };
fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = {
let m: *stream = s: *stream;
if (m.pos >= m.len) { return 0: size; };
let space: i32 = m.len - m.pos;
let n: i32 = buf.len;
if (space < n) { n = space; };
@@ -160,11 +154,11 @@ fn fixedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
i += 1;
};
m.pos += n;
return n;
return n: size;
};
fn dynamicwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
let m: *state = s.ctx: *state;
fn dynamicwrite(s: io.stream, buf: []u8) (size | io.error) = {
let m: *stream = s: *stream;
let need: i32 = m.pos + buf.len;
if (need > m.cap) { dynamicgrow(m, need); };
let i: i32 = 0;
@@ -174,11 +168,11 @@ fn dynamicwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
};
m.pos += buf.len;
if (m.pos > m.len) { m.len = m.pos; };
return buf.len;
return buf.len: size;
};
fn dynamicclose(s: *io.stream) (void | io.closed) = {
let m: *state = s.ctx: *state;
fn dynamicclose(s: io.stream) (void | io.error) = {
let m: *stream = s: *stream;
if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nil;
m.len = 0;
@@ -187,30 +181,77 @@ fn dynamicclose(s: *io.stream) (void | io.closed) = {
return;
};
fn closenoop(s: *io.stream) (void | io.closed) = {
return;
};
// Double-and-copy growth. Initial bump from 0 lands at 8 to amortise
// small write bursts without a tail of reallocs.
//
// `dynamicgrow`, not Hare's bare `grow`: cstage bundles all imported
// modules into a flat TU and resolves private fns by unqualified name,
// so historically two `fn grow` decls (here + the now-deleted bump
// arena's `grow`) would have collided. Module-prefixed name kept the
// symmetry with `dynamicwrite`/`dynamicclose`; retained pending task
// #9 (module-aware private-fn scoping in cstage).
fn dynamicgrow(m: *state, need: i32) void = {
let newcap: i32 = m.cap;
// Double-and-copy growth with floor at 8. `dynamicgrow`, not Hare's
// bare `grow`: cstage bundles all imported modules into a flat TU and
// resolves private fns by unqualified name (task #9), so the
// module-prefixed name keeps the symmetry with dynamicwrite/dynamicclose.
fn dynamicgrow(d: *stream, need: i32) void = {
let newcap: i32 = d.cap;
if (newcap < 8) { newcap = 8; };
for (newcap < need) { newcap *= 2; };
let nbuf: *u8 = rt.malloc(newcap: u64): *u8;
let i: i32 = 0;
for (i < m.len) {
nbuf[i] = m.ptr[i];
for (i < d.len) {
nbuf[i] = d.ptr[i];
i += 1;
};
if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nbuf;
m.cap = newcap;
if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); };
d.ptr = nbuf;
d.cap = newcap;
};
// ---- accessors over the common `stream` header -----------------------
//
// Single fn each: Hare's string/reset/buffer/borrowedread all take
// `*stream` and read the flat header.
// string — bytes written so far, as a str view (buf[0..pos]).
//
// Mirrors ref/hare/memio/stream.ha:81 string(in: *stream). Hare returns
// (str | utf8::invalid) — the validating constructor. ww returns a bare
// `str` per the CLAUDE.md rule-9 frombytes carve-out: utf8.validate at
// the IO source is opt-in, never wrapped per-construction; the honest
// name reserves a future validating helper.
export fn string(s: *stream) str = {
let r: str;
r.ptr = s.ptr;
r.len = s.pos;
return r;
};
// buffer — borrowed []u8 view of bytes written so far (buf[0..pos]).
//
// Mirrors ref/hare/memio/stream.ha:74 buffer(in: *stream).
export fn buffer(s: *stream) []u8 = {
let r: []u8;
r.ptr = s.ptr;
r.len = s.pos;
return r;
};
// reset — rewind the cursor and truncate the logical content to 0.
// Backing storage is preserved; subsequent writes (dynamic) re-fill
// from the start without reallocation.
//
// Mirrors ref/hare/memio/stream.ha:87 reset(in: *stream).
export fn reset(s: *stream) void = {
s.pos = 0;
s.len = 0;
};
// borrowedread — return an `amt`-byte view starting at `pos` without
// copying, advancing the cursor. eof if fewer bytes are available.
//
// Mirrors ref/hare/memio/stream.ha:94 borrowedread(st: *stream, amt).
// `amt: i32` (not Hare's `size`) per the i32-index convention.
export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = {
if (s.len - s.pos < amt) {
let e: io.eof;
return e;
};
let r: []u8;
r.ptr = s.ptr + (s.pos: u64);
r.len = amt;
s.pos += amt;
return r;
};

View File

@@ -3,7 +3,9 @@
// Every @test enumerates parallel `[N]T` arrays of inputs and
// expectations, then iterates one body across them. Parallel arrays
// (rather than `[N]struct{...}`) sidestep the cstage cgen's chained
// `arr[i].field` store gap (task #6).
// `arr[i].field` store gap (task #6). #94 fold-eFinal: the unified
// value-return surface (`let st = memio.fixed(buf); &st.vt` into the
// io dispatchers; io.read/write/close return size/io.error).
package memio;
@@ -47,9 +49,8 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
req[3]=1; wantn[3]=0; wantf[3]=0u8; wantl[3]=0u8; weof[3]=1;
req[4]=0; wantn[4]=0; wantf[4]=0u8; wantl[4]=0u8; weof[4]=1;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, arr[0:8]);
let st: memio.stream = memio.fixed(arr[0:8]);
let s: io.stream = &st.vt;
let out: [8]u8;
let i: i32 = 0;
@@ -58,32 +59,32 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
let k: i32 = 0;
for (k < 8) { out[k] = 0u8; k += 1; };
let r: (i32 | io.eof | io.closed) = io.read(&s, out[0:req[i]]);
let r: (size | io.eof | io.error) = io.read(s, out[0:req[i]]);
match (r) {
case let n: i32 => {
case let nz: size => {
let n: i32 = nz: i32;
if (weof[i] != 0) { fail(); };
if (n != wantn[i]) { fail(); };
if (n > 0 && out[0] != wantf[i]) { fail(); };
if (n > 0 && out[n - 1] != wantl[i]) { fail(); };
};
case io.eof => { if (weof[i] == 0) { fail(); }; };
case io.closed => fail();
case let e: io.error => fail();
};
i += 1;
};
// closenoop on a fixed stream returns void.
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
// fixed stream has no closer → io.close returns void.
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => fail(); };
};
// ---- fixedwritecases: full-fit / exact-fill / partial / overflow -------------
@test fn fixedwritecases() void = {
let dst: [16]u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, dst[0:16]);
let st: memio.stream = memio.fixed(dst[0:16]);
let s: io.stream = &st.vt;
// One flat source; rows index into it via (off, len). Sequence:
// "hello " (6), "world!!" (7), "XXXXXXX" (7), "Y" (1).
@@ -104,10 +105,10 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
for (i < 4) {
let lo: i32 = off[i];
let hi: i32 = lo + ln[i];
let r: (i32 | io.closed) = io.write(&s, src[lo:hi]);
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) {
case let n: i32 => { if (n != wantn[i]) { fail(); }; };
case io.closed => fail();
case let n: size => { if (n: i32 != wantn[i]) { fail(); }; };
case let e: io.error => fail();
};
i += 1;
};
@@ -117,27 +118,23 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
if (!bytes.equal(dst[0:16], want[0:16])) { fail(); };
// 0-byte write is always 0 with no side effects.
let r0: (i32 | io.closed) = io.write(&s, src[0:0]);
let r0: (size | io.error) = io.write(s, src[0:0]);
match (r0) {
case let n: i32 => { if (n != 0) { fail(); }; };
case io.closed => fail();
case let n: size => { if (n != 0: size) { fail(); }; };
case let e: io.error => fail();
};
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => fail(); };
};
// ---- dynamicgrowcases: every cap doubling exercised ---------------------
// Drive grow 0 → 8 → 16 → 32 by writing sized chunks. Verify
// accumulated `pos` after each step. Suffix `cases` mirrors
// `fixedwritecases` / `borrowedreadcases`; bare `dynamicgrow`
// would collide with memio.ww's private `dynamicgrow` in the same
// package.
// accumulated `pos` after each step.
@test fn dynamicgrowcases() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
let src: [32]u8;
let _: i32 = putstr("abcdefghijklmnopqrstuvwxyz012345", src[0:32], 0);
@@ -157,20 +154,20 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
for (i < 5) {
let lo: i32 = off;
let hi: i32 = lo + chunk[i];
let r: (i32 | io.closed) = io.write(&s, src[lo:hi]);
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) {
case let n: i32 => { if (n != chunk[i]) { fail(); }; };
case io.closed => fail();
case let n: size => { if (n: i32 != chunk[i]) { fail(); }; };
case let e: io.error => fail();
};
if (memio.buffer(&mem).len != total[i]) { fail(); };
if (memio.buffer(&st).len != total[i]) { fail(); };
off += chunk[i];
i += 1;
};
if (!bytes.equal(memio.buffer(&mem), src[0:26])) { fail(); };
if (!bytes.equal(memio.buffer(&st), src[0:26])) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => fail(); };
};
// ---- dynamicreset: write / reset / write cycles -------------------------
@@ -178,9 +175,8 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
// op=0 writes `ln` bytes from a rolling source; op=1 resets and ignores
// ln. After each row the accumulated len must equal `want`.
@test fn dynamicreset() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
let src: [16]u8;
src[0]=10u8; src[1]=20u8; src[2]=30u8; src[3]=40u8;
@@ -200,26 +196,26 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
let i: i32 = 0;
for (i < 6) {
if (op[i] == 1) {
memio.reset(&mem);
memio.reset(&st);
off = 0;
} else {
let lo: i32 = off;
let hi: i32 = lo + ln[i];
let r: (i32 | io.closed) = io.write(&s, src[lo:hi]);
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) {
case let n: i32 => { if (n != ln[i]) { fail(); }; };
case io.closed => fail();
case let n: size => { if (n: i32 != ln[i]) { fail(); }; };
case let e: io.error => fail();
};
off += ln[i];
};
if (memio.buffer(&mem).len != want[i]) { fail(); };
if (memio.buffer(&st).len != want[i]) { fail(); };
i += 1;
};
if (!bytes.equal(memio.buffer(&mem), src[0:8])) { fail(); };
if (!bytes.equal(memio.buffer(&st), src[0:8])) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => fail(); };
};
// ---- borrowedread: under / exact / over / 0-byte ------------------------
@@ -228,9 +224,7 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
let arr: [6]u8;
arr[0]=0u8; arr[1]=1u8; arr[2]=2u8; arr[3]=3u8; arr[4]=4u8; arr[5]=5u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, arr[0:6]);
let st: memio.stream = memio.fixed(arr[0:6]);
// (amt, wantfirst, wantlast, weof)
let amt: [4]i32;
@@ -244,7 +238,7 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
let i: i32 = 0;
for (i < 4) {
let r: ([]u8 | io.eof) = memio.borrowedread(&mem, amt[i]);
let r: ([]u8 | io.eof) = memio.borrowedread(&st, amt[i]);
match (r) {
case let v: []u8 => {
if (weof[i] != 0) { fail(); };
@@ -261,11 +255,10 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
// ---- stringview: len + endpoints track pos across appends ---------------
@test fn stringview() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
if (memio.string(&mem).len != 0) { fail(); };
if (memio.string(&st).len != 0) { fail(); };
let src: [32]u8;
let _: i32 = putstr("hello world!!", src[0:32], 0);
@@ -284,17 +277,17 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
for (i < 3) {
let lo: i32 = off[i];
let hi: i32 = lo + ln[i];
let r: (i32 | io.closed) = io.write(&s, src[lo:hi]);
match (r) { case let n: i32 => {}; case io.closed => fail(); };
let v: str = memio.string(&mem);
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) { case let n: size => {}; case let e: io.error => fail(); };
let v: str = memio.string(&st);
if (v.len != want[i]) { fail(); };
if (v[0] != wf[i]) { fail(); };
if (v[v.len - 1] != wl[i]) { fail(); };
i += 1;
};
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => fail(); };
};
// ---- dynamicfromseed: ownership-transferred seed, read then write -------
@@ -302,15 +295,13 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
@test fn dynamicfromseed() void = {
// Build a heap-allocated seed via append (rt_ensure path), then
// hand ownership to memio. dynamicclose then frees `seed.cap`
// bytes — the regression that motivated the `m.cap = buf.cap`
// fix in lib/memio/memio.ww.
// bytes — the regression that motivated the `cap = buf.cap` fix.
let seed: []u8;
seed.ptr = nil; seed.len = 0; seed.cap = 0;
append(seed, 100u8, 101u8, 102u8, 103u8);
let mem: memio.state;
let s: io.stream;
memio.dynamicfrom(&mem, &s, seed);
let st: memio.stream = memio.dynamicfrom(seed);
let s: io.stream = &st.vt;
// (readn, wantbyte, weof)
let rn: [5]i32;
@@ -325,15 +316,16 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
let out: [4]u8;
let i: i32 = 0;
for (i < 5) {
let r: (i32 | io.eof | io.closed) = io.read(&s, out[0:rn[i]]);
let r: (size | io.eof | io.error) = io.read(s, out[0:rn[i]]);
match (r) {
case let n: i32 => {
case let nz: size => {
let n: i32 = nz: i32;
if (weof[i] != 0) { fail(); };
if (n != rn[i]) { fail(); };
if (out[0] != want[i]) { fail(); };
};
case io.eof => { if (weof[i] == 0) { fail(); }; };
case io.closed => fail();
case let e: io.error => fail();
};
i += 1;
};
@@ -342,14 +334,14 @@ fn putstr(s: str, into: []u8, off: i32) i32 = {
// must free the (post-grow) cap, not the seed cap.
let extra: [4]u8;
extra[0]=200u8; extra[1]=201u8; extra[2]=202u8; extra[3]=203u8;
let w: (i32 | io.closed) = io.write(&s, extra[0:4]);
let w: (size | io.error) = io.write(s, extra[0:4]);
match (w) {
case let n: i32 => { if (n != 4) { fail(); }; };
case io.closed => fail();
case let n: size => { if (n: i32 != 4) { fail(); }; };
case let e: io.error => fail();
};
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => fail(); };
};
export fn main() i32 = {

View File

@@ -1,257 +0,0 @@
// vstream — Hare-shaped vtable wrappers over memio. Project #94
// fold-eFinal(PREP).
//
// Adds three constructors that return a memio `stream` BY VALUE (the
// Hare shape, ref/hare/memio/stream.ha:46,58,64) alongside the
// pre-vtable memio.fixed / memio.dynamic / memio.dynamicfrom in
// memio.ww. The OLD surface stays untouched here — fold-eFinal (FLIP)
// atomically flips the package shape: deletes the OLD constructors +
// `state` + callbacks, renames the `_vstream`/`_v` suffix off (so
// `fixed_vstream → fixed`, `stream` stays, `string_v → string`), and
// migrates the few callers (lib/fmt/vstream.ww + the 776 probe).
//
// Hare's memio::fixed/dynamic/dynamic_from return a `stream` whose
// FIRST field IS the `io::stream` (= `*vtable`). ww mirrors that
// intrusively: `stream`'s first field is `vt: io.vtable` (the vtable
// embedded INLINE) so a stack `stream` is castable to
// `vstream = *vtable` via `&s.vt` — and the callbacks recover the
// outer `stream` by casting the dispatch arg back to `*stream`. Same
// intrusive shape as lib/bufio + lib/log over their embedded vtables.
//
// VALUE-RETURN (drew, gating): the constructor builds the struct in a
// local `let r: stream;`, field-assigns every slot (including the
// tagged vt sub-fields), and `return r;` — the proven sret round-trip
// shape pinned by test/wcc/925 ("ident_return_rhs" row) and extended
// to tagged-union-field structs by the 776 byte-id rows here. NO heap,
// NO `nomem`: the alloc that forced the OLD `(io.vstream | nomem)`
// return is gone, so the constructor cannot fail. Caller owns the
// returned `stream` (stack ownership, no-GC) and passes `&s.vt` to the
// io.st_* dispatchers — exactly Hare's `&s` into io::write.
//
// Cast workaround per #206-payoff (ken: KEEP the explicit casts; they
// are cgen-neutral and sidestep the #214 over-acceptance surface). The
// `(&fn_name): *io.<role>` cast at each store site is the Hare-faithful
// minimum-touch route — same workaround test/wcc/775_io_vtable_run.c
// uses for the bare-vtable init.
//
// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note
// (memio.ww:39): chained-dot writes through a state pointer into a
// slice subfield miscompile silently (#195 family); the flat shape
// sidesteps it. dynamicfrom_vstream uses the slice's `cap` (NOT `len`)
// to track the allocated-capacity-to-free on close — mirrors the OLD
// memio.dynamicfrom note (memio.ww:73).
package memio;
import io;
import os;
import rt;
// stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at
// offset 0 for the intrusive stream→vstream cast (`&s.vt`) and the
// callbacks' reverse `s: *stream` cast. Unified across fixed/dynamic
// (Hare keeps a single `stream` over per-mode vtable singletons; ww
// wires the per-mode callbacks post-construction instead). ptr/len/cap
// flat per the memio.state note above.
export type stream = struct {
vt: io.vtable,
ptr: *u8,
len: i32,
cap: i32,
pos: i32,
};
// fixed_vstream — wire a stream over a caller-supplied buffer. Writes
// never grow; they return 0 once `pos` reaches the end of the buffer
// (Hare returns `nomem` here; ww surfaces 0 to mirror the OLD
// memio.fixedwrite divergence — graduating to Hare's `nomem` return
// needs the widen-from-bare-nomem path that #173 blocks).
//
// Mirrors ref/hare/memio/stream.ha:46.
export fn fixed_vstream(buf: []u8) stream = {
let r: stream;
r.vt.reader = (&read_v): *io.reader;
r.vt.writer = (&fixedwrite_v): *io.writer;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.len;
r.pos = 0;
return r;
};
// dynamic_vstream — wire a stream with no initial buffer. Writes grow
// the backing allocation; [[io.st_close]] frees it.
//
// Mirrors ref/hare/memio/stream.ha:58.
export fn dynamic_vstream() stream = {
let r: stream;
r.vt.reader = (&read_v): *io.reader;
r.vt.writer = (&dynamicwrite_v): *io.writer;
r.vt.closer = (&dynamicclose_v): *io.closer;
r.ptr = nil;
r.len = 0;
r.cap = 0;
r.pos = 0;
return r;
};
// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an
// existing slice. Ownership transfers; close frees `cap` bytes from
// the slice's allocated capacity (NOT logical length) — passing a
// half-filled append slice with len < cap and using only `buf.len`
// would under-free on close.
//
// Mirrors ref/hare/memio/stream.ha:64.
export fn dynamicfrom_vstream(buf: []u8) stream = {
let r: stream;
r.vt.reader = (&read_v): *io.reader;
r.vt.writer = (&dynamicwrite_v): *io.writer;
r.vt.closer = (&dynamicclose_v): *io.closer;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.cap;
r.pos = 0;
return r;
};
// ---- vtable callbacks ----------------------------------------------------
// read_v — recover the stream from the vstream's `*vtable` via the
// intrusive offset-0 cast. Single fn over the common header (Hare's
// single `read` at ref/hare/memio/stream.ha:103); fixed and dynamic
// share it because the read path is buffer-flavour-agnostic.
fn read_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
let m: *stream = s: *stream;
if (m.pos >= m.len) {
let e: io.eof;
return e;
};
let avail: i32 = m.len - m.pos;
let n: i32 = buf.len;
if (avail < n) { n = avail; };
let i: i32 = 0;
for (i < n) {
buf[i] = m.ptr[m.pos + i];
i += 1;
};
m.pos += n;
return n: size;
};
fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
let m: *stream = s: *stream;
if (m.pos >= m.len) { return 0: size; };
let space: i32 = m.len - m.pos;
let n: i32 = buf.len;
if (space < n) { n = space; };
let i: i32 = 0;
for (i < n) {
m.ptr[m.pos + i] = buf[i];
i += 1;
};
m.pos += n;
return n: size;
};
fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
let m: *stream = s: *stream;
let need: i32 = m.pos + buf.len;
if (need > m.cap) { dynamicgrow_v(m, need); };
let i: i32 = 0;
for (i < buf.len) {
m.ptr[m.pos + i] = buf[i];
i += 1;
};
m.pos += buf.len;
if (m.pos > m.len) { m.len = m.pos; };
return buf.len: size;
};
fn dynamicclose_v(s: io.vstream) (void | io.error) = {
let m: *stream = s: *stream;
if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nil;
m.len = 0;
m.cap = 0;
m.pos = 0;
return;
};
// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at
// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps
// the cstage flat-TU private-fn scope from colliding (memio.ww:200
// note + task #9).
fn dynamicgrow_v(d: *stream, need: i32) void = {
let newcap: i32 = d.cap;
if (newcap < 8) { newcap = 8; };
for (newcap < need) { newcap *= 2; };
let nbuf: *u8 = rt.malloc(newcap: u64): *u8;
let i: i32 = 0;
for (i < d.len) {
nbuf[i] = d.ptr[i];
i += 1;
};
if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); };
d.ptr = nbuf;
d.cap = newcap;
};
// ---- accessors over the common `stream` header -----------------------
//
// Single fn each (drew string-collapse): Hare's string/reset/buffer/
// borrowedread all take `*stream` and read the flat header, so the
// fixed/dynamic split the OLD V-side carried (fixed_string /
// dynamic_string) collapses to one. FLIP drops the `_v` suffix
// (string_v → string, …) and deletes the OLD `*state` twins in
// memio.ww.
// string_v — bytes written so far, as a str view (buf[0..pos]).
//
// Mirrors ref/hare/memio/stream.ha:81 string(in: *stream). Hare returns
// (str | utf8::invalid) — the validating constructor. ww returns a bare
// `str` per the CLAUDE.md rule-9 frombytes carve-out: utf8.validate at
// the IO source is opt-in, never wrapped per-construction; the honest
// name reserves a future validating helper.
export fn string_v(s: *stream) str = {
let r: str;
r.ptr = s.ptr;
r.len = s.pos;
return r;
};
// buffer_v — borrowed []u8 view of bytes written so far (buf[0..pos]).
//
// Mirrors ref/hare/memio/stream.ha:74 buffer(in: *stream).
export fn buffer_v(s: *stream) []u8 = {
let r: []u8;
r.ptr = s.ptr;
r.len = s.pos;
return r;
};
// reset_v — rewind the cursor and truncate the logical content to 0.
// Backing storage is preserved; subsequent writes (dynamic) re-fill
// from the start without reallocation.
//
// Mirrors ref/hare/memio/stream.ha:87 reset(in: *stream).
export fn reset_v(s: *stream) void = {
s.pos = 0;
s.len = 0;
};
// borrowedread_v — return an `amt`-byte view starting at `pos` without
// copying, advancing the cursor. eof if fewer bytes are available.
//
// Mirrors ref/hare/memio/stream.ha:94 borrowedread(st: *stream, amt).
// `amt: i32` (not Hare's `size`) per the i32-index convention.
export fn borrowedread_v(s: *stream, amt: i32) ([]u8 | io.eof) = {
if (s.len - s.pos < amt) {
let e: io.eof;
return e;
};
let r: []u8;
r.ptr = s.ptr + (s.pos: u64);
r.len = amt;
s.pos += amt;
return r;
};