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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user