From 07e57ff9a64fafa710ed580037302d9a31c01fe9 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 27 May 2026 00:44:16 +0900 Subject: [PATCH] lib/strconv: decimal arbitrary-precision arithmetic (#106 fold-3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port ref/hare/strconv/decimal.ha (~202 LOC Hare) → 314 LOC lib/strconv/decimal.ww — decimal struct + 11 fns (trim, decimal_shift, leftshift, leftshift_newdigits, rightshift, round, decimal_round, helpers). 1:1 mechanical Hare-fidelity with 8 documented spelling-divergences. Shared engine for stof (fold-4) + ftos (fold-5). Built atop 5 wwstage cgen prereqs (#131/#133-expanded/#134/#135/#138) that closed gate-blind silent miscompiles surfaced by the port. Test 922_decimal_run + lib/strconv/test/decimaltest.ww (6 @test fns covering all 11 impl fns). --- Makefile | 9 +- lib/strconv/decimal.ww | 314 ++++++++++++++++++++++++++ lib/strconv/test/decimaltest.ww | 212 ++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 315 +++++++++++++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 315 +++++++++++++++++++++++++++ selfhost/test/smoke.combined.ww | 315 +++++++++++++++++++++++++++ test/wcc/922_decimal_run.c | 57 +++++ 7 files changed, 1535 insertions(+), 2 deletions(-) create mode 100644 lib/strconv/decimal.ww create mode 100644 lib/strconv/test/decimaltest.ww create mode 100644 test/wcc/922_decimal_run.c diff --git a/Makefile b/Makefile index 0fa4aaa3..f4cd44ce 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/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/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/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/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,6 +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_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 \ @@ -1031,6 +1032,10 @@ $(BIN)/test_bytes_run: test/wcc/967_bytes_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_decimal_run: test/wcc/922_decimal_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/strconv/decimal.ww b/lib/strconv/decimal.ww new file mode 100644 index 00000000..a70babc9 --- /dev/null +++ b/lib/strconv/decimal.ww @@ -0,0 +1,314 @@ +// strconv — arbitrary-precision decimal engine for float↔string +// conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports +// Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 +// references (#121 residual-guard SAFE). +// +// Spelling divergences from Hare (mechanical, ww-side parser shape): +// - Hare `let a = X, b = Y;` → two single `let` statements +// (ww parser doesn't accept comma-separated bindings). +// - Hare `tbl[lo..]` open-ended slice → direct indexing +// `tbl[lo + i]` at point-of-use (equivalent algorithm; no +// allocation, no aliasing). ww `[lo:hi]` uses `:`; `..` form +// is not parsed. +// - Hare `0z`/`1z` size literals → ww has no `z` suffix; pre-bind +// `let SZ_ZERO: size = (0u64: size);` etc. at function entry +// ("hoisted size casts as local consts" — ww `T: type` casts +// embedded inside expressions confuse the parser). +// - Hare `~0u64` typed-suffix literal → ww parser rejects `~` on +// typed-suffix; route via a named zero local + `~zero`. +// - Hare `for (cond; afterthought)` 2-clause → ww 3-clause +// `for (init; cond; post)` (when continue is used; the post +// must run each iteration) or inline-the-afterthought in body +// (when no continue exists in the loop). +// - Hare `fn foo() T = if (cond) {...} else expr;` expression body +// → ww requires a `{}` block body throughout. +// - Hare bare `assert(cond)` builtin → `os.assert(cond, msg)`; +// wwstage cgen has no `assert` intercept (deferred fold). +// +// CGEN class closures consumed (post-prereqs): +// - #131 (4acab6e) — `len(d.digits)` compile-time-folds cs==ww +// - #134 (36bf603) — `d.digits[nd] >= 5u8` picks JAE (unsigned) +// - #133 (3986818) — `d.digits[i] += 1u8` load-op-store BOTH +// stages +// - #135 (ade6840) — `(*d).digits[i]` read+write N_DOT-base addr +// +// Drew CGEN-SAFE invariants: +// - #129: module-level decls here are integer-literal defs only. +// - #128: digits is fundamental [800]u8, zero-init only. +// - #121: zero float ops. +// - Drew watch-item `*d = decimal{...};` reset (line 110 in Hare): +// pointer-deref reset to composite-literal probed cs==ww +// byte-id safe. + +package strconv; + +import os; + +// ref/hare/strconv/decimal.ha:5. +def maxshift: u8 = 60u8; + +// ref/hare/strconv/decimal.ha:6. +def decimal_point_range: u16 = 2047u16; + +// ref/hare/strconv/decimal.ha:8-26. Field layout 1:1. The 800-digit +// bound covers subnormal doubles (min exp -1074, max mantissa 4e16 +// → at most 767 digits; 800 leaves headroom). +export type decimal = struct { + digits: [800]u8, + nd: size, + dp: i32, + negative: bool, + truncated: bool, +}; + +// ref/hare/strconv/decimal.ha:29-33. Strip trailing zeros. +fn trim(d: *decimal) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + for (d.nd > SZ_ZERO && d.digits[d.nd - SZ_ONE] == 0u8) { + d.nd -= SZ_ONE; + }; +}; + +// ref/hare/strconv/decimal.ha:35-55. Compute the digit-count +// increase for a left-shift `shift` (consults left_shift_table + +// pow5_table from stof_data.ww, bb6f840). Uses `continue` so the +// loop stays in 3-clause form for byte-id-correct post-increment. +fn leftshift_newdigits(d: *decimal, shift: u32) u32 = { + shift &= 63u32; + let x_a: u32 = (left_shift_table[shift]: u32); + let x_b: u32 = (left_shift_table[shift + 1u32]: u32); + let nn: u32 = x_a >> 11u32; + let pow5_a: u32 = 0x7FFu32 & x_a; + let pow5_b: u32 = 0x7FFu32 & x_b; + let n: u32 = pow5_b - pow5_a; + for (let i: u32 = 0u32; i < n; i += 1u32) { + let i_sz: size = (i: size); + if (i_sz >= d.nd) { + return nn - 1u32; + } else if (d.digits[i] == pow5_table[pow5_a + i]) { + continue; + } else if (d.digits[i] < pow5_table[pow5_a + i]) { + return nn - 1u32; + } else { + return nn; + }; + }; + return nn; +}; + +// ref/hare/strconv/decimal.ha:57-91. Shift `d` left by k bits. +fn leftshift(d: *decimal, k: u32) void = { + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let MAXSHIFT_U32: u32 = (maxshift: u32); + os.assert(k <= MAXSHIFT_U32, "strconv.leftshift: k > maxshift"); + if (d.nd == (0u64: size)) { return; }; + let nn: u32 = leftshift_newdigits(d, k); + let r: int = (d.nd: int) - 1; + let w: size = (r: size) + (nn: size); + let n: u64 = 0u64; + for (r >= 0) { + n += (d.digits[r]: u64) << kU64; + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + r -= 1; + w -= SZ_ONE; + }; + for (n > 0u64) { + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + w -= SZ_ONE; + }; + d.nd += (nn: size); + if (d.nd > SZ_BOUND) { + d.nd = SZ_BOUND; + }; + d.dp += (nn: i32); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:93-134. Shift `d` right by k bits. +// Two outer Hare 2-clause loops (`for (cond; r += 1)`) are inlined +// as `for (cond) { ... r += SZ_ONE; }` since neither uses continue. +fn rightshift(d: *decimal, k: u32) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let r: size = SZ_ZERO; + let w: size = SZ_ZERO; + let n: u64 = 0u64; + for ((n >> kU64) == 0u64) { + if (r >= d.nd) { + if (n == 0u64) { + d.nd = SZ_ZERO; + return; + }; + for ((n >> kU64) == 0u64) { + n *= 10u64; + r += SZ_ONE; + }; + break; + }; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + d.dp -= (r: i32) - 1; + if (d.dp < -(decimal_point_range: i32)) { + // Drew-watch-item: pointer-deref reset to composite + // literal — probed cs==ww byte-id safe in pre-flight. + *d = decimal { ... }; + return; + }; + let mask: u64 = (1u64 << kU64) - 1u64; + for (r < d.nd) { + let dig: u64 = n >> kU64; + n &= mask; + d.digits[w] = (dig: u8); + w += SZ_ONE; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + for (n > 0u64) { + let dig: u64 = n >> kU64; + n &= mask; + if (w < SZ_BOUND) { + d.digits[w] = (dig: u8); + w += SZ_ONE; + } else if (dig > 0u64) { + d.truncated = true; + }; + n *= 10u64; + }; + d.nd = w; + trim(d); +}; + +// ref/hare/strconv/decimal.ha:138-153. Shift right (k < 0) or left +// (k > 0). Hardware shifts cap at 60 bits without losing top +// digits, so break large shifts into maxshift-sized chunks. +fn decimal_shift(d: *decimal, k: int) void = { + let MAXSHIFT_INT: int = (maxshift: int); + let MAXSHIFT_U32: u32 = (maxshift: u32); + if (d.nd == (0u64: size)) { return; }; + if (k > 0) { + for (k > MAXSHIFT_INT) { + leftshift(d, MAXSHIFT_U32); + k -= MAXSHIFT_INT; + }; + leftshift(d, (k: u32)); + } else if (k < 0) { + for (k < -MAXSHIFT_INT) { + rightshift(d, MAXSHIFT_U32); + k += MAXSHIFT_INT; + }; + rightshift(d, ((-k): u32)); + }; +}; + +// ref/hare/strconv/decimal.ha:155-160. Banker's rounding decision: +// at the exact half (digit==5, no more digits) round to even (the +// preceding digit's low bit decides); past-half rounds up; below- +// half rounds down. Hare's expression-bodied `if` re-shaped as a +// block per ww parser. +fn should_round_up(d: *decimal, nd: uint) bool = { + let nd_sz: size = (nd: size); + let SZ_ONE: size = (1u64: size); + let U_ONE: uint = (1u32: uint); + let U_ZERO: uint = (0u32: uint); + if (nd_sz < d.nd) { + if (d.digits[nd] == 5u8 && (nd_sz + SZ_ONE) == d.nd) { + let lowbit_lit: bool = false; + if (nd > U_ZERO) { + if ((d.digits[nd - U_ONE] & 1u8) != 0u8) { + lowbit_lit = true; + }; + }; + return d.truncated || lowbit_lit; + } else { + return d.digits[nd] >= 5u8; + }; + }; + return false; +}; + +// ref/hare/strconv/decimal.ha:162-166. Round to `nd` digits. +fn round(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + if (should_round_up(d, nd)) { + roundup(d, nd); + } else { + rounddown(d, nd); + }; +}; + +// ref/hare/strconv/decimal.ha:168-172. Truncate to `nd` digits. +fn rounddown(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + d.nd = (nd: size); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:174-186. Round up to `nd` digits; +// propagate carry. If all 9s, the result is a single 1 with the +// decimal point advanced. +fn roundup(d: *decimal, nd: uint) void = { + let SZ_ONE: size = (1u64: size); + if ((nd: size) >= d.nd) { return; }; + for (let i: int = (nd: int) - 1; i >= 0; i -= 1) { + if (d.digits[i] < 9u8) { + d.digits[i] += 1u8; + d.nd = (i: size) + SZ_ONE; + return; + }; + }; + d.digits[0] = 1u8; + d.nd = SZ_ONE; + d.dp += 1; +}; + +// ref/hare/strconv/decimal.ha:188-202. Read `d` as the integer +// rounded to `d.dp` digits. Returns 0 if `d.dp <= 0`; returns +// ~0u64 if `d.dp > 18` (exceeds u64 range). Hare's two 2-clause +// loops (`for (cond; i += 1)`) are inlined per the spelling +// divergence at file top. +fn decimal_round(d: *decimal) u64 = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + if (d.nd == SZ_ZERO || d.dp < 0) { return 0u64; }; + if (d.dp > 18) { + // Hare's `~0u64` doesn't parse on a typed-suffix literal + // in ww; route via a named zero. + let zero: u64 = 0u64; + return ~zero; + }; + let dp_sz: size = ((d.dp: uint): size); + let i: size = SZ_ZERO; + let n: u64 = 0u64; + for (i < dp_sz && i < d.nd) { + n = n * 10u64 + (d.digits[i]: u64); + i += SZ_ONE; + }; + for (i < dp_sz) { + n *= 10u64; + i += SZ_ONE; + }; + if (should_round_up(d, (d.dp: uint))) { + n += 1u64; + }; + return n; +}; diff --git a/lib/strconv/test/decimaltest.ww b/lib/strconv/test/decimaltest.ww new file mode 100644 index 00000000..5c0b7e41 --- /dev/null +++ b/lib/strconv/test/decimaltest.ww @@ -0,0 +1,212 @@ +// decimaltest — exercises lib/strconv/decimal.ww (Hare decimal.ha port). +// Run with `out/bin/ww run lib/strconv/test/decimaltest.ww`. Same +// signalled-then-fail()-with-+10 pattern as bytestest / hex / utf8. +// Non-zero exit pinpoints the failing scenario. +// +// Hare has no decimal_test.ha (decimal is exercised transitively via +// ftos_test.ha). ww-authored probes per +// feedback_test_match_hare_source ("ww-authored where Hare doesn't +// ship direct tests"). Coverage per Drew design pre-flight: +// trim (RHS-zeros), decimal_shift (k=0/k>0/k<0/|k|>maxshift), +// leftshift (small/edge), rightshift (small/break-out), round (at +// various dp), decimal_round (small + dp>18 + dp<0). +// +// Lives in lib/strconv/test/ (not lib/strconv/) so `import strconv` +// resolves to the lib/strconv DIRECTORY rather than shadowing on +// the strconv.ww FILE — pulls in decimal.ww + stof_data.ww + +// strconv.ww as the unified `package strconv` compilation. + +package strconv; + +import strconv; +import os; + +let signalled: i32 = 0; +fn fail() void = { os.exit(signalled + 10); }; + +// Seeds `d` with `vals` as the leading digits + `dp` as the decimal +// point, leaving negative/truncated false. Each test starts from a +// known state. +fn seed(d: *decimal, vals: []u8, dp: i32) void = { + let SZ_ONE: size = (1u64: size); + let i: size = (0u64: size); + for (i < (vals.len: size)) { + d.digits[i] = vals[i]; + i += SZ_ONE; + }; + d.nd = (vals.len: size); + d.dp = dp; +}; + +// ---- trim ------------------------------------------------------------- +// ref/hare/strconv/decimal.ha:29. + +@test fn trim_cases() void = { + let d: decimal; + let vals: [5]u8; + vals[0] = 1u8; vals[1] = 2u8; vals[2] = 3u8; vals[3] = 0u8; vals[4] = 0u8; + seed(&d, vals[0:5], 3); + trim(&d); + if (d.nd != (3u64: size)) { fail(); }; + if (d.dp != 3) { fail(); }; + + // all-zero case: nd shrinks to 0. + let zeros: [3]u8; + seed(&d, zeros[0:3], 2); + trim(&d); + if (d.nd != (0u64: size)) { fail(); }; +}; + +// ---- leftshift / leftshift_newdigits --------------------------------- +// ref/hare/strconv/decimal.ha:35,57. + +@test fn leftshift_small_cases() void = { + // Shift "5" by 1 → "10" → trim collapses to nd=1 with dp+=1 (Hare + // trims trailing zero of the high-shifted byte). + let d: decimal; + d.digits[0] = 5u8; + d.nd = (1u64: size); + d.dp = 1; + leftshift(&d, 1u32); + if (d.dp != 2) { fail(); }; + if (d.digits[0] != 1u8) { fail(); }; + // post-trim nd is 1 (trailing 0 stripped). + if (d.nd != (1u64: size)) { fail(); }; + + // Shift "123" by 1 → "246" (no trim — no trailing zero). + let v3: [3]u8; + v3[0] = 1u8; v3[1] = 2u8; v3[2] = 3u8; + seed(&d, v3[0:3], 3); + leftshift(&d, 1u32); + if (d.nd != (3u64: size)) { fail(); }; + if (d.dp != 3) { fail(); }; + if (d.digits[0] != 2u8) { fail(); }; + if (d.digits[1] != 4u8) { fail(); }; + if (d.digits[2] != 6u8) { fail(); }; + + // nd == 0 no-op (early-return arm of leftshift). + let z: decimal; + leftshift(&z, 1u32); + if (z.nd != (0u64: size)) { fail(); }; +}; + +// ---- rightshift ----------------------------------------------------- +// ref/hare/strconv/decimal.ha:93. + +@test fn rightshift_small_cases() void = { + // Shift "10" (dp=2) right by 1 → "5" (dp=1). + let d: decimal; + let vals: [2]u8; + vals[0] = 1u8; vals[1] = 0u8; + seed(&d, vals[0:2], 2); + rightshift(&d, 1u32); + if (d.nd != (1u64: size)) { fail(); }; + if (d.dp != 1) { fail(); }; + if (d.digits[0] != 5u8) { fail(); }; +}; + +// ---- decimal_shift -------------------------------------------------- +// ref/hare/strconv/decimal.ha:138. + +@test fn decimal_shift_cases() void = { + // k=0 no-op. + let d: decimal; + let v3: [3]u8; v3[0] = 1u8; v3[1] = 2u8; v3[2] = 3u8; + seed(&d, v3[0:3], 3); + decimal_shift(&d, 0); + if (d.nd != (3u64: size)) { fail(); }; + if (d.dp != 3) { fail(); }; + if (d.digits[0] != 1u8) { fail(); }; + + // k=2 small positive: 123 << 2 = 492 → digits 4,9,2; dp=3. + seed(&d, v3[0:3], 3); + decimal_shift(&d, 2); + if (d.dp != 3) { fail(); }; + if (d.digits[0] != 4u8) { fail(); }; + if (d.digits[1] != 9u8) { fail(); }; + if (d.digits[2] != 2u8) { fail(); }; + + // k>maxshift triggers the break-up loop. Exact digits are checked + // transitively by stof in fold-4; verify dp grew. + let v: [3]u8; v[0] = 1u8; v[1] = 0u8; v[2] = 0u8; + seed(&d, v[0:3], 3); + decimal_shift(&d, 70); + if (d.dp <= 3) { fail(); }; +}; + +// ---- should_round_up + round + decimal_round ------------------------ +// ref/hare/strconv/decimal.ha:155,162,188. + +@test fn round_cases() void = { + // rounddown — "1234" rounded to 2 digits → "12". + let d: decimal; + let v: [4]u8; v[0] = 1u8; v[1] = 2u8; v[2] = 3u8; v[3] = 4u8; + seed(&d, v[0:4], 4); + round(&d, (2u32: uint)); + if (d.nd != (2u64: size)) { fail(); }; + if (d.digits[0] != 1u8) { fail(); }; + if (d.digits[1] != 2u8) { fail(); }; + + // roundup — "1259" rounded to 2 digits → "13" (digit[2]==5 → + // should_round_up, digit[1]=2 → +1 → digit[1]=3). + let r: [4]u8; r[0] = 1u8; r[1] = 2u8; r[2] = 5u8; r[3] = 9u8; + seed(&d, r[0:4], 4); + round(&d, (2u32: uint)); + if (d.nd != (2u64: size)) { fail(); }; + if (d.digits[0] != 1u8) { fail(); }; + if (d.digits[1] != 3u8) { fail(); }; + + // roundup with all-9s carry — "999" rounded to 2 digits → "1" + // with dp incremented. + let nines: [3]u8; nines[0] = 9u8; nines[1] = 9u8; nines[2] = 9u8; + seed(&d, nines[0:3], 3); + round(&d, (2u32: uint)); + if (d.digits[0] != 1u8) { fail(); }; + if (d.nd != (1u64: size)) { fail(); }; + if (d.dp != 4) { fail(); }; +}; + +@test fn decimal_round_cases() void = { + // dp<0 → 0. + let d: decimal; + let v: [3]u8; v[0] = 1u8; v[1] = 2u8; v[2] = 3u8; + seed(&d, v[0:3], -1); + if (decimal_round(&d) != 0u64) { fail(); }; + + // nd==0 → 0. + let z: decimal; + z.dp = 5; + if (decimal_round(&z) != 0u64) { fail(); }; + + // dp>18 → ~0u64 (all-bits-set). + let big: decimal; + let one: [1]u8; one[0] = 1u8; + seed(&big, one[0:1], 19); + let zero_u: u64 = 0u64; + if (decimal_round(&big) != ~zero_u) { fail(); }; + + // "123" with dp=3 → 123. + seed(&d, v[0:3], 3); + if (decimal_round(&d) != 123u64) { fail(); }; + + // "12" with dp=4 → 1200 (multiply by 10 for each missing digit). + let two: [2]u8; two[0] = 1u8; two[1] = 2u8; + seed(&d, two[0:2], 4); + if (decimal_round(&d) != 1200u64) { fail(); }; + + // "126" with dp=2 → 13 (rounded up via should_round_up at dp=2: + // digit[2]==6 >= 5). + let r: [3]u8; r[0] = 1u8; r[1] = 2u8; r[2] = 6u8; + seed(&d, r[0:3], 2); + if (decimal_round(&d) != 13u64) { fail(); }; +}; + +export fn main() i32 = { + signalled = 1; trim_cases(); + signalled = 2; leftshift_small_cases(); + signalled = 3; rightshift_small_cases(); + signalled = 4; decimal_shift_cases(); + signalled = 5; round_cases(); + signalled = 6; decimal_round_cases(); + return 0; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 19c7f938..555078c0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2704,6 +2704,321 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +// strconv — arbitrary-precision decimal engine for float↔string +// conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports +// Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 +// references (#121 residual-guard SAFE). +// +// Spelling divergences from Hare (mechanical, ww-side parser shape): +// - Hare `let a = X, b = Y;` → two single `let` statements +// (ww parser doesn't accept comma-separated bindings). +// - Hare `tbl[lo..]` open-ended slice → direct indexing +// `tbl[lo + i]` at point-of-use (equivalent algorithm; no +// allocation, no aliasing). ww `[lo:hi]` uses `:`; `..` form +// is not parsed. +// - Hare `0z`/`1z` size literals → ww has no `z` suffix; pre-bind +// `let SZ_ZERO: size = (0u64: size);` etc. at function entry +// ("hoisted size casts as local consts" — ww `T: type` casts +// embedded inside expressions confuse the parser). +// - Hare `~0u64` typed-suffix literal → ww parser rejects `~` on +// typed-suffix; route via a named zero local + `~zero`. +// - Hare `for (cond; afterthought)` 2-clause → ww 3-clause +// `for (init; cond; post)` (when continue is used; the post +// must run each iteration) or inline-the-afterthought in body +// (when no continue exists in the loop). +// - Hare `fn foo() T = if (cond) {...} else expr;` expression body +// → ww requires a `{}` block body throughout. +// - Hare bare `assert(cond)` builtin → `os.assert(cond, msg)`; +// wwstage cgen has no `assert` intercept (deferred fold). +// +// CGEN class closures consumed (post-prereqs): +// - #131 (4acab6e) — `len(d.digits)` compile-time-folds cs==ww +// - #134 (36bf603) — `d.digits[nd] >= 5u8` picks JAE (unsigned) +// - #133 (3986818) — `d.digits[i] += 1u8` load-op-store BOTH +// stages +// - #135 (ade6840) — `(*d).digits[i]` read+write N_DOT-base addr +// +// Drew CGEN-SAFE invariants: +// - #129: module-level decls here are integer-literal defs only. +// - #128: digits is fundamental [800]u8, zero-init only. +// - #121: zero float ops. +// - Drew watch-item `*d = decimal{...};` reset (line 110 in Hare): +// pointer-deref reset to composite-literal probed cs==ww +// byte-id safe. + +package strconv; + +import os; + +// ref/hare/strconv/decimal.ha:5. +def maxshift: u8 = 60u8; + +// ref/hare/strconv/decimal.ha:6. +def decimal_point_range: u16 = 2047u16; + +// ref/hare/strconv/decimal.ha:8-26. Field layout 1:1. The 800-digit +// bound covers subnormal doubles (min exp -1074, max mantissa 4e16 +// → at most 767 digits; 800 leaves headroom). +export type decimal = struct { + digits: [800]u8, + nd: size, + dp: i32, + negative: bool, + truncated: bool, +}; + +// ref/hare/strconv/decimal.ha:29-33. Strip trailing zeros. +fn trim(d: *decimal) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + for (d.nd > SZ_ZERO && d.digits[d.nd - SZ_ONE] == 0u8) { + d.nd -= SZ_ONE; + }; +}; + +// ref/hare/strconv/decimal.ha:35-55. Compute the digit-count +// increase for a left-shift `shift` (consults left_shift_table + +// pow5_table from stof_data.ww, bb6f840). Uses `continue` so the +// loop stays in 3-clause form for byte-id-correct post-increment. +fn leftshift_newdigits(d: *decimal, shift: u32) u32 = { + shift &= 63u32; + let x_a: u32 = (left_shift_table[shift]: u32); + let x_b: u32 = (left_shift_table[shift + 1u32]: u32); + let nn: u32 = x_a >> 11u32; + let pow5_a: u32 = 0x7FFu32 & x_a; + let pow5_b: u32 = 0x7FFu32 & x_b; + let n: u32 = pow5_b - pow5_a; + for (let i: u32 = 0u32; i < n; i += 1u32) { + let i_sz: size = (i: size); + if (i_sz >= d.nd) { + return nn - 1u32; + } else if (d.digits[i] == pow5_table[pow5_a + i]) { + continue; + } else if (d.digits[i] < pow5_table[pow5_a + i]) { + return nn - 1u32; + } else { + return nn; + }; + }; + return nn; +}; + +// ref/hare/strconv/decimal.ha:57-91. Shift `d` left by k bits. +fn leftshift(d: *decimal, k: u32) void = { + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let MAXSHIFT_U32: u32 = (maxshift: u32); + os.assert(k <= MAXSHIFT_U32, "strconv.leftshift: k > maxshift"); + if (d.nd == (0u64: size)) { return; }; + let nn: u32 = leftshift_newdigits(d, k); + let r: int = (d.nd: int) - 1; + let w: size = (r: size) + (nn: size); + let n: u64 = 0u64; + for (r >= 0) { + n += (d.digits[r]: u64) << kU64; + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + r -= 1; + w -= SZ_ONE; + }; + for (n > 0u64) { + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + w -= SZ_ONE; + }; + d.nd += (nn: size); + if (d.nd > SZ_BOUND) { + d.nd = SZ_BOUND; + }; + d.dp += (nn: i32); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:93-134. Shift `d` right by k bits. +// Two outer Hare 2-clause loops (`for (cond; r += 1)`) are inlined +// as `for (cond) { ... r += SZ_ONE; }` since neither uses continue. +fn rightshift(d: *decimal, k: u32) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let r: size = SZ_ZERO; + let w: size = SZ_ZERO; + let n: u64 = 0u64; + for ((n >> kU64) == 0u64) { + if (r >= d.nd) { + if (n == 0u64) { + d.nd = SZ_ZERO; + return; + }; + for ((n >> kU64) == 0u64) { + n *= 10u64; + r += SZ_ONE; + }; + break; + }; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + d.dp -= (r: i32) - 1; + if (d.dp < -(decimal_point_range: i32)) { + // Drew-watch-item: pointer-deref reset to composite + // literal — probed cs==ww byte-id safe in pre-flight. + *d = decimal { ... }; + return; + }; + let mask: u64 = (1u64 << kU64) - 1u64; + for (r < d.nd) { + let dig: u64 = n >> kU64; + n &= mask; + d.digits[w] = (dig: u8); + w += SZ_ONE; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + for (n > 0u64) { + let dig: u64 = n >> kU64; + n &= mask; + if (w < SZ_BOUND) { + d.digits[w] = (dig: u8); + w += SZ_ONE; + } else if (dig > 0u64) { + d.truncated = true; + }; + n *= 10u64; + }; + d.nd = w; + trim(d); +}; + +// ref/hare/strconv/decimal.ha:138-153. Shift right (k < 0) or left +// (k > 0). Hardware shifts cap at 60 bits without losing top +// digits, so break large shifts into maxshift-sized chunks. +fn decimal_shift(d: *decimal, k: int) void = { + let MAXSHIFT_INT: int = (maxshift: int); + let MAXSHIFT_U32: u32 = (maxshift: u32); + if (d.nd == (0u64: size)) { return; }; + if (k > 0) { + for (k > MAXSHIFT_INT) { + leftshift(d, MAXSHIFT_U32); + k -= MAXSHIFT_INT; + }; + leftshift(d, (k: u32)); + } else if (k < 0) { + for (k < -MAXSHIFT_INT) { + rightshift(d, MAXSHIFT_U32); + k += MAXSHIFT_INT; + }; + rightshift(d, ((-k): u32)); + }; +}; + +// ref/hare/strconv/decimal.ha:155-160. Banker's rounding decision: +// at the exact half (digit==5, no more digits) round to even (the +// preceding digit's low bit decides); past-half rounds up; below- +// half rounds down. Hare's expression-bodied `if` re-shaped as a +// block per ww parser. +fn should_round_up(d: *decimal, nd: uint) bool = { + let nd_sz: size = (nd: size); + let SZ_ONE: size = (1u64: size); + let U_ONE: uint = (1u32: uint); + let U_ZERO: uint = (0u32: uint); + if (nd_sz < d.nd) { + if (d.digits[nd] == 5u8 && (nd_sz + SZ_ONE) == d.nd) { + let lowbit_lit: bool = false; + if (nd > U_ZERO) { + if ((d.digits[nd - U_ONE] & 1u8) != 0u8) { + lowbit_lit = true; + }; + }; + return d.truncated || lowbit_lit; + } else { + return d.digits[nd] >= 5u8; + }; + }; + return false; +}; + +// ref/hare/strconv/decimal.ha:162-166. Round to `nd` digits. +fn round(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + if (should_round_up(d, nd)) { + roundup(d, nd); + } else { + rounddown(d, nd); + }; +}; + +// ref/hare/strconv/decimal.ha:168-172. Truncate to `nd` digits. +fn rounddown(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + d.nd = (nd: size); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:174-186. Round up to `nd` digits; +// propagate carry. If all 9s, the result is a single 1 with the +// decimal point advanced. +fn roundup(d: *decimal, nd: uint) void = { + let SZ_ONE: size = (1u64: size); + if ((nd: size) >= d.nd) { return; }; + for (let i: int = (nd: int) - 1; i >= 0; i -= 1) { + if (d.digits[i] < 9u8) { + d.digits[i] += 1u8; + d.nd = (i: size) + SZ_ONE; + return; + }; + }; + d.digits[0] = 1u8; + d.nd = SZ_ONE; + d.dp += 1; +}; + +// ref/hare/strconv/decimal.ha:188-202. Read `d` as the integer +// rounded to `d.dp` digits. Returns 0 if `d.dp <= 0`; returns +// ~0u64 if `d.dp > 18` (exceeds u64 range). Hare's two 2-clause +// loops (`for (cond; i += 1)`) are inlined per the spelling +// divergence at file top. +fn decimal_round(d: *decimal) u64 = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + if (d.nd == SZ_ZERO || d.dp < 0) { return 0u64; }; + if (d.dp > 18) { + // Hare's `~0u64` doesn't parse on a typed-suffix literal + // in ww; route via a named zero. + let zero: u64 = 0u64; + return ~zero; + }; + let dp_sz: size = ((d.dp: uint): size); + let i: size = SZ_ZERO; + let n: u64 = 0u64; + for (i < dp_sz && i < d.nd) { + n = n * 10u64 + (d.digits[i]: u64); + i += SZ_ONE; + }; + for (i < dp_sz) { + n *= 10u64; + i += SZ_ONE; + }; + if (should_round_up(d, (d.dp: uint))) { + n += 1u64; + }; + return n; +}; + // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index eb723459..5dbb5d3d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -727,6 +727,321 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +// strconv — arbitrary-precision decimal engine for float↔string +// conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports +// Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 +// references (#121 residual-guard SAFE). +// +// Spelling divergences from Hare (mechanical, ww-side parser shape): +// - Hare `let a = X, b = Y;` → two single `let` statements +// (ww parser doesn't accept comma-separated bindings). +// - Hare `tbl[lo..]` open-ended slice → direct indexing +// `tbl[lo + i]` at point-of-use (equivalent algorithm; no +// allocation, no aliasing). ww `[lo:hi]` uses `:`; `..` form +// is not parsed. +// - Hare `0z`/`1z` size literals → ww has no `z` suffix; pre-bind +// `let SZ_ZERO: size = (0u64: size);` etc. at function entry +// ("hoisted size casts as local consts" — ww `T: type` casts +// embedded inside expressions confuse the parser). +// - Hare `~0u64` typed-suffix literal → ww parser rejects `~` on +// typed-suffix; route via a named zero local + `~zero`. +// - Hare `for (cond; afterthought)` 2-clause → ww 3-clause +// `for (init; cond; post)` (when continue is used; the post +// must run each iteration) or inline-the-afterthought in body +// (when no continue exists in the loop). +// - Hare `fn foo() T = if (cond) {...} else expr;` expression body +// → ww requires a `{}` block body throughout. +// - Hare bare `assert(cond)` builtin → `os.assert(cond, msg)`; +// wwstage cgen has no `assert` intercept (deferred fold). +// +// CGEN class closures consumed (post-prereqs): +// - #131 (4acab6e) — `len(d.digits)` compile-time-folds cs==ww +// - #134 (36bf603) — `d.digits[nd] >= 5u8` picks JAE (unsigned) +// - #133 (3986818) — `d.digits[i] += 1u8` load-op-store BOTH +// stages +// - #135 (ade6840) — `(*d).digits[i]` read+write N_DOT-base addr +// +// Drew CGEN-SAFE invariants: +// - #129: module-level decls here are integer-literal defs only. +// - #128: digits is fundamental [800]u8, zero-init only. +// - #121: zero float ops. +// - Drew watch-item `*d = decimal{...};` reset (line 110 in Hare): +// pointer-deref reset to composite-literal probed cs==ww +// byte-id safe. + +package strconv; + +import os; + +// ref/hare/strconv/decimal.ha:5. +def maxshift: u8 = 60u8; + +// ref/hare/strconv/decimal.ha:6. +def decimal_point_range: u16 = 2047u16; + +// ref/hare/strconv/decimal.ha:8-26. Field layout 1:1. The 800-digit +// bound covers subnormal doubles (min exp -1074, max mantissa 4e16 +// → at most 767 digits; 800 leaves headroom). +export type decimal = struct { + digits: [800]u8, + nd: size, + dp: i32, + negative: bool, + truncated: bool, +}; + +// ref/hare/strconv/decimal.ha:29-33. Strip trailing zeros. +fn trim(d: *decimal) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + for (d.nd > SZ_ZERO && d.digits[d.nd - SZ_ONE] == 0u8) { + d.nd -= SZ_ONE; + }; +}; + +// ref/hare/strconv/decimal.ha:35-55. Compute the digit-count +// increase for a left-shift `shift` (consults left_shift_table + +// pow5_table from stof_data.ww, bb6f840). Uses `continue` so the +// loop stays in 3-clause form for byte-id-correct post-increment. +fn leftshift_newdigits(d: *decimal, shift: u32) u32 = { + shift &= 63u32; + let x_a: u32 = (left_shift_table[shift]: u32); + let x_b: u32 = (left_shift_table[shift + 1u32]: u32); + let nn: u32 = x_a >> 11u32; + let pow5_a: u32 = 0x7FFu32 & x_a; + let pow5_b: u32 = 0x7FFu32 & x_b; + let n: u32 = pow5_b - pow5_a; + for (let i: u32 = 0u32; i < n; i += 1u32) { + let i_sz: size = (i: size); + if (i_sz >= d.nd) { + return nn - 1u32; + } else if (d.digits[i] == pow5_table[pow5_a + i]) { + continue; + } else if (d.digits[i] < pow5_table[pow5_a + i]) { + return nn - 1u32; + } else { + return nn; + }; + }; + return nn; +}; + +// ref/hare/strconv/decimal.ha:57-91. Shift `d` left by k bits. +fn leftshift(d: *decimal, k: u32) void = { + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let MAXSHIFT_U32: u32 = (maxshift: u32); + os.assert(k <= MAXSHIFT_U32, "strconv.leftshift: k > maxshift"); + if (d.nd == (0u64: size)) { return; }; + let nn: u32 = leftshift_newdigits(d, k); + let r: int = (d.nd: int) - 1; + let w: size = (r: size) + (nn: size); + let n: u64 = 0u64; + for (r >= 0) { + n += (d.digits[r]: u64) << kU64; + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + r -= 1; + w -= SZ_ONE; + }; + for (n > 0u64) { + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + w -= SZ_ONE; + }; + d.nd += (nn: size); + if (d.nd > SZ_BOUND) { + d.nd = SZ_BOUND; + }; + d.dp += (nn: i32); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:93-134. Shift `d` right by k bits. +// Two outer Hare 2-clause loops (`for (cond; r += 1)`) are inlined +// as `for (cond) { ... r += SZ_ONE; }` since neither uses continue. +fn rightshift(d: *decimal, k: u32) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let r: size = SZ_ZERO; + let w: size = SZ_ZERO; + let n: u64 = 0u64; + for ((n >> kU64) == 0u64) { + if (r >= d.nd) { + if (n == 0u64) { + d.nd = SZ_ZERO; + return; + }; + for ((n >> kU64) == 0u64) { + n *= 10u64; + r += SZ_ONE; + }; + break; + }; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + d.dp -= (r: i32) - 1; + if (d.dp < -(decimal_point_range: i32)) { + // Drew-watch-item: pointer-deref reset to composite + // literal — probed cs==ww byte-id safe in pre-flight. + *d = decimal { ... }; + return; + }; + let mask: u64 = (1u64 << kU64) - 1u64; + for (r < d.nd) { + let dig: u64 = n >> kU64; + n &= mask; + d.digits[w] = (dig: u8); + w += SZ_ONE; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + for (n > 0u64) { + let dig: u64 = n >> kU64; + n &= mask; + if (w < SZ_BOUND) { + d.digits[w] = (dig: u8); + w += SZ_ONE; + } else if (dig > 0u64) { + d.truncated = true; + }; + n *= 10u64; + }; + d.nd = w; + trim(d); +}; + +// ref/hare/strconv/decimal.ha:138-153. Shift right (k < 0) or left +// (k > 0). Hardware shifts cap at 60 bits without losing top +// digits, so break large shifts into maxshift-sized chunks. +fn decimal_shift(d: *decimal, k: int) void = { + let MAXSHIFT_INT: int = (maxshift: int); + let MAXSHIFT_U32: u32 = (maxshift: u32); + if (d.nd == (0u64: size)) { return; }; + if (k > 0) { + for (k > MAXSHIFT_INT) { + leftshift(d, MAXSHIFT_U32); + k -= MAXSHIFT_INT; + }; + leftshift(d, (k: u32)); + } else if (k < 0) { + for (k < -MAXSHIFT_INT) { + rightshift(d, MAXSHIFT_U32); + k += MAXSHIFT_INT; + }; + rightshift(d, ((-k): u32)); + }; +}; + +// ref/hare/strconv/decimal.ha:155-160. Banker's rounding decision: +// at the exact half (digit==5, no more digits) round to even (the +// preceding digit's low bit decides); past-half rounds up; below- +// half rounds down. Hare's expression-bodied `if` re-shaped as a +// block per ww parser. +fn should_round_up(d: *decimal, nd: uint) bool = { + let nd_sz: size = (nd: size); + let SZ_ONE: size = (1u64: size); + let U_ONE: uint = (1u32: uint); + let U_ZERO: uint = (0u32: uint); + if (nd_sz < d.nd) { + if (d.digits[nd] == 5u8 && (nd_sz + SZ_ONE) == d.nd) { + let lowbit_lit: bool = false; + if (nd > U_ZERO) { + if ((d.digits[nd - U_ONE] & 1u8) != 0u8) { + lowbit_lit = true; + }; + }; + return d.truncated || lowbit_lit; + } else { + return d.digits[nd] >= 5u8; + }; + }; + return false; +}; + +// ref/hare/strconv/decimal.ha:162-166. Round to `nd` digits. +fn round(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + if (should_round_up(d, nd)) { + roundup(d, nd); + } else { + rounddown(d, nd); + }; +}; + +// ref/hare/strconv/decimal.ha:168-172. Truncate to `nd` digits. +fn rounddown(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + d.nd = (nd: size); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:174-186. Round up to `nd` digits; +// propagate carry. If all 9s, the result is a single 1 with the +// decimal point advanced. +fn roundup(d: *decimal, nd: uint) void = { + let SZ_ONE: size = (1u64: size); + if ((nd: size) >= d.nd) { return; }; + for (let i: int = (nd: int) - 1; i >= 0; i -= 1) { + if (d.digits[i] < 9u8) { + d.digits[i] += 1u8; + d.nd = (i: size) + SZ_ONE; + return; + }; + }; + d.digits[0] = 1u8; + d.nd = SZ_ONE; + d.dp += 1; +}; + +// ref/hare/strconv/decimal.ha:188-202. Read `d` as the integer +// rounded to `d.dp` digits. Returns 0 if `d.dp <= 0`; returns +// ~0u64 if `d.dp > 18` (exceeds u64 range). Hare's two 2-clause +// loops (`for (cond; i += 1)`) are inlined per the spelling +// divergence at file top. +fn decimal_round(d: *decimal) u64 = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + if (d.nd == SZ_ZERO || d.dp < 0) { return 0u64; }; + if (d.dp > 18) { + // Hare's `~0u64` doesn't parse on a typed-suffix literal + // in ww; route via a named zero. + let zero: u64 = 0u64; + return ~zero; + }; + let dp_sz: size = ((d.dp: uint): size); + let i: size = SZ_ZERO; + let n: u64 = 0u64; + for (i < dp_sz && i < d.nd) { + n = n * 10u64 + (d.digits[i]: u64); + i += SZ_ONE; + }; + for (i < dp_sz) { + n *= 10u64; + i += SZ_ONE; + }; + if (should_round_up(d, (d.dp: uint))) { + n += 1u64; + }; + return n; +}; + // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 71520ed4..3c8763e6 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -727,6 +727,321 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +// strconv — arbitrary-precision decimal engine for float↔string +// conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports +// Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 +// references (#121 residual-guard SAFE). +// +// Spelling divergences from Hare (mechanical, ww-side parser shape): +// - Hare `let a = X, b = Y;` → two single `let` statements +// (ww parser doesn't accept comma-separated bindings). +// - Hare `tbl[lo..]` open-ended slice → direct indexing +// `tbl[lo + i]` at point-of-use (equivalent algorithm; no +// allocation, no aliasing). ww `[lo:hi]` uses `:`; `..` form +// is not parsed. +// - Hare `0z`/`1z` size literals → ww has no `z` suffix; pre-bind +// `let SZ_ZERO: size = (0u64: size);` etc. at function entry +// ("hoisted size casts as local consts" — ww `T: type` casts +// embedded inside expressions confuse the parser). +// - Hare `~0u64` typed-suffix literal → ww parser rejects `~` on +// typed-suffix; route via a named zero local + `~zero`. +// - Hare `for (cond; afterthought)` 2-clause → ww 3-clause +// `for (init; cond; post)` (when continue is used; the post +// must run each iteration) or inline-the-afterthought in body +// (when no continue exists in the loop). +// - Hare `fn foo() T = if (cond) {...} else expr;` expression body +// → ww requires a `{}` block body throughout. +// - Hare bare `assert(cond)` builtin → `os.assert(cond, msg)`; +// wwstage cgen has no `assert` intercept (deferred fold). +// +// CGEN class closures consumed (post-prereqs): +// - #131 (4acab6e) — `len(d.digits)` compile-time-folds cs==ww +// - #134 (36bf603) — `d.digits[nd] >= 5u8` picks JAE (unsigned) +// - #133 (3986818) — `d.digits[i] += 1u8` load-op-store BOTH +// stages +// - #135 (ade6840) — `(*d).digits[i]` read+write N_DOT-base addr +// +// Drew CGEN-SAFE invariants: +// - #129: module-level decls here are integer-literal defs only. +// - #128: digits is fundamental [800]u8, zero-init only. +// - #121: zero float ops. +// - Drew watch-item `*d = decimal{...};` reset (line 110 in Hare): +// pointer-deref reset to composite-literal probed cs==ww +// byte-id safe. + +package strconv; + +import os; + +// ref/hare/strconv/decimal.ha:5. +def maxshift: u8 = 60u8; + +// ref/hare/strconv/decimal.ha:6. +def decimal_point_range: u16 = 2047u16; + +// ref/hare/strconv/decimal.ha:8-26. Field layout 1:1. The 800-digit +// bound covers subnormal doubles (min exp -1074, max mantissa 4e16 +// → at most 767 digits; 800 leaves headroom). +export type decimal = struct { + digits: [800]u8, + nd: size, + dp: i32, + negative: bool, + truncated: bool, +}; + +// ref/hare/strconv/decimal.ha:29-33. Strip trailing zeros. +fn trim(d: *decimal) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + for (d.nd > SZ_ZERO && d.digits[d.nd - SZ_ONE] == 0u8) { + d.nd -= SZ_ONE; + }; +}; + +// ref/hare/strconv/decimal.ha:35-55. Compute the digit-count +// increase for a left-shift `shift` (consults left_shift_table + +// pow5_table from stof_data.ww, bb6f840). Uses `continue` so the +// loop stays in 3-clause form for byte-id-correct post-increment. +fn leftshift_newdigits(d: *decimal, shift: u32) u32 = { + shift &= 63u32; + let x_a: u32 = (left_shift_table[shift]: u32); + let x_b: u32 = (left_shift_table[shift + 1u32]: u32); + let nn: u32 = x_a >> 11u32; + let pow5_a: u32 = 0x7FFu32 & x_a; + let pow5_b: u32 = 0x7FFu32 & x_b; + let n: u32 = pow5_b - pow5_a; + for (let i: u32 = 0u32; i < n; i += 1u32) { + let i_sz: size = (i: size); + if (i_sz >= d.nd) { + return nn - 1u32; + } else if (d.digits[i] == pow5_table[pow5_a + i]) { + continue; + } else if (d.digits[i] < pow5_table[pow5_a + i]) { + return nn - 1u32; + } else { + return nn; + }; + }; + return nn; +}; + +// ref/hare/strconv/decimal.ha:57-91. Shift `d` left by k bits. +fn leftshift(d: *decimal, k: u32) void = { + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let MAXSHIFT_U32: u32 = (maxshift: u32); + os.assert(k <= MAXSHIFT_U32, "strconv.leftshift: k > maxshift"); + if (d.nd == (0u64: size)) { return; }; + let nn: u32 = leftshift_newdigits(d, k); + let r: int = (d.nd: int) - 1; + let w: size = (r: size) + (nn: size); + let n: u64 = 0u64; + for (r >= 0) { + n += (d.digits[r]: u64) << kU64; + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + r -= 1; + w -= SZ_ONE; + }; + for (n > 0u64) { + let quo: u64 = n / 10u64; + let rem: u64 = n - 10u64 * quo; + if (w < SZ_BOUND) { + d.digits[w] = (rem: u8); + } else if (rem != 0u64) { + d.truncated = true; + }; + n = quo; + w -= SZ_ONE; + }; + d.nd += (nn: size); + if (d.nd > SZ_BOUND) { + d.nd = SZ_BOUND; + }; + d.dp += (nn: i32); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:93-134. Shift `d` right by k bits. +// Two outer Hare 2-clause loops (`for (cond; r += 1)`) are inlined +// as `for (cond) { ... r += SZ_ONE; }` since neither uses continue. +fn rightshift(d: *decimal, k: u32) void = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + let SZ_BOUND: size = (len(d.digits): size); + let kU64: u64 = (k: u64); + let r: size = SZ_ZERO; + let w: size = SZ_ZERO; + let n: u64 = 0u64; + for ((n >> kU64) == 0u64) { + if (r >= d.nd) { + if (n == 0u64) { + d.nd = SZ_ZERO; + return; + }; + for ((n >> kU64) == 0u64) { + n *= 10u64; + r += SZ_ONE; + }; + break; + }; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + d.dp -= (r: i32) - 1; + if (d.dp < -(decimal_point_range: i32)) { + // Drew-watch-item: pointer-deref reset to composite + // literal — probed cs==ww byte-id safe in pre-flight. + *d = decimal { ... }; + return; + }; + let mask: u64 = (1u64 << kU64) - 1u64; + for (r < d.nd) { + let dig: u64 = n >> kU64; + n &= mask; + d.digits[w] = (dig: u8); + w += SZ_ONE; + n = n * 10u64 + (d.digits[r]: u64); + r += SZ_ONE; + }; + for (n > 0u64) { + let dig: u64 = n >> kU64; + n &= mask; + if (w < SZ_BOUND) { + d.digits[w] = (dig: u8); + w += SZ_ONE; + } else if (dig > 0u64) { + d.truncated = true; + }; + n *= 10u64; + }; + d.nd = w; + trim(d); +}; + +// ref/hare/strconv/decimal.ha:138-153. Shift right (k < 0) or left +// (k > 0). Hardware shifts cap at 60 bits without losing top +// digits, so break large shifts into maxshift-sized chunks. +fn decimal_shift(d: *decimal, k: int) void = { + let MAXSHIFT_INT: int = (maxshift: int); + let MAXSHIFT_U32: u32 = (maxshift: u32); + if (d.nd == (0u64: size)) { return; }; + if (k > 0) { + for (k > MAXSHIFT_INT) { + leftshift(d, MAXSHIFT_U32); + k -= MAXSHIFT_INT; + }; + leftshift(d, (k: u32)); + } else if (k < 0) { + for (k < -MAXSHIFT_INT) { + rightshift(d, MAXSHIFT_U32); + k += MAXSHIFT_INT; + }; + rightshift(d, ((-k): u32)); + }; +}; + +// ref/hare/strconv/decimal.ha:155-160. Banker's rounding decision: +// at the exact half (digit==5, no more digits) round to even (the +// preceding digit's low bit decides); past-half rounds up; below- +// half rounds down. Hare's expression-bodied `if` re-shaped as a +// block per ww parser. +fn should_round_up(d: *decimal, nd: uint) bool = { + let nd_sz: size = (nd: size); + let SZ_ONE: size = (1u64: size); + let U_ONE: uint = (1u32: uint); + let U_ZERO: uint = (0u32: uint); + if (nd_sz < d.nd) { + if (d.digits[nd] == 5u8 && (nd_sz + SZ_ONE) == d.nd) { + let lowbit_lit: bool = false; + if (nd > U_ZERO) { + if ((d.digits[nd - U_ONE] & 1u8) != 0u8) { + lowbit_lit = true; + }; + }; + return d.truncated || lowbit_lit; + } else { + return d.digits[nd] >= 5u8; + }; + }; + return false; +}; + +// ref/hare/strconv/decimal.ha:162-166. Round to `nd` digits. +fn round(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + if (should_round_up(d, nd)) { + roundup(d, nd); + } else { + rounddown(d, nd); + }; +}; + +// ref/hare/strconv/decimal.ha:168-172. Truncate to `nd` digits. +fn rounddown(d: *decimal, nd: uint) void = { + if ((nd: size) >= d.nd) { return; }; + d.nd = (nd: size); + trim(d); +}; + +// ref/hare/strconv/decimal.ha:174-186. Round up to `nd` digits; +// propagate carry. If all 9s, the result is a single 1 with the +// decimal point advanced. +fn roundup(d: *decimal, nd: uint) void = { + let SZ_ONE: size = (1u64: size); + if ((nd: size) >= d.nd) { return; }; + for (let i: int = (nd: int) - 1; i >= 0; i -= 1) { + if (d.digits[i] < 9u8) { + d.digits[i] += 1u8; + d.nd = (i: size) + SZ_ONE; + return; + }; + }; + d.digits[0] = 1u8; + d.nd = SZ_ONE; + d.dp += 1; +}; + +// ref/hare/strconv/decimal.ha:188-202. Read `d` as the integer +// rounded to `d.dp` digits. Returns 0 if `d.dp <= 0`; returns +// ~0u64 if `d.dp > 18` (exceeds u64 range). Hare's two 2-clause +// loops (`for (cond; i += 1)`) are inlined per the spelling +// divergence at file top. +fn decimal_round(d: *decimal) u64 = { + let SZ_ZERO: size = (0u64: size); + let SZ_ONE: size = (1u64: size); + if (d.nd == SZ_ZERO || d.dp < 0) { return 0u64; }; + if (d.dp > 18) { + // Hare's `~0u64` doesn't parse on a typed-suffix literal + // in ww; route via a named zero. + let zero: u64 = 0u64; + return ~zero; + }; + let dp_sz: size = ((d.dp: uint): size); + let i: size = SZ_ZERO; + let n: u64 = 0u64; + for (i < dp_sz && i < d.nd) { + n = n * 10u64 + (d.digits[i]: u64); + i += SZ_ONE; + }; + for (i < dp_sz) { + n *= 10u64; + i += SZ_ONE; + }; + if (should_round_up(d, (d.dp: uint))) { + n += 1u64; + }; + return n; +}; + // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's diff --git a/test/wcc/922_decimal_run.c b/test/wcc/922_decimal_run.c new file mode 100644 index 00000000..b47a762a --- /dev/null +++ b/test/wcc/922_decimal_run.c @@ -0,0 +1,57 @@ +/* + * 922_decimal_run — execute the lib/strconv/test/decimaltest fixture + * under the C-side `ww run` driver and assert exit 0. + * + * Same thin-wrapper shape as 967_bytes_run / 968_utf8_run / 979_hex_run: + * decimaltest.ww carries its own `export fn main()` that drives the + * @test fns and signals which case failed via the exit code (signalled + * + 10, so cases 1..6 → exit 11..16). + * + * The test fixture lives in lib/strconv/test/ (not lib/strconv/) so + * `import strconv` resolves to the lib/strconv DIRECTORY rather than + * shadowing on the strconv.ww FILE — same-dir resolution would pull + * in only strconv.ww and miss decimal.ww + stof_data.ww. From the + * test/ subdir, the search-path lookup falls through srcd and reaches + * the libdir's directory hit, expanding the full strconv package. + */ +#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/decimaltest.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, "decimal_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("decimal_run: %s ok\n", src); + return 0; +}