// memio — in-memory io stream. // // Hare's memio:: surface, drop underscores. Two flavours behind a // single [[io.stream]]: // // 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"). // // let mem: memio.state; // let s: io.stream; // memio.fixed(&mem, &s, buf); // io.write(&s, bytes); // // 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. package memio; 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 { 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; }; // 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; }; // 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; 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; }; // 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; 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; }; fn fixedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { let m: *state = s.ctx: *state; if (m.pos >= m.len) { return 0; }; 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; }; fn dynamicwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { let m: *state = s.ctx: *state; 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; }; fn dynamicclose(s: *io.stream) (void | io.closed) = { let m: *state = s.ctx: *state; 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; }; 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 two `fn grow` decls (here + selfhost/cmd/wcc/mem.ww's // arena `grow`) collide. Module-prefixed name keeps the symmetry // with `dynamicwrite`/`dynamicclose` until task #9 (module-aware // private-fn scoping in cstage) lands. fn dynamicgrow(m: *state, need: i32) void = { let newcap: i32 = m.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]; i += 1; }; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; m.ptr = nbuf; m.cap = newcap; };