fmt: make formatting complete and rune-safe

This commit is contained in:
2026-08-09 17:59:41 +09:00
parent 976dc8a813
commit d7d5b593c2
3 changed files with 310 additions and 116 deletions

View File

@@ -34,8 +34,8 @@ Signatures mirror Hare too, modulo:
the two uses don't overlap because `T...` only attaches to a the two uses don't overlap because `T...` only attaches to a
*param* decl. `lib/fmt` ships both the print family (`print` / *param* decl. `lib/fmt` ships both the print family (`print` /
`println` / `fprint` / `fprintln`) and the {n}-placeholder family `println` / `fprint` / `fprintln`) and the {n}-placeholder family
(`printf` / `fprintf` / `bsprintf` / `fatalf`); the `%`-parametric (`printf` / `fprintf` / `bsprintf` / `fatalf`), including Hare's
modifier form is parsed but its `*mods` arg slot aborts on dispatch. `%`-parametric `*mods` form.
- `(T | U)` sum-typed parameters dispatch via `match` inside the - `(T | U)` sum-typed parameters dispatch via `match` inside the
callee. `strings.byteindex(haystack: str, needle: (str | rune))`, callee. `strings.byteindex(haystack: str, needle: (str | rune))`,

View File

@@ -6,60 +6,20 @@ package fmt;
import io; import io;
import encoding.utf8; import encoding.utf8;
import math;
import memio; import memio;
import os; import os;
import strings; import strings;
import strconv; 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 = {
// ref/hare/fmt/print.ha:124-129: magnitude via math::absi64 into u64,
// sign tested separately. `n = -n` on an i64 wraps at i64::MIN
// (-MIN == MIN), so the old loop never ran and only the '-' printed.
let neg: bool = v < 0;
let n: u64 = math.absi64(v);
let tmp: [20]u8;
let i: i32 = 0;
if (n == 0u64) { tmp[0] = '0'; i = 1; };
for (n > 0u64) {
let d: u64 = n % 10u64;
tmp[i] = (d + 48u64): u8;
n = n / 10u64;
i += 1;
};
let out: i32 = 0;
if (neg) { i64dec_buf[out] = '-'; 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 // formattable — tagged union of types fmt can render. Mirrors Hare's
// `fmt::formattable = (...types::numeric | uintptr | str | rune | // `fmt::formattable = (...types::numeric | uintptr | str | rune |
// bool | nullable *opaque | void)`, narrowed to the set ww actually // bool | nullable *opaque | void)`, narrowed to the set ww actually
// has codegen for. Slot size is 24B (8 tag + 16 str payload — f64 // has codegen for. Slot size is 24B (8 tag + 16 str payload — f64
// arm is only 8B and rides under the str payload). // arm is only 8B and rides under the str payload).
// //
// No `f32` arm: strconv ships no `f32tos` and there is no in-tree // No `f32` arm: strconv has the primitive, but there is no in-tree fmt
// caller. Callers with an `f32` cast at the call site (`myf: f64`), // caller and adding a tagged-union arm is a public ABI change. Callers
// mirroring how `i64` covers every int width today. Ship the `f32` // cast to f64 until consumer evidence justifies widening this surface.
// arm when the first in-tree caller needs it.
// //
// `int`/`uint` appended LAST: variant tags follow declaration order // `int`/`uint` appended LAST: variant tags follow declaration order
// (cstage cg_tag_for_variant / wwstage flatvariantidxt), so the // (cstage cg_tag_for_variant / wwstage flatvariantidxt), so the
@@ -74,16 +34,33 @@ fn i64dec(v: i64) str = {
export type formattable = (i64 | str | bool | rune | f64 | int | uint); export type formattable = (i64 | str | bool | rune | f64 | int | uint);
fn putbytes(s: io.handle, p: *u8, n: i32) (size | io.error) = { fn putbytes(s: io.handle, p: *u8, n: i32) (size | io.error) = {
let off: i32 = 0;
for (off < n) {
let v: []u8; let v: []u8;
v.ptr = p; v.ptr = p + (off: u64);
v.len = n; v.len = n - off;
return io.write(s, v); 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) = { fn writeone(s: io.handle, a: formattable) (size | io.error) = {
match (a) { match (a) {
case let n: i64 => { case let n: i64 => {
let v: str = i64dec(n); let v: str = strconv.i64tos(n, strconv.base.DEC);
return putbytes(s, v.ptr, v.len); return putbytes(s, v.ptr, v.len);
}; };
case let v: str => return putbytes(s, v.ptr, v.len); case let v: str => return putbytes(s, v.ptr, v.len);
@@ -110,7 +87,7 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = {
return putbytes(s, v2.ptr, v2.len); return putbytes(s, v2.ptr, v2.len);
}; };
case let n: int => { case let n: int => {
let v: str = i64dec(n: i64); let v: str = strconv.i64tos(n: i64, strconv.base.DEC);
return putbytes(s, v.ptr, v.len); return putbytes(s, v.ptr, v.len);
}; };
case let n: uint => { case let n: uint => {
@@ -128,6 +105,7 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = {
// {N} explicit-positional arg N // {N} explicit-positional arg N
// {:mods} inline modifiers on next arg // {:mods} inline modifiers on next arg
// {N:mods} inline modifiers on arg N // {N:mods} inline modifiers on arg N
// {%} / {N%M} modifiers supplied by a *mods argument
// {{ }} literal '{' / '}' // {{ }} literal '{' / '}'
// //
// Modifier set (subset of iter.ha:129 scan_modifiers): // Modifier set (subset of iter.ha:129 scan_modifiers):
@@ -141,9 +119,9 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = {
// <digits> width (first char 1..9) // <digits> width (first char 1..9)
// .<digits> precision // .<digits> precision
// //
// Not v1: '%' parametric form ({0%1}); float modifiers (e/f/g/F…); // Not v1: float format selectors (e/f/g/F…) and void/null arms (not in
// void / null arms (not in formattable). Invalid format aborts via // formattable). Invalid format aborts via os.exit — Hare uses `abort()`;
// os.exit — Hare uses `abort()`; same effect. // same effect.
// neg, alignment — mirror iter.ha:19 / iter.ha:26. // neg, alignment — mirror iter.ha:19 / iter.ha:26.
export type neg = enum i32 { NONE = 0, SPACE = 1, PLUS = 2 }; export type neg = enum i32 { NONE = 0, SPACE = 1, PLUS = 2 };
@@ -162,9 +140,7 @@ export type mods = struct {
}; };
// field — variadic arg slot. `(...formattable | *mods)` per iter.ha:11. // field — variadic arg slot. `(...formattable | *mods)` per iter.ha:11.
// The *mods arm is the parametric-modifier form ({0%1}); accepted by // The *mods arm supplies the parametric-modifier form ({0%1}).
// 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); export type field = (...formattable | *mods);
// fmtabort — invalid format string. Matches Hare's abort() (iter.ha:71) // fmtabort — invalid format string. Matches Hare's abort() (iter.ha:71)
@@ -220,8 +196,18 @@ fn scanmods(s: str, pos: *i32, m: *mods) void = {
else if (c == '=') { m.alignment = alignment.CENTER; } else if (c == '=') { m.alignment = alignment.CENTER; }
else if (c == '_') { else if (c == '_') {
if (*pos >= s.len) { fmtabort(); }; if (*pos >= s.len) { fmtabort(); };
m.pad = s[*pos]: rune; let src: []u8;
*pos += 1; 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.SPACE; }
else if (c == '+') { m.neg = neg.PLUS; } else if (c == '+') { m.neg = neg.PLUS; }
@@ -302,11 +288,27 @@ fn rawlenu64(v: u64, m: *mods) i32 = {
return signlen + inner; return signlen + inner;
}; };
// rawlenstr — bytes the raw render of `s` would emit (after `prec` // precstr — borrowed rune-wise precision view. A precision larger than the
// truncation, per Hare print.ha:86). // 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 = { fn rawlenstr(s: str, m: *mods) i32 = {
if (m.prec > 0 && m.prec < s.len) { return m.prec; }; return precstr(s, m).len;
return s.len;
}; };
// rawlenf64 — bytes the raw render of `v` under `m` would emit; the // rawlenf64 — bytes the raw render of `v` under `m` would emit; the
@@ -315,6 +317,10 @@ fn rawlenstr(s: str, m: *mods) i32 = {
// safe (strconv emits no leading '-' on nan/inf, so the peel is a no-op // safe (strconv emits no leading '-' on nan/inf, so the peel is a no-op
// on those views). Mirror print.ha. // on those views). Mirror print.ha.
fn rawlenf64(v: f64, m: *mods) i32 = { 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 view: str = strconv.f64tos(v);
let body: i32 = view.len; let body: i32 = view.len;
let had_neg: bool = false; let had_neg: bool = false;
@@ -345,9 +351,8 @@ fn rawlen(arg: formattable, m: *mods) i32 = {
}; };
// formatraw — write the bare value (no width padding) to `s`. Mirror // formatraw — write the bare value (no width padding) to `s`. Mirror
// print.ha:76 format_raw. `prec` ignored on f64 (strconv.f64tos is // print.ha:76 format_raw. Unsupported f64 precision/base combinations are
// shortest-G with no precision knob); base ignored on f64 (Hare too — // rejected rather than silently ignored. NaN/infinity strings are emitted
// base is int-only). drew NaN/Inf signoff: nan/infinity strings emitted
// unchanged. // unchanged.
fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
match (arg) { match (arg) {
@@ -391,8 +396,8 @@ fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
return total; return total;
}; };
case let v: str => { case let v: str => {
let n: i32 = rawlenstr(v, m); let view: str = precstr(v, m);
return putbytes(s, v.ptr, n); return putbytes(s, view.ptr, view.len);
}; };
case let b: bool => { case let b: bool => {
let v: str = "false"; let v: str = "false";
@@ -410,6 +415,10 @@ fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
return putbytes(s, &buf[0], nn); return putbytes(s, &buf[0], nn);
}; };
case let v: f64 => { 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 // strconv.f64tos prepends '-' for negative values; peel here so
// signof folds neg/plus/space mods uniformly with the i64 arm. // signof folds neg/plus/space mods uniformly with the i64 arm.
// drew NaN/Inf signoff: nan/infinity views have no leading '-', // drew NaN/Inf signoff: nan/infinity views have no leading '-',
@@ -525,49 +534,57 @@ fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
let z: size = 0; return z; // unreachable — match is exhaustive let z: size = 0; return z; // unreachable — match is exhaustive
}; };
// formatone — render `arg` to `s` with `m`'s width / alignment / pad // formatone — render `arg` to `s` with `m`'s minimum byte width,
// applied. Mirror print.ha:45 format, modulo the tail-pad loop: it // alignment, and complete UTF-8 pad runes applied. Mirrors print.ha:45.
// drives on a `need` counter rather than Hare's `total < m.width` form.
// memio.fixedwrite now returns nomem on a full sink, so the width form
// would terminate too; restoring it is a deferred follow-up — kept
// counter-driven here to stay in F-R scope.
fn formatone(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { 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 start: i32 = 0;
if (m.width > 0 && m.alignment != alignment.LEFT) { let needpad: bool = false;
if (m.width > 0) {
let raw: i32 = rawlen(arg, m); let raw: i32 = rawlen(arg, m);
let pad: i32 = 0; let pad: i32 = 0;
if (raw < m.width) { pad = m.width - raw; }; if (raw < m.width) {
if (m.alignment == alignment.CENTER) { start = (pad + 1) / 2; } pad = m.width - raw;
else { start = pad; }; needpad = true;
};
if (m.alignment != alignment.LEFT) {
if (m.alignment == alignment.CENTER) {
start = (pad + 1) / 2;
} else {
start = pad;
};
};
}; };
let total: size = 0; let total: size = 0;
let i: i32 = 0; let padb: [4]u8;
let padb: [1]u8; let pads: []u8;
padb[0] = m.pad: u8; let padn: i32 = 0;
for (i < start) { if (needpad) {
let r: (size | io.error) = putbytes(s, &padb[0], 1); 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) { match (r) {
case let n: size => { total += n; }; case let n: size => { total += n; lead += n; };
case let e: io.error => return e; case let e: io.error => return e;
}; };
i += 1;
}; };
let r1: (size | io.error) = formatraw(s, arg, m); let r1: (size | io.error) = formatraw(s, arg, m);
match (r1) { match (r1) {
case let n: size => { total += n; }; case let n: size => { total += n; };
case let e: io.error => return e; case let e: io.error => return e;
}; };
let need: i32 = 0;
let twidth: size = m.width: size; let twidth: size = m.width: size;
if (twidth > total) { need = (twidth - total): i32; }; for (total < twidth) {
let j: i32 = 0; let r: (size | io.error) = putbytes(s, &padb[0], padn);
for (j < need) {
let r: (size | io.error) = putbytes(s, &padb[0], 1);
match (r) { match (r) {
case let n: size => { total += n; }; case let n: size => { total += n; };
case let e: io.error => return e; case let e: io.error => return e;
}; };
j += 1;
}; };
return total; return total;
}; };
@@ -575,8 +592,8 @@ fn formatone(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
// formatfield — field→formattable inline-per-arm dispatch through // formatfield — field→formattable inline-per-arm dispatch through
// formatone. A field→formattable widen helper called from fprintf's // 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 would trip #18 (silent miscompile of 24B return-by-value in
// for-loop context); inline-per-arm sidesteps it. `*mods` arm aborts // for-loop context); inline-per-arm sidesteps it. A *mods is metadata,
// (parametric '%' form not implemented). Mirror print.ha:648. // never a value to render. Mirror print.ha:648.
fn formatfield(s: io.handle, f: field, m: *mods) (size | io.error) = { fn formatfield(s: io.handle, f: field, m: *mods) (size | io.error) = {
match (f) { match (f) {
case let v: i64 => { case let v: i64 => {
@@ -611,14 +628,33 @@ fn formatfield(s: io.handle, f: field, m: *mods) (size | io.error) = {
}; };
}; };
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`, // fprint — write the formatted form of each `args` element to `s`,
// separated by spaces. Returns total bytes written or the first // separated by spaces. Returns total bytes written or the first
// io.error. Mirrors ref/hare/fmt/print.ha (fprint) + wrappers.ha. // io.error. Mirrors ref/hare/fmt/print.ha (fprint) + wrappers.ha.
// //
// A short write (sink accepts fewer bytes than asked) is reported by // Each value is written completely. A writer that stops making progress
// the returned count, not as an error — matches [[io.write]]'s contract // returns nomem through io.error rather than a successful truncated prefix.
// per [[memio.fixed]]. Callers that need write-all semantics layer it
// on top, the same way they do over raw [[io.write]].
export fn fprint(s: io.handle, args: formattable...) (size | io.error) = { export fn fprint(s: io.handle, args: formattable...) (size | io.error) = {
let total: size = 0; let total: size = 0;
let i: i32 = 0; let i: i32 = 0;
@@ -674,6 +710,19 @@ export fn fprintf(s: io.handle, fmt: str, args: field...) (size | io.error) = {
if (i < fmt.len && fmt[i] == ':') { if (i < fmt.len && fmt[i] == ':') {
i += 1; i += 1;
scanmods(fmt, &i, &m); 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(); }; if (i >= fmt.len || fmt[i] != '}') { fmtabort(); };
i += 1; i += 1;
@@ -694,12 +743,16 @@ export fn fprintf(s: io.handle, fmt: str, args: field...) (size | io.error) = {
}; };
i += 1; i += 1;
} else { } else {
let r: (size | io.error) = putbytes(s, fmt.ptr + i: u64, 1); 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) { match (r) {
case let n: size => { total += n; }; case let n: size => { total += n; };
case let e: io.error => return e; case let e: io.error => return e;
}; };
i += 1;
}; };
}; };
if (checkunused && nextimpl != args.len) { fmtabort(); }; if (checkunused && nextimpl != args.len) { fmtabort(); };

View File

@@ -10,6 +10,8 @@ import fmt;
import io; import io;
import memio; import memio;
import os; import os;
import strconv;
import test;
fn streq(a: str, b: str) bool = { fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; }; if (a.len != b.len) { return false; };
@@ -35,6 +37,38 @@ fn errsource() io.stream = {
return &errvt; return &errvt;
}; };
type shortstream = struct {
vt: io.vtable,
buf: [32]u8,
pos: i32,
calls: i32,
};
fn shortwrite(s: io.stream, buf: []u8) (size | io.error) = {
let out: *shortstream = s: *shortstream;
let n: i32 = buf.len;
if (n > 2) { n = 2; };
let i: i32 = 0;
for (i < n) {
out.buf[out.pos + i] = buf[i];
i += 1;
};
out.pos += n;
out.calls += 1;
return n: size;
};
type countstream = struct {
vt: io.vtable,
calls: i32,
};
fn countwrite(s: io.stream, buf: []u8) (size | io.error) = {
let out: *countstream = s: *countstream;
out.calls += 1;
return buf.len: size;
};
@test fn fprintbarestr() void = { @test fn fprintbarestr() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt; let s: io.stream = &mem.vt;
@@ -65,10 +99,8 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
// #67: i64dec used `n = -n` within i64, which wraps at i64::MIN (-MIN == MIN), // These rows preserve the I64_MIN boundary while fprint delegates integer
// so the digit loop never ran and only the bare '-' was emitted. The fix // conversion to strconv.i64tos instead of keeping a second decimal engine.
// takes the magnitude through math.absi64 into u64. These rows pin the
// boundary value plus a normal negative, zero, and int MIN.
@test fn fprinti64min() void = { @test fn fprinti64min() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();
@@ -162,10 +194,8 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
// memio.fixedwrite caps each call at the remaining buffer space and // A partial fixed-buffer write cannot be reported as a successful prefix.
// never closes the stream, so fprint sees a short i32 result, not // putbytes retries the unwritten suffix; the full sink then returns nomem.
// io.error. Verifies the inner-loop arithmetic adds the actual byte
// count rather than the requested length.
@test fn fprintfixedshort() void = { @test fn fprintfixedshort() void = {
let buf: [3]u8; let buf: [3]u8;
@@ -174,8 +204,8 @@ fn errsource() io.stream = {
let r: (size | io.error) = fmt.fprint(s, "hello"); let r: (size | io.error) = fmt.fprint(s, "hello");
match (r) { match (r) {
case let n: size => { assert(!(n: i32 != 3)); }; case let n: size => abort();
case let eioe: io.error => abort(); case let eioe: io.error => { assert(eioe is nomem); };
}; };
assert(!(!streq(memio.string(&mem), "hel"))); assert(!(!streq(memio.string(&mem), "hel")));
@@ -183,6 +213,24 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
@test fn fprintcompleteshortwrites() void = {
let out: shortstream;
out.vt.writer = (&shortwrite): *io.writer;
out.pos = 0;
out.calls = 0;
let r: (size | io.error) = fmt.fprint(&out.vt, "hello");
match (r) {
case let n: size => assert(n == 5: size);
case let e: io.error => abort();
};
let got: str;
got.ptr = &out.buf[0];
got.len = out.pos;
got.cap = got.len;
assert(streq(got, "hello"));
assert(out.calls == 3);
};
// Exercises the early-return arm in fprint's inner match — distinct from // Exercises the early-return arm in fprint's inner match — distinct from
// the short-write path above, which keeps returning i32 from a partial // the short-write path above, which keeps returning i32 from a partial
// accept. Single arm is enough: every formattable case routes errors // accept. Single arm is enough: every formattable case routes errors
@@ -213,6 +261,20 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
@test fn fprintf_coalesces_literals() void = {
let out: countstream;
out.vt.writer = (&countwrite): *io.writer;
out.calls = 0;
let r: (size | io.error) = fmt.fprintf(&out.vt,
"hello {} world", 42i64);
match (r) {
case let n: size => assert(n == 14: size);
case let e: io.error => abort();
};
// One call for each literal run and one for the formatted integer.
assert(out.calls == 3);
};
@test fn fprintf_indexed() void = { @test fn fprintf_indexed() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt; let s: io.stream = &mem.vt;
@@ -226,6 +288,27 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
@test fn fprintf_parametric_mods() void = {
let m: fmt.mods;
m.alignment = fmt.alignment.RIGHT;
m.pad = '0': rune;
m.neg = fmt.neg.NONE;
m.width = 5;
m.prec = 0;
m.base = strconv.base.DEC;
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;
let r: (size | io.error) = fmt.fprintf(s,
"{%} {2%3}", 42i64, &m, 7i64, &m);
match (r) {
case let n: size => assert(n: i32 == 11);
case let e: io.error => abort();
};
assert(streq(memio.string(&mem), "00042 00007"));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
@test fn fprintf_literal_braces() void = { @test fn fprintf_literal_braces() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt; let s: io.stream = &mem.vt;
@@ -330,6 +413,19 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
@test fn fprintf_pad_multibyte() void = {
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;
let r: (size | io.error) = fmt.fprintf(s, "{:_€5}", "hi");
match (r) {
case let n: size => assert(n: i32 == 5);
case let e: io.error => abort();
};
assert(streq(memio.string(&mem), "€hi"));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
@test fn fprintf_base_hex() void = { @test fn fprintf_base_hex() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt; let s: io.stream = &mem.vt;
@@ -382,6 +478,33 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); }; match (c) { case void => {}; case let eioe: io.error => abort(); };
}; };
@test fn fprintf_prec_str_runes() void = {
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;
let r: (size | io.error) = fmt.fprintf(s,
"{:.1}|{:.2}|{:.2}", "éx", "éx", "😀");
match (r) {
case let n: size => assert(n: i32 == 11);
case let e: io.error => abort();
};
assert(streq(memio.string(&mem), "é|éx|😀"));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
@test fn fprintf_prec_str_rune_width() void = {
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;
let r: (size | io.error) = fmt.fprintf(s, "{:5.1}", "éx");
match (r) {
case let n: size => assert(n: i32 == 5);
case let e: io.error => abort();
};
assert(streq(memio.string(&mem), " é"));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let e: io.error => abort(); };
};
@test fn fprintf_sign_neg() void = { @test fn fprintf_sign_neg() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt; let s: io.stream = &mem.vt;
@@ -439,6 +562,15 @@ fn errsource() io.stream = {
}; };
}; };
@test fn bsprintf_final_short() void = {
let buf: [3]u8;
let r: (str | io.error) = fmt.bsprintf(buf[0:3], "hello");
match (r) {
case let s: str => abort();
case let e: io.error => assert(e is nomem);
};
};
// Pins the formatfield bool / rune arms inside fprintf's for-loop — // Pins the formatfield bool / rune arms inside fprintf's for-loop —
// the codegen-smell repro shape (task #18). Pre-task-#18, the str arm // the codegen-smell repro shape (task #18). Pre-task-#18, the str arm
// is also exercised by fprintf_implicit; this adds the two remaining // is also exercised by fprintf_implicit; this adds the two remaining
@@ -532,14 +664,23 @@ fn errsource() io.stream = {
os.free(r.ptr: *void, r.len: u64); os.free(r.ptr: *void, r.len: u64);
}; };
// Pins the f64 formattable arm landed under task #17 (unblocked by #30 // strconv.f64tos drives shortest round-trippable decimal output, including
// — the variant-widen-from-X0 fix). strconv.f64tos drives the render; // signed zero, infinities, NaNs, subnormals, and scientific notation.
// see lib/strconv/strconv.ww for the documented subset (fixed-point,
// 6 fractional digits, trailing-zero trim, magnitudes ≥ 9e18 → "huge",
// no NaN/±Inf detection).
// //
// Mods scope under v1: width / alignment / pad / sign mods honored. // Width / alignment / pad / sign mods are honored. Precision and
// `prec` and `base` ignored — graduate when strconv grows ffmt/fflags. // non-decimal bases are rejected until strconv grows ffmt/fflags.
@test fn fprintf_f64_precision_aborts() void = {
test.expectabort();
let buf: [16]u8;
let r: (str | io.error) = fmt.bsprintf(buf[0:16], "{:.1}", 1.5);
};
@test fn fprintf_f64_base_aborts() void = {
test.expectabort();
let buf: [16]u8;
let r: (str | io.error) = fmt.bsprintf(buf[0:16], "{:x}", 1.5);
};
@test fn fprintf_f64_basic() void = { @test fn fprintf_f64_basic() void = {
let mem: memio.stream = memio.dynamic(); let mem: memio.stream = memio.dynamic();