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:
@@ -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;
|
||||
|
||||
@@ -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); };
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user