// memio — in-memory io stream. Project #94 fold-eFinal. // // Hare's memio:: surface, drop underscores. Two flavours behind a // single [[io.stream]] (= `*io.vtable`): // // fixed caller owns the buffer, writes stop when full. // dynamic memio owns the buffer, writes grow it; close frees. // // 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 s: memio.stream = memio.fixed(buf); // io.write(&s.vt, bytes); // let view: str = memio.string(&s); // // 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.` 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; 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→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 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 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 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; }; // ---- vtable callbacks ---------------------------------------------------- // 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; }; 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(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; }; let i: i32 = 0; for (i < n) { m.ptr[m.pos + i] = buf[i]; i += 1; }; m.pos += n; return n: size; }; 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; 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(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; m.cap = 0; m.pos = 0; return; }; // 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 < 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: 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; };