From 10cb835f99eacd1e3927b3a2247aa05046c037a7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 21:14:39 +0900 Subject: [PATCH] lib: complete the parallel vstream surface to Hare value-return shape (#94 fold-eFinal prep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #94 Option-C vstream surface was left incomplete and structurally divergent from Hare: constructors heap-allocated and returned (X | nomem) or used out-params instead of Hare's by-value stack ownership; memio lacked reset/buffer/borrowedread; bufio's scanner was never ported to the vtable. This is the additive half of the eFinal collapse — OLD surface stays fully live; the destructive FLIP (delete OLD + drop _v + repoint) is the next commit. Reshape all constructors to VALUE-RETURN (field-by-field sret; the heap + nomem was an unnecessary crutch — wide slice-bearing struct return-by-value is byte-id-proven, cf 925_sret_struct_return_run). memio fixed/dynamic/ dynamicfrom, bufio init, log new now return the struct by value; the nomem is gone with the alloc that forced it. memio: unify the per-flavour ctx structs onto one `stream` (vt at offset 0); collapse fixed_string + dynamic_string into a single string() over the common header (bare-str return is the ratified rule-9 frombytes carve-out, cited at the site per ref/hare/memio/stream.ha:81); port reset/buffer/borrowedread as single fns over the header. bufio: collapse the EXISTING scanner subset (newscannerbuf/scanbyte/scanbytes/ scanline/finish + setflush/flush/unread/isbuffered) onto the vtable, with src now io.vstream so reads go through io.st_read. The Hare scanner functions ww never implemented (scanrune/scanstring-arbitrary-delim/readtok/readline/ auto-grow newscanner) are out of scope and deferred to #217 — eFinal is a collapse, not a feature expansion. Keep the explicit (&fn): *io.T casts on vtable-slot stores (cgen-neutral; avoids the #214 (X|void) over-acceptance surface; dropping the casts is a deferred #206 payoff gated on #214). Self-gate: 776 (memio) 18/18 and 778 (bufio) 27/27, every row carrying a cs.s == ww.s byte-id check — bufio/fmt/log are not compiler-embedded, so these rows are their only byte-id coverage. 779/781 stay STAGE_CS-only pending #209. Regen w6c+wwdump combined.ww (io+memio are the embedded modules). --- lib/bufio/vstream.ww | 477 ++++++++++++-------- lib/fmt/vstream.ww | 47 +- lib/log/vstream.ww | 22 +- lib/memio/vstream.ww | 304 ++++++------- selfhost/cmd/w6c/main.combined.ww | 304 ++++++------- selfhost/cmd/wwdump/main.combined.ww | 304 ++++++------- test/wcc/776_memio_vstream_run.c | 137 ++++-- test/wcc/778_bufio_vstream_run.c | 280 ++++++++---- test/wcc/779_log_vstream_run.c | 33 +- test/wcc/781_fmt_vstream_compositions_run.c | 20 +- 10 files changed, 1068 insertions(+), 860 deletions(-) diff --git a/lib/bufio/vstream.ww b/lib/bufio/vstream.ww index 8e0f3220..ba5f2616 100644 --- a/lib/bufio/vstream.ww +++ b/lib/bufio/vstream.ww @@ -1,105 +1,79 @@ -// vstream — Hare-shaped vtable wrappers over [[bufio.stream]]. Project -// #94 fold-e4 (Option C, parallel API for lib/bufio). +// vstream — Hare-shaped vtable port of lib/bufio. Project #94 +// fold-eFinal(PREP). // -// 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. +// Collapses the OLD bufio surface (bufio.ww's pre-vtable `stream` + +// `scanner` over a legacy `*io.stream` src) onto the io.vtable surface, +// as parallel `_v`-suffixed additions. The OLD surface stays untouched +// here — fold-eFinal (FLIP) deletes it, renames the `_v` suffix off +// (bufio_ctx→stream, bufio_vstream→init, scanner_v→scanner, scanbytes_v +// →scanbytes, isbuffered_v→isbuffered, …) per Hare's +// ref/hare/bufio/{stream,scanner}.ha, 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. +// SCOPE (#94 fold-eFinal): this is a COLLAPSE of the surface ww ALREADY +// ships, NOT a port of Hare's full scanner. The features OLD bufio +// never implemented — auto-grow newscanner, multibyte-delim scanbytes / +// scanstring, scanrune / readbyte / readtok / readline / readrune / +// unreadrune — are a separate future feature fold (#217) and are OUT of +// eFinal. The ported set mirrors OLD exactly: the fixed-buffer +// newscanner_buf, single-byte scanbytes (OLD scantok), single-byte +// scanline, scanbyte, finish; and the buffered-stream init / setflush / +// flush / unread / isbuffered. // -// drew-approved deferrals (Hare-shape, not collapsed here): +// VALUE-RETURN (drew, gating): Hare's bufio constructors return BY +// VALUE (ref/hare/bufio/stream.ha:69 init, scanner.ha:92 +// newscanner_buf). The OLD out-param shape existed only because +// wide-struct sret was unimplemented — it now round-trips +// (test/wcc/925 + 776/778 byte-id), so each constructor builds in a +// local `let r: T;`, field-assigns every slot, and `return r;`. The +// caller owns the returned struct (stack ownership, no-GC). For the +// buffered stream the caller passes `&b.vt` to the io.st_* dispatchers; +// the scanner is a plain read-ahead tokenizer (no embedded vtable — +// matching OLD), driven directly via scanbyte_v / scanbytes_v / … // -// - 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). +// VTABLE-NATIVE SRC (the eFinal point): the buffered stream's `src` and +// the scanner's `src` are now `io.vstream`, and every underlying read / +// write / close goes through io.st_read / io.st_write / io.st_close +// (the vtable dispatchers) — NOT the legacy io.read / io.write / +// io.close over `*io.stream`. The io.st_* surface returns io.error +// directly, so the OLD `io.closed → nomem-widen → io.error` dance +// (per the #173 deferral) is GONE: the error simply forwards. // -// - 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-payoff (ken: KEEP the explicit casts; they +// are cgen-neutral and sidestep the #214 over-acceptance surface). +// `(&fn_name): *io.` at each vtable store + the isbuffered_v +// fn-ptr-equality comparand — same workaround memio/vstream.ww + +// test/wcc/775 use. // -// Cast workaround per #206: bare `&fn_name` does not type-check as -// a `(* | void)` field-init / let-binding. Explicit -// `(&fn_name): *io.` 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. +// #207 (struct-lit multi-tagged-field copy drops past first) and #210 +// (slice-field struct-lit drop under alloc): not reachable from the +// value-return form — every struct is built field-by-field in a local +// then sret-returned, no `alloc(T{...})` struct-lit. Both filings stay +// open for the struct-lit path; this fold doesn't take it. // -// 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. +// #209 (wwstage formattable match-arm bail when fmt imported): bufio +// does NOT import fmt (only io), so test/wcc/778 runs both stages + +// cs.s == ww.s byte-id on every row — bufio's only byte-id coverage +// (it is not compiler-embedded). // -// 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. +// Mode discrimination (drew-deferred): Hare uses three vtable +// singletons (vtable_r / vtable_w / vtable_rw); bufio_vstream installs +// all three callbacks per bufio.ww:32-38's divergence (zero-length +// rbuf/wbuf degenerates the matching callback in-cb). Defer-handle +// (MANAGED_* ownership bits) also deferred — caller owns rbuf/wbuf/src; +// bclose_v flushes + forwards close but frees nothing. Both graduate +// with io fold-2 (#5). 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. +// bufio_ctx — heap-free state for bufio_vstream. `vt` at offset 0 for +// the intrusive vstream→*bufio_ctx cast. `src` is an io.vstream (the +// vtable-native underlying handle), not the OLD `*io.stream`. Mirrors +// lib/bufio.stream (bufio.ww:252) modulo io.vtable + io.vstream src. export type bufio_ctx = struct { vt: io.vtable, - src: *io.stream, + src: io.vstream, rbuf: []u8, rstart: i32, rend: i32, @@ -108,42 +82,89 @@ export type bufio_ctx = struct { 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. +// bufio_vstream — wire a buffered stream over an underlying io.vstream +// with caller-supplied read/write buffers. Both rbuf and wbuf may be +// empty; bread_v / bwrite_v degenerate per bufio.ww:34-38. The flush +// byte-set defaults to "\n" (line-buffered writes); setflush_v swaps it. +// +// Returns the ctx BY VALUE (sret — field-by-field build then +// `return r;`); the caller passes `&b.vt` to the io.st_* dispatchers. // // 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; +export fn bufio_vstream(src: io.vstream, rbuf: []u8, wbuf: []u8) bufio_ctx = { + let r: bufio_ctx; + r.vt.reader = (&bread_v): *io.reader; + r.vt.writer = (&bwrite_v): *io.writer; + r.vt.closer = (&bclose_v): *io.closer; + r.src = src; + r.rbuf = rbuf; + r.rstart = rbuf.len; + r.rend = rbuf.len; + r.wbuf = wbuf; + r.wend = 0; + r.flush = flushdefault[0:1]; + return r; +}; + +// setflush_v — install a new flush byte-set. Any byte from `bs` +// appearing in a write payload triggers an automatic flush after the +// write copies into wbuf. Mirrors OLD setflush (bufio.ww:287) + +// ref/hare/bufio/stream.ha:128. +export fn setflush_v(b: *bufio_ctx, bs: []u8) void = { + b.flush = bs; +}; + +// flush_v — drain any pending wbuf data to src. Public API AND the +// internal drain called by bwrite_v / bclose_v — OLD bufio.flush is +// itself dual-purpose (bufio.ww:294, called from bwrite/bclose), so a +// single fn serves both; no separate private callback to rename. The +// io.closed → nomem-widen of the OLD V-side is gone: io.st_write +// returns io.error directly. A 0-byte non-error write means the sink +// made no progress (memio.fixed full) — surfaced as a nomem-carried +// io.error rather than spinning, matching what io.writeall would do in +// Hare. Mirrors OLD flush (bufio.ww:294) + ref/hare/bufio/stream.ha. +export fn flush_v(b: *bufio_ctx) (void | io.error) = { + if (b.wend == 0) { return; }; + let off: i32 = 0; + for (off < b.wend) { + let r: (size | io.error) = io.st_write(b.src, b.wbuf[off:b.wend]); + match (r) { + case let n: size => { + if (n == 0: size) { + let nm: nomem; + let e: io.error = nm; + return e; + }; + off += n: i32; + }; + case let e: io.error => return e; + }; + }; + b.wend = 0; + return; +}; + +// unread_v — push `buf` back into the read buffer so the next bread_v +// returns it first. The bytes must fit in front of the pending region +// (rstart >= buf.len); Hare aborts on overflow, ww does the same via +// rtabort (declared in bufio.ww, same package). Mirrors OLD unread +// (bufio.ww:321) + ref/hare/bufio/stream.ha:164. +export fn unread_v(b: *bufio_ctx, buf: []u8) void = { + if (b.rstart < buf.len) { + rtabort("bufio.unread_v: more data than rbuf has room for"); + }; + let i: i32 = 0; + for (i < buf.len) { + b.rbuf[b.rstart - buf.len + i] = buf[i]; + i += 1; + }; + b.rstart -= buf.len; }; // 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). +// Hare uses callback-identity (ref/hare/bufio/stream.ha:179); matches +// the OLD isbuffered shape (bufio.ww:338) over the new `_v` symbols. +// Either reader or writer matching is sufficient. export fn isbuffered_v(s: io.vstream) bool = { match (s.reader) { case let r: *io.reader => { @@ -160,14 +181,12 @@ export fn isbuffered_v(s: io.vstream) bool = { return false; }; -// ---- vtable callbacks ---------------------------------------------------- +// ---- buffered-stream 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). +// bread_v — vstream-side buffered read. Recover ctx via the intrusive +// cast; refill rbuf from src via io.st_read on empty pending region. +// Mirrors bufio.ww:346 (OLD bread) modulo the io.vstream src + +// io.st_read dispatch (io.error forwards directly — no nomem-widen). 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) { @@ -176,15 +195,11 @@ fn bread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { if (b.rstart >= b.rend) { b.rstart = 0; b.rend = 0; - let r: (i32 | io.eof | io.closed) = io.read(b.src, b.rbuf); + let r: (size | io.eof | io.error) = io.st_read(b.src, b.rbuf); match (r) { - case let n: i32 => { b.rend = n; }; + case let n: size => { b.rend = n: i32; }; case io.eof => { let e: io.eof; return e; }; - case io.closed => { - let nm: nomem; - let e: io.error = nm; - return e; - }; + case let e: io.error => return e; }; }; let avail: i32 = b.rend - b.rstart; @@ -199,24 +214,15 @@ fn bread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { 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). +// bwrite_v — vstream-side buffered write. wbuf-empty passes through to +// src; otherwise default-flush scan + per-batch copy + post-write +// conditional flush. Mirrors bufio.ww:376 (OLD bwrite) modulo the +// io.vstream src + io.st_write dispatch. Labeled-break inlined per +// bufio.ww:382 (ww has no labeled break). 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; - }; - }; + return io.st_write(b.src, buf); }; let doflush: bool = false; if (b.flush.len != 0) { @@ -265,12 +271,9 @@ fn bwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { 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). +// bclose_v — flush pending wbuf, forward close to src via io.st_close. +// Mirrors bufio.ww:431. Caller-owned buffers/src are not freed (drew +// defer-handle deferral, bufio.ww:33-47). fn bclose_v(s: io.vstream) (void | io.error) = { let b: *bufio_ctx = s: *bufio_ctx; let fr: (void | io.error) = flush_v(b); @@ -278,43 +281,149 @@ fn bclose_v(s: io.vstream) (void | io.error) = { case void => { }; case let e: io.error => return e; }; - let cr: (void | io.closed) = io.close(b.src); + let cr: (void | io.error) = io.st_close(b.src); match (cr) { case void => return void; - case io.closed => { - let nm: nomem; - let e: io.error = nm; - return e; - }; + case let e: io.error => 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]); +// ---- scanner (read-ahead tokenizer over an io.vstream src) --------------- +// +// Plain tokenizer — no embedded vtable (matching OLD bufio.scanner, +// bufio.ww:104); the scanner is driven directly via scanbyte_v / +// scanbytes_v / scanline_v, not through io.st_*. Its `src` is an +// io.vstream and refills go through io.st_read. Value-return +// constructor (Hare newscanner_buf, scanner.ha:92). +// +// ptr/cap kept flat (no `buf: []u8`) per the OLD scanner shape +// (bufio.ww:104-110); the scanner predates struct-held slice support. + +export type scanner_v = struct { + src: io.vstream, + ptr: *u8, + cap: i32, + start: i32, // index where the pending region starts in ptr + avail: i32, // pending byte count; pending = ptr[start..start+avail] +}; + +// newscannerbuf_v — wire a scanner to read through `src` using `buf` as +// the fixed read-ahead window. Returns the scanner BY VALUE. This is +// Hare's newscanner_buf (ref/hare/bufio/scanner.ha:92); the auto-grow +// newscanner is a separate feature (#217, OUT of eFinal). Mirrors OLD +// newscanner (bufio.ww:116), which is itself the fixed-buffer form. +export fn newscannerbuf_v(src: io.vstream, buf: []u8) scanner_v = { + let r: scanner_v; + r.src = src; + r.ptr = buf.ptr; + r.cap = buf.len; + r.start = 0; + r.avail = 0; + return r; +}; + +// finish_v — release scanner-owned resources. No-op (buffer is +// caller-owned, src isn't closed); kept on the surface so callers won't +// churn. Mirrors OLD finish (bufio.ww:127) + scanner.ha:110. +export fn finish_v(s: *scanner_v) void = { }; + +// readahead_v — make room and read once from src into the back of the +// pending region. Returns bytes newly buffered (>=0), or io.eof/io.error +// from src. Mirrors OLD readahead (bufio.ww:133) modulo io.st_read + +// io.error (no nomem-widen). Returns i32 (OLD shape); the size from +// io.st_read narrows to i32 (buffer-length type). +fn readahead_v(s: *scanner_v) (i32 | io.eof | io.error) = { + if (s.start + s.avail == s.cap && s.start > 0) { + let i: i32 = 0; + for (i < s.avail) { + s.ptr[i] = s.ptr[s.start + i]; + i += 1; + }; + s.start = 0; + }; + let off: i32 = s.start + s.avail; + let v: []u8; + v.ptr = s.ptr + (off: u64); + v.len = s.cap - off; + let r: (size | io.eof | io.error) = io.st_read(s.src, v); + match (r) { + case let n: size => { + s.avail += n: i32; + return n: i32; + }; + case io.eof => { let e: io.eof; return e; }; + case let e: io.error => return e; + }; +}; + +// scanbyte_v — pop one byte, refilling from src on demand. Mirrors OLD +// scanbyte (bufio.ww:160) + ref/hare/bufio/scanner.ha:204. +export fn scanbyte_v(s: *scanner_v) (u8 | io.eof | io.error) = { + for (s.avail == 0) { + let r: (i32 | io.eof | io.error) = readahead_v(s); 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; - }; + case let n: i32 => { }; + case io.eof => { let e: io.eof; return e; }; + case let e: io.error => return e; }; }; - b.wend = 0; - return; + let b: u8 = s.ptr[s.start]; + s.start += 1; + s.avail -= 1; + return b; +}; + +// scanbytes_v — read up to (and not including) the next byte equal to +// `delim`. The delim is consumed but not returned. The returned slice +// borrows from the scanner buffer and is invalidated by the next scan. +// EOF without delim discards the trailing fragment and returns io.eof +// (Hare EOF_DISCARD default); buffer-full without delim returns +// overflow. Single-byte delim only — Hare's `(u8 | []u8)` multibyte +// form is #217 (OUT). drew renames OLD scantok → scanbytes. Mirrors OLD +// scantok (bufio.ww:186) + ref/hare/bufio/scanner.ha:220 (narrowed). +export fn scanbytes_v(s: *scanner_v, delim: u8) ([]u8 | io.eof | io.error | overflow) = { + let i: i32 = 0; + for (true) { + for (i < s.avail) { + if (s.ptr[s.start + i] == delim) { + let v: []u8; + v.ptr = s.ptr + (s.start: u64); + v.len = i; + s.start += i + 1; + s.avail -= i + 1; + return v; + }; + i += 1; + }; + if (s.start + s.avail == s.cap && s.start == 0) { + let e: overflow; return e; + }; + let r: (i32 | io.eof | io.error) = readahead_v(s); + match (r) { + case let n: i32 => { }; + case io.eof => { let e: io.eof; return e; }; + case let e: io.error => return e; + }; + }; + let e: io.eof; return e; +}; + +// scanline_v — read up to (and not including) the next '\n'. The newline +// is consumed; the returned str view borrows from the scanner buffer. +// Single-byte route (Hare's scan_line = scan_string(s, "\n"); the +// arbitrary multibyte-delim scan_string is #217, OUT). Mirrors OLD +// scanline (bufio.ww:225) + ref/hare/bufio/scanner.ha:307. +export fn scanline_v(s: *scanner_v) (str | io.eof | io.error | overflow) = { + let r: ([]u8 | io.eof | io.error | overflow) = scanbytes_v(s, 10u8); + match (r) { + case let bs: []u8 => { + let v: str; + v.ptr = bs.ptr; + v.len = bs.len; + return v; + }; + case io.eof => { let e: io.eof; return e; }; + case let e: io.error => return e; + case overflow => { let e: overflow; return e; }; + }; }; diff --git a/lib/fmt/vstream.ww b/lib/fmt/vstream.ww index d293f0a1..edfc67f3 100644 --- a/lib/fmt/vstream.ww +++ b/lib/fmt/vstream.ww @@ -560,9 +560,10 @@ export fn fdprintfln_v(fd: i32, fmt: str, args: field...) (size | io.error) = { }; // ---- V-side compositions over the vfprint / vfprintf primitives. -// Project #94 fold-e7. drew-approved bundle (memio.fixed_string / -// memio.dynamic_string land alongside as direct enablers, not churn — -// feedback_refactor_routing_same_class_drops); ken cs==ww mechanical +// Project #94 fold-e7. drew-approved bundle (memio.string_v is the +// collapsed single accessor over the common `stream` header — direct +// enabler, not churn — feedback_refactor_routing_same_class_drops); +// ken cs==ww mechanical // (additive only). eFinal (#50) collapses both surfaces and renames // `v` suffix off. // @@ -604,20 +605,18 @@ export fn vfprintfln(vs: io.vstream, fmt: str, args: field...) (size | io.error) // vbsprintf — render into `buf` through memio.fixed_vstream; return // the str view of bytes actually written. Mirror fmt.bsprintf -// (fmt.ww:839); nomem from fixed_vstream widens into io.error (Hare -// wrappers.ha:42 returns `(const str | nomem)` directly — ww collapses -// to (str | io.error) so the vfprintf path's io.error arm stays -// uniform). Short writes surface as a prefix (memio.fixed contract, -// vstream.ww:71 note). +// (fmt.ww:839). memio.fixed_vstream now returns the `stream` BY VALUE +// (fold-eFinal PREP, ref/hare/memio/stream.ha:46) — no alloc, no +// `nomem` arm. The stream lives in this frame; `&st.vt` is the +// io.vstream and `&st` the accessor handle. Hare wrappers.ha:42 returns +// `(const str | nomem)`; ww collapses to (str | io.error) so the +// vfprintf path's io.error arm stays uniform. Short writes surface as a +// prefix (memio.fixed contract, vstream.ww fixedwrite note). export fn vbsprintf(buf: []u8, fmt: str, args: field...) (str | io.error) = { - let r: (io.vstream | nomem) = memio.fixed_vstream(buf); - let vs: io.vstream = nil: *io.vtable; - match (r) { - case let v: io.vstream => { vs = v; }; - case let nm: nomem => { let e: io.error = nm; return e; }; - }; + let st: memio.stream = memio.fixed_vstream(buf); + let vs: io.vstream = &st.vt; match (vfprintf(vs, fmt, args...)) { - case let n: size => { return memio.fixed_string(vs); }; + case let n: size => { return memio.string_v(&st); }; case let e: io.error => return e; }; }; @@ -630,26 +629,26 @@ export fn vbsprintf(buf: []u8, fmt: str, args: field...) (str | io.error) = { // frees with `os.free(r.ptr, r.len: u64)` when r.len > 0; r.len == 0 // is a no-op free (same shape strings.dup uses at strings.ww:70). // Shrink-to-fit rationale: memio.dynamic_vstream's cap doubles past -// pos during growth (memio/vstream.ww:230); io.st_close frees the -// cap-sized mapping. Returning memio.dynamic_string directly would +// pos during growth (memio dynamicgrow_v); io.st_close frees the +// cap-sized mapping. Returning memio.string_v directly would // leak the cap-vs-len slack (skip close) or dangle the view (close // first); the copy lets the caller free with r.len. export fn vasprintf(fmt: str, args: field...) str = { let out: str; out.ptr = nil; out.len = 0; - let r: (io.vstream | nomem) = memio.dynamic_vstream(); - let vs: io.vstream = nil: *io.vtable; - match (r) { - case let v: io.vstream => { vs = v; }; - case nomem => { return out; }; - }; + // memio.dynamic_vstream returns the `stream` BY VALUE (fold-eFinal + // PREP, ref/hare/memio/stream.ha:58) — no alloc, no `nomem` arm. + // The stream lives in this frame across the vfprintf + close; the + // heap-grown backing (st.ptr) is freed by io.st_close. + let st: memio.stream = memio.dynamic_vstream(); + let vs: io.vstream = &st.vt; let wres: (size | io.error) = vfprintf(vs, fmt, args...); match (wres) { case let n: size => {}; case let e: io.error => {}; }; - let view: str = memio.dynamic_string(vs); + let view: str = memio.string_v(&st); if (view.len == 0) { let cres: (void | io.error) = io.st_close(vs); match (cres) { case void => {}; case let e: io.error => {}; }; diff --git a/lib/log/vstream.ww b/lib/log/vstream.ww index bb9df654..98cece92 100644 --- a/lib/log/vstream.ww +++ b/lib/log/vstream.ww @@ -220,15 +220,21 @@ fn silentprintfln_v(l: *vlogger, format: str, fields: fmt.field...) void = { }; // ---- public API (10 _v variants) ----------------------------------------- -// new_v — wire `sl` as a vstdlogger over `sink`. Hare returns by -// value (ref/hare/log/logger.ha:20); ww cgen can't return wide -// structs, so we take an out-parameter pointer (same shape as OLD -// new at log.ww:193 + memio.fixed + bufio.init). -export fn new_v(sl: *vstdlogger, sink: io.vstream) void = { +// new_v — build a vstdlogger over `sink`, returned BY VALUE (fold-eFinal +// PREP). Hare returns the stdlogger by value (ref/hare/log/logger.ha:20); +// the OLD out-parameter shape existed only because wide-struct sret was +// unimplemented — it now round-trips (test/wcc/925), so the constructor +// builds in a local and `return r;` (proven field-by-field sret form). +// vstdlogger is 24B (two fn-ptrs + sink), returned via the SysV memory +// class; the caller owns the returned logger (stack ownership, no-GC) +// and passes `&sl.logger` to the dispatch fns. +export fn new_v(sink: io.vstream) vstdlogger = { ensureinit_v(); - sl.logger.println_v = stdprintln_v; - sl.logger.printfln_v = stdprintfln_v; - sl.sink = sink; + let r: vstdlogger; + r.logger.println_v = stdprintln_v; + r.logger.printfln_v = stdprintfln_v; + r.sink = sink; + return r; }; // lprintln_v — dispatch `args` through `log`. Mirrors OLD lprintln diff --git a/lib/memio/vstream.ww b/lib/memio/vstream.ww index fcd07083..13e0f24c 100644 --- a/lib/memio/vstream.ww +++ b/lib/memio/vstream.ww @@ -1,41 +1,45 @@ // vstream — Hare-shaped vtable wrappers over memio. Project #94 -// fold-e2 (Option C, parallel API). +// fold-eFinal(PREP). // -// Adds three constructors that return `io.vstream` (= `*io.vtable`, -// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable -// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The -// OLD surface stays untouched here — fold-eFinal (task #50) atomically -// flips the package shape: deletes the OLD constructors + callbacks, -// renames `_vstream` suffix off, and migrates the few callers. +// Adds three constructors that return a memio `stream` BY VALUE (the +// Hare shape, ref/hare/memio/stream.ha:46,58,64) alongside the +// pre-vtable memio.fixed / memio.dynamic / memio.dynamicfrom in +// memio.ww. The OLD surface stays untouched here — fold-eFinal (FLIP) +// atomically flips the package shape: deletes the OLD constructors + +// `state` + callbacks, renames the `_vstream`/`_v` suffix off (so +// `fixed_vstream → fixed`, `stream` stays, `string_v → string`), and +// migrates the few callers (lib/fmt/vstream.ww + the 776 probe). // -// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS -// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx -// struct's first field is `vt: io.vtable` (the vtable embedded -// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is -// castable to `vstream = *vtable` via `&c.vt` — and the callbacks -// recover the outer ctx by casting the dispatch arg back to -// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over -// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger -// (log.ww:95-98). +// 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 +// `vstream = *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. // -// Cast workaround per #206: bare `&fn_name` does not type-check as -// a `(* | void)` field-init / let-binding (the structural -// `*fn(...)` value isn't accepted as the `*reader` named variant -// of the tagged slot). Explicit `(&fn_name): *io.` cast at -// each store site is the Hare-faithful minimum-touch route — same -// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable -// init. 8 cast tokens here (2 in fixed_vstream + 3 each in -// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer -// void per Hare's fixed_vt, which lets st_close's void-arm return -// plain `void` with no callback needed). Casts drop out wholesale -// once #206 closes. +// VALUE-RETURN (drew, gating): the constructor builds the struct in a +// local `let r: stream;`, field-assigns every slot (including the +// tagged vt sub-fields), and `return r;` — the proven sret round-trip +// shape pinned by test/wcc/925 ("ident_return_rhs" row) and extended +// to tagged-union-field structs by the 776 byte-id rows here. NO heap, +// NO `nomem`: the alloc that forced the OLD `(io.vstream | nomem)` +// return is gone, so the constructor cannot fail. Caller owns the +// returned `stream` (stack ownership, no-GC) and passes `&s.vt` to the +// io.st_* dispatchers — exactly Hare's `&s` into io::write. +// +// 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 — same workaround test/wcc/775_io_vtable_run.c +// uses for the bare-vtable init. // // ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note // (memio.ww:39): chained-dot writes through a state pointer into a -// slice subfield miscompile silently (related to the #195 family); -// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's -// `cap` (NOT `len`) to track the allocated-capacity-to-free on -// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73). +// slice subfield miscompile silently (#195 family); the flat shape +// sidesteps it. dynamicfrom_vstream uses the slice's `cap` (NOT `len`) +// to track the allocated-capacity-to-free on close — mirrors the OLD +// memio.dynamicfrom note (memio.ww:73). package memio; @@ -43,9 +47,13 @@ import io; import os; import rt; -// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 -// for the intrusive vstream cast. -export type fixed_ctx = struct { +// stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at +// offset 0 for the intrusive stream→vstream 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 memio.state note above. +export type stream = struct { vt: io.vtable, ptr: *u8, len: i32, @@ -53,68 +61,38 @@ export type fixed_ctx = struct { pos: i32, }; -// dynamic_ctx — heap-alloc'd state for dynamic_vstream / -// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the -// difference is the vtable wired (writer=dynamicwrite_v, -// closer=dynamicclose_v) and that the buffer can grow via -// dynamicgrow_v on write overflow. -export type dynamic_ctx = struct { - vt: io.vtable, - ptr: *u8, - len: i32, - cap: i32, - pos: i32, +// fixed_vstream — 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 to mirror the OLD +// memio.fixedwrite divergence — graduating to Hare's `nomem` return +// needs the widen-from-bare-nomem path that #173 blocks). +// +// Mirrors ref/hare/memio/stream.ha:46. +export fn fixed_vstream(buf: []u8) stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&fixedwrite_v): *io.writer; + r.ptr = buf.ptr; + r.len = buf.len; + r.cap = buf.len; + r.pos = 0; + return r; }; -// fixed_vstream — wire a vstream 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 to mirror -// the OLD memio.fixedwrite divergence — graduating to Hare's -// `nomem` return needs the widen-from-bare-nomem path that #173 -// blocks, deferred to eFinal). +// dynamic_vstream — wire a stream with no initial buffer. Writes grow +// the backing allocation; [[io.st_close]] frees it. // -// Mirrors ref/hare/memio/stream.ha:47. -// -// Three steps (vs Hare's single struct literal): alloc with vt -// zero-init (struct-lit `vt = local_vt` silently drops tagged-union -// fields past the first — sibling task; the `let zero` + post-alloc -// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer -// is the proven-working route, same shape lib/bufio.init uses on -// b.vtable). Field-assign of a TY_PTR/void variant through the -// chained pointer-into-struct is fine here (*fixed_ctx is a plain -// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr -// receiver" carve-out doesn't bite). -export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { - let zero: io.vtable; - let c: *fixed_ctx = alloc(fixed_ctx{ - vt = zero, - ptr = buf.ptr, - len = buf.len, - cap = buf.len, - pos = 0, - })?; - c.vt.reader = (&fixedread_v): *io.reader; - c.vt.writer = (&fixedwrite_v): *io.writer; - return &c.vt; -}; - -// dynamic_vstream — wire a vstream with no initial buffer. Writes -// grow the backing allocation; [[io.st_close]] frees it. -// -// Mirrors ref/hare/memio/stream.ha:54. -export fn dynamic_vstream() (io.vstream | nomem) = { - let zero: io.vtable; - let c: *dynamic_ctx = alloc(dynamic_ctx{ - vt = zero, - ptr = nil, - len = 0, - cap = 0, - pos = 0, - })?; - c.vt.reader = (&dynread_v): *io.reader; - c.vt.writer = (&dynamicwrite_v): *io.writer; - c.vt.closer = (&dynamicclose_v): *io.closer; - return &c.vt; +// Mirrors ref/hare/memio/stream.ha:58. +export fn dynamic_vstream() stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&dynamicwrite_v): *io.writer; + r.vt.closer = (&dynamicclose_v): *io.closer; + r.ptr = nil; + r.len = 0; + r.cap = 0; + r.pos = 0; + return r; }; // dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an @@ -123,32 +101,27 @@ export fn dynamic_vstream() (io.vstream | nomem) = { // half-filled append slice with len < cap and using only `buf.len` // would under-free on close. // -// Mirrors ref/hare/memio/stream.ha:65. -export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { - let zero: io.vtable; - let c: *dynamic_ctx = alloc(dynamic_ctx{ - vt = zero, - ptr = buf.ptr, - len = buf.len, - cap = buf.cap, - pos = 0, - })?; - c.vt.reader = (&dynread_v): *io.reader; - c.vt.writer = (&dynamicwrite_v): *io.writer; - c.vt.closer = (&dynamicclose_v): *io.closer; - return &c.vt; +// Mirrors ref/hare/memio/stream.ha:64. +export fn dynamicfrom_vstream(buf: []u8) stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&dynamicwrite_v): *io.writer; + r.vt.closer = (&dynamicclose_v): *io.closer; + r.ptr = buf.ptr; + r.len = buf.len; + r.cap = buf.cap; + r.pos = 0; + return r; }; // ---- vtable callbacks ---------------------------------------------------- -// fixedread_v / dynread_v — recover the ctx from vstream's -// `*vtable` via the intrusive offset-0 cast. Separate fns (vs -// sharing one readfn) so each cast targets the matching ctx type -// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 -// has the same shape but only one ctx flavour. - -fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { - let m: *fixed_ctx = s: *fixed_ctx; +// read_v — recover the stream from the vstream'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 read_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *stream = s: *stream; if (m.pos >= m.len) { let e: io.eof; return e; @@ -166,7 +139,7 @@ fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { }; fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { - let m: *fixed_ctx = s: *fixed_ctx; + 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; @@ -180,26 +153,8 @@ fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { return n: size; }; -fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; - 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 dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; + let m: *stream = s: *stream; let need: i32 = m.pos + buf.len; if (need > m.cap) { dynamicgrow_v(m, need); }; let i: i32 = 0; @@ -213,7 +168,7 @@ fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { }; fn dynamicclose_v(s: io.vstream) (void | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; + let m: *stream = s: *stream; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; m.ptr = nil; m.len = 0; @@ -226,7 +181,7 @@ fn dynamicclose_v(s: io.vstream) (void | io.error) = { // 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps // the cstage flat-TU private-fn scope from colliding (memio.ww:200 // note + task #9). -fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { +fn dynamicgrow_v(d: *stream, need: i32) void = { let newcap: i32 = d.cap; if (newcap < 8) { newcap = 8; }; for (newcap < need) { newcap *= 2; }; @@ -241,35 +196,62 @@ fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { d.cap = newcap; }; -// fixed_string / dynamic_string — borrowed str view of buf[0..pos] for -// the matching ctx flavour. Project #94 fold-e7 bundles them here as -// direct enablers for fmt.vbsprintf / fmt.vasprintf (drew-approved per -// feedback_refactor_routing_same_class_drops — prereqs, not churn). -// Mirrors OLD memio.string (memio.ww:102) over `*state`; the V-side -// twins key off the intrusive *_ctx cast same shape as the -// vtable callbacks (vstream.ww:151,184). Two flavours kept distinct -// (vs one fn over a shared header) so each cast targets the matching -// ctx type — same rationale as the split fixedread_v / dynread_v at -// line 144-148. Unchecked str view per CLAUDE.md rule 9 carve-out -// (utf8.validate at IO source is opt-in, not wrapped per-construction). +// ---- accessors over the common `stream` header ----------------------- // -// Mirrors ref/hare/memio/ops.ha:51 string(s: io::handle) and the -// per-handle dispatch shape. fold-e8 (task #50 prereq) extends with -// buffer/reset/borrowedread accessors; this fold ships only the -// vbsprintf / vasprintf enablers. +// Single fn each (drew string-collapse): Hare's string/reset/buffer/ +// borrowedread all take `*stream` and read the flat header, so the +// fixed/dynamic split the OLD V-side carried (fixed_string / +// dynamic_string) collapses to one. FLIP drops the `_v` suffix +// (string_v → string, …) and deletes the OLD `*state` twins in +// memio.ww. -export fn fixed_string(vs: io.vstream) str = { - let c: *fixed_ctx = vs: *fixed_ctx; +// string_v — 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_v(s: *stream) str = { let r: str; - r.ptr = c.ptr; - r.len = c.pos; + r.ptr = s.ptr; + r.len = s.pos; return r; }; -export fn dynamic_string(vs: io.vstream) str = { - let c: *dynamic_ctx = vs: *dynamic_ctx; - let r: str; - r.ptr = c.ptr; - r.len = c.pos; +// buffer_v — borrowed []u8 view of bytes written so far (buf[0..pos]). +// +// Mirrors ref/hare/memio/stream.ha:74 buffer(in: *stream). +export fn buffer_v(s: *stream) []u8 = { + let r: []u8; + r.ptr = s.ptr; + r.len = s.pos; + return r; +}; + +// reset_v — 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_v(s: *stream) void = { + s.pos = 0; + s.len = 0; +}; + +// borrowedread_v — 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_v(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; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 147314d1..03076d80 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14482,43 +14482,47 @@ fn dynamicgrow(m: *state, need: i32) void = { }; // vstream — Hare-shaped vtable wrappers over memio. Project #94 -// fold-e2 (Option C, parallel API). +// fold-eFinal(PREP). // -// Adds three constructors that return `io.vstream` (= `*io.vtable`, -// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable -// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The -// OLD surface stays untouched here — fold-eFinal (task #50) atomically -// flips the package shape: deletes the OLD constructors + callbacks, -// renames `_vstream` suffix off, and migrates the few callers. +// Adds three constructors that return a memio `stream` BY VALUE (the +// Hare shape, ref/hare/memio/stream.ha:46,58,64) alongside the +// pre-vtable memio.fixed / memio.dynamic / memio.dynamicfrom in +// memio.ww. The OLD surface stays untouched here — fold-eFinal (FLIP) +// atomically flips the package shape: deletes the OLD constructors + +// `state` + callbacks, renames the `_vstream`/`_v` suffix off (so +// `fixed_vstream → fixed`, `stream` stays, `string_v → string`), and +// migrates the few callers (lib/fmt/vstream.ww + the 776 probe). // -// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS -// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx -// struct's first field is `vt: io.vtable` (the vtable embedded -// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is -// castable to `vstream = *vtable` via `&c.vt` — and the callbacks -// recover the outer ctx by casting the dispatch arg back to -// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over -// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger -// (log.ww:95-98). +// 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 +// `vstream = *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. // -// Cast workaround per #206: bare `&fn_name` does not type-check as -// a `(* | void)` field-init / let-binding (the structural -// `*fn(...)` value isn't accepted as the `*reader` named variant -// of the tagged slot). Explicit `(&fn_name): *io.` cast at -// each store site is the Hare-faithful minimum-touch route — same -// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable -// init. 8 cast tokens here (2 in fixed_vstream + 3 each in -// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer -// void per Hare's fixed_vt, which lets st_close's void-arm return -// plain `void` with no callback needed). Casts drop out wholesale -// once #206 closes. +// VALUE-RETURN (drew, gating): the constructor builds the struct in a +// local `let r: stream;`, field-assigns every slot (including the +// tagged vt sub-fields), and `return r;` — the proven sret round-trip +// shape pinned by test/wcc/925 ("ident_return_rhs" row) and extended +// to tagged-union-field structs by the 776 byte-id rows here. NO heap, +// NO `nomem`: the alloc that forced the OLD `(io.vstream | nomem)` +// return is gone, so the constructor cannot fail. Caller owns the +// returned `stream` (stack ownership, no-GC) and passes `&s.vt` to the +// io.st_* dispatchers — exactly Hare's `&s` into io::write. +// +// 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 — same workaround test/wcc/775_io_vtable_run.c +// uses for the bare-vtable init. // // ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note // (memio.ww:39): chained-dot writes through a state pointer into a -// slice subfield miscompile silently (related to the #195 family); -// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's -// `cap` (NOT `len`) to track the allocated-capacity-to-free on -// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73). +// slice subfield miscompile silently (#195 family); the flat shape +// sidesteps it. dynamicfrom_vstream uses the slice's `cap` (NOT `len`) +// to track the allocated-capacity-to-free on close — mirrors the OLD +// memio.dynamicfrom note (memio.ww:73). package memio; @@ -14526,9 +14530,13 @@ import io; import os; import rt; -// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 -// for the intrusive vstream cast. -export type fixed_ctx = struct { +// stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at +// offset 0 for the intrusive stream→vstream 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 memio.state note above. +export type stream = struct { vt: io.vtable, ptr: *u8, len: i32, @@ -14536,68 +14544,38 @@ export type fixed_ctx = struct { pos: i32, }; -// dynamic_ctx — heap-alloc'd state for dynamic_vstream / -// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the -// difference is the vtable wired (writer=dynamicwrite_v, -// closer=dynamicclose_v) and that the buffer can grow via -// dynamicgrow_v on write overflow. -export type dynamic_ctx = struct { - vt: io.vtable, - ptr: *u8, - len: i32, - cap: i32, - pos: i32, +// fixed_vstream — 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 to mirror the OLD +// memio.fixedwrite divergence — graduating to Hare's `nomem` return +// needs the widen-from-bare-nomem path that #173 blocks). +// +// Mirrors ref/hare/memio/stream.ha:46. +export fn fixed_vstream(buf: []u8) stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&fixedwrite_v): *io.writer; + r.ptr = buf.ptr; + r.len = buf.len; + r.cap = buf.len; + r.pos = 0; + return r; }; -// fixed_vstream — wire a vstream 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 to mirror -// the OLD memio.fixedwrite divergence — graduating to Hare's -// `nomem` return needs the widen-from-bare-nomem path that #173 -// blocks, deferred to eFinal). +// dynamic_vstream — wire a stream with no initial buffer. Writes grow +// the backing allocation; [[io.st_close]] frees it. // -// Mirrors ref/hare/memio/stream.ha:47. -// -// Three steps (vs Hare's single struct literal): alloc with vt -// zero-init (struct-lit `vt = local_vt` silently drops tagged-union -// fields past the first — sibling task; the `let zero` + post-alloc -// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer -// is the proven-working route, same shape lib/bufio.init uses on -// b.vtable). Field-assign of a TY_PTR/void variant through the -// chained pointer-into-struct is fine here (*fixed_ctx is a plain -// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr -// receiver" carve-out doesn't bite). -export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { - let zero: io.vtable; - let c: *fixed_ctx = alloc(fixed_ctx{ - vt = zero, - ptr = buf.ptr, - len = buf.len, - cap = buf.len, - pos = 0, - })?; - c.vt.reader = (&fixedread_v): *io.reader; - c.vt.writer = (&fixedwrite_v): *io.writer; - return &c.vt; -}; - -// dynamic_vstream — wire a vstream with no initial buffer. Writes -// grow the backing allocation; [[io.st_close]] frees it. -// -// Mirrors ref/hare/memio/stream.ha:54. -export fn dynamic_vstream() (io.vstream | nomem) = { - let zero: io.vtable; - let c: *dynamic_ctx = alloc(dynamic_ctx{ - vt = zero, - ptr = nil, - len = 0, - cap = 0, - pos = 0, - })?; - c.vt.reader = (&dynread_v): *io.reader; - c.vt.writer = (&dynamicwrite_v): *io.writer; - c.vt.closer = (&dynamicclose_v): *io.closer; - return &c.vt; +// Mirrors ref/hare/memio/stream.ha:58. +export fn dynamic_vstream() stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&dynamicwrite_v): *io.writer; + r.vt.closer = (&dynamicclose_v): *io.closer; + r.ptr = nil; + r.len = 0; + r.cap = 0; + r.pos = 0; + return r; }; // dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an @@ -14606,32 +14584,27 @@ export fn dynamic_vstream() (io.vstream | nomem) = { // half-filled append slice with len < cap and using only `buf.len` // would under-free on close. // -// Mirrors ref/hare/memio/stream.ha:65. -export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { - let zero: io.vtable; - let c: *dynamic_ctx = alloc(dynamic_ctx{ - vt = zero, - ptr = buf.ptr, - len = buf.len, - cap = buf.cap, - pos = 0, - })?; - c.vt.reader = (&dynread_v): *io.reader; - c.vt.writer = (&dynamicwrite_v): *io.writer; - c.vt.closer = (&dynamicclose_v): *io.closer; - return &c.vt; +// Mirrors ref/hare/memio/stream.ha:64. +export fn dynamicfrom_vstream(buf: []u8) stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&dynamicwrite_v): *io.writer; + r.vt.closer = (&dynamicclose_v): *io.closer; + r.ptr = buf.ptr; + r.len = buf.len; + r.cap = buf.cap; + r.pos = 0; + return r; }; // ---- vtable callbacks ---------------------------------------------------- -// fixedread_v / dynread_v — recover the ctx from vstream's -// `*vtable` via the intrusive offset-0 cast. Separate fns (vs -// sharing one readfn) so each cast targets the matching ctx type -// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 -// has the same shape but only one ctx flavour. - -fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { - let m: *fixed_ctx = s: *fixed_ctx; +// read_v — recover the stream from the vstream'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 read_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *stream = s: *stream; if (m.pos >= m.len) { let e: io.eof; return e; @@ -14649,7 +14622,7 @@ fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { }; fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { - let m: *fixed_ctx = s: *fixed_ctx; + 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; @@ -14663,26 +14636,8 @@ fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { return n: size; }; -fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; - 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 dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; + let m: *stream = s: *stream; let need: i32 = m.pos + buf.len; if (need > m.cap) { dynamicgrow_v(m, need); }; let i: i32 = 0; @@ -14696,7 +14651,7 @@ fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { }; fn dynamicclose_v(s: io.vstream) (void | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; + let m: *stream = s: *stream; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; m.ptr = nil; m.len = 0; @@ -14709,7 +14664,7 @@ fn dynamicclose_v(s: io.vstream) (void | io.error) = { // 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps // the cstage flat-TU private-fn scope from colliding (memio.ww:200 // note + task #9). -fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { +fn dynamicgrow_v(d: *stream, need: i32) void = { let newcap: i32 = d.cap; if (newcap < 8) { newcap = 8; }; for (newcap < need) { newcap *= 2; }; @@ -14724,36 +14679,63 @@ fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { d.cap = newcap; }; -// fixed_string / dynamic_string — borrowed str view of buf[0..pos] for -// the matching ctx flavour. Project #94 fold-e7 bundles them here as -// direct enablers for fmt.vbsprintf / fmt.vasprintf (drew-approved per -// feedback_refactor_routing_same_class_drops — prereqs, not churn). -// Mirrors OLD memio.string (memio.ww:102) over `*state`; the V-side -// twins key off the intrusive *_ctx cast same shape as the -// vtable callbacks (vstream.ww:151,184). Two flavours kept distinct -// (vs one fn over a shared header) so each cast targets the matching -// ctx type — same rationale as the split fixedread_v / dynread_v at -// line 144-148. Unchecked str view per CLAUDE.md rule 9 carve-out -// (utf8.validate at IO source is opt-in, not wrapped per-construction). +// ---- accessors over the common `stream` header ----------------------- // -// Mirrors ref/hare/memio/ops.ha:51 string(s: io::handle) and the -// per-handle dispatch shape. fold-e8 (task #50 prereq) extends with -// buffer/reset/borrowedread accessors; this fold ships only the -// vbsprintf / vasprintf enablers. +// Single fn each (drew string-collapse): Hare's string/reset/buffer/ +// borrowedread all take `*stream` and read the flat header, so the +// fixed/dynamic split the OLD V-side carried (fixed_string / +// dynamic_string) collapses to one. FLIP drops the `_v` suffix +// (string_v → string, …) and deletes the OLD `*state` twins in +// memio.ww. -export fn fixed_string(vs: io.vstream) str = { - let c: *fixed_ctx = vs: *fixed_ctx; +// string_v — 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_v(s: *stream) str = { let r: str; - r.ptr = c.ptr; - r.len = c.pos; + r.ptr = s.ptr; + r.len = s.pos; return r; }; -export fn dynamic_string(vs: io.vstream) str = { - let c: *dynamic_ctx = vs: *dynamic_ctx; - let r: str; - r.ptr = c.ptr; - r.len = c.pos; +// buffer_v — borrowed []u8 view of bytes written so far (buf[0..pos]). +// +// Mirrors ref/hare/memio/stream.ha:74 buffer(in: *stream). +export fn buffer_v(s: *stream) []u8 = { + let r: []u8; + r.ptr = s.ptr; + r.len = s.pos; + return r; +}; + +// reset_v — 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_v(s: *stream) void = { + s.pos = 0; + s.len = 0; +}; + +// borrowedread_v — 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_v(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; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0cadc597..29e00f92 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14482,43 +14482,47 @@ fn dynamicgrow(m: *state, need: i32) void = { }; // vstream — Hare-shaped vtable wrappers over memio. Project #94 -// fold-e2 (Option C, parallel API). +// fold-eFinal(PREP). // -// Adds three constructors that return `io.vstream` (= `*io.vtable`, -// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable -// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The -// OLD surface stays untouched here — fold-eFinal (task #50) atomically -// flips the package shape: deletes the OLD constructors + callbacks, -// renames `_vstream` suffix off, and migrates the few callers. +// Adds three constructors that return a memio `stream` BY VALUE (the +// Hare shape, ref/hare/memio/stream.ha:46,58,64) alongside the +// pre-vtable memio.fixed / memio.dynamic / memio.dynamicfrom in +// memio.ww. The OLD surface stays untouched here — fold-eFinal (FLIP) +// atomically flips the package shape: deletes the OLD constructors + +// `state` + callbacks, renames the `_vstream`/`_v` suffix off (so +// `fixed_vstream → fixed`, `stream` stays, `string_v → string`), and +// migrates the few callers (lib/fmt/vstream.ww + the 776 probe). // -// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS -// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx -// struct's first field is `vt: io.vtable` (the vtable embedded -// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is -// castable to `vstream = *vtable` via `&c.vt` — and the callbacks -// recover the outer ctx by casting the dispatch arg back to -// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over -// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger -// (log.ww:95-98). +// 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 +// `vstream = *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. // -// Cast workaround per #206: bare `&fn_name` does not type-check as -// a `(* | void)` field-init / let-binding (the structural -// `*fn(...)` value isn't accepted as the `*reader` named variant -// of the tagged slot). Explicit `(&fn_name): *io.` cast at -// each store site is the Hare-faithful minimum-touch route — same -// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable -// init. 8 cast tokens here (2 in fixed_vstream + 3 each in -// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer -// void per Hare's fixed_vt, which lets st_close's void-arm return -// plain `void` with no callback needed). Casts drop out wholesale -// once #206 closes. +// VALUE-RETURN (drew, gating): the constructor builds the struct in a +// local `let r: stream;`, field-assigns every slot (including the +// tagged vt sub-fields), and `return r;` — the proven sret round-trip +// shape pinned by test/wcc/925 ("ident_return_rhs" row) and extended +// to tagged-union-field structs by the 776 byte-id rows here. NO heap, +// NO `nomem`: the alloc that forced the OLD `(io.vstream | nomem)` +// return is gone, so the constructor cannot fail. Caller owns the +// returned `stream` (stack ownership, no-GC) and passes `&s.vt` to the +// io.st_* dispatchers — exactly Hare's `&s` into io::write. +// +// 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 — same workaround test/wcc/775_io_vtable_run.c +// uses for the bare-vtable init. // // ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note // (memio.ww:39): chained-dot writes through a state pointer into a -// slice subfield miscompile silently (related to the #195 family); -// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's -// `cap` (NOT `len`) to track the allocated-capacity-to-free on -// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73). +// slice subfield miscompile silently (#195 family); the flat shape +// sidesteps it. dynamicfrom_vstream uses the slice's `cap` (NOT `len`) +// to track the allocated-capacity-to-free on close — mirrors the OLD +// memio.dynamicfrom note (memio.ww:73). package memio; @@ -14526,9 +14530,13 @@ import io; import os; import rt; -// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 -// for the intrusive vstream cast. -export type fixed_ctx = struct { +// stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at +// offset 0 for the intrusive stream→vstream 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 memio.state note above. +export type stream = struct { vt: io.vtable, ptr: *u8, len: i32, @@ -14536,68 +14544,38 @@ export type fixed_ctx = struct { pos: i32, }; -// dynamic_ctx — heap-alloc'd state for dynamic_vstream / -// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the -// difference is the vtable wired (writer=dynamicwrite_v, -// closer=dynamicclose_v) and that the buffer can grow via -// dynamicgrow_v on write overflow. -export type dynamic_ctx = struct { - vt: io.vtable, - ptr: *u8, - len: i32, - cap: i32, - pos: i32, +// fixed_vstream — 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 to mirror the OLD +// memio.fixedwrite divergence — graduating to Hare's `nomem` return +// needs the widen-from-bare-nomem path that #173 blocks). +// +// Mirrors ref/hare/memio/stream.ha:46. +export fn fixed_vstream(buf: []u8) stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&fixedwrite_v): *io.writer; + r.ptr = buf.ptr; + r.len = buf.len; + r.cap = buf.len; + r.pos = 0; + return r; }; -// fixed_vstream — wire a vstream 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 to mirror -// the OLD memio.fixedwrite divergence — graduating to Hare's -// `nomem` return needs the widen-from-bare-nomem path that #173 -// blocks, deferred to eFinal). +// dynamic_vstream — wire a stream with no initial buffer. Writes grow +// the backing allocation; [[io.st_close]] frees it. // -// Mirrors ref/hare/memio/stream.ha:47. -// -// Three steps (vs Hare's single struct literal): alloc with vt -// zero-init (struct-lit `vt = local_vt` silently drops tagged-union -// fields past the first — sibling task; the `let zero` + post-alloc -// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer -// is the proven-working route, same shape lib/bufio.init uses on -// b.vtable). Field-assign of a TY_PTR/void variant through the -// chained pointer-into-struct is fine here (*fixed_ctx is a plain -// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr -// receiver" carve-out doesn't bite). -export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { - let zero: io.vtable; - let c: *fixed_ctx = alloc(fixed_ctx{ - vt = zero, - ptr = buf.ptr, - len = buf.len, - cap = buf.len, - pos = 0, - })?; - c.vt.reader = (&fixedread_v): *io.reader; - c.vt.writer = (&fixedwrite_v): *io.writer; - return &c.vt; -}; - -// dynamic_vstream — wire a vstream with no initial buffer. Writes -// grow the backing allocation; [[io.st_close]] frees it. -// -// Mirrors ref/hare/memio/stream.ha:54. -export fn dynamic_vstream() (io.vstream | nomem) = { - let zero: io.vtable; - let c: *dynamic_ctx = alloc(dynamic_ctx{ - vt = zero, - ptr = nil, - len = 0, - cap = 0, - pos = 0, - })?; - c.vt.reader = (&dynread_v): *io.reader; - c.vt.writer = (&dynamicwrite_v): *io.writer; - c.vt.closer = (&dynamicclose_v): *io.closer; - return &c.vt; +// Mirrors ref/hare/memio/stream.ha:58. +export fn dynamic_vstream() stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&dynamicwrite_v): *io.writer; + r.vt.closer = (&dynamicclose_v): *io.closer; + r.ptr = nil; + r.len = 0; + r.cap = 0; + r.pos = 0; + return r; }; // dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an @@ -14606,32 +14584,27 @@ export fn dynamic_vstream() (io.vstream | nomem) = { // half-filled append slice with len < cap and using only `buf.len` // would under-free on close. // -// Mirrors ref/hare/memio/stream.ha:65. -export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { - let zero: io.vtable; - let c: *dynamic_ctx = alloc(dynamic_ctx{ - vt = zero, - ptr = buf.ptr, - len = buf.len, - cap = buf.cap, - pos = 0, - })?; - c.vt.reader = (&dynread_v): *io.reader; - c.vt.writer = (&dynamicwrite_v): *io.writer; - c.vt.closer = (&dynamicclose_v): *io.closer; - return &c.vt; +// Mirrors ref/hare/memio/stream.ha:64. +export fn dynamicfrom_vstream(buf: []u8) stream = { + let r: stream; + r.vt.reader = (&read_v): *io.reader; + r.vt.writer = (&dynamicwrite_v): *io.writer; + r.vt.closer = (&dynamicclose_v): *io.closer; + r.ptr = buf.ptr; + r.len = buf.len; + r.cap = buf.cap; + r.pos = 0; + return r; }; // ---- vtable callbacks ---------------------------------------------------- -// fixedread_v / dynread_v — recover the ctx from vstream's -// `*vtable` via the intrusive offset-0 cast. Separate fns (vs -// sharing one readfn) so each cast targets the matching ctx type -// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 -// has the same shape but only one ctx flavour. - -fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { - let m: *fixed_ctx = s: *fixed_ctx; +// read_v — recover the stream from the vstream'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 read_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *stream = s: *stream; if (m.pos >= m.len) { let e: io.eof; return e; @@ -14649,7 +14622,7 @@ fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { }; fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { - let m: *fixed_ctx = s: *fixed_ctx; + 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; @@ -14663,26 +14636,8 @@ fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { return n: size; }; -fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; - 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 dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; + let m: *stream = s: *stream; let need: i32 = m.pos + buf.len; if (need > m.cap) { dynamicgrow_v(m, need); }; let i: i32 = 0; @@ -14696,7 +14651,7 @@ fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { }; fn dynamicclose_v(s: io.vstream) (void | io.error) = { - let m: *dynamic_ctx = s: *dynamic_ctx; + let m: *stream = s: *stream; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; m.ptr = nil; m.len = 0; @@ -14709,7 +14664,7 @@ fn dynamicclose_v(s: io.vstream) (void | io.error) = { // 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps // the cstage flat-TU private-fn scope from colliding (memio.ww:200 // note + task #9). -fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { +fn dynamicgrow_v(d: *stream, need: i32) void = { let newcap: i32 = d.cap; if (newcap < 8) { newcap = 8; }; for (newcap < need) { newcap *= 2; }; @@ -14724,36 +14679,63 @@ fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { d.cap = newcap; }; -// fixed_string / dynamic_string — borrowed str view of buf[0..pos] for -// the matching ctx flavour. Project #94 fold-e7 bundles them here as -// direct enablers for fmt.vbsprintf / fmt.vasprintf (drew-approved per -// feedback_refactor_routing_same_class_drops — prereqs, not churn). -// Mirrors OLD memio.string (memio.ww:102) over `*state`; the V-side -// twins key off the intrusive *_ctx cast same shape as the -// vtable callbacks (vstream.ww:151,184). Two flavours kept distinct -// (vs one fn over a shared header) so each cast targets the matching -// ctx type — same rationale as the split fixedread_v / dynread_v at -// line 144-148. Unchecked str view per CLAUDE.md rule 9 carve-out -// (utf8.validate at IO source is opt-in, not wrapped per-construction). +// ---- accessors over the common `stream` header ----------------------- // -// Mirrors ref/hare/memio/ops.ha:51 string(s: io::handle) and the -// per-handle dispatch shape. fold-e8 (task #50 prereq) extends with -// buffer/reset/borrowedread accessors; this fold ships only the -// vbsprintf / vasprintf enablers. +// Single fn each (drew string-collapse): Hare's string/reset/buffer/ +// borrowedread all take `*stream` and read the flat header, so the +// fixed/dynamic split the OLD V-side carried (fixed_string / +// dynamic_string) collapses to one. FLIP drops the `_v` suffix +// (string_v → string, …) and deletes the OLD `*state` twins in +// memio.ww. -export fn fixed_string(vs: io.vstream) str = { - let c: *fixed_ctx = vs: *fixed_ctx; +// string_v — 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_v(s: *stream) str = { let r: str; - r.ptr = c.ptr; - r.len = c.pos; + r.ptr = s.ptr; + r.len = s.pos; return r; }; -export fn dynamic_string(vs: io.vstream) str = { - let c: *dynamic_ctx = vs: *dynamic_ctx; - let r: str; - r.ptr = c.ptr; - r.len = c.pos; +// buffer_v — borrowed []u8 view of bytes written so far (buf[0..pos]). +// +// Mirrors ref/hare/memio/stream.ha:74 buffer(in: *stream). +export fn buffer_v(s: *stream) []u8 = { + let r: []u8; + r.ptr = s.ptr; + r.len = s.pos; + return r; +}; + +// reset_v — 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_v(s: *stream) void = { + s.pos = 0; + s.len = 0; +}; + +// borrowedread_v — 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_v(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; }; diff --git a/test/wcc/776_memio_vstream_run.c b/test/wcc/776_memio_vstream_run.c index 64e78853..6756be9e 100644 --- a/test/wcc/776_memio_vstream_run.c +++ b/test/wcc/776_memio_vstream_run.c @@ -1,13 +1,17 @@ /* - * 776_memio_vstream_run — project #94 fold-e2 sentinel. Pins the - * additive lib/memio/vstream.ww Option C parallel API: heap-alloc'd - * ctx structs (fixed_ctx / dynamic_ctx) with `vt: io.vtable` as the - * first field for the intrusive cast, three constructors - * (fixed_vstream / dynamic_vstream / dynamicfrom_vstream) that - * return `(io.vstream | nomem)`, and the matching read/write/close - * callbacks that recover the outer ctx via a vstream→*ctx pointer - * cast. Coexists with the pre-vtable memio.fixed/dynamic/dynamicfrom - * surface in memio.ww (fold-eFinal, task #50, retires the latter). + * 776_memio_vstream_run — project #94 fold-eFinal(PREP) sentinel. Pins + * the additive lib/memio/vstream.ww parallel API: a single unified + * `stream` struct with `vt: io.vtable` as the first field for the + * intrusive cast, three constructors (fixed_vstream / dynamic_vstream / + * dynamicfrom_vstream) that return the `stream` BY VALUE (sret — + * field-by-field build then `return r;`, ref/hare/memio/stream.ha:46, + * 58,64), the unified read_v / fixedwrite_v / dynamicwrite_v / + * dynamicclose_v callbacks that recover the outer stream via a + * vstream→*stream pointer cast, and the collapsed single accessors + * (string_v / buffer_v / reset_v / borrowedread_v) over the common + * header. Coexists with the pre-vtable memio.fixed/dynamic/dynamicfrom + * + `state` surface in memio.ww (fold-eFinal FLIP retires the latter + * and drops the `_vstream`/`_v` suffix wholesale). * * Each row imports memio + io, calls one or more constructors, drives * the io.st_read/st_write/st_close dispatchers, and asserts an exit @@ -19,9 +23,10 @@ * | over an 8-byte source. Asserts the * | return is `size` and equal to 5, and * | that out[0]/out[4] mirror the buffer. - * | Pins the full intrusive shape (alloc - * | fixed_ctx → return &c.vt; dispatcher - * | recovers via s: *fixed_ctx). + * | Pins the full intrusive shape (build + * | stream field-by-field → return r (sret); + * | caller takes &st.vt; dispatcher recovers + * | via s: *stream). * dynamic_write_grow | dynamic_vstream + io.st_write of 3 * | bytes. Asserts the write reports 3 * | and that the dynamicgrow_v path @@ -67,11 +72,10 @@ * fold-eFinal can graduate once #173 closes. * * - #195 (cgassign N_DOT TK_ASSIGN on aliased-ptr receiver): - * would bite if vstream.ww used chained `c.vt.reader = …` - * stores through the alloc'd ctx pointer. We sidestep via a - * local `let vt: io.vtable;` + field-assigns + struct-lit - * `alloc(fixed_ctx{vt=vt, …})`. Mirrors memio.ww:39's flat- - * slice-fields workaround. eFinal can collapse once #195 closes. + * not bitten — the value-return constructor builds the stream in + * a local `let r: stream;` and field-assigns each slot (including + * r.vt.reader) through a plain pointer-to-local, not an aliased-ptr + * receiver. The flat ptr/len/cap fields (memio.ww:39) stay flat. * * GATE POLARITY: must stay GREEN. A red here means vstream.ww * regressed, the alloc-struct-? path miscompiled, the intrusive @@ -114,12 +118,8 @@ static const struct row rows[] = { " let buf: [8]u8;\n" " buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 68u8;\n" " buf[4] = 69u8; buf[5] = 70u8; buf[6] = 71u8; buf[7] = 72u8;\n" - " let r = memio.fixed_vstream(buf[0:8]);\n" - " let s: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { s = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let st: memio.stream = memio.fixed_vstream(buf[0:8]);\n" + " let s: io.vstream = &st.vt;\n" " let out: [5]u8;\n" " let n: size = 0;\n" " let rd = io.st_read(s, out[0:5]);\n" @@ -135,12 +135,8 @@ static const struct row rows[] = { "import io;\n" "import os;\n" "export fn main() i32 = {\n" - " let r = memio.dynamic_vstream();\n" - " let s: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { s = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let st: memio.stream = memio.dynamic_vstream();\n" + " let s: io.vstream = &st.vt;\n" " let src: [3]u8;\n" " src[0] = 88u8; src[1] = 89u8; src[2] = 90u8;\n" " let wr = io.st_write(s, src[0:3]);\n" @@ -160,12 +156,8 @@ static const struct row rows[] = { "export fn main() i32 = {\n" " let seed: [4]u8;\n" " seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n" - " let r = memio.dynamicfrom_vstream(seed[0:4]);\n" - " let s: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { s = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let st: memio.stream = memio.dynamicfrom_vstream(seed[0:4]);\n" + " let s: io.vstream = &st.vt;\n" " let out: [4]u8;\n" " let rd = io.st_read(s, out[0:4]);\n" " let rn: size = 0;\n" @@ -190,18 +182,10 @@ static const struct row rows[] = { " a[0] = 10u8; a[1] = 11u8; a[2] = 12u8;\n" " let b: [5]u8;\n" " b[0] = 20u8; b[1] = 21u8; b[2] = 22u8; b[3] = 23u8; b[4] = 24u8;\n" - " let ra = memio.fixed_vstream(a[0:3]);\n" - " let rb = memio.fixed_vstream(b[0:5]);\n" - " let sa: io.vstream = nil: *io.vtable;\n" - " let sb: io.vstream = nil: *io.vtable;\n" - " match (ra) {\n" - " case let v: io.vstream => { sa = v; };\n" - " case nomem => { return 50; };\n" - " };\n" - " match (rb) {\n" - " case let v: io.vstream => { sb = v; };\n" - " case nomem => { return 51; };\n" - " };\n" + " let sta: memio.stream = memio.fixed_vstream(a[0:3]);\n" + " let stb: memio.stream = memio.fixed_vstream(b[0:5]);\n" + " let sa: io.vstream = &sta.vt;\n" + " let sb: io.vstream = &stb.vt;\n" " let outa: [3]u8;\n" " let outb: [5]u8;\n" " let rda = io.st_read(sa, outa[0:3]);\n" @@ -215,6 +199,65 @@ static const struct row rows[] = { "};\n", 45, STAGE_CS | STAGE_WW, 1 }, + /* accessors_string_buffer_reset — fold-eFinal PREP: the collapsed + * single accessors over the common `stream` header (string_v / + * buffer_v / reset_v, ref/hare/memio/stream.ha:81,74,87). Writes 3 + * bytes through the value-returned fixed stream, then asserts + * string_v (str view), buffer_v ([]u8 view), and reset_v (rewind to + * 0) all read the flat header correctly. Pins the value-return + * field integrity (st.vt + st.ptr/len/cap/pos survive sret) AND the + * accessor collapse. cs.s == ww.s byte-id. */ + { "accessors_string_buffer_reset", + "package main;\n" + "import memio;\n" + "import io;\n" + "import os;\n" + "export fn main() i32 = {\n" + " let buf: [16]u8;\n" + " let st: memio.stream = memio.fixed_vstream(buf[0:16]);\n" + " let s: io.vstream = &st.vt;\n" + " let payload: [3]u8;\n" + " payload[0] = 65u8; payload[1] = 66u8; payload[2] = 67u8;\n" + " let wr = io.st_write(s, payload[0:3]);\n" + " if (!(wr is size)) { return 90; };\n" + " let v: str = memio.string_v(&st);\n" + " if (v.len != 3 || v[0] != 65u8 || v[2] != 67u8) { return 91; };\n" + " let bv: []u8 = memio.buffer_v(&st);\n" + " if (bv.len != 3 || bv[1] != 66u8) { return 92; };\n" + " memio.reset_v(&st);\n" + " if (memio.string_v(&st).len != 0) { return 93; };\n" + " return 46;\n" + "};\n", + 46, + STAGE_CS | STAGE_WW, 1 }, + /* borrowedread_view_eof — the collapsed borrowedread_v + * (ref/hare/memio/stream.ha:94): returns an amt-byte borrowed view + * advancing the cursor, eof when fewer remain. Reads 3 of 4 seeded + * bytes (view len 3), then a 2-byte borrowedread over the remaining + * 1 byte returns io.eof. cs.s == ww.s byte-id. */ + { "borrowedread_view_eof", + "package main;\n" + "import memio;\n" + "import io;\n" + "import os;\n" + "export fn main() i32 = {\n" + " let seed: [4]u8;\n" + " seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n" + " let st: memio.stream = memio.dynamicfrom_vstream(seed[0:4]);\n" + " let br = memio.borrowedread_v(&st, 3);\n" + " match (br) {\n" + " case let b: []u8 => { if (b.len != 3 || b[0] != 1u8 || b[2] != 3u8) { return 91; }; };\n" + " case io.eof => { return 92; };\n" + " };\n" + " let br2 = memio.borrowedread_v(&st, 2);\n" + " match (br2) {\n" + " case let b: []u8 => { return 93; };\n" + " case io.eof => { return 47; };\n" + " };\n" + " return 99;\n" + "};\n", + 47, + STAGE_CS | STAGE_WW, 1 }, }; static int diff --git a/test/wcc/778_bufio_vstream_run.c b/test/wcc/778_bufio_vstream_run.c index 4b5ae7d9..a079cd4d 100644 --- a/test/wcc/778_bufio_vstream_run.c +++ b/test/wcc/778_bufio_vstream_run.c @@ -1,19 +1,27 @@ /* - * 778_bufio_vstream_run — project #94 fold-e4 sentinel. Pins the - * additive lib/bufio/vstream.ww Option C parallel API: heap-alloc'd - * bufio_ctx with `vt: io.vtable` as the first field for the intrusive - * vstream cast, the bufio_vstream constructor that returns - * `(io.vstream | nomem)` wrapping an underlying *io.stream (OLD - * API; bufio_vstream's `src` parameter type stays OLD until io - * fold-2 lands `handle = (file | int)`), and isbuffered_v as the - * fn-ptr-equality discriminator over the new `_v` callback symbols. - * Coexists with the pre-vtable bufio.init / bufio.isbuffered surface - * in bufio.ww (fold-eFinal, task #50, retires the latter once io - * fold-2 ports `handle = (file | int)`). + * 778_bufio_vstream_run — project #94 fold-eFinal(PREP) sentinel. Pins + * the lib/bufio/vstream.ww parallel API — a COLLAPSE of the OLD bufio + * surface onto the io.vtable, NOT a port of Hare's missing scanner + * features (those are #217, OUT of eFinal). Covers: + * - buffered stream: `bufio_ctx` (vt@offset0 for the intrusive cast), + * `bufio_vstream` (returns the ctx BY VALUE; sret field-by-field) + * over an underlying **io.vstream** src — every underlying read/ + * write/close goes through io.st_read/io.st_write/io.st_close (the + * vtable dispatchers), io.error forwards directly (no nomem-widen). + * Plus setflush_v / flush_v (public + internal drain) / unread_v / + * isbuffered_v (fn-ptr-equality discriminator). + * - scanner: `scanner_v` (value-return newscannerbuf_v over an + * io.vstream src) + scanbyte_v / scanbytes_v (single-byte delim) / + * scanline_v / finish_v. + * Coexists with the pre-vtable bufio.init / scanner / isbuffered surface + * in bufio.ww (fold-eFinal FLIP retires the latter + drops the `_v` + * suffix: bufio_ctx→stream, bufio_vstream→init, scanner_v→scanner, + * scanbytes_v→scanbytes, newscannerbuf_v→newscannerbuf, …). * * Each row imports os + bufio + memio + io (NOT fmt — bufio doesn't * transitively pull fmt, so #209's formattable-match bail doesn't - * propagate; both stages run on every row). + * propagate; both stages run on every row, cs.s==ww.s — bufio is NOT + * compiler-embedded, so these byte-id rows are its ONLY byte-id cover). * * row | what it pins * --------------------------+-------------------------------------- @@ -22,7 +30,8 @@ * | io.st_close. Asserts the data * | reaches the underlying memio buffer * | after close drains wbuf. Full chain: - * | alloc bufio_ctx → return &c.vt → + * | bufio_ctx built field-by-field → + * | return r (sret) → caller takes &b.vt → * | dispatcher recovers via *bufio_ctx → * | flush_v → io.write(b.src, ...). * stream_read_unread | bufio_vstream read path: io.st_read @@ -83,11 +92,10 @@ * than using `io.write(...)?`; same memio.vstream.ww + * fmt.vstream.ww shape. * - * - #207 (struct-lit multi-tagged-field copy drops past first): - * alloc-zero-chain pattern (`vt = zero` + post-alloc - * `c.vt.X = ...`) sidesteps; only 1 tagged field in bufio_ctx - * so the workaround is overkill, but stays for idiom - * consistency with memio/vstream.ww. + * - #207 (struct-lit multi-tagged-field copy drops past first) and + * #210 (slice-field struct-lit drop): not reachable from the + * value-return form — bufio_ctx is built field-by-field in a + * local then sret-returned, no `alloc(T{...})` struct-lit. * * BOOTSTRAP-EMBED CHECK: bufio is NOT embedded in any selfhost * combined.ww (grep confirmed). Adding lib/bufio/vstream.ww does @@ -136,27 +144,22 @@ static const struct row rows[] = { "import io;\n" "export fn main() i32 = {\n" " let raw: [16]u8;\n" - " let mem: memio.state;\n" - " let m: io.stream;\n" - " memio.fixed(&mem, &m, raw[0:16]);\n" + " let mst: memio.stream = memio.fixed_vstream(raw[0:16]);\n" + " let msrc: io.vstream = &mst.vt;\n" " let rbuf: [8]u8;\n" " let wbuf: [8]u8;\n" - " let r = bufio.bufio_vstream(&m, rbuf[0:8], wbuf[0:8]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let b: bufio.bufio_ctx = bufio.bufio_vstream(msrc, rbuf[0:8], wbuf[0:8]);\n" + " let vs: io.vstream = &b.vt;\n" " let payload: [5]u8;\n" " payload[0] = 104u8; payload[1] = 105u8;\n" " payload[2] = 33u8; payload[3] = 98u8; payload[4] = 121u8;\n" " let wr = io.st_write(vs, payload[0:5]);\n" " let nw: size = 0;\n" " if (wr is size) { nw = wr as size; };\n" - " if (mem.pos != 0) { return 91; };\n" + " if (mst.pos != 0) { return 91; };\n" " let cl = io.st_close(vs);\n" " if (cl is io.error) { return 92; };\n" - " if (mem.pos != 5) { return 93; };\n" + " if (mst.pos != 5) { return 93; };\n" " if (nw != 5: size) { return 94; };\n" " if (raw[0] != 104u8 || raw[4] != 121u8) { return 95; };\n" " return 46;\n" @@ -173,17 +176,12 @@ static const struct row rows[] = { " let raw: [8]u8;\n" " raw[0] = 65u8; raw[1] = 66u8; raw[2] = 67u8; raw[3] = 68u8;\n" " raw[4] = 69u8; raw[5] = 70u8; raw[6] = 71u8; raw[7] = 72u8;\n" - " let mem: memio.state;\n" - " let m: io.stream;\n" - " memio.fixed(&mem, &m, raw[0:8]);\n" + " let mst: memio.stream = memio.fixed_vstream(raw[0:8]);\n" + " let msrc: io.vstream = &mst.vt;\n" " let rbuf: [8]u8;\n" " let wbuf: [4]u8;\n" - " let r = bufio.bufio_vstream(&m, rbuf[0:8], wbuf[0:4]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let b: bufio.bufio_ctx = bufio.bufio_vstream(msrc, rbuf[0:8], wbuf[0:4]);\n" + " let vs: io.vstream = &b.vt;\n" " let out: [4]u8;\n" " let rd1 = io.st_read(vs, out[0:3]);\n" " let n1: size = 0;\n" @@ -207,24 +205,15 @@ static const struct row rows[] = { "import io;\n" "export fn main() i32 = {\n" " let raw: [8]u8;\n" - " let mem: memio.state;\n" - " let m: io.stream;\n" - " memio.fixed(&mem, &m, raw[0:8]);\n" + " let mst: memio.stream = memio.fixed_vstream(raw[0:8]);\n" + " let msrc: io.vstream = &mst.vt;\n" " let rbuf: [4]u8;\n" " let wbuf: [4]u8;\n" - " let r = bufio.bufio_vstream(&m, rbuf[0:4], wbuf[0:4]);\n" - " let vsbuf: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vsbuf = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let b: bufio.bufio_ctx = bufio.bufio_vstream(msrc, rbuf[0:4], wbuf[0:4]);\n" + " let vsbuf: io.vstream = &b.vt;\n" " let buf2: [4]u8;\n" - " let r2 = memio.fixed_vstream(buf2[0:4]);\n" - " let vsplain: io.vstream = nil: *io.vtable;\n" - " match (r2) {\n" - " case let v: io.vstream => { vsplain = v; };\n" - " case nomem => { return 51; };\n" - " };\n" + " let st2: memio.stream = memio.fixed_vstream(buf2[0:4]);\n" + " let vsplain: io.vstream = &st2.vt;\n" " if (!bufio.isbuffered_v(vsbuf)) { return 91; };\n" " if (bufio.isbuffered_v(vsplain)) { return 92; };\n" " return 48;\n" @@ -239,17 +228,12 @@ static const struct row rows[] = { "import io;\n" "export fn main() i32 = {\n" " let raw: [8]u8;\n" - " let mem: memio.state;\n" - " let m: io.stream;\n" - " memio.fixed(&mem, &m, raw[0:8]);\n" + " let mst: memio.stream = memio.fixed_vstream(raw[0:8]);\n" + " let msrc: io.vstream = &mst.vt;\n" " let rbuf: [4]u8;\n" " let wbuf: [4]u8;\n" - " let r = bufio.bufio_vstream(&m, rbuf[0:4], wbuf[0:4]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let bc: bufio.bufio_ctx = bufio.bufio_vstream(msrc, rbuf[0:4], wbuf[0:4]);\n" + " let vs: io.vstream = &bc.vt;\n" " if (!bufio.isbuffered_v(vs)) { return 91; };\n" " let raw2: u64 = vs: u64;\n" " let asold: *io.stream = raw2: *io.stream;\n" @@ -278,23 +262,15 @@ static const struct row rows[] = { " raA[0] = 10u8; raA[1] = 11u8; raA[2] = 12u8; raA[3] = 13u8;\n" " let raB: [4]u8;\n" " raB[0] = 20u8; raB[1] = 21u8; raB[2] = 22u8; raB[3] = 23u8;\n" - " let memA: memio.state;\n" - " let memB: memio.state;\n" - " let mA: io.stream;\n" - " let mB: io.stream;\n" - " memio.fixed(&memA, &mA, raA[0:4]);\n" - " memio.fixed(&memB, &mB, raB[0:4]);\n" + " let mstA: memio.stream = memio.fixed_vstream(raA[0:4]);\n" + " let mstB: memio.stream = memio.fixed_vstream(raB[0:4]);\n" " let sel: i32 = 1;\n" - " let src: *io.stream = &mA;\n" - " if (sel == 0) { src = &mB; };\n" + " let src: io.vstream = &mstA.vt;\n" + " if (sel == 0) { src = &mstB.vt; };\n" " let rbuf: [4]u8;\n" " let wbuf: [4]u8;\n" - " let r = bufio.bufio_vstream(src, rbuf[0:4], wbuf[0:4]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 50; };\n" - " };\n" + " let bc: bufio.bufio_ctx = bufio.bufio_vstream(src, rbuf[0:4], wbuf[0:4]);\n" + " let vs: io.vstream = &bc.vt;\n" " let out: [4]u8;\n" " let rd = io.st_read(vs, out[0:4]);\n" " let n: size = 0;\n" @@ -305,6 +281,158 @@ static const struct row rows[] = { "};\n", 51, STAGE_CS | STAGE_WW, 1 }, + /* scanner_lines — fold-eFinal PREP: the value-return scanner over an + * io.vstream src. newscannerbuf_v (Hare newscanner_buf) → scanline_v + * twice ("ab", "c") → trailing "def" with no newline is EOF_DISCARD + * → io.eof → finish_v. Pins the scanner reads through io.st_read + * (vtable dispatch) on the underlying memio stream. cs.s==ww.s. */ + { "scanner_lines", + "package main;\n" + "import os;\n" + "import bufio;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let raw: [8]u8;\n" + " raw[0]=97u8; raw[1]=98u8; raw[2]=10u8;\n" + " raw[3]=99u8; raw[4]=10u8;\n" + " raw[5]=100u8; raw[6]=101u8; raw[7]=102u8;\n" + " let mst: memio.stream = memio.dynamicfrom_vstream(raw[0:8]);\n" + " let vs: io.vstream = &mst.vt;\n" + " let win: [16]u8;\n" + " let sc: bufio.scanner_v = bufio.newscannerbuf_v(vs, win[0:16]);\n" + " let ok: i32 = 0;\n" + " match (bufio.scanline_v(&sc)) {\n" + " case let s: str => { if (s.len == 2 && s[0] == 97u8 && s[1] == 98u8) { ok += 1; }; };\n" + " case io.eof => { return 81; };\n" + " case let e: io.error => { return 82; };\n" + " case bufio.overflow => { return 83; };\n" + " };\n" + " match (bufio.scanline_v(&sc)) {\n" + " case let s: str => { if (s.len == 1 && s[0] == 99u8) { ok += 1; }; };\n" + " case io.eof => { return 84; };\n" + " case let e: io.error => { return 85; };\n" + " case bufio.overflow => { return 86; };\n" + " };\n" + " match (bufio.scanline_v(&sc)) {\n" + " case let s: str => { return 87; };\n" + " case io.eof => { ok += 1; };\n" + " case let e: io.error => { return 88; };\n" + " case bufio.overflow => { return 89; };\n" + " };\n" + " bufio.finish_v(&sc);\n" + " if (ok != 3) { return 90; };\n" + " return 52;\n" + "};\n", + 52, + STAGE_CS | STAGE_WW, 1 }, + /* scanner_byte_bytes — scanbyte_v pops 'A'; scanbytes_v(',') then + * tokenizes "B" and "CD" from "AB,CD,". Single-byte delim (the OLD + * scantok behaviour; multibyte is #217). cs.s==ww.s. */ + { "scanner_byte_bytes", + "package main;\n" + "import os;\n" + "import bufio;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let raw: [6]u8;\n" + " raw[0]=65u8; raw[1]=66u8; raw[2]=44u8;\n" + " raw[3]=67u8; raw[4]=68u8; raw[5]=44u8;\n" + " let mst: memio.stream = memio.dynamicfrom_vstream(raw[0:6]);\n" + " let vs: io.vstream = &mst.vt;\n" + " let win: [8]u8;\n" + " let sc: bufio.scanner_v = bufio.newscannerbuf_v(vs, win[0:8]);\n" + " let fb: u8 = 0u8;\n" + " match (bufio.scanbyte_v(&sc)) {\n" + " case let b: u8 => { fb = b; };\n" + " case io.eof => { return 81; };\n" + " case let e: io.error => { return 82; };\n" + " };\n" + " if (fb != 65u8) { return 83; };\n" + " match (bufio.scanbytes_v(&sc, 44u8)) {\n" + " case let bs: []u8 => { if (bs.len != 1 || bs[0] != 66u8) { return 84; }; };\n" + " case io.eof => { return 85; };\n" + " case let e: io.error => { return 86; };\n" + " case bufio.overflow => { return 87; };\n" + " };\n" + " match (bufio.scanbytes_v(&sc, 44u8)) {\n" + " case let bs: []u8 => { if (bs.len != 2 || bs[0] != 67u8 || bs[1] != 68u8) { return 88; }; };\n" + " case io.eof => { return 89; };\n" + " case let e: io.error => { return 90; };\n" + " case bufio.overflow => { return 91; };\n" + " };\n" + " return 53;\n" + "};\n", + 53, + STAGE_CS | STAGE_WW, 1 }, + /* stream_setflush — setflush_v swaps the auto-flush byte-set to ';'; + * writing "ab;" auto-flushes (contains ';') so the sink advances to + * 3 with no explicit flush. Pins setflush_v + the flush-scan path. + * cs.s==ww.s. */ + { "stream_setflush", + "package main;\n" + "import os;\n" + "import bufio;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let sinkbuf: [16]u8;\n" + " let sink: memio.stream = memio.fixed_vstream(sinkbuf[0:16]);\n" + " let sinkvs: io.vstream = &sink.vt;\n" + " let rb: [4]u8;\n" + " let wb: [8]u8;\n" + " let b: bufio.bufio_ctx = bufio.bufio_vstream(sinkvs, rb[0:4], wb[0:8]);\n" + " let bvs: io.vstream = &b.vt;\n" + " let semi: [1]u8;\n" + " semi[0] = 59u8;\n" + " bufio.setflush_v(&b, semi[0:1]);\n" + " let payload: [3]u8;\n" + " payload[0]=97u8; payload[1]=98u8; payload[2]=59u8;\n" + " let wr = io.st_write(bvs, payload[0:3]);\n" + " if (!(wr is size)) { return 81; };\n" + " if (sink.pos != 3) { return 82; };\n" + " if (sinkbuf[0] != 97u8 || sinkbuf[2] != 59u8) { return 83; };\n" + " return 54;\n" + "};\n", + 54, + STAGE_CS | STAGE_WW, 1 }, + /* stream_unread — read "XYZ" then unread_v("XY") pushes the two + * bytes back in front of the read buffer; the next read returns them + * first. Pins unread_v's in-place shift through *bufio_ctx. + * cs.s==ww.s. */ + { "stream_unread", + "package main;\n" + "import os;\n" + "import bufio;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let raw: [4]u8;\n" + " raw[0]=88u8; raw[1]=89u8; raw[2]=90u8;\n" + " let mst: memio.stream = memio.dynamicfrom_vstream(raw[0:3]);\n" + " let vs: io.vstream = &mst.vt;\n" + " let rbuf: [4]u8;\n" + " let wbuf: [4]u8;\n" + " let b: bufio.bufio_ctx = bufio.bufio_vstream(vs, rbuf[0:4], wbuf[0:4]);\n" + " let bvs: io.vstream = &b.vt;\n" + " let out: [4]u8;\n" + " let rd = io.st_read(bvs, out[0:3]);\n" + " let n: size = 0;\n" + " if (rd is size) { n = rd as size; };\n" + " if (n != 3: size || out[0] != 88u8 || out[2] != 90u8) { return 81; };\n" + " let push: [2]u8;\n" + " push[0]=88u8; push[1]=89u8;\n" + " bufio.unread_v(&b, push[0:2]);\n" + " let out2: [2]u8;\n" + " let rd2 = io.st_read(bvs, out2[0:2]);\n" + " let n2: size = 0;\n" + " if (rd2 is size) { n2 = rd2 as size; };\n" + " if (n2 != 2: size || out2[0] != 88u8 || out2[1] != 89u8) { return 82; };\n" + " return 55;\n" + "};\n", + 55, + STAGE_CS | STAGE_WW, 1 }, }; static int diff --git a/test/wcc/779_log_vstream_run.c b/test/wcc/779_log_vstream_run.c index bd37d0c1..b934f669 100644 --- a/test/wcc/779_log_vstream_run.c +++ b/test/wcc/779_log_vstream_run.c @@ -151,14 +151,9 @@ static const struct row rows[] = { "import io;\n" "export fn main() i32 = {\n" " let buf: [16]u8;\n" - " let r = memio.fixed_vstream(buf[0:16]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 50; };\n" - " };\n" - " let sl: log.vstdlogger;\n" - " log.new_v(&sl, vs);\n" + " let st: memio.stream = memio.fixed_vstream(buf[0:16]);\n" + " let vs: io.vstream = &st.vt;\n" + " let sl: log.vstdlogger = log.new_v(vs);\n" " log.lprintln_v(&sl.logger, \"hi\", 7i64);\n" " if (buf[0] != 104u8) { return 91; };\n" " if (buf[1] != 105u8) { return 92; };\n" @@ -178,22 +173,12 @@ static const struct row rows[] = { "export fn main() i32 = {\n" " let bufA: [16]u8;\n" " let bufB: [16]u8;\n" - " let rA = memio.fixed_vstream(bufA[0:16]);\n" - " let vsA: io.vstream = nil: *io.vtable;\n" - " match (rA) {\n" - " case let v: io.vstream => { vsA = v; };\n" - " case nomem => { return 50; };\n" - " };\n" - " let rB = memio.fixed_vstream(bufB[0:16]);\n" - " let vsB: io.vstream = nil: *io.vtable;\n" - " match (rB) {\n" - " case let v: io.vstream => { vsB = v; };\n" - " case nomem => { return 51; };\n" - " };\n" - " let slA: log.vstdlogger;\n" - " let slB: log.vstdlogger;\n" - " log.new_v(&slA, vsA);\n" - " log.new_v(&slB, vsB);\n" + " let stA: memio.stream = memio.fixed_vstream(bufA[0:16]);\n" + " let vsA: io.vstream = &stA.vt;\n" + " let stB: memio.stream = memio.fixed_vstream(bufB[0:16]);\n" + " let vsB: io.vstream = &stB.vt;\n" + " let slA: log.vstdlogger = log.new_v(vsA);\n" + " let slB: log.vstdlogger = log.new_v(vsB);\n" " let sel: i32 = 1;\n" " let chosen: *log.vlogger = &slA.logger;\n" " if (sel == 0) { chosen = &slB.logger; };\n" diff --git a/test/wcc/781_fmt_vstream_compositions_run.c b/test/wcc/781_fmt_vstream_compositions_run.c index 45e4bd0c..3c014d73 100644 --- a/test/wcc/781_fmt_vstream_compositions_run.c +++ b/test/wcc/781_fmt_vstream_compositions_run.c @@ -190,12 +190,8 @@ static const struct row rows[] = { "import memio;\n" "export fn main() i32 = {\n" " let buf: [16]u8;\n" - " let r: (io.vstream | nomem) = memio.fixed_vstream(buf[0:16]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 90; };\n" - " };\n" + " let st: memio.stream = memio.fixed_vstream(buf[0:16]);\n" + " let vs: io.vstream = &st.vt;\n" " let wr = fmt.vfprintln(vs, 1i64, \"hi\", true);\n" " let nw: size = 0;\n" " match (wr) {\n" @@ -203,7 +199,7 @@ static const struct row rows[] = { " case io.error => { return 91; };\n" " };\n" " if (nw != 10: size) { return 92; };\n" - " let view: str = memio.fixed_string(vs);\n" + " let view: str = memio.string_v(&st);\n" " if (view.len != 10) { return 93; };\n" " if (view[0] != 49u8) { return 94; };\n" " if (view[1] != 32u8) { return 95; };\n" @@ -221,12 +217,8 @@ static const struct row rows[] = { "import memio;\n" "export fn main() i32 = {\n" " let buf: [16]u8;\n" - " let r: (io.vstream | nomem) = memio.fixed_vstream(buf[0:16]);\n" - " let vs: io.vstream = nil: *io.vtable;\n" - " match (r) {\n" - " case let v: io.vstream => { vs = v; };\n" - " case nomem => { return 90; };\n" - " };\n" + " let st: memio.stream = memio.fixed_vstream(buf[0:16]);\n" + " let vs: io.vstream = &st.vt;\n" " let wr = fmt.vfprintfln(vs, \"x={}\", 7i64);\n" " let nw: size = 0;\n" " match (wr) {\n" @@ -234,7 +226,7 @@ static const struct row rows[] = { " case io.error => { return 91; };\n" " };\n" " if (nw != 4: size) { return 92; };\n" - " let view: str = memio.fixed_string(vs);\n" + " let view: str = memio.string_v(&st);\n" " if (view.len != 4) { return 93; };\n" " if (view[0] != 120u8 || view[1] != 61u8) { return 94; };\n" " if (view[2] != 55u8 || view[3] != 10u8) { return 95; };\n"