From 7f320d3e753256b7663fdc85a6f2ce26c8a56e71 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 16 May 2026 14:36:45 +0900 Subject: [PATCH] lib/fmt+test: add f64 dispatch arm (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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. --- lib/fmt/fmt.ww | 92 ++++++++++++++++++- lib/fmt/fmttest.ww | 219 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 2 deletions(-) diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index f48fa022..c7b7e903 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -68,8 +68,14 @@ fn i64dec(v: i64) str = { // 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). -export type formattable = (i64 | str | bool | rune); +// 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. +export type formattable = (i64 | str | bool | rune | f64); // ---- fd sinks -------------------------------------------------------- @@ -117,6 +123,15 @@ export fn fdprint(fd: i32, args: formattable...) i64 = { if (n < 0) { return n; }; total += n; }; + case let v: f64 => { + // strconv.f64tos's static-buffer view is consumed + // immediately by os.write; no intervening strconv + // call against the same buffer between bind and write. + let s: str = strconv.f64tos(v); + let r: i64 = os.write(fd, s.ptr, s.len: u64); + if (r < 0) { return r; }; + total += r; + }; }; i += 1; }; @@ -201,6 +216,18 @@ export fn fprint(s: *io.stream, args: formattable...) (i32 | io.closed) = { case io.closed => { let c: io.closed; return c; }; }; }; + case let v: f64 => { + // strconv.f64tos returns a static-buffer view — + // emit to the sink before yielding control to the + // next arg iteration so no sibling f64/i64 strconv + // call clobbers the buffer mid-flight. + let view: str = strconv.f64tos(v); + let rs: (i32 | io.closed) = putbytes(s, view.ptr, view.len); + match (rs) { + case let m: i32 => { total += m; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; }; i += 1; }; @@ -487,10 +514,66 @@ fn formatraw(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = { buf[0] = r: u8; return putbytes(out, &buf[0], 1); }; + case let v: f64 => { + // strconv.f64tos already prepends '-' for negative values; + // peel it back off so [[signof]] can fold neg/plus/space + // mods uniformly with the i64 arm. The view bytes are then + // emitted directly — strconv's static buf is not held past + // the putbytes call (no sibling strconv call lands between). + // + // Mods scope: width / alignment / pad / sign honored via + // the same raw-then-pad machinery as i64. `prec` ignored — + // strconv.f64tos has no precision knob; graduate when a + // Ryū-shaped strconv lands and mods grows ffmt / fflags. + // `base` ignored (Hare does too — base is int-only). + // NaN/±Inf print as whatever strconv emits today (garbage); + // strconv detection blocked on f64↔u64 bit-reinterpret cgen. + let view: str = strconv.f64tos(v); + let neg_flag: bool = false; + if (view.len > 0 && view.ptr[0] == 45u8) { // '-' + neg_flag = true; + view.ptr = view.ptr + 1u64; + view.len -= 1; + }; + let sb: u8 = signof(neg_flag, m); + let total: i32 = 0; + if (sb != 0u8) { + let buf: [1]u8; + buf[0] = sb; + let r: (i32 | io.closed) = putbytes(out, &buf[0], 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + let r: (i32 | io.closed) = putbytes(out, view.ptr, view.len); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + return total; + }; }; let z: io.closed; return z; // unreachable — match is exhaustive }; +// rawlenf64 — bytes the raw render of `v` under `m` would emit. +// Calls strconv.f64tos to count digits + decimal point; peels off +// the natural '-' so signof's neg/plus/space byte is counted exactly +// once, the same accounting [[rawleni64]] does. +fn rawlenf64(v: f64, m: *mods) i32 = { + let view: str = strconv.f64tos(v); + let body: i32 = view.len; + let had_neg: bool = false; + if (body > 0 && view.ptr[0] == 45u8) { + had_neg = true; + body -= 1; + }; + let signlen: i32 = 0; + if (signof(had_neg, m) != 0u8) { signlen = 1; }; + return signlen + body; +}; + // rawlen — bytes formatraw would emit for `arg` under `m`. Used by // formatone's width-alignment path (Hare's print.ha:53 calls // format_raw with io::empty for the same purpose). @@ -500,6 +583,7 @@ fn rawlen(arg: formattable, m: *mods) i32 = { case let v: str => return rawlenstr(v, m); case let b: bool => { if (b) { return 4; }; return 5; }; case let r: rune => return 1; + case let v: f64 => return rawlenf64(v, m); }; return 0; // unreachable — match is exhaustive }; @@ -574,6 +658,10 @@ fn formatfield(out: *io.stream, f: field, m: *mods) (i32 | io.closed) = { let a: formattable = r; return formatone(out, a, m); }; + case let v: f64 => { + let a: formattable = v; + return formatone(out, a, m); + }; case let p: *mods => { fmtabort(); let c: io.closed; return c; }; }; }; diff --git a/lib/fmt/fmttest.ww b/lib/fmt/fmttest.ww index 0257f31c..a07b5734 100644 --- a/lib/fmt/fmttest.ww +++ b/lib/fmt/fmttest.ww @@ -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; };