lib/strconv: f64tos Ryū shortest float→string (#106 fold-5a)

Graduate f64tos from the lossy fixed-point placeholder to Ryū shortest-
round-trippable (ftos.ha:432 + ftos_ryu.ha). f64tos(n:f64) str, G-format
(void/NONE) — the faithful documented subset (full parametric fftosf is
dead code for G/void/NONE → deferred #64). Ryū core decomposed: struct-
RETURN + scalar params (Hare's r128 idiom; avoids tuple-ABI #163-166).
f64 powers tables [15][2]/[13][2]u64 (2D #156). Zero float literals.
Both E+F encode paths reachable+tested, no dead code.

Graduation (lib-note "don't keep both"): old lossy f64tos deleted; fmt
fprintf_f64_huge "huge"→"9.5e18" (improvement). 5 value-faithful filed-
bug dodges (byte-id, documented): #167/#169/#170/#43/#144. Test 908.
Make test 186/186 incl 990-997 byte-id + combined_ww_fresh.

Drew's strconv 5-fold plan — primary completion (f64tos). f32tos coda
#67 (behind #143); parametric ftosf #64.
This commit is contained in:
2026-05-27 15:39:03 +09:00
parent 81796cd533
commit 0e66073e32
11 changed files with 3219 additions and 762 deletions

549
lib/strconv/ftos.ww Normal file
View File

@@ -0,0 +1,549 @@
// strconv — float→string via Ryū (shortest round-trippable decimal).
// Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) +
// ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams,
// https://doi.org/10.1145/3192366.3192369 — Hare translated it from the
// reference C (https://github.com/ulfjack/ryu); ww follows Hare.
//
// SCOPE — the f64tos shortest-representation subset (Hare's ffmt::G,
// prec=void, fflags::NONE). Two deferrals:
// - f32tos (ftos.ha:448) + its f32 Ryū sub-path (f32todecf32 +
// mulpow5inv/pow5_divpow2 + mulshift32 + the *32 helpers + the
// F32_POW5_*_BITCOUNT defs) → fold-5b (task #67), gated on the #143
// f32-arg-push cgen fix: f32tos must call math.f32bits(n), passing
// an f32 arg, which spills MOVSD (cstage) vs MOVSS (wwstage) →
// 990-997 byte-id break. Not isolatable (f32bits IS the reinterpret),
// only fixable. f32tos IS a real Hare entry — it ships in #67.
// - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs
// io::handle/memio + a `(size|io::error)?` per appendrune (#158);
// for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback
// machinery is provably dead code — `ok` is always true → init_dec/
// compute_round/round unreachable — which bootstrap-coverage rejects).
// The lib note blesses "a documented subset". This file ships ZERO float
// literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the
// wwdump TK_FLOAT embedding concern is moot.
//
// Decomposition divergences (the #163-166 tuple/struct-ABI cluster —
// ww's partial tuple support miscompiles the shapes Hare uses; the
// WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's
// own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for
// u128mul, and fold-4/stof.ww decomposed likewise):
// - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params
// `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple
// return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return
// reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO
// struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww).
// - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)`
// return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the
// math/floats.ww frexpf64 precedent).
// - dead `mulshift64` (tuple-param, never called) + dead
// `F32/F64_DECIMAL_DIGITS` dropped.
//
// Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha):
// - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local
// (stof.ww hex_to_bits precedent).
// - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block.
// comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound
// local + block. `assert()` → `os.assert(cond,msg)`.
// - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]`
// (#155 / #156, stof.ww eisel_lemire precedent).
// - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64
// at each use (Hare promotes; ww is strict — int-machine-word note).
// - a `(N: uint)` cast embedded inside an array subscript `[ ]` is
// rejected by the ww parser → hoist to a named local before the
// index (decimal.ww "hoist size casts" note); see init_dec_mant_exp
// + encode_e_dec.
package strconv;
import math;
import os;
// ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's
// "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition.
// (u128mul + the r128 struct live in stof.ww, fold-4's first consumer;
// reused in-package here.)
fn u128rshift(lo: u64, hi: u64, s: u32) u64 = {
os.assert(s <= 64u32, "strconv.u128rshift: s > 64");
return (hi << (64u64 - (s: u64))) | (lo >> (s: u64));
};
// ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value.
fn pow5fac(v: u64) u32 = {
let value: u64 = v;
let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64)
let n_div_5: u64 = 3689348814741910323u64;
let count: u32 = 0u32;
for (true) {
os.assert(value != 0u64, "strconv.pow5fac: value == 0");
value *= m_inv_5;
if (value > n_div_5) { break; };
count += 1u32;
};
return count;
};
// ref/hare/strconv/ftos_ryu.ha:64.
fn ibool(b: bool) u8 = {
if (b) { return 1u8; };
return 0u8;
};
// ref/hare/strconv/ftos_ryu.ha:66-67.
fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; };
// ref/hare/strconv/ftos_ryu.ha:69.
fn pow2multiple(v: u64, p: u32) bool = {
os.assert(v > 0u64, "strconv.pow2multiple: v == 0");
os.assert(p < 64u32, "strconv.pow2multiple: p >= 64");
return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64;
};
// ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple.
// Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return →
// this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound
// `v_minus` + block.
type ryuv = struct { vp: u64, vr: u64, vm: u64 };
fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = {
let mm: u64 = m << 1u64;
let r0: r128 = u128mul(mm, mul0);
let r1: r128 = u128mul(mm, mul1);
let lo: u64 = r0.lo;
let tmp: u64 = r0.hi;
let mid: u64 = tmp + r1.lo;
let hi: u64 = r1.hi + (ibool(mid < tmp): u64);
let lo2: u64 = lo + mul0;
let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64);
let hi2: u64 = hi + (ibool(mid2 < mid): u64);
let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32));
let v_minus: u64 = 0u64;
if (mm_shift == 1u32) {
let lo3: u64 = lo - mul0;
let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64);
let hi3: u64 = hi - (ibool(mid3 > mid): u64);
v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32));
} else {
let lo3: u64 = lo + lo;
let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64);
let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64);
let lo4: u64 = lo3 - mul0;
let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64);
let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64);
v_minus = u128rshift(mid4, hi4, ((j - 64): u32));
};
let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32));
return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus };
};
// ref/hare/strconv/ftos_ryu.ha:140.
fn log2pow5(e: u32) u32 = {
os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528");
return (e * 1217359u32) >> 19u32;
};
// ref/hare/strconv/ftos_ryu.ha:145-147.
fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; };
fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); };
// ref/hare/strconv/ftos_ryu.ha:149.
fn log10pow2(e: u32) u32 = {
os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650");
return (e * 78913u32) >> 18u32;
};
// ref/hare/strconv/ftos_ryu.ha:154.
fn log10pow5(e: u32) u32 = {
os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620");
return (e * 732923u32) >> 20u32;
};
// ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the
// inverse power of five. 2-tuple kept (works); row-bind → double-index.
fn f64computeinvpow5(i: u32) (u64, u64) = {
let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32);
let base2: u32 = base * (POW5_TABLE_SZ: u32);
let off: u32 = base2 - i;
if (off == 0u32) {
return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]);
};
let m: u64 = POW5_TABLE[off];
let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]);
let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64);
let high1: u64 = r1.hi;
let low1: u64 = r1.lo;
let high0: u64 = r0.hi;
let low0: u64 = r0.lo;
let sum: u64 = high0 + low1;
if (sum < high0) {
high1 += 1u64;
};
let delta: u32 = pow5bits(base2) - pow5bits(i);
let res0: u64 = u128rshift(low0, sum, delta) + 1u64 +
(((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64);
let res1: u64 = u128rshift(sum, high1, delta);
return (res0, res1);
};
// ref/hare/strconv/ftos_ryu.ha:246.
fn f64computepow5(i: u32) (u64, u64) = {
let base: u32 = i / (POW5_TABLE_SZ: u32);
let base2: u32 = base * (POW5_TABLE_SZ: u32);
let off: u32 = i - base2;
if (off == 0u32) {
return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]);
};
let m: u64 = POW5_TABLE[off];
let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]);
let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]);
let high1: u64 = r1.hi;
let low1: u64 = r1.lo;
let high0: u64 = r0.hi;
let low0: u64 = r0.lo;
let sum: u64 = high0 + low1;
if (sum < high0) {
high1 += 1u64;
};
let delta: u32 = pow5bits(i) - pow5bits(base2);
let res0: u64 = u128rshift(low0, sum, delta) +
(((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64);
let res1: u64 = u128rshift(sum, high1, delta);
return (res0, res1);
};
// ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64:
// value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32
// (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second
// field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww
// byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value
// always fits i32 (cast at the init_dec_mant_exp call site).
type decf64 = struct { mantissa: u64, exponent: i64 };
// ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw
// IEEE-754 fields of an f64.
fn f64todecf64(mantissa: u64, exponent: u32) decf64 = {
let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32;
let m2: u64 = 0u64;
if (exponent == 0u32) {
e2 = 1i32 - e2;
m2 = mantissa;
} else {
e2 = (exponent: i32) - e2;
m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa;
};
let accept_bounds: bool = (m2 & 1u64) == 0u64;
let mv: u64 = 4u64 * m2;
let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32;
let vp: u64 = 0u64;
let vr: u64 = 0u64;
let vm: u64 = 0u64;
let e10: i32 = 0i32;
let vm_trailing_zeros: bool = false;
let vr_trailing_zeros: bool = false;
if (e2 >= 0i32) {
let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32);
e10 = q: i32;
let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32;
let i: i32 = -e2 + ((q + k): i32);
let pow5 = f64computeinvpow5(q);
let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift);
vp = res.vp; vr = res.vr; vm = res.vm;
if (q <= 21u32) {
if ((mv - 5u64 * (mv / 5u64)) == 0u64) {
vr_trailing_zeros = pow5multiple(mv, q);
} else if (accept_bounds) {
vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q);
} else {
vp -= (ibool(pow5multiple(mv + 2u64, q)): u64);
};
};
} else {
let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32);
e10 = e2 + (q: i32);
let i: i32 = -e2 - (q: i32);
let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32);
let j: i32 = (q: i32) - k;
let pow5 = f64computepow5(i: u32);
let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift);
vp = res.vp; vr = res.vr; vm = res.vm;
if (q <= 1u32) {
vr_trailing_zeros = true;
if (accept_bounds) {
vm_trailing_zeros = mm_shift == 1u32;
} else {
vp -= 1u64;
};
} else if (q < 63u32) {
vr_trailing_zeros = pow2multiple(mv, q);
};
};
let removed: i32 = 0i32;
let last_removed_digit: u8 = 0u8;
let output: u64 = 0u64;
if (vm_trailing_zeros || vr_trailing_zeros) {
for (true) {
let vpby10: u64 = vp / 10u64;
let vmby10: u64 = vm / 10u64;
if (vpby10 <= vmby10) { break; };
let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32);
let vrby10: u64 = vr / 10u64;
let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32);
vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32);
vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8);
last_removed_digit = (vrmod10: u8);
vr = vrby10; vp = vpby10; vm = vmby10;
removed += 1i32;
};
if (vm_trailing_zeros) {
for (true) {
let vmby10: u64 = vm / 10u64;
let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32);
if (vmmod10 != 0u32) { break; };
let vpby10: u64 = vp / 10u64;
let vrby10: u64 = vr / 10u64;
let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32);
vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8);
last_removed_digit = (vrmod10: u8);
vr = vrby10; vp = vpby10; vm = vmby10;
removed += 1i32;
};
};
if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) {
last_removed_digit = 4u8; // round to even
};
let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros));
let cond2: bool = last_removed_digit >= 5u8;
output = vr + (ibool(cond1 || cond2): u64);
} else {
let round_up: bool = false;
let vpby100: u64 = vp / 100u64;
let vmby100: u64 = vm / 100u64;
if (vpby100 > vmby100) {
let vrby100: u64 = vr / 100u64;
let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32);
round_up = vrmod100 >= 50u32;
vr = vrby100; vp = vpby100; vm = vmby100;
removed += 2i32;
};
for (true) {
let vmby10: u64 = vm / 10u64;
let vpby10: u64 = vp / 10u64;
if (vpby10 <= vmby10) { break; };
let vrby10: u64 = vr / 10u64;
let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32);
round_up = vrmod10 >= 5u32;
vr = vrby10; vp = vpby10; vm = vmby10;
removed += 1i32;
};
output = vr + (ibool(vr == vm || round_up): u64);
};
let exp: i32 = e10 + removed;
return decf64 { exponent = (exp: i64), mantissa = output };
};
// ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void /
// fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase
// arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE
// for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always
// G) and are NOT ported — porting them stubbed would be untested dead
// code. The parametric ftosf/ffmt/fflags surface is deferred (task #64;
// needs a parametric consumer + io::handle + #158). ====
// ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17).
fn declen(n: u64) uint = {
os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17");
if (n >= 100000000000000000u64) { return (18u32: uint); };
if (n >= 10000000000000000u64) { return (17u32: uint); };
if (n >= 1000000000000000u64) { return (16u32: uint); };
if (n >= 100000000000000u64) { return (15u32: uint); };
if (n >= 10000000000000u64) { return (14u32: uint); };
if (n >= 1000000000000u64) { return (13u32: uint); };
if (n >= 100000000000u64) { return (12u32: uint); };
if (n >= 10000000000u64) { return (11u32: uint); };
if (n >= 1000000000u64) { return (10u32: uint); };
if (n >= 100000000u64) { return (9u32: uint); };
if (n >= 10000000u64) { return (8u32: uint); };
if (n >= 1000000u64) { return (7u32: uint); };
if (n >= 100000u64) { return (6u32: uint); };
if (n >= 10000u64) { return (5u32: uint); };
if (n >= 1000u64) { return (4u32: uint); };
if (n >= 100u64) { return (3u32: uint); };
if (n >= 10u64) { return (2u32: uint); };
return (1u32: uint);
};
// ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent)
// into the decimal `d`. `mantissa` is mutated in Hare → local `mant`.
fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = {
// Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded
// inside an array subscript (decimal.ww "hoist size casts" note).
let U_ZERO: uint = (0u32: uint);
let U_ONE: uint = (1u32: uint);
let mant: u64 = mantissa;
let dl: uint = declen(mant);
let i: uint = U_ZERO;
for (i < dl) {
d.digits[dl - i - U_ONE] = (mant % 10u64): u8;
mant /= 10u64;
i += U_ONE;
};
d.nd = (dl: size);
d.dp = (dl: i32) + exponent;
};
// ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the
// *tos static-buffer convention replaces Hare's io::handle sink).
fn putstr(buf: []u8, out: i32, s: str) i32 = {
let o: i32 = out;
let k: i32 = 0i32;
for (k < s.len) {
buf[o] = s[k];
o += 1i32;
k += 1i32;
};
return o;
};
// ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable
// logic only). Writes into `buf` at cursor `out`, returns the new cursor.
fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = {
let o: i32 = out;
let lo: i32 = 0i32;
if (d.dp <= 0i32) { lo = d.dp - 1i32; };
let hi: i32 = d.dp;
if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); };
if (hi > (d.nd: i32) && d.dp <= 0i32) {
hi = (d.nd: i32);
} else if (hi > d.dp && d.dp > 0i32) {
hi = d.dp;
if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); };
};
let i: i32 = lo;
for (i < hi) {
if (i == d.dp) {
buf[o] = 46u8; // '.'
o += 1i32;
};
if (0i32 <= i && i < (d.nd: i32)) {
buf[o] = (d.digits[i] + 48u8): u8;
} else {
buf[o] = 48u8; // '0'
};
o += 1i32;
i += 1i32;
};
return o;
};
// ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable
// logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad.
fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = {
let o: i32 = out;
os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0");
buf[o] = (d.digits[0] + 48u8): u8;
o += 1i32;
if ((d.nd: i32) > 1i32) {
buf[o] = 46u8; // '.'
o += 1i32;
};
let i: size = (1u64: size);
for (i < d.nd) {
buf[o] = (d.digits[i] + 48u8): u8;
o += 1i32;
i += (1u64: size);
};
buf[o] = 101u8; // 'e'
o += 1i32;
let e: i32 = d.dp - 1i32;
if (e < 0i32) {
e = -e;
buf[o] = 45u8; // '-'
o += 1i32;
};
// Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`).
let U_ONE: uint = (1u32: uint);
let U_TWO: uint = (2u32: uint);
let U_THREE: uint = (3u32: uint);
let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits
let l: uint = declen(e: u64);
let k: uint = (0u32: uint);
for (k < l) {
ebuf[U_TWO - k] = (e % 10i32): u8;
e /= 10i32;
k += U_ONE;
};
let m: uint = U_THREE - l;
for (m < U_THREE) {
buf[o] = (ebuf[m] + 48u8): u8;
o += 1i32;
m += U_ONE;
};
return o;
};
// ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a
// view into a static buffer overwritten on the next call (the *tos
// convention; see strings.dup to retain). Equivalent to Hare's ftosf
// with format G + precision void. The fftosf G/void/NONE path is inlined
// (the parametric surface is deferred — task #64).
//
// Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' +
// exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer
// emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw
// split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused.
let f64tos_buf: [32]u8;
export fn f64tos(n: f64) str = {
let bits: u64 = math.f64bits(n);
let mantissa: u64 = bits & math.F64_MANTISSA_MASK;
let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32;
let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64;
let special: bool = exponent == (math.F64_EXPONENT_MASK: u32);
let o: i32 = 0i32;
let r: str;
r.ptr = &f64tos_buf[0];
// NaN carries no sign prefix (ftos.ha:331-333, before sign handling).
if (special && mantissa != 0u64) {
o = putstr(f64tos_buf[0:32], o, "nan");
r.len = o;
return r;
};
if (sign) {
f64tos_buf[o] = 45u8; // '-'
o += 1i32;
};
if (special) {
o = putstr(f64tos_buf[0:32], o, "infinity");
r.len = o;
return r;
};
if (exponent == 0u32 && mantissa == 0u64) {
f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE)
o += 1i32;
r.len = o;
return r;
};
let d = decimal { ... };
// Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a
// scalar-field read of a LOCAL struct (`d.nd`) to a bogus global
// symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers
// correctly in both stages (the stof.ww/decimal.ww *decimal precedent)
// — #170. The init/trim/encode calls already took &d; route via pd.
let pd: *decimal = &d;
let dd: decf64 = f64todecf64(mantissa, exponent);
init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32));
// ok = !ffpoint(NONE) || ... is always true → no multiprecision
// fallback (ftos.ha:365). f == G → trim (ftos.ha:386).
trim(pd);
if (pd.nd == (0u64: size)) {
f64tos_buf[o] = 48u8; // rounded to zero
o += 1i32;
} else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) {
o = encode_e_dec(pd, f64tos_buf[0:32], o);
} else {
o = encode_f_dec(pd, f64tos_buf[0:32], o);
};
r.len = o;
return r;
};

104
lib/strconv/ftos_data.ww Normal file
View File

@@ -0,0 +1,104 @@
// strconv — Ryū float→string lookup tables + bit-count constants.
// Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data
// fold (strconv #106 fold-5): no logic, consumed by ftos.ww's
// f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores).
//
// File-organisation divergence: Hare keeps these tables INLINE in
// ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring
// the stof.ww / stof_data.ww split) — same `package strconv`, so the
// tables stay visible to ftos.ww with no qualification.
//
// Spelling divergences (same as stof_data.ww, candidate #130 + rule-12):
// - Hare `const TBL = [...]` → ww module-level `let` (ww has no
// module-`const` keyword; the values are never written).
// - every literal carries its element-width suffix (`u64`/`u32`):
// cstage rejects bare integer literals in `[N]uXX` init while
// wwstage accepts them; the suffixed form is the only shape both
// stages agree on.
// - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened);
// the 2D module-level static-init + double-index read landed in
// #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64.
package strconv;
// ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split
// power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32
// at each use site (Hare promotes a u8 def inside mixed-width arithmetic;
// ww is strict — explicit cast, project_int_machine_word_derived_limits).
// The F32_POW5_*_BITCOUNT siblings (ftos_ryu.ha:162-163) land in fold-5b
// (task #67) with their only consumer, f32todecf32 — omitted here to keep
// this fold dead-code-free.
def F64_POW5_INV_BITCOUNT: u8 = 125u8;
def F64_POW5_BITCOUNT: u8 = 125u8;
// ref/hare/strconv/ftos_ryu.ha:165-181.
let F64_POW5_INV_SPLIT2: [15][2]u64 = [
[1u64, 2305843009213693952u64],
[5955668970331000884u64, 1784059615882449851u64],
[8982663654677661702u64, 1380349269358112757u64],
[7286864317269821294u64, 2135987035920910082u64],
[7005857020398200553u64, 1652639921975621497u64],
[17965325103354776697u64, 1278668206209430417u64],
[8928596168509315048u64, 1978643211784836272u64],
[10075671573058298858u64, 1530901034580419511u64],
[597001226353042382u64, 1184477304306571148u64],
[1527430471115325346u64, 1832889850782397517u64],
[12533209867169019542u64, 1418129833677084982u64],
[5577825024675947042u64, 2194449627517475473u64],
[11006974540203867551u64, 1697873161311732311u64],
[10313493231639821582u64, 1313665730009899186u64],
[12701016819766672773u64, 2032799256770390445u64],
];
// ref/hare/strconv/ftos_ryu.ha:183-188.
let POW5_INV_OFFSETS: [19]u32 = [
0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32,
0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32,
0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32,
0x00000000u32,
];
// ref/hare/strconv/ftos_ryu.ha:190-204.
let F64_POW5_SPLIT2: [13][2]u64 = [
[0u64, 1152921504606846976u64],
[0u64, 1490116119384765625u64],
[1032610780636961552u64, 1925929944387235853u64],
[7910200175544436838u64, 1244603055572228341u64],
[16941905809032713930u64, 1608611746708759036u64],
[13024893955298202172u64, 2079081953128979843u64],
[6607496772837067824u64, 1343575221513417750u64],
[17332926989895652603u64, 1736530273035216783u64],
[13037379183483547984u64, 2244412773384604712u64],
[1605989338741628675u64, 1450417759929778918u64],
[9630225068416591280u64, 1874621017369538693u64],
[665883850346957067u64, 1211445438634777304u64],
[14931890668723713708u64, 1565756531257009982u64],
];
// ref/hare/strconv/ftos_ryu.ha:206-211.
let POW5_OFFSETS: [21]u32 = [
0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32,
0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32,
0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32,
0x55559155u32, 0x51405555u32, 0x00000105u32,
];
// ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in
// f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those
// arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage
// rejects a def-named array length — "array length must be an integer
// literal"; wwstage accepts it but emits an empty DATAW — divergence
// #167, so the literal `26` is the only shape both stages agree on;
// matches decimal.ww's `[800]u8` array-dimension-literal precedent).
def POW5_TABLE_SZ: u8 = 26u8;
// ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is
// commented out in Hare too — it lives implicitly in the SPLIT2 tables).
let POW5_TABLE: [26]u64 = [
1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64,
390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64,
1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64,
762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64,
476837158203125u64, 2384185791015625u64, 11920928955078125u64,
59604644775390625u64, 298023223876953125u64,
];

View File

@@ -261,91 +261,12 @@ export fn stou8(s: str, b: base) (u8 | invalid | overflow) = {
return 0: invalid;
};
// f64tos — convert v to a decimal string. Returns owned str; release
// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is
// fixed-point only, max 6 fractional digits, no NaN/Inf support —
// see graduate-to-Ryū note below).
//
// Surface:
//
// - finite values only. NaN/±Inf detection needs an f64→u64 bit
// reinterpret cast that the cgen doesn't expose yet.
// - fixed-point only, up to 6 fractional digits. Trailing zeros
// after the decimal point are trimmed. Trailing '.' is dropped.
// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast)
// fall back to the literal token "huge". Hare would print these
// in scientific notation via Ryū; we will graduate when the
// compiler grows the bit-reinterpret cast.
//
// Round-trip is therefore lossy past 6 fractional digits.
//
// No float literals in the body — 990's wwdump diff requires this
// file's TK_FLOAT count to match between C and ww front-ends, and
// the ww-side wwdump currently skips TK_FLOAT.fval while the C side
// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses:
// build f64 constants via int-to-f64 casts.
let f64tos_buf: [64]u8;
export fn f64tos(v: f64) str = {
let out: i32 = 0;
let f: f64 = v;
let zero: f64 = 0: f64;
if (f < zero) {
f64tos_buf[out] = 45u8; // '-'
out += 1;
f = -f;
};
// 9e18 is comfortably under I64_MAX (9.22e18). Past this the
// `f: i64` cast wraps and the integer part comes back as garbage.
let cap: f64 = 9000000000000000000i64: f64;
if (f >= cap) {
let s: str = "huge";
let k: i32 = 0;
for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; };
let r: str;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
let ip: i64 = f: i64;
// Fractional part scaled to 6 decimal digits, with round-to-
// nearest via +0.5. (f64 compound assigns mis-lower in cgen —
// use the explicit form, as the rest of lib does.)
let frac: f64 = f - (ip: f64);
let scale: f64 = 1000000: f64;
frac = frac * scale;
let half: f64 = (1: f64) / (2: f64);
let fp: i64 = (frac + half): i64;
// Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer
// part needs to advance.
if (fp >= 1000000) {
ip += 1;
fp = 0;
};
let intstr: str = i64tos(ip, base.DEC);
let k: i32 = 0;
for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; };
if (fp != 0) {
f64tos_buf[out] = 46u8; // '.'
out += 1;
let fracstr: str = u64tos(fp: u64, base.DEC);
// Pad fractional to 6 digits with leading zeros (e.g. 0.05 →
// fp=50000, fracstr="50000", pad one '0' before).
let z: i32 = 6 - fracstr.len;
for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; };
k = 0;
for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; };
// Trim trailing zeros in the fractional part.
for (out > 0) {
if (f64tos_buf[out - 1] != 48u8) { break; };
out -= 1;
};
};
let r: str;
r.ptr = &f64tos_buf[0];
r.len = out;
return r;
};
// f64tos — graduated to the Ryū shortest-round-trippable implementation
// in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version
// (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted
// here per the lib-note graduation rule ("replace in one go, don't keep
// both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b
// (task #67, gated on the #143 f32-arg-push cgen fix).
// strerror — convert an strconv error to a user-readable string.
// Returns owned str; release via os.free. Mirrors Hare's

View File

@@ -0,0 +1,93 @@
// ftostest — exercises lib/strconv/ftos.ww (Hare ftos.ha / ftos_ryu.ha
// Ryū port). Run with `out/bin/ww run lib/strconv/test/ftostest.ww`.
// Same signalled-then-os.exit(signalled+10) pattern as stoftest/
// decimaltest: a non-zero exit names the failing @test (1..N → 11..N+10).
//
// Ports ref/hare/strconv/+test/ftos_test.ha's ffmt::G / prec=void /
// fflags::NONE rows (the f64tos cases) verbatim — these are exactly
// strconv.f64tos's output. encode_f_dec is covered by 13.37/1100/0.011/…
// and encode_e_dec by 1e4/1.1e4/1e-3/1.1e-3/1e-4. nan/±infinity via
// math.f64frombits of the IEEE bit patterns (ww math has no f64 NAN/INF
// const — see math/floats.ww).
//
// f32tos is NOT exercised here: it is deferred to fold-5b (task #67),
// gated on the #143 f32-arg-push cs≠ww cgen fix.
//
// Lives in lib/strconv/test/ so `import strconv` resolves to the
// DIRECTORY (full package), not the strconv.ww FILE — same rationale as
// stoftest/decimaltest.
package strconv;
import strconv;
import os;
import math;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let k: i32 = 0i32;
for (k < a.len) {
if (a[k] != b[k]) { return false; };
k += 1i32;
};
return true;
};
fn chk(n: f64, want: str) bool = {
return streq(f64tos(n), want);
};
// ftos_test.ha:57-64 (G/void/NONE-equivalent) — fixed-point renders.
@test fn f64tos_fixed() void = {
if (!chk(13.37, "13.37")) { fail(); };
if (!chk(12345.0, "12345")) { fail(); };
if (!chk(1100.0, "1100")) { fail(); };
if (!chk(100.0, "100")) { fail(); };
if (!chk(10.0, "10")) { fail(); };
if (!chk(1.0, "1")) { fail(); };
if (!chk(0.3, "0.3")) { fail(); };
if (!chk(0.1, "0.1")) { fail(); };
if (!chk(0.01, "0.01")) { fail(); };
if (!chk(0.011, "0.011")) { fail(); };
if (!chk(1.414, "1.414")) { fail(); };
if (!chk(-6345.972, "-6345.972")) { fail(); };
};
// ftos_test.ha:78-90 — scientific renders (the shortest-G E-dispatch).
@test fn f64tos_scientific() void = {
if (!chk(10000.0, "1e4")) { fail(); };
if (!chk(11000.0, "1.1e4")) { fail(); };
if (!chk(1000.0, "1e3")) { fail(); };
if (!chk(0.001, "1e-3")) { fail(); };
if (!chk(0.0011, "1.1e-3")) { fail(); };
if (!chk(0.0001, "1e-4")) { fail(); };
// dp=-2 < -1 → G dispatches to E (ftos_test.ha:62 is the ffmt::F row;
// shortest-G of 0.0031415 is scientific).
if (!chk(0.0031415, "3.1415e-3")) { fail(); };
};
// ftos_test.ha:12/16/27 — zero, ±infinity, nan.
@test fn f64tos_special() void = {
if (!chk(0.0, "0")) { fail(); };
// sign bit via (1u64 << 63), not the 0x8000000000000000 literal:
// wwstage mis-emits the I64_MIN-magnitude literal (#144 family).
let negzero: f64 = math.f64frombits(1u64 << 63u64);
if (!chk(negzero, "-0")) { fail(); };
let inf: f64 = math.f64frombits(0x7FF0000000000000u64);
if (!chk(inf, "infinity")) { fail(); };
let ninf: f64 = math.f64frombits(0xFFF0000000000000u64);
if (!chk(ninf, "-infinity")) { fail(); };
let nan: f64 = math.f64frombits(0x7FF8000000000000u64);
if (!chk(nan, "nan")) { fail(); };
};
export fn main() i32 = {
signalled = 1; f64tos_fixed();
signalled = 2; f64tos_scientific();
signalled = 3; f64tos_special();
os.exit(0);
return 0;
};