// 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.` 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; };