fmt: make formatting complete and rune-safe
This commit is contained in:
@@ -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