diff --git a/Makefile b/Makefile index c163dabd..af1b72ef 100644 --- a/Makefile +++ b/Makefile @@ -405,6 +405,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_dotbase_addr_slice_run \ $(BIN)/test_structlit_arrfield_run \ $(BIN)/test_arraytoslice_run \ + $(BIN)/test_valstruct_subsize_run \ $(BIN)/test_continue_run \ $(BIN)/test_callret_unsigned_arith_run \ $(BIN)/test_sar_shr_run \ @@ -1611,6 +1612,11 @@ $(BIN)/test_arraytoslice_run: test/wcc/953_arraytoslice_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_valstruct_subsize_run: test/wcc/949_valstruct_subsize_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_continue_run: test/wcc/911_continue_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3a437d7c..209f7530 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -28899,17 +28899,29 @@ fn cglet(c: *cgen, n: *node) void = { if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; }; // Zero-fill extent. cstage sizes the run on `lu->size` - // (the maxalign-rounded ABI size, check.c:760); wwstage's - // `sz` from letslotsize is slot-padded (round-to-8), so a - // struct with maxalign<8 and a sub-8 tail would over-zero - // MOVQ where cstage emits MOVL/MOVB. Read structabisize - // for a struct-typed let to converge; slot allocation - // stays on `sz` (frame uses slot-padded slots). + // (the natural ABI size from the type table, cgen.c:8397); + // wwstage's `sz` from letslotsize is slot-padded (round-to-8), + // so a struct with maxalign<8 and a sub-8 tail would over-zero + // MOVQ where cstage emits nothing. Source the extent from the + // type table's tinfo.size for a struct-typed let to converge; + // slot allocation stays on `sz` (frame uses slot-padded slots). + // #254: structabisize is NOT a sound ABI-size source here — it + // sums fieldsize(), which slot-pads a nested value-struct field + // to 8, so a sub-8 outer struct (e.g. `struct{struct{[4]u8}}`, + // ABI 4) read 8 and emitted a stray MOVQ $0 cstage doesn't. + // fieldsize / registerstruct / frame slot-padding stay + // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); - if (szi != nil) { zsz = structabisize(szi); }; + if (szi != nil) { + let ti: *tinfo = n.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { zsz = ti.size: i32; }; + }; }; }; if (typeis8byteprimitive(c, n.lhs)) { @@ -32324,13 +32336,29 @@ fn emitletdataw(c: *cgen, file: *node) void = { // A struct literal init isn't compile-time // evaluated yet; skip and the link will surface // an undefined-symbol error if referenced. + // #254: the zero-fill byte count comes from the + // type table's tinfo.size (cstage cg_let_emit_size + // returns u->size, cgen.c:978), NOT letemitsize/ + // si.totsize — registerstruct rounds the nested + // value-struct field's slot to 8, so a sub-8 outer + // struct (ABI 4) over-emitted DATAW 8 bytes vs + // cstage's 4. registerstruct / fieldsize / frame + // slot-padding stay UNTOUCHED (field offsets). if (issg) { if (d.rhs == nil) { + let zsz: i32 = sz; + if (d.lhs != nil) { + let ti: *tinfo = d.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { zsz = ti.size: i32; }; + }; emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; - for (i < sz) { + for (i < zsz) { emitdatawbyte(0u8); i += 1; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index b44d1de4..b679bdb3 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1990,13 +1990,29 @@ fn emitletdataw(c: *cgen, file: *node) void = { // A struct literal init isn't compile-time // evaluated yet; skip and the link will surface // an undefined-symbol error if referenced. + // #254: the zero-fill byte count comes from the + // type table's tinfo.size (cstage cg_let_emit_size + // returns u->size, cgen.c:978), NOT letemitsize/ + // si.totsize — registerstruct rounds the nested + // value-struct field's slot to 8, so a sub-8 outer + // struct (ABI 4) over-emitted DATAW 8 bytes vs + // cstage's 4. registerstruct / fieldsize / frame + // slot-padding stay UNTOUCHED (field offsets). if (issg) { if (d.rhs == nil) { + let zsz: i32 = sz; + if (d.lhs != nil) { + let ti: *tinfo = d.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { zsz = ti.size: i32; }; + }; emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; - for (i < sz) { + for (i < zsz) { emitdatawbyte(0u8); i += 1; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 197f2e3d..e66bb6a7 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -1768,17 +1768,29 @@ fn cglet(c: *cgen, n: *node) void = { if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; }; // Zero-fill extent. cstage sizes the run on `lu->size` - // (the maxalign-rounded ABI size, check.c:760); wwstage's - // `sz` from letslotsize is slot-padded (round-to-8), so a - // struct with maxalign<8 and a sub-8 tail would over-zero - // MOVQ where cstage emits MOVL/MOVB. Read structabisize - // for a struct-typed let to converge; slot allocation - // stays on `sz` (frame uses slot-padded slots). + // (the natural ABI size from the type table, cgen.c:8397); + // wwstage's `sz` from letslotsize is slot-padded (round-to-8), + // so a struct with maxalign<8 and a sub-8 tail would over-zero + // MOVQ where cstage emits nothing. Source the extent from the + // type table's tinfo.size for a struct-typed let to converge; + // slot allocation stays on `sz` (frame uses slot-padded slots). + // #254: structabisize is NOT a sound ABI-size source here — it + // sums fieldsize(), which slot-pads a nested value-struct field + // to 8, so a sub-8 outer struct (e.g. `struct{struct{[4]u8}}`, + // ABI 4) read 8 and emitted a stray MOVQ $0 cstage doesn't. + // fieldsize / registerstruct / frame slot-padding stay + // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); - if (szi != nil) { zsz = structabisize(szi); }; + if (szi != nil) { + let ti: *tinfo = n.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { zsz = ti.size: i32; }; + }; }; }; if (typeis8byteprimitive(c, n.lhs)) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 263cddae..721f5b74 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -28899,17 +28899,29 @@ fn cglet(c: *cgen, n: *node) void = { if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; }; // Zero-fill extent. cstage sizes the run on `lu->size` - // (the maxalign-rounded ABI size, check.c:760); wwstage's - // `sz` from letslotsize is slot-padded (round-to-8), so a - // struct with maxalign<8 and a sub-8 tail would over-zero - // MOVQ where cstage emits MOVL/MOVB. Read structabisize - // for a struct-typed let to converge; slot allocation - // stays on `sz` (frame uses slot-padded slots). + // (the natural ABI size from the type table, cgen.c:8397); + // wwstage's `sz` from letslotsize is slot-padded (round-to-8), + // so a struct with maxalign<8 and a sub-8 tail would over-zero + // MOVQ where cstage emits nothing. Source the extent from the + // type table's tinfo.size for a struct-typed let to converge; + // slot allocation stays on `sz` (frame uses slot-padded slots). + // #254: structabisize is NOT a sound ABI-size source here — it + // sums fieldsize(), which slot-pads a nested value-struct field + // to 8, so a sub-8 outer struct (e.g. `struct{struct{[4]u8}}`, + // ABI 4) read 8 and emitted a stray MOVQ $0 cstage doesn't. + // fieldsize / registerstruct / frame slot-padding stay + // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); - if (szi != nil) { zsz = structabisize(szi); }; + if (szi != nil) { + let ti: *tinfo = n.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { zsz = ti.size: i32; }; + }; }; }; if (typeis8byteprimitive(c, n.lhs)) { @@ -32324,13 +32336,29 @@ fn emitletdataw(c: *cgen, file: *node) void = { // A struct literal init isn't compile-time // evaluated yet; skip and the link will surface // an undefined-symbol error if referenced. + // #254: the zero-fill byte count comes from the + // type table's tinfo.size (cstage cg_let_emit_size + // returns u->size, cgen.c:978), NOT letemitsize/ + // si.totsize — registerstruct rounds the nested + // value-struct field's slot to 8, so a sub-8 outer + // struct (ABI 4) over-emitted DATAW 8 bytes vs + // cstage's 4. registerstruct / fieldsize / frame + // slot-padding stay UNTOUCHED (field offsets). if (issg) { if (d.rhs == nil) { + let zsz: i32 = sz; + if (d.lhs != nil) { + let ti: *tinfo = d.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { zsz = ti.size: i32; }; + }; emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; - for (i < sz) { + for (i < zsz) { emitdatawbyte(0u8); i += 1; }; diff --git a/test/wcc/949_valstruct_subsize_run.c b/test/wcc/949_valstruct_subsize_run.c new file mode 100644 index 00000000..a4539764 --- /dev/null +++ b/test/wcc/949_valstruct_subsize_run.c @@ -0,0 +1,255 @@ +/* + * 949_valstruct_subsize_run — runtime + byte-id net for #254: a sub-8 + * (non-8-multiple) nested value-struct must size its zero-init extent + * from the type table's natural ABI size (cstage lu->size), NOT the + * slot-padded register-struct width. + * + * Root: wwstage conflated SLOT-size (round-to-8, for frame layout) with + * ABI-size (true). A nested value-struct field was sized via fieldsize() + * (cgenutil.ww TY_STRUCT -> ti.slotsize = 8), poisoning structabisize + + * registerstruct si.totsize to 8 for a struct whose true ABI size is 4. + * Two emission sites then over-sized: + * D1 (local): cglet zsz = structabisize = 8 hit the `zsz == 8` zero + * arm (cgenstmt.ww #213) -> a stray `MOVQ $0, off(BP)` that cstage + * (ABI 4 is sub-8 -> left uninit per the shared no-rhs policy) + * never emits. + * D2 (global): emitletdataw struct arm wrote si.totsize = 8 zero bytes + * of DATAW; cstage cg_let_emit_size returns u->size = 4. + * Both were SILENT cs!=ww divergences (gate-blind: a standalone wwstage + * is self-consistent; only the cs==ww .s cmp catches it). + * + * Fix (rule-13 SSoT): both sites source the extent from tinfo.size + * (peeling TY_NAMED), the same value cstage reads. fieldsize / + * registerstruct / frame slot-padding stay UNTOUCHED — moving the fix + * into the size helpers would shift nested-struct field offsets and + * re-diverge other byte-id. + * + * Rows cover the whole sub-8 class (ABI size 1/2/4) in both the local + * (D1) and global (D2) emission contexts, plus a >8 NEGATIVE control + * proving the fix didn't disable legitimate multi-word zero-init. Each + * row: cstage `ww build` + run for the exit code (correctness) AND + * w6c vs w6c_ww `.s` cmp for rule-10 byte-id (the silent-divergence net). + */ +#include +#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; +} + +struct row { const char *label; const char *src; int want_exit; }; + +static const struct row rows[] = { + /* D1 local, ABI size 4 (inner {[4]u8}). Pre-fix wwstage emitted a + * stray MOVQ $0 cstage didn't -> .s differ. Write+read a byte so + * the exit is deterministic (sub-8 is left uninit by BOTH stages, + * matching cstage's no-rhs policy). */ + { "d1_local_4", + "package main;\n" + "type inner = struct { m: [4]u8 };\n" + "type outv = struct { i: inner };\n" + "export fn main() i32 = {\n" + " let o: outv;\n" + " o.i.m[0] = 66u8;\n" + " return o.i.m[0]: i32;\n" + "};\n", 66 }, + /* D1 local, ABI size 2 ([2]u8). */ + { "d1_local_2", + "package main;\n" + "type inner = struct { m: [2]u8 };\n" + "type outv = struct { i: inner };\n" + "export fn main() i32 = {\n" + " let o: outv;\n" + " o.i.m[1] = 55u8;\n" + " return o.i.m[1]: i32;\n" + "};\n", 55 }, + /* D1 local, ABI size 1 ([1]u8) — the tightest sub-8 case. */ + { "d1_local_1", + "package main;\n" + "type inner = struct { m: [1]u8 };\n" + "type outv = struct { i: inner };\n" + "export fn main() i32 = {\n" + " let o: outv;\n" + " o.i.m[0] = 44u8;\n" + " return o.i.m[0]: i32;\n" + "};\n", 44 }, + /* D2 global, ABI size 4. Pre-fix wwstage emitted DATAW of 8 zero + * bytes vs cstage's 4 -> .s differ. */ + { "d2_global_4", + "package main;\n" + "type inner = struct { m: [4]u8 };\n" + "type outv = struct { i: inner };\n" + "let g: outv;\n" + "export fn main() i32 = {\n" + " g.i.m[0] = 66u8;\n" + " return g.i.m[0]: i32;\n" + "};\n", 66 }, + /* D2 global, ABI size 2. */ + { "d2_global_2", + "package main;\n" + "type inner = struct { m: [2]u8 };\n" + "type outv = struct { i: inner };\n" + "let g: outv;\n" + "export fn main() i32 = {\n" + " g.i.m[1] = 55u8;\n" + " return g.i.m[1]: i32;\n" + "};\n", 55 }, + /* D2 global, ABI size 1. */ + { "d2_global_1", + "package main;\n" + "type inner = struct { m: [1]u8 };\n" + "type outv = struct { i: inner };\n" + "let g: outv;\n" + "export fn main() i32 = {\n" + " g.i.m[0] = 44u8;\n" + " return g.i.m[0]: i32;\n" + "};\n", 44 }, + /* NEGATIVE control — a >8 (multi-word) value-struct still zero- + * inits. Read an UNWRITTEN byte: a working multi-word zero-init + * fill makes it 0. If the fix had wrongly suppressed the >8 zero + * arm, this would read stack garbage (and byte-id would diff + * against the still-zeroing cstage). Local + global both proven. */ + { "ctl_local_16", + "package main;\n" + "type inner = struct { m: [16]u8 };\n" + "type outv = struct { i: inner };\n" + "export fn main() i32 = {\n" + " let o: outv;\n" + " return o.i.m[7]: i32;\n" + "};\n", 0 }, + { "ctl_global_16", + "package main;\n" + "type inner = struct { m: [16]u8 };\n" + "type outv = struct { i: inner };\n" + "let g: outv;\n" + "export fn main() i32 = {\n" + " return g.i.m[7]: i32;\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, "valstruct_subsize: 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/wwvss_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwvss_%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); + + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwvss_%d_%d_cs.s", + getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwvss_%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 valstruct-subsize tests failed\n", + fail, n); + return 1; + } + printf("valstruct_subsize: %d/%d ok (cstage run + cs==ww byte-id)\n", + n, n); + return 0; +}