lib/strconv: decimal arbitrary-precision arithmetic (#106 fold-3)

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).
This commit is contained in:
2026-05-27 00:44:16 +09:00
parent d960971c6e
commit 07e57ff9a6
7 changed files with 1535 additions and 2 deletions

View File

@@ -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/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \
selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \
selfhost/cmd/wcc/cgendecl.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 \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/wwdump_ww.d @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/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \
selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \
selfhost/cmd/wcc/cgendecl.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 \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/w6c_ww.d @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_intdiv_signed \
$(BIN)/test_strings_run \ $(BIN)/test_strings_run \
$(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_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_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
$(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \
$(BIN)/test_adler32_run $(BIN)/test_crc16_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) $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(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)/test_strings_run: test/wcc/966_strings_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(CC) $(CFLAGS) -o $@ $<

314
lib/strconv/decimal.ww Normal file
View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -2704,6 +2704,321 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = {
return frombytes(buf); 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 // 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 // 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 // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's

View File

@@ -727,6 +727,321 @@ export fn exists(path: str) bool = {
return r >= 0i64; 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 // 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 // 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 // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's

View File

@@ -727,6 +727,321 @@ export fn exists(path: str) bool = {
return r >= 0i64; 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 // 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 // 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 // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
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;
}