// fmt — formatting writers. Mirrors Hare's lib/fmt subset (primary // surface ref/hare/fmt/print.ha:13). Project #94 fold-eFinal; io // fold-2 (#5) graduated the sink to [[io.handle]]. package fmt; import io; import encoding.utf8; import memio; import os; import strings; import strconv; // 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 has the primitive, but there is no in-tree fmt // caller and adding a tagged-union arm is a public ABI change. Callers // cast to f64 until consumer evidence justifies widening this surface. // // `int`/`uint` appended LAST: variant tags follow declaration order // (cstage cg_tag_for_variant / wwstage flatvariantidxt), so the // pre-existing tags i64=0,str=1,bool=2,rune=3,f64=4 stay frozen and // int=5,uint=6 — zero byte-id churn for existing callers (#6). This is a // STAGED step toward Hare's full `types::numeric` (ref/hare/types/ // classes.ha:5-17 → ref/hare/fmt/iter.ha:14): int/uint are the machine- // word types apps actually print; narrower widths (i8/i16/i32, u8/u16/ // u32, size) graduate when a caller lands. Making them real members (not // a size/name-keyed coercion) closes the #128 int-path leniency by // construction — both stages now accept bare int by MEMBERSHIP. export type formattable = (i64 | str | bool | rune | f64 | int | uint); fn putbytes(s: io.handle, p: *u8, n: i32) (size | io.error) = { let off: i32 = 0; for (off < n) { let v: []u8; v.ptr = p + (off: u64); v.len = n - off; v.cap = v.len; match (io.write(s, v)) { case let z: size => { assert(z <= (v.len: size), "fmt.putbytes: writer returned an oversized count"); if (z == 0) { let nm: nomem; let e: io.error = nm; return e; }; off += z: i32; }; case let e: io.error => return e; }; }; return n: size; }; fn writeone(s: io.handle, a: formattable) (size | io.error) = { match (a) { case let n: i64 => { let v: str = strconv.i64tos(n, strconv.base.DEC); return putbytes(s, v.ptr, v.len); }; case let v: str => return putbytes(s, v.ptr, v.len); case let b: bool => { let v: str = "false"; if (b) { v = "true"; }; return putbytes(s, v.ptr, v.len); }; case let r: rune => { // ref/hare/fmt/print.ha:84 io::write(out, utf8::encoderune(r)): // emit the full UTF-8 encoding, not the truncated low byte. let buf: [4]u8; let sl: []u8; sl.ptr = &buf[0]; sl.len = 4; sl.cap = 4; let nn: i32 = utf8.encoderune(sl, r); return putbytes(s, &buf[0], nn); }; case let v: f64 => { // strconv.f64tos static-buffer view consumed before next // strconv call. let v2: str = strconv.f64tos(v); return putbytes(s, v2.ptr, v2.len); }; case let n: int => { let v: str = strconv.i64tos(n: i64, strconv.base.DEC); return putbytes(s, v.ptr, v.len); }; case let n: uint => { // unsigned path: i64dec would render a high-bit value negative. let v: str = strconv.u64tos(n: u64, strconv.base.DEC); return putbytes(s, v.ptr, v.len); }; }; return 0: size; }; // 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 // {%} / {N%M} modifiers supplied by a *mods argument // {{ }} 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: float format selectors (e/f/g/F…) and void/null arms (not in // formattable). 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 supplies the parametric-modifier form ({0%1}). 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 < '0' || c > '9') { if (!any) { fmtabort(); }; return v; }; any = true; // PRE-multiply guard (i32 MAX 2147483647, /10, %10): Hare's // scan_sz uses stoz's UNSIGNED post-multiply wrap-check // (stou.ha:60 `n < old`), UB-class here in signed i32. if (v > 214748364 || (v == 214748364 && (c - 48u8): i32 > 7)) { 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 == '}') { return; }; *pos += 1; if (c == '-') { m.alignment = alignment.LEFT; } else if (c == '=') { m.alignment = alignment.CENTER; } else if (c == '_') { if (*pos >= s.len) { fmtabort(); }; let src: []u8; src.ptr = s.ptr + ((*pos): u64); src.len = s.len - *pos; src.cap = src.len; let d: utf8.decoder = utf8.decode(src); match (utf8.next(&d)) { case let r: rune => m.pad = r; case let dn: utf8.done => fmtabort(); case let mr: utf8.more => fmtabort(); case let e: utf8.invalid => fmtabort(); }; *pos += utf8.position(&d); } else if (c == ' ') { m.neg = neg.SPACE; } else if (c == '+') { m.neg = neg.PLUS; } else if (c == 'x') { m.base = strconv.base.HEX_LOWER; } else if (c == 'X') { m.base = strconv.base.HEX_UPPER; } else if (c == 'o') { m.base = strconv.base.OCT; } else if (c == 'b') { m.base = strconv.base.BIN; } else if (c == '.') { m.prec = scandigits(s, pos); } else if (c >= '1' && c <= '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. Mirror print.ha. 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; }; // rawlenu64 — bytes the raw render of unsigned `v` under `m` would // emit. uint twin of [[rawleni64]]: no value-derived sign (uint is // never negative), so neg_flag is fixed false — only an explicit // PLUS/SPACE mod adds a sign byte. Mirror print.ha. fn rawlenu64(v: u64, m: *mods) i32 = { let signlen: i32 = 0; if (signof(false, m) != 0u8) { signlen = 1; }; let dlen: i32 = digitsu64(v, basenum(m.base)); let inner: i32 = dlen; if (m.prec > signlen + dlen) { inner = m.prec - signlen; }; return signlen + inner; }; // precstr — borrowed rune-wise precision view. A precision larger than the // rune count leaves the string whole even when its byte length is larger. fn precstr(s: str, m: *mods) str = { if (m.prec <= 0 || m.prec >= s.len) { return s; }; let it: strings.iterator = strings.iter(s); let n: i32 = 0; for (n < m.prec) { match (strings.next(&it)) { case let r: rune => n += 1; case utf8.done => return s; }; }; let out: str = s; out.len = strings.position(&it); out.cap = out.len; return out; }; // rawlenstr — bytes the rune-precision view would emit. fn rawlenstr(s: str, m: *mods) i32 = { return precstr(s, m).len; }; // rawlenf64 — bytes the raw render of `v` under `m` would emit; the // strconv.f64tos static-buffer view is consumed by reading view.len + // view.ptr[0] before the sign byte is folded into signof. drew NaN/Inf // safe (strconv emits no leading '-' on nan/inf, so the peel is a no-op // on those views). Mirror print.ha. fn rawlenf64(v: f64, m: *mods) i32 = { if (m.prec != 0 || (m.base != strconv.base.DEFAULT && m.base != strconv.base.DEC)) { fmtabort(); }; let view: str = strconv.f64tos(v); let body: i32 = view.len; let had_neg: bool = false; if (body > 0 && view.ptr[0] == '-') { had_neg = true; body -= 1; }; let signlen: i32 = 0; if (signof(had_neg, m) != 0u8) { signlen = 1; }; return signlen + body; }; // rawlen — dispatch the rawlen sum over formattable. Mirror print.ha:53. 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; }; // width-padding must count the rune's encoded byte length, else a // multibyte rune desyncs the pad (ref/hare/fmt/print.ha:84 emits the // full encoding; runesz is its length). case let r: rune => return utf8.runesz(r); case let v: f64 => return rawlenf64(v, m); case let v: int => return rawleni64(v: i64, m); case let v: uint => return rawlenu64(v: u64, m); }; return 0; // unreachable — match is exhaustive }; // formatraw — write the bare value (no width padding) to `s`. Mirror // print.ha:76 format_raw. Unsupported f64 precision/base combinations are // rejected rather than silently ignored. NaN/infinity strings are emitted // unchanged. fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { 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: size = 0; if (sb != 0u8) { let buf: [1]u8; buf[0] = sb; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; 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] = '0'; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; pi += 1; }; let view: str = strconv.u64tos(u, m.base); let r: (size | io.error) = putbytes(s, view.ptr, view.len); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; return total; }; case let v: str => { let view: str = precstr(v, m); return putbytes(s, view.ptr, view.len); }; case let b: bool => { let v: str = "false"; if (b) { v = "true"; }; return putbytes(s, v.ptr, v.len); }; case let r: rune => { // ref/hare/fmt/print.ha:84: full UTF-8 encoding, not the low byte. let buf: [4]u8; let sl: []u8; sl.ptr = &buf[0]; sl.len = 4; sl.cap = 4; let nn: i32 = utf8.encoderune(sl, r); return putbytes(s, &buf[0], nn); }; case let v: f64 => { if (m.prec != 0 || (m.base != strconv.base.DEFAULT && m.base != strconv.base.DEC)) { fmtabort(); }; // strconv.f64tos prepends '-' for negative values; peel here so // signof folds neg/plus/space mods uniformly with the i64 arm. // drew NaN/Inf signoff: nan/infinity views have no leading '-', // so this peel is a no-op for them. let view: str = strconv.f64tos(v); let neg_flag: bool = false; if (view.len > 0 && view.ptr[0] == '-') { neg_flag = true; view.ptr = view.ptr + 1u64; view.len -= 1; }; let sb: u8 = signof(neg_flag, m); let total: size = 0; if (sb != 0u8) { let buf: [1]u8; buf[0] = sb; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; let r: (size | io.error) = putbytes(s, view.ptr, view.len); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; return total; }; case let vi: int => { // int is the signed machine word; widen to i64 and emit the // signed render — identical to the i64 arm above. let v: i64 = vi: 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: size = 0; if (sb != 0u8) { let buf: [1]u8; buf[0] = sb; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; 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] = '0'; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; pi += 1; }; let view: str = strconv.u64tos(u, m.base); let r: (size | io.error) = putbytes(s, view.ptr, view.len); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; return total; }; case let vu: uint => { // unsigned path: no value-derived sign (uint never negative), so // neg_flag is fixed false — a high-bit value renders as its true // unsigned decimal, not negative. strconv.u64tos, not i64dec. let u: u64 = vu: u64; let sb: u8 = signof(false, m); let total: size = 0; if (sb != 0u8) { let buf: [1]u8; buf[0] = sb; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; 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] = '0'; let r: (size | io.error) = putbytes(s, &buf[0], 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; pi += 1; }; let view: str = strconv.u64tos(u, m.base); let r: (size | io.error) = putbytes(s, view.ptr, view.len); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; return total; }; }; let z: size = 0; return z; // unreachable — match is exhaustive }; // formatone — render `arg` to `s` with `m`'s minimum byte width, // alignment, and complete UTF-8 pad runes applied. Mirrors print.ha:45. fn formatone(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { if (m.width < 0 || m.prec < 0) { fmtabort(); }; let start: i32 = 0; let needpad: bool = false; if (m.width > 0) { let raw: i32 = rawlen(arg, m); let pad: i32 = 0; if (raw < m.width) { pad = m.width - raw; needpad = true; }; if (m.alignment != alignment.LEFT) { if (m.alignment == alignment.CENTER) { start = (pad + 1) / 2; } else { start = pad; }; }; }; let total: size = 0; let padb: [4]u8; let pads: []u8; let padn: i32 = 0; if (needpad) { pads.ptr = &padb[0]; pads.len = 4; pads.cap = 4; padn = utf8.encoderune(pads, m.pad); }; let lead: size = 0; for (lead < (start: size)) { let r: (size | io.error) = putbytes(s, &padb[0], padn); match (r) { case let n: size => { total += n; lead += n; }; case let e: io.error => return e; }; }; let r1: (size | io.error) = formatraw(s, arg, m); match (r1) { case let n: size => { total += n; }; case let e: io.error => return e; }; let twidth: size = m.width: size; for (total < twidth) { let r: (size | io.error) = putbytes(s, &padb[0], padn); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; return total; }; // formatfield — field→formattable inline-per-arm dispatch through // formatone. A field→formattable widen helper called from fprintf's // for-loop would trip #18 (silent miscompile of 24B return-by-value in // for-loop context); inline-per-arm sidesteps it. A *mods is metadata, // never a value to render. Mirror print.ha:648. fn formatfield(s: io.handle, f: field, m: *mods) (size | io.error) = { match (f) { case let v: i64 => { let a: formattable = v; return formatone(s, a, m); }; case let v: str => { let a: formattable = v; return formatone(s, a, m); }; case let b: bool => { let a: formattable = b; return formatone(s, a, m); }; case let r: rune => { let a: formattable = r; return formatone(s, a, m); }; case let v: f64 => { let a: formattable = v; return formatone(s, a, m); }; case let v: int => { let a: formattable = v; return formatone(s, a, m); }; case let v: uint => { let a: formattable = v; return formatone(s, a, m); }; case let p: *mods => { fmtabort(); let z: size = 0; return z; }; }; }; fn copymods(f: field, m: *mods) void = { match (f) { case let p: *mods => { m.alignment = p.alignment; m.pad = p.pad; m.neg = p.neg; m.width = p.width; m.prec = p.prec; m.base = p.base; return; }; case let v: i64 => fmtabort(); case let v: str => fmtabort(); case let v: bool => fmtabort(); case let v: rune => fmtabort(); case let v: f64 => fmtabort(); case let v: int => fmtabort(); case let v: uint => fmtabort(); }; }; // fprint — write the formatted form of each `args` element to `s`, // separated by spaces. Returns total bytes written or the first // io.error. Mirrors ref/hare/fmt/print.ha (fprint) + wrappers.ha. // // Each value is written completely. A writer that stops making progress // returns nomem through io.error rather than a successful truncated prefix. export fn fprint(s: io.handle, args: formattable...) (size | io.error) = { let total: size = 0; let i: i32 = 0; for (i < args.len) { if (i > 0) { let r: (size | io.error) = putbytes(s, " ".ptr, 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; let r: (size | io.error) = writeone(s, args[i]); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; i += 1; }; return total; }; // fprintf — Hare's primary printf-family surface. Mirrors print.ha:26. // Returns total bytes written or io.error on the first sink failure. export fn fprintf(s: io.handle, fmt: str, args: field...) (size | io.error) = { let total: size = 0; let i: i32 = 0; let nextimpl: i32 = 0; let checkunused: bool = true; for (i < fmt.len) { let c: u8 = fmt[i]; if (c == '{') { i += 1; if (i >= fmt.len) { fmtabort(); }; if (fmt[i] == '{') { // '{{' literal let r: (size | io.error) = putbytes(s, fmt.ptr + i: u64, 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; i += 1; } else { let idx: i32 = 0; let d: u8 = fmt[i]; if (d >= '0' && d <= '9') { checkunused = false; idx = scandigits(fmt, &i); } else { idx = nextimpl; nextimpl += 1; }; let m: mods; modsinit(&m); if (i < fmt.len && fmt[i] == ':') { i += 1; scanmods(fmt, &i, &m); } else if (i < fmt.len && fmt[i] == '%') { i += 1; if (i >= fmt.len) { fmtabort(); }; let midx: i32 = 0; if (fmt[i] >= '0' && fmt[i] <= '9') { checkunused = false; midx = scandigits(fmt, &i); } else { midx = nextimpl; nextimpl += 1; }; if (midx >= args.len) { fmtabort(); }; copymods(args[midx], &m); }; if (i >= fmt.len || fmt[i] != '}') { fmtabort(); }; i += 1; if (idx >= args.len) { fmtabort(); }; let r: (size | io.error) = formatfield(s, args[idx], &m); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; } else if (c == '}') { i += 1; if (i >= fmt.len || fmt[i] != '}') { fmtabort(); }; let r: (size | io.error) = putbytes(s, fmt.ptr + i: u64, 1); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; i += 1; } else { let start: i32 = i; for (i < fmt.len && fmt[i] != '{' && fmt[i] != '}') { i += 1; }; let r: (size | io.error) = putbytes(s, fmt.ptr + (start: u64), i - start); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; }; }; }; if (checkunused && nextimpl != args.len) { fmtabort(); }; return total; }; // fprintln — fprint plus a trailing newline. Mirrors wrappers.ha. export fn fprintln(s: io.handle, args: formattable...) (size | io.error) = { let total: size = 0; match (fprint(s, args...)) { case let n: size => { total = n; }; case let e: io.error => return e; }; match (putbytes(s, "\n".ptr, 1)) { case let n: size => { total += n; }; case let e: io.error => return e; }; return total; }; // fprintfln — fprintf plus a trailing newline. Mirrors wrappers.ha:69. export fn fprintfln(s: io.handle, fmt: str, args: field...) (size | io.error) = { let total: size = 0; match (fprintf(s, fmt, args...)) { case let n: size => { total = n; }; case let e: io.error => return e; }; match (putbytes(s, "\n".ptr, 1)) { case let n: size => { total += n; }; case let e: io.error => return e; }; return total; }; // bsprintf — render into `buf` through memio.fixed; return the str view // of bytes actually written. Mirrors wrappers.ha:42. memio.fixed returns // 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. 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; match (fprintf(s, fmt, args...)) { case let n: size => { return memio.string(&st); }; case let e: io.error => return e; }; }; // asprintf — render `fmt`/`args` into a heap-allocated str through // memio.dynamic, shrink-copy to a tight allocation, close the dynamic // backing. Mirrors wrappers.ha:29 modulo: bare `str` return (Hare // returns `(str | nomem)`; ww has no `nomem` variant on the public // surface — os.alloc faults on OOM per lib/os.ww). Caller frees with // `os.free(r.ptr, r.len: u64)` when r.len > 0; r.len == 0 is a no-op // free. Shrink-to-fit: memio.dynamic's cap doubles past pos during // growth; io.close frees the cap-sized mapping. Returning // memio.string directly would leak the cap-vs-len slack (skip close) or // dangle the view (close first); the copy lets the caller free with // r.len. export fn asprintf(fmt: str, args: field...) str = { let out: str; out.ptr = nil; out.len = 0; let st: memio.stream = memio.dynamic(); let s: io.stream = &st.vt; let wres: (size | io.error) = fprintf(s, fmt, args...); match (wres) { case let n: size => {}; case let e: io.error => {}; }; let view: str = memio.string(&st); if (view.len == 0) { let cres: (void | io.error) = io.close(s); match (cres) { case void => {}; case let e: io.error => {}; }; 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.error) = io.close(s); match (cres) { case void => {}; case let e: io.error => {}; }; tight.len = view.len; return strings.frombytes(tight); }; // Mirror ref/hare/fmt/wrappers.ha. Hare routes the stdio wrappers through // os::stdout / os::stderr (io::handle); ww's os plays the sys role and // can't import io (import floor), so it exports the std fd NUMBERS // (os.STD{OUT,ERR}_FILENO) and the io.file binding is cast at the call // site — `os.STDOUT_FILENO: io.file` widens into the fprint handle param // (io fold-2, #5). errorf / asprint / bsprint omitted: the bare `error` // name collides with strconv.error under the driver's flat-scope concat; // ship the -ln forms only. // print / println — wrappers.ha:78/:84 on stdout. export fn print(args: formattable...) (size | io.error) = { return fprint(os.STDOUT_FILENO: io.file, args...); }; export fn println(args: formattable...) (size | io.error) = { return fprintln(os.STDOUT_FILENO: io.file, args...); }; // errorln — wrappers.ha:96 on stderr. export fn errorln(args: formattable...) (size | io.error) = { return fprintln(os.STDERR_FILENO: io.file, args...); }; // fatal — errorln then exit(255). `never` return marks the bottom type // so flow-control checks treat callers as terminated. Mirrors // wrappers.ha:63. export fn fatal(args: formattable...) never = { fprintln(os.STDERR_FILENO: io.file, args...); os.exit(255); }; // printf / printfln — wrappers.ha:10/:15 on stdout. export fn printf(fmt: str, args: field...) (size | io.error) = { return fprintf(os.STDOUT_FILENO: io.file, fmt, args...); }; export fn printfln(fmt: str, args: field...) (size | io.error) = { return fprintfln(os.STDOUT_FILENO: io.file, fmt, args...); }; // errorfln — wrappers.ha:24 on stderr. export fn errorfln(fmt: str, args: field...) (size | io.error) = { return fprintfln(os.STDERR_FILENO: io.file, fmt, args...); }; // fatalf — errorfln then exit(255). Mirrors wrappers.ha:54. export fn fatalf(fmt: str, args: field...) never = { fprintfln(os.STDERR_FILENO: io.file, fmt, args...); os.exit(255); };