diff --git a/lib/CLAUDE.md b/lib/CLAUDE.md index a253fcc5..d46dc0e8 100644 --- a/lib/CLAUDE.md +++ b/lib/CLAUDE.md @@ -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))`, diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index d2864255..1bfc3a2f 100644 --- a/lib/fmt/fmt.ww +++ b/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) = { // width (first char 1..9) // . 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(); }; diff --git a/lib/fmt/fmt_test.ww b/lib/fmt/fmt_test.ww index e4621333..c145e09e 100644 --- a/lib/fmt/fmt_test.ww +++ b/lib/fmt/fmt_test.ww @@ -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();