lib/test: Go external-test packages (<mod>_test); retire decimaltest (#16 PREP-a)

30 lib test files: package <mod> -> <mod>_test, the sanctioned Go-over-Hare
departure (CLAUDE.md rule-9 carve-out; white-box testing deferred, not
forbidden). decimaltest retired per .ai/rob-16-decimaltest-ruling.md: Hare
ships no decimal_test.ha (engine covered transitively); coverage migrated
losslessly into stoftest rows (all-9s carry, nd>19 pure-decimal) + ftostest
f64_roundtrip mirroring ref/hare strconv ftos_test.ha tcsf64; k=0 micro-gap
documented at site. regex_test keeps its white-box internal probes under a
documented rule-7 divergence (rehome = task #9). 922_decimal_run + its 989
byte-id row removed with the fixture.
This commit is contained in:
2026-06-10 12:11:11 +09:00
parent b795c320c9
commit 99e3393048
35 changed files with 77 additions and 306 deletions

View File

@@ -1,212 +0,0 @@
// 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

@@ -20,7 +20,7 @@
// DIRECTORY (full package), not the strconv.ww FILE — same rationale as
// stoftest/decimaltest.
package strconv;
package strconv_test;
import strconv;
import os;
@@ -43,6 +43,17 @@ fn chk(n: f64, want: str) bool = {
return streq(f64tos(n), want);
};
// rt64 — bit-exact round-trip: render then parse back must reproduce the
// exact bits. The parse-back re-runs strconv's decimal slow path.
fn rt64(x: f64) bool = {
match (stof64(f64tos(x), base.DEC)) {
case let v: f64 => { return math.f64bits(v) == math.f64bits(x); };
case let e: invalid => { return false; };
case let e: overflow => { return false; };
};
return false;
};
// ftos_test.ha:57-64 (G/void/NONE-equivalent) — fixed-point renders.
@test fn f64tos_fixed() void = {
if (!chk(13.37, "13.37")) { fail(); };
@@ -151,6 +162,21 @@ fn chkf32(n: f32, want: str) bool = {
if (!chkf32(nan, "nan")) { fail(); };
};
// ftos_test.ha:196-204 (tcsf64) — Hare's f64-exclusive extremes, here as a
// bit-exact ROUND-TRIP (f64bits(stof64(f64tos(x)))==f64bits(x)): the parse-
// back re-runs strconv's decimal engine end-to-end, preserving the coverage
// the retired white-box decimaltest.ww gave directly (#16). Extremes built
// via f64frombits (ww math has no F64_MIN_*/MAX_NORMAL const); exact and
// 17-digit-halfway values as literals.
@test fn f64_roundtrip() void = {
if (!rt64(9007199254740991.0)) { fail(); }; // 2^53-1
if (!rt64(90071992547409915.0)) { fail(); }; // 17-digit halfway pair
if (!rt64(90071992547409925.0)) { fail(); };
if (!rt64(math.f64frombits(0x0000000000000001u64))) { fail(); }; // F64_MIN_SUBNORMAL (5e-324)
if (!rt64(math.f64frombits(0x0010000000000000u64))) { fail(); }; // F64_MIN_NORMAL
if (!rt64(math.f64frombits(0x7FEFFFFFFFFFFFFFu64))) { fail(); }; // F64_MAX_NORMAL
};
export fn main() i32 = {
signalled = 1; f64tos_fixed();
signalled = 2; f64tos_scientific();
@@ -159,6 +185,7 @@ export fn main() i32 = {
signalled = 5; f32tos_scientific();
signalled = 6; f32tos_extremes();
signalled = 7; f32tos_special();
signalled = 8; f64_roundtrip();
os.exit(0);
return 0;
};

View File

@@ -19,7 +19,7 @@
// literal form for the expectation side); the original Hare radix form
// is noted inline.
package strconv;
package strconv_test;
import strconv;
import os;

View File

@@ -15,7 +15,7 @@
// DIRECTORY (full package: strconv.ww + decimal.ww + stof_data.ww +
// stof.ww), not the strconv.ww FILE — same rationale as decimaltest.
package strconv;
package strconv_test;
import strconv;
import os;
@@ -123,6 +123,18 @@ fn nan32(s: str) bool = {
if (!chk64("-1e-324", base.DEC, nzero)) { fail(); };
if (!chk64("5e-324", base.DEC, subn)) { fail(); };
// Decimal-engine edge probes migrated here when the white-box
// decimaltest.ww retired (#16): the external-test idiom can't reach
// strconv's unexported decimal fns, so these two black-box rows pin
// the only edges not already guaranteed transitively. C1: all-9s
// round-up carry + decimal_round dp>18 boundary. C2: nd>19 skips
// eisel-lemire, forcing the pure-decimal slow path (extra trim).
// Micro-gap (rule-7, honest): decimal_shift's k=0 early-return no-op
// is a trivial guard, not reached transitively by any slow-path input
// — consciously un-migrated, not silently dropped.
if (!chk64("9999999999999999", base.DEC, 1.0e16)) { fail(); };
if (!chk64("100000000000000000001", base.DEC, 1.0e20)) { fail(); };
if (inv64("") != 0) { fail(); };
if (inv64("0ZO") != 1) { fail(); };
if (inv64("1.23ezz") != 5) { fail(); };