diff --git a/lib/bufio/bufio.ww b/lib/bufio/bufio.ww index e3066daa..a0e4c9e1 100644 --- a/lib/bufio/bufio.ww +++ b/lib/bufio/bufio.ww @@ -192,25 +192,39 @@ export fn isbuffered(s: io.stream) bool = { // ---- buffered-stream vtable callbacks ------------------------------------ -// bread — buffered read. Recover stream via the intrusive cast; refill -// rbuf from src via io.read on empty pending region. Mirrors -// ref/hare/bufio/stream.ha (stream_read). +// bread — buffered read. Recover stream via the intrusive cast; top up +// rbuf from src via io.read whenever the pending region holds fewer +// bytes than requested AND rbuf has spare capacity (Hare's short-read +// refill, ref/hare/bufio/stream.ha:209). The pending bytes shift to the +// front, then io.read fills the tail; EOF is fatal only when nothing is +// pending. Mirrors ref/hare/bufio/stream.ha:205 (stream_read). fn bread(s: io.stream, buf: []u8) (size | io.eof | io.error) = { let b: *stream = s: *stream; + // Degenerate read-disabled mode (zero-length rbuf, bufio.ww:87-89); + // ww's contract returns eof where Hare (always given a buffer) would + // serve 0 — a separate concern from the top-up below. if (b.rbuf.len == 0) { let e: io.eof; return e; }; - if (b.rstart >= b.rend) { + let avail: i32 = b.rend - b.rstart; + if (avail < buf.len && avail < b.rbuf.len) { + let i: i32 = 0; + for (i < avail) { + b.rbuf[i] = b.rbuf[b.rstart + i]; + i += 1; + }; b.rstart = 0; - b.rend = 0; - let r: (size | io.eof | io.error) = io.read(b.src, b.rbuf); + b.rend = avail; + let r: (size | io.eof | io.error) = io.read(b.src, b.rbuf[b.rend:b.rbuf.len]); match (r) { - case let n: size => { b.rend = n: i32; }; - case io.eof => { let e: io.eof; return e; }; + case let n: size => { b.rend += n: i32; }; + case io.eof => { + if (avail == 0) { let e: io.eof; return e; }; + }; case let e: io.error => return e; }; }; - let avail: i32 = b.rend - b.rstart; + avail = b.rend - b.rstart; let n: i32 = buf.len; if (avail < n) { n = avail; }; let i: i32 = 0; diff --git a/lib/bufio/bufiotest.ww b/lib/bufio/bufiotest.ww index 9a826877..db1d6857 100644 --- a/lib/bufio/bufiotest.ww +++ b/lib/bufio/bufiotest.ww @@ -511,6 +511,89 @@ fn errsource() io.stream = { assert(!(bufio.isbuffered(m))); }; +// ---- bread tops up a partial pending buffer (Hare short-read) -------- +// +// rbuf holds SOME pending bytes (fewer than requested) AND has spare +// capacity: Hare shifts the pending region to the front and reads more +// into the tail before serving (ref/hare/bufio/stream.ha:209). Pre-fix +// ww served only the short pending count and never topped up. +@test fn streamreadtopup() void = { + let raw: [16]u8; + let n: i32 = putstr("ABCDEFGHIJKLMNOP", raw[0:16], 0); + + let mem: memio.stream = memio.fixed(raw[0:n]); + let m: io.stream = &mem.vt; + + let rb: [8]u8; + let wb: [4]u8; + let b: bufio.stream = bufio.init(m, rb[0:8], wb[0:4]); + + let p: io.stream = &b.vt; + + // First read fills rbuf (8B "ABCDEFGH"), serves 3 — 5 pending remain. + let out: [10]u8; + let r1: (size | io.eof | io.error) = io.read(p, out[0:3]); + match (r1) { + case let z: size => { assert(!(z: i32 != 3)); }; + case io.eof => abort(); + case let e: io.error => abort(); + }; + + // Second read wants 10 > 5 pending: Hare shifts "DEFGH" to the front, + // reads 3 more ("IJK") into the tail, serves 8. Pre-fix ww returned 5. + let r2: (size | io.eof | io.error) = io.read(p, out[0:10]); + match (r2) { + case let z: size => { assert(!(z: i32 != 8)); }; + case io.eof => abort(); + case let e: io.error => abort(); + }; + assert(!(out[0] != 68u8)); // 'D' + assert(!(out[7] != 75u8)); // 'K' +}; + +// ---- bread serves pending bytes when the top-up read hits EOF -------- +// +// The load-bearing "EOF fatal only when avail==0" path +// (ref/hare/bufio/stream.ha:216): rbuf still holds pending bytes when +// the top-up io.read returns EOF — Hare drains the pending region, it +// does NOT propagate eof. Source is shorter than two reads so the +// second read's top-up io.read EOFs with bytes still buffered. +@test fn streamreadtopupeof() void = { + let raw: [5]u8; + let n: i32 = putstr("ABCDE", raw[0:5], 0); + + let mem: memio.stream = memio.fixed(raw[0:n]); + let m: io.stream = &mem.vt; + + let rb: [8]u8; + let wb: [4]u8; + let b: bufio.stream = bufio.init(m, rb[0:8], wb[0:4]); + + let p: io.stream = &b.vt; + + // First read fills rbuf (5B "ABCDE", source drained), serves 3 — + // 2 pending remain. + let out: [10]u8; + let r1: (size | io.eof | io.error) = io.read(p, out[0:3]); + match (r1) { + case let z: size => { assert(!(z: i32 != 3)); }; + case io.eof => abort(); + case let e: io.error => abort(); + }; + + // Second read wants 10 > 2 pending: shift "DE" to the front, top-up + // io.read hits EOF (source drained). avail==2 != 0 so Hare drains the + // pending region — serves 2, NOT eof. "always fatal" would return eof. + let r2: (size | io.eof | io.error) = io.read(p, out[0:10]); + match (r2) { + case let z: size => { assert(!(z: i32 != 2)); }; + case io.eof => abort(); + case let e: io.error => abort(); + }; + assert(!(out[0] != 68u8)); // 'D' + assert(!(out[1] != 69u8)); // 'E' +}; + // ---- bread + unread: pushed-back bytes come out first ---------------- @test fn streamunread() void = {