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

@@ -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; };
};
};