Files
ww/lib/fmt/fmt.ww

897 lines
28 KiB
Plaintext

// 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
// _<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: 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);
};