Files
ww/lib/fmt/fmt.ww
Hojun-Cho f906081c8c lib/fmt+test: add asprintf (heap-allocated formatter)
Now that os.alloc/free ship (db2b05b), the heap-shape printf wrapper
that bb10ee7 deferred 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.
2026-05-16 10:06:09 +09:00

810 lines
26 KiB
Plaintext

// fmt — formatting writers. Mirrors Hare's lib/fmt subset.
//
// Hare's `fmt::fprint` takes `io::handle = (io::file | int)`, which
// ww doesn't yet have. So we ship two sinks side-by-side, with the
// distinction baked into the name:
//
// fprint / fprintln write to a [[io.stream]] — Hare's
// primary surface. Errors via `io.closed`.
// fdprint / fdprintln write to a raw fd via [[os.write]] — ww-
// specific. Errors via the raw `-errno` i64
// convention. Used by the process-stdio
// wrappers below (print/println/errorln/
// fatal) until lib/io grows an fd-backed
// stream; at that point both halves
// graduate "in one go" (lib/CLAUDE.md) and
// the fd-suffixed names disappear.
//
// The printf-family ({n}-placeholder parser) is provided alongside —
// see the "{n}-placeholder parser" section below. Same sink split:
// `fprintf` / `fprintfln` on streams; `fdprintf` / `fdprintfln` on
// raw fds; the process-stdio wrappers route through fd 1 / fd 2.
//
// Call sites take Hare's variadic shape: `fmt.println(42, "hi", true)`
// gathers the args into a `[]formattable` slice; wrappers forward
// via `args...`.
use io;
use memio;
use os;
use strconv;
// i64dec_buf — scratch buffer for [[i64dec]] below. Module-level
// because Hare's `strconv::i64tos` is a static-buffer view and we
// match that shape here. 21 bytes is enough for `-9223372036854775808`
// (20 digits + sign).
let i64dec_buf: [21]u8;
// i64dec — render `v` as a base-10 ASCII string into [[i64dec_buf]],
// returning a borrowed view. Preserved as a base-10 specialisation
// of strconv.i64tos for the pre-printf-family print path; the
// printf-family path below dispatches through strconv directly.
fn i64dec(v: i64) str = {
let neg: bool = false;
let n: i64 = v;
if (n < 0) { neg = true; n = -n; };
let tmp: [20]u8;
let i: i32 = 0;
if (n == 0) { tmp[0] = 48u8; i = 1; };
for (n > 0) {
let d: i64 = n % 10i64;
tmp[i] = (d + 48i64): u8;
n = n / 10i64;
i += 1;
};
let out: i32 = 0;
if (neg) { i64dec_buf[out] = 45u8; out += 1; }; // '-'
for (i > 0) {
i -= 1;
i64dec_buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = &i64dec_buf[0];
r.len = out;
return r;
};
// formattable — tagged union of types fmt can render. Mirrors Hare's
// `fmt::formattable = (...types::numeric | uintptr | str | rune |
// bool | nullable *opaque | void)`, narrowed to the set ww actually
// has codegen for. Slot size is 24B (8 tag + 16 str payload).
export type formattable = (i64 | str | bool | rune);
// ---- fd sinks --------------------------------------------------------
// fdprint — write the formatted form of each `args` element to `fd`,
// separated by spaces. Returns total bytes written or the first
// negative [[os.write]] result (Linux's `-errno`). Hare's separator-
// by-space matches.
//
// Renamed from `fprint` once lib/fmt grew an io.stream sink (`fprint`
// now points at that). This entry stays under `fd`-prefix until lib/io
// can express the full Hare `io::handle = (file | int)` union, at
// which point both halves graduate in one go.
export fn fdprint(fd: i32, args: formattable...) i64 = {
let total: i64 = 0;
let i: i32 = 0;
for (i < args.len) {
if (i > 0) {
let r: i64 = os.write(fd, " ".ptr, 1u64);
if (r < 0) { return r; };
total += r;
};
match (args[i]) {
case let n: i64 => {
let s: str = i64dec(n);
let r: i64 = os.write(fd, s.ptr, s.len: u64);
if (r < 0) { return r; };
total += r;
};
case let s: str => {
let r: i64 = os.write(fd, s.ptr, s.len: u64);
if (r < 0) { return r; };
total += r;
};
case let b: bool => {
let s: str = "false";
if (b) { s = "true"; };
let r: i64 = os.write(fd, s.ptr, s.len: u64);
if (r < 0) { return r; };
total += r;
};
case let r: rune => {
let buf: [4]u8;
buf[0] = r: u8;
let n: i64 = os.write(fd, &buf[0], 1u64);
if (n < 0) { return n; };
total += n;
};
};
i += 1;
};
return total;
};
// fdprintln — fdprint plus a trailing newline.
export fn fdprintln(fd: i32, args: formattable...) i64 = {
let n: i64 = fdprint(fd, args...);
if (n < 0) { return n; };
let m: i64 = os.write(fd, "\n".ptr, 1u64);
if (m < 0) { return m; };
return n + m;
};
// ---- stream sinks ----------------------------------------------------
// putbytes — internal helper that wraps (`*u8`, `i32`) into a `[]u8`
// slice and feeds it to [[io.write]]. Not exported: callers compose
// the same `(ptr, len)` triple as their underlying source (str view,
// strconv buffer, stack rune buffer), and the slice is invariant
// in shape across the formattable arms.
fn putbytes(s: *io.stream, p: *u8, n: i32) (i32 | io.closed) = {
let v: []u8;
v.ptr = p;
v.len = n;
return io.write(s, v);
};
// fprint — write the formatted form of each `args` element to `s`,
// separated by spaces. Returns total bytes written, or `io.closed`
// if the sink rejects mid-write. Mirrors Hare's `fmt::fprint` shape
// for an `io::handle` sink, modulo ww's i32-sized byte counters and
// the narrower `io.closed`-only error set on lib/io's stream vtable.
//
// A short write (sink accepts fewer bytes than asked) is reported by
// the returned count, not as an error — matches [[io.write]]'s contract
// per [[memio.fixedwrite]]. Callers that need write-all semantics layer
// it on top, the same way they do over raw [[io.write]].
export fn fprint(s: *io.stream, args: formattable...) (i32 | io.closed) = {
let total: i32 = 0;
let i: i32 = 0;
for (i < args.len) {
if (i > 0) {
let r: (i32 | io.closed) = putbytes(s, " ".ptr, 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
};
match (args[i]) {
case let n: i64 => {
let view: str = i64dec(n);
let r: (i32 | io.closed) = putbytes(s, view.ptr, view.len);
match (r) {
case let m: i32 => { total += m; };
case io.closed => { let c: io.closed; return c; };
};
};
case let v: str => {
let r: (i32 | io.closed) = putbytes(s, v.ptr, v.len);
match (r) {
case let m: i32 => { total += m; };
case io.closed => { let c: io.closed; return c; };
};
};
case let b: bool => {
let v: str = "false";
if (b) { v = "true"; };
let r: (i32 | io.closed) = putbytes(s, v.ptr, v.len);
match (r) {
case let m: i32 => { total += m; };
case io.closed => { let c: io.closed; return c; };
};
};
case let r: rune => {
let buf: [4]u8;
buf[0] = r: u8;
let rs: (i32 | io.closed) = io.write(s, buf[0:1]);
match (rs) {
case let m: i32 => { total += m; };
case io.closed => { let c: io.closed; return c; };
};
};
};
i += 1;
};
return total;
};
// fprintln — fprint plus a trailing newline. Mirrors Hare's
// `fmt::fprintln(io::handle, args...)`.
export fn fprintln(s: *io.stream, args: formattable...) (i32 | io.closed) = {
let total: i32 = 0;
let r1: (i32 | io.closed) = fprint(s, args...);
match (r1) {
case let n: i32 => { total = n; };
case io.closed => { let c: io.closed; return c; };
};
let r2: (i32 | io.closed) = putbytes(s, "\n".ptr, 1);
match (r2) {
case let m: i32 => { total += m; };
case io.closed => { let c: io.closed; return c; };
};
return total;
};
// ---- process-stdio wrappers -----------------------------------------
// print / println — fdprint / fdprintln on stdout. Direct counterparts
// of Hare's fmt::print / fmt::println.
export fn print(args: formattable...) i64 = {
return fdprint(1, args...);
};
export fn println(args: formattable...) i64 = {
return fdprintln(1, args...);
};
// errorln — fdprintln on stderr. Hare's `fmt::error` (without -ln) is
// skipped here: the bare `error` name collides with strconv's
// `type error = !(invalid | overflow)` under the driver's flat
// concatenation namespace. Callers wanting the no-newline form use
// `fdprint(2, args...)` directly.
export fn errorln(args: formattable...) i64 = {
return fdprintln(2, args...);
};
// fatal — errorln then exit(255). `never` return marks the bottom
// type so flow-control checks treat callers as terminated. The
// fdprintln result is dropped as an expression statement (Hare's
// `_ = fprintln(...)` wouldn't add safety here — process exit
// follows immediately).
export fn fatal(args: formattable...) never = {
fdprintln(2, args...);
os.exit(255);
};
// ---- {n}-placeholder parser -----------------------------------------
//
// Mirrors ref/hare/fmt/{iter,print,wrappers}.ha. Format sequences:
//
// {} implicit-positional next arg
// {N} explicit-positional arg N
// {:mods} inline modifiers on next arg
// {N:mods} inline modifiers on arg N
// {{ }} literal '{' / '}'
//
// Modifier set (subset of iter.ha:129 scan_modifiers):
//
// - align LEFT
// = align CENTER
// _<rune> pad rune (default space when : is seen)
// ' ' sign SPACE
// + sign PLUS
// x X o b base
// <digits> width (first char 1..9)
// .<digits> precision
//
// Not v1: '%' parametric form ({0%1}); float modifiers (e/f/g/F…);
// void / null arms (not in formattable). `asprintf` is deferred until
// lib/os exports alloc (task #14). Invalid format aborts via os.exit
// — Hare uses `abort()`; same effect.
// neg, alignment — mirror iter.ha:19 / iter.ha:26.
export type neg = enum i32 { NONE = 0, SPACE = 1, PLUS = 2 };
export type alignment = enum i32 { RIGHT = 0, CENTER = 1, LEFT = 2 };
// mods — per-placeholder modifier set. Mirrors iter.ha:33 minus
// `ffmt` / `fflags` — formattable has no float arm, so the float-
// formatter knobs would be dead. Graduate when fmt.formattable does.
export type mods = struct {
alignment: alignment,
pad: rune,
neg: neg,
width: i32,
prec: i32,
base: strconv.base,
};
// field — variadic arg slot. `(...formattable | *mods)` per iter.ha:11.
// The *mods arm is the parametric-modifier form ({0%1}); accepted by
// the type checker today, but the '%' parser branch is deferred —
// passing a *mods that the parser reaches will abort the program.
export type field = (...formattable | *mods);
// fmtabort — invalid format string. Matches Hare's abort() (iter.ha:71)
// in effect; exit code 255 matches [[fatal]].
fn fmtabort() never = { os.exit(255); };
// modsinit — reset `m` to the all-zero default. pad stays 0 here;
// scan_modifiers (Hare iter.ha:130) defaults it to ' ' only when ':'
// is seen — bare `{}` never reaches the padding loop (width=0).
fn modsinit(m: *mods) void = {
m.alignment = alignment.RIGHT;
m.pad = 0: rune;
m.neg = neg.NONE;
m.width = 0;
m.prec = 0;
m.base = strconv.base.DEFAULT;
};
// scandigits — consume a digit run at *pos in `s`, advancing *pos
// past it; return the value. Aborts on no digits or overflow past
// i32. Mirrors iter.ha:173 scan_sz.
fn scandigits(s: str, pos: *i32) i32 = {
let v: i32 = 0;
let any: bool = false;
for (*pos < s.len) {
let c: u8 = s[*pos];
if (c < 48u8 || c > 57u8) {
if (!any) { fmtabort(); };
return v;
};
any = true;
if (v > 214748364) { fmtabort(); };
v = v * 10 + (c - 48u8): i32;
*pos += 1;
};
if (!any) { fmtabort(); };
return v;
};
// scanmods — parse the `:`-modifier run starting at *pos. *pos is on
// the first byte after `:`. Returns with *pos on the closing `}`.
// Mirrors iter.ha:129 scan_modifiers.
fn scanmods(s: str, pos: *i32, m: *mods) void = {
m.pad = 32: rune; // ' ' — Hare iter.ha:130
for (*pos < s.len) {
let c: u8 = s[*pos];
if (c == 125u8) { return; }; // '}'
*pos += 1;
if (c == 45u8) { m.alignment = alignment.LEFT; } // '-'
else if (c == 61u8) { m.alignment = alignment.CENTER; } // '='
else if (c == 95u8) { // '_'
if (*pos >= s.len) { fmtabort(); };
m.pad = s[*pos]: rune;
*pos += 1;
}
else if (c == 32u8) { m.neg = neg.SPACE; } // ' '
else if (c == 43u8) { m.neg = neg.PLUS; } // '+'
else if (c == 120u8) { m.base = strconv.base.HEX_LOWER; } // 'x'
else if (c == 88u8) { m.base = strconv.base.HEX_UPPER; } // 'X'
else if (c == 111u8) { m.base = strconv.base.OCT; } // 'o'
else if (c == 98u8) { m.base = strconv.base.BIN; } // 'b'
else if (c == 46u8) { // '.'
m.prec = scandigits(s, pos);
}
else if (c >= 49u8 && c <= 57u8) { // '1'..'9'
*pos -= 1;
m.width = scandigits(s, pos);
}
else { fmtabort(); };
};
fmtabort(); // ran off end without '}'
};
// digitsu64 — count base-b digits of `v`. Mirrors the explicit-loop
// shape strconv.u64tos uses internally; avoids materialising the
// digit string twice in the formatone width path.
fn digitsu64(v: u64, b: i64) i32 = {
if (v == 0u64) { return 1; };
let n: i32 = 0;
let nb: u64 = b: u64;
let x: u64 = v;
for (x > 0u64) {
n += 1;
x = x / nb;
};
return n;
};
// basenum — copy of strconv's internal basenum, lifted here because
// strconv keeps it private. Same shape as strconv.ww:40.
fn basenum(b: strconv.base) i64 = {
if (b == strconv.base.BIN) { return 2; };
if (b == strconv.base.OCT) { return 8; };
if (b == strconv.base.HEX) { return 16; };
if (b == strconv.base.HEX_UPPER) { return 16; };
if (b == strconv.base.HEX_LOWER) { return 16; };
return 10;
};
// signof — sign byte for `v` under `m.neg`, or 0u8 if none.
fn signof(neg_flag: bool, m: *mods) u8 = {
if (neg_flag) { return 45u8; }; // '-'
if (m.neg == neg.PLUS) { return 43u8; }; // '+'
if (m.neg == neg.SPACE) { return 32u8; }; // ' '
return 0u8;
};
// rawleni64 — bytes the raw render of `v` under `m` would emit. Used
// for width-alignment without rendering twice.
fn rawleni64(v: i64, m: *mods) i32 = {
let neg_flag: bool = v < 0;
let u: u64 = v: u64;
if (neg_flag) { u = (-v): u64; };
let signlen: i32 = 0;
if (signof(neg_flag, m) != 0u8) { signlen = 1; };
let dlen: i32 = digitsu64(u, basenum(m.base));
let inner: i32 = dlen;
if (m.prec > signlen + dlen) { inner = m.prec - signlen; };
return signlen + inner;
};
// rawlenstr — bytes the raw render of `s` would emit (after `prec`
// truncation, per Hare print.ha:86).
fn rawlenstr(s: str, m: *mods) i32 = {
if (m.prec > 0 && m.prec < s.len) { return m.prec; };
return s.len;
};
// formatraw — write the bare value (no width padding) to `s`.
// Mirrors print.ha:76 format_raw, narrowed to the formattable arms
// fmt.formattable carries today.
fn formatraw(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = {
match (arg) {
case let v: i64 => {
let neg_flag: bool = v < 0;
let u: u64 = v: u64;
if (neg_flag) { u = (-v): u64; };
let sb: u8 = signof(neg_flag, m);
let total: i32 = 0;
if (sb != 0u8) {
let buf: [1]u8;
buf[0] = sb;
let r: (i32 | io.closed) = putbytes(out, &buf[0], 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
};
let dlen: i32 = digitsu64(u, basenum(m.base));
let signlen: i32 = 0;
if (sb != 0u8) { signlen = 1; };
let pad0: i32 = 0;
if (m.prec > signlen + dlen) { pad0 = m.prec - signlen - dlen; };
let pi: i32 = 0;
for (pi < pad0) {
let buf: [1]u8;
buf[0] = 48u8; // '0'
let r: (i32 | io.closed) = putbytes(out, &buf[0], 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
pi += 1;
};
let view: str = strconv.u64tos(u, m.base);
let r: (i32 | io.closed) = putbytes(out, view.ptr, view.len);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
return total;
};
case let v: str => {
let n: i32 = rawlenstr(v, m);
return putbytes(out, v.ptr, n);
};
case let b: bool => {
let v: str = "false";
if (b) { v = "true"; };
return putbytes(out, v.ptr, v.len);
};
case let r: rune => {
let buf: [4]u8;
buf[0] = r: u8;
return putbytes(out, &buf[0], 1);
};
};
let z: io.closed; return z; // unreachable — match is exhaustive
};
// rawlen — bytes formatraw would emit for `arg` under `m`. Used by
// formatone's width-alignment path (Hare's print.ha:53 calls
// format_raw with io::empty for the same purpose).
fn rawlen(arg: formattable, m: *mods) i32 = {
match (arg) {
case let v: i64 => return rawleni64(v, m);
case let v: str => return rawlenstr(v, m);
case let b: bool => { if (b) { return 4; }; return 5; };
case let r: rune => return 1;
};
return 0; // unreachable — match is exhaustive
};
// formatone — render `arg` to `s` with `m`'s width / alignment / pad
// applied. Mirrors print.ha:45 format.
fn formatone(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = {
let start: i32 = 0;
if (m.width > 0 && m.alignment != alignment.LEFT) {
let raw: i32 = rawlen(arg, m);
let pad: i32 = 0;
if (raw < m.width) { pad = m.width - raw; };
if (m.alignment == alignment.CENTER) { start = (pad + 1) / 2; }
else { start = pad; };
};
let total: i32 = 0;
let i: i32 = 0;
let padb: [1]u8;
padb[0] = m.pad: u8;
for (i < start) {
let r: (i32 | io.closed) = putbytes(out, &padb[0], 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
i += 1;
};
let r1: (i32 | io.closed) = formatraw(out, arg, m);
match (r1) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
// Tail-pad: drive with a counter (mirrors the start-pad shape
// above). ww's memio.fixed reports a full buffer as a 0-byte
// partial write, not io.closed (unlike Hare's errors::overflow
// + `?` shape at print.ha:69); looping on `total < m.width`
// would spin on bsprintf when the sink runs out of room.
let need: i32 = 0;
if (m.width > total) { need = m.width - total; };
let j: i32 = 0;
for (j < need) {
let r: (i32 | io.closed) = putbytes(out, &padb[0], 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let c: io.closed; return c; };
};
j += 1;
};
return total;
};
// formatfield — dispatch on the `field` slot directly. A
// `field→formattable` widen helper called from fprintf's for-loop
// would trip task #18 (silent miscompile of 24B return-by-value in
// for-loop context); inline-per-arm sidesteps it and is independently
// the cleaner shape.
fn formatfield(out: *io.stream, f: field, m: *mods) (i32 | io.closed) = {
match (f) {
case let v: i64 => {
let a: formattable = v;
return formatone(out, a, m);
};
case let v: str => {
let a: formattable = v;
return formatone(out, a, m);
};
case let b: bool => {
let a: formattable = b;
return formatone(out, a, m);
};
case let r: rune => {
let a: formattable = r;
return formatone(out, a, m);
};
case let p: *mods => { fmtabort(); let c: io.closed; return c; };
};
};
// fprintf — Hare's primary printf-family surface. Mirrors print.ha:26.
// Returns total bytes written or io.closed on the first sink failure.
export fn fprintf(s: *io.stream, fmt: str, args: field...) (i32 | io.closed) = {
let total: i32 = 0;
let i: i32 = 0;
let nextimpl: i32 = 0;
let checkunused: bool = true;
for (i < fmt.len) {
let c: u8 = fmt[i];
if (c == 123u8) { // '{'
i += 1;
if (i >= fmt.len) { fmtabort(); };
if (fmt[i] == 123u8) { // '{{' literal
let r: (i32 | io.closed) = putbytes(s, fmt.ptr + i: u64, 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let cl: io.closed; return cl; };
};
i += 1;
} else {
let idx: i32 = 0;
let d: u8 = fmt[i];
if (d >= 48u8 && d <= 57u8) {
checkunused = false;
idx = scandigits(fmt, &i);
} else {
idx = nextimpl;
nextimpl += 1;
};
let m: mods;
modsinit(&m);
if (i < fmt.len && fmt[i] == 58u8) { // ':'
i += 1;
scanmods(fmt, &i, &m);
};
if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); };
i += 1;
if (idx >= args.len) { fmtabort(); };
let r: (i32 | io.closed) = formatfield(s, args[idx], &m);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let cl: io.closed; return cl; };
};
};
} else if (c == 125u8) { // '}'
i += 1;
if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); };
let r: (i32 | io.closed) = putbytes(s, fmt.ptr + i: u64, 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let cl: io.closed; return cl; };
};
i += 1;
} else {
let r: (i32 | io.closed) = putbytes(s, fmt.ptr + i: u64, 1);
match (r) {
case let n: i32 => { total += n; };
case io.closed => { let cl: io.closed; return cl; };
};
i += 1;
};
};
if (checkunused && nextimpl != args.len) { fmtabort(); };
return total;
};
// fprintfln — fprintf plus a trailing newline. Mirrors wrappers.ha:69.
export fn fprintfln(s: *io.stream, fmt: str, args: field...) (i32 | io.closed) = {
let total: i32 = 0;
let r1: (i32 | io.closed) = fprintf(s, fmt, args...);
match (r1) {
case let n: i32 => { total = n; };
case io.closed => { let c: io.closed; return c; };
};
let r2: (i32 | io.closed) = putbytes(s, "\n".ptr, 1);
match (r2) {
case let m: i32 => { total += m; };
case io.closed => { let c: io.closed; return c; };
};
return total;
};
// ---- fd-sink printf twins -------------------------------------------
//
// Hare's wrappers.ha:10 routes printf through os::stdout (an
// io::handle). ww has no fd-backed stream yet, so the fd path here
// wraps the fd in a stack-resident io.stream and dispatches through
// fprintf. -errno from [[os.write]] collapses to io.closed inside the
// wrapper; the public i64 return signals success-bytes vs -1 failure.
// Symmetric with the fdprint / fdprintln pair above.
fn fdsinkread(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = {
let e: io.eof; return e;
};
fn fdsinkwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
let fdp: *i32 = s.ctx: *i32;
let r: i64 = os.write(*fdp, buf.ptr, buf.len: u64);
if (r < 0) { let c: io.closed; return c; };
return r: i32;
};
fn fdsinkclose(s: *io.stream) (void | io.closed) = { return; };
fn fdwrap(fd: *i32, s: *io.stream) void = {
s.ctx = fd: *void;
s.read = fdsinkread;
s.write = fdsinkwrite;
s.close = fdsinkclose;
};
// fdprintf / fdprintfln — fprintf / fprintfln over a raw fd. Failure
// (any underlying os.write returning -errno) collapses to -1; success
// returns total bytes written. See header comment.
export fn fdprintf(fd: i32, fmt: str, args: field...) i64 = {
let f: i32 = fd;
let s: io.stream;
fdwrap(&f, &s);
let r: (i32 | io.closed) = fprintf(&s, fmt, args...);
match (r) {
case let n: i32 => return n: i64;
case io.closed => return -1i64;
};
};
export fn fdprintfln(fd: i32, fmt: str, args: field...) i64 = {
let f: i32 = fd;
let s: io.stream;
fdwrap(&f, &s);
let r: (i32 | io.closed) = fprintfln(&s, fmt, args...);
match (r) {
case let n: i32 => return n: i64;
case io.closed => return -1i64;
};
};
// ---- process-stdio printf wrappers ----------------------------------
//
// printf / printfln — wrappers.ha:10 / :15 on os.stdout (fd 1).
// errorfln — wrappers.ha:24 on stderr. Hare ships `errorf` too, but
// the bare `error` name collides with strconv.error under the
// driver's flat-scope concat (same divergence the existing [[errorln]]
// notes at line 234); ship the -ln form only and document.
// fatalf — wrappers.ha:54 errorfln-then-exit(255).
export fn printf(fmt: str, args: field...) i64 = {
return fdprintf(1, fmt, args...);
};
export fn printfln(fmt: str, args: field...) i64 = {
return fdprintfln(1, fmt, args...);
};
export fn errorfln(fmt: str, args: field...) i64 = {
return fdprintfln(2, fmt, args...);
};
export fn fatalf(fmt: str, args: field...) never = {
fdprintfln(2, fmt, args...);
os.exit(255);
};
// bsprintf — render into a caller-supplied buffer through memio.fixed;
// return the str view of the bytes actually written. Mirrors
// wrappers.ha:42. io.closed propagates as-is (Hare's `nomem` arm). On
// short writes the returned str is just the prefix that fit — Hare's
// memio.fixed contract is the same.
export fn bsprintf(buf: []u8, fmt: str, args: field...) (str | io.closed) = {
let m: memio.state;
let s: io.stream;
memio.fixed(&m, &s, buf);
let r: (i32 | io.closed) = fprintf(&s, fmt, args...);
match (r) {
case let n: i32 => { return memio.string(&m); };
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;
};