Files
ww/test/wcc/964_f32lit_run.c
Hojun-Cho acaf0152da wcc: narrow f32-typed float literals at materialisation (#104 fold-1)
Both stages materialise a float literal as a 64-bit double in X0 (MOVQ
bits -> MOVSD), ignoring the node type. For an f32-typed literal the
downstream MOVSS reads the low 4 bytes of that double — garbage (0.0f
for clean values, which is why 0.0 survived the bug and 951's f32 rows,
which only assert NaN ordering, never caught it). Append CVTSD2SS X0,X0
at both literal sites (N_FLOATLIT + the float-typed N_INTLIT arm) when
the node is f32-typed, so the value reaches X0 as a true single. Mirror
in cgenexpr.ww (rule-10) and regen the w6c/wwdump combined.ww embeds.

Covers literals carrying an explicit f32 type (the `f32` suffix and the
no-decimal `8f32` N_INTLIT arm). An un-suffixed literal in an f32
context (`let x: f32 = 1.0`) stays ty_untyped_float through the checker,
so its node is never f32-typed and this branch can't fire — that needs
fold-2 (checker untyped-float -> f32 lowering, both checkers).

964_f32lit_run: cstage run + cs==ww byte-id probe over concrete f32
values (suffixed), the hole 951 leaves open.
2026-05-26 11:19:37 +09:00

220 lines
6.9 KiB
C

/*
* 964_f32lit_run — runtime + byte-id regression net for #104 fold-1: an
* f32-typed float literal must NARROW to single precision in X0 before
* the f32 consumer reads it. Both stages (cstage cgen.c cgexpr_float,
* wwstage cgenexpr.ww cgfloatbits) materialise a float literal as a
* 64-bit DOUBLE in X0 (MOVQ bits -> MOVSD). For an f32-typed literal the
* downstream MOVSS store/return then reads the LOW 4 BYTES of that
* double — garbage (0x00000000 == 0.0f for most clean values, which is
* why 0.0 coincidentally survived the bug). fold-1 appends CVTSD2SS
* X0,X0 at both literal sites (N_FLOATLIT + the float-typed N_INTLIT
* arm) when the node is f32-typed, so the value reaches X0 as a true
* single. Both stages emit byte-identical asm, so the 990-997 byte-id
* gates can NEVER catch a reintroduction — only an executed-and-checked
* runtime probe can. (951_f64cgen is GATE-BLIND here: its f32 rows only
* assert NaN ordering, never a concrete f32 value.)
*
* SCOPE: fold-1 covers literals that carry an explicit f32 TYPE — the
* `f32` suffix (`1.0f32`, `1.5f32`) and the no-decimal N_INTLIT-float
* arm (`8f32`). An UN-suffixed literal in an f32 context (`let x: f32 =
* 1.0`) stays ty_untyped_float through the checker, so the literal node
* is never f32-typed and fold-1's branch can't fire — materialised as a
* double, stored low-4-bytes -> 0.0f. Fixing that needs fold-2: the
* checker lowering untyped-float literals to their f32 context type
* (#104, both checkers). This probe therefore uses suffixed literals
* exclusively; the un-suffixed gap is tracked under #104 fold-2.
*
* Each row carries BOTH dimensions (like 955_f64xmm_run):
* (a) cstage `ww build` + run, asserting the exit code.
* (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (rule-10).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.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 *label; const char *src; int want_exit; };
static const struct row rows[] = {
/* The hole 951 misses: a CONCRETE non-trivial f32 value. On the
* bug `let x: f32 = 1.0f32` stores the low 4 bytes of double 1.0
* (== 0x00000000 == 0.0f), so x:f64 == 0.0 != 1.0 -> 1. */
{ "bare_value",
"package main;\n"
"export fn main() i32 = {\n"
" let x: f32 = 1.0f32;\n"
" if (x: f64 != 1.0) { return 1; };\n"
" return 0;\n"
"};\n", 0 },
/* arith on f32 literals: 1.5 + 2.5 == 4.0. On the bug both operands
* land as 0.0f -> sum 0.0 != 4.0. */
{ "arith",
"package main;\n"
"export fn main() i32 = {\n"
" let a: f32 = 1.5f32;\n"
" let b: f32 = 2.5f32;\n"
" let s: f32 = a + b;\n"
" if (s: f64 != 4.0) { return 1; };\n"
" return 0;\n"
"};\n", 0 },
/* value-propagation: f32 returned through an f32 fn + arith on an
* f32 literal, truncated to i32. 2.5 + 1.5 == 4.0 -> 4. */
{ "return_arith",
"package main;\n"
"fn g() f32 = { return 2.5f32; };\n"
"export fn main() i32 = {\n"
" let r: f32 = g() + 1.5f32;\n"
" return r: i32;\n"
"};\n", 4 },
/* the float-typed N_INTLIT arm (`8f32` — no decimal, f32 suffix).
* Same materialiser, same fold-1 branch. -> 8. */
{ "intlit_f32_arm",
"package main;\n"
"export fn main() i32 = {\n"
" let y: f32 = 8f32;\n"
" return y: i32;\n"
"};\n", 8 },
/* genuine single-rounding: 2^24 + 1 is NOT representable in f32 and
* rounds back to 2^24 (round-to-even). If the add ran in double it
* would be 16777217.0 != 16777216.0 -> 1. Proves the value is a
* true single, not the low half of a double. */
{ "single_round",
"package main;\n"
"export fn main() i32 = {\n"
" let big: f32 = 16777216.0f32;\n"
" let r: f32 = big + 1.0f32;\n"
" if (r: f64 != 16777216.0) { return 1; };\n"
" return 0;\n"
"};\n", 0 },
{ NULL, NULL, 0 }
};
static int
slurp_eq(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
int rc = 0;
for (;;) {
int ca = fgetc(fa);
int cb = fgetc(fb);
if (ca != cb) { rc = -1; break; }
if (ca == EOF) break;
}
fclose(fa); fclose(fb);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char w6c[1100], w6c_ww[1100];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
if (access(w6c_ww, X_OK) != 0) {
fprintf(stderr, "f32lit: w6c_ww missing — cannot run the "
"cs==ww byte-id gate (the whole point of this test)\n");
return 1;
}
int n = 0, fail = 0;
for (int i = 0; rows[i].src; i++, n++) {
char src[64];
snprintf(src, sizeof src, "/tmp/wwf32l_%d_%d.ww", getpid(), i);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; continue; }
fputs(rows[i].src, f);
fclose(f);
/* (a) cstage build + run. */
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwf32l_%d_d_%d",
getpid(), i);
mkdir(tmpdir, 0755);
char cmd[2048];
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
tmpdir, bin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage build failed\n",
rows[i].label);
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[%s]: cstage exit %d, want %d\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
unlink(outbin); rmdir(tmpdir);
/* (b) cs==ww byte-id gate: emit .s from both stages, cmp. */
char cs_s[64], ws_s[64];
snprintf(cs_s, sizeof cs_s, "/tmp/wwf32l_%d_%d_cs.s",
getpid(), i);
snprintf(ws_s, sizeof ws_s, "/tmp/wwf32l_%d_%d_ww.s",
getpid(), i);
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c, cs_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
fail++; unlink(src); continue;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c_ww, ws_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww failed\n",
rows[i].label);
fail++; unlink(src); unlink(cs_s); continue;
}
if (slurp_eq(cs_s, ws_s) != 0) {
fprintf(stderr,
"row[%s]: cstage/wwstage .s DIFFER (rule-10 "
"byte-id violation)\n", rows[i].label);
fail++;
}
unlink(src); unlink(cs_s); unlink(ws_s);
}
if (fail) {
fprintf(stderr, "%d/%d f32 literal-materialise tests failed\n",
fail, n);
return 1;
}
printf("f32lit: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n);
return 0;
}