From 93d8ece740ee3b1d187b7d860acbbe79c0a91b1a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 1 Jun 2026 17:29:32 +0900 Subject: [PATCH] wwstage: size struct tuple-field slot via fieldsize (#237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wwstage checker `fieldslotsize` (check.ww) summed each struct field's SLOT width to stamp the enclosing struct's tinfo.slotsize, but had no TY_TUPLE arm — a tuple-typed field fell through to the 8B default. So `struct { f: ([]u8,[]u8) }` stamped slotsize=8 while size=48 (the natural element sum, correct). A `let s: S` slot is allocated off ti.slotsize (cgenutil.ww slotsize), so wwstage reserved an 8-byte frame slot for a 48-byte struct: a SILENT stack-corrupting miscompile. cstage has no size/slotsize split — it sizes the field at f->type->size=48 throughout — so the stages diverged on the emitted frame ($16 wwstage vs $64 cstage), invisible to a cstage-only check and caught only by cs==ww byte-id (rule 10). Add the TY_TUPLE arm (return the tuple's own slotsize, the per-element slot sum already stamped at the N_TTUPLE arm with slices at 24 each). This aligns the checker's field-slotsize with cgenutil.ww fieldsize, which already returns the tuple's natural size (48). The stale comment claiming "TY_TUPLE inside a struct currently defaults to 8 in cgenutil" is removed — fieldsize stopped defaulting to 8 at the 2026-05-23 review. Test 930 pins cs==ww .s byte-id for a struct with a tuple field (with and without a leading scalar field, foff 0 and !=0); pure frame-size gate, no runtime — the divergence is fully visible in the emitted assembly. No selfhost source has a tuple-typed struct field, so the w6c/wwdump combined amalgams regen with no asm change (byte-id-neutral bootstrap). --- Makefile | 7 ++ selfhost/cmd/w6c/main.combined.ww | 11 +- selfhost/cmd/wcc/check.ww | 11 +- selfhost/cmd/wwdump/main.combined.ww | 11 +- test/wcc/930_struct_tuple_field_slot.c | 143 +++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 test/wcc/930_struct_tuple_field_slot.c diff --git a/Makefile b/Makefile index b9ad1cf0..e49148f3 100644 --- a/Makefile +++ b/Makefile @@ -364,6 +364,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_len_strglobal_run \ $(BIN)/test_tuple_sret_callee \ $(BIN)/test_tuple_sret_receive_run \ + $(BIN)/test_struct_tuple_field_slot \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ $(BIN)/test_single_field_struct_zeroinit \ @@ -879,6 +880,12 @@ $(BIN)/test_tuple_sret_receive_run: test/wcc/799_tuple_sret_receive_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #237: a tuple-typed struct field must contribute its real slot width to +# the enclosing struct's slotsize — pure cs==ww .s byte-id (frame size). +$(BIN)/test_struct_tuple_field_slot: test/wcc/930_struct_tuple_field_slot.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #15: widening a bare *vtable into a NAMED-alias variant (`stream` = # *vtable) of `(file | stream)` must compute the right tag, not default # to tag 0. Both-stage byte-id + runtime, plus a degenerate-ambiguity diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 162cacfd..1f37252c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11762,10 +11762,15 @@ fn fieldslotsize(ft: *tinfo) u64 = { if (fk == tykind.TY_PTR || fk == tykind.TY_FN || fk == tykind.TY_CHAN) { return 8u64; }; if (fk == tykind.TY_STR) { return t.size; }; + // #237: a tuple-typed struct field carries its own slot total (the + // per-element slot sum stamped at the N_TTUPLE arm above — slices at + // 24 each). Without this it fell to the 8B default below, undersizing + // the enclosing struct's slotsize (size stayed correct), so a `let s:S` + // slot was too small — a silent stack-corrupting miscompile. Aligns + // with cgenutil.ww fieldsize, which already returns the tuple's size. + if (fk == tykind.TY_TUPLE) { return t.slotsize; }; // Primitives keep natural width inside structs (matches - // cgenutil fieldsize: primsize, not pad-to-8). TY_TUPLE inside a - // struct currently defaults to 8 in cgenutil — preserve that - // shape until a future graduation aligns the two. + // cgenutil fieldsize: primsize, not pad-to-8). if (fk == tykind.TY_BOOL || fk == tykind.TY_RUNE || fk == tykind.TY_I8 || fk == tykind.TY_I16 || fk == tykind.TY_I32 || fk == tykind.TY_I64 || diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index b5af2dd8..46963096 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1512,10 +1512,15 @@ fn fieldslotsize(ft: *tinfo) u64 = { if (fk == tykind.TY_PTR || fk == tykind.TY_FN || fk == tykind.TY_CHAN) { return 8u64; }; if (fk == tykind.TY_STR) { return t.size; }; + // #237: a tuple-typed struct field carries its own slot total (the + // per-element slot sum stamped at the N_TTUPLE arm above — slices at + // 24 each). Without this it fell to the 8B default below, undersizing + // the enclosing struct's slotsize (size stayed correct), so a `let s:S` + // slot was too small — a silent stack-corrupting miscompile. Aligns + // with cgenutil.ww fieldsize, which already returns the tuple's size. + if (fk == tykind.TY_TUPLE) { return t.slotsize; }; // Primitives keep natural width inside structs (matches - // cgenutil fieldsize: primsize, not pad-to-8). TY_TUPLE inside a - // struct currently defaults to 8 in cgenutil — preserve that - // shape until a future graduation aligns the two. + // cgenutil fieldsize: primsize, not pad-to-8). if (fk == tykind.TY_BOOL || fk == tykind.TY_RUNE || fk == tykind.TY_I8 || fk == tykind.TY_I16 || fk == tykind.TY_I32 || fk == tykind.TY_I64 || diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3fe9f2ec..bec52b3d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11762,10 +11762,15 @@ fn fieldslotsize(ft: *tinfo) u64 = { if (fk == tykind.TY_PTR || fk == tykind.TY_FN || fk == tykind.TY_CHAN) { return 8u64; }; if (fk == tykind.TY_STR) { return t.size; }; + // #237: a tuple-typed struct field carries its own slot total (the + // per-element slot sum stamped at the N_TTUPLE arm above — slices at + // 24 each). Without this it fell to the 8B default below, undersizing + // the enclosing struct's slotsize (size stayed correct), so a `let s:S` + // slot was too small — a silent stack-corrupting miscompile. Aligns + // with cgenutil.ww fieldsize, which already returns the tuple's size. + if (fk == tykind.TY_TUPLE) { return t.slotsize; }; // Primitives keep natural width inside structs (matches - // cgenutil fieldsize: primsize, not pad-to-8). TY_TUPLE inside a - // struct currently defaults to 8 in cgenutil — preserve that - // shape until a future graduation aligns the two. + // cgenutil fieldsize: primsize, not pad-to-8). if (fk == tykind.TY_BOOL || fk == tykind.TY_RUNE || fk == tykind.TY_I8 || fk == tykind.TY_I16 || fk == tykind.TY_I32 || fk == tykind.TY_I64 || diff --git a/test/wcc/930_struct_tuple_field_slot.c b/test/wcc/930_struct_tuple_field_slot.c new file mode 100644 index 00000000..299eb463 --- /dev/null +++ b/test/wcc/930_struct_tuple_field_slot.c @@ -0,0 +1,143 @@ +/* + * 930_struct_tuple_field_slot — project #237: a tuple-typed struct field + * must contribute its real slot width to the enclosing struct's slotsize. + * + * wwstage's checker `fieldslotsize` (check.ww) summed struct field SLOT + * sizes to stamp a struct's tinfo.slotsize, but had no TY_TUPLE arm — a + * tuple field fell to the 8B default. So `S = struct { f: ([]u8,[]u8) }` + * stamped slotsize=8 while size=48 (the natural sum, correct). A `let s:S` + * slot is allocated off ti.slotsize (cgenutil.ww slotsize), so wwstage + * reserved an 8-byte frame slot for a 48-byte struct: a SILENT stack- + * corrupting miscompile. cstage has no size/slotsize split (it sizes the + * field at f->type->size=48 throughout), so the two stages DIVERGED on the + * emitted frame ($16 wwstage vs $64 cstage) — invisible to a cstage-only + * check, caught only by cs==ww byte-id (rule 10). The fix adds the TY_TUPLE + * arm (return the tuple's own slotsize), aligning fieldslotsize with the + * cgen-side cgenutil.ww fieldsize that already returns 48. + * + * GATE: pure cs==ww .s byte-id. No runtime — the divergence is a frame/slot + * SIZE, fully visible in the emitted assembly. The fixture takes &s.f so the + * struct slot is materialised; no over-cap call is involved (this isolates + * #237 from the #234 store-routing). A FAIL means the stages disagree on the + * struct's frame size (the #237 regression). + */ +#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; +} + +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; +} + +struct row { const char *label; const char *src; }; + +static const struct row rows[] = { + /* one tuple field: slotsize must be 48, not the 8B default. */ + { "single_tuple_field", + "package main;\n" + "type S = struct { f: ([]u8, []u8) };\n" + "export fn main() i32 = {\n" + " let s: S;\n" + " let p: *int = (&s.f): *int;\n" + " p[1] = 1;\n" + " return 0;\n" + "};\n" }, + /* tuple field preceded by a scalar: foff!=0, slot still full-width. */ + { "hdr_then_tuple_field", + "package main;\n" + "type S = struct { hdr: int, f: ([]u8, []u8) };\n" + "export fn main() i32 = {\n" + " let s: S;\n" + " s.hdr = 9;\n" + " let p: *int = (&s.f): *int;\n" + " p[1] = 1;\n" + " return 0;\n" + "};\n" }, + { NULL, NULL }, +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[256]; + 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, "struct_tuple_field_slot: 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], cs_s[64], ws_s[64], cmd[2048]; + snprintf(src, sizeof src, "/tmp/stfs_%d_%d.ww", getpid(), i); + snprintf(cs_s, sizeof cs_s, "/tmp/stfs_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/stfs_%d_%d_ww.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + 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 " + "(#237 struct tuple-field slot-size regression)\n", + rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d struct-tuple-field-slot tests failed\n", + fail, n); + return 1; + } + printf("struct_tuple_field_slot: %d/%d ok (cs==ww byte-id)\n", n, n); + return 0; +}