From 036b2c851fe24d7a767088dff970e42547cb3f7d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 13 May 2026 20:38:55 +0900 Subject: [PATCH] lib+test: graduate bufio to Hare scanner on io.stream Scanner subset: newscanner, finish, scanbyte, scanline, scantok, overflow (named-void). Buffer caller-owned; EOF_DISCARD default. Skips Hare's scanner-as-io.stream embed pending task #6 (chained N_DOT-of-N_DOT miscompile). --- lib/bufio/bufio.ww | 259 +++++++++++++++++++------- lib/bufio/bufiotest.ww | 413 +++++++++++++++++++++++++++++++++++++++++ test/wcc/700_e2e.c | 44 +++-- test/wcc/900_stdlib.c | 7 +- 4 files changed, 640 insertions(+), 83 deletions(-) create mode 100644 lib/bufio/bufiotest.ww diff --git a/lib/bufio/bufio.ww b/lib/bufio/bufio.ww index d3aa8953..3a26027c 100644 --- a/lib/bufio/bufio.ww +++ b/lib/bufio/bufio.ww @@ -1,72 +1,197 @@ -// bufio — buffered reader/writer over io.stream. Plan 9 'bio' -// analogue, lowered to ww. The buffer is owned by the caller and -// passed in at init time; we don't allocate. +// bufio — buffered scanner over [[io.stream]]. Subset of Hare's +// bufio:: scanner shape (ref/hare/bufio/scanner.ha). // -// We keep the stream pointer as *void here to avoid a cross-module -// type name (the module-import system isn't online yet); a later -// revision will replace it with *io.stream once `use` resolves -// types from imported modules. - -type streamp = *void; - -type buf = struct { - s: streamp, - data: *u8, - cap: i32, - r: i32, // read cursor - w: i32, // write cursor (for writers) -}; - -export fn init(b: *buf, s: streamp, data: *u8, cap: i32) void = { - b.s = s; - b.data = data; - b.cap = cap; - b.r = 0; - b.w = 0; -}; - -// readbyte — pop one byte. void variant signals EOF (empty buffer). -// Hare name; the (i32 | void) shape is a subset of Hare's full -// (u8 | EOF | io::error) — error reporting from the underlying -// stream will arrive when bufio actually wires up to io::stream. -export fn readbyte(b: *buf) (i32 | void) = { - if (b.r < b.w) { - let c: u8 = b.data[b.r]; - b.r += 1; - return c: i32; - }; - return; -}; - -// Distinct alias so `(str | linerr)` has two variant types the -// tagged-union machinery can keep apart at the tag level. The error -// variant carries a short description; callers inspect by length. -// Placeholder until bufio graduates to io.stream + named-void tags -// (eof / closed / underread). -type linerr = str; - -// readline — Hare-style fallible line read (name transliterated from -// `read_line`). Drains the buffer up to (but not including) the next -// '\n' and advances the cursor past the newline. Returns the line as -// a borrowed str on success, or a linerr describing why no line was -// available: -// - "eof" when the buffer is empty -// - "no newline" when the buffer contains data but no '\n' +// Surface today: // -// The returned str borrows from the underlying buffer; callers must -// consume it (or copy) before refilling. -export fn readline(b: *buf) (str | linerr) = { - if (b.r >= b.w) { return "eof": linerr; }; - let i: i32 = b.r; - for (i < b.w) { - if (b.data[i] == 10u8) { - let s: str; - s.ptr = b.data + b.r; - s.len = i - b.r; - b.r = i + 1; - return s; +// bufio.newscanner(s: *scanner, src: *io.stream, buf: []u8) void +// bufio.finish (s: *scanner) void +// bufio.scanbyte (s: *scanner) (u8 | io.eof | io.closed) +// bufio.scantok (s: *scanner, delim: u8) +// ([]u8 | io.eof | io.closed | overflow) +// bufio.scanline (s: *scanner) (str | io.eof | io.closed | overflow) +// +// 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 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. +// +// • 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. +// +// • Hare's `newscanner` allocates and grows the buffer up to +// `maxread`; we ship only the caller-supplied shape (Hare's +// `newscanner_buf`) and pick up the auto-grow variant when an +// append over a struct-held slice works. +// +// • EOF-handling defaults to Hare's `EOF_DISCARD`: bytes between +// the last delimiter and EOF are dropped and io.eof is returned +// 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. +// +// let mem: memio.state; +// let m: io.stream; +// memio.fixed(&mem, &m, raw[0:N]); +// let buf: [128]u8; +// let sc: bufio.scanner; +// bufio.newscanner(&sc, &m, buf[0:128]); +// match (bufio.scanline(&sc)) { ... }; +// bufio.finish(&sc); + +use io; + +// 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 +// use of errors::overflow for the same condition. +export type overflow = !void; + +export type scanner = struct { + src: *io.stream, + ptr: *u8, + cap: i32, + start: i32, // index where the pending region starts in ptr + avail: i32, // pending byte count; pending = ptr[start..start+avail] +}; + +// newscanner — wire `s` to read through `src` using `buf` as the +// read-ahead window. Hare returns the scanner by value +// (newscanner_buf); we take an out-parameter pending cgen support +// for wide-struct return. +export fn newscanner(s: *scanner, src: *io.stream, buf: []u8) void = { + s.src = src; + s.ptr = buf.ptr; + s.cap = buf.len; + s.start = 0; + s.avail = 0; +}; + +// finish — release scanner-owned resources. No-op today (buffer is +// caller-owned, src isn't closed); kept on the surface so callers +// won't churn when bufio later grows internal allocations. +export fn finish(s: *scanner) void = { }; + +// readahead — make room and read once from src into the back of the +// pending region. Returns the number of bytes newly buffered (≥0, +// can be 0 if the underlying stream made no progress), or +// io.eof/io.closed propagated from src. +fn readahead(s: *scanner) (i32 | io.eof | io.closed) = { + if (s.start + s.avail == s.cap && s.start > 0) { + // Shift pending region to the front of the buffer. + let i: i32 = 0; + for (i < s.avail) { + s.ptr[i] = s.ptr[s.start + i]; + i += 1; }; - 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: (i32 | io.eof | io.closed) = io.read(s.src, v); + match (r) { + case let n: i32 => { + s.avail += n; + return n; + }; + case io.eof => { let e: io.eof; return e; }; + case io.closed => { let e: io.closed; return e; }; + }; +}; + +// scanbyte — pop one byte from the scanner, refilling from src on +// demand. Mirrors Hare's `scan_byte` (ref/hare/bufio/scanner.ha:204). +export fn scanbyte(s: *scanner) (u8 | io.eof | io.closed) = { + for (s.avail == 0) { + let r: (i32 | io.eof | io.closed) = readahead(s); + match (r) { + case let n: i32 => { }; + case io.eof => { let e: io.eof; return e; }; + case io.closed => { let e: io.closed; return e; }; + }; + }; + let b: u8 = s.ptr[s.start]; + s.start += 1; + s.avail -= 1; + return b; +}; + +// scantok — read up to (and not including) the next byte equal to +// `delim`. The delim byte is consumed from the stream but not +// returned. The returned slice borrows from the scanner's internal +// buffer and is invalidated by the next scan call. +// +// EOF without finding delim discards the trailing fragment and +// returns io.eof (Hare's EOF_DISCARD default). Buffer-full without +// delim returns overflow. +// +// Mirrors Hare's `scan_bytes` (ref/hare/bufio/scanner.ha:220), +// narrowed to a single-byte delimiter. +export fn scantok(s: *scanner, delim: u8) ([]u8 | io.eof | io.closed | 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; + }; + // No delim in pending. If the buffer is full with nowhere + // to shift, the caller's budget is too small — overflow. + if (s.start + s.avail == s.cap && s.start == 0) { + let e: overflow; return e; + }; + let r: (i32 | io.eof | io.closed) = readahead(s); + match (r) { + case let n: i32 => { + // readahead may shift start to 0; the searched bytes + // move with it, so `i` (count from start) is still + // accurate. Resume scanning where we left off. + }; + case io.eof => { let e: io.eof; return e; }; + case io.closed => { let e: io.closed; return e; }; + }; + }; + let e: io.eof; return e; +}; + +// scanline — read up to (and not including) the next '\n'. The +// newline is consumed; the returned str view borrows from the +// scanner buffer and is invalidated by the next scan call. +// Hare's `scan_line` is `scan_string(s, "\n")`; we route through +// scantok directly since lib/strings doesn't yet ship `toutf8` over +// a multi-byte delim. +export fn scanline(s: *scanner) (str | io.eof | io.closed | overflow) = { + let r: ([]u8 | io.eof | io.closed | overflow) = scantok(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 io.closed => { let e: io.closed; return e; }; + case overflow => { let e: overflow; return e; }; }; - return "no newline": linerr; }; diff --git a/lib/bufio/bufiotest.ww b/lib/bufio/bufiotest.ww new file mode 100644 index 00000000..25116abb --- /dev/null +++ b/lib/bufio/bufiotest.ww @@ -0,0 +1,413 @@ +// 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 +// (rather than `[N]struct{...}`) match the same cgen workaround +// memiotest leans on. + +use bufio; +use bytes; +use io; +use memio; + +// Direct exit(2) binding rather than `use os;` — os exports +// read/write/close, which collide with io.read/write/close under +// the driver's flat-scope concat. +@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64; +fn doexit(code: i32) void = { + syscall1ww(60i64, code: i64); +}; + +// signalled — bumped by main before each test so a failing exit code +// pinpoints the offending case. +let signalled: i32 = 0; + +fn fail() void = { doexit(signalled + 10); }; + +// putstr — copy the bytes of `s` into `into` starting at `off`, +// returning the new offset. +fn putstr(s: str, into: []u8, off: i32) i32 = { + let i: i32 = 0; + for (i < s.len) { + into[off + i] = s[i]; + i += 1; + }; + return off + s.len; +}; + +// ---- A closed-on-read stream for the io.closed surfacing test --------- + +fn closedread(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = { + let e: io.closed; return e; +}; +fn closedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { + let e: io.closed; return e; +}; +fn closedclose(s: *io.stream) (void | io.closed) = { return; }; + +fn closedstream(s: *io.stream) void = { + s.ctx = nil; + s.read = closedread; + s.write = closedwrite; + s.close = closedclose; +}; + +// ---- scanbyte: drain four bytes through a 2-byte window --------------- + +// Reading buf is smaller than the stream contents (2 vs 4) — the +// scanner has to refill twice. Row 4 is one past EOF, row 5 stays at +// EOF (idempotent). +@test fn scanbytecases() void = { + let raw: [4]u8; + raw[0] = 11u8; raw[1] = 22u8; raw[2] = 33u8; raw[3] = 44u8; + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:4]); + let buf: [2]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:2]); + + // (wantbyte, weof) + let want: [6]u8; + let weof: [6]i32; + want[0]=11u8; weof[0]=0; + want[1]=22u8; weof[1]=0; + want[2]=33u8; weof[2]=0; + want[3]=44u8; weof[3]=0; + want[4]=0u8; weof[4]=1; // past end → eof + want[5]=0u8; weof[5]=1; // stay at eof + + let i: i32 = 0; + for (i < 6) { + let r: (u8 | io.eof | io.closed) = bufio.scanbyte(&sc); + match (r) { + case let b: u8 => { + if (weof[i] != 0) { fail(); }; + if (b != want[i]) { fail(); }; + }; + case io.eof => { if (weof[i] == 0) { fail(); }; }; + case io.closed => fail(); + }; + i += 1; + }; + + bufio.finish(&sc); +}; + +// ---- scanline: 3 lines + tail fragment + extra calls stay at EOF ------ + +@test fn scanlinecases() void = { + let src: [32]u8; + let n: i32 = putstr("foo\nbar\nbaz\ntrailing", src[0:32], 0); + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, src[0:n]); + let buf: [32]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:32]); + + // (wantlen, wantfirst, weof, wovr) + // Row 0..2: terminated lines. + // Row 3: tail "trailing" — EOF_DISCARD drops it, returns eof. + // Row 4: subsequent call stays at eof. + let wl: [5]i32; + let wf: [5]u8; + let weof: [5]i32; + let wovr: [5]i32; + wl[0]=3; wf[0]=102u8; weof[0]=0; wovr[0]=0; // foo + wl[1]=3; wf[1]=98u8; weof[1]=0; wovr[1]=0; // bar + wl[2]=3; wf[2]=98u8; weof[2]=0; wovr[2]=0; // baz + wl[3]=0; wf[3]=0u8; weof[3]=1; wovr[3]=0; // trailing → eof + wl[4]=0; wf[4]=0u8; weof[4]=1; wovr[4]=0; // stay eof + + let i: i32 = 0; + for (i < 5) { + let r: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc); + match (r) { + case let v: str => { + if (weof[i] != 0) { fail(); }; + if (wovr[i] != 0) { fail(); }; + if (v.len != wl[i]) { fail(); }; + if (v.len > 0 && v[0] != wf[i]) { fail(); }; + }; + case io.eof => { if (weof[i] == 0) { fail(); }; }; + case io.closed => fail(); + case bufio.overflow => { if (wovr[i] == 0) { fail(); }; }; + }; + i += 1; + }; + + bufio.finish(&sc); +}; + +// ---- scanline: line longer than buffer → overflow -------------------- + +@test fn scanlineoverflow() void = { + let src: [32]u8; + let n: i32 = putstr("ABCDEFGHI\n", src[0:32], 0); + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, src[0:n]); + + // Buffer too small for the 9-byte payload + delim. Caller + // owns the budget; overflow signals "raise it". + let buf: [4]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:4]); + + let r: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc); + match (r) { + case let v: str => fail(); + case io.eof => fail(); + case io.closed => fail(); + case bufio.overflow => { }; + }; + + bufio.finish(&sc); +}; + +// ---- scantok: multi-delim hit, delim-at-start, EOF before delim ------ + +@test fn scantokcases() void = { + let src: [16]u8; + // ",a,,b,c" — leading delim, empty token, multi-hit, trailing fragment. + let n: i32 = putstr(",a,,b,c", src[0:16], 0); + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, src[0:n]); + let buf: [8]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:8]); + + // (wantlen, wantfirst, weof) + // Row 0: leading "," → empty token. + // Row 1: "a" before next ",". + // Row 2: empty token between consecutive ",,". + // Row 3: "b". + // Row 4: "c" — no trailing delim → EOF_DISCARD drops + eof. + let wl: [5]i32; + let wf: [5]u8; + let weof: [5]i32; + wl[0]=0; wf[0]=0u8; weof[0]=0; + wl[1]=1; wf[1]=97u8; weof[1]=0; // 'a' + wl[2]=0; wf[2]=0u8; weof[2]=0; + wl[3]=1; wf[3]=98u8; weof[3]=0; // 'b' + wl[4]=0; wf[4]=0u8; weof[4]=1; // 'c' discarded + + let i: i32 = 0; + for (i < 5) { + let r: ([]u8 | io.eof | io.closed | bufio.overflow) = bufio.scantok(&sc, 44u8); // ',' + match (r) { + case let v: []u8 => { + if (weof[i] != 0) { fail(); }; + if (v.len != wl[i]) { fail(); }; + if (v.len > 0 && v[0] != wf[i]) { fail(); }; + }; + case io.eof => { if (weof[i] == 0) { fail(); }; }; + case io.closed => fail(); + case bufio.overflow => fail(); + }; + i += 1; + }; + + bufio.finish(&sc); +}; + +// ---- empty stream: every variant returns eof immediately -------------- + +@test fn emptystream() void = { + let raw: [1]u8; + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, raw[0:0]); + let buf: [8]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:8]); + + let r1: (u8 | io.eof | io.closed) = bufio.scanbyte(&sc); + match (r1) { + case let b: u8 => fail(); + case io.eof => { }; + case io.closed => fail(); + }; + + let r2: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc); + match (r2) { + case let v: str => fail(); + case io.eof => { }; + case io.closed => fail(); + case bufio.overflow => fail(); + }; + + let r3: ([]u8 | io.eof | io.closed | bufio.overflow) = bufio.scantok(&sc, 10u8); + match (r3) { + case let v: []u8 => fail(); + case io.eof => { }; + case io.closed => fail(); + case bufio.overflow => fail(); + }; + + bufio.finish(&sc); +}; + +// ---- io.closed surfacing on a stream that returns closed -------------- + +@test fn closedsource() void = { + let m: io.stream; + closedstream(&m); + let buf: [8]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:8]); + + let r1: (u8 | io.eof | io.closed) = bufio.scanbyte(&sc); + match (r1) { + case let b: u8 => fail(); + case io.eof => fail(); + case io.closed => { }; + }; + + let r2: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc); + match (r2) { + case let v: str => fail(); + case io.eof => fail(); + case io.closed => { }; + case bufio.overflow => fail(); + }; + + let r3: ([]u8 | io.eof | io.closed | bufio.overflow) = bufio.scantok(&sc, 32u8); + match (r3) { + case let v: []u8 => fail(); + case io.eof => fail(); + case io.closed => { }; + case bufio.overflow => fail(); + }; + + bufio.finish(&sc); +}; + +// ---- boundary: empty line + idempotent EOF on scantok ---------------- + +// scanline on a leading '\n' must return an empty (len=0) view, not +// skip the empty line and pretend it's "the first non-empty line". +// scantok after EOF_DISCARD must keep returning io.eof on every +// subsequent call — same idempotency guaranteed by scanbyte rows 4/5 +// and scanline rows 3/4, but the scantok path filled at the bottom +// of `for (true)` is its own state machine. Worth pinning. +@test fn boundarycases() void = { + // ---- empty line via scanline ----------------------------------- + let s1: [16]u8; + let n1: i32 = putstr("\nfoo\n", s1[0:16], 0); + let m1mem: memio.state; + let m1: io.stream; + memio.fixed(&m1mem, &m1, s1[0:n1]); + let b1: [8]u8; + let sc1: bufio.scanner; + bufio.newscanner(&sc1, &m1, b1[0:8]); + + let r1: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc1); + match (r1) { + case let v: str => { if (v.len != 0) { fail(); }; }; + case io.eof => fail(); + case io.closed => fail(); + case bufio.overflow => fail(); + }; + let r2: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc1); + match (r2) { + case let v: str => { + if (v.len != 3) { fail(); }; + if (v[0] != 102u8) { fail(); }; // 'f' + }; + case io.eof => fail(); + case io.closed => fail(); + case bufio.overflow => fail(); + }; + bufio.finish(&sc1); + + // ---- scantok idempotent EOF ----------------------------------- + // "ab" with delim ',' — no delim, EOF_DISCARD drops "ab", first + // call returns eof. The next call must too (state stays at eof). + let s2: [8]u8; + let n2: i32 = putstr("ab", s2[0:8], 0); + let m2mem: memio.state; + let m2: io.stream; + memio.fixed(&m2mem, &m2, s2[0:n2]); + let b2: [8]u8; + let sc2: bufio.scanner; + bufio.newscanner(&sc2, &m2, b2[0:8]); + + let t1: ([]u8 | io.eof | io.closed | bufio.overflow) = bufio.scantok(&sc2, 44u8); + match (t1) { + case let v: []u8 => fail(); + case io.eof => { }; + case io.closed => fail(); + case bufio.overflow => fail(); + }; + let t2: ([]u8 | io.eof | io.closed | bufio.overflow) = bufio.scantok(&sc2, 44u8); + match (t2) { + case let v: []u8 => fail(); + case io.eof => { }; + case io.closed => fail(); + case bufio.overflow => fail(); + }; + bufio.finish(&sc2); +}; + +// ---- multi-fill: window smaller than longest token, shift path -------- + +// Buffer 4B, line "abcd\nxy\n". After reading "abcd", a delim search +// finds nothing, the buffer is full at start=0 → overflow. So bump +// to 5B (room for "abcd" + '\n'). Reads come in 5-byte chunks; the +// second line "xy" fits trivially and exercises the shift path. +@test fn multifill() void = { + let src: [32]u8; + let n: i32 = putstr("abcd\nxy\n", src[0:32], 0); + + let mem: memio.state; + let m: io.stream; + memio.fixed(&mem, &m, src[0:n]); + let buf: [5]u8; + let sc: bufio.scanner; + bufio.newscanner(&sc, &m, buf[0:5]); + + // (wantlen, wantfirst, weof) + let wl: [3]i32; + let wf: [3]u8; + let weof: [3]i32; + wl[0]=4; wf[0]=97u8; weof[0]=0; // abcd + wl[1]=2; wf[1]=120u8; weof[1]=0; // xy + wl[2]=0; wf[2]=0u8; weof[2]=1; // eof + + let i: i32 = 0; + for (i < 3) { + let r: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc); + match (r) { + case let v: str => { + if (weof[i] != 0) { fail(); }; + if (v.len != wl[i]) { fail(); }; + if (v.len > 0 && v[0] != wf[i]) { fail(); }; + }; + case io.eof => { if (weof[i] == 0) { fail(); }; }; + case io.closed => fail(); + case bufio.overflow => fail(); + }; + i += 1; + }; + + bufio.finish(&sc); +}; + +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(); + return 0; +}; diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 455696f6..0e97afcb 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -1300,35 +1300,49 @@ static const struct row rows[] = { "fn main() i32 = {\n" " return dispatch(classify(1)) + dispatch(classify(2)) + dispatch(classify(3));\n" "};", 60 }, - /* bufio.readline: drain successive '\\n'-terminated lines from a - * pre-filled buffer, then a trailing fragment that returns the - * `linerr` variant carrying \"no newline\" (10 chars). */ + /* bufio.scanline: drain '\\n'-terminated lines from a memio.fixed + * source through a bufio.scanner. The trailing "baz" fragment has + * no newline; under the EOF_DISCARD default it's dropped and the + * call returns io.eof. Also pins the cross-module type ref + * (scanner stores *io.stream) end-to-end through `use`. */ { "use bufio;\n" + "use io;\n" + "use memio;\n" "fn main() i32 = {\n" " let raw: [11]u8;\n" " raw[0] = 102u8; raw[1] = 111u8; raw[2] = 111u8; raw[3] = 10u8;\n" " raw[4] = 98u8; raw[5] = 97u8; raw[6] = 114u8; raw[7] = 10u8;\n" " raw[8] = 98u8; raw[9] = 97u8; raw[10] = 122u8;\n" - " let b: bufio.buf;\n" - " b.s = nil; b.data = raw.ptr; b.cap = 11; b.r = 0; b.w = 11;\n" + " let mem: memio.state;\n" + " let m: io.stream;\n" + " memio.fixed(&mem, &m, raw[0:11]);\n" + " let buf: [16]u8;\n" + " let sc: bufio.scanner;\n" + " bufio.newscanner(&sc, &m, buf[0:16]);\n" " let acc: i32 = 0;\n" - " let l1: (str | bufio.linerr) = bufio.readline(&b);\n" + " let l1: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc);\n" " match (l1) {\n" - " case let s: str => acc += s.len: i32;\n" - " case let e: bufio.linerr => acc += -100;\n" + " case let s: str => acc += s.len;\n" + " case io.eof => acc += -100;\n" + " case io.closed => acc += -1000;\n" + " case bufio.overflow => acc += -10000;\n" " };\n" - " let l2: (str | bufio.linerr) = bufio.readline(&b);\n" + " let l2: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc);\n" " match (l2) {\n" - " case let s: str => acc += s.len: i32;\n" - " case let e: bufio.linerr => acc += -100;\n" + " case let s: str => acc += s.len;\n" + " case io.eof => acc += -100;\n" + " case io.closed => acc += -1000;\n" + " case bufio.overflow => acc += -10000;\n" " };\n" - " let l3: (str | bufio.linerr) = bufio.readline(&b);\n" + " let l3: (str | io.eof | io.closed | bufio.overflow) = bufio.scanline(&sc);\n" " match (l3) {\n" - " case let s: str => acc += -100;\n" - " case let e: bufio.linerr => acc += e.len: i32;\n" + " case let s: str => acc += -100;\n" + " case io.eof => acc += 7;\n" + " case io.closed => acc += -1000;\n" + " case bufio.overflow => acc += -10000;\n" " };\n" " return acc;\n" - "};", 16 }, /* 3 + 3 + len(\"no newline\")=10 */ + "};", 13 }, /* 3 + 3 + 7 (eof arm) */ /* `(scalar, str)` tuple return: AX:DX:CX convention extends the * tagged-union ABI. AX = scalar, DX = str.ptr, CX = str.len. * Receive sites destructure off the same regs regardless of diff --git a/test/wcc/900_stdlib.c b/test/wcc/900_stdlib.c index bf4c7bf3..c343ab01 100644 --- a/test/wcc/900_stdlib.c +++ b/test/wcc/900_stdlib.c @@ -33,7 +33,12 @@ static const char *modules[] = { "lib/math/random/random.ww", "lib/time/time.ww", "lib/c/libc/libc.ww", - "lib/bufio/bufio.ww", + /* lib/bufio/bufio.ww moved off this list: graduates to the + * io.stream-based scanner, which carries cross-module type + * refs (*io.stream, io.eof, io.closed) that only resolve once + * the driver concatenates `use`d modules. Coverage lives at + * lib/bufio/bufiotest.ww + the bufio.scanline e2e row in + * test/wcc/700_e2e.c. */ "lib/fmt/fmt.ww", "lib/net/net.ww", NULL