lib/fmt+test: add f64 dispatch arm (#17)

#30 (82be8b9) unblocked tagged-union widen for runtime f64. Add the
f64 arm to fmt's formattable union and dispatch.

`formattable` gains an f64 case (appended last to preserve existing
tag indices). fdprint / fprint mirror their i64-arm shape. formatraw
peels strconv's natural '-' so signof folds neg/+/space uniformly
with i64. formatfield uses the inline widen-per-arm #18 sidestep.
rawlenf64 renders via strconv.f64tos to count bytes for width
alignment (Hare's print.ha:53 uses an io::empty sink for this; ww
has none yet, so we render twice — acceptable v1 trade).

Width / alignment / pad / sign mods honored. prec / base ignored
with inline rationale (no ffmt/fflags in mods yet; Hare aborts on
non-DEC base, ww silently falls through). NaN/Inf deferred — Inf
renders deterministically as "huge"/"-huge" via strconv's `f >= cap`
path; NaN is garbage. Detection waits on f64↔u64 bit-reinterpret in
cgen.

13 test rows at signalled 31-43 cover basic/int-valued/neg/zero/
small-frac/huge/sign±/space/width-right/width-left + fprint
variadic + bsprintf + asprintf sinks. Each pins exact byte output.

Three rows bind negative literal via intermediate `let nv: f64 =
-2.5;` to route around #40 (cstage drops payload on N_UNARY-of-
N_FLOATLIT in tagged-union widen). Comments cite #40 at each row.
Probe at .ai/probe_f64_unary_neg.ww.

Strconv f64tos is fixed-point today; graduate to Ryū (ref/hare/
strconv/ftos.ha:432) when needed.
This commit is contained in:
2026-05-16 14:36:45 +09:00
parent 82be8b9b4b
commit 7f320d3e75
2 changed files with 309 additions and 2 deletions

View File

@@ -480,6 +480,212 @@ fn closedstream(s: *io.stream) void = {
os.free(r.ptr: *void, r.len: u64);
};
// ---- f64 dispatch arm -------------------------------------------------
//
// 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).
//
// Mods scope under v1: width / alignment / pad / sign mods honored.
// `prec` and `base` ignored — graduate when strconv grows ffmt/fflags.
@test fn fprintf_f64_basic() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 1.5);
match (r) {
case let n: i32 => { if (n != 3) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "1.5")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
@test fn fprintf_f64_int_valued() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 1.0);
match (r) {
case let n: i32 => { if (n != 1) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "1")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
// Negative-valued f64 rendered through fmt. PENDING task #40 (cstage
// variant-widen drops payload on N_UNARY-of-N_FLOATLIT, residual of
// #30): the literal-arg form below renders as "0" on cstage today —
//
// fmt.fprintf(&s, "{}", -2.5) // ✗ cstage: "0"
// // ✓ wwstage: "-2.5"
//
// Workaround until #40 lands: bind to a typed local so the widen
// hits the post-#30 N_IDENT path. Matches the cast form
// (`-2.5: f64`), which also works on both stages. Realistic caller
// shape (negatives usually arrive through a variable, not as a
// literal printf arg). Probe: .ai/probe_f64_unary_neg.ww.
@test fn fprintf_f64_neg() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let nv: f64 = -2.5;
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", nv);
match (r) {
case let n: i32 => { if (n != 4) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "-2.5")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
@test fn fprintf_f64_zero() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 0.0);
match (r) {
case let n: i32 => { if (n != 1) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "0")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
@test fn fprintf_f64_small_frac() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 0.05);
match (r) {
case let n: i32 => { if (n != 4) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "0.05")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
// Pins the strconv.f64tos "huge" fallback through the fmt dispatch.
// Catches any regression where mods accidentally pre-truncate the view.
@test fn fprintf_f64_huge() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 9.5e18);
match (r) {
case let n: i32 => { if (n != 4) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "huge")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
// Sign-mod fold: '+' on positive, '-' still wins on negative (the
// natural '-' from strconv is peeled and signof reapplies). Negative
// routed through a typed local — same task #40 workaround as
// fprintf_f64_neg above.
@test fn fprintf_f64_sign_plus() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let nv: f64 = -1.5;
let r: (i32 | io.closed) = fmt.fprintf(&s, "{:+} {:+}", 1.5, nv);
match (r) {
case let n: i32 => { if (n != 9) { fail(); }; }; // "+1.5 -1.5"
case io.closed => fail();
};
if (!streq(memio.string(&mem), "+1.5 -1.5")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
@test fn fprintf_f64_sign_space() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{: }", 1.5);
match (r) {
case let n: i32 => { if (n != 4) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), " 1.5")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
// Width + alignment round-trip through rawlenf64 (sign-aware len count).
@test fn fprintf_f64_width_right() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{:8}", 1.5);
match (r) {
case let n: i32 => { if (n != 8) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), " 1.5")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
@test fn fprintf_f64_width_left() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprintf(&s, "{:-8}", 1.5);
match (r) {
case let n: i32 => { if (n != 8) { fail(); }; };
case io.closed => fail();
};
if (!streq(memio.string(&mem), "1.5 ")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
// Legacy fprint variadic — covers the non-printf surface arm (where
// formatraw isn't reached because there's no mods parser).
@test fn fprint_f64_variadic() void = {
let mem: memio.state;
let s: io.stream;
memio.dynamic(&mem, &s);
let r: (i32 | io.closed) = fmt.fprint(&s, 1.5, "x");
match (r) {
case let n: i32 => { if (n != 5) { fail(); }; }; // "1.5 x"
case io.closed => fail();
};
if (!streq(memio.string(&mem), "1.5 x")) { fail(); };
let c: (void | io.closed) = io.close(&s);
match (c) { case void => {}; case io.closed => fail(); };
};
@test fn bsprintf_f64() void = {
let buf: [16]u8;
let r: (str | io.closed) = fmt.bsprintf(buf[0:16], "x={}", 1.5);
match (r) {
case let s: str => { if (!streq(s, "x=1.5")) { fail(); }; };
case io.closed => fail();
};
};
// Heap sink + both signs. Negative through typed local per task #40
// workaround (see fprintf_f64_neg comment).
@test fn asprintf_f64() void = {
let nv: f64 = -2.5;
let r: str = fmt.asprintf("{} {}", 1.5, nv);
if (!streq(r, "1.5 -2.5")) { fail(); };
os.free(r.ptr: *void, r.len: u64);
};
export fn main() i32 = {
signalled = 1; fprintbarestr();
signalled = 2; fprintintstr();
@@ -511,5 +717,18 @@ export fn main() i32 = {
signalled = 28; asprintf_growth();
signalled = 29; asprintf_empty();
signalled = 30; asprintf_indexed_mods();
signalled = 31; fprintf_f64_basic();
signalled = 32; fprintf_f64_int_valued();
signalled = 33; fprintf_f64_neg();
signalled = 34; fprintf_f64_zero();
signalled = 35; fprintf_f64_small_frac();
signalled = 36; fprintf_f64_huge();
signalled = 37; fprintf_f64_sign_plus();
signalled = 38; fprintf_f64_sign_space();
signalled = 39; fprintf_f64_width_right();
signalled = 40; fprintf_f64_width_left();
signalled = 41; fprint_f64_variadic();
signalled = 42; bsprintf_f64();
signalled = 43; asprintf_f64();
return 0;
};