Files
ww/test/wcc/840_zeroinit_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

261 lines
9.1 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 840_zeroinit_run — task #16. A bare `let x: T;` (no initializer) must
* zero-fill its slot (Go zero-value, rob's ruling). Before the fix, cgen
* emitted the zero-fill ONLY for 8B-primitive slots and >8B composites; a
* SUB-8 aggregate (`let c: [3]u8;` = 3 bytes, a 3-byte struct, …) matched
* neither arm and fell through to NOTHING, so the slot read whatever the
* stack held.
*
* THE BUG (cstage == wwstage, BOTH wrong — shared gap, NOT rule-10):
* ken's bytes verdict (.ai/ken-bytes16-verdict.md) traced lib/bytes'
* green-but-broken 967 to ltrim_cases' `let c: [3]u8;` reading a prior
* deep-frame sibling's leftover bytes. The masking was a stack-zero
* accident: a bare let on a FRESH frame happens to read 0, so the bug
* only fires when a deep-framed fn ran first. byte-id was BLIND (#263):
* both stages emitted the identical no-store sequence.
*
* REPRO SHAPE (ken's bytes-independent minimal repro, generalised):
* dirty() writes a big local ([64]u8 = 222) to soil the stack region;
* probe() then declares the bare let as its FIRST local — reusing
* dirty()'s slot — and reads it. A correct zero-fill returns 0; the
* pre-fix garbage returned (222 * width) & 0xff (154 for [3]u8).
*
* THE FIX (#16, both stages): widen the no-rhs zero-fill gate from
* `sz > 8` to `sz > 0` (cgen.c N_LET) and add the symmetric `zsz > 0`
* run arm (cgenstmt.ww cglet), so 1..7-byte slots zero through the same
* MOVL/MOVB tail. sz == 8 keeps its immediate MOVQ $0; sz == 0 (`[0]T`)
* needs no stores.
*
* EACH ROW CARRIES BOTH DIMENSIONS (802 model):
* (a) cstage `ww build` + run, asserting exit 0 — pins that the bare let
* reads zero even after a dirtied frame (runtime correctness the
* byte-id gate cannot see).
* (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (rule-10).
*
* GATE POLARITY: must stay GREEN. A nonzero exit means a bare-let slot
* regressed to reading stack garbage; a byte-id FAIL means the stages
* diverged on the zero-fill emission.
*/
#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 *label; const char *src; int want_exit; };
/* Every row shares the dirty()→probe() spine: dirty soils the frame,
* probe declares the bare let FIRST and reads it. want_exit is 0 (all
* reads zero). The discriminating rows are the SUB-8 aggregates ([3]u8,
* [5]u8, [7]u8, the 3-byte struct) — the exact gap #16 closed; the
* scalar / [20]u8 / str / slice rows are controls (already zeroed pre-#16
* via the sz==8 and sz>8 arms) pinning no-regression. */
static const struct row rows[] = {
/* the exact ken repro: [3]u8 sub-8 array. Pre-fix returned 154. */
{ "arr3_u8",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = {\n"
" let c: [3]u8;\n"
" return (c[0]: i32) + (c[1]: i32) + (c[2]: i32);\n"
"};\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* sub-8, width 5 — exercises the MOVL+MOVB tail split (4 + 1). */
{ "arr5_u8",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = {\n"
" let c: [5]u8; let acc: i32 = 0; let j: i32 = 0;\n"
" for (j < 5) { acc += (c[j]: i32); j += 1; };\n"
" return acc;\n"
"};\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* sub-8, width 7 — the widest sub-8 extent (MOVL + MOVW + MOVB is
* unreachable under the run's 4/1 tail, so this is MOVL + 3×MOVB). */
{ "arr7_u8",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = {\n"
" let c: [7]u8; let acc: i32 = 0; let j: i32 = 0;\n"
" for (j < 7) { acc += (c[j]: i32); j += 1; };\n"
" return acc;\n"
"};\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* sub-8 STRUCT (3 bytes) — the non-array half of the gap. Read each
* field; a garbage slot would sum nonzero. */
{ "struct3_u8",
"package main;\n"
"type S = struct { a: u8, b: u8, c: u8 };\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = {\n"
" let s: S;\n"
" return (s.a: i32) + (s.b: i32) + (s.c: i32);\n"
"};\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* CONTROL: scalar i32 (sz==8) — zeroed pre-#16 by the MOVQ $0 arm. */
{ "scalar_i32",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = { let x: i32; return x; };\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* CONTROL: [20]u8 (>8) — zeroed pre-#16 by the #84 sz>8 run. */
{ "arr20_u8",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = {\n"
" let c: [20]u8; let acc: i32 = 0; let j: i32 = 0;\n"
" for (j < 20) { acc += (c[j]: i32); j += 1; };\n"
" return acc;\n"
"};\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* CONTROL: bare str header (24B) — rob's declmod `let empty: str;`
* shape. .len must read 0 (zeroed pre-#16 by the sz>8 run). */
{ "str_hdr",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = { let empty: str; return empty.len; };\n"
"export fn main() i32 = { dirty(); return probe(); };\n", 0 },
/* CONTROL: bare slice header (24B) — .len must read 0. */
{ "slice_hdr",
"package main;\n"
"fn dirty() void = {\n"
" let big: [64]u8; let i: i32 = 0;\n"
" for (i < 64) { big[i] = 222u8; i += 1; };\n"
"};\n"
"fn probe() i32 = { let xs: []i32; return xs.len; };\n"
"export fn main() i32 = { dirty(); return probe(); };\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, "zeroinit: w6c_ww missing — cannot run the "
"cs==ww byte-id gate\n");
return 1;
}
int n = 0, fail = 0;
for (int i = 0; rows[i].src; i++, n++) {
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwzi_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
char src[128], outbin[128], cs_s[128], ws_s[128], rmcmd[160];
snprintf(src, sizeof src, "%s/wwzi_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/wwzi_%d_%d", tmpdir, getpid(), i);
snprintf(cs_s, sizeof cs_s, "%s/wwzi_%d_%d_cs.s", tmpdir, getpid(), i);
snprintf(ws_s, sizeof ws_s, "%s/wwzi_%d_%d_ww.s", tmpdir, getpid(), i);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; runwait(rmcmd); continue; }
fputs(rows[i].src, f);
fclose(f);
/* (a) cstage build + run. */
char cmd[2048];
snprintf(cmd, sizeof cmd, "%s/ww build -o %s %s", bin, outbin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage build failed\n",
rows[i].label);
fail++; runwait(rmcmd); continue;
}
int got = runwait(outbin);
if (got != rows[i].want_exit) {
fprintf(stderr, "row[%s]: cstage exit %d, want %d "
"(bare let read stack garbage?)\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
/* (b) cs==ww byte-id gate. */
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++; runwait(rmcmd); 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++; runwait(rmcmd); 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++;
}
runwait(rmcmd);
}
if (fail) {
fprintf(stderr, "%d/%d zeroinit tests failed\n", fail, n);
return 1;
}
printf("zeroinit: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n);
return 0;
}