Add a sub-bullet to the 8-spelling-divergences list documenting two
implementation sub-cases reviewer-fold3 surfaced during the 07e57ff
decimal.ha port: (1) `i_sz` per-iteration size-cast hoist inside
leftshift_newdigits' for-loop (decimal.ww:93); (2) `lowbit_lit`
stepwise boolean decomposition in should_round_up (decimal.ww:242)
dodging ww parser precedence on Hare's `(nd > 0 && d.digits[nd - 1]
& 1 != 0)` (ref/hare/strconv/decimal.ha:158). Both are in-file
instances of the documented hoist+restructure patterns — rule-9
doc-completeness, not new divergence. Combined.ww regen for
lib/strconv (compiler-imported into w6c + wwdump + smoke) uses the
build's include paths (`-I lib/ww -I lib/ww/lex -I lib/ww/parse
-I selfhost/cmd/wcc`) for transitive import closure; bare `ww build`
without these flags produces truncated output (reviewer-32c2 +
reviewer-fold3 both hit this).
322 lines
9.7 KiB
Plaintext
322 lines
9.7 KiB
Plaintext
// 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).
|
|
// - In-file instances of the above hoist pattern: `i_sz` (line 93)
|
|
// hoists a per-iteration size cast out of a for-loop comparison
|
|
// (bullet 3 sub-case — the size-cast hoist applied inside a loop
|
|
// body, not just at function entry); `lowbit_lit` (line 242)
|
|
// decomposes Hare's `(nd > 0 && d.digits[nd - 1] & 1 != 0)` into
|
|
// a stepwise boolean local to dodge ww parser precedence on mixed
|
|
// `&` / `&&` / `!=` within a single expression.
|
|
//
|
|
// 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;
|
|
};
|