From 66a91c8969ddd821671c9500ebb97ab91fd34ca3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 14:07:58 +0900 Subject: [PATCH] wcc: converge let-IDENT memcpy, IDENT-assign recv, nested struct call-recv onto structabisize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more wwstage cgen sites still used unrounded structnaturalsize where cstage rounds via lu->size — pre-existing gate-blind cs!=ww latents the #169b reviewer surfaced: cgenstmt N_LET struct-IDENT memcpy (let p2: T = p1; twin cgen.c:7869), cgenexpr N_ASSIGN N_IDENT-lhs register RECV (s = mk(); twin cgen.c:4700-4737), and cgenutil's nested struct N_CALL recv inside cgstructlitfill (twin cgen.c:2121). Converge all three onto structabisize, completing the same-class closure started by #169 and continued by #169b. Also corrected the inline comment at cgenutil.ww:3273-3286 that wrongly claimed fl->type->size was natural (check.c:760 sets ABI). sretretsize at cgenutil.ww:1301 is gate-equivalent natural and is left alone. Probe 698 +3 rows (one per converged site) with cs==ww .s byte-cmp and a pre-fix-rebuild discriminator. 990-997 byte-id hold. --- selfhost/cmd/w6c/main.combined.ww | 52 +++++++++++++++++----------- selfhost/cmd/wcc/cgenexpr.ww | 11 ++++-- selfhost/cmd/wcc/cgenstmt.ww | 12 +++++-- selfhost/cmd/wcc/cgenutil.ww | 29 ++++++++-------- selfhost/cmd/wwdump/main.combined.ww | 52 +++++++++++++++++----------- test/wcc/698_cgreturn_struct.c | 46 ++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 60 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 091620fe..75d7028c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17337,21 +17337,22 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, if (primsize(fi.tnode.str) == 0) { let csi: *structinfo = structlookup(c, fi.tnode.str); if (csi != nil) { - // Use the inner struct's - // NATURAL size (no 8B slot - // rounding) so MOVL/MOVW/ - // MOVB tail dispatch matches - // cstage's fl->type->size - // (which is natural per - // check.c). fi.fsz here is + // Inner struct's ABI size + // (maxalign-rounded) — cstage + // reads fl->type->size at the + // nested-call branch + // (cgen.c:2121); check.c:760 + // sets that to the + // maxalign-rounded ABI extent + // (NOT natural). fi.fsz is // wwstage's slot-padded - // totsize — using it would - // emit 2× MOVQ where cstage - // emits MOVQ+MOVL for a - // 12B inner, etc. (task #15 - // territory; sidestepped - // locally.) - let cfsz: i32 = structnaturalsize(csi); + // totsize (round-to-8); the + // pre-#169 structnaturalsize + // shorts struct{i64,i32} + // (natural 12, ABI 16) to + // MOVQ+MOVL where cstage + // writes MOVQ+MOVQ. + let cfsz: i32 = structabisize(csi); let crem: i32 = cfsz - (cfsz / 8) * 8; if (cfsz <= 24) { if (crem == 0 || crem == 1 @@ -24267,9 +24268,14 @@ fn cgassign(c: *cgen, n: *node) void = { if (lcsname.len > 0) { let lcsi: *structinfo = structlookup(c, lcsname); if (lcsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lcnsz: i32 = structnaturalsize(lcsi); + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize (cstage + // N_ASSIGN-IDENT branch sets sz = lu->size, + // cgen.c:4704; check.c:760 SSoT). The pre- + // #169 structnaturalsize shorts struct{i64,i32} + // (natural 12, ABI 16) to MOVQ+MOVL where + // cstage writes MOVQ+MOVQ. + let lcnsz: i32 = structabisize(lcsi); if (n.op == tkind.TK_ASSIGN) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { @@ -25876,8 +25882,8 @@ fn cglet(c: *cgen, n: *node) void = { // Struct ident copy: `let p2: T = p1;` where T is a struct // >8B and rhs is a local ident. Per-qword MOVQ from src // slot to dst slot, with a sized tail (MOVL/MOVB) for - // natural sizes that aren't 8-aligned (e.g. `struct - // { i32, i32, i32 }` is 12B). Pre-fix this path fell + // ABI sizes that aren't 8-aligned (e.g. `struct + // { i32, i32, i32 }`, maxalign 4 → ABI 12B). Pre-fix this path fell // through to `cgexpr + MOVQ AX, off(BP)` which stored // only the first qword (and a stale BX for sz==16 lets // via the str-init tail) — silent partial copy. Mirrors @@ -25891,7 +25897,13 @@ fn cglet(c: *cgen, n: *node) void = { if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { - let lsz: i32 = structnaturalsize(lsi); + // memcpy run sizes on the maxalign-rounded ABI + // size — cstage N_LET sets sz = lu->size + // (cgen.c:7533, struct-IDENT branch :7869), + // check.c:760 SSoT. structnaturalsize would + // short struct{i64,i32} (natural 12, ABI 16) + // to MOVQ+MOVL where cstage writes MOVQ+MOVQ. + let lsz: i32 = structabisize(lsi); if (lsz > 8) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index fb164fd4..417dd37c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -6739,9 +6739,14 @@ fn cgassign(c: *cgen, n: *node) void = { if (lcsname.len > 0) { let lcsi: *structinfo = structlookup(c, lcsname); if (lcsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lcnsz: i32 = structnaturalsize(lcsi); + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize (cstage + // N_ASSIGN-IDENT branch sets sz = lu->size, + // cgen.c:4704; check.c:760 SSoT). The pre- + // #169 structnaturalsize shorts struct{i64,i32} + // (natural 12, ABI 16) to MOVQ+MOVL where + // cstage writes MOVQ+MOVQ. + let lcnsz: i32 = structabisize(lcsi); if (n.op == tkind.TK_ASSIGN) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 69f53189..599b9cf9 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -1358,8 +1358,8 @@ fn cglet(c: *cgen, n: *node) void = { // Struct ident copy: `let p2: T = p1;` where T is a struct // >8B and rhs is a local ident. Per-qword MOVQ from src // slot to dst slot, with a sized tail (MOVL/MOVB) for - // natural sizes that aren't 8-aligned (e.g. `struct - // { i32, i32, i32 }` is 12B). Pre-fix this path fell + // ABI sizes that aren't 8-aligned (e.g. `struct + // { i32, i32, i32 }`, maxalign 4 → ABI 12B). Pre-fix this path fell // through to `cgexpr + MOVQ AX, off(BP)` which stored // only the first qword (and a stale BX for sz==16 lets // via the str-init tail) — silent partial copy. Mirrors @@ -1373,7 +1373,13 @@ fn cglet(c: *cgen, n: *node) void = { if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { - let lsz: i32 = structnaturalsize(lsi); + // memcpy run sizes on the maxalign-rounded ABI + // size — cstage N_LET sets sz = lu->size + // (cgen.c:7533, struct-IDENT branch :7869), + // check.c:760 SSoT. structnaturalsize would + // short struct{i64,i32} (natural 12, ABI 16) + // to MOVQ+MOVL where cstage writes MOVQ+MOVQ. + let lsz: i32 = structabisize(lsi); if (lsz > 8) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 22e4fcd6..0b303324 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -3270,21 +3270,22 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, if (primsize(fi.tnode.str) == 0) { let csi: *structinfo = structlookup(c, fi.tnode.str); if (csi != nil) { - // Use the inner struct's - // NATURAL size (no 8B slot - // rounding) so MOVL/MOVW/ - // MOVB tail dispatch matches - // cstage's fl->type->size - // (which is natural per - // check.c). fi.fsz here is + // Inner struct's ABI size + // (maxalign-rounded) — cstage + // reads fl->type->size at the + // nested-call branch + // (cgen.c:2121); check.c:760 + // sets that to the + // maxalign-rounded ABI extent + // (NOT natural). fi.fsz is // wwstage's slot-padded - // totsize — using it would - // emit 2× MOVQ where cstage - // emits MOVQ+MOVL for a - // 12B inner, etc. (task #15 - // territory; sidestepped - // locally.) - let cfsz: i32 = structnaturalsize(csi); + // totsize (round-to-8); the + // pre-#169 structnaturalsize + // shorts struct{i64,i32} + // (natural 12, ABI 16) to + // MOVQ+MOVL where cstage + // writes MOVQ+MOVQ. + let cfsz: i32 = structabisize(csi); let crem: i32 = cfsz - (cfsz / 8) * 8; if (cfsz <= 24) { if (crem == 0 || crem == 1 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 8d4664b4..51560491 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17337,21 +17337,22 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, if (primsize(fi.tnode.str) == 0) { let csi: *structinfo = structlookup(c, fi.tnode.str); if (csi != nil) { - // Use the inner struct's - // NATURAL size (no 8B slot - // rounding) so MOVL/MOVW/ - // MOVB tail dispatch matches - // cstage's fl->type->size - // (which is natural per - // check.c). fi.fsz here is + // Inner struct's ABI size + // (maxalign-rounded) — cstage + // reads fl->type->size at the + // nested-call branch + // (cgen.c:2121); check.c:760 + // sets that to the + // maxalign-rounded ABI extent + // (NOT natural). fi.fsz is // wwstage's slot-padded - // totsize — using it would - // emit 2× MOVQ where cstage - // emits MOVQ+MOVL for a - // 12B inner, etc. (task #15 - // territory; sidestepped - // locally.) - let cfsz: i32 = structnaturalsize(csi); + // totsize (round-to-8); the + // pre-#169 structnaturalsize + // shorts struct{i64,i32} + // (natural 12, ABI 16) to + // MOVQ+MOVL where cstage + // writes MOVQ+MOVQ. + let cfsz: i32 = structabisize(csi); let crem: i32 = cfsz - (cfsz / 8) * 8; if (cfsz <= 24) { if (crem == 0 || crem == 1 @@ -24267,9 +24268,14 @@ fn cgassign(c: *cgen, n: *node) void = { if (lcsname.len > 0) { let lcsi: *structinfo = structlookup(c, lcsname); if (lcsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lcnsz: i32 = structnaturalsize(lcsi); + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize (cstage + // N_ASSIGN-IDENT branch sets sz = lu->size, + // cgen.c:4704; check.c:760 SSoT). The pre- + // #169 structnaturalsize shorts struct{i64,i32} + // (natural 12, ABI 16) to MOVQ+MOVL where + // cstage writes MOVQ+MOVQ. + let lcnsz: i32 = structabisize(lcsi); if (n.op == tkind.TK_ASSIGN) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { @@ -25876,8 +25882,8 @@ fn cglet(c: *cgen, n: *node) void = { // Struct ident copy: `let p2: T = p1;` where T is a struct // >8B and rhs is a local ident. Per-qword MOVQ from src // slot to dst slot, with a sized tail (MOVL/MOVB) for - // natural sizes that aren't 8-aligned (e.g. `struct - // { i32, i32, i32 }` is 12B). Pre-fix this path fell + // ABI sizes that aren't 8-aligned (e.g. `struct + // { i32, i32, i32 }`, maxalign 4 → ABI 12B). Pre-fix this path fell // through to `cgexpr + MOVQ AX, off(BP)` which stored // only the first qword (and a stale BX for sz==16 lets // via the str-init tail) — silent partial copy. Mirrors @@ -25891,7 +25897,13 @@ fn cglet(c: *cgen, n: *node) void = { if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { - let lsz: i32 = structnaturalsize(lsi); + // memcpy run sizes on the maxalign-rounded ABI + // size — cstage N_LET sets sz = lu->size + // (cgen.c:7533, struct-IDENT branch :7869), + // check.c:760 SSoT. structnaturalsize would + // short struct{i64,i32} (natural 12, ABI 16) + // to MOVQ+MOVL where cstage writes MOVQ+MOVQ. + let lsz: i32 = structabisize(lsi); if (lsz > 8) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { diff --git a/test/wcc/698_cgreturn_struct.c b/test/wcc/698_cgreturn_struct.c index c1d22168..2b810fc2 100644 --- a/test/wcc/698_cgreturn_struct.c +++ b/test/wcc/698_cgreturn_struct.c @@ -242,6 +242,52 @@ static const struct row rows[] = { " return z.a + z.b + z.c;\n" "};\n", 0 }, + /* `let p2: T = p1;` ident memcpy. struct{i64,i32} natural 12, + * maxalign 8 → cstage N_LET sets sz = lu->size = 16 (cgen.c:7533, + * struct-IDENT branch :7869) and emits MOVQ+MOVQ for the per-qword + * copy. Pre-fix wwstage sized the run on structnaturalsize (12) → + * MOVQ+MOVL. The byte-id catches the 4B widen; both stages + * round-trip the value correctly because the high 4B of the rhs + * slot were zero-init'd by the source let. */ + { "let_ident_memcpy_pair_i64_i32", + "type pt = struct { a: i64, b: i32 };\n" + "fn main() i32 = {\n" + " let p1: pt = pt { a = 7i64, b = 9i32 };\n" + " let p2: pt = p1;\n" + " return p2.b - 9;\n" + "};\n", + 0 }, + /* `s = mk();` ident-lhs register RECV (sibling of dot_recv_call_*). + * struct{i64,i32} natural 12, maxalign 8 → cstage N_ASSIGN-IDENT + * sets sz = lu->size = 16 (cgen.c:4704) and stores AX/DX with + * MOVQ+MOVQ. Pre-fix wwstage used structnaturalsize (12) → + * MOVQ+MOVL. byte-id discriminator regardless of value readback. */ + { "assign_ident_call_recv_pair_i64_i32", + "type pt = struct { a: i64, b: i32 };\n" + "fn mk() pt = { return pt { a = 7i64, b = 9i32 }; };\n" + "fn main() i32 = {\n" + " let s: pt = pt { a = 0i64, b = 0i32 };\n" + " s = mk();\n" + " return s.b - 9;\n" + "};\n", + 0 }, + /* Nested struct N_CALL inside a structlit field: the outer literal + * has field `v` whose type is struct{i64,i32}; the value is mk(). + * cstage's cg_structlit_fill reads fl->type->size at the nested- + * call branch (cgen.c:2121) — per check.c:760 that's the maxalign- + * rounded ABI extent (16). Pre-fix wwstage cgstructlitfill used + * structnaturalsize (12) → MOVQ+MOVL where cstage emits MOVQ+MOVQ. + * mk returns via a structlit so the producer side stays out of the + * comparison; only the nested-call-recv ladder shifts. */ + { "structlit_nested_call_recv_pair_i64_i32", + "type pt = struct { a: i64, b: i32 };\n" + "type box = struct { v: pt };\n" + "fn mk() pt = { return pt { a = 7i64, b = 9i32 }; };\n" + "fn main() i32 = {\n" + " let b: box = box { v = mk() };\n" + " return b.v.b - 9;\n" + "};\n", + 0 }, }; static int