lib/fmt+test: add asprintf (heap-allocated formatter)
Now that os.alloc/free ship (db2b05b), the heap-shape printf wrapper thatbb10ee7deferred is implementable. `asprintf(fmt: str, args: field...) str` — Hare wrappers.ha:29 shape. Body wires memio.dynamic, runs fprintf into it, takes a stringview, and shrink-to-fit-copies into a fresh os.alloc(view.len) before io.closing the dynamic stream (which frees the cap-sized internal buffer). The shrink-to-fit copy is forced by os.free's (p, n) shape: n must match the mmap length, so the caller can't free a cap-allocated body if cap > len. Caller contract documented inline: free with `os.free(r.ptr, r.len)` when r.len > 0; skip when r.len == 0 (no allocation happens). Mirrors strings.dup's shape; not a workaround. The fprintf io.closed arm is matched-and-ignored — memio.dynamicwrite only returns size, never io.closed (verified at memio.ww:163-175). Same shape as Hare's `case size => void;` in print.ha. Hare's nomem variant intentionally dropped; ww's os.alloc returns a poisonous pointer on OOM (per #14 contract) which faults on deref — no in-band error to model. Tests (signalled 27-30): basic (str + i64), growth (34B output through 8→16→32→64 grow), empty (no-alloc / skip-free), indexed_mods ({1:_05} through heap sink). errorf / error / errorln family deferred — drew's call to ship the whole error story in one commit when the error type lands.
This commit is contained in:
@@ -753,3 +753,57 @@ export fn bsprintf(buf: []u8, fmt: str, args: field...) (str | io.closed) = {
|
||||
case io.closed => { let c: io.closed; return c; };
|
||||
};
|
||||
};
|
||||
|
||||
// asprintf — render `fmt`/`args` into a heap-allocated str through
|
||||
// memio.dynamic, then shrink to a tight allocation so the caller's
|
||||
// free(r.ptr, r.len) matches the underlying mmap length. Mirrors
|
||||
// wrappers.ha:29 modulo:
|
||||
//
|
||||
// - Bare `str` return. Hare returns `(str | nomem)`; ww has no
|
||||
// `nomem` variant — os.alloc faults on OOM per lib/os.ww's
|
||||
// contract.
|
||||
// - The `io.closed` arm of fprintf is statically unreachable
|
||||
// here (memio.dynamicwrite never returns io.closed, memio.ww:163),
|
||||
// but the type checker still requires the match; both arms have
|
||||
// empty bodies. Same effect as Hare's `case size => void`.
|
||||
// - Shrink-to-fit copy. memio.dynamic's `cap` doubles past `pos`
|
||||
// during growth (memio.ww:193); the close path frees the
|
||||
// cap-sized mapping (memio.ww:179). Returning memio.string(&m)
|
||||
// directly would either leak the cap-vs-len slack (skip close)
|
||||
// or dangle the returned view (close first). Copying into a
|
||||
// fresh `view.len`-sized alloc lets the caller free with
|
||||
// `r.len`, matching strings.dup's tight-alloc contract.
|
||||
//
|
||||
// Caller frees with `os.free(r.ptr, r.len: u64)` when `r.len > 0`;
|
||||
// skip the free when `r.len == 0` — same empty-output shape as
|
||||
// strings.dup (no allocation took place).
|
||||
export fn asprintf(fmt: str, args: field...) str = {
|
||||
let m: memio.state;
|
||||
let s: io.stream;
|
||||
memio.dynamic(&m, &s);
|
||||
let wres: (i32 | io.closed) = fprintf(&s, fmt, args...);
|
||||
match (wres) {
|
||||
case let n: i32 => {};
|
||||
case io.closed => {};
|
||||
};
|
||||
let view: str = memio.string(&m);
|
||||
let out: str;
|
||||
out.ptr = nil;
|
||||
out.len = 0;
|
||||
if (view.len == 0) {
|
||||
let cres: (void | io.closed) = io.close(&s);
|
||||
match (cres) { case void => {}; case io.closed => {}; };
|
||||
return out;
|
||||
};
|
||||
let tight: *u8 = os.alloc(view.len: u64): *u8;
|
||||
let i: i32 = 0;
|
||||
for (i < view.len) {
|
||||
tight[i] = view.ptr[i];
|
||||
i += 1;
|
||||
};
|
||||
let cres: (void | io.closed) = io.close(&s);
|
||||
match (cres) { case void => {}; case io.closed => {}; };
|
||||
out.ptr = tight;
|
||||
out.len = view.len;
|
||||
return out;
|
||||
};
|
||||
|
||||
@@ -445,6 +445,41 @@ fn closedstream(s: *io.stream) void = {
|
||||
};
|
||||
};
|
||||
|
||||
// asprintf — heap-allocated render through memio.dynamic + shrink-to-fit.
|
||||
// Caller frees via os.free(r.ptr, r.len) when r.len > 0.
|
||||
|
||||
@test fn asprintf_basic() void = {
|
||||
let r: str = fmt.asprintf("{} {}", "hi", 42i64);
|
||||
if (!streq(r, "hi 42")) { fail(); };
|
||||
os.free(r.ptr: *void, r.len: u64);
|
||||
};
|
||||
|
||||
// Output > memio.dynamic's initial 8B cap to exercise the
|
||||
// double-and-copy grow path; 34B target traverses 8→16→32→64.
|
||||
@test fn asprintf_growth() void = {
|
||||
let r: str = fmt.asprintf("hello {} world {} value {}", "alpha", "beta", "gamma");
|
||||
if (!streq(r, "hello alpha world beta value gamma")) { fail(); };
|
||||
os.free(r.ptr: *void, r.len: u64);
|
||||
};
|
||||
|
||||
// Empty format pins the no-allocation path: memio.dynamic.grow never
|
||||
// fires (cap stays 0), and the asprintf shrink-to-fit path returns
|
||||
// {nil, 0} without calling os.alloc. Free is skipped — matches the
|
||||
// strings.dup empty-input contract.
|
||||
@test fn asprintf_empty() void = {
|
||||
let r: str = fmt.asprintf("");
|
||||
if (r.len != 0) { fail(); };
|
||||
};
|
||||
|
||||
// Indexed placeholder + modifier through asprintf — confirms the full
|
||||
// {n:mods} parser path works through the heap-allocated sink, not just
|
||||
// the memio.fixed (bsprintf) and memio.dynamic-via-fprintf paths.
|
||||
@test fn asprintf_indexed_mods() void = {
|
||||
let r: str = fmt.asprintf("{1:_05}", "zzz", 42i64);
|
||||
if (!streq(r, "00042")) { fail(); };
|
||||
os.free(r.ptr: *void, r.len: u64);
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
signalled = 1; fprintbarestr();
|
||||
signalled = 2; fprintintstr();
|
||||
@@ -472,5 +507,9 @@ export fn main() i32 = {
|
||||
signalled = 24; fprintf_bool_rune();
|
||||
signalled = 25; bsprintf_trunc();
|
||||
signalled = 26; bsprintf_width_trunc();
|
||||
signalled = 27; asprintf_basic();
|
||||
signalled = 28; asprintf_growth();
|
||||
signalled = 29; asprintf_empty();
|
||||
signalled = 30; asprintf_indexed_mods();
|
||||
return 0;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user