From d4e500f61cd7874300fdd5880e16578cf7a8cce0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 01:02:41 +0900 Subject: [PATCH] wcc: struct ABI size maxalign-rounded via structabisize (#169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage struct-return RECV and RETURN used unrounded / round-to-8 sizes where cstage uses the maxalign-rounded lu->size / rt->size, so a struct with maxalign 8 and a sub-8 tail (e.g. struct{i64,i32}) — or a maxalign<8 struct on the return path — unpacked with a different trailing-word width (MOVL vs MOVQ) between stages. Value-correct either way, but a cs!=ww asm divergence. Add a dedicated structabisize = round(natural, maxalign) used only at the two register-ABI sites. structnaturalsize stays unrounded: cstage's >24B sret and memory-move path (cgen.c:8150, Task #33) genuinely uses the unrounded natural size, so the two are different sizes — rounding the shared metric breaks 995. maxalign derives from each field's tinfo.align (mirrors cstage check.c:708), not an fsz ladder (a ladder over-rounds composite [N]u8 fields). Gate-blind (no bootstrap struct hits the maxalign-8+tail shape) — the discriminator is the cs==ww .s byte-cmp; covered by probe 698. --- selfhost/cmd/w6c/main.combined.ww | 76 ++++++++++++++++++++++------ selfhost/cmd/wcc/cgenstmt.ww | 14 +++-- selfhost/cmd/wcc/cgenutil.ww | 62 +++++++++++++++++++---- selfhost/cmd/wwdump/main.combined.ww | 76 ++++++++++++++++++++++------ test/wcc/698_cgreturn_struct.c | 43 ++++++++++++++++ 5 files changed, 223 insertions(+), 48 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 402fbc61..8a106c1d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14990,18 +14990,19 @@ fn nodeprimwidth(c: *cgen, n: *node) i32 = { // ---- type-driven slot sizing ---------------------------------------- // structnaturalsize — type-natural size of `si`, i.e. max(foff + -// fsz) across declared fields. Mirrors cstage's `lu->size` for a -// TY_STRUCT (rounded only to the struct's maxalign). +// fsz) across declared fields, UNROUNDED. This is the memory-copy +// extent: cstage copies exactly these bytes for the >24B sret +// write-through (cgen.c:8150 `int sz; for fields end=foff+fsz`) and +// struct-to-struct moves, so a trailing narrow field (bool@32 in a +// 33B struct padded to 40) keeps its MOVB tail rather than widening +// to a slot-overrunning MOVQ. #33 fixed cstage to use this; ww +// mirrors it. The ≤24B register RECV/RETURN ABI wants a DIFFERENT +// number — see structabisize. // -// NOTE: si.totsize is mis-named — it's actually the *slot-padded* -// size (rounded up to 8 for stack-slot use; see registerstruct's -// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride, -// and similar consumers want that slot-padded number. The -// receive-side ABI (#5) and any future "TYPE size, not slot size" -// query wants the natural size. Until si.totsize is split into -// si.naturalsize + si.slotsize (tracked as the wwstage-sizing -// follow-up task), recover the type-natural size from the field -// chain here. +// NOTE: si.totsize is yet a THIRD metric — the slot-padded size +// (rounded up to 8 for stack-slot use; see registerstruct's tail +// `if ((off & 7) != 0) ...`). Frame allocation and [N]foo stride +// want that slot number. fn structnaturalsize(si: *structinfo) i32 = { if (si == nil) { return 0; }; let n: i32 = 0; @@ -15014,6 +15015,45 @@ fn structnaturalsize(si: *structinfo) i32 = { return n; }; +// structabisize — the ≤24B register-return ABI size of `si`: the +// natural extent rounded up to the struct's maxalign. SSoT-equal to +// cstage's `lu->size` (check.c:760 `(off+maxalign-1)&~(maxalign-1)`). +// Distinct from structnaturalsize because the register RECV/RETURN +// ABI packs the value into AX/DX/CX at 8-byte granularity: cstage +// writes/reads the tail at maxalign width (cgen.c:7720 `sz=lu->size`, +// :8230 `sz=rt->size`), so a maxalign==8 struct with a sub-8 tail +// (struct{i64,i32}, natural 12) round-trips as MOVQ+MOVQ (16), not +// MOVQ+MOVL (12). Used ONLY at those register-ABI sites; memory +// copies (sret >24B, struct ident-copy) and field-offset math stay +// on structnaturalsize. #169. +// +// maxalign comes from each field's TRUE alignment (tinfo.align), not +// the slot-padded fsz: a [N]u8 / sub-struct field has slot ≥8 but +// align 1, so an fsz ladder would over-round. Mirrors cstage's +// maxalign = max(ft->align) (check.c:708). +fn structabisize(si: *structinfo) i32 = { + if (si == nil) { return 0; }; + let n: i32 = 0; + let maxaln: i32 = 1; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let end: i32 = fi.foff + fi.fsz; + if (end > n) { n = end; }; + if (fi.tnode != nil) { + let ti: *tinfo = fi.tnode.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { + let aln: i32 = ti.align: i32; + if (aln > maxaln) { maxaln = aln; }; + }; + }; + fi = fi.finext; + }; + return (n + maxaln - 1) & ~(maxaln - 1); +}; + // sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B, // return its natural size; else 0. Tagged unions, tuples, str, // slices, scalars route through their existing register-return ABIs @@ -24949,7 +24989,10 @@ fn cgreturn(c: *cgen, n: *node) void = { if (rname.len > 0) { let rsi: *structinfo = structlookup(c, rname); if (rsi != nil) { - let rsz: i32 = rsi.totsize; + // ≤24B register RETURN: cstage sizes by rt->size + // (maxalign-rounded), not the slot-padded totsize + // (round-to-8) — see structabisize (#169). + let rsz: i32 = structabisize(rsi); if (rsz <= 24) { let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { @@ -25507,10 +25550,11 @@ fn cglet(c: *cgen, n: *node) void = { if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8) for - // stack-slot use; the receive ABI needs the - // TYPE's natural size — see structnaturalsize. - let lsz: i32 = structnaturalsize(lsi); + // ≤24B register RECV: the value arrives packed + // in AX/DX/CX, so size by the maxalign-rounded + // ABI size (cstage lu->size), not the natural + // extent — see structabisize (#169). + let lsz: i32 = structabisize(lsi); let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 7845f4c2..a542bd9c 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -656,7 +656,10 @@ fn cgreturn(c: *cgen, n: *node) void = { if (rname.len > 0) { let rsi: *structinfo = structlookup(c, rname); if (rsi != nil) { - let rsz: i32 = rsi.totsize; + // ≤24B register RETURN: cstage sizes by rt->size + // (maxalign-rounded), not the slot-padded totsize + // (round-to-8) — see structabisize (#169). + let rsz: i32 = structabisize(rsi); if (rsz <= 24) { let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { @@ -1214,10 +1217,11 @@ fn cglet(c: *cgen, n: *node) void = { if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8) for - // stack-slot use; the receive ABI needs the - // TYPE's natural size — see structnaturalsize. - let lsz: i32 = structnaturalsize(lsi); + // ≤24B register RECV: the value arrives packed + // in AX/DX/CX, so size by the maxalign-rounded + // ABI size (cstage lu->size), not the natural + // extent — see structabisize (#169). + let lsz: i32 = structabisize(lsi); let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 4433b5a1..353a22da 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1168,18 +1168,19 @@ fn nodeprimwidth(c: *cgen, n: *node) i32 = { // ---- type-driven slot sizing ---------------------------------------- // structnaturalsize — type-natural size of `si`, i.e. max(foff + -// fsz) across declared fields. Mirrors cstage's `lu->size` for a -// TY_STRUCT (rounded only to the struct's maxalign). +// fsz) across declared fields, UNROUNDED. This is the memory-copy +// extent: cstage copies exactly these bytes for the >24B sret +// write-through (cgen.c:8150 `int sz; for fields end=foff+fsz`) and +// struct-to-struct moves, so a trailing narrow field (bool@32 in a +// 33B struct padded to 40) keeps its MOVB tail rather than widening +// to a slot-overrunning MOVQ. #33 fixed cstage to use this; ww +// mirrors it. The ≤24B register RECV/RETURN ABI wants a DIFFERENT +// number — see structabisize. // -// NOTE: si.totsize is mis-named — it's actually the *slot-padded* -// size (rounded up to 8 for stack-slot use; see registerstruct's -// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride, -// and similar consumers want that slot-padded number. The -// receive-side ABI (#5) and any future "TYPE size, not slot size" -// query wants the natural size. Until si.totsize is split into -// si.naturalsize + si.slotsize (tracked as the wwstage-sizing -// follow-up task), recover the type-natural size from the field -// chain here. +// NOTE: si.totsize is yet a THIRD metric — the slot-padded size +// (rounded up to 8 for stack-slot use; see registerstruct's tail +// `if ((off & 7) != 0) ...`). Frame allocation and [N]foo stride +// want that slot number. fn structnaturalsize(si: *structinfo) i32 = { if (si == nil) { return 0; }; let n: i32 = 0; @@ -1192,6 +1193,45 @@ fn structnaturalsize(si: *structinfo) i32 = { return n; }; +// structabisize — the ≤24B register-return ABI size of `si`: the +// natural extent rounded up to the struct's maxalign. SSoT-equal to +// cstage's `lu->size` (check.c:760 `(off+maxalign-1)&~(maxalign-1)`). +// Distinct from structnaturalsize because the register RECV/RETURN +// ABI packs the value into AX/DX/CX at 8-byte granularity: cstage +// writes/reads the tail at maxalign width (cgen.c:7720 `sz=lu->size`, +// :8230 `sz=rt->size`), so a maxalign==8 struct with a sub-8 tail +// (struct{i64,i32}, natural 12) round-trips as MOVQ+MOVQ (16), not +// MOVQ+MOVL (12). Used ONLY at those register-ABI sites; memory +// copies (sret >24B, struct ident-copy) and field-offset math stay +// on structnaturalsize. #169. +// +// maxalign comes from each field's TRUE alignment (tinfo.align), not +// the slot-padded fsz: a [N]u8 / sub-struct field has slot ≥8 but +// align 1, so an fsz ladder would over-round. Mirrors cstage's +// maxalign = max(ft->align) (check.c:708). +fn structabisize(si: *structinfo) i32 = { + if (si == nil) { return 0; }; + let n: i32 = 0; + let maxaln: i32 = 1; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let end: i32 = fi.foff + fi.fsz; + if (end > n) { n = end; }; + if (fi.tnode != nil) { + let ti: *tinfo = fi.tnode.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { + let aln: i32 = ti.align: i32; + if (aln > maxaln) { maxaln = aln; }; + }; + }; + fi = fi.finext; + }; + return (n + maxaln - 1) & ~(maxaln - 1); +}; + // sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B, // return its natural size; else 0. Tagged unions, tuples, str, // slices, scalars route through their existing register-return ABIs diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 455e2654..9802f0f9 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14990,18 +14990,19 @@ fn nodeprimwidth(c: *cgen, n: *node) i32 = { // ---- type-driven slot sizing ---------------------------------------- // structnaturalsize — type-natural size of `si`, i.e. max(foff + -// fsz) across declared fields. Mirrors cstage's `lu->size` for a -// TY_STRUCT (rounded only to the struct's maxalign). +// fsz) across declared fields, UNROUNDED. This is the memory-copy +// extent: cstage copies exactly these bytes for the >24B sret +// write-through (cgen.c:8150 `int sz; for fields end=foff+fsz`) and +// struct-to-struct moves, so a trailing narrow field (bool@32 in a +// 33B struct padded to 40) keeps its MOVB tail rather than widening +// to a slot-overrunning MOVQ. #33 fixed cstage to use this; ww +// mirrors it. The ≤24B register RECV/RETURN ABI wants a DIFFERENT +// number — see structabisize. // -// NOTE: si.totsize is mis-named — it's actually the *slot-padded* -// size (rounded up to 8 for stack-slot use; see registerstruct's -// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride, -// and similar consumers want that slot-padded number. The -// receive-side ABI (#5) and any future "TYPE size, not slot size" -// query wants the natural size. Until si.totsize is split into -// si.naturalsize + si.slotsize (tracked as the wwstage-sizing -// follow-up task), recover the type-natural size from the field -// chain here. +// NOTE: si.totsize is yet a THIRD metric — the slot-padded size +// (rounded up to 8 for stack-slot use; see registerstruct's tail +// `if ((off & 7) != 0) ...`). Frame allocation and [N]foo stride +// want that slot number. fn structnaturalsize(si: *structinfo) i32 = { if (si == nil) { return 0; }; let n: i32 = 0; @@ -15014,6 +15015,45 @@ fn structnaturalsize(si: *structinfo) i32 = { return n; }; +// structabisize — the ≤24B register-return ABI size of `si`: the +// natural extent rounded up to the struct's maxalign. SSoT-equal to +// cstage's `lu->size` (check.c:760 `(off+maxalign-1)&~(maxalign-1)`). +// Distinct from structnaturalsize because the register RECV/RETURN +// ABI packs the value into AX/DX/CX at 8-byte granularity: cstage +// writes/reads the tail at maxalign width (cgen.c:7720 `sz=lu->size`, +// :8230 `sz=rt->size`), so a maxalign==8 struct with a sub-8 tail +// (struct{i64,i32}, natural 12) round-trips as MOVQ+MOVQ (16), not +// MOVQ+MOVL (12). Used ONLY at those register-ABI sites; memory +// copies (sret >24B, struct ident-copy) and field-offset math stay +// on structnaturalsize. #169. +// +// maxalign comes from each field's TRUE alignment (tinfo.align), not +// the slot-padded fsz: a [N]u8 / sub-struct field has slot ≥8 but +// align 1, so an fsz ladder would over-round. Mirrors cstage's +// maxalign = max(ft->align) (check.c:708). +fn structabisize(si: *structinfo) i32 = { + if (si == nil) { return 0; }; + let n: i32 = 0; + let maxaln: i32 = 1; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let end: i32 = fi.foff + fi.fsz; + if (end > n) { n = end; }; + if (fi.tnode != nil) { + let ti: *tinfo = fi.tnode.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { + ti = ti.under; + }; + if (ti != nil) { + let aln: i32 = ti.align: i32; + if (aln > maxaln) { maxaln = aln; }; + }; + }; + fi = fi.finext; + }; + return (n + maxaln - 1) & ~(maxaln - 1); +}; + // sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B, // return its natural size; else 0. Tagged unions, tuples, str, // slices, scalars route through their existing register-return ABIs @@ -24949,7 +24989,10 @@ fn cgreturn(c: *cgen, n: *node) void = { if (rname.len > 0) { let rsi: *structinfo = structlookup(c, rname); if (rsi != nil) { - let rsz: i32 = rsi.totsize; + // ≤24B register RETURN: cstage sizes by rt->size + // (maxalign-rounded), not the slot-padded totsize + // (round-to-8) — see structabisize (#169). + let rsz: i32 = structabisize(rsi); if (rsz <= 24) { let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { @@ -25507,10 +25550,11 @@ fn cglet(c: *cgen, n: *node) void = { if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8) for - // stack-slot use; the receive ABI needs the - // TYPE's natural size — see structnaturalsize. - let lsz: i32 = structnaturalsize(lsi); + // ≤24B register RECV: the value arrives packed + // in AX/DX/CX, so size by the maxalign-rounded + // ABI size (cstage lu->size), not the natural + // extent — see structabisize (#169). + let lsz: i32 = structabisize(lsi); let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 diff --git a/test/wcc/698_cgreturn_struct.c b/test/wcc/698_cgreturn_struct.c index c274d2ba..7e810d19 100644 --- a/test/wcc/698_cgreturn_struct.c +++ b/test/wcc/698_cgreturn_struct.c @@ -133,6 +133,49 @@ static const struct row rows[] = { "};\n" "fn main() i32 = { let f: five = mk(); return 0; };\n", 0 }, + /* #169 RETURN, maxalign<8: struct{i32,i32,i32} natural extent 12, + * maxalign 4 → cstage rt->size = round(12,4) = 12. The N_IDENT + * return word-copy loop sizes on rsz; pre-fix wwstage used the + * slot-padded totsize (round(12,8) = 16) → emitted MOVQ+MOVQ + * where cstage emits MOVQ+MOVL. Byte-id catches it; value + * readback (a+b+c) is correct in both stages either way. */ + { "trip_i32_ident_return", + "type t3 = struct { a: i32, b: i32, c: i32 };\n" + "fn mk() t3 = {\n" + " let s: t3 = t3 { a = 1, b = 2, c = 3 };\n" + " return s;\n" + "};\n" + "fn main() i32 = { let p: t3 = mk(); return p.a + p.b + p.c - 6; };\n", + 0 }, + /* #169 RECV, maxalign==8 + sub-8 tail: struct{i64,i32} natural + * extent 12, maxalign 8 → cstage lu->size = round(12,8) = 16. + * The `let p = mk()` receive sizes on structnaturalsize; pre-fix + * it returned the unrounded 12 → emitted MOVQ+MOVL where cstage + * emits MOVQ+MOVQ. mk returns via structlit (no RETURN-side + * divergence), isolating the RECV split. */ + { "pair_i64_i32_recv", + "type pt = struct { a: i64, b: i32 };\n" + "fn mk() pt = { return pt { a = 7i64, b = 9i32 }; };\n" + "fn main() i32 = { let p: pt = mk(); return p.b - 9; };\n", + 0 }, + /* #169 RECV, composite [N]u8 field: struct{i32,[6]u8} natural + * extent 10, maxalign 4 → cstage lu->size = round(10,4) = 12. + * Guards that a [N]u8 field's natural extent (10, NOT slot- + * rounded) feeds structabisize and rounds to maxalign, not 8: + * pre-fix the RECV used the unrounded structnaturalsize (10) → + * MOVW tail; the slot-padded totsize (16) would give MOVQ; only + * the maxalign-rounded 12 gives the MOVL cstage emits. The [6]u8 + * align must come from tinfo.align (1), not an fsz ladder (which + * would read 4 off the 6-byte slot) — here the i32 sets maxalign + * anyway, but the row pins the composite-field path byte-id. + * mk returns via struct literal so the bare-let local zero-init + * (a separate same-class latent) stays out of the fixture. */ + { "recv_i32_arr6_u8", + "type s6 = struct { n: i32, a: [6]u8 };\n" + "fn mk() s6 = " + "{ return s6 { n = 4i32, a = [1u8, 2u8, 3u8, 4u8, 5u8, 6u8] }; };\n" + "fn main() i32 = { let p: s6 = mk(); return p.n - 4; };\n", + 0 }, }; static int