// 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...`. package fmt; import io; import memio; import os; import strings; import 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 — f64 // arm is only 8B and rides under the str payload). // // No `f32` arm: strconv ships no `f32tos` and there is no in-tree // caller. Callers with an `f32` cast at the call site (`myf: f64`), // mirroring how `i64` covers every int width today. Ship the `f32` // arm when the first in-tree caller needs it. export type formattable = (i64 | str | bool | rune | f64); // ---- 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; }; case let v: f64 => { // strconv.f64tos's static-buffer view is consumed // immediately by os.write; no intervening strconv // call against the same buffer between bind and write. let s: str = strconv.f64tos(v); let r: i64 = os.write(fd, s.ptr, s.len: u64); if (r < 0) { return r; }; total += r; }; }; 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; }; }; }; case let v: f64 => { // strconv.f64tos returns a static-buffer view — // emit to the sink before yielding control to the // next arg iteration so no sibling f64/i64 strconv // call clobbers the buffer mid-flight. let view: str = strconv.f64tos(v); let rs: (i32 | io.closed) = putbytes(s, view.ptr, view.len); 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 // _ pad rune (default space when : is seen) // ' ' sign SPACE // + sign PLUS // x X o b base // width (first char 1..9) // . 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); }; case let v: f64 => { // strconv.f64tos already prepends '-' for negative values; // peel it back off so [[signof]] can fold neg/plus/space // mods uniformly with the i64 arm. The view bytes are then // emitted directly — strconv's static buf is not held past // the putbytes call (no sibling strconv call lands between). // // Mods scope: width / alignment / pad / sign honored via // the same raw-then-pad machinery as i64. `prec` ignored — // strconv.f64tos is shortest-G with no precision knob; grows a // precision path when mods gains ffmt / fflags (the parametric // ftosf, strconv task #64). `base` ignored (Hare does too — // base is int-only). NaN/±Inf now render as "nan"/"infinity" // (Ryū graduation, fold-5); the uniform sign-peel below would // turn a SHOW_POS nan into "+nan" (a minor fmt-layer edge, not // strconv's — strconv emits a bare "nan"). let view: str = strconv.f64tos(v); let neg_flag: bool = false; if (view.len > 0 && view.ptr[0] == 45u8) { // '-' neg_flag = true; view.ptr = view.ptr + 1u64; view.len -= 1; }; 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 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; }; }; let z: io.closed; return z; // unreachable — match is exhaustive }; // rawlenf64 — bytes the raw render of `v` under `m` would emit. // Calls strconv.f64tos to count digits + decimal point; peels off // the natural '-' so signof's neg/plus/space byte is counted exactly // once, the same accounting [[rawleni64]] does. fn rawlenf64(v: f64, m: *mods) i32 = { let view: str = strconv.f64tos(v); let body: i32 = view.len; let had_neg: bool = false; if (body > 0 && view.ptr[0] == 45u8) { had_neg = true; body -= 1; }; let signlen: i32 = 0; if (signof(had_neg, m) != 0u8) { signlen = 1; }; return signlen + body; }; // 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; case let v: f64 => return rawlenf64(v, m); }; 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 v: f64 => { let a: formattable = v; 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 = alloc([], view.len: u64)!; 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 => {}; }; tight.len = view.len; return strings.frombytes(tight); };