From 0e66073e3245e32d93c6d6cfcb7d1bf55d86c17e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 27 May 2026 15:39:03 +0900 Subject: [PATCH] =?UTF-8?q?lib/strconv:=20f64tos=20Ry=C5=AB=20shortest=20f?= =?UTF-8?q?loat=E2=86=92string=20(#106=20fold-5a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graduate f64tos from the lossy fixed-point placeholder to Ryū shortest- round-trippable (ftos.ha:432 + ftos_ryu.ha). f64tos(n:f64) str, G-format (void/NONE) — the faithful documented subset (full parametric fftosf is dead code for G/void/NONE → deferred #64). Ryū core decomposed: struct- RETURN + scalar params (Hare's r128 idiom; avoids tuple-ABI #163-166). f64 powers tables [15][2]/[13][2]u64 (2D #156). Zero float literals. Both E+F encode paths reachable+tested, no dead code. Graduation (lib-note "don't keep both"): old lossy f64tos deleted; fmt fprintf_f64_huge "huge"→"9.5e18" (improvement). 5 value-faithful filed- bug dodges (byte-id, documented): #167/#169/#170/#43/#144. Test 908. Make test 186/186 incl 990-997 byte-id + combined_ww_fresh. Drew's strconv 5-fold plan — primary completion (f64tos). f32tos coda #67 (behind #143); parametric ftosf #64. --- Makefile | 10 +- lib/fmt/fmt.ww | 12 +- lib/fmt/fmttest.ww | 7 +- lib/strconv/ftos.ww | 549 ++++++++++++++ lib/strconv/ftos_data.ww | 104 +++ lib/strconv/strconv.ww | 91 +-- lib/strconv/test/ftostest.ww | 93 +++ selfhost/cmd/w6c/main.combined.ww | 1020 ++++++++++++++++++++------ selfhost/cmd/wwdump/main.combined.ww | 1020 ++++++++++++++++++++------ selfhost/test/smoke.combined.ww | 1020 ++++++++++++++++++++------ test/wcc/908_ftos_run.c | 55 ++ 11 files changed, 3219 insertions(+), 762 deletions(-) create mode 100644 lib/strconv/ftos.ww create mode 100644 lib/strconv/ftos_data.ww create mode 100644 lib/strconv/test/ftostest.ww create mode 100644 test/wcc/908_ftos_run.c diff --git a/Makefile b/Makefile index 14d75092..7f6f1c72 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,7 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgendecl.ww \ - lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \ + lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/wwdump_ww.d @@ -135,7 +135,7 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgendecl.ww \ - lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \ + lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6c_ww.d @@ -322,7 +322,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_intdiv_signed \ $(BIN)/test_strings_run \ $(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \ - $(BIN)/test_decimal_run $(BIN)/test_stof_run \ + $(BIN)/test_decimal_run $(BIN)/test_stof_run $(BIN)/test_ftos_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ @@ -1050,6 +1050,10 @@ $(BIN)/test_stof_run: test/wcc/909_stof_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_ftos_run: test/wcc/908_ftos_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_strings_run: test/wcc/966_strings_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index 970aa015..764158f1 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -526,11 +526,13 @@ fn formatraw(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = { // // Mods scope: width / alignment / pad / sign honored via // the same raw-then-pad machinery as i64. `prec` ignored — - // strconv.f64tos has no precision knob; graduate when a - // Ryū-shaped strconv lands and mods grows ffmt / fflags. - // `base` ignored (Hare does too — base is int-only). - // NaN/±Inf print as whatever strconv emits today (garbage); - // strconv detection blocked on f64↔u64 bit-reinterpret cgen. + // strconv.f64tos is shortest-G with no precision knob; grows a + // precision path when mods gains ffmt / fflags (the parametric + // ftosf, strconv task #64). `base` ignored (Hare does too — + // base is int-only). NaN/±Inf now render as "nan"/"infinity" + // (Ryū graduation, fold-5); the uniform sign-peel below would + // turn a SHOW_POS nan into "+nan" (a minor fmt-layer edge, not + // strconv's — strconv emits a bare "nan"). let view: str = strconv.f64tos(v); let neg_flag: bool = false; if (view.len > 0 && view.ptr[0] == 45u8) { // '-' diff --git a/lib/fmt/fmttest.ww b/lib/fmt/fmttest.ww index b1f007e4..d07a1a04 100644 --- a/lib/fmt/fmttest.ww +++ b/lib/fmt/fmttest.ww @@ -563,7 +563,8 @@ fn closedstream(s: *io.stream) void = { match (c) { case void => {}; case io.closed => fail(); }; }; -// Pins the strconv.f64tos "huge" fallback through the fmt dispatch. +// Pins a large magnitude through the fmt dispatch — post-Ryū-graduation +// (fold-5) this is scientific notation, not the old lossy "huge" fallback. // Catches any regression where mods accidentally pre-truncate the view. @test fn fprintf_f64_huge() void = { let mem: memio.state; @@ -571,10 +572,10 @@ fn closedstream(s: *io.stream) void = { memio.dynamic(&mem, &s); let r: (i32 | io.closed) = fmt.fprintf(&s, "{}", 9.5e18); match (r) { - case let n: i32 => { if (n != 4) { fail(); }; }; + case let n: i32 => { if (n != 6) { fail(); }; }; // "9.5e18" case io.closed => fail(); }; - if (!streq(memio.string(&mem), "huge")) { fail(); }; + if (!streq(memio.string(&mem), "9.5e18")) { fail(); }; let c: (void | io.closed) = io.close(&s); match (c) { case void => {}; case io.closed => fail(); }; }; diff --git a/lib/strconv/ftos.ww b/lib/strconv/ftos.ww new file mode 100644 index 00000000..91223556 --- /dev/null +++ b/lib/strconv/ftos.ww @@ -0,0 +1,549 @@ +// strconv — float→string via Ryū (shortest round-trippable decimal). +// Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + +// ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, +// https://doi.org/10.1145/3192366.3192369 — Hare translated it from the +// reference C (https://github.com/ulfjack/ryu); ww follows Hare. +// +// SCOPE — the f64tos shortest-representation subset (Hare's ffmt::G, +// prec=void, fflags::NONE). Two deferrals: +// - f32tos (ftos.ha:448) + its f32 Ryū sub-path (f32todecf32 + +// mulpow5inv/pow5_divpow2 + mulshift32 + the *32 helpers + the +// F32_POW5_*_BITCOUNT defs) → fold-5b (task #67), gated on the #143 +// f32-arg-push cgen fix: f32tos must call math.f32bits(n), passing +// an f32 arg, which spills MOVSD (cstage) vs MOVSS (wwstage) → +// 990-997 byte-id break. Not isolatable (f32bits IS the reinterpret), +// only fixable. f32tos IS a real Hare entry — it ships in #67. +// - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs +// io::handle/memio + a `(size|io::error)?` per appendrune (#158); +// for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback +// machinery is provably dead code — `ok` is always true → init_dec/ +// compute_round/round unreachable — which bootstrap-coverage rejects). +// The lib note blesses "a documented subset". This file ships ZERO float +// literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the +// wwdump TK_FLOAT embedding concern is moot. +// +// Decomposition divergences (the #163-166 tuple/struct-ABI cluster — +// ww's partial tuple support miscompiles the shapes Hare uses; the +// WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's +// own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for +// u128mul, and fold-4/stof.ww decomposed likewise): +// - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params +// `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple +// return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return +// reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO +// struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww). +// - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)` +// return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the +// math/floats.ww frexpf64 precedent). +// - dead `mulshift64` (tuple-param, never called) + dead +// `F32/F64_DECIMAL_DIGITS` dropped. +// +// Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha): +// - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local +// (stof.ww hex_to_bits precedent). +// - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block. +// comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound +// local + block. `assert()` → `os.assert(cond,msg)`. +// - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]` +// (#155 / #156, stof.ww eisel_lemire precedent). +// - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64 +// at each use (Hare promotes; ww is strict — int-machine-word note). +// - a `(N: uint)` cast embedded inside an array subscript `[ ]` is +// rejected by the ww parser → hoist to a named local before the +// index (decimal.ww "hoist size casts" note); see init_dec_mant_exp +// + encode_e_dec. + +package strconv; + +import math; +import os; + +// ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's +// "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition. +// (u128mul + the r128 struct live in stof.ww, fold-4's first consumer; +// reused in-package here.) +fn u128rshift(lo: u64, hi: u64, s: u32) u64 = { + os.assert(s <= 64u32, "strconv.u128rshift: s > 64"); + return (hi << (64u64 - (s: u64))) | (lo >> (s: u64)); +}; + +// ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value. +fn pow5fac(v: u64) u32 = { + let value: u64 = v; + let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64) + let n_div_5: u64 = 3689348814741910323u64; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u64, "strconv.pow5fac: value == 0"); + value *= m_inv_5; + if (value > n_div_5) { break; }; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:64. +fn ibool(b: bool) u8 = { + if (b) { return 1u8; }; + return 0u8; +}; + +// ref/hare/strconv/ftos_ryu.ha:66-67. +fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:69. +fn pow2multiple(v: u64, p: u32) bool = { + os.assert(v > 0u64, "strconv.pow2multiple: v == 0"); + os.assert(p < 64u32, "strconv.pow2multiple: p >= 64"); + return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64; +}; + +// ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple. +// Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return → +// this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound +// `v_minus` + block. +type ryuv = struct { vp: u64, vr: u64, vm: u64 }; + +fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = { + let mm: u64 = m << 1u64; + let r0: r128 = u128mul(mm, mul0); + let r1: r128 = u128mul(mm, mul1); + let lo: u64 = r0.lo; + let tmp: u64 = r0.hi; + let mid: u64 = tmp + r1.lo; + let hi: u64 = r1.hi + (ibool(mid < tmp): u64); + let lo2: u64 = lo + mul0; + let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64); + let hi2: u64 = hi + (ibool(mid2 < mid): u64); + let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32)); + let v_minus: u64 = 0u64; + if (mm_shift == 1u32) { + let lo3: u64 = lo - mul0; + let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64); + let hi3: u64 = hi - (ibool(mid3 > mid): u64); + v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32)); + } else { + let lo3: u64 = lo + lo; + let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64); + let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64); + let lo4: u64 = lo3 - mul0; + let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64); + let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64); + v_minus = u128rshift(mid4, hi4, ((j - 64): u32)); + }; + let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32)); + return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus }; +}; + +// ref/hare/strconv/ftos_ryu.ha:140. +fn log2pow5(e: u32) u32 = { + os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528"); + return (e * 1217359u32) >> 19u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:145-147. +fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; }; +fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); }; + +// ref/hare/strconv/ftos_ryu.ha:149. +fn log10pow2(e: u32) u32 = { + os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650"); + return (e * 78913u32) >> 18u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:154. +fn log10pow5(e: u32) u32 = { + os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620"); + return (e * 732923u32) >> 20u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the +// inverse power of five. 2-tuple kept (works); row-bind → double-index. +fn f64computeinvpow5(i: u32) (u64, u64) = { + let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = base2 - i; + if (off == 0u32) { + return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(base2) - pow5bits(i); + let res0: u64 = u128rshift(low0, sum, delta) + 1u64 + + (((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:246. +fn f64computepow5(i: u32) (u64, u64) = { + let base: u32 = i / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = i - base2; + if (off == 0u32) { + return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(i) - pow5bits(base2); + let res0: u64 = u128rshift(low0, sum, delta) + + (((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64: +// value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32 +// (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second +// field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww +// byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value +// always fits i32 (cast at the init_dec_mant_exp call site). +type decf64 = struct { mantissa: u64, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f64. +fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { + let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32; + let m2: u64 = 0u64; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u64) == 0u64; + let mv: u64 = 4u64 * m2; + let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32; + let vp: u64 = 0u64; + let vr: u64 = 0u64; + let vm: u64 = 0u64; + let e10: i32 = 0i32; + let vm_trailing_zeros: bool = false; + let vr_trailing_zeros: bool = false; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32); + e10 = q: i32; + let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + let pow5 = f64computeinvpow5(q); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 21u32) { + if ((mv - 5u64 * (mv / 5u64)) == 0u64) { + vr_trailing_zeros = pow5multiple(mv, q); + } else if (accept_bounds) { + vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q); + } else { + vp -= (ibool(pow5multiple(mv + 2u64, q)): u64); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32); + e10 = e2 + (q: i32); + let i: i32 = -e2 - (q: i32); + let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32); + let j: i32 = (q: i32) - k; + let pow5 = f64computepow5(i: u32); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 1u32) { + vr_trailing_zeros = true; + if (accept_bounds) { + vm_trailing_zeros = mm_shift == 1u32; + } else { + vp -= 1u64; + }; + } else if (q < 63u32) { + vr_trailing_zeros = pow2multiple(mv, q); + }; + }; + let removed: i32 = 0i32; + let last_removed_digit: u8 = 0u8; + let output: u64 = 0u64; + if (vm_trailing_zeros || vr_trailing_zeros) { + for (true) { + let vpby10: u64 = vp / 10u64; + let vmby10: u64 = vm / 10u64; + if (vpby10 <= vmby10) { break; }; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + if (vm_trailing_zeros) { + for (true) { + let vmby10: u64 = vm / 10u64; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + if (vmmod10 != 0u32) { break; }; + let vpby10: u64 = vp / 10u64; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + }; + if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u64); + } else { + let round_up: bool = false; + let vpby100: u64 = vp / 100u64; + let vmby100: u64 = vm / 100u64; + if (vpby100 > vmby100) { + let vrby100: u64 = vr / 100u64; + let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32); + round_up = vrmod100 >= 50u32; + vr = vrby100; vp = vpby100; vm = vmby100; + removed += 2i32; + }; + for (true) { + let vmby10: u64 = vm / 10u64; + let vpby10: u64 = vp / 10u64; + if (vpby10 <= vmby10) { break; }; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + round_up = vrmod10 >= 5u32; + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || round_up): u64); + }; + let exp: i32 = e10 + removed; + return decf64 { exponent = (exp: i64), mantissa = output }; +}; + +// ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void / +// fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase +// arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE +// for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always +// G) and are NOT ported — porting them stubbed would be untested dead +// code. The parametric ftosf/ffmt/fflags surface is deferred (task #64; +// needs a parametric consumer + io::handle + #158). ==== + +// ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17). +fn declen(n: u64) uint = { + os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17"); + if (n >= 100000000000000000u64) { return (18u32: uint); }; + if (n >= 10000000000000000u64) { return (17u32: uint); }; + if (n >= 1000000000000000u64) { return (16u32: uint); }; + if (n >= 100000000000000u64) { return (15u32: uint); }; + if (n >= 10000000000000u64) { return (14u32: uint); }; + if (n >= 1000000000000u64) { return (13u32: uint); }; + if (n >= 100000000000u64) { return (12u32: uint); }; + if (n >= 10000000000u64) { return (11u32: uint); }; + if (n >= 1000000000u64) { return (10u32: uint); }; + if (n >= 100000000u64) { return (9u32: uint); }; + if (n >= 10000000u64) { return (8u32: uint); }; + if (n >= 1000000u64) { return (7u32: uint); }; + if (n >= 100000u64) { return (6u32: uint); }; + if (n >= 10000u64) { return (5u32: uint); }; + if (n >= 1000u64) { return (4u32: uint); }; + if (n >= 100u64) { return (3u32: uint); }; + if (n >= 10u64) { return (2u32: uint); }; + return (1u32: uint); +}; + +// ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent) +// into the decimal `d`. `mantissa` is mutated in Hare → local `mant`. +fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = { + // Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded + // inside an array subscript (decimal.ww "hoist size casts" note). + let U_ZERO: uint = (0u32: uint); + let U_ONE: uint = (1u32: uint); + let mant: u64 = mantissa; + let dl: uint = declen(mant); + let i: uint = U_ZERO; + for (i < dl) { + d.digits[dl - i - U_ONE] = (mant % 10u64): u8; + mant /= 10u64; + i += U_ONE; + }; + d.nd = (dl: size); + d.dp = (dl: i32) + exponent; +}; + +// ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the +// *tos static-buffer convention replaces Hare's io::handle sink). +fn putstr(buf: []u8, out: i32, s: str) i32 = { + let o: i32 = out; + let k: i32 = 0i32; + for (k < s.len) { + buf[o] = s[k]; + o += 1i32; + k += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable +// logic only). Writes into `buf` at cursor `out`, returns the new cursor. +fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + let lo: i32 = 0i32; + if (d.dp <= 0i32) { lo = d.dp - 1i32; }; + let hi: i32 = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + if (hi > (d.nd: i32) && d.dp <= 0i32) { + hi = (d.nd: i32); + } else if (hi > d.dp && d.dp > 0i32) { + hi = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + }; + let i: i32 = lo; + for (i < hi) { + if (i == d.dp) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + if (0i32 <= i && i < (d.nd: i32)) { + buf[o] = (d.digits[i] + 48u8): u8; + } else { + buf[o] = 48u8; // '0' + }; + o += 1i32; + i += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable +// logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad. +fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0"); + buf[o] = (d.digits[0] + 48u8): u8; + o += 1i32; + if ((d.nd: i32) > 1i32) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + let i: size = (1u64: size); + for (i < d.nd) { + buf[o] = (d.digits[i] + 48u8): u8; + o += 1i32; + i += (1u64: size); + }; + buf[o] = 101u8; // 'e' + o += 1i32; + let e: i32 = d.dp - 1i32; + if (e < 0i32) { + e = -e; + buf[o] = 45u8; // '-' + o += 1i32; + }; + // Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`). + let U_ONE: uint = (1u32: uint); + let U_TWO: uint = (2u32: uint); + let U_THREE: uint = (3u32: uint); + let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits + let l: uint = declen(e: u64); + let k: uint = (0u32: uint); + for (k < l) { + ebuf[U_TWO - k] = (e % 10i32): u8; + e /= 10i32; + k += U_ONE; + }; + let m: uint = U_THREE - l; + for (m < U_THREE) { + buf[o] = (ebuf[m] + 48u8): u8; + o += 1i32; + m += U_ONE; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a +// view into a static buffer overwritten on the next call (the *tos +// convention; see strings.dup to retain). Equivalent to Hare's ftosf +// with format G + precision void. The fftosf G/void/NONE path is inlined +// (the parametric surface is deferred — task #64). +// +// Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' + +// exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer +// emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw +// split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused. +let f64tos_buf: [32]u8; + +export fn f64tos(n: f64) str = { + let bits: u64 = math.f64bits(n); + let mantissa: u64 = bits & math.F64_MANTISSA_MASK; + let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32; + let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64; + let special: bool = exponent == (math.F64_EXPONENT_MASK: u32); + + let o: i32 = 0i32; + let r: str; + r.ptr = &f64tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u64) { + o = putstr(f64tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f64tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f64tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u64) { + f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a + // scalar-field read of a LOCAL struct (`d.nd`) to a bogus global + // symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers + // correctly in both stages (the stof.ww/decimal.ww *decimal precedent) + // — #170. The init/trim/encode calls already took &d; route via pd. + let pd: *decimal = &d; + let dd: decf64 = f64todecf64(mantissa, exponent); + init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32)); + // ok = !ffpoint(NONE) || ... is always true → no multiprecision + // fallback (ftos.ha:365). f == G → trim (ftos.ha:386). + trim(pd); + if (pd.nd == (0u64: size)) { + f64tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f64tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f64tos_buf[0:32], o); + }; + r.len = o; + return r; +}; diff --git a/lib/strconv/ftos_data.ww b/lib/strconv/ftos_data.ww new file mode 100644 index 00000000..0b9a90bb --- /dev/null +++ b/lib/strconv/ftos_data.ww @@ -0,0 +1,104 @@ +// strconv — Ryū float→string lookup tables + bit-count constants. +// Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data +// fold (strconv #106 fold-5): no logic, consumed by ftos.ww's +// f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores). +// +// File-organisation divergence: Hare keeps these tables INLINE in +// ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring +// the stof.ww / stof_data.ww split) — same `package strconv`, so the +// tables stay visible to ftos.ww with no qualification. +// +// Spelling divergences (same as stof_data.ww, candidate #130 + rule-12): +// - Hare `const TBL = [...]` → ww module-level `let` (ww has no +// module-`const` keyword; the values are never written). +// - every literal carries its element-width suffix (`u64`/`u32`): +// cstage rejects bare integer literals in `[N]uXX` init while +// wwstage accepts them; the suffixed form is the only shape both +// stages agree on. +// - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened); +// the 2D module-level static-init + double-index read landed in +// #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64. + +package strconv; + +// ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split +// power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32 +// at each use site (Hare promotes a u8 def inside mixed-width arithmetic; +// ww is strict — explicit cast, project_int_machine_word_derived_limits). +// The F32_POW5_*_BITCOUNT siblings (ftos_ryu.ha:162-163) land in fold-5b +// (task #67) with their only consumer, f32todecf32 — omitted here to keep +// this fold dead-code-free. +def F64_POW5_INV_BITCOUNT: u8 = 125u8; +def F64_POW5_BITCOUNT: u8 = 125u8; + +// ref/hare/strconv/ftos_ryu.ha:165-181. +let F64_POW5_INV_SPLIT2: [15][2]u64 = [ + [1u64, 2305843009213693952u64], + [5955668970331000884u64, 1784059615882449851u64], + [8982663654677661702u64, 1380349269358112757u64], + [7286864317269821294u64, 2135987035920910082u64], + [7005857020398200553u64, 1652639921975621497u64], + [17965325103354776697u64, 1278668206209430417u64], + [8928596168509315048u64, 1978643211784836272u64], + [10075671573058298858u64, 1530901034580419511u64], + [597001226353042382u64, 1184477304306571148u64], + [1527430471115325346u64, 1832889850782397517u64], + [12533209867169019542u64, 1418129833677084982u64], + [5577825024675947042u64, 2194449627517475473u64], + [11006974540203867551u64, 1697873161311732311u64], + [10313493231639821582u64, 1313665730009899186u64], + [12701016819766672773u64, 2032799256770390445u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:183-188. +let POW5_INV_OFFSETS: [19]u32 = [ + 0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32, + 0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32, + 0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32, + 0x00000000u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:190-204. +let F64_POW5_SPLIT2: [13][2]u64 = [ + [0u64, 1152921504606846976u64], + [0u64, 1490116119384765625u64], + [1032610780636961552u64, 1925929944387235853u64], + [7910200175544436838u64, 1244603055572228341u64], + [16941905809032713930u64, 1608611746708759036u64], + [13024893955298202172u64, 2079081953128979843u64], + [6607496772837067824u64, 1343575221513417750u64], + [17332926989895652603u64, 1736530273035216783u64], + [13037379183483547984u64, 2244412773384604712u64], + [1605989338741628675u64, 1450417759929778918u64], + [9630225068416591280u64, 1874621017369538693u64], + [665883850346957067u64, 1211445438634777304u64], + [14931890668723713708u64, 1565756531257009982u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:206-211. +let POW5_OFFSETS: [21]u32 = [ + 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32, + 0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32, + 0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32, + 0x55559155u32, 0x51405555u32, 0x00000105u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in +// f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those +// arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage +// rejects a def-named array length — "array length must be an integer +// literal"; wwstage accepts it but emits an empty DATAW — divergence +// #167, so the literal `26` is the only shape both stages agree on; +// matches decimal.ww's `[800]u8` array-dimension-literal precedent). +def POW5_TABLE_SZ: u8 = 26u8; + +// ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is +// commented out in Hare too — it lives implicitly in the SPLIT2 tables). +let POW5_TABLE: [26]u64 = [ + 1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64, + 390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64, + 1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64, + 762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64, + 476837158203125u64, 2384185791015625u64, 11920928955078125u64, + 59604644775390625u64, 298023223876953125u64, +]; diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww index 6d55f5ba..9c8e0a4a 100644 --- a/lib/strconv/strconv.ww +++ b/lib/strconv/strconv.ww @@ -261,91 +261,12 @@ export fn stou8(s: str, b: base) (u8 | invalid | overflow) = { return 0: invalid; }; -// f64tos — convert v to a decimal string. Returns owned str; release -// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is -// fixed-point only, max 6 fractional digits, no NaN/Inf support — -// see graduate-to-Ryū note below). -// -// Surface: -// -// - finite values only. NaN/±Inf detection needs an f64→u64 bit -// reinterpret cast that the cgen doesn't expose yet. -// - fixed-point only, up to 6 fractional digits. Trailing zeros -// after the decimal point are trimmed. Trailing '.' is dropped. -// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) -// fall back to the literal token "huge". Hare would print these -// in scientific notation via Ryū; we will graduate when the -// compiler grows the bit-reinterpret cast. -// -// Round-trip is therefore lossy past 6 fractional digits. -// -// No float literals in the body — 990's wwdump diff requires this -// file's TK_FLOAT count to match between C and ww front-ends, and -// the ww-side wwdump currently skips TK_FLOAT.fval while the C side -// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: -// build f64 constants via int-to-f64 casts. -let f64tos_buf: [64]u8; - -export fn f64tos(v: f64) str = { - let out: i32 = 0; - let f: f64 = v; - let zero: f64 = 0: f64; - if (f < zero) { - f64tos_buf[out] = 45u8; // '-' - out += 1; - f = -f; - }; - // 9e18 is comfortably under I64_MAX (9.22e18). Past this the - // `f: i64` cast wraps and the integer part comes back as garbage. - let cap: f64 = 9000000000000000000i64: f64; - if (f >= cap) { - let s: str = "huge"; - let k: i32 = 0; - for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; - }; - let ip: i64 = f: i64; - // Fractional part scaled to 6 decimal digits, with round-to- - // nearest via +0.5. (f64 compound assigns mis-lower in cgen — - // use the explicit form, as the rest of lib does.) - let frac: f64 = f - (ip: f64); - let scale: f64 = 1000000: f64; - frac = frac * scale; - let half: f64 = (1: f64) / (2: f64); - let fp: i64 = (frac + half): i64; - // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer - // part needs to advance. - if (fp >= 1000000) { - ip += 1; - fp = 0; - }; - let intstr: str = i64tos(ip, base.DEC); - let k: i32 = 0; - for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; }; - if (fp != 0) { - f64tos_buf[out] = 46u8; // '.' - out += 1; - let fracstr: str = u64tos(fp: u64, base.DEC); - // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → - // fp=50000, fracstr="50000", pad one '0' before). - let z: i32 = 6 - fracstr.len; - for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; }; - k = 0; - for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; }; - // Trim trailing zeros in the fractional part. - for (out > 0) { - if (f64tos_buf[out - 1] != 48u8) { break; }; - out -= 1; - }; - }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; -}; +// f64tos — graduated to the Ryū shortest-round-trippable implementation +// in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version +// (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted +// here per the lib-note graduation rule ("replace in one go, don't keep +// both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b +// (task #67, gated on the #143 f32-arg-push cgen fix). // strerror — convert an strconv error to a user-readable string. // Returns owned str; release via os.free. Mirrors Hare's diff --git a/lib/strconv/test/ftostest.ww b/lib/strconv/test/ftostest.ww new file mode 100644 index 00000000..0b96f725 --- /dev/null +++ b/lib/strconv/test/ftostest.ww @@ -0,0 +1,93 @@ +// ftostest — exercises lib/strconv/ftos.ww (Hare ftos.ha / ftos_ryu.ha +// Ryū port). Run with `out/bin/ww run lib/strconv/test/ftostest.ww`. +// Same signalled-then-os.exit(signalled+10) pattern as stoftest/ +// decimaltest: a non-zero exit names the failing @test (1..N → 11..N+10). +// +// Ports ref/hare/strconv/+test/ftos_test.ha's ffmt::G / prec=void / +// fflags::NONE rows (the f64tos cases) verbatim — these are exactly +// strconv.f64tos's output. encode_f_dec is covered by 13.37/1100/0.011/… +// and encode_e_dec by 1e4/1.1e4/1e-3/1.1e-3/1e-4. nan/±infinity via +// math.f64frombits of the IEEE bit patterns (ww math has no f64 NAN/INF +// const — see math/floats.ww). +// +// f32tos is NOT exercised here: it is deferred to fold-5b (task #67), +// gated on the #143 f32-arg-push cs≠ww cgen fix. +// +// Lives in lib/strconv/test/ so `import strconv` resolves to the +// DIRECTORY (full package), not the strconv.ww FILE — same rationale as +// stoftest/decimaltest. + +package strconv; + +import strconv; +import os; +import math; + +let signalled: i32 = 0; +fn fail() void = { os.exit(signalled + 10); }; + +fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let k: i32 = 0i32; + for (k < a.len) { + if (a[k] != b[k]) { return false; }; + k += 1i32; + }; + return true; +}; + +fn chk(n: f64, want: str) bool = { + return streq(f64tos(n), want); +}; + +// ftos_test.ha:57-64 (G/void/NONE-equivalent) — fixed-point renders. +@test fn f64tos_fixed() void = { + if (!chk(13.37, "13.37")) { fail(); }; + if (!chk(12345.0, "12345")) { fail(); }; + if (!chk(1100.0, "1100")) { fail(); }; + if (!chk(100.0, "100")) { fail(); }; + if (!chk(10.0, "10")) { fail(); }; + if (!chk(1.0, "1")) { fail(); }; + if (!chk(0.3, "0.3")) { fail(); }; + if (!chk(0.1, "0.1")) { fail(); }; + if (!chk(0.01, "0.01")) { fail(); }; + if (!chk(0.011, "0.011")) { fail(); }; + if (!chk(1.414, "1.414")) { fail(); }; + if (!chk(-6345.972, "-6345.972")) { fail(); }; +}; + +// ftos_test.ha:78-90 — scientific renders (the shortest-G E-dispatch). +@test fn f64tos_scientific() void = { + if (!chk(10000.0, "1e4")) { fail(); }; + if (!chk(11000.0, "1.1e4")) { fail(); }; + if (!chk(1000.0, "1e3")) { fail(); }; + if (!chk(0.001, "1e-3")) { fail(); }; + if (!chk(0.0011, "1.1e-3")) { fail(); }; + if (!chk(0.0001, "1e-4")) { fail(); }; + // dp=-2 < -1 → G dispatches to E (ftos_test.ha:62 is the ffmt::F row; + // shortest-G of 0.0031415 is scientific). + if (!chk(0.0031415, "3.1415e-3")) { fail(); }; +}; + +// ftos_test.ha:12/16/27 — zero, ±infinity, nan. +@test fn f64tos_special() void = { + if (!chk(0.0, "0")) { fail(); }; + // sign bit via (1u64 << 63), not the 0x8000000000000000 literal: + // wwstage mis-emits the I64_MIN-magnitude literal (#144 family). + let negzero: f64 = math.f64frombits(1u64 << 63u64); + if (!chk(negzero, "-0")) { fail(); }; + let inf: f64 = math.f64frombits(0x7FF0000000000000u64); + if (!chk(inf, "infinity")) { fail(); }; + let ninf: f64 = math.f64frombits(0xFFF0000000000000u64); + if (!chk(ninf, "-infinity")) { fail(); }; + let nan: f64 = math.f64frombits(0x7FF8000000000000u64); + if (!chk(nan, "nan")) { fail(); }; +}; + +export fn main() i32 = { + signalled = 1; f64tos_fixed(); + signalled = 2; f64tos_scientific(); + signalled = 3; f64tos_special(); + os.exit(0); + return 0; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 289ea873..e1f20a5a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -3026,143 +3026,6 @@ fn decimal_round(d: *decimal) u64 = { return n; }; -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -package ascii; - -export fn isdigit(c: rune) bool = { - if (c < 48) { return false; }; - if (c > 57) { return false; }; - return true; -}; - -export fn isupper(c: rune) bool = { - if (c < 65) { return false; }; - if (c > 90) { return false; }; - return true; -}; - -export fn islower(c: rune) bool = { - if (c < 97) { return false; }; - if (c > 122) { return false; }; - return true; -}; - -export fn isalpha(c: rune) bool = { - if (isupper(c)) { return true; }; - return islower(c); -}; - -export fn isalnum(c: rune) bool = { - if (isalpha(c)) { return true; }; - return isdigit(c); -}; - -// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. -export fn isspace(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - if (c == 10) { return true; }; // '\n' - if (c == 11) { return true; }; // '\v' - if (c == 12) { return true; }; // '\f' - if (c == 13) { return true; }; // '\r' - return false; -}; - -export fn isxdigit(c: rune) bool = { - if (isdigit(c)) { return true; }; - if (c >= 65) { - if (c <= 70) { return true; }; // 'A'..'F' - }; - if (c >= 97) { - if (c <= 102) { return true; }; // 'a'..'f' - }; - return false; -}; - -// valid — `c` is in the 0..127 ASCII range. -export fn valid(c: rune) bool = { - if (c < 0) { return false; }; - if (c > 127) { return false; }; - return true; -}; - -// validstr — every byte in `s` is ASCII (0..127). -export fn validstr(s: str) bool = { - let i: i32 = 0; - for (i < s.len) { - // High-bit test rather than `> 127u8`; both cgens lower - // the bitwise form identically. The `> u8` form picks - // JA vs JG depending on signed/unsigned dispatch. - if ((s[i] & 128u8) != 0u8) { return false; }; - i += 1; - }; - return true; -}; - -// iscntrl — control chars: 0..31 and 127. -export fn iscntrl(c: rune) bool = { - if (c >= 0) { if (c <= 31) { return true; }; }; - if (c == 127) { return true; }; - return false; -}; - -// isblank — space and tab. -export fn isblank(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - return false; -}; - -// isprint — printable: space through '~'. -export fn isprint(c: rune) bool = { - if (c < 32) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// isgraph — printable, non-space. -export fn isgraph(c: rune) bool = { - if (c < 33) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// ispunct — printable, non-alnum, non-space. -export fn ispunct(c: rune) bool = { - if (!isgraph(c)) { return false; }; - if (isalnum(c)) { return false; }; - return true; -}; - -// tolower / toupper — fold ASCII case. Non-letters pass through. -export fn tolower(c: rune) rune = { - if (isupper(c)) { return c + 32; }; - return c; -}; - -export fn toupper(c: rune) rune = { - if (islower(c)) { return c - 32; }; - return c; -}; - -// strcasecmp — three-way ASCII case-insensitive compare. -export fn strcasecmp(a: str, b: str) i32 = { - let n: i32 = a.len; - if (b.len < n) { n = b.len; }; - let i: i32 = 0; - for (i < n) { - let ca: rune = tolower(a[i]: rune); - let cb: rune = tolower(b[i]: rune); - if (ca != cb) { return (ca - cb): i32; }; - i += 1; - }; - return a.len - b.len; -}; - // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: @@ -3453,6 +3316,798 @@ export fn absi64(n: i64) u64 = { return n: u64; }; +// strconv — float→string via Ryū (shortest round-trippable decimal). +// Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + +// ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, +// https://doi.org/10.1145/3192366.3192369 — Hare translated it from the +// reference C (https://github.com/ulfjack/ryu); ww follows Hare. +// +// SCOPE — the f64tos shortest-representation subset (Hare's ffmt::G, +// prec=void, fflags::NONE). Two deferrals: +// - f32tos (ftos.ha:448) + its f32 Ryū sub-path (f32todecf32 + +// mulpow5inv/pow5_divpow2 + mulshift32 + the *32 helpers + the +// F32_POW5_*_BITCOUNT defs) → fold-5b (task #67), gated on the #143 +// f32-arg-push cgen fix: f32tos must call math.f32bits(n), passing +// an f32 arg, which spills MOVSD (cstage) vs MOVSS (wwstage) → +// 990-997 byte-id break. Not isolatable (f32bits IS the reinterpret), +// only fixable. f32tos IS a real Hare entry — it ships in #67. +// - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs +// io::handle/memio + a `(size|io::error)?` per appendrune (#158); +// for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback +// machinery is provably dead code — `ok` is always true → init_dec/ +// compute_round/round unreachable — which bootstrap-coverage rejects). +// The lib note blesses "a documented subset". This file ships ZERO float +// literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the +// wwdump TK_FLOAT embedding concern is moot. +// +// Decomposition divergences (the #163-166 tuple/struct-ABI cluster — +// ww's partial tuple support miscompiles the shapes Hare uses; the +// WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's +// own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for +// u128mul, and fold-4/stof.ww decomposed likewise): +// - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params +// `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple +// return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return +// reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO +// struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww). +// - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)` +// return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the +// math/floats.ww frexpf64 precedent). +// - dead `mulshift64` (tuple-param, never called) + dead +// `F32/F64_DECIMAL_DIGITS` dropped. +// +// Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha): +// - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local +// (stof.ww hex_to_bits precedent). +// - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block. +// comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound +// local + block. `assert()` → `os.assert(cond,msg)`. +// - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]` +// (#155 / #156, stof.ww eisel_lemire precedent). +// - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64 +// at each use (Hare promotes; ww is strict — int-machine-word note). +// - a `(N: uint)` cast embedded inside an array subscript `[ ]` is +// rejected by the ww parser → hoist to a named local before the +// index (decimal.ww "hoist size casts" note); see init_dec_mant_exp +// + encode_e_dec. + +package strconv; + +import math; +import os; + +// ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's +// "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition. +// (u128mul + the r128 struct live in stof.ww, fold-4's first consumer; +// reused in-package here.) +fn u128rshift(lo: u64, hi: u64, s: u32) u64 = { + os.assert(s <= 64u32, "strconv.u128rshift: s > 64"); + return (hi << (64u64 - (s: u64))) | (lo >> (s: u64)); +}; + +// ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value. +fn pow5fac(v: u64) u32 = { + let value: u64 = v; + let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64) + let n_div_5: u64 = 3689348814741910323u64; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u64, "strconv.pow5fac: value == 0"); + value *= m_inv_5; + if (value > n_div_5) { break; }; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:64. +fn ibool(b: bool) u8 = { + if (b) { return 1u8; }; + return 0u8; +}; + +// ref/hare/strconv/ftos_ryu.ha:66-67. +fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:69. +fn pow2multiple(v: u64, p: u32) bool = { + os.assert(v > 0u64, "strconv.pow2multiple: v == 0"); + os.assert(p < 64u32, "strconv.pow2multiple: p >= 64"); + return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64; +}; + +// ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple. +// Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return → +// this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound +// `v_minus` + block. +type ryuv = struct { vp: u64, vr: u64, vm: u64 }; + +fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = { + let mm: u64 = m << 1u64; + let r0: r128 = u128mul(mm, mul0); + let r1: r128 = u128mul(mm, mul1); + let lo: u64 = r0.lo; + let tmp: u64 = r0.hi; + let mid: u64 = tmp + r1.lo; + let hi: u64 = r1.hi + (ibool(mid < tmp): u64); + let lo2: u64 = lo + mul0; + let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64); + let hi2: u64 = hi + (ibool(mid2 < mid): u64); + let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32)); + let v_minus: u64 = 0u64; + if (mm_shift == 1u32) { + let lo3: u64 = lo - mul0; + let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64); + let hi3: u64 = hi - (ibool(mid3 > mid): u64); + v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32)); + } else { + let lo3: u64 = lo + lo; + let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64); + let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64); + let lo4: u64 = lo3 - mul0; + let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64); + let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64); + v_minus = u128rshift(mid4, hi4, ((j - 64): u32)); + }; + let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32)); + return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus }; +}; + +// ref/hare/strconv/ftos_ryu.ha:140. +fn log2pow5(e: u32) u32 = { + os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528"); + return (e * 1217359u32) >> 19u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:145-147. +fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; }; +fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); }; + +// ref/hare/strconv/ftos_ryu.ha:149. +fn log10pow2(e: u32) u32 = { + os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650"); + return (e * 78913u32) >> 18u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:154. +fn log10pow5(e: u32) u32 = { + os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620"); + return (e * 732923u32) >> 20u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the +// inverse power of five. 2-tuple kept (works); row-bind → double-index. +fn f64computeinvpow5(i: u32) (u64, u64) = { + let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = base2 - i; + if (off == 0u32) { + return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(base2) - pow5bits(i); + let res0: u64 = u128rshift(low0, sum, delta) + 1u64 + + (((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:246. +fn f64computepow5(i: u32) (u64, u64) = { + let base: u32 = i / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = i - base2; + if (off == 0u32) { + return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(i) - pow5bits(base2); + let res0: u64 = u128rshift(low0, sum, delta) + + (((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64: +// value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32 +// (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second +// field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww +// byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value +// always fits i32 (cast at the init_dec_mant_exp call site). +type decf64 = struct { mantissa: u64, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f64. +fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { + let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32; + let m2: u64 = 0u64; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u64) == 0u64; + let mv: u64 = 4u64 * m2; + let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32; + let vp: u64 = 0u64; + let vr: u64 = 0u64; + let vm: u64 = 0u64; + let e10: i32 = 0i32; + let vm_trailing_zeros: bool = false; + let vr_trailing_zeros: bool = false; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32); + e10 = q: i32; + let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + let pow5 = f64computeinvpow5(q); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 21u32) { + if ((mv - 5u64 * (mv / 5u64)) == 0u64) { + vr_trailing_zeros = pow5multiple(mv, q); + } else if (accept_bounds) { + vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q); + } else { + vp -= (ibool(pow5multiple(mv + 2u64, q)): u64); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32); + e10 = e2 + (q: i32); + let i: i32 = -e2 - (q: i32); + let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32); + let j: i32 = (q: i32) - k; + let pow5 = f64computepow5(i: u32); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 1u32) { + vr_trailing_zeros = true; + if (accept_bounds) { + vm_trailing_zeros = mm_shift == 1u32; + } else { + vp -= 1u64; + }; + } else if (q < 63u32) { + vr_trailing_zeros = pow2multiple(mv, q); + }; + }; + let removed: i32 = 0i32; + let last_removed_digit: u8 = 0u8; + let output: u64 = 0u64; + if (vm_trailing_zeros || vr_trailing_zeros) { + for (true) { + let vpby10: u64 = vp / 10u64; + let vmby10: u64 = vm / 10u64; + if (vpby10 <= vmby10) { break; }; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + if (vm_trailing_zeros) { + for (true) { + let vmby10: u64 = vm / 10u64; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + if (vmmod10 != 0u32) { break; }; + let vpby10: u64 = vp / 10u64; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + }; + if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u64); + } else { + let round_up: bool = false; + let vpby100: u64 = vp / 100u64; + let vmby100: u64 = vm / 100u64; + if (vpby100 > vmby100) { + let vrby100: u64 = vr / 100u64; + let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32); + round_up = vrmod100 >= 50u32; + vr = vrby100; vp = vpby100; vm = vmby100; + removed += 2i32; + }; + for (true) { + let vmby10: u64 = vm / 10u64; + let vpby10: u64 = vp / 10u64; + if (vpby10 <= vmby10) { break; }; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + round_up = vrmod10 >= 5u32; + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || round_up): u64); + }; + let exp: i32 = e10 + removed; + return decf64 { exponent = (exp: i64), mantissa = output }; +}; + +// ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void / +// fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase +// arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE +// for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always +// G) and are NOT ported — porting them stubbed would be untested dead +// code. The parametric ftosf/ffmt/fflags surface is deferred (task #64; +// needs a parametric consumer + io::handle + #158). ==== + +// ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17). +fn declen(n: u64) uint = { + os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17"); + if (n >= 100000000000000000u64) { return (18u32: uint); }; + if (n >= 10000000000000000u64) { return (17u32: uint); }; + if (n >= 1000000000000000u64) { return (16u32: uint); }; + if (n >= 100000000000000u64) { return (15u32: uint); }; + if (n >= 10000000000000u64) { return (14u32: uint); }; + if (n >= 1000000000000u64) { return (13u32: uint); }; + if (n >= 100000000000u64) { return (12u32: uint); }; + if (n >= 10000000000u64) { return (11u32: uint); }; + if (n >= 1000000000u64) { return (10u32: uint); }; + if (n >= 100000000u64) { return (9u32: uint); }; + if (n >= 10000000u64) { return (8u32: uint); }; + if (n >= 1000000u64) { return (7u32: uint); }; + if (n >= 100000u64) { return (6u32: uint); }; + if (n >= 10000u64) { return (5u32: uint); }; + if (n >= 1000u64) { return (4u32: uint); }; + if (n >= 100u64) { return (3u32: uint); }; + if (n >= 10u64) { return (2u32: uint); }; + return (1u32: uint); +}; + +// ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent) +// into the decimal `d`. `mantissa` is mutated in Hare → local `mant`. +fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = { + // Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded + // inside an array subscript (decimal.ww "hoist size casts" note). + let U_ZERO: uint = (0u32: uint); + let U_ONE: uint = (1u32: uint); + let mant: u64 = mantissa; + let dl: uint = declen(mant); + let i: uint = U_ZERO; + for (i < dl) { + d.digits[dl - i - U_ONE] = (mant % 10u64): u8; + mant /= 10u64; + i += U_ONE; + }; + d.nd = (dl: size); + d.dp = (dl: i32) + exponent; +}; + +// ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the +// *tos static-buffer convention replaces Hare's io::handle sink). +fn putstr(buf: []u8, out: i32, s: str) i32 = { + let o: i32 = out; + let k: i32 = 0i32; + for (k < s.len) { + buf[o] = s[k]; + o += 1i32; + k += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable +// logic only). Writes into `buf` at cursor `out`, returns the new cursor. +fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + let lo: i32 = 0i32; + if (d.dp <= 0i32) { lo = d.dp - 1i32; }; + let hi: i32 = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + if (hi > (d.nd: i32) && d.dp <= 0i32) { + hi = (d.nd: i32); + } else if (hi > d.dp && d.dp > 0i32) { + hi = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + }; + let i: i32 = lo; + for (i < hi) { + if (i == d.dp) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + if (0i32 <= i && i < (d.nd: i32)) { + buf[o] = (d.digits[i] + 48u8): u8; + } else { + buf[o] = 48u8; // '0' + }; + o += 1i32; + i += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable +// logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad. +fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0"); + buf[o] = (d.digits[0] + 48u8): u8; + o += 1i32; + if ((d.nd: i32) > 1i32) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + let i: size = (1u64: size); + for (i < d.nd) { + buf[o] = (d.digits[i] + 48u8): u8; + o += 1i32; + i += (1u64: size); + }; + buf[o] = 101u8; // 'e' + o += 1i32; + let e: i32 = d.dp - 1i32; + if (e < 0i32) { + e = -e; + buf[o] = 45u8; // '-' + o += 1i32; + }; + // Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`). + let U_ONE: uint = (1u32: uint); + let U_TWO: uint = (2u32: uint); + let U_THREE: uint = (3u32: uint); + let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits + let l: uint = declen(e: u64); + let k: uint = (0u32: uint); + for (k < l) { + ebuf[U_TWO - k] = (e % 10i32): u8; + e /= 10i32; + k += U_ONE; + }; + let m: uint = U_THREE - l; + for (m < U_THREE) { + buf[o] = (ebuf[m] + 48u8): u8; + o += 1i32; + m += U_ONE; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a +// view into a static buffer overwritten on the next call (the *tos +// convention; see strings.dup to retain). Equivalent to Hare's ftosf +// with format G + precision void. The fftosf G/void/NONE path is inlined +// (the parametric surface is deferred — task #64). +// +// Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' + +// exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer +// emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw +// split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused. +let f64tos_buf: [32]u8; + +export fn f64tos(n: f64) str = { + let bits: u64 = math.f64bits(n); + let mantissa: u64 = bits & math.F64_MANTISSA_MASK; + let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32; + let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64; + let special: bool = exponent == (math.F64_EXPONENT_MASK: u32); + + let o: i32 = 0i32; + let r: str; + r.ptr = &f64tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u64) { + o = putstr(f64tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f64tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f64tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u64) { + f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a + // scalar-field read of a LOCAL struct (`d.nd`) to a bogus global + // symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers + // correctly in both stages (the stof.ww/decimal.ww *decimal precedent) + // — #170. The init/trim/encode calls already took &d; route via pd. + let pd: *decimal = &d; + let dd: decf64 = f64todecf64(mantissa, exponent); + init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32)); + // ok = !ffpoint(NONE) || ... is always true → no multiprecision + // fallback (ftos.ha:365). f == G → trim (ftos.ha:386). + trim(pd); + if (pd.nd == (0u64: size)) { + f64tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f64tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f64tos_buf[0:32], o); + }; + r.len = o; + return r; +}; + +// strconv — Ryū float→string lookup tables + bit-count constants. +// Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data +// fold (strconv #106 fold-5): no logic, consumed by ftos.ww's +// f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores). +// +// File-organisation divergence: Hare keeps these tables INLINE in +// ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring +// the stof.ww / stof_data.ww split) — same `package strconv`, so the +// tables stay visible to ftos.ww with no qualification. +// +// Spelling divergences (same as stof_data.ww, candidate #130 + rule-12): +// - Hare `const TBL = [...]` → ww module-level `let` (ww has no +// module-`const` keyword; the values are never written). +// - every literal carries its element-width suffix (`u64`/`u32`): +// cstage rejects bare integer literals in `[N]uXX` init while +// wwstage accepts them; the suffixed form is the only shape both +// stages agree on. +// - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened); +// the 2D module-level static-init + double-index read landed in +// #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64. + +package strconv; + +// ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split +// power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32 +// at each use site (Hare promotes a u8 def inside mixed-width arithmetic; +// ww is strict — explicit cast, project_int_machine_word_derived_limits). +// The F32_POW5_*_BITCOUNT siblings (ftos_ryu.ha:162-163) land in fold-5b +// (task #67) with their only consumer, f32todecf32 — omitted here to keep +// this fold dead-code-free. +def F64_POW5_INV_BITCOUNT: u8 = 125u8; +def F64_POW5_BITCOUNT: u8 = 125u8; + +// ref/hare/strconv/ftos_ryu.ha:165-181. +let F64_POW5_INV_SPLIT2: [15][2]u64 = [ + [1u64, 2305843009213693952u64], + [5955668970331000884u64, 1784059615882449851u64], + [8982663654677661702u64, 1380349269358112757u64], + [7286864317269821294u64, 2135987035920910082u64], + [7005857020398200553u64, 1652639921975621497u64], + [17965325103354776697u64, 1278668206209430417u64], + [8928596168509315048u64, 1978643211784836272u64], + [10075671573058298858u64, 1530901034580419511u64], + [597001226353042382u64, 1184477304306571148u64], + [1527430471115325346u64, 1832889850782397517u64], + [12533209867169019542u64, 1418129833677084982u64], + [5577825024675947042u64, 2194449627517475473u64], + [11006974540203867551u64, 1697873161311732311u64], + [10313493231639821582u64, 1313665730009899186u64], + [12701016819766672773u64, 2032799256770390445u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:183-188. +let POW5_INV_OFFSETS: [19]u32 = [ + 0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32, + 0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32, + 0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32, + 0x00000000u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:190-204. +let F64_POW5_SPLIT2: [13][2]u64 = [ + [0u64, 1152921504606846976u64], + [0u64, 1490116119384765625u64], + [1032610780636961552u64, 1925929944387235853u64], + [7910200175544436838u64, 1244603055572228341u64], + [16941905809032713930u64, 1608611746708759036u64], + [13024893955298202172u64, 2079081953128979843u64], + [6607496772837067824u64, 1343575221513417750u64], + [17332926989895652603u64, 1736530273035216783u64], + [13037379183483547984u64, 2244412773384604712u64], + [1605989338741628675u64, 1450417759929778918u64], + [9630225068416591280u64, 1874621017369538693u64], + [665883850346957067u64, 1211445438634777304u64], + [14931890668723713708u64, 1565756531257009982u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:206-211. +let POW5_OFFSETS: [21]u32 = [ + 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32, + 0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32, + 0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32, + 0x55559155u32, 0x51405555u32, 0x00000105u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in +// f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those +// arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage +// rejects a def-named array length — "array length must be an integer +// literal"; wwstage accepts it but emits an empty DATAW — divergence +// #167, so the literal `26` is the only shape both stages agree on; +// matches decimal.ww's `[800]u8` array-dimension-literal precedent). +def POW5_TABLE_SZ: u8 = 26u8; + +// ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is +// commented out in Hare too — it lives implicitly in the SPLIT2 tables). +let POW5_TABLE: [26]u64 = [ + 1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64, + 390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64, + 1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64, + 762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64, + 476837158203125u64, 2384185791015625u64, 11920928955078125u64, + 59604644775390625u64, 298023223876953125u64, +]; + +// ascii — rune-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family (rune-taking signature). Runes +// outside 0..127 always answer `false`. The lexer hot path uses these +// inline; they are expected to inline to a couple of compares. + +package ascii; + +export fn isdigit(c: rune) bool = { + if (c < 48) { return false; }; + if (c > 57) { return false; }; + return true; +}; + +export fn isupper(c: rune) bool = { + if (c < 65) { return false; }; + if (c > 90) { return false; }; + return true; +}; + +export fn islower(c: rune) bool = { + if (c < 97) { return false; }; + if (c > 122) { return false; }; + return true; +}; + +export fn isalpha(c: rune) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: rune) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + if (c == 10) { return true; }; // '\n' + if (c == 11) { return true; }; // '\v' + if (c == 12) { return true; }; // '\f' + if (c == 13) { return true; }; // '\r' + return false; +}; + +export fn isxdigit(c: rune) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65) { + if (c <= 70) { return true; }; // 'A'..'F' + }; + if (c >= 97) { + if (c <= 102) { return true; }; // 'a'..'f' + }; + return false; +}; + +// valid — `c` is in the 0..127 ASCII range. +export fn valid(c: rune) bool = { + if (c < 0) { return false; }; + if (c > 127) { return false; }; + return true; +}; + +// validstr — every byte in `s` is ASCII (0..127). +export fn validstr(s: str) bool = { + let i: i32 = 0; + for (i < s.len) { + // High-bit test rather than `> 127u8`; both cgens lower + // the bitwise form identically. The `> u8` form picks + // JA vs JG depending on signed/unsigned dispatch. + if ((s[i] & 128u8) != 0u8) { return false; }; + i += 1; + }; + return true; +}; + +// iscntrl — control chars: 0..31 and 127. +export fn iscntrl(c: rune) bool = { + if (c >= 0) { if (c <= 31) { return true; }; }; + if (c == 127) { return true; }; + return false; +}; + +// isblank — space and tab. +export fn isblank(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + return false; +}; + +// isprint — printable: space through '~'. +export fn isprint(c: rune) bool = { + if (c < 32) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// isgraph — printable, non-space. +export fn isgraph(c: rune) bool = { + if (c < 33) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// ispunct — printable, non-alnum, non-space. +export fn ispunct(c: rune) bool = { + if (!isgraph(c)) { return false; }; + if (isalnum(c)) { return false; }; + return true; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: rune) rune = { + if (isupper(c)) { return c + 32; }; + return c; +}; + +export fn toupper(c: rune) rune = { + if (islower(c)) { return c - 32; }; + return c; +}; + +// strcasecmp — three-way ASCII case-insensitive compare. +export fn strcasecmp(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + let ca: rune = tolower(a[i]: rune); + let cb: rune = tolower(b[i]: rune); + if (ca != cb) { return (ca - cb): i32; }; + i += 1; + }; + return a.len - b.len; +}; + // 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. @@ -5116,91 +5771,12 @@ export fn stou8(s: str, b: base) (u8 | invalid | overflow) = { return 0: invalid; }; -// f64tos — convert v to a decimal string. Returns owned str; release -// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is -// fixed-point only, max 6 fractional digits, no NaN/Inf support — -// see graduate-to-Ryū note below). -// -// Surface: -// -// - finite values only. NaN/±Inf detection needs an f64→u64 bit -// reinterpret cast that the cgen doesn't expose yet. -// - fixed-point only, up to 6 fractional digits. Trailing zeros -// after the decimal point are trimmed. Trailing '.' is dropped. -// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) -// fall back to the literal token "huge". Hare would print these -// in scientific notation via Ryū; we will graduate when the -// compiler grows the bit-reinterpret cast. -// -// Round-trip is therefore lossy past 6 fractional digits. -// -// No float literals in the body — 990's wwdump diff requires this -// file's TK_FLOAT count to match between C and ww front-ends, and -// the ww-side wwdump currently skips TK_FLOAT.fval while the C side -// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: -// build f64 constants via int-to-f64 casts. -let f64tos_buf: [64]u8; - -export fn f64tos(v: f64) str = { - let out: i32 = 0; - let f: f64 = v; - let zero: f64 = 0: f64; - if (f < zero) { - f64tos_buf[out] = 45u8; // '-' - out += 1; - f = -f; - }; - // 9e18 is comfortably under I64_MAX (9.22e18). Past this the - // `f: i64` cast wraps and the integer part comes back as garbage. - let cap: f64 = 9000000000000000000i64: f64; - if (f >= cap) { - let s: str = "huge"; - let k: i32 = 0; - for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; - }; - let ip: i64 = f: i64; - // Fractional part scaled to 6 decimal digits, with round-to- - // nearest via +0.5. (f64 compound assigns mis-lower in cgen — - // use the explicit form, as the rest of lib does.) - let frac: f64 = f - (ip: f64); - let scale: f64 = 1000000: f64; - frac = frac * scale; - let half: f64 = (1: f64) / (2: f64); - let fp: i64 = (frac + half): i64; - // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer - // part needs to advance. - if (fp >= 1000000) { - ip += 1; - fp = 0; - }; - let intstr: str = i64tos(ip, base.DEC); - let k: i32 = 0; - for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; }; - if (fp != 0) { - f64tos_buf[out] = 46u8; // '.' - out += 1; - let fracstr: str = u64tos(fp: u64, base.DEC); - // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → - // fp=50000, fracstr="50000", pad one '0' before). - let z: i32 = 6 - fracstr.len; - for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; }; - k = 0; - for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; }; - // Trim trailing zeros in the fractional part. - for (out > 0) { - if (f64tos_buf[out - 1] != 48u8) { break; }; - out -= 1; - }; - }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; -}; +// f64tos — graduated to the Ryū shortest-round-trippable implementation +// in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version +// (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted +// here per the lib-note graduation rule ("replace in one go, don't keep +// both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b +// (task #67, gated on the #143 f32-arg-push cgen fix). // strerror — convert an strconv error to a user-readable string. // Returns owned str; release via os.free. Mirrors Hare's diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f61626a1..1407ee29 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1049,143 +1049,6 @@ fn decimal_round(d: *decimal) u64 = { return n; }; -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -package ascii; - -export fn isdigit(c: rune) bool = { - if (c < 48) { return false; }; - if (c > 57) { return false; }; - return true; -}; - -export fn isupper(c: rune) bool = { - if (c < 65) { return false; }; - if (c > 90) { return false; }; - return true; -}; - -export fn islower(c: rune) bool = { - if (c < 97) { return false; }; - if (c > 122) { return false; }; - return true; -}; - -export fn isalpha(c: rune) bool = { - if (isupper(c)) { return true; }; - return islower(c); -}; - -export fn isalnum(c: rune) bool = { - if (isalpha(c)) { return true; }; - return isdigit(c); -}; - -// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. -export fn isspace(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - if (c == 10) { return true; }; // '\n' - if (c == 11) { return true; }; // '\v' - if (c == 12) { return true; }; // '\f' - if (c == 13) { return true; }; // '\r' - return false; -}; - -export fn isxdigit(c: rune) bool = { - if (isdigit(c)) { return true; }; - if (c >= 65) { - if (c <= 70) { return true; }; // 'A'..'F' - }; - if (c >= 97) { - if (c <= 102) { return true; }; // 'a'..'f' - }; - return false; -}; - -// valid — `c` is in the 0..127 ASCII range. -export fn valid(c: rune) bool = { - if (c < 0) { return false; }; - if (c > 127) { return false; }; - return true; -}; - -// validstr — every byte in `s` is ASCII (0..127). -export fn validstr(s: str) bool = { - let i: i32 = 0; - for (i < s.len) { - // High-bit test rather than `> 127u8`; both cgens lower - // the bitwise form identically. The `> u8` form picks - // JA vs JG depending on signed/unsigned dispatch. - if ((s[i] & 128u8) != 0u8) { return false; }; - i += 1; - }; - return true; -}; - -// iscntrl — control chars: 0..31 and 127. -export fn iscntrl(c: rune) bool = { - if (c >= 0) { if (c <= 31) { return true; }; }; - if (c == 127) { return true; }; - return false; -}; - -// isblank — space and tab. -export fn isblank(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - return false; -}; - -// isprint — printable: space through '~'. -export fn isprint(c: rune) bool = { - if (c < 32) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// isgraph — printable, non-space. -export fn isgraph(c: rune) bool = { - if (c < 33) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// ispunct — printable, non-alnum, non-space. -export fn ispunct(c: rune) bool = { - if (!isgraph(c)) { return false; }; - if (isalnum(c)) { return false; }; - return true; -}; - -// tolower / toupper — fold ASCII case. Non-letters pass through. -export fn tolower(c: rune) rune = { - if (isupper(c)) { return c + 32; }; - return c; -}; - -export fn toupper(c: rune) rune = { - if (islower(c)) { return c - 32; }; - return c; -}; - -// strcasecmp — three-way ASCII case-insensitive compare. -export fn strcasecmp(a: str, b: str) i32 = { - let n: i32 = a.len; - if (b.len < n) { n = b.len; }; - let i: i32 = 0; - for (i < n) { - let ca: rune = tolower(a[i]: rune); - let cb: rune = tolower(b[i]: rune); - if (ca != cb) { return (ca - cb): i32; }; - i += 1; - }; - return a.len - b.len; -}; - // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: @@ -1476,6 +1339,798 @@ export fn absi64(n: i64) u64 = { return n: u64; }; +// strconv — float→string via Ryū (shortest round-trippable decimal). +// Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + +// ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, +// https://doi.org/10.1145/3192366.3192369 — Hare translated it from the +// reference C (https://github.com/ulfjack/ryu); ww follows Hare. +// +// SCOPE — the f64tos shortest-representation subset (Hare's ffmt::G, +// prec=void, fflags::NONE). Two deferrals: +// - f32tos (ftos.ha:448) + its f32 Ryū sub-path (f32todecf32 + +// mulpow5inv/pow5_divpow2 + mulshift32 + the *32 helpers + the +// F32_POW5_*_BITCOUNT defs) → fold-5b (task #67), gated on the #143 +// f32-arg-push cgen fix: f32tos must call math.f32bits(n), passing +// an f32 arg, which spills MOVSD (cstage) vs MOVSS (wwstage) → +// 990-997 byte-id break. Not isolatable (f32bits IS the reinterpret), +// only fixable. f32tos IS a real Hare entry — it ships in #67. +// - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs +// io::handle/memio + a `(size|io::error)?` per appendrune (#158); +// for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback +// machinery is provably dead code — `ok` is always true → init_dec/ +// compute_round/round unreachable — which bootstrap-coverage rejects). +// The lib note blesses "a documented subset". This file ships ZERO float +// literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the +// wwdump TK_FLOAT embedding concern is moot. +// +// Decomposition divergences (the #163-166 tuple/struct-ABI cluster — +// ww's partial tuple support miscompiles the shapes Hare uses; the +// WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's +// own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for +// u128mul, and fold-4/stof.ww decomposed likewise): +// - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params +// `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple +// return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return +// reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO +// struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww). +// - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)` +// return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the +// math/floats.ww frexpf64 precedent). +// - dead `mulshift64` (tuple-param, never called) + dead +// `F32/F64_DECIMAL_DIGITS` dropped. +// +// Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha): +// - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local +// (stof.ww hex_to_bits precedent). +// - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block. +// comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound +// local + block. `assert()` → `os.assert(cond,msg)`. +// - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]` +// (#155 / #156, stof.ww eisel_lemire precedent). +// - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64 +// at each use (Hare promotes; ww is strict — int-machine-word note). +// - a `(N: uint)` cast embedded inside an array subscript `[ ]` is +// rejected by the ww parser → hoist to a named local before the +// index (decimal.ww "hoist size casts" note); see init_dec_mant_exp +// + encode_e_dec. + +package strconv; + +import math; +import os; + +// ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's +// "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition. +// (u128mul + the r128 struct live in stof.ww, fold-4's first consumer; +// reused in-package here.) +fn u128rshift(lo: u64, hi: u64, s: u32) u64 = { + os.assert(s <= 64u32, "strconv.u128rshift: s > 64"); + return (hi << (64u64 - (s: u64))) | (lo >> (s: u64)); +}; + +// ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value. +fn pow5fac(v: u64) u32 = { + let value: u64 = v; + let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64) + let n_div_5: u64 = 3689348814741910323u64; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u64, "strconv.pow5fac: value == 0"); + value *= m_inv_5; + if (value > n_div_5) { break; }; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:64. +fn ibool(b: bool) u8 = { + if (b) { return 1u8; }; + return 0u8; +}; + +// ref/hare/strconv/ftos_ryu.ha:66-67. +fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:69. +fn pow2multiple(v: u64, p: u32) bool = { + os.assert(v > 0u64, "strconv.pow2multiple: v == 0"); + os.assert(p < 64u32, "strconv.pow2multiple: p >= 64"); + return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64; +}; + +// ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple. +// Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return → +// this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound +// `v_minus` + block. +type ryuv = struct { vp: u64, vr: u64, vm: u64 }; + +fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = { + let mm: u64 = m << 1u64; + let r0: r128 = u128mul(mm, mul0); + let r1: r128 = u128mul(mm, mul1); + let lo: u64 = r0.lo; + let tmp: u64 = r0.hi; + let mid: u64 = tmp + r1.lo; + let hi: u64 = r1.hi + (ibool(mid < tmp): u64); + let lo2: u64 = lo + mul0; + let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64); + let hi2: u64 = hi + (ibool(mid2 < mid): u64); + let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32)); + let v_minus: u64 = 0u64; + if (mm_shift == 1u32) { + let lo3: u64 = lo - mul0; + let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64); + let hi3: u64 = hi - (ibool(mid3 > mid): u64); + v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32)); + } else { + let lo3: u64 = lo + lo; + let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64); + let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64); + let lo4: u64 = lo3 - mul0; + let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64); + let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64); + v_minus = u128rshift(mid4, hi4, ((j - 64): u32)); + }; + let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32)); + return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus }; +}; + +// ref/hare/strconv/ftos_ryu.ha:140. +fn log2pow5(e: u32) u32 = { + os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528"); + return (e * 1217359u32) >> 19u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:145-147. +fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; }; +fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); }; + +// ref/hare/strconv/ftos_ryu.ha:149. +fn log10pow2(e: u32) u32 = { + os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650"); + return (e * 78913u32) >> 18u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:154. +fn log10pow5(e: u32) u32 = { + os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620"); + return (e * 732923u32) >> 20u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the +// inverse power of five. 2-tuple kept (works); row-bind → double-index. +fn f64computeinvpow5(i: u32) (u64, u64) = { + let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = base2 - i; + if (off == 0u32) { + return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(base2) - pow5bits(i); + let res0: u64 = u128rshift(low0, sum, delta) + 1u64 + + (((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:246. +fn f64computepow5(i: u32) (u64, u64) = { + let base: u32 = i / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = i - base2; + if (off == 0u32) { + return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(i) - pow5bits(base2); + let res0: u64 = u128rshift(low0, sum, delta) + + (((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64: +// value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32 +// (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second +// field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww +// byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value +// always fits i32 (cast at the init_dec_mant_exp call site). +type decf64 = struct { mantissa: u64, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f64. +fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { + let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32; + let m2: u64 = 0u64; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u64) == 0u64; + let mv: u64 = 4u64 * m2; + let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32; + let vp: u64 = 0u64; + let vr: u64 = 0u64; + let vm: u64 = 0u64; + let e10: i32 = 0i32; + let vm_trailing_zeros: bool = false; + let vr_trailing_zeros: bool = false; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32); + e10 = q: i32; + let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + let pow5 = f64computeinvpow5(q); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 21u32) { + if ((mv - 5u64 * (mv / 5u64)) == 0u64) { + vr_trailing_zeros = pow5multiple(mv, q); + } else if (accept_bounds) { + vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q); + } else { + vp -= (ibool(pow5multiple(mv + 2u64, q)): u64); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32); + e10 = e2 + (q: i32); + let i: i32 = -e2 - (q: i32); + let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32); + let j: i32 = (q: i32) - k; + let pow5 = f64computepow5(i: u32); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 1u32) { + vr_trailing_zeros = true; + if (accept_bounds) { + vm_trailing_zeros = mm_shift == 1u32; + } else { + vp -= 1u64; + }; + } else if (q < 63u32) { + vr_trailing_zeros = pow2multiple(mv, q); + }; + }; + let removed: i32 = 0i32; + let last_removed_digit: u8 = 0u8; + let output: u64 = 0u64; + if (vm_trailing_zeros || vr_trailing_zeros) { + for (true) { + let vpby10: u64 = vp / 10u64; + let vmby10: u64 = vm / 10u64; + if (vpby10 <= vmby10) { break; }; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + if (vm_trailing_zeros) { + for (true) { + let vmby10: u64 = vm / 10u64; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + if (vmmod10 != 0u32) { break; }; + let vpby10: u64 = vp / 10u64; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + }; + if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u64); + } else { + let round_up: bool = false; + let vpby100: u64 = vp / 100u64; + let vmby100: u64 = vm / 100u64; + if (vpby100 > vmby100) { + let vrby100: u64 = vr / 100u64; + let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32); + round_up = vrmod100 >= 50u32; + vr = vrby100; vp = vpby100; vm = vmby100; + removed += 2i32; + }; + for (true) { + let vmby10: u64 = vm / 10u64; + let vpby10: u64 = vp / 10u64; + if (vpby10 <= vmby10) { break; }; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + round_up = vrmod10 >= 5u32; + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || round_up): u64); + }; + let exp: i32 = e10 + removed; + return decf64 { exponent = (exp: i64), mantissa = output }; +}; + +// ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void / +// fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase +// arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE +// for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always +// G) and are NOT ported — porting them stubbed would be untested dead +// code. The parametric ftosf/ffmt/fflags surface is deferred (task #64; +// needs a parametric consumer + io::handle + #158). ==== + +// ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17). +fn declen(n: u64) uint = { + os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17"); + if (n >= 100000000000000000u64) { return (18u32: uint); }; + if (n >= 10000000000000000u64) { return (17u32: uint); }; + if (n >= 1000000000000000u64) { return (16u32: uint); }; + if (n >= 100000000000000u64) { return (15u32: uint); }; + if (n >= 10000000000000u64) { return (14u32: uint); }; + if (n >= 1000000000000u64) { return (13u32: uint); }; + if (n >= 100000000000u64) { return (12u32: uint); }; + if (n >= 10000000000u64) { return (11u32: uint); }; + if (n >= 1000000000u64) { return (10u32: uint); }; + if (n >= 100000000u64) { return (9u32: uint); }; + if (n >= 10000000u64) { return (8u32: uint); }; + if (n >= 1000000u64) { return (7u32: uint); }; + if (n >= 100000u64) { return (6u32: uint); }; + if (n >= 10000u64) { return (5u32: uint); }; + if (n >= 1000u64) { return (4u32: uint); }; + if (n >= 100u64) { return (3u32: uint); }; + if (n >= 10u64) { return (2u32: uint); }; + return (1u32: uint); +}; + +// ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent) +// into the decimal `d`. `mantissa` is mutated in Hare → local `mant`. +fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = { + // Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded + // inside an array subscript (decimal.ww "hoist size casts" note). + let U_ZERO: uint = (0u32: uint); + let U_ONE: uint = (1u32: uint); + let mant: u64 = mantissa; + let dl: uint = declen(mant); + let i: uint = U_ZERO; + for (i < dl) { + d.digits[dl - i - U_ONE] = (mant % 10u64): u8; + mant /= 10u64; + i += U_ONE; + }; + d.nd = (dl: size); + d.dp = (dl: i32) + exponent; +}; + +// ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the +// *tos static-buffer convention replaces Hare's io::handle sink). +fn putstr(buf: []u8, out: i32, s: str) i32 = { + let o: i32 = out; + let k: i32 = 0i32; + for (k < s.len) { + buf[o] = s[k]; + o += 1i32; + k += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable +// logic only). Writes into `buf` at cursor `out`, returns the new cursor. +fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + let lo: i32 = 0i32; + if (d.dp <= 0i32) { lo = d.dp - 1i32; }; + let hi: i32 = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + if (hi > (d.nd: i32) && d.dp <= 0i32) { + hi = (d.nd: i32); + } else if (hi > d.dp && d.dp > 0i32) { + hi = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + }; + let i: i32 = lo; + for (i < hi) { + if (i == d.dp) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + if (0i32 <= i && i < (d.nd: i32)) { + buf[o] = (d.digits[i] + 48u8): u8; + } else { + buf[o] = 48u8; // '0' + }; + o += 1i32; + i += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable +// logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad. +fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0"); + buf[o] = (d.digits[0] + 48u8): u8; + o += 1i32; + if ((d.nd: i32) > 1i32) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + let i: size = (1u64: size); + for (i < d.nd) { + buf[o] = (d.digits[i] + 48u8): u8; + o += 1i32; + i += (1u64: size); + }; + buf[o] = 101u8; // 'e' + o += 1i32; + let e: i32 = d.dp - 1i32; + if (e < 0i32) { + e = -e; + buf[o] = 45u8; // '-' + o += 1i32; + }; + // Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`). + let U_ONE: uint = (1u32: uint); + let U_TWO: uint = (2u32: uint); + let U_THREE: uint = (3u32: uint); + let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits + let l: uint = declen(e: u64); + let k: uint = (0u32: uint); + for (k < l) { + ebuf[U_TWO - k] = (e % 10i32): u8; + e /= 10i32; + k += U_ONE; + }; + let m: uint = U_THREE - l; + for (m < U_THREE) { + buf[o] = (ebuf[m] + 48u8): u8; + o += 1i32; + m += U_ONE; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a +// view into a static buffer overwritten on the next call (the *tos +// convention; see strings.dup to retain). Equivalent to Hare's ftosf +// with format G + precision void. The fftosf G/void/NONE path is inlined +// (the parametric surface is deferred — task #64). +// +// Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' + +// exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer +// emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw +// split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused. +let f64tos_buf: [32]u8; + +export fn f64tos(n: f64) str = { + let bits: u64 = math.f64bits(n); + let mantissa: u64 = bits & math.F64_MANTISSA_MASK; + let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32; + let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64; + let special: bool = exponent == (math.F64_EXPONENT_MASK: u32); + + let o: i32 = 0i32; + let r: str; + r.ptr = &f64tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u64) { + o = putstr(f64tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f64tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f64tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u64) { + f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a + // scalar-field read of a LOCAL struct (`d.nd`) to a bogus global + // symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers + // correctly in both stages (the stof.ww/decimal.ww *decimal precedent) + // — #170. The init/trim/encode calls already took &d; route via pd. + let pd: *decimal = &d; + let dd: decf64 = f64todecf64(mantissa, exponent); + init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32)); + // ok = !ffpoint(NONE) || ... is always true → no multiprecision + // fallback (ftos.ha:365). f == G → trim (ftos.ha:386). + trim(pd); + if (pd.nd == (0u64: size)) { + f64tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f64tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f64tos_buf[0:32], o); + }; + r.len = o; + return r; +}; + +// strconv — Ryū float→string lookup tables + bit-count constants. +// Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data +// fold (strconv #106 fold-5): no logic, consumed by ftos.ww's +// f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores). +// +// File-organisation divergence: Hare keeps these tables INLINE in +// ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring +// the stof.ww / stof_data.ww split) — same `package strconv`, so the +// tables stay visible to ftos.ww with no qualification. +// +// Spelling divergences (same as stof_data.ww, candidate #130 + rule-12): +// - Hare `const TBL = [...]` → ww module-level `let` (ww has no +// module-`const` keyword; the values are never written). +// - every literal carries its element-width suffix (`u64`/`u32`): +// cstage rejects bare integer literals in `[N]uXX` init while +// wwstage accepts them; the suffixed form is the only shape both +// stages agree on. +// - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened); +// the 2D module-level static-init + double-index read landed in +// #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64. + +package strconv; + +// ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split +// power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32 +// at each use site (Hare promotes a u8 def inside mixed-width arithmetic; +// ww is strict — explicit cast, project_int_machine_word_derived_limits). +// The F32_POW5_*_BITCOUNT siblings (ftos_ryu.ha:162-163) land in fold-5b +// (task #67) with their only consumer, f32todecf32 — omitted here to keep +// this fold dead-code-free. +def F64_POW5_INV_BITCOUNT: u8 = 125u8; +def F64_POW5_BITCOUNT: u8 = 125u8; + +// ref/hare/strconv/ftos_ryu.ha:165-181. +let F64_POW5_INV_SPLIT2: [15][2]u64 = [ + [1u64, 2305843009213693952u64], + [5955668970331000884u64, 1784059615882449851u64], + [8982663654677661702u64, 1380349269358112757u64], + [7286864317269821294u64, 2135987035920910082u64], + [7005857020398200553u64, 1652639921975621497u64], + [17965325103354776697u64, 1278668206209430417u64], + [8928596168509315048u64, 1978643211784836272u64], + [10075671573058298858u64, 1530901034580419511u64], + [597001226353042382u64, 1184477304306571148u64], + [1527430471115325346u64, 1832889850782397517u64], + [12533209867169019542u64, 1418129833677084982u64], + [5577825024675947042u64, 2194449627517475473u64], + [11006974540203867551u64, 1697873161311732311u64], + [10313493231639821582u64, 1313665730009899186u64], + [12701016819766672773u64, 2032799256770390445u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:183-188. +let POW5_INV_OFFSETS: [19]u32 = [ + 0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32, + 0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32, + 0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32, + 0x00000000u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:190-204. +let F64_POW5_SPLIT2: [13][2]u64 = [ + [0u64, 1152921504606846976u64], + [0u64, 1490116119384765625u64], + [1032610780636961552u64, 1925929944387235853u64], + [7910200175544436838u64, 1244603055572228341u64], + [16941905809032713930u64, 1608611746708759036u64], + [13024893955298202172u64, 2079081953128979843u64], + [6607496772837067824u64, 1343575221513417750u64], + [17332926989895652603u64, 1736530273035216783u64], + [13037379183483547984u64, 2244412773384604712u64], + [1605989338741628675u64, 1450417759929778918u64], + [9630225068416591280u64, 1874621017369538693u64], + [665883850346957067u64, 1211445438634777304u64], + [14931890668723713708u64, 1565756531257009982u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:206-211. +let POW5_OFFSETS: [21]u32 = [ + 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32, + 0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32, + 0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32, + 0x55559155u32, 0x51405555u32, 0x00000105u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in +// f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those +// arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage +// rejects a def-named array length — "array length must be an integer +// literal"; wwstage accepts it but emits an empty DATAW — divergence +// #167, so the literal `26` is the only shape both stages agree on; +// matches decimal.ww's `[800]u8` array-dimension-literal precedent). +def POW5_TABLE_SZ: u8 = 26u8; + +// ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is +// commented out in Hare too — it lives implicitly in the SPLIT2 tables). +let POW5_TABLE: [26]u64 = [ + 1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64, + 390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64, + 1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64, + 762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64, + 476837158203125u64, 2384185791015625u64, 11920928955078125u64, + 59604644775390625u64, 298023223876953125u64, +]; + +// ascii — rune-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family (rune-taking signature). Runes +// outside 0..127 always answer `false`. The lexer hot path uses these +// inline; they are expected to inline to a couple of compares. + +package ascii; + +export fn isdigit(c: rune) bool = { + if (c < 48) { return false; }; + if (c > 57) { return false; }; + return true; +}; + +export fn isupper(c: rune) bool = { + if (c < 65) { return false; }; + if (c > 90) { return false; }; + return true; +}; + +export fn islower(c: rune) bool = { + if (c < 97) { return false; }; + if (c > 122) { return false; }; + return true; +}; + +export fn isalpha(c: rune) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: rune) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + if (c == 10) { return true; }; // '\n' + if (c == 11) { return true; }; // '\v' + if (c == 12) { return true; }; // '\f' + if (c == 13) { return true; }; // '\r' + return false; +}; + +export fn isxdigit(c: rune) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65) { + if (c <= 70) { return true; }; // 'A'..'F' + }; + if (c >= 97) { + if (c <= 102) { return true; }; // 'a'..'f' + }; + return false; +}; + +// valid — `c` is in the 0..127 ASCII range. +export fn valid(c: rune) bool = { + if (c < 0) { return false; }; + if (c > 127) { return false; }; + return true; +}; + +// validstr — every byte in `s` is ASCII (0..127). +export fn validstr(s: str) bool = { + let i: i32 = 0; + for (i < s.len) { + // High-bit test rather than `> 127u8`; both cgens lower + // the bitwise form identically. The `> u8` form picks + // JA vs JG depending on signed/unsigned dispatch. + if ((s[i] & 128u8) != 0u8) { return false; }; + i += 1; + }; + return true; +}; + +// iscntrl — control chars: 0..31 and 127. +export fn iscntrl(c: rune) bool = { + if (c >= 0) { if (c <= 31) { return true; }; }; + if (c == 127) { return true; }; + return false; +}; + +// isblank — space and tab. +export fn isblank(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + return false; +}; + +// isprint — printable: space through '~'. +export fn isprint(c: rune) bool = { + if (c < 32) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// isgraph — printable, non-space. +export fn isgraph(c: rune) bool = { + if (c < 33) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// ispunct — printable, non-alnum, non-space. +export fn ispunct(c: rune) bool = { + if (!isgraph(c)) { return false; }; + if (isalnum(c)) { return false; }; + return true; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: rune) rune = { + if (isupper(c)) { return c + 32; }; + return c; +}; + +export fn toupper(c: rune) rune = { + if (islower(c)) { return c - 32; }; + return c; +}; + +// strcasecmp — three-way ASCII case-insensitive compare. +export fn strcasecmp(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + let ca: rune = tolower(a[i]: rune); + let cb: rune = tolower(b[i]: rune); + if (ca != cb) { return (ca - cb): i32; }; + i += 1; + }; + return a.len - b.len; +}; + // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -5116,91 +5771,12 @@ export fn stou8(s: str, b: base) (u8 | invalid | overflow) = { return 0: invalid; }; -// f64tos — convert v to a decimal string. Returns owned str; release -// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is -// fixed-point only, max 6 fractional digits, no NaN/Inf support — -// see graduate-to-Ryū note below). -// -// Surface: -// -// - finite values only. NaN/±Inf detection needs an f64→u64 bit -// reinterpret cast that the cgen doesn't expose yet. -// - fixed-point only, up to 6 fractional digits. Trailing zeros -// after the decimal point are trimmed. Trailing '.' is dropped. -// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) -// fall back to the literal token "huge". Hare would print these -// in scientific notation via Ryū; we will graduate when the -// compiler grows the bit-reinterpret cast. -// -// Round-trip is therefore lossy past 6 fractional digits. -// -// No float literals in the body — 990's wwdump diff requires this -// file's TK_FLOAT count to match between C and ww front-ends, and -// the ww-side wwdump currently skips TK_FLOAT.fval while the C side -// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: -// build f64 constants via int-to-f64 casts. -let f64tos_buf: [64]u8; - -export fn f64tos(v: f64) str = { - let out: i32 = 0; - let f: f64 = v; - let zero: f64 = 0: f64; - if (f < zero) { - f64tos_buf[out] = 45u8; // '-' - out += 1; - f = -f; - }; - // 9e18 is comfortably under I64_MAX (9.22e18). Past this the - // `f: i64` cast wraps and the integer part comes back as garbage. - let cap: f64 = 9000000000000000000i64: f64; - if (f >= cap) { - let s: str = "huge"; - let k: i32 = 0; - for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; - }; - let ip: i64 = f: i64; - // Fractional part scaled to 6 decimal digits, with round-to- - // nearest via +0.5. (f64 compound assigns mis-lower in cgen — - // use the explicit form, as the rest of lib does.) - let frac: f64 = f - (ip: f64); - let scale: f64 = 1000000: f64; - frac = frac * scale; - let half: f64 = (1: f64) / (2: f64); - let fp: i64 = (frac + half): i64; - // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer - // part needs to advance. - if (fp >= 1000000) { - ip += 1; - fp = 0; - }; - let intstr: str = i64tos(ip, base.DEC); - let k: i32 = 0; - for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; }; - if (fp != 0) { - f64tos_buf[out] = 46u8; // '.' - out += 1; - let fracstr: str = u64tos(fp: u64, base.DEC); - // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → - // fp=50000, fracstr="50000", pad one '0' before). - let z: i32 = 6 - fracstr.len; - for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; }; - k = 0; - for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; }; - // Trim trailing zeros in the fractional part. - for (out > 0) { - if (f64tos_buf[out - 1] != 48u8) { break; }; - out -= 1; - }; - }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; -}; +// f64tos — graduated to the Ryū shortest-round-trippable implementation +// in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version +// (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted +// here per the lib-note graduation rule ("replace in one go, don't keep +// both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b +// (task #67, gated on the #143 f32-arg-push cgen fix). // strerror — convert an strconv error to a user-readable string. // Returns owned str; release via os.free. Mirrors Hare's diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index a87433a8..7863f99b 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -1049,143 +1049,6 @@ fn decimal_round(d: *decimal) u64 = { return n; }; -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -package ascii; - -export fn isdigit(c: rune) bool = { - if (c < 48) { return false; }; - if (c > 57) { return false; }; - return true; -}; - -export fn isupper(c: rune) bool = { - if (c < 65) { return false; }; - if (c > 90) { return false; }; - return true; -}; - -export fn islower(c: rune) bool = { - if (c < 97) { return false; }; - if (c > 122) { return false; }; - return true; -}; - -export fn isalpha(c: rune) bool = { - if (isupper(c)) { return true; }; - return islower(c); -}; - -export fn isalnum(c: rune) bool = { - if (isalpha(c)) { return true; }; - return isdigit(c); -}; - -// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. -export fn isspace(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - if (c == 10) { return true; }; // '\n' - if (c == 11) { return true; }; // '\v' - if (c == 12) { return true; }; // '\f' - if (c == 13) { return true; }; // '\r' - return false; -}; - -export fn isxdigit(c: rune) bool = { - if (isdigit(c)) { return true; }; - if (c >= 65) { - if (c <= 70) { return true; }; // 'A'..'F' - }; - if (c >= 97) { - if (c <= 102) { return true; }; // 'a'..'f' - }; - return false; -}; - -// valid — `c` is in the 0..127 ASCII range. -export fn valid(c: rune) bool = { - if (c < 0) { return false; }; - if (c > 127) { return false; }; - return true; -}; - -// validstr — every byte in `s` is ASCII (0..127). -export fn validstr(s: str) bool = { - let i: i32 = 0; - for (i < s.len) { - // High-bit test rather than `> 127u8`; both cgens lower - // the bitwise form identically. The `> u8` form picks - // JA vs JG depending on signed/unsigned dispatch. - if ((s[i] & 128u8) != 0u8) { return false; }; - i += 1; - }; - return true; -}; - -// iscntrl — control chars: 0..31 and 127. -export fn iscntrl(c: rune) bool = { - if (c >= 0) { if (c <= 31) { return true; }; }; - if (c == 127) { return true; }; - return false; -}; - -// isblank — space and tab. -export fn isblank(c: rune) bool = { - if (c == 32) { return true; }; // ' ' - if (c == 9) { return true; }; // '\t' - return false; -}; - -// isprint — printable: space through '~'. -export fn isprint(c: rune) bool = { - if (c < 32) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// isgraph — printable, non-space. -export fn isgraph(c: rune) bool = { - if (c < 33) { return false; }; - if (c > 126) { return false; }; - return true; -}; - -// ispunct — printable, non-alnum, non-space. -export fn ispunct(c: rune) bool = { - if (!isgraph(c)) { return false; }; - if (isalnum(c)) { return false; }; - return true; -}; - -// tolower / toupper — fold ASCII case. Non-letters pass through. -export fn tolower(c: rune) rune = { - if (isupper(c)) { return c + 32; }; - return c; -}; - -export fn toupper(c: rune) rune = { - if (islower(c)) { return c - 32; }; - return c; -}; - -// strcasecmp — three-way ASCII case-insensitive compare. -export fn strcasecmp(a: str, b: str) i32 = { - let n: i32 = a.len; - if (b.len < n) { n = b.len; }; - let i: i32 = 0; - for (i < n) { - let ca: rune = tolower(a[i]: rune); - let cb: rune = tolower(b[i]: rune); - if (ca != cb) { return (ca - cb): i32; }; - i += 1; - }; - return a.len - b.len; -}; - // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: @@ -1476,6 +1339,798 @@ export fn absi64(n: i64) u64 = { return n: u64; }; +// strconv — float→string via Ryū (shortest round-trippable decimal). +// Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + +// ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, +// https://doi.org/10.1145/3192366.3192369 — Hare translated it from the +// reference C (https://github.com/ulfjack/ryu); ww follows Hare. +// +// SCOPE — the f64tos shortest-representation subset (Hare's ffmt::G, +// prec=void, fflags::NONE). Two deferrals: +// - f32tos (ftos.ha:448) + its f32 Ryū sub-path (f32todecf32 + +// mulpow5inv/pow5_divpow2 + mulshift32 + the *32 helpers + the +// F32_POW5_*_BITCOUNT defs) → fold-5b (task #67), gated on the #143 +// f32-arg-push cgen fix: f32tos must call math.f32bits(n), passing +// an f32 arg, which spills MOVSD (cstage) vs MOVSS (wwstage) → +// 990-997 byte-id break. Not isolatable (f32bits IS the reinterpret), +// only fixable. f32tos IS a real Hare entry — it ships in #67. +// - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs +// io::handle/memio + a `(size|io::error)?` per appendrune (#158); +// for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback +// machinery is provably dead code — `ok` is always true → init_dec/ +// compute_round/round unreachable — which bootstrap-coverage rejects). +// The lib note blesses "a documented subset". This file ships ZERO float +// literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the +// wwdump TK_FLOAT embedding concern is moot. +// +// Decomposition divergences (the #163-166 tuple/struct-ABI cluster — +// ww's partial tuple support miscompiles the shapes Hare uses; the +// WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's +// own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for +// u128mul, and fold-4/stof.ww decomposed likewise): +// - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params +// `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple +// return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return +// reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO +// struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww). +// - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)` +// return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the +// math/floats.ww frexpf64 precedent). +// - dead `mulshift64` (tuple-param, never called) + dead +// `F32/F64_DECIMAL_DIGITS` dropped. +// +// Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha): +// - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local +// (stof.ww hex_to_bits precedent). +// - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block. +// comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound +// local + block. `assert()` → `os.assert(cond,msg)`. +// - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]` +// (#155 / #156, stof.ww eisel_lemire precedent). +// - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64 +// at each use (Hare promotes; ww is strict — int-machine-word note). +// - a `(N: uint)` cast embedded inside an array subscript `[ ]` is +// rejected by the ww parser → hoist to a named local before the +// index (decimal.ww "hoist size casts" note); see init_dec_mant_exp +// + encode_e_dec. + +package strconv; + +import math; +import os; + +// ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's +// "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition. +// (u128mul + the r128 struct live in stof.ww, fold-4's first consumer; +// reused in-package here.) +fn u128rshift(lo: u64, hi: u64, s: u32) u64 = { + os.assert(s <= 64u32, "strconv.u128rshift: s > 64"); + return (hi << (64u64 - (s: u64))) | (lo >> (s: u64)); +}; + +// ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value. +fn pow5fac(v: u64) u32 = { + let value: u64 = v; + let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64) + let n_div_5: u64 = 3689348814741910323u64; + let count: u32 = 0u32; + for (true) { + os.assert(value != 0u64, "strconv.pow5fac: value == 0"); + value *= m_inv_5; + if (value > n_div_5) { break; }; + count += 1u32; + }; + return count; +}; + +// ref/hare/strconv/ftos_ryu.ha:64. +fn ibool(b: bool) u8 = { + if (b) { return 1u8; }; + return 0u8; +}; + +// ref/hare/strconv/ftos_ryu.ha:66-67. +fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; }; + +// ref/hare/strconv/ftos_ryu.ha:69. +fn pow2multiple(v: u64, p: u32) bool = { + os.assert(v > 0u64, "strconv.pow2multiple: v == 0"); + os.assert(p < 64u32, "strconv.pow2multiple: p >= 64"); + return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64; +}; + +// ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple. +// Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return → +// this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound +// `v_minus` + block. +type ryuv = struct { vp: u64, vr: u64, vm: u64 }; + +fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = { + let mm: u64 = m << 1u64; + let r0: r128 = u128mul(mm, mul0); + let r1: r128 = u128mul(mm, mul1); + let lo: u64 = r0.lo; + let tmp: u64 = r0.hi; + let mid: u64 = tmp + r1.lo; + let hi: u64 = r1.hi + (ibool(mid < tmp): u64); + let lo2: u64 = lo + mul0; + let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64); + let hi2: u64 = hi + (ibool(mid2 < mid): u64); + let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32)); + let v_minus: u64 = 0u64; + if (mm_shift == 1u32) { + let lo3: u64 = lo - mul0; + let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64); + let hi3: u64 = hi - (ibool(mid3 > mid): u64); + v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32)); + } else { + let lo3: u64 = lo + lo; + let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64); + let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64); + let lo4: u64 = lo3 - mul0; + let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64); + let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64); + v_minus = u128rshift(mid4, hi4, ((j - 64): u32)); + }; + let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32)); + return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus }; +}; + +// ref/hare/strconv/ftos_ryu.ha:140. +fn log2pow5(e: u32) u32 = { + os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528"); + return (e * 1217359u32) >> 19u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:145-147. +fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; }; +fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); }; + +// ref/hare/strconv/ftos_ryu.ha:149. +fn log10pow2(e: u32) u32 = { + os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650"); + return (e * 78913u32) >> 18u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:154. +fn log10pow5(e: u32) u32 = { + os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620"); + return (e * 732923u32) >> 20u32; +}; + +// ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the +// inverse power of five. 2-tuple kept (works); row-bind → double-index. +fn f64computeinvpow5(i: u32) (u64, u64) = { + let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = base2 - i; + if (off == 0u32) { + return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(base2) - pow5bits(i); + let res0: u64 = u128rshift(low0, sum, delta) + 1u64 + + (((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:246. +fn f64computepow5(i: u32) (u64, u64) = { + let base: u32 = i / (POW5_TABLE_SZ: u32); + let base2: u32 = base * (POW5_TABLE_SZ: u32); + let off: u32 = i - base2; + if (off == 0u32) { + return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]); + }; + let m: u64 = POW5_TABLE[off]; + let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]); + let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]); + let high1: u64 = r1.hi; + let low1: u64 = r1.lo; + let high0: u64 = r0.hi; + let low0: u64 = r0.lo; + let sum: u64 = high0 + low1; + if (sum < high0) { + high1 += 1u64; + }; + let delta: u32 = pow5bits(i) - pow5bits(base2); + let res0: u64 = u128rshift(low0, sum, delta) + + (((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); + let res1: u64 = u128rshift(sum, high1, delta); + return (res0, res1); +}; + +// ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64: +// value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32 +// (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second +// field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww +// byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value +// always fits i32 (cast at the init_dec_mant_exp call site). +type decf64 = struct { mantissa: u64, exponent: i64 }; + +// ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw +// IEEE-754 fields of an f64. +fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { + let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32; + let m2: u64 = 0u64; + if (exponent == 0u32) { + e2 = 1i32 - e2; + m2 = mantissa; + } else { + e2 = (exponent: i32) - e2; + m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa; + }; + let accept_bounds: bool = (m2 & 1u64) == 0u64; + let mv: u64 = 4u64 * m2; + let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32; + let vp: u64 = 0u64; + let vr: u64 = 0u64; + let vm: u64 = 0u64; + let e10: i32 = 0i32; + let vm_trailing_zeros: bool = false; + let vr_trailing_zeros: bool = false; + if (e2 >= 0i32) { + let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32); + e10 = q: i32; + let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; + let i: i32 = -e2 + ((q + k): i32); + let pow5 = f64computeinvpow5(q); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 21u32) { + if ((mv - 5u64 * (mv / 5u64)) == 0u64) { + vr_trailing_zeros = pow5multiple(mv, q); + } else if (accept_bounds) { + vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q); + } else { + vp -= (ibool(pow5multiple(mv + 2u64, q)): u64); + }; + }; + } else { + let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32); + e10 = e2 + (q: i32); + let i: i32 = -e2 - (q: i32); + let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32); + let j: i32 = (q: i32) - k; + let pow5 = f64computepow5(i: u32); + let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift); + vp = res.vp; vr = res.vr; vm = res.vm; + if (q <= 1u32) { + vr_trailing_zeros = true; + if (accept_bounds) { + vm_trailing_zeros = mm_shift == 1u32; + } else { + vp -= 1u64; + }; + } else if (q < 63u32) { + vr_trailing_zeros = pow2multiple(mv, q); + }; + }; + let removed: i32 = 0i32; + let last_removed_digit: u8 = 0u8; + let output: u64 = 0u64; + if (vm_trailing_zeros || vr_trailing_zeros) { + for (true) { + let vpby10: u64 = vp / 10u64; + let vmby10: u64 = vm / 10u64; + if (vpby10 <= vmby10) { break; }; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + if (vm_trailing_zeros) { + for (true) { + let vmby10: u64 = vm / 10u64; + let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); + if (vmmod10 != 0u32) { break; }; + let vpby10: u64 = vp / 10u64; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); + last_removed_digit = (vrmod10: u8); + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + }; + if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) { + last_removed_digit = 4u8; // round to even + }; + let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros)); + let cond2: bool = last_removed_digit >= 5u8; + output = vr + (ibool(cond1 || cond2): u64); + } else { + let round_up: bool = false; + let vpby100: u64 = vp / 100u64; + let vmby100: u64 = vm / 100u64; + if (vpby100 > vmby100) { + let vrby100: u64 = vr / 100u64; + let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32); + round_up = vrmod100 >= 50u32; + vr = vrby100; vp = vpby100; vm = vmby100; + removed += 2i32; + }; + for (true) { + let vmby10: u64 = vm / 10u64; + let vpby10: u64 = vp / 10u64; + if (vpby10 <= vmby10) { break; }; + let vrby10: u64 = vr / 10u64; + let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); + round_up = vrmod10 >= 5u32; + vr = vrby10; vp = vpby10; vm = vmby10; + removed += 1i32; + }; + output = vr + (ibool(vr == vm || round_up): u64); + }; + let exp: i32 = e10 + removed; + return decf64 { exponent = (exp: i64), mantissa = output }; +}; + +// ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void / +// fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase +// arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE +// for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always +// G) and are NOT ported — porting them stubbed would be untested dead +// code. The parametric ftosf/ffmt/fflags surface is deferred (task #64; +// needs a parametric consumer + io::handle + #158). ==== + +// ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17). +fn declen(n: u64) uint = { + os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17"); + if (n >= 100000000000000000u64) { return (18u32: uint); }; + if (n >= 10000000000000000u64) { return (17u32: uint); }; + if (n >= 1000000000000000u64) { return (16u32: uint); }; + if (n >= 100000000000000u64) { return (15u32: uint); }; + if (n >= 10000000000000u64) { return (14u32: uint); }; + if (n >= 1000000000000u64) { return (13u32: uint); }; + if (n >= 100000000000u64) { return (12u32: uint); }; + if (n >= 10000000000u64) { return (11u32: uint); }; + if (n >= 1000000000u64) { return (10u32: uint); }; + if (n >= 100000000u64) { return (9u32: uint); }; + if (n >= 10000000u64) { return (8u32: uint); }; + if (n >= 1000000u64) { return (7u32: uint); }; + if (n >= 100000u64) { return (6u32: uint); }; + if (n >= 10000u64) { return (5u32: uint); }; + if (n >= 1000u64) { return (4u32: uint); }; + if (n >= 100u64) { return (3u32: uint); }; + if (n >= 10u64) { return (2u32: uint); }; + return (1u32: uint); +}; + +// ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent) +// into the decimal `d`. `mantissa` is mutated in Hare → local `mant`. +fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = { + // Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded + // inside an array subscript (decimal.ww "hoist size casts" note). + let U_ZERO: uint = (0u32: uint); + let U_ONE: uint = (1u32: uint); + let mant: u64 = mantissa; + let dl: uint = declen(mant); + let i: uint = U_ZERO; + for (i < dl) { + d.digits[dl - i - U_ONE] = (mant % 10u64): u8; + mant /= 10u64; + i += U_ONE; + }; + d.nd = (dl: size); + d.dp = (dl: i32) + exponent; +}; + +// ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the +// *tos static-buffer convention replaces Hare's io::handle sink). +fn putstr(buf: []u8, out: i32, s: str) i32 = { + let o: i32 = out; + let k: i32 = 0i32; + for (k < s.len) { + buf[o] = s[k]; + o += 1i32; + k += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable +// logic only). Writes into `buf` at cursor `out`, returns the new cursor. +fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + let lo: i32 = 0i32; + if (d.dp <= 0i32) { lo = d.dp - 1i32; }; + let hi: i32 = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + if (hi > (d.nd: i32) && d.dp <= 0i32) { + hi = (d.nd: i32); + } else if (hi > d.dp && d.dp > 0i32) { + hi = d.dp; + if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; + }; + let i: i32 = lo; + for (i < hi) { + if (i == d.dp) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + if (0i32 <= i && i < (d.nd: i32)) { + buf[o] = (d.digits[i] + 48u8): u8; + } else { + buf[o] = 48u8; // '0' + }; + o += 1i32; + i += 1i32; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable +// logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad. +fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = { + let o: i32 = out; + os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0"); + buf[o] = (d.digits[0] + 48u8): u8; + o += 1i32; + if ((d.nd: i32) > 1i32) { + buf[o] = 46u8; // '.' + o += 1i32; + }; + let i: size = (1u64: size); + for (i < d.nd) { + buf[o] = (d.digits[i] + 48u8): u8; + o += 1i32; + i += (1u64: size); + }; + buf[o] = 101u8; // 'e' + o += 1i32; + let e: i32 = d.dp - 1i32; + if (e < 0i32) { + e = -e; + buf[o] = 45u8; // '-' + o += 1i32; + }; + // Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`). + let U_ONE: uint = (1u32: uint); + let U_TWO: uint = (2u32: uint); + let U_THREE: uint = (3u32: uint); + let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits + let l: uint = declen(e: u64); + let k: uint = (0u32: uint); + for (k < l) { + ebuf[U_TWO - k] = (e % 10i32): u8; + e /= 10i32; + k += U_ONE; + }; + let m: uint = U_THREE - l; + for (m < U_THREE) { + buf[o] = (ebuf[m] + 48u8): u8; + o += 1i32; + m += U_ONE; + }; + return o; +}; + +// ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a +// view into a static buffer overwritten on the next call (the *tos +// convention; see strings.dup to retain). Equivalent to Hare's ftosf +// with format G + precision void. The fftosf G/void/NONE path is inlined +// (the parametric surface is deferred — task #64). +// +// Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' + +// exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer +// emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw +// split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused. +let f64tos_buf: [32]u8; + +export fn f64tos(n: f64) str = { + let bits: u64 = math.f64bits(n); + let mantissa: u64 = bits & math.F64_MANTISSA_MASK; + let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32; + let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64; + let special: bool = exponent == (math.F64_EXPONENT_MASK: u32); + + let o: i32 = 0i32; + let r: str; + r.ptr = &f64tos_buf[0]; + // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). + if (special && mantissa != 0u64) { + o = putstr(f64tos_buf[0:32], o, "nan"); + r.len = o; + return r; + }; + if (sign) { + f64tos_buf[o] = 45u8; // '-' + o += 1i32; + }; + if (special) { + o = putstr(f64tos_buf[0:32], o, "infinity"); + r.len = o; + return r; + }; + if (exponent == 0u32 && mantissa == 0u64) { + f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) + o += 1i32; + r.len = o; + return r; + }; + + let d = decimal { ... }; + // Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a + // scalar-field read of a LOCAL struct (`d.nd`) to a bogus global + // symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers + // correctly in both stages (the stof.ww/decimal.ww *decimal precedent) + // — #170. The init/trim/encode calls already took &d; route via pd. + let pd: *decimal = &d; + let dd: decf64 = f64todecf64(mantissa, exponent); + init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32)); + // ok = !ffpoint(NONE) || ... is always true → no multiprecision + // fallback (ftos.ha:365). f == G → trim (ftos.ha:386). + trim(pd); + if (pd.nd == (0u64: size)) { + f64tos_buf[o] = 48u8; // rounded to zero + o += 1i32; + } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { + o = encode_e_dec(pd, f64tos_buf[0:32], o); + } else { + o = encode_f_dec(pd, f64tos_buf[0:32], o); + }; + r.len = o; + return r; +}; + +// strconv — Ryū float→string lookup tables + bit-count constants. +// Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data +// fold (strconv #106 fold-5): no logic, consumed by ftos.ww's +// f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores). +// +// File-organisation divergence: Hare keeps these tables INLINE in +// ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring +// the stof.ww / stof_data.ww split) — same `package strconv`, so the +// tables stay visible to ftos.ww with no qualification. +// +// Spelling divergences (same as stof_data.ww, candidate #130 + rule-12): +// - Hare `const TBL = [...]` → ww module-level `let` (ww has no +// module-`const` keyword; the values are never written). +// - every literal carries its element-width suffix (`u64`/`u32`): +// cstage rejects bare integer literals in `[N]uXX` init while +// wwstage accepts them; the suffixed form is the only shape both +// stages agree on. +// - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened); +// the 2D module-level static-init + double-index read landed in +// #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64. + +package strconv; + +// ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split +// power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32 +// at each use site (Hare promotes a u8 def inside mixed-width arithmetic; +// ww is strict — explicit cast, project_int_machine_word_derived_limits). +// The F32_POW5_*_BITCOUNT siblings (ftos_ryu.ha:162-163) land in fold-5b +// (task #67) with their only consumer, f32todecf32 — omitted here to keep +// this fold dead-code-free. +def F64_POW5_INV_BITCOUNT: u8 = 125u8; +def F64_POW5_BITCOUNT: u8 = 125u8; + +// ref/hare/strconv/ftos_ryu.ha:165-181. +let F64_POW5_INV_SPLIT2: [15][2]u64 = [ + [1u64, 2305843009213693952u64], + [5955668970331000884u64, 1784059615882449851u64], + [8982663654677661702u64, 1380349269358112757u64], + [7286864317269821294u64, 2135987035920910082u64], + [7005857020398200553u64, 1652639921975621497u64], + [17965325103354776697u64, 1278668206209430417u64], + [8928596168509315048u64, 1978643211784836272u64], + [10075671573058298858u64, 1530901034580419511u64], + [597001226353042382u64, 1184477304306571148u64], + [1527430471115325346u64, 1832889850782397517u64], + [12533209867169019542u64, 1418129833677084982u64], + [5577825024675947042u64, 2194449627517475473u64], + [11006974540203867551u64, 1697873161311732311u64], + [10313493231639821582u64, 1313665730009899186u64], + [12701016819766672773u64, 2032799256770390445u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:183-188. +let POW5_INV_OFFSETS: [19]u32 = [ + 0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32, + 0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32, + 0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32, + 0x00000000u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:190-204. +let F64_POW5_SPLIT2: [13][2]u64 = [ + [0u64, 1152921504606846976u64], + [0u64, 1490116119384765625u64], + [1032610780636961552u64, 1925929944387235853u64], + [7910200175544436838u64, 1244603055572228341u64], + [16941905809032713930u64, 1608611746708759036u64], + [13024893955298202172u64, 2079081953128979843u64], + [6607496772837067824u64, 1343575221513417750u64], + [17332926989895652603u64, 1736530273035216783u64], + [13037379183483547984u64, 2244412773384604712u64], + [1605989338741628675u64, 1450417759929778918u64], + [9630225068416591280u64, 1874621017369538693u64], + [665883850346957067u64, 1211445438634777304u64], + [14931890668723713708u64, 1565756531257009982u64], +]; + +// ref/hare/strconv/ftos_ryu.ha:206-211. +let POW5_OFFSETS: [21]u32 = [ + 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32, + 0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32, + 0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32, + 0x55559155u32, 0x51405555u32, 0x00000105u32, +]; + +// ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in +// f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those +// arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage +// rejects a def-named array length — "array length must be an integer +// literal"; wwstage accepts it but emits an empty DATAW — divergence +// #167, so the literal `26` is the only shape both stages agree on; +// matches decimal.ww's `[800]u8` array-dimension-literal precedent). +def POW5_TABLE_SZ: u8 = 26u8; + +// ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is +// commented out in Hare too — it lives implicitly in the SPLIT2 tables). +let POW5_TABLE: [26]u64 = [ + 1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64, + 390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64, + 1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64, + 762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64, + 476837158203125u64, 2384185791015625u64, 11920928955078125u64, + 59604644775390625u64, 298023223876953125u64, +]; + +// ascii — rune-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family (rune-taking signature). Runes +// outside 0..127 always answer `false`. The lexer hot path uses these +// inline; they are expected to inline to a couple of compares. + +package ascii; + +export fn isdigit(c: rune) bool = { + if (c < 48) { return false; }; + if (c > 57) { return false; }; + return true; +}; + +export fn isupper(c: rune) bool = { + if (c < 65) { return false; }; + if (c > 90) { return false; }; + return true; +}; + +export fn islower(c: rune) bool = { + if (c < 97) { return false; }; + if (c > 122) { return false; }; + return true; +}; + +export fn isalpha(c: rune) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: rune) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + if (c == 10) { return true; }; // '\n' + if (c == 11) { return true; }; // '\v' + if (c == 12) { return true; }; // '\f' + if (c == 13) { return true; }; // '\r' + return false; +}; + +export fn isxdigit(c: rune) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65) { + if (c <= 70) { return true; }; // 'A'..'F' + }; + if (c >= 97) { + if (c <= 102) { return true; }; // 'a'..'f' + }; + return false; +}; + +// valid — `c` is in the 0..127 ASCII range. +export fn valid(c: rune) bool = { + if (c < 0) { return false; }; + if (c > 127) { return false; }; + return true; +}; + +// validstr — every byte in `s` is ASCII (0..127). +export fn validstr(s: str) bool = { + let i: i32 = 0; + for (i < s.len) { + // High-bit test rather than `> 127u8`; both cgens lower + // the bitwise form identically. The `> u8` form picks + // JA vs JG depending on signed/unsigned dispatch. + if ((s[i] & 128u8) != 0u8) { return false; }; + i += 1; + }; + return true; +}; + +// iscntrl — control chars: 0..31 and 127. +export fn iscntrl(c: rune) bool = { + if (c >= 0) { if (c <= 31) { return true; }; }; + if (c == 127) { return true; }; + return false; +}; + +// isblank — space and tab. +export fn isblank(c: rune) bool = { + if (c == 32) { return true; }; // ' ' + if (c == 9) { return true; }; // '\t' + return false; +}; + +// isprint — printable: space through '~'. +export fn isprint(c: rune) bool = { + if (c < 32) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// isgraph — printable, non-space. +export fn isgraph(c: rune) bool = { + if (c < 33) { return false; }; + if (c > 126) { return false; }; + return true; +}; + +// ispunct — printable, non-alnum, non-space. +export fn ispunct(c: rune) bool = { + if (!isgraph(c)) { return false; }; + if (isalnum(c)) { return false; }; + return true; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: rune) rune = { + if (isupper(c)) { return c + 32; }; + return c; +}; + +export fn toupper(c: rune) rune = { + if (islower(c)) { return c - 32; }; + return c; +}; + +// strcasecmp — three-way ASCII case-insensitive compare. +export fn strcasecmp(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + let ca: rune = tolower(a[i]: rune); + let cb: rune = tolower(b[i]: rune); + if (ca != cb) { return (ca - cb): i32; }; + i += 1; + }; + return a.len - b.len; +}; + // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -5116,91 +5771,12 @@ export fn stou8(s: str, b: base) (u8 | invalid | overflow) = { return 0: invalid; }; -// f64tos — convert v to a decimal string. Returns owned str; release -// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is -// fixed-point only, max 6 fractional digits, no NaN/Inf support — -// see graduate-to-Ryū note below). -// -// Surface: -// -// - finite values only. NaN/±Inf detection needs an f64→u64 bit -// reinterpret cast that the cgen doesn't expose yet. -// - fixed-point only, up to 6 fractional digits. Trailing zeros -// after the decimal point are trimmed. Trailing '.' is dropped. -// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) -// fall back to the literal token "huge". Hare would print these -// in scientific notation via Ryū; we will graduate when the -// compiler grows the bit-reinterpret cast. -// -// Round-trip is therefore lossy past 6 fractional digits. -// -// No float literals in the body — 990's wwdump diff requires this -// file's TK_FLOAT count to match between C and ww front-ends, and -// the ww-side wwdump currently skips TK_FLOAT.fval while the C side -// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: -// build f64 constants via int-to-f64 casts. -let f64tos_buf: [64]u8; - -export fn f64tos(v: f64) str = { - let out: i32 = 0; - let f: f64 = v; - let zero: f64 = 0: f64; - if (f < zero) { - f64tos_buf[out] = 45u8; // '-' - out += 1; - f = -f; - }; - // 9e18 is comfortably under I64_MAX (9.22e18). Past this the - // `f: i64` cast wraps and the integer part comes back as garbage. - let cap: f64 = 9000000000000000000i64: f64; - if (f >= cap) { - let s: str = "huge"; - let k: i32 = 0; - for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; - }; - let ip: i64 = f: i64; - // Fractional part scaled to 6 decimal digits, with round-to- - // nearest via +0.5. (f64 compound assigns mis-lower in cgen — - // use the explicit form, as the rest of lib does.) - let frac: f64 = f - (ip: f64); - let scale: f64 = 1000000: f64; - frac = frac * scale; - let half: f64 = (1: f64) / (2: f64); - let fp: i64 = (frac + half): i64; - // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer - // part needs to advance. - if (fp >= 1000000) { - ip += 1; - fp = 0; - }; - let intstr: str = i64tos(ip, base.DEC); - let k: i32 = 0; - for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; }; - if (fp != 0) { - f64tos_buf[out] = 46u8; // '.' - out += 1; - let fracstr: str = u64tos(fp: u64, base.DEC); - // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → - // fp=50000, fracstr="50000", pad one '0' before). - let z: i32 = 6 - fracstr.len; - for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; }; - k = 0; - for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; }; - // Trim trailing zeros in the fractional part. - for (out > 0) { - if (f64tos_buf[out - 1] != 48u8) { break; }; - out -= 1; - }; - }; - let r: str; - r.ptr = &f64tos_buf[0]; - r.len = out; - return r; -}; +// f64tos — graduated to the Ryū shortest-round-trippable implementation +// in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version +// (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted +// here per the lib-note graduation rule ("replace in one go, don't keep +// both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b +// (task #67, gated on the #143 f32-arg-push cgen fix). // strerror — convert an strconv error to a user-readable string. // Returns owned str; release via os.free. Mirrors Hare's diff --git a/test/wcc/908_ftos_run.c b/test/wcc/908_ftos_run.c new file mode 100644 index 00000000..a9253a18 --- /dev/null +++ b/test/wcc/908_ftos_run.c @@ -0,0 +1,55 @@ +/* + * 908_ftos_run — execute the lib/strconv/test/ftostest fixture under the + * C-side `ww run` driver and assert exit 0. + * + * Same thin-wrapper shape as 909_stof_run / 922_decimal_run: ftostest.ww + * carries its own `export fn main()` that drives the @test fns (Ryū + * f64tos: fixed + scientific + nan/±inf/zero) and signals which case + * failed via the exit code (signalled + 10). + * + * The fixture lives in lib/strconv/test/ (not lib/strconv/) so `import + * strconv` resolves to the strconv DIRECTORY (full package: strconv.ww + + * decimal.ww + stof_data.ww + stof.ww + ftos_data.ww + ftos.ww) rather + * than shadowing on the strconv.ww FILE — same rationale as 922. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/strconv/test/ftostest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "ftos_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("ftos_run: %s ok\n", src); + return 0; +}