lib/strconv: stof.ha port — Eisel-Lemire string→float (#106 fold-4)

stof64/stof32 (f64|f32 | invalid | overflow) via Eisel-Lemire fast-path
(powers_of_ten[596][2]u64 + eisel_lemire 128-bit multiply) + decimal
slow-path fallback (decimal.ww, fold-3). 16 fns + faithful powers_of_ten
(byte-identical to Hare). u128 via pure-u64 64×64→128 (ftos_ryu.ha).
Consumes &math.f64info (γ-cleanup), tagged-float-return (PREREQ-2 #157),
2D double-index (PREREQ-1 #156).

13 documented spelling-divergences (rule-9, each cites stof.ha): #155
(po10 double-index + per-field struct-copy), #161 (compound-assign explicit
form), #144 (-0.0 via 1u64<<63), #158, #138, test-only #143/parsef64.
Test 909 (DEC+hex+NaN/Inf/invalid/overflow, bit-exact, cstage ww run).
Make test 185/185 incl 990-997 byte-id + combined_ww_fresh. Makefile:
stof.ww added to w6c_ww/wwdump_ww deps (freshness, fold-3 precedent).

Drew's strconv 5-fold plan 4/5. Followup #162 (wwstage lexer parsef64
1-ULP — could adopt stof64).
This commit is contained in:
2026-05-27 13:55:56 +09:00
parent 4a91bdc8db
commit 81796cd533
8 changed files with 6977 additions and 606 deletions

700
lib/strconv/stof.ww Normal file
View File

@@ -0,0 +1,700 @@
// strconv — string-to-float. 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 os;
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)
};
@symbol("rt_abort") fn abort(msg: str) void;
// 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 = {
os.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;
};
os.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;
};
os.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)
};