Files
ww/test/wcc/840_zeroinit_run.c
Hojun-Cho 5b212e51cf wcc/cgen: zero-init sub-8-byte bare lets, both stages; bytes test honest (#16-team)
A bare 'let x: T;' with 1 <= size(T) <= 7 matched no zero-fill arm in
either stage (8B and >8B were already zeroed) - 'let c: [3]u8;' read
stack garbage. User-ruled zero-value semantics: cstage gate sz>8 ->
sz>0; wwstage zsz==8 arm hoisted above the fill-run arm (required -
8B would otherwise route into the run and diverge) and run gate
zsz>0. New 840 pin: dirty-frame probe rows, dual-dim (run + cs/ww
byte-id); discriminators fail exit-154 on pre-fix binaries.

Fused with the lib/bytes test conversion (rule 11): either half alone
turns 967 red. The old exit(signalled+10) wrapped a real 1782-count
ltrim failure to exit 0 - green depended on the garbage. Converted to
assert form (completes the 35/35 @test conversion); ltrim rows keep
the bare 'let c: [3]u8;' as the consumer proof of the fix.
2026-06-10 20:08:58 +09:00

272 lines
9.3 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 <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; };
/* 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 src[64];
snprintf(src, sizeof src, "/tmp/wwzi_%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 in a scratch dir. */
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwzi_%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 "
"(bare let read stack garbage?)\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
unlink(outbin); rmdir(tmpdir);
/* (b) cs==ww byte-id gate. */
char cs_s[64], ws_s[64];
snprintf(cs_s, sizeof cs_s, "/tmp/wwzi_%d_%d_cs.s", getpid(), i);
snprintf(ws_s, sizeof ws_s, "/tmp/wwzi_%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 zeroinit tests failed\n", fail, n);
return 1;
}
printf("zeroinit: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n);
return 0;
}