cgen + memio: cgoutarena → memio.dynamic, grow → dynamicgrow (β-3)
Phase 0 last β-shape site. Two concerns in one commit because the refactor surfaced the rename: - selfhost/cmd/wcc/cgen.ww cgout buffer (cgoutbuf/cap/len + arena + cgout_grow + CGOUT_INIT_CAP) → memio.state + io.stream behind a one-shot lazy-init guard. cgout_enable drops its *arena param; memio.reset in cgout_flush keeps the buffer sticky across fns so the arena's amortisation survives — re-init per fn would abandon the buffer and re-grow from 0 via the 8→…→65536 ladder for every function (no io.close path → no os.free). - lib/memio/memio.ww private fn grow → dynamicgrow. Symmetric with dynamicwrite / dynamicclose; required because cstage bundles all imported modules into a flat TU and resolves private fns by unqualified name, so the new `import memio;` in wcc's bundle collided with selfhost/cmd/wcc/mem.ww's arena `grow`. Module-aware private-fn scoping in cstage is task #9. @test fn dynamicgrow in memiotest.ww (same package as memio.ww) renamed to dynamicgrowcases to free the name; new suffix mirrors the file's existing fixedwritecases / borrowedreadcases convention. Lazy-init guard cgoutinit. memio.dynamic runs once on first cgout_enable; subsequent enables just set cgoutmode. Mirrors lib/log/log.ww:124 ensureinit. Without it, ~14 mmap syscalls per fn and ~100 MiB+ cumulative leak on a typical bootstrap. io.write bare discard in emitbytes mirrors lib/log/log.ww:169 — memio.dynamicwrite never returns io.closed (memio.ww:166). Verified 132/132 incl. 995_self_rebuild byte-identity.
This commit is contained in:
@@ -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; };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
//
|
||||
// `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 cgoutarena: *arena = nil;
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
//
|
||||
// `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 cgoutarena: *arena = nil;
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
//
|
||||
// `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 cgoutarena: *arena = nil;
|
||||
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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user