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).
This commit is contained in:
2026-06-14 23:52:15 +09:00
parent 46ed712352
commit cab85f5bc9
6 changed files with 128 additions and 45 deletions

View File

@@ -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;

View File

@@ -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); };
};
};

View File

@@ -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; };

View File

@@ -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.
// (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;
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
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

View File

@@ -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; };

View File

@@ -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; };