diff --git a/lib/memio/memio.ww b/lib/memio/memio.ww index 32491ec3..af081c26 100644 --- a/lib/memio/memio.ww +++ b/lib/memio/memio.ww @@ -166,7 +166,7 @@ fn fixedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { 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) { grow(m, need); }; + if (need > m.cap) { dynamicgrow(m, need); }; let i: i32 = 0; for (i < buf.len) { m.ptr[m.pos + i] = buf[i]; @@ -193,7 +193,14 @@ fn closenoop(s: *io.stream) (void | io.closed) = { // Double-and-copy growth. Initial bump from 0 lands at 8 to amortise // small write bursts without a tail of reallocs. -fn grow(m: *state, need: i32) void = { +// +// `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; }; diff --git a/lib/memio/memiotest.ww b/lib/memio/memiotest.ww index 85d5a203..eb1fdbd5 100644 --- a/lib/memio/memiotest.ww +++ b/lib/memio/memiotest.ww @@ -127,11 +127,14 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { match (c) { case void => {}; case io.closed => fail(); }; }; -// ---- dynamicgrow: every cap doubling exercised -------------------------- +// ---- dynamicgrowcases: every cap doubling exercised --------------------- // Drive grow 0 → 8 → 16 → 32 by writing sized chunks. Verify -// accumulated `pos` after each step. -@test fn dynamicgrow() void = { +// accumulated `pos` after each step. Suffix `cases` mirrors +// `fixedwritecases` / `borrowedreadcases`; bare `dynamicgrow` +// would collide with memio.ww's private `dynamicgrow` in the same +// package. +@test fn dynamicgrowcases() void = { let mem: memio.state; let s: io.stream; memio.dynamic(&mem, &s); @@ -352,7 +355,7 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { export fn main() i32 = { signalled = 1; fixedread(); signalled = 2; fixedwritecases(); - signalled = 3; dynamicgrow(); + signalled = 3; dynamicgrowcases(); signalled = 4; dynamicreset(); signalled = 5; borrowedreadcases(); signalled = 6; stringview(); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 2ff6b5c7..b079f03c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9155,6 +9155,268 @@ export fn checkfile(c: *checker, file: *node) void = { }; +// io — stream interface (Plan 9 Bio / Hare io::stream shape). +// +// No closures, no methods. A `stream` is a struct of function +// pointers plus a `ctx: *void`. The error channel is the return +// value of read/write/close — Hare-shaped tagged unions instead of +// errno-style integer sentinels. + +// eof — read past the end of the stream. Hare uses the `done` +// singleton for EOF; ww doesn't have `done` yet so we ship a +// named-void variant tag. +package io; + +export type eof = void; + +// closed — operation attempted on a stream that has already been +// closed. ww-specific: Hare's io collapses this into the wider +// errors union, but our stream vtable has no handle-ownership +// semantics, so a distinct tag is honest. Named void. +export type closed = void; + +// underread — an I/O handle hit eof partway through a fixed-size +// read. Payload is the byte count actually delivered. Mirrors +// Hare's `io::underread = !size`; ww uses i32 because the +// underlying buffer-length type is i32 today. +export type underread = !i32; + +export type stream = struct { + ctx: *void, + read: fn(s: *stream, buf: []u8) (i32 | eof | closed), + write: fn(s: *stream, buf: []u8) (i32 | closed), + close: fn(s: *stream) (void | closed), +}; + +export fn read(s: *stream, buf: []u8) (i32 | eof | closed) = { + return s.read(s, buf); +}; + +export fn write(s: *stream, buf: []u8) (i32 | closed) = { + return s.write(s, buf); +}; + +export fn close(s: *stream) (void | closed) = { + return s.close(s); +}; + +// 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; +}; + // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: @@ -20599,7 +20861,7 @@ fn cgcontinue(c: *cgen, n: *node) void = { // Houses the top-level emission glue: // - cgfnparams: parameter spilling per SysV // - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue -// deferred via cgen.ww's cgoutbuf so the frame size +// deferred via cgen.ww's cgoutstate so the frame size // reflects every emit-time localadd (#15/#26c) // - cgfile: file-level entry (the exported driver) // @@ -20946,7 +21208,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { // through *(@sretarg) and returns @sretarg in RAX. let sret_callee: bool = sretretsize(c, c.fnret) > 0; - // Capture the body into cgoutbuf while c.frame grows under + // Capture the body into cgoutstate while c.frame grows under // emit-time localadd calls (#15/#26c — wwstage dropped its // scanlocals pre-pass to align DOWN with cstage's first-use // pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits @@ -20954,7 +21216,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { // localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds // `subsp`/`text` Progs up front and patches their `from.offset` // at the end via txt_emit. - cgout_enable(c.a); + cgout_enable(); if (sret_callee) { let saoff: i32 = localadd(c, "@sretarg", 8, nil); @@ -21085,6 +21347,8 @@ import tok; import typ; import sym; import strconv; +import io; +import memio; // Split files. Bundler pulls these in transitively so consumers only // need `use cgen;`. Order matters for the flat-bundle concat — utils // first so cgenexpr/stmt/decl can reference helpers defined here. @@ -21732,60 +21996,50 @@ fn localfind(c: *cgen, name: str) i32 = { // Cgfn defers its prologue (TEXT / SUBQ) until after the body so the // frame size reflects every emit-time localadd — the scanlocals pre- // pass that previously pre-computed it was dropped per #15/#26c. The -// body is captured into cgoutbuf while cgoutmode != 0, then flushed +// body is captured into cgoutstate while cgoutmode != 0, then flushed // after the prologue is written to stdout. Module-level state so the // existing emitline/emitint/emitlabel/emitsymname callers don't have // to thread a *cgen they don't already hold. Mirrors cstage's deferred // Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up // front and patches `from.offset` after the body finishes). -let cgoutbuf: *u8 = nil; -let cgoutbufcap: i32 = 0; -let cgoutbuflen: i32 = 0; -let cgoutmode: i32 = 0; -let cgoutarena: *arena = nil; +// +// `cgoutinit` guards a one-shot [[memio.dynamic]] wiring so the +// backing buffer is sticky across fns: [[cgout_flush]]'s +// [[memio.reset]] rewinds `pos`/`len` without touching `cap`, so the +// allocation amortises the same way the previous arena buffer did. +// Re-init per fn would abandon the buffer (no [[io.close]] path → no +// [[os.free]]) and re-grow from 0 via the 8→…→65536 ladder for every +// function. Same idiom as lib/log/log.ww:124 `ensureinit`. +let cgoutstate: memio.state; +let cgoutstream: io.stream; +let cgoutmode: i32 = 0; +let cgoutinit: i32 = 0; -def CGOUT_INIT_CAP: i32 = 65536; - -fn cgout_grow(need: i32) void = { - if (need <= cgoutbufcap) { return; }; - let want: i32 = cgoutbufcap; - if (want == 0) { want = CGOUT_INIT_CAP; }; - for (want < need) { want = want * 2; }; - let p: *u8 = amalloc(cgoutarena, want: u64): *u8; - let i: i32 = 0; - for (i < cgoutbuflen) { - p[i] = cgoutbuf[i]; - i += 1; +fn cgout_enable() void = { + if (cgoutinit == 0) { + memio.dynamic(&cgoutstate, &cgoutstream); + cgoutinit = 1; }; - cgoutbuf = p; - cgoutbufcap = want; -}; - -fn cgout_enable(a: *arena) void = { - cgoutarena = a; - cgoutbuflen = 0; cgoutmode = 1; }; fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { - if (cgoutbuflen > 0) { - os.write(1, cgoutbuf, cgoutbuflen: u64); - cgoutbuflen = 0; + if (cgoutstate.pos > 0) { + os.write(1, cgoutstate.ptr, cgoutstate.pos: u64); + memio.reset(&cgoutstate); }; }; fn emitbytes(p: *u8, n: u64) void = { if (cgoutmode != 0) { - let nn: i32 = n: i32; - cgout_grow(cgoutbuflen + nn); - let i: i32 = 0; - for (i < nn) { - cgoutbuf[cgoutbuflen + i] = p[i]; - i += 1; - }; - cgoutbuflen += nn; + let buf: []u8; + buf.ptr = p; + buf.len = n: i32; + // memio.dynamicwrite never returns io.closed (memio.ww:166); + // bare-discard mirrors lib/log/log.ww:169 fmt.fprintln. + io.write(&cgoutstream, buf); } else { os.write(1, p, n); }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index d9cf57a5..1d5c2438 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -31,6 +31,8 @@ import tok; import typ; import sym; import strconv; +import io; +import memio; // Split files. Bundler pulls these in transitively so consumers only // need `use cgen;`. Order matters for the flat-bundle concat — utils // first so cgenexpr/stmt/decl can reference helpers defined here. @@ -678,60 +680,50 @@ fn localfind(c: *cgen, name: str) i32 = { // Cgfn defers its prologue (TEXT / SUBQ) until after the body so the // frame size reflects every emit-time localadd — the scanlocals pre- // pass that previously pre-computed it was dropped per #15/#26c. The -// body is captured into cgoutbuf while cgoutmode != 0, then flushed +// body is captured into cgoutstate while cgoutmode != 0, then flushed // after the prologue is written to stdout. Module-level state so the // existing emitline/emitint/emitlabel/emitsymname callers don't have // to thread a *cgen they don't already hold. Mirrors cstage's deferred // Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up // front and patches `from.offset` after the body finishes). -let cgoutbuf: *u8 = nil; -let cgoutbufcap: i32 = 0; -let cgoutbuflen: i32 = 0; -let cgoutmode: i32 = 0; -let cgoutarena: *arena = nil; +// +// `cgoutinit` guards a one-shot [[memio.dynamic]] wiring so the +// backing buffer is sticky across fns: [[cgout_flush]]'s +// [[memio.reset]] rewinds `pos`/`len` without touching `cap`, so the +// allocation amortises the same way the previous arena buffer did. +// Re-init per fn would abandon the buffer (no [[io.close]] path → no +// [[os.free]]) and re-grow from 0 via the 8→…→65536 ladder for every +// function. Same idiom as lib/log/log.ww:124 `ensureinit`. +let cgoutstate: memio.state; +let cgoutstream: io.stream; +let cgoutmode: i32 = 0; +let cgoutinit: i32 = 0; -def CGOUT_INIT_CAP: i32 = 65536; - -fn cgout_grow(need: i32) void = { - if (need <= cgoutbufcap) { return; }; - let want: i32 = cgoutbufcap; - if (want == 0) { want = CGOUT_INIT_CAP; }; - for (want < need) { want = want * 2; }; - let p: *u8 = amalloc(cgoutarena, want: u64): *u8; - let i: i32 = 0; - for (i < cgoutbuflen) { - p[i] = cgoutbuf[i]; - i += 1; +fn cgout_enable() void = { + if (cgoutinit == 0) { + memio.dynamic(&cgoutstate, &cgoutstream); + cgoutinit = 1; }; - cgoutbuf = p; - cgoutbufcap = want; -}; - -fn cgout_enable(a: *arena) void = { - cgoutarena = a; - cgoutbuflen = 0; cgoutmode = 1; }; fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { - if (cgoutbuflen > 0) { - os.write(1, cgoutbuf, cgoutbuflen: u64); - cgoutbuflen = 0; + if (cgoutstate.pos > 0) { + os.write(1, cgoutstate.ptr, cgoutstate.pos: u64); + memio.reset(&cgoutstate); }; }; fn emitbytes(p: *u8, n: u64) void = { if (cgoutmode != 0) { - let nn: i32 = n: i32; - cgout_grow(cgoutbuflen + nn); - let i: i32 = 0; - for (i < nn) { - cgoutbuf[cgoutbuflen + i] = p[i]; - i += 1; - }; - cgoutbuflen += nn; + let buf: []u8; + buf.ptr = p; + buf.len = n: i32; + // memio.dynamicwrite never returns io.closed (memio.ww:166); + // bare-discard mirrors lib/log/log.ww:169 fmt.fprintln. + io.write(&cgoutstream, buf); } else { os.write(1, p, n); }; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 3150528d..d3b334aa 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -3,7 +3,7 @@ // Houses the top-level emission glue: // - cgfnparams: parameter spilling per SysV // - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue -// deferred via cgen.ww's cgoutbuf so the frame size +// deferred via cgen.ww's cgoutstate so the frame size // reflects every emit-time localadd (#15/#26c) // - cgfile: file-level entry (the exported driver) // @@ -350,7 +350,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { // through *(@sretarg) and returns @sretarg in RAX. let sret_callee: bool = sretretsize(c, c.fnret) > 0; - // Capture the body into cgoutbuf while c.frame grows under + // Capture the body into cgoutstate while c.frame grows under // emit-time localadd calls (#15/#26c — wwstage dropped its // scanlocals pre-pass to align DOWN with cstage's first-use // pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits @@ -358,7 +358,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { // localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds // `subsp`/`text` Progs up front and patches their `from.offset` // at the end via txt_emit. - cgout_enable(c.a); + cgout_enable(); if (sret_callee) { let saoff: i32 = localadd(c, "@sretarg", 8, nil); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ace9d173..2dd8ed63 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9155,6 +9155,268 @@ export fn checkfile(c: *checker, file: *node) void = { }; +// io — stream interface (Plan 9 Bio / Hare io::stream shape). +// +// No closures, no methods. A `stream` is a struct of function +// pointers plus a `ctx: *void`. The error channel is the return +// value of read/write/close — Hare-shaped tagged unions instead of +// errno-style integer sentinels. + +// eof — read past the end of the stream. Hare uses the `done` +// singleton for EOF; ww doesn't have `done` yet so we ship a +// named-void variant tag. +package io; + +export type eof = void; + +// closed — operation attempted on a stream that has already been +// closed. ww-specific: Hare's io collapses this into the wider +// errors union, but our stream vtable has no handle-ownership +// semantics, so a distinct tag is honest. Named void. +export type closed = void; + +// underread — an I/O handle hit eof partway through a fixed-size +// read. Payload is the byte count actually delivered. Mirrors +// Hare's `io::underread = !size`; ww uses i32 because the +// underlying buffer-length type is i32 today. +export type underread = !i32; + +export type stream = struct { + ctx: *void, + read: fn(s: *stream, buf: []u8) (i32 | eof | closed), + write: fn(s: *stream, buf: []u8) (i32 | closed), + close: fn(s: *stream) (void | closed), +}; + +export fn read(s: *stream, buf: []u8) (i32 | eof | closed) = { + return s.read(s, buf); +}; + +export fn write(s: *stream, buf: []u8) (i32 | closed) = { + return s.write(s, buf); +}; + +export fn close(s: *stream) (void | closed) = { + return s.close(s); +}; + +// 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; +}; + // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: @@ -20599,7 +20861,7 @@ fn cgcontinue(c: *cgen, n: *node) void = { // Houses the top-level emission glue: // - cgfnparams: parameter spilling per SysV // - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue -// deferred via cgen.ww's cgoutbuf so the frame size +// deferred via cgen.ww's cgoutstate so the frame size // reflects every emit-time localadd (#15/#26c) // - cgfile: file-level entry (the exported driver) // @@ -20946,7 +21208,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { // through *(@sretarg) and returns @sretarg in RAX. let sret_callee: bool = sretretsize(c, c.fnret) > 0; - // Capture the body into cgoutbuf while c.frame grows under + // Capture the body into cgoutstate while c.frame grows under // emit-time localadd calls (#15/#26c — wwstage dropped its // scanlocals pre-pass to align DOWN with cstage's first-use // pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits @@ -20954,7 +21216,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { // localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds // `subsp`/`text` Progs up front and patches their `from.offset` // at the end via txt_emit. - cgout_enable(c.a); + cgout_enable(); if (sret_callee) { let saoff: i32 = localadd(c, "@sretarg", 8, nil); @@ -21085,6 +21347,8 @@ import tok; import typ; import sym; import strconv; +import io; +import memio; // Split files. Bundler pulls these in transitively so consumers only // need `use cgen;`. Order matters for the flat-bundle concat — utils // first so cgenexpr/stmt/decl can reference helpers defined here. @@ -21732,60 +21996,50 @@ fn localfind(c: *cgen, name: str) i32 = { // Cgfn defers its prologue (TEXT / SUBQ) until after the body so the // frame size reflects every emit-time localadd — the scanlocals pre- // pass that previously pre-computed it was dropped per #15/#26c. The -// body is captured into cgoutbuf while cgoutmode != 0, then flushed +// body is captured into cgoutstate while cgoutmode != 0, then flushed // after the prologue is written to stdout. Module-level state so the // existing emitline/emitint/emitlabel/emitsymname callers don't have // to thread a *cgen they don't already hold. Mirrors cstage's deferred // Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up // front and patches `from.offset` after the body finishes). -let cgoutbuf: *u8 = nil; -let cgoutbufcap: i32 = 0; -let cgoutbuflen: i32 = 0; -let cgoutmode: i32 = 0; -let cgoutarena: *arena = nil; +// +// `cgoutinit` guards a one-shot [[memio.dynamic]] wiring so the +// backing buffer is sticky across fns: [[cgout_flush]]'s +// [[memio.reset]] rewinds `pos`/`len` without touching `cap`, so the +// allocation amortises the same way the previous arena buffer did. +// Re-init per fn would abandon the buffer (no [[io.close]] path → no +// [[os.free]]) and re-grow from 0 via the 8→…→65536 ladder for every +// function. Same idiom as lib/log/log.ww:124 `ensureinit`. +let cgoutstate: memio.state; +let cgoutstream: io.stream; +let cgoutmode: i32 = 0; +let cgoutinit: i32 = 0; -def CGOUT_INIT_CAP: i32 = 65536; - -fn cgout_grow(need: i32) void = { - if (need <= cgoutbufcap) { return; }; - let want: i32 = cgoutbufcap; - if (want == 0) { want = CGOUT_INIT_CAP; }; - for (want < need) { want = want * 2; }; - let p: *u8 = amalloc(cgoutarena, want: u64): *u8; - let i: i32 = 0; - for (i < cgoutbuflen) { - p[i] = cgoutbuf[i]; - i += 1; +fn cgout_enable() void = { + if (cgoutinit == 0) { + memio.dynamic(&cgoutstate, &cgoutstream); + cgoutinit = 1; }; - cgoutbuf = p; - cgoutbufcap = want; -}; - -fn cgout_enable(a: *arena) void = { - cgoutarena = a; - cgoutbuflen = 0; cgoutmode = 1; }; fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { - if (cgoutbuflen > 0) { - os.write(1, cgoutbuf, cgoutbuflen: u64); - cgoutbuflen = 0; + if (cgoutstate.pos > 0) { + os.write(1, cgoutstate.ptr, cgoutstate.pos: u64); + memio.reset(&cgoutstate); }; }; fn emitbytes(p: *u8, n: u64) void = { if (cgoutmode != 0) { - let nn: i32 = n: i32; - cgout_grow(cgoutbuflen + nn); - let i: i32 = 0; - for (i < nn) { - cgoutbuf[cgoutbuflen + i] = p[i]; - i += 1; - }; - cgoutbuflen += nn; + let buf: []u8; + buf.ptr = p; + buf.len = n: i32; + // memio.dynamicwrite never returns io.closed (memio.ww:166); + // bare-discard mirrors lib/log/log.ww:169 fmt.fprintln. + io.write(&cgoutstream, buf); } else { os.write(1, p, n); };