Files
ww/lib/memio/memiotest.ww
Hojun-Cho cab85f5bc9 lib/memio: fixedwrite returns nomem on full buffer (F-R)
memio.fixedwrite returned a successful 0-byte write once the sink
filled, so an overflowing fprintf/bsprintf surfaced a truncated prefix
as a successful str instead of an error. Hare's fixed_write returns
nomem there (ref/hare/memio/stream.ha:161); the bsprintf/fprintf
io.error arm already forwards it, so the prefix-on-overflow path is the
only divergence.

Mirror Hare's full guard order: an empty input buf short-circuits to 0
(stream.ha:157) before the full-sink nomem guard, so a 0-byte write to
a full sink stays 0 (no new divergence). fmt.bsprintf/formatone keep
their logic; only their now-stale WHY-comments are rewritten, and
formatone's tail-pad counter is left as-is (the width-form restore is a
deferred follow-up, out of F-R scope). memio's own `fixed` doc comment,
which still claimed ww surfaces 0 on a full buffer, is corrected to the
new nomem contract.

Tests: flip the two fmt rows that pinned the prefix bug (bsprintf_trunc,
bsprintf_width_trunc) plus memiotest fixedwritecases' overflow row to
assert `is nomem`; add positive controls (bsprintf_exact must still
succeed) + an empty-sink discriminator (bsprintf_empty) + a dedicated
fixedwritefull unit pinning the memio.ww:190 contract.

Regenerates the w6c and wwdump combined.ww (memio's fixedwrite change
and `fixed` doc comment are the only embedded changes; fmt is
dead-code-eliminated from both).
2026-06-14 23:59:29 +09:00

570 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// memiotest — exercises lib/memio. Run with `out/bin/ww run lib/memio/memiotest.ww`.
//
// Every @test enumerates parallel `[N]T` arrays of inputs and
// expectations, then iterates one body across them. Parallel arrays
// (rather than `[N]struct{...}`) sidestep the cstage cgen's chained
// `arr[i].field` store gap (task #6). #94 fold-eFinal: the unified
// value-return surface (`let st = memio.fixed(buf); &st.vt` into the
// io dispatchers; io.read/write/close return size/io.error).
package memio_test;
import bytes;
import errors;
import io;
import memio;
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;
};
// ---- fixedread: read sizes drive partial / full / eof outcomes ----------
@test fn fixedread() void = {
let arr: [8]u8;
arr[0] = 1u8; arr[1] = 2u8; arr[2] = 3u8; arr[3] = 4u8;
arr[4] = 5u8; arr[5] = 6u8; arr[6] = 7u8; arr[7] = 8u8;
// (request, wantn, wantfirst, wantlast, weof)
// 5 rows: under-remaining, full slot, last-byte-only, post-end, 0-len-at-end.
let req: [5]i32;
let wantn: [5]i32;
let wantf: [5]u8;
let wantl: [5]u8;
let weof: [5]i32;
req[0]=3; wantn[0]=3; wantf[0]=1u8; wantl[0]=3u8; weof[0]=0;
req[1]=4; wantn[1]=4; wantf[1]=4u8; wantl[1]=7u8; weof[1]=0;
req[2]=5; wantn[2]=1; wantf[2]=8u8; wantl[2]=8u8; weof[2]=0;
req[3]=1; wantn[3]=0; wantf[3]=0u8; wantl[3]=0u8; weof[3]=1;
req[4]=0; wantn[4]=0; wantf[4]=0u8; wantl[4]=0u8; weof[4]=1;
let st: memio.stream = memio.fixed(arr[0:8]);
let s: io.stream = &st.vt;
let out: [8]u8;
let i: i32 = 0;
for (i < 5) {
// Clear out so spot checks read real data, not stale bytes.
let k: i32 = 0;
for (k < 8) { out[k] = 0u8; k += 1; };
let r: (size | io.eof | io.error) = io.read(s, out[0:req[i]]);
match (r) {
case let nz: size => {
let n: i32 = nz: i32;
assert(!(weof[i] != 0));
assert(!(n != wantn[i]));
assert(!(n > 0 && out[0] != wantf[i]));
assert(!(n > 0 && out[n - 1] != wantl[i]));
};
case io.eof => { assert(!(weof[i] == 0)); };
case let e: io.error => abort();
};
i += 1;
};
// fixed stream has no closer → io.close returns void.
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
// ---- fixedwritecases: full-fit / exact-fill / partial / overflow -------------
@test fn fixedwritecases() void = {
let dst: [16]u8;
let st: memio.stream = memio.fixed(dst[0:16]);
let s: io.stream = &st.vt;
// One flat source; rows index into it via (off, len). Sequence:
// "hello " (6), "world!!" (7), "XXXXXXX" (7), "Y" (1).
let src: [32]u8;
let _: i32 = putstr("hello world!!XXXXXXXY", src[0:32], 0);
// (srcoff, inlen, wantn, wantnomem) — wantn diverges from inlen on
// the partial row; the overflow row writes into a now-full sink and
// returns nomem (ref/hare/memio/stream.ha:161).
let off: [4]i32;
let ln: [4]i32;
let wantn: [4]i32;
let wantnomem: [4]bool;
off[0]=0; ln[0]=6; wantn[0]=6; wantnomem[0]=false; // full fit
off[1]=6; ln[1]=7; wantn[1]=7; wantnomem[1]=false; // exact-fills cap (pos=13)
off[2]=13; ln[2]=7; wantn[2]=3; wantnomem[2]=false; // partial: 3 free
off[3]=20; ln[3]=1; wantn[3]=0; wantnomem[3]=true; // overflow: full sink → nomem
let i: i32 = 0;
for (i < 4) {
let lo: i32 = off[i];
let hi: i32 = lo + ln[i];
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) {
case let n: size => {
assert(!wantnomem[i]);
assert(!(n: i32 != wantn[i]));
};
case let e: io.error => {
assert(wantnomem[i]);
assert(e is nomem);
};
};
i += 1;
};
let want: [16]u8;
let _: i32 = putstr("hello world!!XXX", want[0:16], 0);
assert(!(!bytes.equal(dst[0:16], want[0:16])));
// 0-byte write is always 0 with no side effects.
let r0: (size | io.error) = io.write(s, src[0:0]);
match (r0) {
case let n: size => { assert(!(n != 0: size)); };
case let e: io.error => abort();
};
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
// ---- fixedwritefull: the full-sink nomem contract (memio.ww:190) -------
// A write that exactly fills the sink succeeds; any further non-empty
// write into the full sink returns nomem, not a 0-byte success
// (ref/hare/memio/stream.ha:161). A zero-length sink is full from the
// start, so its first non-empty write is already nomem.
@test fn fixedwritefull() void = {
let buf: [4]u8;
let st: memio.stream = memio.fixed(buf[0:4]);
let s: io.stream = &st.vt;
let src: [4]u8;
let _: i32 = putstr("abcd", src[0:4], 0);
let r4: (size | io.error) = io.write(s, src[0:4]); // exact fill
match (r4) {
case let n: size => { assert(!(n != 4: size)); };
case let e: io.error => abort();
};
let r1: (size | io.error) = io.write(s, src[0:1]); // full sink
match (r1) {
case let n: size => abort();
case let e: io.error => { assert(e is nomem); };
};
let ze: memio.stream = memio.fixed(buf[0:0]);
let zs: io.stream = &ze.vt;
let rz: (size | io.error) = io.write(zs, src[0:1]); // zero-len sink
match (rz) {
case let n: size => abort();
case let e: io.error => { assert(e is nomem); };
};
};
// ---- dynamicgrowcases: every cap doubling exercised ---------------------
// Drive grow 0 → 8 → 16 → 32 by writing sized chunks. Verify
// accumulated `pos` after each step.
@test fn dynamicgrowcases() void = {
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
let src: [32]u8;
let _: i32 = putstr("abcdefghijklmnopqrstuvwxyz012345", src[0:32], 0);
// (chunkn, totaln) — cap transitions: 0→8 at row 0; 8→16 at row 2;
// 16→32 at row 4.
let chunk: [5]i32;
let total: [5]i32;
chunk[0]=3; total[0]=3;
chunk[1]=5; total[1]=8;
chunk[2]=1; total[2]=9;
chunk[3]=7; total[3]=16;
chunk[4]=10; total[4]=26;
let off: i32 = 0;
let i: i32 = 0;
for (i < 5) {
let lo: i32 = off;
let hi: i32 = lo + chunk[i];
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) {
case let n: size => { assert(!(n: i32 != chunk[i])); };
case let e: io.error => abort();
};
assert(!(memio.buffer(&st).len != total[i]));
off += chunk[i];
i += 1;
};
assert(!(!bytes.equal(memio.buffer(&st), src[0:26])));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
// ---- dynamicreset: write / reset / write cycles -------------------------
// op=0 writes `ln` bytes from a rolling source; op=1 resets and ignores
// ln. After each row the accumulated len must equal `want`.
@test fn dynamicreset() void = {
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
let src: [16]u8;
src[0]=10u8; src[1]=20u8; src[2]=30u8; src[3]=40u8;
src[4]=50u8; src[5]=60u8; src[6]=70u8; src[7]=80u8;
let op: [6]i32;
let ln: [6]i32;
let want: [6]i32;
op[0]=0; ln[0]=5; want[0]=5; // initial write
op[1]=1; ln[1]=0; want[1]=0; // reset (after writes)
op[2]=0; ln[2]=3; want[2]=3; // refill from 0
op[3]=0; ln[3]=2; want[3]=5; // continue
op[4]=1; ln[4]=0; want[4]=0; // reset (after partial)
op[5]=0; ln[5]=8; want[5]=8; // larger refill
let off: i32 = 0;
let i: i32 = 0;
for (i < 6) {
if (op[i] == 1) {
memio.reset(&st);
off = 0;
} else {
let lo: i32 = off;
let hi: i32 = lo + ln[i];
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) {
case let n: size => { assert(!(n: i32 != ln[i])); };
case let e: io.error => abort();
};
off += ln[i];
};
assert(!(memio.buffer(&st).len != want[i]));
i += 1;
};
assert(!(!bytes.equal(memio.buffer(&st), src[0:8])));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
// ---- borrowedread: under / exact / over / 0-byte ------------------------
@test fn borrowedreadcases() void = {
let arr: [6]u8;
arr[0]=0u8; arr[1]=1u8; arr[2]=2u8; arr[3]=3u8; arr[4]=4u8; arr[5]=5u8;
let st: memio.stream = memio.fixed(arr[0:6]);
// (amt, wantfirst, wantlast, weof)
let amt: [4]i32;
let wf: [4]u8;
let wl: [4]u8;
let weof: [4]i32;
amt[0]=4; wf[0]=0u8; wl[0]=3u8; weof[0]=0; // under remaining
amt[1]=2; wf[1]=4u8; wl[1]=5u8; weof[1]=0; // exact remaining
amt[2]=1; wf[2]=0u8; wl[2]=0u8; weof[2]=1; // past end → eof
amt[3]=0; wf[3]=0u8; wl[3]=0u8; weof[3]=0; // 0-byte view always ok
let i: i32 = 0;
for (i < 4) {
let r: ([]u8 | io.eof) = memio.borrowedread(&st, amt[i]);
match (r) {
case let v: []u8 => {
assert(!(weof[i] != 0));
assert(!(v.len != amt[i]));
assert(!(v.len > 0 && v[0] != wf[i]));
assert(!(v.len > 0 && v[v.len - 1] != wl[i]));
};
case io.eof => { assert(!(weof[i] == 0)); };
};
i += 1;
};
};
// ---- stringview: len + endpoints track pos across appends ---------------
@test fn stringview() void = {
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
assert(!(memio.string(&st).len != 0));
let src: [32]u8;
let _: i32 = putstr("hello world!!", src[0:32], 0);
// (srcoff, inlen, wantacc, wantfirst, wantlast)
let off: [3]i32;
let ln: [3]i32;
let want: [3]i32;
let wf: [3]u8;
let wl: [3]u8;
off[0]=0; ln[0]=5; want[0]=5; wf[0]=104u8; wl[0]=111u8; // "hello" h..o
off[1]=5; ln[1]=6; want[1]=11; wf[1]=104u8; wl[1]=100u8; // +" world" h..d
off[2]=11; ln[2]=2; want[2]=13; wf[2]=104u8; wl[2]=33u8; // +"!!" h..!
let i: i32 = 0;
for (i < 3) {
let lo: i32 = off[i];
let hi: i32 = lo + ln[i];
let r: (size | io.error) = io.write(s, src[lo:hi]);
match (r) { case let n: size => {}; case let e: io.error => abort(); };
let v: str = memio.string(&st);
assert(!(v.len != want[i]));
assert(!(v[0] != wf[i]));
assert(!(v[v.len - 1] != wl[i]));
i += 1;
};
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
// ---- dynamicfromseed: ownership-transferred seed, read then write -------
@test fn dynamicfromseed() void = {
// Build a heap-allocated seed via append (rt_ensure path), then
// hand ownership to memio. dynamicclose then frees `seed.cap`
// bytes — the regression that motivated the `cap = buf.cap` fix.
let seed: []u8;
seed.ptr = nil; seed.len = 0; seed.cap = 0;
append(seed, 100u8, 101u8, 102u8, 103u8);
let st: memio.stream = memio.dynamicfrom(seed);
let s: io.stream = &st.vt;
// (readn, wantbyte, weof)
let rn: [5]i32;
let want: [5]u8;
let weof: [5]i32;
rn[0]=1; want[0]=100u8; weof[0]=0;
rn[1]=1; want[1]=101u8; weof[1]=0;
rn[2]=1; want[2]=102u8; weof[2]=0;
rn[3]=1; want[3]=103u8; weof[3]=0;
rn[4]=1; want[4]=0u8; weof[4]=1; // past seed end → eof
let out: [4]u8;
let i: i32 = 0;
for (i < 5) {
let r: (size | io.eof | io.error) = io.read(s, out[0:rn[i]]);
match (r) {
case let nz: size => {
let n: i32 = nz: i32;
assert(!(weof[i] != 0));
assert(!(n != rn[i]));
assert(!(out[0] != want[i]));
};
case io.eof => { assert(!(weof[i] == 0)); };
case let e: io.error => abort();
};
i += 1;
};
// Now write extends past the seed; grow path runs and close
// must free the (post-grow) cap, not the seed cap.
let extra: [4]u8;
extra[0]=200u8; extra[1]=201u8; extra[2]=202u8; extra[3]=203u8;
let w: (size | io.error) = io.write(s, extra[0:4]);
match (w) {
case let n: size => { assert(!(n: i32 != 4)); };
case let e: io.error => abort();
};
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
// ---- seekcases: SET/CUR/END × in-bounds/OOB over one fixed stream --------
// Sequential rows over one 8-byte fixed stream; CUR rows depend on the
// cursor left by earlier rows. want = resulting pos for valid rows,
// -1 = errors.invalid expected. After EVERY row an io.tell readback
// checks the cursor against a shadow `pos` — invalid rows must leave
// it untouched (ref/hare/memio/stream.ha:122-140 returns before the
// pos store).
@test fn seekcases() void = {
let arr: [8]u8;
arr[0]=10u8; arr[1]=11u8; arr[2]=12u8; arr[3]=13u8;
arr[4]=14u8; arr[5]=15u8; arr[6]=16u8; arr[7]=17u8;
// (whence, off, want)
let wh: [12]i32; // 0 SET / 1 CUR / 2 END
let off: [12]i64;
let want: [12]i64;
wh[0] =0; off[0] =3; want[0] =3; // SET forward
wh[1] =1; off[1] =2; want[1] =5; // CUR forward
wh[2] =1; off[2] =-5; want[2] =0; // CUR back to start
wh[3] =2; off[3] =0; want[3] =8; // END exact end
wh[4] =2; off[4] =-8; want[4] =0; // END back to start
wh[5] =0; off[5] =9; want[5] =-1; // SET past end
wh[6] =1; off[6] =-1; want[6] =-1; // CUR before start
wh[7] =2; off[7] =1; want[7] =-1; // END past end
wh[8] =2; off[8] =-9; want[8] =-1; // END before start
wh[9] =0; off[9] =8; want[9] =8; // SET end exact
wh[10]=1; off[10]=0; want[10]=8; // CUR no-op at end
wh[11]=0; off[11]=0; want[11]=0; // SET rewind
let st: memio.stream = memio.fixed(arr[0:8]);
let s: io.stream = &st.vt;
let pos: i64 = 0;
let i: i32 = 0;
for (i < 12) {
let w: io.whence = wh[i]: io.whence;
let r: (io.off | io.error) = io.seek(s, off[i], w);
match (r) {
case let o: io.off => {
assert(!(want[i] < 0));
assert(!((o: i64) != want[i]));
pos = o: i64;
};
case let e: io.error => {
assert(!(want[i] >= 0));
match (e) {
case errors.invalid => {};
case => abort();
};
};
};
let t: (io.off | io.error) = io.tell(s);
match (t) {
case let o: io.off => { assert(!((o: i64) != pos)); };
case let e: io.error => abort();
};
i += 1;
};
// Data readback through the repositioned cursor: SET 5 then read
// drains bytes 5..7, then eof.
let r: (io.off | io.error) = io.seek(s, 5, io.whence.SET);
match (r) {
case let o: io.off => { assert(!((o: i64) != 5)); };
case let e: io.error => abort();
};
let out: [8]u8;
let rd: (size | io.eof | io.error) = io.read(s, out[0:8]);
match (rd) {
case let nz: size => {
assert(!(nz: i32 != 3));
assert(!(out[0] != 15u8));
assert(!(out[2] != 17u8));
};
case io.eof => abort();
case let e: io.error => abort();
};
let rd2: (size | io.eof | io.error) = io.read(s, out[0:8]);
match (rd2) {
case io.eof => {};
case let nz: size => abort();
case let e: io.error => abort();
};
};
// ---- emptyseek: every whence over a 0-length buffer ----------------------
@test fn emptyseek() void = {
let arr: [1]u8;
arr[0] = 0u8;
let st: memio.stream = memio.fixed(arr[0:0]);
let s: io.stream = &st.vt;
let wh: [5]i32;
let off: [5]i64;
let want: [5]i64;
wh[0]=0; off[0]=0; want[0]=0; // SET 0 — only valid target
wh[1]=2; off[1]=0; want[1]=0; // END 0 == start
wh[2]=0; off[2]=1; want[2]=-1;
wh[3]=1; off[3]=-1; want[3]=-1;
wh[4]=2; off[4]=-1; want[4]=-1;
let i: i32 = 0;
for (i < 5) {
let w: io.whence = wh[i]: io.whence;
let r: (io.off | io.error) = io.seek(s, off[i], w);
match (r) {
case let o: io.off => {
assert(!(want[i] < 0));
assert(!((o: i64) != want[i]));
};
case let e: io.error => {
assert(!(want[i] >= 0));
match (e) {
case errors.invalid => {};
case => abort();
};
};
};
i += 1;
};
};
// ---- dynamicseek: rewind-and-reread over written data --------------------
// The dynamic vtable wires the same seekfn; bounds run against the
// LOGICAL length (m.len), not capacity (Hare: len(s.buf),
// ref/hare/memio/stream.ha:131,136).
@test fn dynamicseek() void = {
let st: memio.stream = memio.dynamic();
let s: io.stream = &st.vt;
let src: [5]u8;
src[0]=30u8; src[1]=31u8; src[2]=32u8; src[3]=33u8; src[4]=34u8;
let wr: (size | io.error) = io.write(s, src[0:5]);
match (wr) {
case let n: size => { assert(!(n: i32 != 5)); };
case let e: io.error => abort();
};
// END 0 → 5 (logical length); cap is 8 post-grow, so END 1 must
// still be invalid even though capacity would fit it.
let r: (io.off | io.error) = io.seek(s, 0, io.whence.END);
match (r) {
case let o: io.off => { assert(!((o: i64) != 5)); };
case let e: io.error => abort();
};
let r1: (io.off | io.error) = io.seek(s, 1, io.whence.END);
match (r1) {
case let o: io.off => abort();
case let e: io.error => {
match (e) {
case errors.invalid => {};
case => abort();
};
};
};
// CUR -2 → 3; read drains 33,34 then eof.
let r2: (io.off | io.error) = io.seek(s, -2, io.whence.CUR);
match (r2) {
case let o: io.off => { assert(!((o: i64) != 3)); };
case let e: io.error => abort();
};
let out: [4]u8;
let rd: (size | io.eof | io.error) = io.read(s, out[0:4]);
match (rd) {
case let nz: size => {
assert(!(nz: i32 != 2));
assert(!(out[0] != 33u8));
assert(!(out[1] != 34u8));
};
case io.eof => abort();
case let e: io.error => abort();
};
let rd2: (size | io.eof | io.error) = io.read(s, out[0:4]);
match (rd2) {
case io.eof => {};
case let nz: size => abort();
case let e: io.error => abort();
};
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};