/* * 952_floats_run — runtime regression net for lib/math/floats fold-1 * (f64bits, f64frombits, isnan, isinf, signf64, absf64, copysignf64, * ispositivef64, isnegativef64) plus the fold-2a decompose step * (issubnormalf64, normalizef64, frexpf64). The classify/sign/bits surface rides * the f64-codegen paths fixed by #96 (deref-load -> MOVSD/X0) and #97 * (compare consults PF for NaN); normalizef64 additionally rides the #102 * 16B-tuple-from-call receive (its (f64, i64) return) and the f64 multiply * on the subnormal path. Both stages emit byte-identical asm, so the * 990-997 byte-id gates can't catch a reintroduction — only an * executed-and-checked runtime probe can. This file is that probe. * * frexpf64 rides two now-landed cgen fixes: the `n == 0f64` zero guard * (#103 — a no-decimal f64 literal now reaches XMM) and its (f64, i64) * tuple return + .0/.1 destructure (#105 — the tuple f64-word read). The * frexpf64(0.0) row is the key regressor: it exercises both the early * return AND the tuple receive. As with the normalizef64 rows, tuple * fields are spilled to a let before any f64-literal comparison (a * tuple-field f64 compared directly against a literal `r.0 == 0.0` was the * #105 mis-load); the .1 i64 field is compared in place. * * Table-driven like 951_f64cgen_run: each row is a self-contained ww * program importing math; the C-side cstage `ww build -I lib` compiles * it, we run the binary and assert the exit code. Self-checking rows * return 0 on pass / a locator code on the first failing assertion; * value-propagation rows return a computed result compared to want_exit. * * cstage-only by design (mirrors 951 + 969_checked_run): `ww_ww run` is * broken (#95) and per-program wwstage byte-id is the 990-997 gates' job. * * NaN/INF operands are built at runtime through opaque zero()/one() fns * so the checker can't const-fold them away (matches 951's #97 rows). */ #include #include #include #include #include #include static int runwait(const char *cmd) { int rc = system(cmd); if (rc == -1) return -1; if (WIFEXITED(rc)) return WEXITSTATUS(rc); return 1; } struct row { const char *src; int want_exit; }; static const struct row rows[] = { /* f64bits + f64frombits round-trip. 0x3FF0000000000000 == 1.0, * 0x4045000000000000 == 42.0. Round-trip through tobits/frombits * must preserve the bit pattern (both u64 and f64 deref-loads). */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " if (math.f64bits(1.0) != 0x3FF0000000000000u64) { return 1; };\n" " if (math.f64bits(0.0) != 0u64) { return 2; };\n" " let rt: f64 = math.f64frombits(math.f64bits(-2.5));\n" " if (rt != -2.5) { return 3; };\n" " if (math.f64frombits(0x4045000000000000u64) != 42.0) { return 4; };\n" " return 0;\n" "};\n", 0 }, /* f64frombits value propagation: a u64 bit pattern reinterpreted as * f64 then truncated to i32. On #96 the deref strands the value in a * GPR and the i32 cast reads stale X0. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " return math.f64frombits(0x4045000000000000u64): i32;\n" "};\n", 42 }, /* isnan + isinf. Runtime NaN/INF via opaque zero()/one() so the * checker can't const-fold. isnan rides #97 (self-inequality must * be true for NaN); isinf rides #96 (f64bits deref-load). */ { "package main;\n" "import math;\n" "fn zero() f64 = { return 0.0; };\n" "fn one() f64 = { return 1.0; };\n" "export fn main() i32 = {\n" " let nan: f64 = zero() / zero();\n" " if (!math.isnan(nan)) { return 1; };\n" " if (math.isnan(1.0)) { return 2; };\n" " let inf: f64 = one() / zero();\n" " if (!math.isinf(inf)) { return 3; };\n" " if (!math.isinf(-inf)) { return 4; };\n" " if (math.isinf(nan)) { return 5; };\n" " if (math.isinf(1.23)) { return 6; };\n" " return 0;\n" "};\n", 0 }, /* signf64 + ispositivef64 + isnegativef64. +0.0 has a clear sign * bit so signf64(0.0) == 1 (zero is positive). */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " if (math.signf64(3.0) != 1i64) { return 1; };\n" " if (math.signf64(-3.0) != -1i64) { return 2; };\n" " if (math.signf64(0.0) != 1i64) { return 3; };\n" " if (!math.ispositivef64(3.0)) { return 4; };\n" " if (math.ispositivef64(-3.0)) { return 5; };\n" " if (!math.isnegativef64(-3.0)) { return 6; };\n" " if (math.isnegativef64(3.0)) { return 7; };\n" " return 0;\n" "};\n", 0 }, /* absf64 + copysignf64. absf64(NaN) stays NaN (early return before * the bit-mask). copysign transfers y's sign onto x's magnitude. */ { "package main;\n" "import math;\n" "fn zero() f64 = { return 0.0; };\n" "export fn main() i32 = {\n" " if (math.absf64(-3.0) != 3.0) { return 1; };\n" " if (math.absf64(3.0) != 3.0) { return 2; };\n" " let nan: f64 = zero() / zero();\n" " if (!math.isnan(math.absf64(nan))) { return 3; };\n" " if (math.copysignf64(3.0, -1.0) != -3.0) { return 4; };\n" " if (math.copysignf64(-3.0, 1.0) != 3.0) { return 5; };\n" " if (math.copysignf64(3.0, 1.0) != 3.0) { return 6; };\n" " return 0;\n" "};\n", 0 }, /* absf64 value propagation: f64 result truncated to i32 (rides #96 * f64-return through X0). */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " return math.absf64(-7.0): i32;\n" "};\n", 7 }, /* issubnormalf64: normals and zero are not subnormal; the smallest * positive f64 (bits == 1, ~5e-324) is. Built via f64frombits so the * checker can't fold a subnormal literal (which it would flush). */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " if (math.issubnormalf64(16.0)) { return 1; };\n" " if (math.issubnormalf64(0.0)) { return 2; };\n" " if (math.issubnormalf64(1.0)) { return 3; };\n" " let s: f64 = math.f64frombits(1u64);\n" " if (!math.issubnormalf64(s)) { return 4; };\n" " return 0;\n" "};\n", 0 }, /* normalizef64 on a normal value returns (n, 0). The (f64, i64) tuple * rides the #102 16B-tuple-from-call receive; r.0 is spilled to a let * before comparison (see header). */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " const r = math.normalizef64(16.0);\n" " const m: f64 = r.0;\n" " if (m != 16.0) { return 1; };\n" " if (r.1 != 0i64) { return 2; };\n" " return 0;\n" "};\n", 0 }, /* normalizef64 on a subnormal multiplies by 2^52 (f64 multiply inside a * tuple return) and returns exp -52; the result is no longer subnormal * and is nonzero. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " let s: f64 = math.f64frombits(1u64);\n" " const r = math.normalizef64(s);\n" " if (r.1 != -52i64) { return 1; };\n" " const m: f64 = r.0;\n" " if (math.issubnormalf64(m)) { return 2; };\n" " if (m == 0.0) { return 3; };\n" " return 0;\n" "};\n", 0 }, /* normalizef64 value propagation: normal-path .0 truncated to i32 * (spilled to a let to dodge the tuple-field-compare mis-load; the * cast itself reads the field fine). */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " const r = math.normalizef64(42.0);\n" " const m: f64 = r.0;\n" " return m: i32;\n" "};\n", 42 }, /* frexpf64 exact (mantissa, exp): 8.0 -> (0.5, 4), 1.0 -> (0.5, 1), * 0.75 -> (0.75, 0), and the key 0.0 -> (0.0, 0) early-return row * (rides #103 `n == 0f64` + #105 tuple receive). Tuple .0 spilled to a * let before the f64-literal compare; .1 compared in place. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " const a = math.frexpf64(8.0);\n" " const am: f64 = a.0;\n" " if (am != 0.5) { return 1; };\n" " if (a.1 != 4i64) { return 2; };\n" " const b = math.frexpf64(1.0);\n" " const bm: f64 = b.0;\n" " if (bm != 0.5) { return 3; };\n" " if (b.1 != 1i64) { return 4; };\n" " const c = math.frexpf64(0.0);\n" " const cm: f64 = c.0;\n" " if (cm != 0.0) { return 5; };\n" " if (c.1 != 0i64) { return 6; };\n" " const d = math.frexpf64(0.75);\n" " const dm: f64 = d.0;\n" " if (dm != 0.75) { return 7; };\n" " if (d.1 != 0i64) { return 8; };\n" " return 0;\n" "};\n", 0 }, /* frexpf64 mantissa range + reconstruction: for a nonzero input the * mantissa is in [0.5, 1) and mantissa * 2^exp reproduces n. 12.0 -> * (0.75, 4); 0.75 * 16 (== 2^4) == 12. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " const r = math.frexpf64(12.0);\n" " const m: f64 = r.0;\n" " if (m < 0.5) { return 1; };\n" " if (m >= 1.0) { return 2; };\n" " if (r.1 != 4i64) { return 3; };\n" " if (m * 16.0 != 12.0) { return 4; };\n" " return 0;\n" "};\n", 0 }, /* frexpf64 value propagation: 1024.0 == 2^10 -> (0.5, 11); the i64 exp * field cast to i32 propagates as 11. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " const r = math.frexpf64(1024.0);\n" " return r.1: i32;\n" "};\n", 11 }, /* --- strconv-foundation fold-1: F32 width constants. Mirror * ref/hare/math/floats.ha:27,30,33,43,46. The shape values (23/8/ * 127, 0x7FFFFF, 0xFF) are facts of IEEE 754 binary32 — any drift * here means F32_* def-folding broke. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " if (math.F32_MANTISSA_BITS != 23u32) { return 1; };\n" " if (math.F32_EXPONENT_BITS != 8u32) { return 2; };\n" " if (math.F32_EXPONENT_BIAS != 127u32) { return 3; };\n" " if (math.F32_MANTISSA_MASK != 0x7FFFFFu32) { return 4; };\n" " if (math.F32_EXPONENT_MASK != 0xFFu32) { return 5; };\n" " return 0;\n" "};\n", 0 }, /* f32bits / f32frombits round-trip. Mirror * ref/hare/math/floats.ha:8,14 + ref/hare/math/+test/floats_test.ha:4 * (the f32 leg). 1.0f32 -> 0x3F800000, 2.0f32 -> 0x40000000, * -1.0f32 -> 0xBF800000, 0.0f32 -> 0u32. Rides the f32-literal * materialise path (#104 fold-1) and the f32 ptr-deref read/write. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " if (math.f32bits(1.0f32) != 0x3F800000u32) { return 1; };\n" " if (math.f32bits(2.0f32) != 0x40000000u32) { return 2; };\n" " if (math.f32bits(-1.0f32) != 0xBF800000u32) { return 3; };\n" " if (math.f32bits(0.0f32) != 0u32) { return 4; };\n" " if (math.f32frombits(0x3F800000u32) != 1.0f32) { return 5; };\n" " if (math.f32frombits(0x40000000u32) != 2.0f32) { return 6; };\n" " if (math.f32frombits(0xBF800000u32) != -1.0f32) { return 7; };\n" " if (math.f32frombits(0u32) != 0.0f32) { return 8; };\n" " let v: f32 = 123456.0f32;\n" " if (math.f32frombits(math.f32bits(v)) != v) { return 9; };\n" " return 0;\n" "};\n", 0 }, /* f32bits value propagation: bit pattern 0x42280000 == 42.0f32; the * u32 cast to i32 propagates as 42. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " return math.f32frombits(0x42280000u32): i32;\n" "};\n", 42 }, /* floatinfo struct: f64-shape instance, address-of must round-trip * through a *floatinfo pointer parameter — that's the call shape * stof's eisel_lemire (`f: *floatinfo`) uses in fold-4. Mirrors * ref/hare/math/floats.ha:103. Stack-local because module-level * struct lets-with-init don't lower yet (gap reported with lead). * Field assignments rather than a struct literal: ww's N_DOT-qualified * `math.floatinfo { ... }` literal trips the cross-module resolver * (#16/#17, task list #12). */ { "package main;\n" "import math;\n" "fn readbits(p: *math.floatinfo) u64 = { return p.mantbits; };\n" "export fn main() i32 = {\n" " let info: math.floatinfo;\n" " info.mantbits = 52u64;\n" " info.expbits = 11u64;\n" " info.expbias = 1023;\n" " info.mantmask = 0xFFFFFFFFFFFFFu64;\n" " info.expmask = 0x7FFu64;\n" " if (info.mantbits != 52u64) { return 1; };\n" " if (info.expbits != 11u64) { return 2; };\n" " if (info.expbias != 1023) { return 3; };\n" " if (info.mantmask != 0xFFFFFFFFFFFFFu64) { return 4; };\n" " if (info.expmask != 0x7FFu64) { return 5; };\n" " if (readbits(&info) != 52u64) { return 6; };\n" " return 0;\n" "};\n", 0 }, /* floatinfo f32-shape instance. Same fold as above; verifies the * struct type is width-agnostic (Hare exports both f64info and * f32info, ref/hare/math/floats.ha:117,126). */ { "package main;\n" "import math;\n" "fn readbits(p: *math.floatinfo) u64 = { return p.mantbits; };\n" "export fn main() i32 = {\n" " let info: math.floatinfo;\n" " info.mantbits = 23u64;\n" " info.expbits = 8u64;\n" " info.expbias = 127;\n" " info.mantmask = 0x7FFFFFu64;\n" " info.expmask = 0xFFu64;\n" " if (info.mantbits != 23u64) { return 1; };\n" " if (info.expbias != 127) { return 2; };\n" " if (readbits(&info) != 23u64) { return 3; };\n" " return 0;\n" "};\n", 0 }, /* --- strconv-foundation fold-1b: NAN_BITS / INF_BITS sentinels. * Hare exports `def NAN = 0.0/0.0;` / `def INF = 1.0/0.0;` (ref/hare/ * math/floats.ha:137,141); ww materializes via f64frombits(NAN_BITS) * / f64frombits(INF_BITS) until #129 closes. The bit values are * IEEE-754 facts (quiet-NaN: exp-all-ones + mantissa-MSB; +Inf: * exp-all-ones + mantissa-zero) — any drift means the def-folded * sentinels broke. */ { "package main;\n" "import math;\n" "export fn main() i32 = {\n" " if (math.NAN_BITS != 0x7FF8000000000000u64) { return 1; };\n" " if (math.INF_BITS != 0x7FF0000000000000u64) { return 2; };\n" " let n: f64 = math.f64frombits(math.NAN_BITS);\n" " if (!math.isnan(n)) { return 3; };\n" " if (math.isnan(1.0)) { return 4; };\n" " let i: f64 = math.f64frombits(math.INF_BITS);\n" " if (!math.isinf(i)) { return 5; };\n" " if (!math.isinf(-i)) { return 6; };\n" " if (math.isinf(1.23)) { return 7; };\n" " return 0;\n" "};\n", 0 }, /* f64info / f32info module-scope instances, read cross-module through * a *floatinfo pointer parameter — the exact call shape stof's * eisel_lemire (`f: *floatinfo`) uses in fold-4. Address-of a * module-level def lowers to LEAQ since #149; the field reads * round-trip the static-init DATA the #129 A.2 path emits. Mirrors * ref/hare/math/floats.ha:117,126. Pointer-param (not direct * math.f64info.expbias) because a cross-module struct-field direct read * leaks the field name as an extern (#150, task list #48). */ { "package main;\n" "import math;\n" "fn rdbias(f: *math.floatinfo) int = { return f.expbias; };\n" "fn rdmant(f: *math.floatinfo) u64 = { return f.mantbits; };\n" "fn rdmask(f: *math.floatinfo) u64 = { return f.mantmask; };\n" "fn rdebits(f: *math.floatinfo) u64 = { return f.expbits; };\n" "fn rdemask(f: *math.floatinfo) u64 = { return f.expmask; };\n" "export fn main() i32 = {\n" " if (rdbias(&math.f64info) != 1023) { return 1; };\n" " if (rdmant(&math.f64info) != 52u64) { return 2; };\n" " if (rdmask(&math.f64info) != 0xFFFFFFFFFFFFFu64) { return 3; };\n" " if (rdbias(&math.f32info) != 127) { return 4; };\n" " if (rdmant(&math.f32info) != 23u64) { return 5; };\n" " if (rdmask(&math.f32info) != 0x7FFFFFu64) { return 6; };\n" " if (rdebits(&math.f64info) != 11u64) { return 7; };\n" " if (rdemask(&math.f64info) != 0x7FFu64) { return 8; };\n" " if (rdebits(&math.f32info) != 8u64) { return 9; };\n" " if (rdemask(&math.f32info) != 0xFFu64) { return 10; };\n" " return 0;\n" "};\n", 0 }, { NULL, 0 } }; int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; char absbin[1024]; if (bin[0] != '/') { snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); bin = absbin; } int n = 0, fail = 0; for (int i = 0; rows[i].src; i++, n++) { char src[64]; snprintf(src, sizeof src, "/tmp/wwflt_%d_%d.ww", getpid(), i); FILE *f = fopen(src, "wb"); if (f == NULL) { fail++; continue; } fputs(rows[i].src, f); fclose(f); char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwflt_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); char cmd[2048]; snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s/lib %s", tmpdir, bin, cwd, src); if (runwait(cmd) != 0) { fprintf(stderr, "row %d: build failed\n src: %s\n", i, rows[i].src); fail++; unlink(src); rmdir(tmpdir); continue; } char outbin[128]; const char *base = strrchr(src, '/'); base = base ? base + 1 : src; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; int got = runwait(outbin); if (got != rows[i].want_exit) { fprintf(stderr, "row %d: exit %d, want %d\n src: %s\n", i, got, rows[i].want_exit, rows[i].src); fail++; } unlink(src); unlink(outbin); rmdir(tmpdir); } if (fail) { fprintf(stderr, "%d/%d floats tests failed\n", fail, n); return 1; } printf("floats: %d/%d ok\n", n, n); return 0; }