From e10e95321f1daa7f14c17c410b5c4deaa6aae7b5 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 00:19:06 +0900 Subject: [PATCH] lib+test: bufio bstream writer half (closes #18) bstream wraps *io.stream with caller-supplied rbuf/wbuf; init, flush, setflush, unread, isbuffered, bread, bwrite, bclose mirror ref/hare/bufio/stream.ha modulo the bstream-vs-stream rename (cross-module type collision, see task #21). Default flush byte-set is "\n" (Hare flag::NONE default at stream.ha:75). Drops the prior ww-only FLUSH_ON_WRITE flag for Hare's flush []u8 + setflush byte-set. bufiotest.ww adds 10 @test fns: small-write, auto-flush, line-flush default, manual-flush, isbuffered, unread (post-read and pre-read budgets), scanner-over-bstream unread, flush-empty no-op, bclose flushes, setflush custom byte. --- lib/bufio/bufio.ww | 286 +++++++++++++++++++++++++++-- lib/bufio/bufiotest.ww | 403 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 659 insertions(+), 30 deletions(-) diff --git a/lib/bufio/bufio.ww b/lib/bufio/bufio.ww index 3a26027c..f11ae0b9 100644 --- a/lib/bufio/bufio.ww +++ b/lib/bufio/bufio.ww @@ -1,5 +1,5 @@ -// bufio — buffered scanner over [[io.stream]]. Subset of Hare's -// bufio:: scanner shape (ref/hare/bufio/scanner.ha). +// bufio — buffered I/O over [[io.stream]]. Subset of Hare's bufio:: +// surface (ref/hare/bufio/{scanner,stream}.ha). // // Surface today: // @@ -10,23 +10,49 @@ // ([]u8 | io.eof | io.closed | overflow) // bufio.scanline (s: *scanner) (str | io.eof | io.closed | overflow) // +// bufio.init (b: *bstream, src: *io.stream, rbuf: []u8, wbuf: []u8) void +// bufio.flush (b: *bstream) (void | io.closed) +// bufio.unread (b: *bstream, buf: []u8) void +// bufio.isbuffered (s: *io.stream) bool +// // Divergence from Hare: // -// • Hare returns the scanner by value; ww cgen can't return -// structs wider than 16B by value yet, so `newscanner` is an -// out-parameter (`*scanner`). Same workaround memio.fixed uses. +// • Hare's `bufio::stream` (ref/hare/bufio/stream.ha:27) becomes +// `bufio.bstream` here. Two modules can't both `export type +// stream` under ww's flat-scope module concat — `io.stream` +// is already taken. Filed as a compiler followup +// (cross-module type collision, sibling of the `mod.mod` +// use_alias fix); graduate to `bufio.stream` once that lands. +// Same precedent as `memio.state` (not `stream`). // -// • Hare embeds an io::stream as the scanner's first field so the -// scanner doubles as a reader for higher layers. ww cgen -// currently miscompiles chained dotted reads through nested -// value-struct fields (`o.i.a` lowers to an undefined symbol -// `a`); the embedded-stream slot would hit that path on every -// callback. Drop the embed until the cgen fix lands; the -// scanner is consumed via scanbyte / scantok / scanline today. +// • Hare returns the scanner / stream by value; ww cgen can't +// return structs wider than 16B by value yet, so `newscanner` +// and `init` are out-parameter shaped. Same workaround +// memio.fixed uses. // -// • The buffer is held flat as `(ptr, cap)` rather than a `[]u8` -// field — chained dot through a slice field (`s.buf.len`) is -// still broken. Mirrors lib/memio's flat-field workaround. +// • The scanner's read buffer is held flat as `(ptr, cap)` rather +// than a `[]u8` field; the scanner predates struct-held slice +// support and is wired through scanbyte/scantok/scanline. The +// new bstream writer half uses slice-typed `rbuf` / `wbuf` +// fields directly — chained dot through a struct slice field +// works after task #6. +// +// • Hare's `bufio::stream` discriminates r-only / w-only / r+w +// via three different vtable singletons (vtable_r, vtable_w, +// vtable_rw); ww's [[io.stream]] vtable always carries all +// three callbacks, so bstream always installs `bread` / +// `bwrite` / `bclose`. A zero-length rbuf or wbuf makes the +// matching callback degenerate (rbuf=[]: bread short-circuits +// to io.eof; wbuf=[]: bwrite passes through to src). +// +// • Hare's `init` also takes a `flag` argument carrying +// MANAGED_HANDLE / MANAGED_RDBUF / MANAGED_WRBUF ownership +// bits; bstream never owns its buffers or src, so the +// `flags` field and the init-time argument are dropped +// wholesale. The Hare `flush: []u8` byte-set is preserved — +// [[init]] seeds it to `['\n']` (line buffering, matching +// Hare's `flag::NONE` default at stream.ha:75) and +// [[setflush]] swaps it. // // • Hare's `newscanner` allocates and grows the buffer up to // `maxread`; we ship only the caller-supplied shape (Hare's @@ -38,10 +64,12 @@ // on the call that would have read them. Hare's EOF_GREEDY mode // isn't shipped (no caller needs it yet). // -// Owning model: caller owns the scanner state, the byte buffer, and -// the source stream. `finish` doesn't free the buffer and doesn't -// close src; it's a no-op today but stays on the surface so callers -// don't churn when bufio grows internal allocations. +// Owning model: caller owns the scanner state, the bstream state, +// the byte buffers, and the source stream. `finish` doesn't free +// the buffer and doesn't close src; it's a no-op today but stays +// on the surface so callers don't churn when bufio grows internal +// allocations. The bstream vtable `close` callback flushes pending +// writes and forwards close to src; it does not free rbuf / wbuf. // // let mem: memio.state; // let m: io.stream; @@ -51,9 +79,28 @@ // bufio.newscanner(&sc, &m, buf[0:128]); // match (bufio.scanline(&sc)) { ... }; // bufio.finish(&sc); +// +// let b: bufio.bstream; +// let rbuf: [256]u8; +// let wbuf: [128]u8; +// bufio.init(&b, src, rbuf[0:256], wbuf[0:128]); +// let p: *io.stream = &b.vtable; // first-field embed +// io.write(p, msg); +// bufio.flush(&b); use io; +// flushdefault — backing storage for the default flush byte-set +// ("\n"). Hare scopes it inside `init` as `static let +// flush_default = ['\n': u8]` (ref/hare/bufio/stream.ha:75); ww +// has no function-scope statics, so it lives at module scope. +let flushdefault: [1]u8 = [10u8]; + +// rt_abort — terminate on a precondition violation. Used by +// [[unread]] for the "buf fits in front of rbuf" assertion that +// Hare expresses with `assert`. +@symbol("rt_abort") fn rtabort(msg: str) void; + // overflow — the scanner buffer filled before the delimiter (or // underlying EOF) was hit. With a caller-supplied buffer we can't // grow; bumping the budget is on the caller. Mirrors Hare bufio's @@ -195,3 +242,204 @@ export fn scanline(s: *scanner) (str | io.eof | io.closed | overflow) = { case overflow => { let e: overflow; return e; }; }; }; + +// bstream — buffered read+write over an underlying *io.stream. +// +// `vtable` is the first field so a `*bstream` is castable to an +// `*io.stream` via address-of-field (`&b.vtable`). Higher layers +// (fmt.fprint, scanner, …) only see the io.stream view; bufio +// recovers the outer bstream by casting the dispatch arg back to +// `*bstream` inside the bread/bwrite/bclose callbacks. +// +// rbuf[rstart..rend] is pending read data; wbuf[0..wend] is pending +// write data. rbuf or wbuf may be zero-length: read-empty makes +// bread return io.eof immediately, write-empty makes bwrite a +// pass-through to src (uses src.write directly, no buffering). +export type bstream = struct { + vtable: io.stream, + src: *io.stream, + rbuf: []u8, + rstart: i32, + rend: i32, + wbuf: []u8, + wend: i32, + flush: []u8, +}; + +// init — wire `b` over `src` with caller-supplied buffers. Both +// rbuf and wbuf may be empty slices; the corresponding direction +// degenerates (see [[bstream]]). The flush byte-set defaults to +// "\n" (line-buffered writes); [[setflush]] swaps it. +export fn init(b: *bstream, src: *io.stream, rbuf: []u8, wbuf: []u8) void = { + b.vtable.ctx = b: *void; + b.vtable.read = bread; + b.vtable.write = bwrite; + b.vtable.close = bclose; + b.src = src; + b.rbuf = rbuf; + // rstart=rbuf.len, rend=rbuf.len: pre-read unread budget = rbuf.len + // (Hare bufio/stream.ha:101). + b.rstart = rbuf.len; + b.rend = rbuf.len; + b.wbuf = wbuf; + b.wend = 0; + b.flush = flushdefault[0:1]; +}; + +// setflush — 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 Hare's `setflush` +// (ref/hare/bufio/stream.ha:128). +export fn setflush(b: *bstream, bs: []u8) void = { + b.flush = bs; +}; + +// flush — drain any pending wbuf data to src. Hare returns +// `(void | io::error)`; ww's io.stream write-side error channel +// is `io.closed` alone, so the return shape narrows. +export fn flush(b: *bstream) (void | io.closed) = { + if (b.wend == 0) { return; }; + let off: i32 = 0; + for (off < b.wend) { + let r: (i32 | io.closed) = io.write(b.src, b.wbuf[off:b.wend]); + match (r) { + case let n: i32 => { + if (n == 0) { + // Underlying stream made no progress. Treat as + // closed rather than spin; matches what + // io.writeall would do in Hare. + let e: io.closed; return e; + }; + off += n; + }; + case io.closed => { let e: io.closed; return e; }; + }; + }; + b.wend = 0; + return; +}; + +// unread — push `buf` back into the read buffer so the next bread +// 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 rt_abort. Mirrors Hare's `stream_unread` +// (ref/hare/bufio/stream.ha:164). +export fn unread(b: *bstream, buf: []u8) void = { + if (b.rstart < buf.len) { + rtabort("bufio.unread: 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 — true when `s` is a [[bufio.bstream]]'s embedded +// vtable. Hare's discriminator is callback identity +// (ref/hare/bufio/stream.ha:179); we match the shape directly. The +// read or write callback being bread / bwrite is sufficient (close +// alone isn't unique to bufio). +export fn isbuffered(s: *io.stream) bool = { + if (s.read == bread) { return true; }; + if (s.write == bwrite) { return true; }; + return false; +}; + +// ---- vtable callbacks ------------------------------------------------ + +fn bread(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = { + let b: *bstream = s.ctx: *bstream; + // No read buffer configured: surface eof immediately rather + // than punch through to src (callers asked for a write-only + // stream, reads against it are a misuse). + if (b.rbuf.len == 0) { let e: io.eof; return e; }; + // Empty pending region: refill from src. Reset rstart so + // unread has the full buffer to push back into. + if (b.rstart >= b.rend) { + b.rstart = 0; + b.rend = 0; + let r: (i32 | io.eof | io.closed) = io.read(b.src, b.rbuf); + match (r) { + case let n: i32 => { b.rend = n; }; + case io.eof => { let e: io.eof; return e; }; + case io.closed => { let e: io.closed; return e; }; + }; + }; + let avail: i32 = b.rend - b.rstart; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = b.rbuf[b.rstart + i]; + i += 1; + }; + b.rstart += n; + return n; +}; + +fn bwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { + let b: *bstream = s.ctx: *bstream; + // No write buffer configured: pass through to src directly. + if (b.wbuf.len == 0) { return io.write(b.src, buf); }; + // Scan the write payload for any byte present in b.flush — a + // hit triggers a post-copy flush. Hare uses a labeled break to + // exit both loops on first hit (stream.ha:236-246); ww has no + // labeled break, so we bump both indices past their bounds. + let doflush: bool = false; + if (b.flush.len != 0) { + let i: i32 = 0; + for (i < buf.len) { + let j: i32 = 0; + for (j < b.flush.len) { + if (buf[i] == b.flush[j]) { + doflush = true; + i = buf.len; + j = b.flush.len; + }; + j += 1; + }; + i += 1; + }; + }; + let z: i32 = 0; + for (z < buf.len) { + let avail: i32 = b.wbuf.len - b.wend; + if (avail == 0) { + let r: (void | io.closed) = flush(b); + match (r) { + case void => { }; + case io.closed => { let e: io.closed; return e; }; + }; + avail = b.wbuf.len; + }; + let n: i32 = buf.len - z; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + b.wbuf[b.wend + i] = buf[z + i]; + i += 1; + }; + b.wend += n; + z += n; + }; + if (doflush) { + let r: (void | io.closed) = flush(b); + match (r) { + case void => { }; + case io.closed => { let e: io.closed; return e; }; + }; + }; + return buf.len; +}; + +fn bclose(s: *io.stream) (void | io.closed) = { + let b: *bstream = s.ctx: *bstream; + let r: (void | io.closed) = flush(b); + match (r) { + case void => { }; + case io.closed => { let e: io.closed; return e; }; + }; + return io.close(b.src); +}; diff --git a/lib/bufio/bufiotest.ww b/lib/bufio/bufiotest.ww index 25116abb..1fe9efe8 100644 --- a/lib/bufio/bufiotest.ww +++ b/lib/bufio/bufiotest.ww @@ -1,9 +1,11 @@ // bufiotest — exercises lib/bufio. Run with `out/bin/ww run lib/bufio/bufiotest.ww`. // -// Each @test enumerates parallel `[N]T` arrays of inputs and -// expectations, then iterates one body across them. Parallel arrays +// The scanner @tests enumerate parallel `[N]T` arrays of inputs and +// expectations, then iterate one body across them. Parallel arrays // (rather than `[N]struct{...}`) match the same cgen workaround -// memiotest leans on. +// memiotest leans on. The bstream @tests are one scenario per fn +// (the ww-stdlib idiom): the "table" is the fn list in main, not +// a row array. use bufio; use bytes; @@ -400,14 +402,393 @@ fn closedstream(s: *io.stream) void = { bufio.finish(&sc); }; +// ---- bstream init + flush round-trip --------------------------------- + +@test fn bstreamsmallwrite() void = { + let raw: [16]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:16]); + + let b: bufio.bstream; + let rb: [8]u8; + let wb: [8]u8; + bufio.init(&b, &m, rb[0:8], wb[0:8]); + + // Write under the buffer limit — nothing reaches src. + let buf: [5]u8; + let z: i32 = putstr("hello", buf[0:5], 0); + let p: *io.stream = &b.vtable; + let r: (i32 | io.closed) = io.write(p, buf[0:5]); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (mem.pos != 0) { fail(); }; + + let fr: (void | io.closed) = bufio.flush(&b); + match (fr) { + case void => { }; + case io.closed => fail(); + }; + if (mem.pos != 5) { fail(); }; + if (raw[0] != 104u8) { fail(); }; + if (raw[4] != 111u8) { fail(); }; +}; + +// ---- write larger than wbuf → auto-flush ----------------------------- + +@test fn bstreamautoflush() void = { + let raw: [32]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:32]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [4]u8; + bufio.init(&b, &m, rb[0:4], wb[0:4]); + + // 10B payload through a 4B wbuf: bwrite must flush twice mid-write, + // leaving the final 2B pending until an explicit flush. + let buf: [10]u8; + let z: i32 = putstr("abcdefghij", buf[0:10], 0); + let p: *io.stream = &b.vtable; + let r: (i32 | io.closed) = io.write(p, buf[0:10]); + match (r) { + case let n: i32 => { if (n != 10) { fail(); }; }; + case io.closed => fail(); + }; + // Two full flushes happened (8B); 2B still pending. + if (mem.pos != 8) { fail(); }; + + let fr: (void | io.closed) = bufio.flush(&b); + match (fr) { + case void => { }; + case io.closed => fail(); + }; + if (mem.pos != 10) { fail(); }; + if (raw[0] != 97u8) { fail(); }; // 'a' + if (raw[9] != 106u8) { fail(); }; // 'j' +}; + +// ---- default flush byte-set: '\n' in payload triggers flush --------- + +// init seeds b.flush to ['\n']; bwrite must flush after copying when +// the payload contains any byte from b.flush. No setflush call — +// this exercises the default. Mirrors Hare's flag::NONE default +// (ref/hare/bufio/stream.ha:75 + 100). +@test fn bstreamlineflush() void = { + let raw: [32]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:32]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [16]u8; + bufio.init(&b, &m, rb[0:4], wb[0:16]); + + let p: *io.stream = &b.vtable; + + // First write: no '\n', stays buffered. + let h: [5]u8; + let zh: i32 = putstr("hello", h[0:5], 0); + let r1: (i32 | io.closed) = io.write(p, h[0:5]); + match (r1) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (mem.pos != 0) { fail(); }; + + // Second write: '\n' present, triggers flush of everything (6B). + let nl: [1]u8; + nl[0] = 10u8; + let r2: (i32 | io.closed) = io.write(p, nl[0:1]); + match (r2) { + case let n: i32 => { if (n != 1) { fail(); }; }; + case io.closed => fail(); + }; + if (mem.pos != 6) { fail(); }; + if (raw[0] != 104u8) { fail(); }; + if (raw[5] != 10u8) { fail(); }; +}; + +// ---- explicit-flush-per-write: caller drives the drain --------------- + +// The previous ww-only FLUSH_ON_WRITE flag is gone; the Hare way +// to drain after every write is a manual flush call. Setflush to +// an empty byte-set first so the default '\n' detector doesn't +// shadow what we're testing. +@test fn bstreamflushonwrite() void = { + let raw: [32]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:32]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [16]u8; + bufio.init(&b, &m, rb[0:4], wb[0:16]); + let nilbs: [1]u8; + bufio.setflush(&b, nilbs[0:0]); + + let p: *io.stream = &b.vtable; + let h: [2]u8; + h[0] = 65u8; h[1] = 66u8; // "AB" + let r1: (i32 | io.closed) = io.write(p, h[0:2]); + match (r1) { + case let n: i32 => { if (n != 2) { fail(); }; }; + case io.closed => fail(); + }; + let f1: (void | io.closed) = bufio.flush(&b); + match (f1) { + case void => { }; + case io.closed => fail(); + }; + if (mem.pos != 2) { fail(); }; + if (raw[0] != 65u8) { fail(); }; + + let r2: (i32 | io.closed) = io.write(p, h[0:1]); + match (r2) { + case let n: i32 => { if (n != 1) { fail(); }; }; + case io.closed => fail(); + }; + let f2: (void | io.closed) = bufio.flush(&b); + match (f2) { + case void => { }; + case io.closed => fail(); + }; + if (mem.pos != 3) { fail(); }; + if (raw[2] != 65u8) { fail(); }; +}; + +// ---- isbuffered: bstream → true, plain memio → false ----------------- + +@test fn bstreamisbuffered() void = { + let raw: [8]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:8]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [4]u8; + bufio.init(&b, &m, rb[0:4], wb[0:4]); + + if (!bufio.isbuffered(&b.vtable)) { fail(); }; + if (bufio.isbuffered(&m)) { fail(); }; +}; + +// ---- bread + unread: pushed-back bytes come out first ---------------- + +@test fn bstreamunread() void = { + let raw: [8]u8; + let n: i32 = putstr("ABCDEFGH", raw[0:8], 0); + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:n]); + + let b: bufio.bstream; + let rb: [8]u8; + let wb: [4]u8; + bufio.init(&b, &m, rb[0:8], wb[0:4]); + + let p: *io.stream = &b.vtable; + + // Pull 3 bytes through bread — fills rbuf from src (8B), serves + // 3 of them; rstart=3 after. + let out: [4]u8; + let r1: (i32 | io.eof | io.closed) = io.read(p, out[0:3]); + match (r1) { + case let z: i32 => { if (z != 3) { fail(); }; }; + case io.eof => fail(); + case io.closed => fail(); + }; + if (out[0] != 65u8) { fail(); }; // 'A' + if (out[2] != 67u8) { fail(); }; // 'C' + + // Push back 2 bytes; next read returns them first. + let push: [2]u8; + push[0] = 88u8; push[1] = 89u8; // "XY" + bufio.unread(&b, push[0:2]); + + let r2: (i32 | io.eof | io.closed) = io.read(p, out[0:2]); + match (r2) { + case let z: i32 => { if (z != 2) { fail(); }; }; + case io.eof => fail(); + case io.closed => fail(); + }; + if (out[0] != 88u8) { fail(); }; // 'X' + if (out[1] != 89u8) { fail(); }; // 'Y' + + // Subsequent read returns the original tail. + let r3: (i32 | io.eof | io.closed) = io.read(p, out[0:4]); + match (r3) { + case let z: i32 => { if (z != 4) { fail(); }; }; + case io.eof => fail(); + case io.closed => fail(); + }; + if (out[0] != 68u8) { fail(); }; // 'D' + if (out[3] != 71u8) { fail(); }; // 'G' +}; + +// ---- scanner over a bstream-wrapped src: pre-read unread + scanline -- + +// Push three bytes into a freshly-init'd bstream (rstart=rend=rbuf.len +// post-init, per Hare bufio/stream.ha:101 — the unread budget before +// the first read is the full rbuf), then scan a line through the +// bstream. The scanner pulls "XYZ" from rbuf first, refills "hello\n" +// from src, and scanline returns "XYZhello". +@test fn bstreamscannerunread() void = { + let raw: [16]u8; + let n: i32 = putstr("hello\n", raw[0:16], 0); + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:n]); + + let b: bufio.bstream; + let rb: [16]u8; + let wb: [4]u8; + bufio.init(&b, &m, rb[0:16], wb[0:4]); + + let push: [3]u8; + push[0] = 88u8; push[1] = 89u8; push[2] = 90u8; // "XYZ" + bufio.unread(&b, push[0:3]); + + let p: *io.stream = &b.vtable; + let sbuf: [32]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, p, sbuf[0:32]); + + let lr: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc); + match (lr) { + case let v: str => { + if (v.len != 8) { fail(); }; // "XYZhello" + if (v[0] != 88u8) { fail(); }; // 'X' + if (v[7] != 111u8) { fail(); }; // 'o' + }; + case io.eof => fail(); + case io.closed => fail(); + case bufio.overflow => fail(); + }; + bufio.finish(&sc); +}; + +// ---- flush() on empty wbuf is a no-op -------------------------------- + +@test fn bstreamflushempty() void = { + let raw: [8]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:8]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [4]u8; + bufio.init(&b, &m, rb[0:4], wb[0:4]); + + let r: (void | io.closed) = bufio.flush(&b); + match (r) { + case void => { }; + case io.closed => fail(); + }; + if (mem.pos != 0) { fail(); }; +}; + +// ---- bclose drains pending wbuf then forwards close to src ---------- + +// io.close on the bstream vtable must run the flush path (Hare +// stream.ha:188-202). Buffer two bytes, route io.close through the +// bstream, and confirm both reach src. +@test fn bstreamcloseflushes() void = { + let raw: [8]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:8]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [4]u8; + bufio.init(&b, &m, rb[0:4], wb[0:4]); + let nilbs: [1]u8; + bufio.setflush(&b, nilbs[0:0]); // suppress the default '\n' detector + + let p: *io.stream = &b.vtable; + let h: [2]u8; + h[0] = 80u8; h[1] = 81u8; // "PQ" + let r: (i32 | io.closed) = io.write(p, h[0:2]); + match (r) { + case let n: i32 => { if (n != 2) { fail(); }; }; + case io.closed => fail(); + }; + if (mem.pos != 0) { fail(); }; // still buffered + + let cr: (void | io.closed) = io.close(p); + match (cr) { + case void => { }; + case io.closed => fail(); + }; + if (mem.pos != 2) { fail(); }; + if (raw[0] != 80u8) { fail(); }; + if (raw[1] != 81u8) { fail(); }; +}; + +// ---- setflush with a custom non-empty byte-set ----------------------- + +// setflush installs a fresh byte-set (not just clears it). Use ' ' +// (space) as the trigger; "ab cd" must flush at the space, leaving +// "cd" pending. +@test fn bstreamsetflushcustom() void = { + let raw: [16]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:16]); + + let b: bufio.bstream; + let rb: [4]u8; + let wb: [16]u8; + bufio.init(&b, &m, rb[0:4], wb[0:16]); + let bs: [1]u8; + bs[0] = 32u8; // ' ' + bufio.setflush(&b, bs[0:1]); + + let p: *io.stream = &b.vtable; + let buf: [5]u8; + let z: i32 = putstr("ab cd", buf[0:5], 0); + let r: (i32 | io.closed) = io.write(p, buf[0:5]); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + // Space at index 2 triggers post-copy flush of all 5 bytes; the + // scan-then-copy-then-flush ordering writes everything, so mem.pos + // hits 5 in one shot. + if (mem.pos != 5) { fail(); }; + if (raw[2] != 32u8) { fail(); }; + if (raw[4] != 100u8) { fail(); }; // 'd' +}; + export fn main() i32 = { - signalled = 1; scanbytecases(); - signalled = 2; scanlinecases(); - signalled = 3; scanlineoverflow(); - signalled = 4; scantokcases(); - signalled = 5; emptystream(); - signalled = 6; closedsource(); - signalled = 7; boundarycases(); - signalled = 8; multifill(); + signalled = 1; scanbytecases(); + signalled = 2; scanlinecases(); + signalled = 3; scanlineoverflow(); + signalled = 4; scantokcases(); + signalled = 5; emptystream(); + signalled = 6; closedsource(); + signalled = 7; boundarycases(); + signalled = 8; multifill(); + signalled = 9; bstreamsmallwrite(); + signalled = 10; bstreamautoflush(); + signalled = 11; bstreamlineflush(); + signalled = 12; bstreamflushonwrite(); + signalled = 13; bstreamisbuffered(); + signalled = 14; bstreamunread(); + signalled = 15; bstreamscannerunread(); + signalled = 16; bstreamflushempty(); + signalled = 17; bstreamcloseflushes(); + signalled = 18; bstreamsetflushcustom(); return 0; };