Files
ww/test/wcc/952_floats_run.c
Hojun-Cho ce3a25a0b4 test: contain sepwork scratch per-driver tmpdir, fix /tmp+in-repo leak (#8)
The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the
compiler's <stem>.sepwork scratch landed beside the source and was never
cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs
and fabricates phantom test failures + silent harness aborts, and for
in-repo fixture builds leaked .sepwork into the tracked tree.

Each leaking build now writes its source + output inside a per-invocation
tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and
rm -rf's the tmpdir on every exit path -- including fopen-fail and the
expected-fail reject builds (scratch is mkdir'd before the build can fail).
`ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993
byte-id comparison logic is byte-for-byte unchanged.

Two items filed separately (this commit holds the no-Makefile / no-main.c
rail):
- #13: a stale <src>.s byte-id readback (749) silently no-ops since
  separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline.
- #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no
  -o and leak main.sepwork in-tree (bounded, gitignored; own commit).

One concern -- sepwork leak hygiene -- across 228 drivers; uniform
transform applied per-file and two-round reviewed. make test: all 402
passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
2026-06-22 23:29:39 +09:00

419 lines
17 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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 tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwflt_%d_%d", getpid(), i);
mkdir(tmpdir, 0755);
char rmcmd[128];
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
char src[128];
snprintf(src, sizeof src, "%s/wwflt_%d_%d.ww", tmpdir, getpid(), i);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; runwait(rmcmd); continue; }
fputs(rows[i].src, f);
fclose(f);
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/out", tmpdir);
char cmd[2048];
snprintf(cmd, sizeof cmd, "%s/ww build -I %s/lib -o %s %s",
bin, cwd, outbin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row %d: build failed\n src: %s\n",
i, rows[i].src);
fail++;
runwait(rmcmd);
continue;
}
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++;
}
runwait(rmcmd);
}
if (fail) {
fprintf(stderr, "%d/%d floats tests failed\n", fail, n);
return 1;
}
printf("floats: %d/%d ok\n", n, n);
return 0;
}