bufio: bread tops up a partial buffer (Hare short-read)

This commit is contained in:
2026-06-15 02:08:55 +09:00
parent 9fcb3be541
commit 9c10a731b0
2 changed files with 106 additions and 9 deletions

View File

@@ -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 = {