lib/bufio: add Option C parallel vstream API (#94 fold-e4)

Adds bufio_vstream + isbuffered_v alongside the pre-vtable
bufio.init / bufio.isbuffered surface, mirroring fold-e2's
lib/memio and fold-e3's lib/fmt parallel-API shape. The OLD
bufio.ww surface stays untouched; fold-eFinal (#50) atomically
flips the package shape, drops the `_v` suffix, and retires the
legacy callbacks.

bufio_ctx wraps an underlying *io.stream (OLD API) — bufio_vstream
src parameter type stays *io.stream until io fold-2 lands
`handle = (file | int)` (drew-deferred). vt is the first field
for the intrusive vstream→*bufio_ctx cast, same shape as
memio/fmt vstream wrappers.

Sibling task filed:

  #210  struct-lit slice-typed field silently drops under
        alloc(T{slice = val})?. Parallel to #207 for slice fields;
        scalar/ptr fields in the same alloc-struct-lit populate
        correctly. Workaround: post-alloc field-assign
        c.slicefield = val. Documented inline; drops out on close.

Other deferrals retained inline: #206 cast wrappers (3 vtable
wire-up + 2 isbuffered_v comparand), #173 nomem-widen for the
io.closed → io.error boundary, #207 alloc-zero-chain for vt.

bufio is not embedded in any selfhost combined.ww (test-only);
no Makefile regen needed (#110-blind safe).

test/wcc/778_bufio_vstream_run pins the 5-row scenario set:
write+flush, read+refill, isbuffered_v discriminator, OLD/NEW
boundary check, and a branched-callee #105 row. cs+ww+byte-id
green on all 5 rows.

make test: 211 passed (was 210).
This commit is contained in:
2026-05-29 10:24:02 +09:00
parent 2a405a4ad6
commit c62e8e5560
3 changed files with 803 additions and 0 deletions

320
lib/bufio/vstream.ww Normal file
View File

@@ -0,0 +1,320 @@
// vstream — Hare-shaped vtable wrappers over [[bufio.stream]]. Project
// #94 fold-e4 (Option C, parallel API for lib/bufio).
//
// Adds bufio_vstream + isbuffered_v alongside the pre-vtable
// bufio.init / bufio.isbuffered surface in bufio.ww. The OLD surface
// stays untouched here — fold-eFinal (task #50) atomically flips the
// package shape: deletes the OLD constructors + callbacks, renames
// `_v` suffix off (drops the `isbuffered_v` to `isbuffered` per Hare's
// ref/hare/bufio/stream.ha:179), and migrates the few callers.
//
// Hare's ref/hare/bufio/stream.ha:69 (init) returns the stream by
// value, with `vtable: nullable *io::vtable` pre-resolved per-mode
// (vtable_r / vtable_w / vtable_rw module-static singletons at
// stream.ha:8-25); ww doesn't ship static-storage tagged-union
// singletons yet, so we heap-allocate bufio_ctx, fill the vtable
// post-alloc per slot, and return &c.vt as the io.vstream. Same
// intrusive shape memio/vstream.ww + fmt/vstream.ww use.
//
// drew-approved deferrals (Hare-shape, not collapsed here):
//
// - Hare uses three vtable singletons (vtable_r / vtable_w /
// vtable_rw) to discriminate r-only / w-only / r+w mode;
// bufio_vstream always installs all three callbacks per
// bufio.ww:32-38's pre-existing divergence (zero-length rbuf or
// wbuf degenerates the matching callback in-cb). The Hare-shape
// mode-keyed singletons graduate with io fold-2 (handle).
//
// - Hare's defer-handle (`flag::MANAGED_*` ownership bits) deferred
// to io fold-2 (drew). Caller owns rbuf/wbuf/src for now;
// bclose_v flushes and forwards close to src but doesn't free
// any buffers (mirrors bufio.ww:60-64 OLD-side ownership note).
//
// Cast workaround per #206: bare `&fn_name` does not type-check as
// a `(*<alias> | void)` field-init / let-binding. Explicit
// `(&fn_name): *io.<role>` cast at each store site is the Hare-
// faithful minimum-touch route — same workaround memio/vstream.ww
// and fmt/vstream.ww use. 3 cast tokens at the vtable wire-up site,
// plus 2 in isbuffered_v for the fn-ptr-equality comparand. All 5
// drop out wholesale once #206 closes.
//
// Slice-field workaround per #210 (sibling of #207, surfaced here):
// struct-lit slice-typed field through `alloc(bufio_ctx{rbuf = rbuf,
// ...})?` SILENTLY drops the slice (reads back as zero-length).
// Scalar i32 and pointer fields in the same struct-lit populate
// correctly. Workaround: post-alloc `c.rbuf = rbuf;` field-assign,
// which works through the *bufio_ctx pointer. Same alloc-zero-chain
// shape as memio/vstream.ww uses for #207 over vt's tagged fields.
// 3 slice-field stores (rbuf / wbuf / flush) move post-alloc.
// Drops out wholesale on #210 close.
//
// fn-ptr-equality discriminator per ref/hare/bufio/stream.ha:179-184:
// isbuffered_v pattern-extracts the reader/writer slot and compares
// against `(&bread_v): *io.reader` / `(&bwrite_v): *io.writer`. Hare
// uses static-singleton symbol-address equality; ww routes through
// the cast-then-eq form because each slot is a tagged union
// (`(*reader | void)`) rather than a plain pointer. eFinal collapses
// to the Hare-direct form (`s.reader == &read`) once #192 closes and
// the slot becomes a plain nullable.
//
// io.closed → io.error conversion at the OLD-API boundary: the
// underlying *io.stream's io.read/io.write return (.. | io.closed)
// but the V API surface returns (.. | io.error). We construct nomem
// and widen to io.error per the fdsinkwrite_v precedent
// (fmt/vstream.ww:88-92) — same shape memio/vstream.ww uses for its
// os.write < 0 path. nomem is the carrier (not a semantic match for
// "closed") until io fold-2 lands a properly-shaped error variant.
//
// Sibling tasks parked here (filed, NOT fixed):
//
// - io fold-2 (handle port): drew-deferred. Hare's
// ref/hare/bufio/stream.ha:69 takes `src: io::handle`; once
// handle = (file | int) lands, bufio_vstream's src parameter
// collapses accordingly. eFinal (#50) graduates.
//
// - #206 (bare &fn → (*alias|void)): 5 cast sites here; drop
// out wholesale on close.
//
// - #207 (struct-lit multi-tagged-field copy drops past first):
// alloc-zero-chain pattern (`vt = zero` + post-alloc
// `c.vt.X = ...`) sidesteps; same shape memio/vstream.ww uses.
// Only one tagged field in bufio_ctx (vt), but the local-zero
// route stays consistent with memio's idiom.
//
// - #210 (struct-lit slice-typed field drops under alloc):
// surfaced by this fold. rbuf/wbuf/flush move post-alloc;
// drops out wholesale on close.
//
// - #209 (wwstage formattable match-arm bail when fmt imported):
// bufio doesn't import fmt transitively (only io), so the
// test/wcc/778 probe runs both stages.
package bufio;
import io;
// bufio_ctx — heap-alloc'd state for bufio_vstream. `vt` at offset 0
// for the intrusive vstream→*bufio_ctx cast. Mirrors lib/bufio.stream
// (bufio.ww:252) modulo the new io.vtable in place of the OLD
// io.stream first-field embed.
export type bufio_ctx = struct {
vt: io.vtable,
src: *io.stream,
rbuf: []u8,
rstart: i32,
rend: i32,
wbuf: []u8,
wend: i32,
flush: []u8,
};
// bufio_vstream — wire a vstream over an underlying *io.stream with
// caller-supplied read/write buffers. Both rbuf and wbuf may be
// empty; bread_v / bwrite_v degenerate per bufio.ww:34-38 in that
// case. The flush byte-set defaults to "\n" (line-buffered writes);
// per-stream setflush graduates with #50 / io fold-2.
//
// Mirrors ref/hare/bufio/stream.ha:69 (init).
export fn bufio_vstream(src: *io.stream, rbuf: []u8, wbuf: []u8) (io.vstream | nomem) = {
let zero: io.vtable;
let c: *bufio_ctx = alloc(bufio_ctx{
vt = zero,
src = src,
rstart = rbuf.len,
rend = rbuf.len,
wend = 0,
})?;
// Post-alloc slice-field stores per #210 (sibling of #207):
// struct-lit `rbuf = rbuf` silently drops slice-typed fields
// under alloc(T{...}). Scalar/ptr fields in the same struct-lit
// store correctly. Same alloc-zero-chain shape memio/vstream.ww
// uses for #207 over vt's tagged fields. 3 stores; drops out on
// #210 close.
c.rbuf = rbuf;
c.wbuf = wbuf;
c.flush = flushdefault[0:1];
c.vt.reader = (&bread_v): *io.reader;
c.vt.writer = (&bwrite_v): *io.writer;
c.vt.closer = (&bclose_v): *io.closer;
return &c.vt;
};
// isbuffered_v — true when `s` was returned by [[bufio_vstream]].
// Hare uses callback-identity (ref/hare/bufio/stream.ha:179);
// matches the OLD isbuffered shape (bufio.ww:338) but over the new
// `_v` fn symbols. Either reader or writer matching is sufficient
// (closer alone isn't unique to bufio).
export fn isbuffered_v(s: io.vstream) bool = {
match (s.reader) {
case let r: *io.reader => {
if (r == (&bread_v): *io.reader) { return true; };
};
case void => { };
};
match (s.writer) {
case let w: *io.writer => {
if (w == (&bwrite_v): *io.writer) { return true; };
};
case void => { };
};
return false;
};
// ---- vtable callbacks ----------------------------------------------------
// bread_v — vstream-side read. Recover ctx via the intrusive
// vstream→*bufio_ctx cast. Mirrors bufio.ww:346 (OLD bread) modulo
// the (size | io.eof | io.error) return surface; io.closed from the
// underlying *io.stream's io.read widens to io.error via nomem
// (same shape fmt/vstream.ww:88-92 + memio/vstream.ww:71-74 use,
// per #173 deferral).
fn bread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
let b: *bufio_ctx = s: *bufio_ctx;
if (b.rbuf.len == 0) {
let e: io.eof; return e;
};
if (b.rstart >= b.rend) {
b.rstart = 0;
b.rend = 0;
let r: (i32 | io.eof | io.closed) = io.read(b.src, b.rbuf);
match (r) {
case let n: i32 => { b.rend = n; };
case io.eof => { let e: io.eof; return e; };
case io.closed => {
let nm: nomem;
let e: io.error = nm;
return e;
};
};
};
let avail: i32 = b.rend - b.rstart;
let n: i32 = buf.len;
if (avail < n) { n = avail; };
let i: i32 = 0;
for (i < n) {
buf[i] = b.rbuf[b.rstart + i];
i += 1;
};
b.rstart += n;
return n: size;
};
// bwrite_v — vstream-side write. Mirrors bufio.ww:376 (OLD bwrite)
// modulo the (size | io.error) return surface; io.closed widens to
// io.error per the bread_v note. Default-flush scan + per-batch copy
// + post-write conditional flush — same flush-byte-set semantics as
// the OLD path (ref/hare/bufio/stream.ha:236-246, labeled-break
// inlined per bufio.ww:382's note).
fn bwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
let b: *bufio_ctx = s: *bufio_ctx;
if (b.wbuf.len == 0) {
let r: (i32 | io.closed) = io.write(b.src, buf);
match (r) {
case let n: i32 => { return n: size; };
case io.closed => {
let nm: nomem;
let e: io.error = nm;
return e;
};
};
};
let doflush: bool = false;
if (b.flush.len != 0) {
let i: i32 = 0;
for (i < buf.len) {
let j: i32 = 0;
for (j < b.flush.len) {
if (buf[i] == b.flush[j]) {
doflush = true;
i = buf.len;
j = b.flush.len;
};
j += 1;
};
i += 1;
};
};
let z: i32 = 0;
for (z < buf.len) {
let avail: i32 = b.wbuf.len - b.wend;
if (avail == 0) {
let fr: (void | io.error) = flush_v(b);
match (fr) {
case void => { };
case let e: io.error => return e;
};
avail = b.wbuf.len;
};
let n: i32 = buf.len - z;
if (avail < n) { n = avail; };
let i: i32 = 0;
for (i < n) {
b.wbuf[b.wend + i] = buf[z + i];
i += 1;
};
b.wend += n;
z += n;
};
if (doflush) {
let fr: (void | io.error) = flush_v(b);
match (fr) {
case void => { };
case let e: io.error => return e;
};
};
return buf.len: size;
};
// bclose_v — flush pending wbuf, forward close to src. Mirrors
// bufio.ww:431. Hare's close_buffered also handles the
// MANAGED_HANDLE / MANAGED_RDBUF / MANAGED_WRBUF ownership bits
// (ref/hare/bufio/stream.ha:188-202); ww drops them per bufio.ww:33-47
// (caller-owned today; defer-handle graduates with io fold-2,
// drew-deferred).
fn bclose_v(s: io.vstream) (void | io.error) = {
let b: *bufio_ctx = s: *bufio_ctx;
let fr: (void | io.error) = flush_v(b);
match (fr) {
case void => { };
case let e: io.error => return e;
};
let cr: (void | io.closed) = io.close(b.src);
match (cr) {
case void => return void;
case io.closed => {
let nm: nomem;
let e: io.error = nm;
return e;
};
};
};
// flush_v — drain pending wbuf to src. Mirror of bufio.ww:294 (OLD
// flush) over the V surface; io.closed widens to io.error per the
// bread_v note. Module-prefixed `_v` suffix keeps the cstage flat-TU
// private-fn scope from colliding with OLD flush (same shape
// memio.dynamicgrow_v at memio/vstream.ww:229).
fn flush_v(b: *bufio_ctx) (void | io.error) = {
if (b.wend == 0) { return; };
let off: i32 = 0;
for (off < b.wend) {
let r: (i32 | io.closed) = io.write(b.src, b.wbuf[off:b.wend]);
match (r) {
case let n: i32 => {
if (n == 0) {
let nm: nomem;
let e: io.error = nm;
return e;
};
off += n;
};
case io.closed => {
let nm: nomem;
let e: io.error = nm;
return e;
};
};
};
b.wend = 0;
return;
};