diff --git a/Makefile b/Makefile index 217ce3ec..f944e386 100644 --- a/Makefile +++ b/Makefile @@ -257,6 +257,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tupfieldsize_run \ $(BIN)/test_tagtupfieldsize_run \ $(BIN)/test_nestfield_run \ + $(BIN)/test_structlocal_frame \ $(BIN)/test_arrlit_tail_zero_run \ $(BIN)/test_defdim_slice_run \ $(BIN)/test_trystr_run \ @@ -776,6 +777,18 @@ $(BIN)/test_nestfield_run: test/wcc/989_nestfield_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_structlocal_frame (#75): a sub-8 nested-composite struct LOCAL must +# reserve its frame slot at the struct's NATURAL size (round8(ti.size)), +# not the slot-padded ti.slotsize. wwstage's slotsize() TY_STRUCT arm +# over-reserved, so outer{a:u8,p:inner{x:u8,y:u8},z:i64} emitted frame $32 +# / -24(BP) vs cstage's $16 / -16(BP) — runtime-invisible (both exit 0) +# but a cs!=ww .s divergence (rule 10) and a latent byte-id landmine. The +# FRAME-ABSOLUTE twin of the RUNTIME 989_nestfield_run: emits .s via w6c +# and w6c_ww and byte-diffs (no link, no run). +$(BIN)/test_structlocal_frame: test/wcc/989_structlocal_frame.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_arrlit_tail_zero_run (#13): an under-length array literal zero-fills the # unspecified tail, not the last value. Builds+runs on BOTH driver twins # (rule-10), pinning the absolute value (pre-fix ww tail = last value). diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 093c573e..048c7299 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20353,8 +20353,19 @@ fn slotsize(c: *cgen, typn: *node) i32 = { kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { return ti.size: i32; }; - if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE || - kk == tykind.TY_ARRAY) { + // #75: a struct LOCAL's stack slot is the struct's NATURAL size + // rounded up to the 8B slot grain — NOT ti.slotsize, which sums the + // slot-padded field widths and over-reserves on sub-8 nested + // composites (e.g. outer{a:u8, p:inner{x:u8,y:u8}, z:i64} → ww $32 vs + // cstage $16). cstage reserves the local at f->type->size (cmd/w6c/ + // cgen.c), i.e. the checker's natural r.size (check.ww:2256). Same + // dual-SSoT leak as #44 (field-offset) / #55, one notion over. The + // TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element + // stride, #48 [N]Alias 24B) keep ti.slotsize — deliberate divergences. + if (kk == tykind.TY_STRUCT) { + return ((ti.size + 7u64) & ~7u64): i32; + }; + if (kk == tykind.TY_TUPLE || kk == tykind.TY_ARRAY) { return ti.slotsize: i32; }; return 8; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index bdb3d5ef..e26af541 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2559,8 +2559,19 @@ fn slotsize(c: *cgen, typn: *node) i32 = { kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { return ti.size: i32; }; - if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE || - kk == tykind.TY_ARRAY) { + // #75: a struct LOCAL's stack slot is the struct's NATURAL size + // rounded up to the 8B slot grain — NOT ti.slotsize, which sums the + // slot-padded field widths and over-reserves on sub-8 nested + // composites (e.g. outer{a:u8, p:inner{x:u8,y:u8}, z:i64} → ww $32 vs + // cstage $16). cstage reserves the local at f->type->size (cmd/w6c/ + // cgen.c), i.e. the checker's natural r.size (check.ww:2256). Same + // dual-SSoT leak as #44 (field-offset) / #55, one notion over. The + // TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element + // stride, #48 [N]Alias 24B) keep ti.slotsize — deliberate divergences. + if (kk == tykind.TY_STRUCT) { + return ((ti.size + 7u64) & ~7u64): i32; + }; + if (kk == tykind.TY_TUPLE || kk == tykind.TY_ARRAY) { return ti.slotsize: i32; }; return 8; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bfbf6552..fcab0b05 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20353,8 +20353,19 @@ fn slotsize(c: *cgen, typn: *node) i32 = { kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { return ti.size: i32; }; - if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE || - kk == tykind.TY_ARRAY) { + // #75: a struct LOCAL's stack slot is the struct's NATURAL size + // rounded up to the 8B slot grain — NOT ti.slotsize, which sums the + // slot-padded field widths and over-reserves on sub-8 nested + // composites (e.g. outer{a:u8, p:inner{x:u8,y:u8}, z:i64} → ww $32 vs + // cstage $16). cstage reserves the local at f->type->size (cmd/w6c/ + // cgen.c), i.e. the checker's natural r.size (check.ww:2256). Same + // dual-SSoT leak as #44 (field-offset) / #55, one notion over. The + // TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element + // stride, #48 [N]Alias 24B) keep ti.slotsize — deliberate divergences. + if (kk == tykind.TY_STRUCT) { + return ((ti.size + 7u64) & ~7u64): i32; + }; + if (kk == tykind.TY_TUPLE || kk == tykind.TY_ARRAY) { return ti.slotsize: i32; }; return 8; diff --git a/test/wcc/989_structlocal_frame.c b/test/wcc/989_structlocal_frame.c new file mode 100644 index 00000000..3c195c11 --- /dev/null +++ b/test/wcc/989_structlocal_frame.c @@ -0,0 +1,190 @@ +/* + * 989_structlocal_frame (#75) — a struct LOCAL's stack slot must be the + * struct's NATURAL size rounded to the 8B grain, identically in both + * stages. wwstage's slotsize() TY_STRUCT arm (selfhost/cmd/wcc/ + * cgenutil.ww) returned ti.slotsize — the SUM of the slot-padded field + * widths — so a sub-8 nested-composite struct local over-reserved: a + * nested inner{x:u8,y:u8} (size 2, slotsize 8) padded its in-struct + * footprint, and the local's frame slot inherited that pad. cstage has + * no slotsize SSoT — it reserves the local at f->type->size, the + * checker's NATURAL r.size (cmd/w6c/cgen.c). So on outer{a:u8, + * p:inner{x:u8,y:u8}, z:i64} ww emitted frame $32 / struct-base -24(BP) + * while cstage emitted $16 / -16(BP): a uniform -8 BP shift on every + * field access. Both stages exit 0 (each self-consistent), so it is + * runtime-invisible — but it is a cs!=ww .s divergence (rule 10) and a + * latent byte-id gate-landmine the day such a struct enters the corpus. + * Same dual-SSoT leak as #44 (field-OFFSET) / #55, one notion over: + * struct-local-slot-SIZE. + * + * THE FIX: slotsize()'s TY_STRUCT arm returns round8(ti.size). The + * TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element + * stride, #48) keep ti.slotsize — deliberate, ruled divergences. + * + * This is the FRAME-ABSOLUTE proof: each fixture is compiled through + * cstage w6c AND wwstage w6c_ww and the emitted .s is byte-diffed (the + * 683 asm_byte_identical pattern). Byte-identity subsumes the frame: + * pre-fix nested3 differs at `TEXT main,$32` vs `$16`, `SUBQ $32` vs + * `$16`, and every `-24(BP)` vs `-16(BP)`; post-fix the two .s are + * byte-for-byte equal. The sibling 989_nestfield_run is the RUNTIME + * exit-code proof (deliberately NOT frame-gated — see its header); this + * file is the .s/frame-absolute one #75 owes. + */ +#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; }; + +static const struct row rows[] = { + /* nested3 — the core divergence. outer{a:u8, p:inner{x:u8,y:u8}, + * z:i64}: natural size 16 (a@0, p@1 packed, z@8), slot-padded sum 24 + * (a:8 + p:8 + z:8). Pre-fix ww frame $32 / base -24(BP); cstage + * $16 / -16(BP). The successor z makes the -8 shift visible on a + * wide field too. */ + { "nested3", + "package main;\n" + "type inner = struct { x: u8, y: u8 };\n" + "type outer = struct { a: u8, p: inner, z: i64 };\n" + "export fn main() int = {\n" + " let o: outer = outer { a = 1, p = inner { x = 2, y = 3 }," + " z = 0x44444444i64 };\n" + " if (o.a: int != 1) { return 1; };\n" + " if (o.p.x: int != 2) { return 2; };\n" + " if (o.p.y: int != 3) { return 3; };\n" + " if (o.z != 0x44444444i64) { return 4; };\n" + " return 0;\n" + "};\n" }, + + /* tail-varied — same sub-8 nested composite, a narrower (u32) tail. + * outt{a:u8, p:inner{x:u8,y:u8}, w:u32}: natural size 8 (w@4), + * slot-padded sum larger. Proves the fix on a second sub-8 shape + * whose natural size differs from nested3's. */ + { "tail_u32", + "package main;\n" + "type inner = struct { x: u8, y: u8 };\n" + "type outt = struct { a: u8, p: inner, w: u32 };\n" + "export fn main() int = {\n" + " let o: outt = outt { a = 1, p = inner { x = 2, y = 3 }," + " w = 0x1234u32 };\n" + " if (o.p.y: int != 3) { return 3; };\n" + " if (o.w != 0x1234u32) { return 5; };\n" + " return 0;\n" + "};\n" }, + + /* flat control — flat{a:u8, b:i64}, no sub-8 composite field. + * Natural and slot-padded coincide (b@8 either way), so the .s is + * byte-id BEFORE and after; proves the fix leaves the common case + * unmoved (no spurious frame change on plain structs). */ + { "flat_ctl", + "package main;\n" + "type flat = struct { a: u8, b: i64 };\n" + "export fn main() int = {\n" + " let o: flat = flat { a = 9, b = 0x33333333i64 };\n" + " if (o.a: int != 9) { return 1; };\n" + " if (o.b != 0x33333333i64) { return 2; };\n" + " return 0;\n" + "};\n" }, +}; + +/* asm_byte_identical — emit .s via cstage w6c and wwstage w6c_ww and + * diff (the 683_arr_strslice_elem harness). Pre-#75 a sub-8 nested + * struct local over-reserved its frame slot on wwstage, so the .s + * diverged (frame size, SUBQ, every field BP offset); the natural-size + * slot makes the two byte-for-byte equal. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/slfr_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/slfr_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/slfr_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "structlocal_frame[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "structlocal_frame[%s]: w6c_ww errored\n", + r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + 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); + if (rc != 0) + fprintf(stderr, "structlocal_frame[%s]: cstage vs wwstage " + "asm differs (#75 struct-local frame)\n", r->label); + unlink(src); unlink(cs); unlink(ws); + 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 wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/w6c_ww", bin); + if (access(wdrv, X_OK) != 0) { + fprintf(stderr, "structlocal_frame: skip (no %s)\n", wdrv); + printf("structlocal_frame: skipped (no wwstage)\n"); + return 0; + } + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + + if (fail) { + fprintf(stderr, + "structlocal_frame: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("structlocal_frame: %d/%d ok\n", total, total); + return 0; +}