// Mirrors ref/hare/strconv/stof.ha (Hare in turn adapts Go): // Eisel-Lemire fast path [1] with the Simple-Decimal-Conversion slow // path [2] (decimal.ww) as fallback. // [1]: https://nigeltao.github.io/blog/2020/eisel-lemire.html // [2]: https://nigeltao.github.io/blog/2020/parse-number-f64-simple.html // // The Eisel-Lemire fast path (`eisel_lemire` + the `powers_of_ten` // table in stof_data.ww + the three call sites: floatbits's d.nd<=19 // block, stof64/stof32's !truncated block) is a pure speed // optimisation — it returns the same correctly-rounded value the // decimal slow path (decimal_parse → floatbits) computes, or void to // defer. Its prereqs landed: the 2D `[596][2]u64` static-init + // double-index read (#156) and the tagged float-variant return-pack // (#157, which the public `(f64|invalid|overflow)` return needs). // // Spelling divergences from Hare (mechanical, ww parser/cgen shape): // - str scan index rides `i32` (ww `str.len: i32` + `invalid = !i32` // payload), not Hare's `size`/`len(s)`. lib CLAUDE.md str-index note. // - char literals kept faithful (`buf[i] == '.'`, `c - '0'`); probed // byte-id + value-correct both stages. // - Hare `?` error-propagation → nested statement-`match` with all- // return arms + a `case void => void` continuation. ww's `?` // lowering and a bound `match`-expression with mixed yield/return // arms both diverge cs≠ww (the latter wwstage-checker-rejected); // strconv.ww's stoi32 set the explicit-match precedent. // - Hare `for (cond; afterthought)` 2-clause + `continue` → ww // 2-clause `for (cond)` with the afterthought inlined at body end // AND before each `continue` (ww has no empty-init 3-clause // `for (; c; p)`; #138 post-skip is dodged since 2-clause has no // post). decimal.ww set the inline-afterthought precedent. // - Hare `if`/`switch`-expression yield → explicit if-statements + // pre-bound scalar locals (ww has no expression-bodied if). // - Hare fn-pointer-in-tuple + `switch yield` selecting the digit // predicate in fast_parse → a `base==HEX` bool + an `isdigitbase` // helper that branches to ascii.isdigit/isxdigit (no fn-ptr, no // tuple, no switch). // - struct-param field MUTATION (hex_to_bits mutates its by-value // `p`) → copy p's fields to scalar locals at entry; ww miscompiles // + diverges on writing a by-value struct param's fields (filed). // - default arg dropped: Hare `b: base = base::DEC` → callers pass // base explicitly (no lib fn ships a default arg; strconv.ww // stoi64 precedent). The base param is normalised through a local // `bb` (param reassignment avoided). // - `math::NAN`/`math::INF` (f32) absent in ww math → materialised // via f32frombits of the IEEE-754 f32 bit patterns (same honest // construction as math/floats.ww's NAN_BITS/INF_BITS). // - narrowing int→i32 assignments carry explicit casts (ww `int` is // an 8B machine word; project_int_machine_word_derived_limits). // - `r128`/`u128mul` live here (fold-4 is first consumer); fold-5 // ftos (Ryū) shares them in-package. package strconv; import ascii; import math; import strings; // ref/hare/strconv/ftos_ryu.ha:12. 64×64→128 result halves. type r128 = struct { hi: u64, lo: u64, }; // ref/hare/strconv/ftos_ryu.ha:18. 64×64→128 via 32-bit decomposition // (Hare's own "TODO: use 128-bit integers when implemented" — ww has // no u128; the decomposition is the portable shape both stages agree // on). Comma let-bindings split per decimal.ww divergence. fn u128mul(a: u64, b: u64) r128 = { let a0: u64 = (a: u32): u64; let a1: u64 = a >> 32u64; let b0: u64 = (b: u32): u64; let b1: u64 = b >> 32u64; let p00: u64 = a0 * b0; let p01: u64 = a0 * b1; let p10: u64 = a1 * b0; let p11: u64 = a1 * b1; let p00_lo: u64 = (p00: u32): u64; let p00_hi: u64 = p00 >> 32u64; let mid1: u64 = p10 + p00_hi; let mid1_lo: u64 = (mid1: u32): u64; let mid1_hi: u64 = mid1 >> 32u64; let mid2: u64 = p01 + mid1_lo; let mid2_lo: u64 = (mid2: u32): u64; let mid2_hi: u64 = mid2 >> 32u64; let r_hi: u64 = p11 + mid1_hi + mid2_hi; let r_lo: u64 = (mid2_lo << 32u64) | p00_lo; return r128 { hi = r_hi, lo = r_lo }; }; // ref/hare/strconv/stof.ha:14. fn todig(c: u8) u8 = { if ('0' <= c && c <= '9') { return c - '0'; }; if ('a' <= c && c <= 'f') { return c - 'a' + 10u8; }; if ('A' <= c && c <= 'F') { return c - 'A' + 10u8; }; abort("strconv.todig: unreachable"); return 0u8; // unreachable; rt_abort is void-typed (path-cov) }; // ref/hare/strconv/stof.ha:25. type fast_parsed_float = struct { mantissa: u64, exponent: i32, negative: bool, truncated: bool, }; // Digit-class predicate selector for fast_parse — replaces Hare's // fn-pointer-in-tuple (`&ascii::isdigit` / `&ascii::isxdigit`). fn isdigitbase(c: rune, ishex: bool) bool = { if (ishex) { return ascii.isxdigit(c); }; return ascii.isdigit(c); }; // ref/hare/strconv/stof.ha:32. fn fast_parse(s: str, b: base) (fast_parsed_float | invalid) = { let buf: []u8 = strings.toutf8(s); let i: i32 = 0; let neg: bool = false; let trunc: bool = false; if (buf[i] == '-') { neg = true; i += 1; } else if (buf[i] == '+') { i += 1; }; let ishex: bool = (b == base.HEX); let expchr: rune = 'e'; let max_ndmant: int = 19; if (ishex) { expchr = 'p'; max_ndmant = 16; }; let bnum: u64 = (b: i32): u64; let sawdot: bool = false; let sawdigits: bool = false; let nd: int = 0; let ndmant: int = 0; let dp: int = 0; let mant: u64 = 0u64; let exp: i32 = 0i32; for (i < s.len) { if (buf[i] == '.') { if (sawdot) { return i: invalid; }; sawdot = true; dp = nd; } else if (isdigitbase(buf[i]: rune, ishex)) { sawdigits = true; if (buf[i] == '0' && nd == 0) { dp -= 1; i += 1; continue; }; nd += 1; if (ndmant < max_ndmant) { mant = mant * bnum + (todig(buf[i]): u64); ndmant += 1; } else if (buf[i] != '0') { trunc = true; }; } else { break; }; i += 1; }; if (!sawdigits) { return i: invalid; }; if (!sawdot) { dp = nd; }; if (b == base.HEX) { dp *= 4; ndmant *= 4; }; if (i < s.len && ascii.tolower(buf[i]: rune) == expchr) { i += 1; if (i >= s.len) { return i: invalid; }; let expsign: int = 1; if (buf[i] == '+') { i += 1; } else if (buf[i] == '-') { expsign = -1; i += 1; }; if (i >= s.len || !ascii.isdigit(buf[i]: rune)) { return i: invalid; }; let e: int = 0; for (i < s.len && ascii.isdigit(buf[i]: rune)) { if (e < 10000) { e = e * 10 + ((buf[i] - '0'): int); }; i += 1; }; dp += e * expsign; } else if (b == base.HEX) { return i: invalid; // hex floats must have an exponent }; if (i != s.len) { return i: invalid; }; if (mant != 0u64) { exp = (dp - ndmant): i32; }; return fast_parsed_float { mantissa = mant, exponent = exp, negative = neg, truncated = trunc, }; }; // ref/hare/strconv/stof.ha:115. Fills the slow-path decimal `d`. fn decimal_parse(d: *decimal, s: str) (void | invalid) = { let i: i32 = 0; let buf: []u8 = strings.toutf8(s); d.negative = false; d.truncated = false; if (buf[0] == '+') { i += 1; } else if (buf[0] == '-') { d.negative = true; i += 1; }; let sawdot: bool = false; let sawdigits: bool = false; for (i < s.len) { if (buf[i] == '.') { if (sawdot) { return i: invalid; }; sawdot = true; d.dp = (d.nd: i32); } else if (ascii.isdigit(buf[i]: rune)) { sawdigits = true; if (buf[i] == '0' && d.nd == (0u64: size)) { d.dp -= 1; i += 1; continue; }; if (d.nd < (len(d.digits): size)) { d.digits[d.nd] = buf[i] - '0'; d.nd += (1u64: size); } else if (buf[i] != '0') { d.truncated = true; }; } else { break; }; i += 1; }; if (!sawdigits) { return i: invalid; }; if (!sawdot) { d.dp = (d.nd: i32); }; if (i < s.len && (buf[i] == 'e' || buf[i] == 'E')) { i += 1; if (i >= s.len) { return i: invalid; }; let expsign: int = 1; if (buf[i] == '+') { i += 1; } else if (buf[i] == '-') { expsign = -1; i += 1; }; if (i >= s.len || !ascii.isdigit(buf[i]: rune)) { return i: invalid; }; let e: int = 0; for (i < s.len && ascii.isdigit(buf[i]: rune)) { if (e < 10000) { e = e * 10 + ((buf[i] - '0'): int); }; i += 1; }; d.dp += (e * expsign): i32; }; if (i != s.len) { return i: invalid; }; return; }; // ref/hare/strconv/stof.ha:173. Count of leading zero bits in n>0. fn leading_zeroes(n: u64) uint = { assert(n > 0u64, "strconv.leading_zeroes: n == 0"); let b: u64 = 0u64; if ((n & 0xFFFFFFFF00000000u64) > 0u64) { n >>= 32u64; b |= 32u64; }; if ((n & 0xFFFF0000u64) > 0u64) { n >>= 16u64; b |= 16u64; }; if ((n & 0xFF00u64) > 0u64) { n >>= 8u64; b |= 8u64; }; if ((n & 0xF0u64) > 0u64) { n >>= 4u64; b |= 4u64; }; if ((n & 0xCu64) > 0u64) { n >>= 2u64; b |= 2u64; }; if ((n & 0x2u64) > 0u64) { n >>= 1u64; b |= 1u64; }; return ((63u64 - b): uint); }; // ref/hare/strconv/stof.ha:203. Eisel-Lemire fast path: a correctly- // rounded f64/f32 from (mantissa, exp10) when the 128-bit product is // unambiguous, else void → caller falls to the decimal slow path. // Divergences at-site: `mantissa <<= clz` (scalar-param mutate) → local // `mnt`; whole-struct local reassign `x = merged` copies only the first // word in cgen → per-field `x.hi = …; x.lo = …` (#155); `po10 = // powers_of_ten[i]` row-bind → direct double-index (#155, A2); bitwise- // vs-compare fully parenthesised; comma let-bindings split. fn eisel_lemire( mantissa: u64, exp10: i32, neg: bool, f: *math.floatinfo, ) (u64 | void) = { if (mantissa == 0u64 || exp10 > 288 || exp10 < -307) { return; }; let idx: i32 = exp10 + 307; let clz: uint = leading_zeroes(mantissa); let mnt: u64 = mantissa << (clz: u64); let shift: u64 = 64u64 - f.mantbits - 3u64; let mask: u64 = (1u64 << shift) - 1u64; // log(10)/log(2) ≈ 217706 / 65536; x / 65536 = x >> 16. let exp: int = (217706 * (exp10: int)) >> 16; let e2: u64 = ((exp + f.expbias + 64): u64) - (clz: u64); let x: r128 = u128mul(mnt, powers_of_ten[idx][1]); if ((x.hi & mask) == mask && (x.lo + mnt) < mnt) { let y: r128 = u128mul(mnt, powers_of_ten[idx][0]); let merged: r128 = r128 { hi = x.hi, lo = x.lo + y.hi }; if (merged.lo < x.lo) { // local-struct-field compound-assign drops the load in // wwstage (sets =1, not +=1) — explicit form, byte-id. merged.hi = merged.hi + 1u64; }; if ((merged.hi & mask) == mask && (merged.lo + 1u64) == 0u64 && (y.lo + mnt) < mnt) { return; }; x.hi = merged.hi; x.lo = merged.lo; }; let msb: u64 = x.hi >> 63u64; let mant: u64 = x.hi >> (msb + shift); e2 -= 1u64 ^ msb; if (x.lo == 0u64 && (x.hi & mask) == 0u64 && (mant & 3u64) == 1u64) { return; }; mant += mant & 1u64; mant >>= 1u64; if ((mant >> (f.mantbits + 1u64)) > 0u64) { mant >>= 1u64; e2 += 1u64; }; if (e2 <= 0u64 || e2 >= (1u64 << f.expbits) - 1u64) { return; }; return mkfloat(mant, (e2: uint), neg, f); }; // ref/hare/strconv/stof.ha:247. Slow-path: decimal `d` → IEEE bits. fn floatbits(d: *decimal, f: *math.floatinfo) (u64 | overflow) = { let e: int = 0; let m: u64 = 0u64; let powtab: [19]i8 = [ 0i8, 3i8, 6i8, 9i8, 13i8, 16i8, 19i8, 23i8, 26i8, 29i8, 33i8, 36i8, 39i8, 43i8, 46i8, 49i8, 53i8, 56i8, 59i8, ]; if (d.nd == (0u64: size) || d.dp < -326) { if (d.negative) { return mkfloat(0u64, (0u32: uint), d.negative, f); }; return 0u64; } else if (d.dp > 310) { return overflow{}; }; if (d.nd <= (19u64: size)) { let dmant: u64 = 0u64; let i: size = (0u64: size); for (i < d.nd) { dmant = 10u64 * dmant + (d.digits[i]: u64); i += (1u64: size); }; let exp10: i32 = d.dp - (d.nd: i32); match (eisel_lemire(dmant, exp10, d.negative, f)) { case let r: u64 => { return r; }; case void => void; }; }; for (d.dp > 0) { let n: int = 0; if ((d.dp: uint) >= (len(powtab): uint)) { n = (maxshift: int); } else { n = (powtab[d.dp]: int); }; decimal_shift(d, -n); e += n; }; for (d.dp <= 0) { let n: int = 0; if (d.dp == 0) { if (d.digits[0] >= 5u8) { break; }; if (d.digits[0] < 2u8) { n = 2; } else { n = 1; }; } else if ((-d.dp) >= (len(powtab): i32)) { n = (maxshift: int); } else { n = (powtab[-d.dp]: int); }; decimal_shift(d, n); e -= n; }; e -= 1; if (e <= -f.expbias + 1) { let nn: int = -f.expbias - e + 1; decimal_shift(d, -nn); e += nn; }; if (e + f.expbias >= ((1u64 << f.expbits): int) - 1) { return overflow{}; }; decimal_shift(d, (f.mantbits: int) + 1); m = decimal_round(d); if (m == (2u64 << f.mantbits)) { m >>= 1u64; e += 1; if (e + f.expbias >= ((1u64 << f.expbits): int) - 1) { return overflow{}; }; }; if ((m & (1u64 << f.mantbits)) == 0u64) { e = -f.expbias; }; return mkfloat(m, ((e + f.expbias): uint), d.negative, f); }; // ref/hare/strconv/stof.ha:311. Assemble sign|exp|mantissa. fn mkfloat(m: u64, e: uint, negative: bool, f: *math.floatinfo) u64 = { let n: u64 = m & ((1u64 << f.mantbits) - 1u64); n |= ((e: u64) & ((1u64 << f.expbits) - 1u64)) << f.mantbits; if (negative) { n |= 1u64 << (f.mantbits + f.expbits); }; return n; }; // ref/hare/strconv/stof.ha:320. Exact f64 powers of ten 1e0..1e22 (all // exactly representable; see stof64exact). let f64pow10: [23]f64 = [ 1.0e0, 1.0e1, 1.0e2, 1.0e3, 1.0e4, 1.0e5, 1.0e6, 1.0e7, 1.0e8, 1.0e9, 1.0e10, 1.0e11, 1.0e12, 1.0e13, 1.0e14, 1.0e15, 1.0e16, 1.0e17, 1.0e18, 1.0e19, 1.0e20, 1.0e21, 1.0e22, ]; // ref/hare/strconv/stof.ha:326. fn stof64exact(mant: u64, exp: i32, neg: bool) (f64 | void) = { if (mant >> math.F64_MANTISSA_BITS != 0u64) { return; }; let n: f64 = (mant: i64): f64; if (neg) { n = -n; }; if (exp == 0i32) { return n; }; if (-22i32 <= exp && exp <= 22i32) { if (exp >= 0i32) { // f64 compound-assign mis-lowers in cgen — explicit // form (strconv.ww f64tos precedent). n = n * f64pow10[exp]; } else { n = n / f64pow10[-exp]; }; } else { return; }; return n; }; // ref/hare/strconv/stof.ha:345. Exact f32 powers of ten 1e0..1e10. let f32pow10: [11]f32 = [ 1.0e0f32, 1.0e1f32, 1.0e2f32, 1.0e3f32, 1.0e4f32, 1.0e5f32, 1.0e6f32, 1.0e7f32, 1.0e8f32, 1.0e9f32, 1.0e10f32, ]; // ref/hare/strconv/stof.ha:349. fn stof32exact(mant: u64, exp: i32, neg: bool) (f32 | void) = { if (mant >> (math.F32_MANTISSA_BITS: u64) != 0u64) { return; }; let n: f32 = (mant: i32): f32; if (neg) { n = -n; }; if (exp == 0i32) { return n; }; if (-10i32 <= exp && exp <= 10i32) { if (exp >= 0i32) { // f32 compound-assign mis-lowers in cgen — explicit form. n = n * f32pow10[exp]; } else { n = n / (f64pow10[-exp]: f32); }; } else { return; }; return n; }; // ref/hare/strconv/stof.ha:369. Adapted from Go's atofHex. The by-value // `p` is mutated in Hare; ww copies its fields to scalar locals (struct // param field-write miscompiles + diverges — filed). fn hex_to_bits(p: fast_parsed_float, info: *math.floatinfo) (u64 | overflow) = { let pmant: u64 = p.mantissa; let pexp: i32 = p.exponent; let pneg: bool = p.negative; let ptrunc: bool = p.truncated; let max_exp: int = ((1u64 << info.expbits): int) - info.expbias - 2; let min_exp: int = -info.expbias + 1; pexp += (info.mantbits: i32); // Shift left until a leading 1 bit followed by mantbits + 2 rounding. for (pmant != 0u64 && pmant >> (info.mantbits + 2u64) == 0u64) { pmant <<= 1u64; pexp -= 1; }; if (ptrunc) { pmant |= 1u64; }; // Too many bits: shift right (sticky-or the dropped bit). for (pmant >> (3u64 + info.mantbits) != 0u64) { pmant = (pmant >> 1u64) | (pmant & 1u64); pexp += 1; }; // Denormalise if the exponent is small. for (pmant > 1u64 && pexp < (min_exp: i32) - 2) { pmant = (pmant >> 1u64) | (pmant & 1u64); pexp += 1; }; // Round to even. let round: u64 = pmant & 3u64; pmant >>= 2u64; round |= pmant & 1u64; pexp += 2; if (round == 3u64) { pmant += 1u64; if (pmant == 1u64 << (1u64 + info.mantbits)) { pmant >>= 1u64; pexp += 1; }; }; // Denormal or zero. if (pmant >> info.mantbits == 0u64) { pexp = (-info.expbias): i32; }; if (pexp > (max_exp: i32)) { return overflow{}; }; let bits: u64 = pmant & info.mantmask; bits |= (((pexp + (info.expbias: i32)): u64) & info.expmask) << info.mantbits; if (pneg) { bits |= 1u64 << (info.mantbits + info.expbits); }; return bits; }; // ref/hare/strconv/stof.ha:425. "nan"/"infinity"/±"infinity", // case-insensitive. ww math has no f32 NAN/INF consts → f32frombits of // the IEEE-754 f32 bit patterns (qNaN 0x7FC00000, ±Inf 0x7F800000 / // 0xFF800000). fn special(s: str) (f32 | void) = { if (ascii.strcasecmp(s, "nan") == 0) { return math.f32frombits(0x7FC00000u32); } else if (ascii.strcasecmp(s, "infinity") == 0) { return math.f32frombits(0x7F800000u32); } else if (ascii.strcasecmp(s, "+infinity") == 0) { return math.f32frombits(0x7F800000u32); } else if (ascii.strcasecmp(s, "-infinity") == 0) { return math.f32frombits(0xFF800000u32); }; return; }; // ref/hare/strconv/stof.ha:445. Parse `s` as f64 (base DEC or HEX). See // the module note: the EL fast path is HELD; the decimal fallback gives // correct results meanwhile. export fn stof64(s: str, b: base) (f64 | invalid | overflow) = { let bb: base = b; if (bb == base.DEFAULT) { bb = base.DEC; } else if (bb == base.HEX_LOWER) { bb = base.HEX; }; assert(bb == base.DEC || bb == base.HEX, "strconv.stof64: base must be DEC or HEX"); if (s.len == 0) { return 0: invalid; }; match (special(s)) { case let f: f32 => { return (f: f64); }; case void => void; }; match (fast_parse(s, bb)) { case let p: fast_parsed_float => { if (bb == base.HEX) { match (hex_to_bits(p, &math.f64info)) { case let bits: u64 => { return math.f64frombits(bits); }; case let eo: overflow => { return eo; }; }; } else if (!p.truncated) { match (stof64exact(p.mantissa, p.exponent, p.negative)) { case let n: f64 => { return n; }; case void => void; }; match (eisel_lemire(p.mantissa, p.exponent, p.negative, &math.f64info)) { case let n: u64 => { return math.f64frombits(n); }; case void => void; }; }; let d = decimal { ... }; match (decimal_parse(&d, s)) { case let ei: invalid => { return ei; }; case void => void; }; match (floatbits(&d, &math.f64info)) { case let n: u64 => { return math.f64frombits(n); }; case let eo: overflow => { return eo; }; }; }; case let ei: invalid => { return ei; }; }; return 0: invalid; // unreachable (path-cov) }; // ref/hare/strconv/stof.ha:491. Parse `s` as f32 (base DEC or HEX). export fn stof32(s: str, b: base) (f32 | invalid | overflow) = { let bb: base = b; if (bb == base.DEFAULT) { bb = base.DEC; } else if (bb == base.HEX_LOWER) { bb = base.HEX; }; assert(bb == base.DEC || bb == base.HEX, "strconv.stof32: base must be DEC or HEX"); if (s.len == 0) { return 0: invalid; }; match (special(s)) { case let f: f32 => { return f; }; case void => void; }; match (fast_parse(s, bb)) { case let p: fast_parsed_float => { if (bb == base.HEX) { match (hex_to_bits(p, &math.f32info)) { case let bits: u64 => { return math.f32frombits(bits: u32); }; case let eo: overflow => { return eo; }; }; } else if (!p.truncated) { match (stof32exact(p.mantissa, p.exponent, p.negative)) { case let n: f32 => { return n; }; case void => void; }; match (eisel_lemire(p.mantissa, p.exponent, p.negative, &math.f32info)) { case let n: u64 => { return math.f32frombits(n: u32); }; case void => void; }; }; let d = decimal { ... }; match (decimal_parse(&d, s)) { case let ei: invalid => { return ei; }; case void => void; }; match (floatbits(&d, &math.f32info)) { case let n: u64 => { return math.f32frombits(n: u32); }; case let eo: overflow => { return eo; }; }; }; case let ei: invalid => { return ei; }; }; return 0: invalid; // unreachable (path-cov) };