fmt: make formatting complete and rune-safe
This commit is contained in:
@@ -34,8 +34,8 @@ Signatures mirror Hare too, modulo:
|
||||
the two uses don't overlap because `T...` only attaches to a
|
||||
*param* decl. `lib/fmt` ships both the print family (`print` /
|
||||
`println` / `fprint` / `fprintln`) and the {n}-placeholder family
|
||||
(`printf` / `fprintf` / `bsprintf` / `fatalf`); the `%`-parametric
|
||||
modifier form is parsed but its `*mods` arg slot aborts on dispatch.
|
||||
(`printf` / `fprintf` / `bsprintf` / `fatalf`), including Hare's
|
||||
`%`-parametric `*mods` form.
|
||||
|
||||
- `(T | U)` sum-typed parameters dispatch via `match` inside the
|
||||
callee. `strings.byteindex(haystack: str, needle: (str | rune))`,
|
||||
|
||||
247
lib/fmt/fmt.ww
247
lib/fmt/fmt.ww
@@ -6,60 +6,20 @@ package fmt;
|
||||
|
||||
import io;
|
||||
import encoding.utf8;
|
||||
import math;
|
||||
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 = {
|
||||
// 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
|
||||
// `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.
|
||||
// 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
|
||||
@@ -74,16 +34,33 @@ fn i64dec(v: i64) str = {
|
||||
export type formattable = (i64 | str | bool | rune | f64 | int | uint);
|
||||
|
||||
fn putbytes(s: io.handle, p: *u8, n: i32) (size | io.error) = {
|
||||
let v: []u8;
|
||||
v.ptr = p;
|
||||
v.len = n;
|
||||
return io.write(s, v);
|
||||
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 = i64dec(n);
|
||||
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);
|
||||
@@ -110,7 +87,7 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = {
|
||||
return putbytes(s, v2.ptr, v2.len);
|
||||
};
|
||||
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);
|
||||
};
|
||||
case let n: uint => {
|
||||
@@ -128,6 +105,7 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = {
|
||||
// {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):
|
||||
@@ -141,9 +119,9 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = {
|
||||
// <digits> width (first char 1..9)
|
||||
// .<digits> precision
|
||||
//
|
||||
// Not v1: '%' parametric form ({0%1}); float modifiers (e/f/g/F…);
|
||||
// void / null arms (not in formattable). Invalid format aborts via
|
||||
// os.exit — Hare uses `abort()`; same effect.
|
||||
// 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 };
|
||||
@@ -162,9 +140,7 @@ export type mods = struct {
|
||||
};
|
||||
|
||||
// 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.
|
||||
// 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)
|
||||
@@ -220,8 +196,18 @@ fn scanmods(s: str, pos: *i32, m: *mods) void = {
|
||||
else if (c == '=') { m.alignment = alignment.CENTER; }
|
||||
else if (c == '_') {
|
||||
if (*pos >= s.len) { fmtabort(); };
|
||||
m.pad = s[*pos]: rune;
|
||||
*pos += 1;
|
||||
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; }
|
||||
@@ -302,11 +288,27 @@ fn rawlenu64(v: u64, m: *mods) i32 = {
|
||||
return signlen + inner;
|
||||
};
|
||||
|
||||
// rawlenstr — bytes the raw render of `s` would emit (after `prec`
|
||||
// truncation, per Hare print.ha:86).
|
||||
// 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 = {
|
||||
if (m.prec > 0 && m.prec < s.len) { return m.prec; };
|
||||
return s.len;
|
||||
return precstr(s, m).len;
|
||||
};
|
||||
|
||||
// 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
|
||||
// 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;
|
||||
@@ -345,9 +351,8 @@ fn rawlen(arg: formattable, m: *mods) i32 = {
|
||||
};
|
||||
|
||||
// formatraw — write the bare value (no width padding) to `s`. Mirror
|
||||
// print.ha:76 format_raw. `prec` ignored on f64 (strconv.f64tos is
|
||||
// shortest-G with no precision knob); base ignored on f64 (Hare too —
|
||||
// base is int-only). drew NaN/Inf signoff: nan/infinity strings emitted
|
||||
// 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) {
|
||||
@@ -391,8 +396,8 @@ fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
|
||||
return total;
|
||||
};
|
||||
case let v: str => {
|
||||
let n: i32 = rawlenstr(v, m);
|
||||
return putbytes(s, v.ptr, n);
|
||||
let view: str = precstr(v, m);
|
||||
return putbytes(s, view.ptr, view.len);
|
||||
};
|
||||
case let b: bool => {
|
||||
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);
|
||||
};
|
||||
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 '-',
|
||||
@@ -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
|
||||
};
|
||||
|
||||
// formatone — render `arg` to `s` with `m`'s width / alignment / pad
|
||||
// applied. Mirror print.ha:45 format, modulo the tail-pad loop: it
|
||||
// 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.
|
||||
// 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;
|
||||
if (m.width > 0 && m.alignment != alignment.LEFT) {
|
||||
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; };
|
||||
if (m.alignment == alignment.CENTER) { start = (pad + 1) / 2; }
|
||||
else { start = pad; };
|
||||
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 i: i32 = 0;
|
||||
let padb: [1]u8;
|
||||
padb[0] = m.pad: u8;
|
||||
for (i < start) {
|
||||
let r: (size | io.error) = putbytes(s, &padb[0], 1);
|
||||
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; };
|
||||
case let n: size => { total += n; lead += n; };
|
||||
case let e: io.error => return e;
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
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 need: i32 = 0;
|
||||
let twidth: size = m.width: size;
|
||||
if (twidth > total) { need = (twidth - total): i32; };
|
||||
let j: i32 = 0;
|
||||
for (j < need) {
|
||||
let r: (size | io.error) = putbytes(s, &padb[0], 1);
|
||||
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;
|
||||
};
|
||||
j += 1;
|
||||
};
|
||||
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
|
||||
// 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. `*mods` arm aborts
|
||||
// (parametric '%' form not implemented). Mirror print.ha:648.
|
||||
// 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 => {
|
||||
@@ -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`,
|
||||
// separated by spaces. Returns total bytes written or the first
|
||||
// io.error. Mirrors ref/hare/fmt/print.ha (fprint) + wrappers.ha.
|
||||
//
|
||||
// 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.fixed]]. Callers that need write-all semantics layer it
|
||||
// on top, the same way they do over raw [[io.write]].
|
||||
// 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;
|
||||
@@ -674,6 +710,19 @@ export fn fprintf(s: io.handle, fmt: str, args: field...) (size | io.error) = {
|
||||
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;
|
||||
@@ -694,12 +743,16 @@ export fn fprintf(s: io.handle, fmt: str, args: field...) (size | io.error) = {
|
||||
};
|
||||
i += 1;
|
||||
} 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) {
|
||||
case let n: size => { total += n; };
|
||||
case let e: io.error => return e;
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
if (checkunused && nextimpl != args.len) { fmtabort(); };
|
||||
|
||||
@@ -10,6 +10,8 @@ import fmt;
|
||||
import io;
|
||||
import memio;
|
||||
import os;
|
||||
import strconv;
|
||||
import test;
|
||||
|
||||
fn streq(a: str, b: str) bool = {
|
||||
if (a.len != b.len) { return false; };
|
||||
@@ -35,6 +37,38 @@ fn errsource() io.stream = {
|
||||
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 = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
let s: io.stream = &mem.vt;
|
||||
@@ -65,10 +99,8 @@ fn errsource() io.stream = {
|
||||
match (c) { case void => {}; case let eioe: io.error => abort(); };
|
||||
};
|
||||
|
||||
// #67: i64dec used `n = -n` within i64, which wraps at i64::MIN (-MIN == MIN),
|
||||
// so the digit loop never ran and only the bare '-' was emitted. The fix
|
||||
// takes the magnitude through math.absi64 into u64. These rows pin the
|
||||
// boundary value plus a normal negative, zero, and int MIN.
|
||||
// These rows preserve the I64_MIN boundary while fprint delegates integer
|
||||
// conversion to strconv.i64tos instead of keeping a second decimal engine.
|
||||
|
||||
@test fn fprinti64min() void = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
@@ -162,10 +194,8 @@ fn errsource() io.stream = {
|
||||
match (c) { case void => {}; case let eioe: io.error => abort(); };
|
||||
};
|
||||
|
||||
// memio.fixedwrite caps each call at the remaining buffer space and
|
||||
// never closes the stream, so fprint sees a short i32 result, not
|
||||
// io.error. Verifies the inner-loop arithmetic adds the actual byte
|
||||
// count rather than the requested length.
|
||||
// A partial fixed-buffer write cannot be reported as a successful prefix.
|
||||
// putbytes retries the unwritten suffix; the full sink then returns nomem.
|
||||
|
||||
@test fn fprintfixedshort() void = {
|
||||
let buf: [3]u8;
|
||||
@@ -174,8 +204,8 @@ fn errsource() io.stream = {
|
||||
|
||||
let r: (size | io.error) = fmt.fprint(s, "hello");
|
||||
match (r) {
|
||||
case let n: size => { assert(!(n: i32 != 3)); };
|
||||
case let eioe: io.error => abort();
|
||||
case let n: size => abort();
|
||||
case let eioe: io.error => { assert(eioe is nomem); };
|
||||
};
|
||||
assert(!(!streq(memio.string(&mem), "hel")));
|
||||
|
||||
@@ -183,6 +213,24 @@ fn errsource() io.stream = {
|
||||
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
|
||||
// the short-write path above, which keeps returning i32 from a partial
|
||||
// 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(); };
|
||||
};
|
||||
|
||||
@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 = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
let s: io.stream = &mem.vt;
|
||||
@@ -226,6 +288,27 @@ fn errsource() io.stream = {
|
||||
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 = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
let s: io.stream = &mem.vt;
|
||||
@@ -330,6 +413,19 @@ fn errsource() io.stream = {
|
||||
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 = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
let s: io.stream = &mem.vt;
|
||||
@@ -382,6 +478,33 @@ fn errsource() io.stream = {
|
||||
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 = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
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 —
|
||||
// the codegen-smell repro shape (task #18). Pre-task-#18, the str arm
|
||||
// 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);
|
||||
};
|
||||
|
||||
// Pins the f64 formattable arm landed under task #17 (unblocked by #30
|
||||
// — the variant-widen-from-X0 fix). strconv.f64tos drives the render;
|
||||
// see lib/strconv/strconv.ww for the documented subset (fixed-point,
|
||||
// 6 fractional digits, trailing-zero trim, magnitudes ≥ 9e18 → "huge",
|
||||
// no NaN/±Inf detection).
|
||||
// strconv.f64tos drives shortest round-trippable decimal output, including
|
||||
// signed zero, infinities, NaNs, subnormals, and scientific notation.
|
||||
//
|
||||
// Mods scope under v1: width / alignment / pad / sign mods honored.
|
||||
// `prec` and `base` ignored — graduate when strconv grows ffmt/fflags.
|
||||
// Width / alignment / pad / sign mods are honored. Precision and
|
||||
// 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 = {
|
||||
let mem: memio.stream = memio.dynamic();
|
||||
|
||||
Reference in New Issue
Block a user