diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index 347ce2e2..e2228eb0 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -551,10 +551,11 @@ fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { }; // formatone — render `arg` to `s` with `m`'s width / alignment / pad -// applied. Mirror print.ha:45 format. The tail-pad loop drives on a -// counter because putbytes over memio.fixed reports a full buffer as a -// 0-byte partial write, not io.error — looping on `total < m.width` -// would spin on bsprintf when the sink runs out. +// applied. Mirror print.ha:45 format, modulo the tail-pad loop: it +// drives on a `need` counter rather than Hare's `total < m.width` form. +// memio.fixedwrite now returns nomem on a full sink, so the width form +// would terminate too; restoring it is a deferred follow-up — kept +// counter-driven here to stay in F-R scope. fn formatone(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { let start: i32 = 0; if (m.width > 0 && m.alignment != alignment.LEFT) { @@ -765,8 +766,9 @@ export fn fprintfln(s: io.handle, fmt: str, args: field...) (size | io.error) = // the `stream` BY VALUE — the stream lives in this frame; `&st.vt` is // the io.stream and `&st` the accessor handle. Hare returns // `(const str | nomem)`; ww collapses to (str | io.error) so the -// fprintf path's io.error arm stays uniform. Short writes surface as a -// prefix (memio.fixed contract). +// fprintf path's io.error arm stays uniform. Truncation surfaces as +// nomem, not a prefix: memio.fixedwrite returns nomem once the sink +// fills (ref/hare/fmt/wrappers.ha:50), and the io.error arm forwards it. export fn bsprintf(buf: []u8, fmt: str, args: field...) (str | io.error) = { let st: memio.stream = memio.fixed(buf); let s: io.stream = &st.vt; diff --git a/lib/fmt/fmttest.ww b/lib/fmt/fmttest.ww index 3766bc8d..03f895ea 100644 --- a/lib/fmt/fmttest.ww +++ b/lib/fmt/fmttest.ww @@ -452,6 +452,19 @@ fn errsource() io.stream = { }; }; +// Positive control: an EXACT-fit render must still succeed — the sink +// fills to the last byte but no write lands on a full sink, so the +// nomem guard (memio.ww:190) must not fire. Guards against the +// truncation fix over-rejecting a render that exactly fits. +@test fn bsprintf_exact() void = { + let buf: [5]u8; + let r: (str | io.error) = fmt.bsprintf(buf[0:5], "hello"); + match (r) { + case let s: str => { assert(!(!streq(s, "hello"))); }; + case let eioe: io.error => abort(); + }; +}; + // Pins the formatfield bool / rune arms inside fprintf's for-loop — // the codegen-smell repro shape (task #18). Pre-task-#18, the str arm // is also exercised by fprintf_implicit; this adds the two remaining @@ -470,30 +483,43 @@ fn errsource() io.stream = { match (c) { case void => {}; case let eioe: io.error => abort(); }; }; -// Verifies bsprintf's documented truncation contract — short writes -// return the prefix that fit (memio.fixedwrite returns 0 once full, -// not io.error). Pairs with fprintfixedshort above which covers the -// underlying fprint path. +// Verifies bsprintf's truncation contract — overflowing a fixed sink +// surfaces nomem, not a prefix (memio.fixedwrite returns nomem once +// full, ref/hare/fmt/wrappers.ha:50). Pairs with fprintfixedshort +// above which covers the partial-write (short i32) path. @test fn bsprintf_trunc() void = { let buf: [3]u8; let r: (str | io.error) = fmt.bsprintf(buf[0:3], "{} {}", "hi", 42i64); match (r) { - case let s: str => { assert(!(!streq(s, "hi "))); }; - case let eioe: io.error => abort(); + case let s: str => abort(); + case let eioe: io.error => { assert(eioe is nomem); }; }; }; -// Pins formatone's tail-pad counter shape. Without the counter, the -// `total < m.width` loop spins on memio.fixed's 0-byte-on-full -// partial-write contract (ww divergence from Hare's errors::overflow -// + `?`). Buffer 3B, target " 42" (5B); first 3 spaces fit, then -// formatraw + tail-pad both write 0 — must terminate. +// Truncation through the width/pad path also surfaces nomem. Buffer +// 3B, target " 42" (5B): the 3 lead pad spaces fill the sink, then +// formatraw writes "42" into the full sink → nomem. Also pins +// formatone's tail-pad counter (the loop must not spin on the full +// sink before the error propagates). @test fn bsprintf_width_trunc() void = { let buf: [3]u8; let r: (str | io.error) = fmt.bsprintf(buf[0:3], "{:5}", 42i64); match (r) { - case let s: str => { assert(!(!streq(s, " "))); }; - case let eioe: io.error => abort(); + case let s: str => abort(); + case let eioe: io.error => { assert(eioe is nomem); }; + }; +}; + +// Empty-sink discriminator: the cleanest single-write red/green split. +// The first non-empty write to a zero-length sink hits the full-sink +// guard → nomem; pre-fix it returned 0 and bsprintf yielded "" as a +// success. +@test fn bsprintf_empty() void = { + let buf: [0]u8; + let r: (str | io.error) = fmt.bsprintf(buf[0:0], "x"); + match (r) { + case let s: str => abort(); + case let eioe: io.error => { assert(eioe is nomem); }; }; }; diff --git a/lib/memio/memio.ww b/lib/memio/memio.ww index 46157356..6431fe5a 100644 --- a/lib/memio/memio.ww +++ b/lib/memio/memio.ww @@ -70,10 +70,9 @@ export type stream = struct { }; // fixed — wire a stream over a caller-supplied buffer. Writes never -// grow; they return 0 once `pos` reaches the end of the buffer (Hare -// returns `nomem` here; ww surfaces 0 — graduating to Hare's `nomem` -// return needs the widen-from-bare-nomem path that #173-family work -// gates). +// grow; a write to a full buffer returns nomem, matching Hare +// (ref/hare/memio/stream.ha:44,161). A zero-length write always +// succeeds with 0 (the empty-input short-circuit, stream.ha:157). // // Mirrors ref/hare/memio/stream.ha:46. export fn fixed(buf: []u8) stream = { @@ -187,7 +186,12 @@ fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = { fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = { let m: *stream = s: *stream; - if (m.pos >= m.len) { return 0: size; }; + if (buf.len == 0) { return 0: size; }; // ref/hare/memio/stream.ha:157 + if (m.pos >= m.len) { // ref/hare/memio/stream.ha:161 + let nm: nomem; + let e: io.error = nm; + return e; + }; let space: i32 = m.len - m.pos; let n: i32 = buf.len; if (space < n) { n = space; }; diff --git a/lib/memio/memiotest.ww b/lib/memio/memiotest.ww index be175230..90298110 100644 --- a/lib/memio/memiotest.ww +++ b/lib/memio/memiotest.ww @@ -85,15 +85,17 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { let src: [32]u8; let _: i32 = putstr("hello world!!XXXXXXXY", src[0:32], 0); - // (srcoff, inlen, wantn) — wantn diverges from inlen on the - // partial and overflow rows. - let off: [4]i32; - let ln: [4]i32; - let wantn: [4]i32; - off[0]=0; ln[0]=6; wantn[0]=6; // full fit - off[1]=6; ln[1]=7; wantn[1]=7; // exact-fills cap (pos=13) - off[2]=13; ln[2]=7; wantn[2]=3; // partial: 3 free - off[3]=20; ln[3]=1; wantn[3]=0; // overflow: 0 free → 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) { @@ -101,8 +103,14 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { 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 != wantn[i])); }; - case let e: io.error => abort(); + 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; }; @@ -122,6 +130,41 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { 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 diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 02b6fc5c..4a634ff2 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17591,10 +17591,9 @@ export type stream = struct { }; // fixed — wire a stream over a caller-supplied buffer. Writes never -// grow; they return 0 once `pos` reaches the end of the buffer (Hare -// returns `nomem` here; ww surfaces 0 — graduating to Hare's `nomem` -// return needs the widen-from-bare-nomem path that #173-family work -// gates). +// grow; a write to a full buffer returns nomem, matching Hare +// (ref/hare/memio/stream.ha:44,161). A zero-length write always +// succeeds with 0 (the empty-input short-circuit, stream.ha:157). // // Mirrors ref/hare/memio/stream.ha:46. export fn fixed(buf: []u8) stream = { @@ -17708,7 +17707,12 @@ fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = { fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = { let m: *stream = s: *stream; - if (m.pos >= m.len) { return 0: size; }; + if (buf.len == 0) { return 0: size; }; // ref/hare/memio/stream.ha:157 + if (m.pos >= m.len) { // ref/hare/memio/stream.ha:161 + let nm: nomem; + let e: io.error = nm; + return e; + }; let space: i32 = m.len - m.pos; let n: i32 = buf.len; if (space < n) { n = space; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ba68f0a2..8c5374f4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17591,10 +17591,9 @@ export type stream = struct { }; // fixed — wire a stream over a caller-supplied buffer. Writes never -// grow; they return 0 once `pos` reaches the end of the buffer (Hare -// returns `nomem` here; ww surfaces 0 — graduating to Hare's `nomem` -// return needs the widen-from-bare-nomem path that #173-family work -// gates). +// grow; a write to a full buffer returns nomem, matching Hare +// (ref/hare/memio/stream.ha:44,161). A zero-length write always +// succeeds with 0 (the empty-input short-circuit, stream.ha:157). // // Mirrors ref/hare/memio/stream.ha:46. export fn fixed(buf: []u8) stream = { @@ -17708,7 +17707,12 @@ fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = { fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = { let m: *stream = s: *stream; - if (m.pos >= m.len) { return 0: size; }; + if (buf.len == 0) { return 0: size; }; // ref/hare/memio/stream.ha:157 + if (m.pos >= m.len) { // ref/hare/memio/stream.ha:161 + let nm: nomem; + let e: io.error = nm; + return e; + }; let space: i32 = m.len - m.pos; let n: i32 = buf.len; if (space < n) { n = space; };