diff --git a/lib/strconv/ftos.ww b/lib/strconv/ftos.ww index 91223556..417818f7 100644 --- a/lib/strconv/ftos.ww +++ b/lib/strconv/ftos.ww @@ -4,15 +4,14 @@ // 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. +// SCOPE — the f64tos + f32tos shortest-representation subset (Hare's +// ffmt::G, prec=void, fflags::NONE). f32tos (ftos.ha:448) + its f32 Ryū +// sub-path (f32todecf32 + mulpow5inv/pow5_divpow2 + mulshift32 + the *32 +// helpers, reusing the shared u64-core + the f64 SPLIT2 tables — the f32 +// path has no separate tables, matching ftos_ryu.ha) ship here in fold-5b +// (task #67): the gating #143 f32-arg-push cgen fix landed (aff7725, MOVSS +// both stages), so f32tos's math.f32bits(n) call — passing an f32 arg — is +// now byte-id-clean. One deferral remains: // - 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 @@ -340,6 +339,195 @@ fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { return decf64 { exponent = (exp: i64), mantissa = output }; }; +// ==== f32 Ryū sub-path (ftos_ryu.ha). The *32 helpers below mirror their +// u64 siblings at 32-bit width; they reuse the SHARED f64computeinvpow5/ +// f64computepow5 (and thus the f64 SPLIT2 tables) per ftos_ryu.ha — there +// is no separate f32 table. Same scalar-PARAM-mutation → copy-to-local, +// comma-split, assert → os.assert, expr-yield → block divergences as the +// f64 path above. ==== + +// ref/hare/strconv/ftos_ryu.ha:52. Largest p with 5^p | value (32-bit). +fn pow5fac32(v: u32) u32 = { + let value: u32 = v; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u32, "strconv.pow5fac32: value == 0"); + let q: u32 = value / 5u32; + let r: u32 = value % 5u32; + if (r != 0u32) { break; }; + value = q; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:67. +fn pow5multiple32(v: u32, p: u32) bool = { return pow5fac32(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:75. +fn pow2multiple32(v: u32, p: u32) bool = { + os.assert(v > 0u32, "strconv.pow2multiple32: v == 0"); + os.assert(p < 32u32, "strconv.pow2multiple32: p >= 32"); + return (v & ((1u32 << p) - 1u32)) == 0u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:121. `m * a_lo` etc. carry an explicit +// (m: u64) cast (Hare promotes the u32 operand; ww is strict). The bound +// assert inlines U32_MAX's value: ww's types.U32_MAX is package-private +// (lib/types/types.ww — no `export`), so Hare's `types::U32_MAX` can't be +// referenced cross-package. +fn mulshift32(m: u32, a: u64, s: u32) u32 = { + os.assert(s > 32u32, "strconv.mulshift32: s <= 32"); + let a_lo: u64 = (a: u32): u64; + let a_hi: u64 = a >> 32u64; + let b0: u64 = (m: u64) * a_lo; + let b1: u64 = (m: u64) * a_hi; + let sum: u64 = (b0 >> 32u64) + b1; + let ss: u64 = sum >> ((s: u64) - 32u64); + os.assert(ss <= 4294967295u64, "strconv.mulshift32: ss > U32_MAX"); + return ss: u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:130. +fn mulpow5inv_divpow2(m: u32, q: u32, j: i32) u32 = { + let pow5 = f64computeinvpow5(q); + return mulshift32(m, pow5.1 + 1u64, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:135. +fn mulpow5_divpow2(m: u32, i: u32, j: i32) u32 = { + let pow5 = f64computepow5(i); + return mulshift32(m, pow5.1, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:387. `exponent` rides i64 not Hare's i32, +// for the same reason decf64 does: widening the field to a full second +// eightbyte SIDESTEPS the #169 narrow-i32-field struct-return unpack (a +// narrow i32 there unpacks MOVL wwstage vs MOVQ cstage). The value always +// fits i32 (cast at the init_dec_mant_exp call site). `mantissa` stays u32 +// (Hare's width); the {u32, pad, i64} layout's first eightbyte holds +// mantissa@0 + 4B pad and reads cleanly — byte-id CONFIRMED by the 990-997 +// gate (0-diff cs vs ww), not relied on as an ABI guarantee. +type decf32 = struct { mantissa: u32, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:392. Shortest decimal of an f32: +// value == mantissa * 10^exponent. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f32. +fn f32todecf32(mantissa: u32, exponent: u32) decf32 = { + let e2: i32 = (math.F32_EXPONENT_BIAS + math.F32_MANTISSA_BITS + 2u32): i32; + let m2: u32 = 0u32; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u32 << math.F32_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u32) == 0u32; + let mv: u32 = 4u32 * m2; + let mp: u32 = mv + 2u32; + let mm_shift: u32 = ibool(mantissa != 0u32 || exponent <= 1u32): u32; + let mm: u32 = mv - 1u32 - mm_shift; + let vr: u32 = 0u32; + let vp: u32 = 0u32; + let vm: u32 = 0u32; + let e10: i32 = 0i32; + let vm_trailing_zeroes: bool = false; + let vr_trailing_zeroes: bool = false; + let last_removed_digit: u8 = 0u8; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32); + e10 = q: i32; + let k: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + vr = mulpow5inv_divpow2(mv, q, i); + vp = mulpow5inv_divpow2(mp, q, i); + vm = mulpow5inv_divpow2(mm, q, i); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + let l: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q - 1u32) - 1u32; + // #168 dodge: div/mod on an inline N_CALL result emits a + // signed IDIVQ in wwstage; bind the call to a local first. + let lrd: u32 = mulpow5inv_divpow2(mv, q - 1u32, + -e2 + ((q + l): i32) - 1i32); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 9u32) { + if (mv % 5u32 == 0u32) { + vr_trailing_zeroes = pow5multiple32(mv, q); + } else if (accept_bounds) { + vm_trailing_zeroes = pow5multiple32(mm, q); + } else { + vp -= (ibool(pow5multiple32(mp, q)): u32); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32); + e10 = (q: i32) + e2; + let i: u32 = (-e2 - (q: i32)): u32; + let k: u32 = pow5bits(i) - (F32_POW5_BITCOUNT: u32); + let j: i32 = (q: i32) - (k: i32); + vr = mulpow5_divpow2(mv, i, j); + vp = mulpow5_divpow2(mp, i, j); + vm = mulpow5_divpow2(mm, i, j); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + j = (q: i32) - 1i32 - ((pow5bits(i + 1u32): i32) - (F32_POW5_BITCOUNT: i32)); + // #168 dodge (see above): local-bind before the % 10. + let lrd: u32 = mulpow5_divpow2(mv, (i + 1u32), j); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 1u32) { + vr_trailing_zeroes = true; + if (accept_bounds) { + vm_trailing_zeroes = mm_shift == 1u32; + } else { + vp -= 1u32; + }; + } else if (q < 31u32) { + vr_trailing_zeroes = pow2multiple32(mv, q - 1u32); + }; + }; + let removed: i32 = 0i32; + let output: u32 = 0u32; + if (vm_trailing_zeroes || vr_trailing_zeroes) { + for ((vp / 10u32) > (vm / 10u32)) { + vm_trailing_zeroes = vm_trailing_zeroes && ((vm - (vm / 10u32) * 10u32) == 0u32); + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + if (vm_trailing_zeroes) { + for ((vm % 10u32) == 0u32) { + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + }; + if (vr_trailing_zeroes && last_removed_digit == 5u8 && vr % 2u32 == 0u32) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeroes)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u32); + } else { + for ((vp / 10u32) > (vm / 10u32)) { + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || last_removed_digit >= 5u8): u32); + }; + let exp: i32 = e10 + removed; + return decf32 { mantissa = output, exponent = (exp: i64) }; +}; + // ==== 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 @@ -547,3 +735,64 @@ export fn f64tos(n: f64) str = { r.len = o; return r; }; + +// ref/hare/strconv/ftos.ha:448. f32 → shortest base-10 str. Same static- +// buffer convention + G/void/NONE-inlined path as f64tos. f32bits(n) +// passes an f32 arg → MOVSS both stages post-#143 (aff7725); this is the +// piece fold-5b was gated on. +// +// Hare sizes this [14]u8 (ftos.ha:451: 1 + 1 + 1 + 7 + 1 + 1 + 2). Sized +// 32 to reuse f64tos's proven byte-id-clean band: a no-rhs [N]u8 module +// buffer at the size-16/24 band emits divergent DATAW counts cs≠ww (#43); +// 32 emits 2 DATAW in both. The unused tail bytes are harmless. +let f32tos_buf: [32]u8; + +export fn f32tos(n: f32) str = { + let bits: u32 = math.f32bits(n); + let mantissa: u32 = bits & math.F32_MANTISSA_MASK; + let exponent: u32 = (bits >> math.F32_MANTISSA_BITS) & math.F32_EXPONENT_MASK; + let sign: bool = (bits >> (math.F32_EXPONENT_BITS + math.F32_MANTISSA_BITS)) > 0u32; + let special: bool = exponent == math.F32_EXPONENT_MASK; + + let o: i32 = 0i32; + let r: str; + r.ptr = &f32tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u32) { + o = putstr(f32tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f32tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f32tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u32) { + f32tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // *decimal pointer for the field reads (the #170 dodge; see f64tos). + let pd: *decimal = &d; + let dd: decf32 = f32todecf32(mantissa, exponent); + init_dec_mant_exp(pd, (dd.mantissa: u64), (dd.exponent: i32)); + trim(pd); + if (pd.nd == (0u64: size)) { + f32tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f32tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f32tos_buf[0:32], o); + }; + r.len = o; + return r; +}; diff --git a/lib/strconv/ftos_data.ww b/lib/strconv/ftos_data.ww index 0b9a90bb..c989f0b2 100644 --- a/lib/strconv/ftos_data.ww +++ b/lib/strconv/ftos_data.ww @@ -25,12 +25,18 @@ package strconv; // 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:162-163. The f32 split-table bit-counts, +// derived from the f64 siblings (Hare: F64_..._BITCOUNT - 64). Consumed by +// f32todecf32 (ftos.ww), landed in fold-5b (task #67) — the f32 path reuses +// the f64 SPLIT2 tables (via f64computeinvpow5/f64computepow5), so no +// separate F32 tables exist (matches ftos_ryu.ha). u8 like the f64 defs; +// ftos.ww casts to u32/i32 at each use. +def F32_POW5_INV_BITCOUNT: u8 = F64_POW5_INV_BITCOUNT - 64u8; +def F32_POW5_BITCOUNT: u8 = F64_POW5_BITCOUNT - 64u8; + // ref/hare/strconv/ftos_ryu.ha:165-181. let F64_POW5_INV_SPLIT2: [15][2]u64 = [ [1u64, 2305843009213693952u64], diff --git a/lib/strconv/test/ftostest.ww b/lib/strconv/test/ftostest.ww index 0b96f725..a89c9f86 100644 --- a/lib/strconv/test/ftostest.ww +++ b/lib/strconv/test/ftostest.ww @@ -10,8 +10,11 @@ // 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. +// f32tos (fold-5b, task #67) is exercised by f32tos_fixed/_scientific/ +// _extremes/_special below. The G/void rows mirror ftos_test.ha's tcs (the +// "pass for both f32 and f64" set, lines 9/183); the _extremes rows mirror +// tcsf32 (ftos_test.ha:218-221, the f32-EXCLUSIVE denormal/min/max — full +// f32 mantissa). f32 values use the `f32` literal suffix or f32frombits. // // Lives in lib/strconv/test/ so `import strconv` resolves to the // DIRECTORY (full package), not the strconv.ww FILE — same rationale as @@ -84,10 +87,78 @@ fn chk(n: f64, want: str) bool = { if (!chk(nan, "nan")) { fail(); }; }; +fn chkf32(n: f32, want: str) bool = { + return streq(f32tos(n), want); +}; + +// ftos_test.ha tcs G/void rows (the "pass for both f32 and f64" set) — +// fixed-point renders. 1.0/1.5/0.1/100/0.5 are the fold-5a probe's 5/5 +// value-validated f32todecf32 pairs. +@test fn f32tos_fixed() void = { + if (!chkf32(13.37f32, "13.37")) { fail(); }; + if (!chkf32(-13.37f32, "-13.37")) { fail(); }; // sign prefix + encode_f at o=1 + // 2^25: the smallest f32 with e2 >= 0 (q == 0) — the ONLY runtime cover + // of f32todecf32's e2>=0 / q<=9 trailing-zero block (the f32-specific + // `q <= 9` + pow5multiple32 arm). External shortest-round-trip check. + if (!chkf32(33554432.0f32, "33554432")) { fail(); }; + if (!chkf32(12345.0f32, "12345")) { fail(); }; + if (!chkf32(1100.0f32, "1100")) { fail(); }; + if (!chkf32(100.0f32, "100")) { fail(); }; + if (!chkf32(10.0f32, "10")) { fail(); }; + if (!chkf32(1.0f32, "1")) { fail(); }; + if (!chkf32(1.5f32, "1.5")) { fail(); }; + if (!chkf32(0.5f32, "0.5")) { fail(); }; + if (!chkf32(0.1f32, "0.1")) { fail(); }; + if (!chkf32(0.01f32, "0.01")) { fail(); }; + if (!chkf32(0.011f32, "0.011")) { fail(); }; +}; + +// ftos_test.ha tcs G/void rows — scientific (shortest-G E-dispatch). +@test fn f32tos_scientific() void = { + if (!chkf32(10000.0f32, "1e4")) { fail(); }; + if (!chkf32(11000.0f32, "1.1e4")) { fail(); }; + if (!chkf32(1000.0f32, "1e3")) { fail(); }; + if (!chkf32(0.001f32, "1e-3")) { fail(); }; + if (!chkf32(0.0011f32, "1.1e-3")) { fail(); }; + if (!chkf32(0.0001f32, "1e-4")) { fail(); }; +}; + +// ftos_test.ha:218-221 (tcsf32) — the f32-EXCLUSIVE denormal/min-normal/ +// max-normal rows (distinct outputs from f64): exercise the full f32 +// mantissa (8 sig digits) + 2-digit exponent. Built via f32frombits of the +// IEEE-754 bit patterns (ww math has no F32_MIN_*/MAX_NORMAL const). +@test fn f32tos_extremes() void = { + let minsub: f32 = math.f32frombits(1u32); // 2^-149 + if (!chkf32(minsub, "1e-45")) { fail(); }; + let minnorm: f32 = math.f32frombits(0x00800000u32); // 2^-126 + if (!chkf32(minnorm, "1.1754944e-38")) { fail(); }; + let maxnorm: f32 = math.f32frombits(0x7F7FFFFFu32); + if (!chkf32(maxnorm, "3.4028235e38")) { fail(); }; +}; + +// ftos_test.ha:11/15/26 — zero, ±infinity, nan (f32 bit patterns). +@test fn f32tos_special() void = { + if (!chkf32(0.0f32, "0")) { fail(); }; + // sign bit via (1u32 << 31), mirroring the f64 test's I64_MIN-literal + // avoidance (#144 family). + let negzero: f32 = math.f32frombits(1u32 << 31u32); + if (!chkf32(negzero, "-0")) { fail(); }; + let inf: f32 = math.f32frombits(0x7F800000u32); + if (!chkf32(inf, "infinity")) { fail(); }; + let ninf: f32 = math.f32frombits(0xFF800000u32); + if (!chkf32(ninf, "-infinity")) { fail(); }; + let nan: f32 = math.f32frombits(0x7FC00000u32); + if (!chkf32(nan, "nan")) { fail(); }; +}; + export fn main() i32 = { signalled = 1; f64tos_fixed(); signalled = 2; f64tos_scientific(); signalled = 3; f64tos_special(); + signalled = 4; f32tos_fixed(); + signalled = 5; f32tos_scientific(); + signalled = 6; f32tos_extremes(); + signalled = 7; f32tos_special(); os.exit(0); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e1f20a5a..5bca2514 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -3322,15 +3322,14 @@ export fn absi64(n: i64) u64 = { // 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. +// SCOPE — the f64tos + f32tos shortest-representation subset (Hare's +// ffmt::G, prec=void, fflags::NONE). f32tos (ftos.ha:448) + its f32 Ryū +// sub-path (f32todecf32 + mulpow5inv/pow5_divpow2 + mulshift32 + the *32 +// helpers, reusing the shared u64-core + the f64 SPLIT2 tables — the f32 +// path has no separate tables, matching ftos_ryu.ha) ship here in fold-5b +// (task #67): the gating #143 f32-arg-push cgen fix landed (aff7725, MOVSS +// both stages), so f32tos's math.f32bits(n) call — passing an f32 arg — is +// now byte-id-clean. One deferral remains: // - 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 @@ -3658,6 +3657,195 @@ fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { return decf64 { exponent = (exp: i64), mantissa = output }; }; +// ==== f32 Ryū sub-path (ftos_ryu.ha). The *32 helpers below mirror their +// u64 siblings at 32-bit width; they reuse the SHARED f64computeinvpow5/ +// f64computepow5 (and thus the f64 SPLIT2 tables) per ftos_ryu.ha — there +// is no separate f32 table. Same scalar-PARAM-mutation → copy-to-local, +// comma-split, assert → os.assert, expr-yield → block divergences as the +// f64 path above. ==== + +// ref/hare/strconv/ftos_ryu.ha:52. Largest p with 5^p | value (32-bit). +fn pow5fac32(v: u32) u32 = { + let value: u32 = v; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u32, "strconv.pow5fac32: value == 0"); + let q: u32 = value / 5u32; + let r: u32 = value % 5u32; + if (r != 0u32) { break; }; + value = q; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:67. +fn pow5multiple32(v: u32, p: u32) bool = { return pow5fac32(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:75. +fn pow2multiple32(v: u32, p: u32) bool = { + os.assert(v > 0u32, "strconv.pow2multiple32: v == 0"); + os.assert(p < 32u32, "strconv.pow2multiple32: p >= 32"); + return (v & ((1u32 << p) - 1u32)) == 0u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:121. `m * a_lo` etc. carry an explicit +// (m: u64) cast (Hare promotes the u32 operand; ww is strict). The bound +// assert inlines U32_MAX's value: ww's types.U32_MAX is package-private +// (lib/types/types.ww — no `export`), so Hare's `types::U32_MAX` can't be +// referenced cross-package. +fn mulshift32(m: u32, a: u64, s: u32) u32 = { + os.assert(s > 32u32, "strconv.mulshift32: s <= 32"); + let a_lo: u64 = (a: u32): u64; + let a_hi: u64 = a >> 32u64; + let b0: u64 = (m: u64) * a_lo; + let b1: u64 = (m: u64) * a_hi; + let sum: u64 = (b0 >> 32u64) + b1; + let ss: u64 = sum >> ((s: u64) - 32u64); + os.assert(ss <= 4294967295u64, "strconv.mulshift32: ss > U32_MAX"); + return ss: u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:130. +fn mulpow5inv_divpow2(m: u32, q: u32, j: i32) u32 = { + let pow5 = f64computeinvpow5(q); + return mulshift32(m, pow5.1 + 1u64, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:135. +fn mulpow5_divpow2(m: u32, i: u32, j: i32) u32 = { + let pow5 = f64computepow5(i); + return mulshift32(m, pow5.1, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:387. `exponent` rides i64 not Hare's i32, +// for the same reason decf64 does: widening the field to a full second +// eightbyte SIDESTEPS the #169 narrow-i32-field struct-return unpack (a +// narrow i32 there unpacks MOVL wwstage vs MOVQ cstage). The value always +// fits i32 (cast at the init_dec_mant_exp call site). `mantissa` stays u32 +// (Hare's width); the {u32, pad, i64} layout's first eightbyte holds +// mantissa@0 + 4B pad and reads cleanly — byte-id CONFIRMED by the 990-997 +// gate (0-diff cs vs ww), not relied on as an ABI guarantee. +type decf32 = struct { mantissa: u32, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:392. Shortest decimal of an f32: +// value == mantissa * 10^exponent. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f32. +fn f32todecf32(mantissa: u32, exponent: u32) decf32 = { + let e2: i32 = (math.F32_EXPONENT_BIAS + math.F32_MANTISSA_BITS + 2u32): i32; + let m2: u32 = 0u32; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u32 << math.F32_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u32) == 0u32; + let mv: u32 = 4u32 * m2; + let mp: u32 = mv + 2u32; + let mm_shift: u32 = ibool(mantissa != 0u32 || exponent <= 1u32): u32; + let mm: u32 = mv - 1u32 - mm_shift; + let vr: u32 = 0u32; + let vp: u32 = 0u32; + let vm: u32 = 0u32; + let e10: i32 = 0i32; + let vm_trailing_zeroes: bool = false; + let vr_trailing_zeroes: bool = false; + let last_removed_digit: u8 = 0u8; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32); + e10 = q: i32; + let k: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + vr = mulpow5inv_divpow2(mv, q, i); + vp = mulpow5inv_divpow2(mp, q, i); + vm = mulpow5inv_divpow2(mm, q, i); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + let l: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q - 1u32) - 1u32; + // #168 dodge: div/mod on an inline N_CALL result emits a + // signed IDIVQ in wwstage; bind the call to a local first. + let lrd: u32 = mulpow5inv_divpow2(mv, q - 1u32, + -e2 + ((q + l): i32) - 1i32); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 9u32) { + if (mv % 5u32 == 0u32) { + vr_trailing_zeroes = pow5multiple32(mv, q); + } else if (accept_bounds) { + vm_trailing_zeroes = pow5multiple32(mm, q); + } else { + vp -= (ibool(pow5multiple32(mp, q)): u32); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32); + e10 = (q: i32) + e2; + let i: u32 = (-e2 - (q: i32)): u32; + let k: u32 = pow5bits(i) - (F32_POW5_BITCOUNT: u32); + let j: i32 = (q: i32) - (k: i32); + vr = mulpow5_divpow2(mv, i, j); + vp = mulpow5_divpow2(mp, i, j); + vm = mulpow5_divpow2(mm, i, j); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + j = (q: i32) - 1i32 - ((pow5bits(i + 1u32): i32) - (F32_POW5_BITCOUNT: i32)); + // #168 dodge (see above): local-bind before the % 10. + let lrd: u32 = mulpow5_divpow2(mv, (i + 1u32), j); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 1u32) { + vr_trailing_zeroes = true; + if (accept_bounds) { + vm_trailing_zeroes = mm_shift == 1u32; + } else { + vp -= 1u32; + }; + } else if (q < 31u32) { + vr_trailing_zeroes = pow2multiple32(mv, q - 1u32); + }; + }; + let removed: i32 = 0i32; + let output: u32 = 0u32; + if (vm_trailing_zeroes || vr_trailing_zeroes) { + for ((vp / 10u32) > (vm / 10u32)) { + vm_trailing_zeroes = vm_trailing_zeroes && ((vm - (vm / 10u32) * 10u32) == 0u32); + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + if (vm_trailing_zeroes) { + for ((vm % 10u32) == 0u32) { + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + }; + if (vr_trailing_zeroes && last_removed_digit == 5u8 && vr % 2u32 == 0u32) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeroes)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u32); + } else { + for ((vp / 10u32) > (vm / 10u32)) { + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || last_removed_digit >= 5u8): u32); + }; + let exp: i32 = e10 + removed; + return decf32 { mantissa = output, exponent = (exp: i64) }; +}; + // ==== 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 @@ -3866,6 +4054,67 @@ export fn f64tos(n: f64) str = { return r; }; +// ref/hare/strconv/ftos.ha:448. f32 → shortest base-10 str. Same static- +// buffer convention + G/void/NONE-inlined path as f64tos. f32bits(n) +// passes an f32 arg → MOVSS both stages post-#143 (aff7725); this is the +// piece fold-5b was gated on. +// +// Hare sizes this [14]u8 (ftos.ha:451: 1 + 1 + 1 + 7 + 1 + 1 + 2). Sized +// 32 to reuse f64tos's proven byte-id-clean band: a no-rhs [N]u8 module +// buffer at the size-16/24 band emits divergent DATAW counts cs≠ww (#43); +// 32 emits 2 DATAW in both. The unused tail bytes are harmless. +let f32tos_buf: [32]u8; + +export fn f32tos(n: f32) str = { + let bits: u32 = math.f32bits(n); + let mantissa: u32 = bits & math.F32_MANTISSA_MASK; + let exponent: u32 = (bits >> math.F32_MANTISSA_BITS) & math.F32_EXPONENT_MASK; + let sign: bool = (bits >> (math.F32_EXPONENT_BITS + math.F32_MANTISSA_BITS)) > 0u32; + let special: bool = exponent == math.F32_EXPONENT_MASK; + + let o: i32 = 0i32; + let r: str; + r.ptr = &f32tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u32) { + o = putstr(f32tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f32tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f32tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u32) { + f32tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // *decimal pointer for the field reads (the #170 dodge; see f64tos). + let pd: *decimal = &d; + let dd: decf32 = f32todecf32(mantissa, exponent); + init_dec_mant_exp(pd, (dd.mantissa: u64), (dd.exponent: i32)); + trim(pd); + if (pd.nd == (0u64: size)) { + f32tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f32tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f32tos_buf[0:32], o); + }; + r.len = o; + return r; +}; + // 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 @@ -3893,12 +4142,18 @@ package strconv; // 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:162-163. The f32 split-table bit-counts, +// derived from the f64 siblings (Hare: F64_..._BITCOUNT - 64). Consumed by +// f32todecf32 (ftos.ww), landed in fold-5b (task #67) — the f32 path reuses +// the f64 SPLIT2 tables (via f64computeinvpow5/f64computepow5), so no +// separate F32 tables exist (matches ftos_ryu.ha). u8 like the f64 defs; +// ftos.ww casts to u32/i32 at each use. +def F32_POW5_INV_BITCOUNT: u8 = F64_POW5_INV_BITCOUNT - 64u8; +def F32_POW5_BITCOUNT: u8 = F64_POW5_BITCOUNT - 64u8; + // ref/hare/strconv/ftos_ryu.ha:165-181. let F64_POW5_INV_SPLIT2: [15][2]u64 = [ [1u64, 2305843009213693952u64], diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1407ee29..70493e59 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1345,15 +1345,14 @@ export fn absi64(n: i64) u64 = { // 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. +// SCOPE — the f64tos + f32tos shortest-representation subset (Hare's +// ffmt::G, prec=void, fflags::NONE). f32tos (ftos.ha:448) + its f32 Ryū +// sub-path (f32todecf32 + mulpow5inv/pow5_divpow2 + mulshift32 + the *32 +// helpers, reusing the shared u64-core + the f64 SPLIT2 tables — the f32 +// path has no separate tables, matching ftos_ryu.ha) ship here in fold-5b +// (task #67): the gating #143 f32-arg-push cgen fix landed (aff7725, MOVSS +// both stages), so f32tos's math.f32bits(n) call — passing an f32 arg — is +// now byte-id-clean. One deferral remains: // - 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 @@ -1681,6 +1680,195 @@ fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { return decf64 { exponent = (exp: i64), mantissa = output }; }; +// ==== f32 Ryū sub-path (ftos_ryu.ha). The *32 helpers below mirror their +// u64 siblings at 32-bit width; they reuse the SHARED f64computeinvpow5/ +// f64computepow5 (and thus the f64 SPLIT2 tables) per ftos_ryu.ha — there +// is no separate f32 table. Same scalar-PARAM-mutation → copy-to-local, +// comma-split, assert → os.assert, expr-yield → block divergences as the +// f64 path above. ==== + +// ref/hare/strconv/ftos_ryu.ha:52. Largest p with 5^p | value (32-bit). +fn pow5fac32(v: u32) u32 = { + let value: u32 = v; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u32, "strconv.pow5fac32: value == 0"); + let q: u32 = value / 5u32; + let r: u32 = value % 5u32; + if (r != 0u32) { break; }; + value = q; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:67. +fn pow5multiple32(v: u32, p: u32) bool = { return pow5fac32(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:75. +fn pow2multiple32(v: u32, p: u32) bool = { + os.assert(v > 0u32, "strconv.pow2multiple32: v == 0"); + os.assert(p < 32u32, "strconv.pow2multiple32: p >= 32"); + return (v & ((1u32 << p) - 1u32)) == 0u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:121. `m * a_lo` etc. carry an explicit +// (m: u64) cast (Hare promotes the u32 operand; ww is strict). The bound +// assert inlines U32_MAX's value: ww's types.U32_MAX is package-private +// (lib/types/types.ww — no `export`), so Hare's `types::U32_MAX` can't be +// referenced cross-package. +fn mulshift32(m: u32, a: u64, s: u32) u32 = { + os.assert(s > 32u32, "strconv.mulshift32: s <= 32"); + let a_lo: u64 = (a: u32): u64; + let a_hi: u64 = a >> 32u64; + let b0: u64 = (m: u64) * a_lo; + let b1: u64 = (m: u64) * a_hi; + let sum: u64 = (b0 >> 32u64) + b1; + let ss: u64 = sum >> ((s: u64) - 32u64); + os.assert(ss <= 4294967295u64, "strconv.mulshift32: ss > U32_MAX"); + return ss: u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:130. +fn mulpow5inv_divpow2(m: u32, q: u32, j: i32) u32 = { + let pow5 = f64computeinvpow5(q); + return mulshift32(m, pow5.1 + 1u64, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:135. +fn mulpow5_divpow2(m: u32, i: u32, j: i32) u32 = { + let pow5 = f64computepow5(i); + return mulshift32(m, pow5.1, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:387. `exponent` rides i64 not Hare's i32, +// for the same reason decf64 does: widening the field to a full second +// eightbyte SIDESTEPS the #169 narrow-i32-field struct-return unpack (a +// narrow i32 there unpacks MOVL wwstage vs MOVQ cstage). The value always +// fits i32 (cast at the init_dec_mant_exp call site). `mantissa` stays u32 +// (Hare's width); the {u32, pad, i64} layout's first eightbyte holds +// mantissa@0 + 4B pad and reads cleanly — byte-id CONFIRMED by the 990-997 +// gate (0-diff cs vs ww), not relied on as an ABI guarantee. +type decf32 = struct { mantissa: u32, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:392. Shortest decimal of an f32: +// value == mantissa * 10^exponent. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f32. +fn f32todecf32(mantissa: u32, exponent: u32) decf32 = { + let e2: i32 = (math.F32_EXPONENT_BIAS + math.F32_MANTISSA_BITS + 2u32): i32; + let m2: u32 = 0u32; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u32 << math.F32_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u32) == 0u32; + let mv: u32 = 4u32 * m2; + let mp: u32 = mv + 2u32; + let mm_shift: u32 = ibool(mantissa != 0u32 || exponent <= 1u32): u32; + let mm: u32 = mv - 1u32 - mm_shift; + let vr: u32 = 0u32; + let vp: u32 = 0u32; + let vm: u32 = 0u32; + let e10: i32 = 0i32; + let vm_trailing_zeroes: bool = false; + let vr_trailing_zeroes: bool = false; + let last_removed_digit: u8 = 0u8; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32); + e10 = q: i32; + let k: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + vr = mulpow5inv_divpow2(mv, q, i); + vp = mulpow5inv_divpow2(mp, q, i); + vm = mulpow5inv_divpow2(mm, q, i); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + let l: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q - 1u32) - 1u32; + // #168 dodge: div/mod on an inline N_CALL result emits a + // signed IDIVQ in wwstage; bind the call to a local first. + let lrd: u32 = mulpow5inv_divpow2(mv, q - 1u32, + -e2 + ((q + l): i32) - 1i32); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 9u32) { + if (mv % 5u32 == 0u32) { + vr_trailing_zeroes = pow5multiple32(mv, q); + } else if (accept_bounds) { + vm_trailing_zeroes = pow5multiple32(mm, q); + } else { + vp -= (ibool(pow5multiple32(mp, q)): u32); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32); + e10 = (q: i32) + e2; + let i: u32 = (-e2 - (q: i32)): u32; + let k: u32 = pow5bits(i) - (F32_POW5_BITCOUNT: u32); + let j: i32 = (q: i32) - (k: i32); + vr = mulpow5_divpow2(mv, i, j); + vp = mulpow5_divpow2(mp, i, j); + vm = mulpow5_divpow2(mm, i, j); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + j = (q: i32) - 1i32 - ((pow5bits(i + 1u32): i32) - (F32_POW5_BITCOUNT: i32)); + // #168 dodge (see above): local-bind before the % 10. + let lrd: u32 = mulpow5_divpow2(mv, (i + 1u32), j); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 1u32) { + vr_trailing_zeroes = true; + if (accept_bounds) { + vm_trailing_zeroes = mm_shift == 1u32; + } else { + vp -= 1u32; + }; + } else if (q < 31u32) { + vr_trailing_zeroes = pow2multiple32(mv, q - 1u32); + }; + }; + let removed: i32 = 0i32; + let output: u32 = 0u32; + if (vm_trailing_zeroes || vr_trailing_zeroes) { + for ((vp / 10u32) > (vm / 10u32)) { + vm_trailing_zeroes = vm_trailing_zeroes && ((vm - (vm / 10u32) * 10u32) == 0u32); + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + if (vm_trailing_zeroes) { + for ((vm % 10u32) == 0u32) { + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + }; + if (vr_trailing_zeroes && last_removed_digit == 5u8 && vr % 2u32 == 0u32) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeroes)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u32); + } else { + for ((vp / 10u32) > (vm / 10u32)) { + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || last_removed_digit >= 5u8): u32); + }; + let exp: i32 = e10 + removed; + return decf32 { mantissa = output, exponent = (exp: i64) }; +}; + // ==== 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 @@ -1889,6 +2077,67 @@ export fn f64tos(n: f64) str = { return r; }; +// ref/hare/strconv/ftos.ha:448. f32 → shortest base-10 str. Same static- +// buffer convention + G/void/NONE-inlined path as f64tos. f32bits(n) +// passes an f32 arg → MOVSS both stages post-#143 (aff7725); this is the +// piece fold-5b was gated on. +// +// Hare sizes this [14]u8 (ftos.ha:451: 1 + 1 + 1 + 7 + 1 + 1 + 2). Sized +// 32 to reuse f64tos's proven byte-id-clean band: a no-rhs [N]u8 module +// buffer at the size-16/24 band emits divergent DATAW counts cs≠ww (#43); +// 32 emits 2 DATAW in both. The unused tail bytes are harmless. +let f32tos_buf: [32]u8; + +export fn f32tos(n: f32) str = { + let bits: u32 = math.f32bits(n); + let mantissa: u32 = bits & math.F32_MANTISSA_MASK; + let exponent: u32 = (bits >> math.F32_MANTISSA_BITS) & math.F32_EXPONENT_MASK; + let sign: bool = (bits >> (math.F32_EXPONENT_BITS + math.F32_MANTISSA_BITS)) > 0u32; + let special: bool = exponent == math.F32_EXPONENT_MASK; + + let o: i32 = 0i32; + let r: str; + r.ptr = &f32tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u32) { + o = putstr(f32tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f32tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f32tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u32) { + f32tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // *decimal pointer for the field reads (the #170 dodge; see f64tos). + let pd: *decimal = &d; + let dd: decf32 = f32todecf32(mantissa, exponent); + init_dec_mant_exp(pd, (dd.mantissa: u64), (dd.exponent: i32)); + trim(pd); + if (pd.nd == (0u64: size)) { + f32tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f32tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f32tos_buf[0:32], o); + }; + r.len = o; + return r; +}; + // 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 @@ -1916,12 +2165,18 @@ package strconv; // 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:162-163. The f32 split-table bit-counts, +// derived from the f64 siblings (Hare: F64_..._BITCOUNT - 64). Consumed by +// f32todecf32 (ftos.ww), landed in fold-5b (task #67) — the f32 path reuses +// the f64 SPLIT2 tables (via f64computeinvpow5/f64computepow5), so no +// separate F32 tables exist (matches ftos_ryu.ha). u8 like the f64 defs; +// ftos.ww casts to u32/i32 at each use. +def F32_POW5_INV_BITCOUNT: u8 = F64_POW5_INV_BITCOUNT - 64u8; +def F32_POW5_BITCOUNT: u8 = F64_POW5_BITCOUNT - 64u8; + // ref/hare/strconv/ftos_ryu.ha:165-181. let F64_POW5_INV_SPLIT2: [15][2]u64 = [ [1u64, 2305843009213693952u64], diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 7863f99b..6d302631 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -1345,15 +1345,14 @@ export fn absi64(n: i64) u64 = { // 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. +// SCOPE — the f64tos + f32tos shortest-representation subset (Hare's +// ffmt::G, prec=void, fflags::NONE). f32tos (ftos.ha:448) + its f32 Ryū +// sub-path (f32todecf32 + mulpow5inv/pow5_divpow2 + mulshift32 + the *32 +// helpers, reusing the shared u64-core + the f64 SPLIT2 tables — the f32 +// path has no separate tables, matching ftos_ryu.ha) ship here in fold-5b +// (task #67): the gating #143 f32-arg-push cgen fix landed (aff7725, MOVSS +// both stages), so f32tos's math.f32bits(n) call — passing an f32 arg — is +// now byte-id-clean. One deferral remains: // - 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 @@ -1681,6 +1680,195 @@ fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { return decf64 { exponent = (exp: i64), mantissa = output }; }; +// ==== f32 Ryū sub-path (ftos_ryu.ha). The *32 helpers below mirror their +// u64 siblings at 32-bit width; they reuse the SHARED f64computeinvpow5/ +// f64computepow5 (and thus the f64 SPLIT2 tables) per ftos_ryu.ha — there +// is no separate f32 table. Same scalar-PARAM-mutation → copy-to-local, +// comma-split, assert → os.assert, expr-yield → block divergences as the +// f64 path above. ==== + +// ref/hare/strconv/ftos_ryu.ha:52. Largest p with 5^p | value (32-bit). +fn pow5fac32(v: u32) u32 = { + let value: u32 = v; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u32, "strconv.pow5fac32: value == 0"); + let q: u32 = value / 5u32; + let r: u32 = value % 5u32; + if (r != 0u32) { break; }; + value = q; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:67. +fn pow5multiple32(v: u32, p: u32) bool = { return pow5fac32(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:75. +fn pow2multiple32(v: u32, p: u32) bool = { + os.assert(v > 0u32, "strconv.pow2multiple32: v == 0"); + os.assert(p < 32u32, "strconv.pow2multiple32: p >= 32"); + return (v & ((1u32 << p) - 1u32)) == 0u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:121. `m * a_lo` etc. carry an explicit +// (m: u64) cast (Hare promotes the u32 operand; ww is strict). The bound +// assert inlines U32_MAX's value: ww's types.U32_MAX is package-private +// (lib/types/types.ww — no `export`), so Hare's `types::U32_MAX` can't be +// referenced cross-package. +fn mulshift32(m: u32, a: u64, s: u32) u32 = { + os.assert(s > 32u32, "strconv.mulshift32: s <= 32"); + let a_lo: u64 = (a: u32): u64; + let a_hi: u64 = a >> 32u64; + let b0: u64 = (m: u64) * a_lo; + let b1: u64 = (m: u64) * a_hi; + let sum: u64 = (b0 >> 32u64) + b1; + let ss: u64 = sum >> ((s: u64) - 32u64); + os.assert(ss <= 4294967295u64, "strconv.mulshift32: ss > U32_MAX"); + return ss: u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:130. +fn mulpow5inv_divpow2(m: u32, q: u32, j: i32) u32 = { + let pow5 = f64computeinvpow5(q); + return mulshift32(m, pow5.1 + 1u64, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:135. +fn mulpow5_divpow2(m: u32, i: u32, j: i32) u32 = { + let pow5 = f64computepow5(i); + return mulshift32(m, pow5.1, (j: u32)); +}; + +// ref/hare/strconv/ftos_ryu.ha:387. `exponent` rides i64 not Hare's i32, +// for the same reason decf64 does: widening the field to a full second +// eightbyte SIDESTEPS the #169 narrow-i32-field struct-return unpack (a +// narrow i32 there unpacks MOVL wwstage vs MOVQ cstage). The value always +// fits i32 (cast at the init_dec_mant_exp call site). `mantissa` stays u32 +// (Hare's width); the {u32, pad, i64} layout's first eightbyte holds +// mantissa@0 + 4B pad and reads cleanly — byte-id CONFIRMED by the 990-997 +// gate (0-diff cs vs ww), not relied on as an ABI guarantee. +type decf32 = struct { mantissa: u32, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:392. Shortest decimal of an f32: +// value == mantissa * 10^exponent. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f32. +fn f32todecf32(mantissa: u32, exponent: u32) decf32 = { + let e2: i32 = (math.F32_EXPONENT_BIAS + math.F32_MANTISSA_BITS + 2u32): i32; + let m2: u32 = 0u32; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u32 << math.F32_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u32) == 0u32; + let mv: u32 = 4u32 * m2; + let mp: u32 = mv + 2u32; + let mm_shift: u32 = ibool(mantissa != 0u32 || exponent <= 1u32): u32; + let mm: u32 = mv - 1u32 - mm_shift; + let vr: u32 = 0u32; + let vp: u32 = 0u32; + let vm: u32 = 0u32; + let e10: i32 = 0i32; + let vm_trailing_zeroes: bool = false; + let vr_trailing_zeroes: bool = false; + let last_removed_digit: u8 = 0u8; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32); + e10 = q: i32; + let k: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + vr = mulpow5inv_divpow2(mv, q, i); + vp = mulpow5inv_divpow2(mp, q, i); + vm = mulpow5inv_divpow2(mm, q, i); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + let l: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q - 1u32) - 1u32; + // #168 dodge: div/mod on an inline N_CALL result emits a + // signed IDIVQ in wwstage; bind the call to a local first. + let lrd: u32 = mulpow5inv_divpow2(mv, q - 1u32, + -e2 + ((q + l): i32) - 1i32); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 9u32) { + if (mv % 5u32 == 0u32) { + vr_trailing_zeroes = pow5multiple32(mv, q); + } else if (accept_bounds) { + vm_trailing_zeroes = pow5multiple32(mm, q); + } else { + vp -= (ibool(pow5multiple32(mp, q)): u32); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32); + e10 = (q: i32) + e2; + let i: u32 = (-e2 - (q: i32)): u32; + let k: u32 = pow5bits(i) - (F32_POW5_BITCOUNT: u32); + let j: i32 = (q: i32) - (k: i32); + vr = mulpow5_divpow2(mv, i, j); + vp = mulpow5_divpow2(mp, i, j); + vm = mulpow5_divpow2(mm, i, j); + if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { + j = (q: i32) - 1i32 - ((pow5bits(i + 1u32): i32) - (F32_POW5_BITCOUNT: i32)); + // #168 dodge (see above): local-bind before the % 10. + let lrd: u32 = mulpow5_divpow2(mv, (i + 1u32), j); + last_removed_digit = (lrd % 10u32): u8; + }; + if (q <= 1u32) { + vr_trailing_zeroes = true; + if (accept_bounds) { + vm_trailing_zeroes = mm_shift == 1u32; + } else { + vp -= 1u32; + }; + } else if (q < 31u32) { + vr_trailing_zeroes = pow2multiple32(mv, q - 1u32); + }; + }; + let removed: i32 = 0i32; + let output: u32 = 0u32; + if (vm_trailing_zeroes || vr_trailing_zeroes) { + for ((vp / 10u32) > (vm / 10u32)) { + vm_trailing_zeroes = vm_trailing_zeroes && ((vm - (vm / 10u32) * 10u32) == 0u32); + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + if (vm_trailing_zeroes) { + for ((vm % 10u32) == 0u32) { + vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + }; + if (vr_trailing_zeroes && last_removed_digit == 5u8 && vr % 2u32 == 0u32) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeroes)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u32); + } else { + for ((vp / 10u32) > (vm / 10u32)) { + last_removed_digit = (vr % 10u32): u8; + vr /= 10u32; + vp /= 10u32; + vm /= 10u32; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || last_removed_digit >= 5u8): u32); + }; + let exp: i32 = e10 + removed; + return decf32 { mantissa = output, exponent = (exp: i64) }; +}; + // ==== 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 @@ -1889,6 +2077,67 @@ export fn f64tos(n: f64) str = { return r; }; +// ref/hare/strconv/ftos.ha:448. f32 → shortest base-10 str. Same static- +// buffer convention + G/void/NONE-inlined path as f64tos. f32bits(n) +// passes an f32 arg → MOVSS both stages post-#143 (aff7725); this is the +// piece fold-5b was gated on. +// +// Hare sizes this [14]u8 (ftos.ha:451: 1 + 1 + 1 + 7 + 1 + 1 + 2). Sized +// 32 to reuse f64tos's proven byte-id-clean band: a no-rhs [N]u8 module +// buffer at the size-16/24 band emits divergent DATAW counts cs≠ww (#43); +// 32 emits 2 DATAW in both. The unused tail bytes are harmless. +let f32tos_buf: [32]u8; + +export fn f32tos(n: f32) str = { + let bits: u32 = math.f32bits(n); + let mantissa: u32 = bits & math.F32_MANTISSA_MASK; + let exponent: u32 = (bits >> math.F32_MANTISSA_BITS) & math.F32_EXPONENT_MASK; + let sign: bool = (bits >> (math.F32_EXPONENT_BITS + math.F32_MANTISSA_BITS)) > 0u32; + let special: bool = exponent == math.F32_EXPONENT_MASK; + + let o: i32 = 0i32; + let r: str; + r.ptr = &f32tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u32) { + o = putstr(f32tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f32tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f32tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u32) { + f32tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // *decimal pointer for the field reads (the #170 dodge; see f64tos). + let pd: *decimal = &d; + let dd: decf32 = f32todecf32(mantissa, exponent); + init_dec_mant_exp(pd, (dd.mantissa: u64), (dd.exponent: i32)); + trim(pd); + if (pd.nd == (0u64: size)) { + f32tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f32tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f32tos_buf[0:32], o); + }; + r.len = o; + return r; +}; + // 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 @@ -1916,12 +2165,18 @@ package strconv; // 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:162-163. The f32 split-table bit-counts, +// derived from the f64 siblings (Hare: F64_..._BITCOUNT - 64). Consumed by +// f32todecf32 (ftos.ww), landed in fold-5b (task #67) — the f32 path reuses +// the f64 SPLIT2 tables (via f64computeinvpow5/f64computepow5), so no +// separate F32 tables exist (matches ftos_ryu.ha). u8 like the f64 defs; +// ftos.ww casts to u32/i32 at each use. +def F32_POW5_INV_BITCOUNT: u8 = F64_POW5_INV_BITCOUNT - 64u8; +def F32_POW5_BITCOUNT: u8 = F64_POW5_BITCOUNT - 64u8; + // ref/hare/strconv/ftos_ryu.ha:165-181. let F64_POW5_INV_SPLIT2: [15][2]u64 = [ [1u64, 2305843009213693952u64],