Files
ww/test/wcc/790_single_field_struct_zeroinit.c
Hojun-Cho c4e29df4e9 wwstage: zero-init 8B composite locals in bare-let to match cstage (#213)
wwstage's cglet no-rhs path zero-inited only 8B primitives (MOVQ) and >8B composites (XORQ run), so an 8B *composite* local (single-field struct/tagged, e.g. struct{src:*vtable}) declared bare (let b: box;) was left uninitialized -- reading an unassigned field returned stack garbage (a silent read-before-init), and it diverged from cstage which zero-inits any 8B local (cs!=ww byte-id, surfaced by #5's bufio box{src:io.stream}). Add the missing arm: a non-array composite of size 8 emits MOVQ $0, matching cstage's no-rhs sz==8 zeroing. cstage unchanged (already correct -- align wwstage UP). Scope is 8B-only: cstage does not zero-init sub-8 composites either (sub-8 falls through to nothing on both stages, already cs==ww), so zeroing sub-8 on wwstage would create a new divergence; the sub-8 read-before-init garbage is a separate shared-both-stages latent (#20). Adds test/wcc/790 (8B byte-id row + read-before-init correctness lock reading 0 on both stages). rule-10 align-up; closes the #213 8B-composite slice; unblocks post-eFinal #5.
2026-05-30 09:36:32 +09:00

244 lines
7.5 KiB
C

/*
* 790_single_field_struct_zeroinit — project #213 (≤8B-composite slice).
* Pins that a bare `let b: T;` of an 8B single-field STRUCT zero-inits
* the slot on BOTH stages byte-identically (rule-10), and that the
* zero is observable (read-before-init sees 0, not stack garbage).
*
* THE BUG (wwstage cgen): wwstage's cglet no-rhs zero-init
* (selfhost/cmd/wcc/cgenstmt.ww) only zeroed an 8B *primitive*
* (scalar/ptr/fn/chan → MOVQ $0) or a `zsz > 8` composite (XORQ run).
* An 8B *composite* (single-field struct/tagged) was NEITHER → it fell
* through with no zero-init, while cstage emits `MOVQ $0` (cgen.c N_LET
* `else if (sz == 8)`, per ww's bare-`let`-composite contract). So a
* read-before-init saw stack garbage on wwstage but 0 on cstage —
* cs!=ww byte-id (surfaced by #5's bufio `box { src: io.stream }`) AND
* a latent silent miscompile. cstage is correct; align wwstage UP.
*
* SCOPE: 8B composites only. Sub-8 (4B/1B) composites are intentionally
* left un-zeroed — cstage doesn't zero them either, so zeroing them on
* wwstage would RE-diverge; that sub-8 read-before-init garbage is a
* SHARED (cs==ww) latent, out of this slice.
*
* row | shape | exit | byte-id
* -------------------+-----------------------------------------+------+--------
* field8_assign | box{src:*vtable}; let b; b.src=&vt; | 42 | cs==ww
* | read b.src.x — the 778/#5 shape | |
* | (#5-independent: no io/union) | |
* read_before_init | box{x:i64}; dirty the stack, then bare | 0 | cs==ww
* | `let b; return b.x` UNassigned — must | |
* | read 0 (the load-bearing zero-init, | |
* | was stack garbage pre-fix on wwstage) | |
*
* GATE POLARITY: must stay GREEN. Red on field8_assign = the 8B-struct
* zero-init regressed (cs!=ww); red on read_before_init = the zero-init
* stopped being emitted (garbage read).
*/
#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;
}
#define STAGE_CS 1
#define STAGE_WW 2
struct row {
const char *label;
const char *src;
int want_exit;
int stage_mask;
int byte_id;
};
static const struct row rows[] = {
/* The 778/#5 shape, #5-independent: single-field struct bare-let
* then field-assign. Pre-fix cstage zero-inits b (dead here since
* b.src is assigned), wwstage omits it → cs!=ww. */
{ "field8_assign",
"package main;\n"
"type vtable = struct { x: i32 };\n"
"type box = struct { src: *vtable };\n"
"fn use(b: *box) i32 = { return b.src.x; };\n"
"export fn main() i32 = {\n"
" let vt: vtable; vt.x = 42;\n"
" let b: box; b.src = &vt;\n"
" return use(&b);\n"
"};\n",
42, STAGE_CS | STAGE_WW, 1 },
/* The correctness lock: dirty the stack, then read an UNassigned
* 8B-struct field — must be 0 (zero-init), not garbage. */
{ "read_before_init",
"package main;\n"
"type box = struct { x: i64 };\n"
"fn dirty() i64 = { let a: i64 = 0x7777777777777777i64; let b: i64 = a; return b; };\n"
"fn readit() i64 = { let b: box; return b.x; };\n"
"export fn main() i32 = {\n"
" let j: i64 = dirty();\n"
" let v: i64 = readit();\n"
" if (v == 0i64) { return 0; };\n"
" return 1;\n"
"};\n",
0, STAGE_CS | STAGE_WW, 1 },
};
static void
cleanup_tmp(const char *tmpdir, const char *base)
{
char p[1024];
snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p);
rmdir(tmpdir);
}
static int
write_source(const char *path, const char *src)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(src, f);
fclose(f);
return 0;
}
static int
build_via_driver(const char *driver, const char *tmpdir, const char *src)
{
char cmd[2048];
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null",
tmpdir, driver, src);
return runwait(cmd);
}
static int
run_row(const char *driver, const struct row *r, int seq)
{
char tmpdir[256], src[512], base[64], outbin[768];
snprintf(tmpdir, sizeof tmpdir, "/tmp/sfsz_%d_d_%d", getpid(), seq);
snprintf(base, sizeof base, "main790");
snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base);
mkdir(tmpdir, 0755);
if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; }
int rc;
if (build_via_driver(driver, tmpdir, src) == 0) {
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
rc = runwait(outbin);
} else {
rc = -1;
}
cleanup_tmp(tmpdir, base);
return rc;
}
static int
asm_byte_identical(const char *cdrv, const char *wdrv, const struct row *r,
int seq)
{
char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512];
snprintf(tdc, sizeof tdc, "/tmp/sfsz_%d_c_%d", getpid(), seq);
snprintf(tdw, sizeof tdw, "/tmp/sfsz_%d_w_%d", getpid(), seq);
snprintf(base, sizeof base, "main790");
mkdir(tdc, 0755);
mkdir(tdw, 0755);
snprintf(src, sizeof src, "%s/%s.ww", tdc, base);
if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; }
int rc = -1;
if (build_via_driver(cdrv, tdc, src) != 0) goto out;
snprintf(cs, sizeof cs, "%s/%s.s", tdc, base);
snprintf(src, sizeof src, "%s/%s.ww", tdw, base);
if (write_source(src, r->src) != 0) goto out;
if (build_via_driver(wdrv, tdw, src) != 0) goto out;
snprintf(ws, sizeof ws, "%s/%s.s", tdw, base);
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
if (fc && fw) {
rc = 0;
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
out:
cleanup_tmp(tdc, base);
cleanup_tmp(tdw, base);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
char absbin[512];
if (bin[0] != '/') {
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[640], wdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0, seq = 0;
int wwpresent = (access(wdrv, X_OK) == 0);
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
if (r->stage_mask & STAGE_CS) {
total++;
int got = run_row(cdrv, r, seq++);
if (got != r->want_exit) {
fprintf(stderr, "sfsz[cs][%s]: exit=%d want=%d\n",
r->label, got, r->want_exit);
fail++;
}
}
if (wwpresent && (r->stage_mask & STAGE_WW)) {
total++;
int got = run_row(wdrv, r, seq++);
if (got != r->want_exit) {
fprintf(stderr, "sfsz[ww][%s]: exit=%d want=%d\n",
r->label, got, r->want_exit);
fail++;
}
if (r->byte_id) {
total++;
if (asm_byte_identical(cdrv, wdrv, r, seq++) != 0) {
fprintf(stderr, "sfsz[byte-id][%s]: cstage vs wwstage asm differs\n",
r->label);
fail++;
}
}
}
}
if (fail) {
fprintf(stderr, "single_field_struct_zeroinit: %d/%d checks failed\n",
fail, total);
return 1;
}
printf("single_field_struct_zeroinit: %d/%d ok\n", total, total);
return 0;
}