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:
@@ -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; };
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user