From c4e29df4e9a7e7d71449baf8d2b82520243140a9 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 30 May 2026 09:36:32 +0900 Subject: [PATCH] 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. --- Makefile | 10 + selfhost/cmd/w6c/main.combined.ww | 15 +- selfhost/cmd/wcc/cgenstmt.ww | 15 +- selfhost/cmd/wwdump/main.combined.ww | 15 +- test/wcc/790_single_field_struct_zeroinit.c | 243 ++++++++++++++++++++ 5 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 test/wcc/790_single_field_struct_zeroinit.c diff --git a/Makefile b/Makefile index d2498dbb..22a24ea1 100644 --- a/Makefile +++ b/Makefile @@ -338,6 +338,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_xmod_variant_match \ $(BIN)/test_named_ptr_alias_variant_widen \ + $(BIN)/test_single_field_struct_zeroinit \ $(BIN)/test_structvariant_largeunion_return \ $(BIN)/test_narrow_alias_deref_store \ $(BIN)/test_bufio_vstream_run \ @@ -766,6 +767,15 @@ $(BIN)/test_named_ptr_alias_variant_widen: test/wcc/789_named_ptr_alias_variant_ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #213: bare `let b: T;` of an 8B single-field struct must zero-init the +# slot (cstage does; wwstage skipped → read-before-init garbage + 778 +# cs!=ww). Both-stage byte-id + a read-before-init correctness row. +$(BIN)/test_single_field_struct_zeroinit: test/wcc/790_single_field_struct_zeroinit.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #9: returning a STRUCT variant of a LARGE (>4-eightbyte) tagged union. # Both-stage byte-id + runtime (reads tag AND the widened &fn field, so a # dropped-store regression can't hide behind self-consistent byte-id). diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a519c735..86763636 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -27166,7 +27166,20 @@ fn cglet(c: *cgen, n: *node) void = { emitline("(BP)\n"); zi += 1; }; - }; }; }; + } else { if (zsz == 8) { + // #213: an 8B composite (single-field struct / tagged) is + // neither an 8B primitive nor zsz>8, so it fell through + // un-zeroed while cstage emits MOVQ $0 (cgen.c N_LET + // `else if (sz == 8)`); a read-before-init then saw stack + // garbage (cs!=ww byte-id + a latent garbage-read). Match + // cstage. Sub-8 (4B/1B) composites stay un-zeroed — cstage + // doesn't zero them either, so zeroing here would re- + // diverge; that sub-8 read-before-init garbage is a SHARED + // latent, out of this slice's scope. + emitline("\tMOVQ\t$0, "); + emitoff(off: i64); + emitline("(BP)\n"); + }; }; }; }; }; c.lastwasreturn = 0; return; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index d55e7125..948f4999 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -1535,7 +1535,20 @@ fn cglet(c: *cgen, n: *node) void = { emitline("(BP)\n"); zi += 1; }; - }; }; }; + } else { if (zsz == 8) { + // #213: an 8B composite (single-field struct / tagged) is + // neither an 8B primitive nor zsz>8, so it fell through + // un-zeroed while cstage emits MOVQ $0 (cgen.c N_LET + // `else if (sz == 8)`); a read-before-init then saw stack + // garbage (cs!=ww byte-id + a latent garbage-read). Match + // cstage. Sub-8 (4B/1B) composites stay un-zeroed — cstage + // doesn't zero them either, so zeroing here would re- + // diverge; that sub-8 read-before-init garbage is a SHARED + // latent, out of this slice's scope. + emitline("\tMOVQ\t$0, "); + emitoff(off: i64); + emitline("(BP)\n"); + }; }; }; }; }; c.lastwasreturn = 0; return; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3eeb2a15..62d3d102 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -27166,7 +27166,20 @@ fn cglet(c: *cgen, n: *node) void = { emitline("(BP)\n"); zi += 1; }; - }; }; }; + } else { if (zsz == 8) { + // #213: an 8B composite (single-field struct / tagged) is + // neither an 8B primitive nor zsz>8, so it fell through + // un-zeroed while cstage emits MOVQ $0 (cgen.c N_LET + // `else if (sz == 8)`); a read-before-init then saw stack + // garbage (cs!=ww byte-id + a latent garbage-read). Match + // cstage. Sub-8 (4B/1B) composites stay un-zeroed — cstage + // doesn't zero them either, so zeroing here would re- + // diverge; that sub-8 read-before-init garbage is a SHARED + // latent, out of this slice's scope. + emitline("\tMOVQ\t$0, "); + emitoff(off: i64); + emitline("(BP)\n"); + }; }; }; }; }; c.lastwasreturn = 0; return; diff --git a/test/wcc/790_single_field_struct_zeroinit.c b/test/wcc/790_single_field_struct_zeroinit.c new file mode 100644 index 00000000..c287693d --- /dev/null +++ b/test/wcc/790_single_field_struct_zeroinit.c @@ -0,0 +1,243 @@ +/* + * 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 +#include +#include +#include +#include + +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; +}