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