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

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

View File

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

View File

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