// vstream — Hare-shaped vtable wrappers over memio. Project #94 // fold-e2 (Option C, parallel API). // // Adds three constructors that return `io.vstream` (= `*io.vtable`, // the Hare-shape from lib/io/stream.ww) alongside the pre-vtable // memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The // OLD surface stays untouched here — fold-eFinal (task #50) atomically // flips the package shape: deletes the OLD constructors + callbacks, // renames `_vstream` suffix off, and migrates the few callers. // // Hare's memio::fixed/dynamic return `stream` whose FIRST field IS // `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx // struct's first field is `vt: io.vtable` (the vtable embedded // INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is // castable to `vstream = *vtable` via `&c.vt` — and the callbacks // recover the outer ctx by casting the dispatch arg back to // `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over // io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger // (log.ww:95-98). // // Cast workaround per #206: bare `&fn_name` does not type-check as // a `(* | void)` field-init / let-binding (the structural // `*fn(...)` value isn't accepted as the `*reader` named variant // of the tagged slot). Explicit `(&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. 8 cast tokens here (2 in fixed_vstream + 3 each in // dynamic_vstream / dynamicfrom_vstream — fixed leaves closer // void per Hare's fixed_vt, which lets st_close's void-arm return // plain `void` with no callback needed). Casts drop out wholesale // once #206 closes. // // 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 (related to the #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 same OLD memio.dynamicfrom note (memio.ww:73). package memio; import io; import os; import rt; // fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 // for the intrusive vstream cast. export type fixed_ctx = struct { vt: io.vtable, ptr: *u8, len: i32, cap: i32, pos: i32, }; // dynamic_ctx — heap-alloc'd state for dynamic_vstream / // dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the // difference is the vtable wired (writer=dynamicwrite_v, // closer=dynamicclose_v) and that the buffer can grow via // dynamicgrow_v on write overflow. export type dynamic_ctx = struct { vt: io.vtable, ptr: *u8, len: i32, cap: i32, pos: i32, }; // fixed_vstream — wire a vstream 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, deferred to eFinal). // // Mirrors ref/hare/memio/stream.ha:47. // // Three steps (vs Hare's single struct literal): alloc with vt // zero-init (struct-lit `vt = local_vt` silently drops tagged-union // fields past the first — sibling task; the `let zero` + post-alloc // chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer // is the proven-working route, same shape lib/bufio.init uses on // b.vtable). Field-assign of a TY_PTR/void variant through the // chained pointer-into-struct is fine here (*fixed_ctx is a plain // pointer-to-struct, not an aliased pointer; #195's "aliased-ptr // receiver" carve-out doesn't bite). export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { let zero: io.vtable; let c: *fixed_ctx = alloc(fixed_ctx{ vt = zero, ptr = buf.ptr, len = buf.len, cap = buf.len, pos = 0, })?; c.vt.reader = (&fixedread_v): *io.reader; c.vt.writer = (&fixedwrite_v): *io.writer; return &c.vt; }; // dynamic_vstream — wire a vstream with no initial buffer. Writes // grow the backing allocation; [[io.st_close]] frees it. // // Mirrors ref/hare/memio/stream.ha:54. export fn dynamic_vstream() (io.vstream | nomem) = { let zero: io.vtable; let c: *dynamic_ctx = alloc(dynamic_ctx{ vt = zero, ptr = nil, len = 0, cap = 0, pos = 0, })?; c.vt.reader = (&dynread_v): *io.reader; c.vt.writer = (&dynamicwrite_v): *io.writer; c.vt.closer = (&dynamicclose_v): *io.closer; return &c.vt; }; // 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:65. export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { let zero: io.vtable; let c: *dynamic_ctx = alloc(dynamic_ctx{ vt = zero, ptr = buf.ptr, len = buf.len, cap = buf.cap, pos = 0, })?; c.vt.reader = (&dynread_v): *io.reader; c.vt.writer = (&dynamicwrite_v): *io.writer; c.vt.closer = (&dynamicclose_v): *io.closer; return &c.vt; }; // ---- vtable callbacks ---------------------------------------------------- // fixedread_v / dynread_v — recover the ctx from vstream's // `*vtable` via the intrusive offset-0 cast. Separate fns (vs // sharing one readfn) so each cast targets the matching ctx type // — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 // has the same shape but only one ctx flavour. fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { let m: *fixed_ctx = s: *fixed_ctx; 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: *fixed_ctx = s: *fixed_ctx; 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 dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { let m: *dynamic_ctx = s: *dynamic_ctx; 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 dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { let m: *dynamic_ctx = s: *dynamic_ctx; 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: *dynamic_ctx = s: *dynamic_ctx; 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: *dynamic_ctx, 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; };