// 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 + 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 // 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 }; }; // ==== 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; last_removed_digit = (mulpow5inv_divpow2(mv, q - 1u32, -e2 + ((q + l): i32) - 1i32) % 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)); last_removed_digit = (mulpow5_divpow2(mv, (i + 1u32), j) % 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 // 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; }; // 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; };