From 39f9267bc95f05effbc989c8d02c17c735b349ba Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 13:02:47 +0900 Subject: [PATCH] wcc: converge DOT-recv, structlit-fill, and bare-let zero-init onto structabisize Three wwstage cgen sites still used the unrounded structnaturalsize where cstage rounds via lu->size (check.c:760), pre-existing gate-blind cs!=ww latents flagged in #169's reviewer notes: cgenexpr DOT register-RECV for obj.f = mk() (~5175/5393/5628/6164); cgstructlitfill's TK_ELLIPSIS zero-fill branch (cgenutil); and cglet bare 'let z: T;' zero-init of a maxalign<8 struct (cgenstmt). Each produced MOVQ-vs-MOVL or wider-write divergence vs cstage on the trailing word of a sub-eightbyte tail. Converge all three onto the maxalign-rounded structabisize the #169 work established at the register-ABI sites (cite cstage cgen.c:7720 RECV twin + cgen.c:2085 cg_structlit_fill). cgstructlitfill's signature drops the external totsize parameter in favor of one internal source; the field-walk path is untouched, only the ELLIPSIS zero-fill uses the ABI size. cglet's slot allocation stays on the frame size; only the zero-fill extent uses ABI. Gate-blind (the bootstrap exercises none of these shapes); covered by 5 new rows in probe 698 with cs==ww .s byte-cmp and a pre-fix-rebuild proving the exact MOVQ-vs-MOVL discrimination. 990-997 byte-id hold. --- selfhost/cmd/w6c/main.combined.ww | 87 +++++++++++++++------------- selfhost/cmd/wcc/cgenexpr.ww | 35 +++++------ selfhost/cmd/wcc/cgenstmt.ww | 24 ++++++-- selfhost/cmd/wcc/cgenutil.ww | 28 +++++---- selfhost/cmd/wwdump/main.combined.ww | 87 +++++++++++++++------------- test/wcc/698_cgreturn_struct.c | 66 +++++++++++++++++++++ 6 files changed, 210 insertions(+), 117 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 433c485c..091620fe 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17159,9 +17159,14 @@ export fn dotchainresolve(c: *cgen, n: *node, // - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are // *constant* across the whole call tree — they identify the // root dst, which doesn't change with depth. -// - `totsize` is also constant; pass the natural size for dot -// sites (structnaturalsize) and si.totsize for BP-rel sites, -// matching each site's pre-#18 zero-fill bound. +// - the ELLIPSIS zero-fill extent is read internally as +// structabisize(si) — cstage's cg_structlit_fill computes +// `sz = lu->size` (cgen.c:2085), the maxalign-rounded ABI size +// (check.c:760 lu->size = (off+maxalign-1)&~(maxalign-1)). The +// pre-#169 callers passed two different sizes (natural at DOT +// sites, slot-padded at BP-rel sites); neither matched cstage +// for maxalign<8 structs (the zero-fill ran MOVQ where cstage +// ran MOVL — value-correct, asm-divergent). // // Why a helper? The inline field-walk previously did // `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose @@ -17188,10 +17193,11 @@ export fn dotchainresolve(c: *cgen, n: *node, // which already returns MOVW where appropriate. fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, mode: i32, srcoff: i32, srcname: str, - disp: i32, totsize: i32) void = { + disp: i32) void = { if (si == nil) { return; }; let basereg: str = "BP"; if (mode != 0) { basereg = "BX"; }; + let totsize: i32 = structabisize(si); if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so // unmentioned fields read as 0. Sized stores: 8/4/1. For @@ -17285,17 +17291,10 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { - // Nested fill: pick the size - // discipline matching the outer - // site — dot sites pass natural - // size, BP-rel sites pass - // totsize. Mirror it. - let inner_tot: i32 = isi.totsize; - if (mode != 0) { inner_tot = structnaturalsize(isi); }; cgstructlitfill(c, isi, fieldnode.lhs, mode, srcoff, srcname, - disp + fi.foff, inner_tot); + disp + fi.foff); nested = true; }; }; @@ -17521,11 +17520,10 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, }; // Thin wrapper preserving the BP-rel call shape used by cglet, -// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to -// the pre-#18 cgstructlitfillbp. +// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (si == nil) { return; }; - cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); + cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. @@ -22678,11 +22676,13 @@ fn cgassign(c: *cgen, n: *node) void = { // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX per #4's cgreturn // ABI; load *struct ptr into BX after the - // call, sized stores per natural struct size. + // call, sized stores per the ABI size. // N_STRUCTLIT: field-walk; reload BX before // each store so cgexpr can clobber AX/BX. - // si.totsize is slot-padded; use - // structnaturalsize for the type-size query. + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize (cstage + // SSoT lu->size, check.c:760; cgen.c:7720 + // sz=lu->size at the receive twin). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL @@ -22691,7 +22691,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -22746,9 +22746,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", - fi.foff, ssz); + fi.foff); return; }; }; @@ -22909,7 +22908,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -22960,9 +22959,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 0, 0, "", - lc.off + fi.foff, ssz); + lc.off + fi.foff); return; }; }; @@ -23144,7 +23142,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -23199,9 +23197,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, - fi.foff, ssz); + fi.foff); return; }; }; @@ -23678,9 +23675,10 @@ fn cgassign(c: *cgen, n: *node) void = { && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lsz: i32 = structnaturalsize(lsi); + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize + // (cstage SSoT lu->size, check.c:760). + let lsz: i32 = structabisize(lsi); if (lsz <= 24) { let tlm: i32 = lsz - (lsz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -23761,9 +23759,6 @@ fn cgassign(c: *cgen, n: *node) void = { && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lsz: i32 = structnaturalsize(lsi); let dmode: i32 = 0; let ddisp: i32 = rootoff + totaloff; if (ptrroot) { @@ -23776,7 +23771,7 @@ fn cgassign(c: *cgen, n: *node) void = { }; cgstructlitfill(c, lsi, n.rhs, dmode, rootoff, rootname, - ddisp, lsz); + ddisp); return; }; }; @@ -25119,7 +25114,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // the dest pointer IS the struct base. cgstructlitfill(c, sret_si, rhs, 1, sretargoff, emptys, - 0, scs); + 0); }; } else { let rl: *local = localfindnode(c, rhs.str); @@ -25992,26 +25987,40 @@ fn cglet(c: *cgen, n: *node) void = { if (n.lhs != nil) { 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). + 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 (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (!isarr) { if (sz > 8) { + } else { if (!isarr) { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; - for (zi + 8 <= sz) { + for (zi + 8 <= zsz) { emitline("\tMOVQ\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 8; }; - for (zi + 4 <= sz) { + for (zi + 4 <= zsz) { emitline("\tMOVL\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 4; }; - for (zi < sz) { + for (zi < zsz) { emitline("\tMOVB\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 5df85cb6..fb164fd4 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5148,11 +5148,13 @@ fn cgassign(c: *cgen, n: *node) void = { // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX per #4's cgreturn // ABI; load *struct ptr into BX after the - // call, sized stores per natural struct size. + // call, sized stores per the ABI size. // N_STRUCTLIT: field-walk; reload BX before // each store so cgexpr can clobber AX/BX. - // si.totsize is slot-padded; use - // structnaturalsize for the type-size query. + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize (cstage + // SSoT lu->size, check.c:760; cgen.c:7720 + // sz=lu->size at the receive twin). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL @@ -5161,7 +5163,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -5216,9 +5218,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", - fi.foff, ssz); + fi.foff); return; }; }; @@ -5379,7 +5380,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -5430,9 +5431,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 0, 0, "", - lc.off + fi.foff, ssz); + lc.off + fi.foff); return; }; }; @@ -5614,7 +5614,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -5669,9 +5669,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, - fi.foff, ssz); + fi.foff); return; }; }; @@ -6148,9 +6147,10 @@ fn cgassign(c: *cgen, n: *node) void = { && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lsz: i32 = structnaturalsize(lsi); + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize + // (cstage SSoT lu->size, check.c:760). + let lsz: i32 = structabisize(lsi); if (lsz <= 24) { let tlm: i32 = lsz - (lsz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -6231,9 +6231,6 @@ fn cgassign(c: *cgen, n: *node) void = { && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lsz: i32 = structnaturalsize(lsi); let dmode: i32 = 0; let ddisp: i32 = rootoff + totaloff; if (ptrroot) { @@ -6246,7 +6243,7 @@ fn cgassign(c: *cgen, n: *node) void = { }; cgstructlitfill(c, lsi, n.rhs, dmode, rootoff, rootname, - ddisp, lsz); + ddisp); return; }; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 1afba74c..69f53189 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -596,7 +596,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // the dest pointer IS the struct base. cgstructlitfill(c, sret_si, rhs, 1, sretargoff, emptys, - 0, scs); + 0); }; } else { let rl: *local = localfindnode(c, rhs.str); @@ -1469,26 +1469,40 @@ fn cglet(c: *cgen, n: *node) void = { if (n.lhs != nil) { 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). + 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 (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (!isarr) { if (sz > 8) { + } else { if (!isarr) { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; - for (zi + 8 <= sz) { + for (zi + 8 <= zsz) { emitline("\tMOVQ\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 8; }; - for (zi + 4 <= sz) { + for (zi + 4 <= zsz) { emitline("\tMOVL\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 4; }; - for (zi < sz) { + for (zi < zsz) { emitline("\tMOVB\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 379fadf6..22e4fcd6 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -3092,9 +3092,14 @@ export fn dotchainresolve(c: *cgen, n: *node, // - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are // *constant* across the whole call tree — they identify the // root dst, which doesn't change with depth. -// - `totsize` is also constant; pass the natural size for dot -// sites (structnaturalsize) and si.totsize for BP-rel sites, -// matching each site's pre-#18 zero-fill bound. +// - the ELLIPSIS zero-fill extent is read internally as +// structabisize(si) — cstage's cg_structlit_fill computes +// `sz = lu->size` (cgen.c:2085), the maxalign-rounded ABI size +// (check.c:760 lu->size = (off+maxalign-1)&~(maxalign-1)). The +// pre-#169 callers passed two different sizes (natural at DOT +// sites, slot-padded at BP-rel sites); neither matched cstage +// for maxalign<8 structs (the zero-fill ran MOVQ where cstage +// ran MOVL — value-correct, asm-divergent). // // Why a helper? The inline field-walk previously did // `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose @@ -3121,10 +3126,11 @@ export fn dotchainresolve(c: *cgen, n: *node, // which already returns MOVW where appropriate. fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, mode: i32, srcoff: i32, srcname: str, - disp: i32, totsize: i32) void = { + disp: i32) void = { if (si == nil) { return; }; let basereg: str = "BP"; if (mode != 0) { basereg = "BX"; }; + let totsize: i32 = structabisize(si); if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so // unmentioned fields read as 0. Sized stores: 8/4/1. For @@ -3218,17 +3224,10 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { - // Nested fill: pick the size - // discipline matching the outer - // site — dot sites pass natural - // size, BP-rel sites pass - // totsize. Mirror it. - let inner_tot: i32 = isi.totsize; - if (mode != 0) { inner_tot = structnaturalsize(isi); }; cgstructlitfill(c, isi, fieldnode.lhs, mode, srcoff, srcname, - disp + fi.foff, inner_tot); + disp + fi.foff); nested = true; }; }; @@ -3454,9 +3453,8 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, }; // Thin wrapper preserving the BP-rel call shape used by cglet, -// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to -// the pre-#18 cgstructlitfillbp. +// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (si == nil) { return; }; - cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); + cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b80449ad..8d4664b4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17159,9 +17159,14 @@ export fn dotchainresolve(c: *cgen, n: *node, // - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are // *constant* across the whole call tree — they identify the // root dst, which doesn't change with depth. -// - `totsize` is also constant; pass the natural size for dot -// sites (structnaturalsize) and si.totsize for BP-rel sites, -// matching each site's pre-#18 zero-fill bound. +// - the ELLIPSIS zero-fill extent is read internally as +// structabisize(si) — cstage's cg_structlit_fill computes +// `sz = lu->size` (cgen.c:2085), the maxalign-rounded ABI size +// (check.c:760 lu->size = (off+maxalign-1)&~(maxalign-1)). The +// pre-#169 callers passed two different sizes (natural at DOT +// sites, slot-padded at BP-rel sites); neither matched cstage +// for maxalign<8 structs (the zero-fill ran MOVQ where cstage +// ran MOVL — value-correct, asm-divergent). // // Why a helper? The inline field-walk previously did // `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose @@ -17188,10 +17193,11 @@ export fn dotchainresolve(c: *cgen, n: *node, // which already returns MOVW where appropriate. fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, mode: i32, srcoff: i32, srcname: str, - disp: i32, totsize: i32) void = { + disp: i32) void = { if (si == nil) { return; }; let basereg: str = "BP"; if (mode != 0) { basereg = "BX"; }; + let totsize: i32 = structabisize(si); if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so // unmentioned fields read as 0. Sized stores: 8/4/1. For @@ -17285,17 +17291,10 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { - // Nested fill: pick the size - // discipline matching the outer - // site — dot sites pass natural - // size, BP-rel sites pass - // totsize. Mirror it. - let inner_tot: i32 = isi.totsize; - if (mode != 0) { inner_tot = structnaturalsize(isi); }; cgstructlitfill(c, isi, fieldnode.lhs, mode, srcoff, srcname, - disp + fi.foff, inner_tot); + disp + fi.foff); nested = true; }; }; @@ -17521,11 +17520,10 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, }; // Thin wrapper preserving the BP-rel call shape used by cglet, -// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to -// the pre-#18 cgstructlitfillbp. +// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (si == nil) { return; }; - cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); + cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. @@ -22678,11 +22676,13 @@ fn cgassign(c: *cgen, n: *node) void = { // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX per #4's cgreturn // ABI; load *struct ptr into BX after the - // call, sized stores per natural struct size. + // call, sized stores per the ABI size. // N_STRUCTLIT: field-walk; reload BX before // each store so cgexpr can clobber AX/BX. - // si.totsize is slot-padded; use - // structnaturalsize for the type-size query. + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize (cstage + // SSoT lu->size, check.c:760; cgen.c:7720 + // sz=lu->size at the receive twin). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL @@ -22691,7 +22691,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -22746,9 +22746,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", - fi.foff, ssz); + fi.foff); return; }; }; @@ -22909,7 +22908,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -22960,9 +22959,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 0, 0, "", - lc.off + fi.foff, ssz); + lc.off + fi.foff); return; }; }; @@ -23144,7 +23142,7 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); + let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -23199,9 +23197,8 @@ fn cgassign(c: *cgen, n: *node) void = { && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { - let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, - fi.foff, ssz); + fi.foff); return; }; }; @@ -23678,9 +23675,10 @@ fn cgassign(c: *cgen, n: *node) void = { && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lsz: i32 = structnaturalsize(lsi); + // register RECV reads AX/DX/CX at 8-byte + // granularity — size via structabisize + // (cstage SSoT lu->size, check.c:760). + let lsz: i32 = structabisize(lsi); if (lsz <= 24) { let tlm: i32 = lsz - (lsz / 8) * 8; if (tlm == 0 || tlm == 1 @@ -23761,9 +23759,6 @@ fn cgassign(c: *cgen, n: *node) void = { && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { - // si.totsize is slot-padded (rounded to 8); - // receive ABI needs the TYPE's natural size. - let lsz: i32 = structnaturalsize(lsi); let dmode: i32 = 0; let ddisp: i32 = rootoff + totaloff; if (ptrroot) { @@ -23776,7 +23771,7 @@ fn cgassign(c: *cgen, n: *node) void = { }; cgstructlitfill(c, lsi, n.rhs, dmode, rootoff, rootname, - ddisp, lsz); + ddisp); return; }; }; @@ -25119,7 +25114,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // the dest pointer IS the struct base. cgstructlitfill(c, sret_si, rhs, 1, sretargoff, emptys, - 0, scs); + 0); }; } else { let rl: *local = localfindnode(c, rhs.str); @@ -25992,26 +25987,40 @@ fn cglet(c: *cgen, n: *node) void = { if (n.lhs != nil) { 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). + 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 (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (!isarr) { if (sz > 8) { + } else { if (!isarr) { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; - for (zi + 8 <= sz) { + for (zi + 8 <= zsz) { emitline("\tMOVQ\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 8; }; - for (zi + 4 <= sz) { + for (zi + 4 <= zsz) { emitline("\tMOVL\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 4; }; - for (zi < sz) { + for (zi < zsz) { emitline("\tMOVB\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); diff --git a/test/wcc/698_cgreturn_struct.c b/test/wcc/698_cgreturn_struct.c index 7e810d19..c1d22168 100644 --- a/test/wcc/698_cgreturn_struct.c +++ b/test/wcc/698_cgreturn_struct.c @@ -176,6 +176,72 @@ static const struct row rows[] = { "{ 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 }, + /* DOT register-RECV: `obj.f = mk();` where the field type is a + * struct{i64,i32} (natural 12, maxalign 8 → cstage lu->size 16). + * Pre-fix wwstage sized the AX/DX/CX → field stores on the + * unrounded structnaturalsize (12) → MOVQ+MOVL where cstage emits + * MOVQ+MOVQ. The byte-id discriminator hits regardless of value + * read-back. Three flavors cover the *struct base, direct local, + * and global base sites that share the class. */ + { "dot_recv_call_local", + "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;\n" + " b.v = mk();\n" + " return b.v.b - 9;\n" + "};\n", + 0 }, + { "dot_recv_call_viaptr", + "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 fill(p: *box) void = { p.v = mk(); return; };\n" + "fn main() i32 = {\n" + " let b: box;\n" + " fill(&b);\n" + " return b.v.b - 9;\n" + "};\n", + 0 }, + { "dot_recv_call_global", + "type pt = struct { a: i64, b: i32 };\n" + "type box = struct { v: pt };\n" + "let g: box = box { v = pt { a = 0i64, b = 0i32 } };\n" + "fn mk() pt = { return pt { a = 7i64, b = 9i32 }; };\n" + "fn main() i32 = { g.v = mk(); return g.v.b - 9; };\n", + 0 }, + /* structlit ELLIPSIS zero-fill at a DOT site. cstage zeroes the + * slot at `lu->size` (maxalign-rounded); wwstage pre-fix passed + * structnaturalsize (12 for struct{i64,i32}) as the totsize + * bound but cstage rounds to maxalign (16) before zeroing. The + * `..., ...` literal forces the ELLIPSIS path. Outer box keeps + * maxalign==8 so its own zero-init / structlit-init stays out + * of the registerstruct fsz-ladder latent (task #18). */ + { "dot_structlit_ellipsis", + "type s = struct { a: i64, b: i32 };\n" + "type box = struct { v: s };\n" + "fn mk() box = " + "{ return box { v = s { a = 0i64, b = 0i32 } }; };\n" + "fn main() i32 = {\n" + " let b: box = mk();\n" + " b.v = s { a = 5i64, ... };\n" + " return b.v.b;\n" + "};\n", + 0 }, + /* Bare `let z: T;` zero-init of a maxalign<8 struct. Slot is + * round-to-8 (ti.slotsize); cstage zeroes only `lu->size` + * (maxalign-rounded, check.c:760). struct{i32,i32,i32} natural 12, + * maxalign 4 → cstage emits XORQ + MOVQ + MOVL (12B); pre-fix + * wwstage emitted XORQ + MOVQ + MOVQ (slot 16B). Value-correct + * either way; byte-id catches the trailing 4B widen. */ + { "bare_let_struct_maxalign4", + "type t3 = struct { a: i32, b: i32, c: i32 };\n" + "fn main() i32 = {\n" + " let z: t3;\n" + " return z.a + z.b + z.c;\n" + "};\n", + 0 }, }; static int