From a00d0528335c6411ea261debb8dcea4f5d3282c0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 13 Jun 2026 13:15:31 +0900 Subject: [PATCH] wcc/ww: whole-struct field copy uses the source's natural length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four wwstage whole-struct field-copy sites (cgenexpr.ww) copied `ssi.totsize` — the slot-padded, round-8 structinfo size — instead of the SOURCE struct's natural size. cstage copies `f->type->size` (the field struct's aligned r.size; cmd/w6c/cgen.c:5302). wwstage over-copied into the field's slot padding. Fix: length = copysrcnatsize(c, n.rhs) = tichase(src.type_).size, read from the SOURCE node's stamped tinfo (the checker's natural r.size, check.ww N_TSTRUCT). This never reads structinfo / fi.foff / fi.fsz, so it is correct at HEAD unconditionally and independent of the registerstruct natural-offset change (#44/#55) that follows — a pure wwstage convergence onto the length cstage already emits. Distinct from the existing structnaturalsize (structinfo max(foff+fsz), a #44-coupled source). LENGTH ONLY. The ragged-tail completeness (both stages' field copies inline a tail handling only {4,1}; a natural size %8 in {2,3,5,6,7} falls through to an 8-byte MOVQ over-read) is a SEPARATE both-stage class — cstage cgen.c:5302 has the identical incomplete tail — folded into #73 (route both stages' field copies through the canonical greedy aggcopy emitter). Touching only ww's tail here would create a gate-blind cs!=ww on narrow-tail inputs, so it is deliberately left for the both-stage fix. No isolated runtime repro: the over-copy writes [natural, totsize), which under HEAD's slot-padded field layout is the field's OWN padding (the successor parks at the next slot). It only becomes a clobber once #44 packs the successor at its natural offset (the 681 ragged_tail_12B regression that forced this ordering). So this commit is byte-id-clean and a no-op on the present corpus; its proof is the all-green run plus the #44 commit that depends on it. --- selfhost/cmd/w6c/main.combined.ww | 36 ++++++++++++++++++++++++---- selfhost/cmd/wcc/cgenexpr.ww | 36 ++++++++++++++++++++++++---- selfhost/cmd/wwdump/main.combined.ww | 36 ++++++++++++++++++++++++---- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 2ab930f6..b865e53c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -25018,6 +25018,34 @@ fn aggcopy(c: *cgen, sz: i32) void = { }; }; +// copysrcnatsize — natural byte width of a whole-struct copy's SOURCE +// operand, read from the source node's stamped tinfo (tichase peels +// NAMED). #71: the four whole-struct field-copy sites must move this many +// bytes, NOT structinfo.totsize (slot-padded, round-8) which over-copies +// into slot padding and clobbers a natural-offset successor once #44 packs +// it there. Reads the source NODE's type, never fi.foff/fi.fsz or +// structinfo, so #71 stays independent of #44's registerstruct offset +// change. Equals cstage's `str_fu->size` (cmd/w6c/cgen.c:5302 SSoT) — the +// field struct's aligned r.size (check.ww N_TSTRUCT), distinct from the +// existing structnaturalsize (which reads structinfo max(foff+fsz), a +// #44-coupled source). The ragged-tail completeness shared by both stages' +// field copies is a separate class, tracked under #73 / the aggcopy +// emitter choke-point. +fn copysrcnatsize(c: *cgen, src: *node) i32 = { + if (src == nil || src.type_ == nil) { + let msg: str = "#71: whole-struct copy source has no stamped tinfo\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let ti: *tinfo = tichase(src.type_: *tinfo); + if (ti == nil) { + let msg: str = "#71: whole-struct copy source tinfo chase yielded nil\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + return ti.size: i32; +}; + // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -33259,7 +33287,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -33485,7 +33513,7 @@ fn cgassign(c: *cgen, n: *node) void = { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -33772,7 +33800,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -34415,7 +34443,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("(SB), CX\n"); }; }; - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 326dd1c8..32eff84b 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1699,6 +1699,34 @@ fn aggcopy(c: *cgen, sz: i32) void = { }; }; +// copysrcnatsize — natural byte width of a whole-struct copy's SOURCE +// operand, read from the source node's stamped tinfo (tichase peels +// NAMED). #71: the four whole-struct field-copy sites must move this many +// bytes, NOT structinfo.totsize (slot-padded, round-8) which over-copies +// into slot padding and clobbers a natural-offset successor once #44 packs +// it there. Reads the source NODE's type, never fi.foff/fi.fsz or +// structinfo, so #71 stays independent of #44's registerstruct offset +// change. Equals cstage's `str_fu->size` (cmd/w6c/cgen.c:5302 SSoT) — the +// field struct's aligned r.size (check.ww N_TSTRUCT), distinct from the +// existing structnaturalsize (which reads structinfo max(foff+fsz), a +// #44-coupled source). The ragged-tail completeness shared by both stages' +// field copies is a separate class, tracked under #73 / the aggcopy +// emitter choke-point. +fn copysrcnatsize(c: *cgen, src: *node) i32 = { + if (src == nil || src.type_ == nil) { + let msg: str = "#71: whole-struct copy source has no stamped tinfo\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let ti: *tinfo = tichase(src.type_: *tinfo); + if (ti == nil) { + let msg: str = "#71: whole-struct copy source tinfo chase yielded nil\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + return ti.size: i32; +}; + // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -9940,7 +9968,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -10166,7 +10194,7 @@ fn cgassign(c: *cgen, n: *node) void = { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -10453,7 +10481,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -11096,7 +11124,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("(SB), CX\n"); }; }; - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a1dd4560..d4e5cc1c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -25018,6 +25018,34 @@ fn aggcopy(c: *cgen, sz: i32) void = { }; }; +// copysrcnatsize — natural byte width of a whole-struct copy's SOURCE +// operand, read from the source node's stamped tinfo (tichase peels +// NAMED). #71: the four whole-struct field-copy sites must move this many +// bytes, NOT structinfo.totsize (slot-padded, round-8) which over-copies +// into slot padding and clobbers a natural-offset successor once #44 packs +// it there. Reads the source NODE's type, never fi.foff/fi.fsz or +// structinfo, so #71 stays independent of #44's registerstruct offset +// change. Equals cstage's `str_fu->size` (cmd/w6c/cgen.c:5302 SSoT) — the +// field struct's aligned r.size (check.ww N_TSTRUCT), distinct from the +// existing structnaturalsize (which reads structinfo max(foff+fsz), a +// #44-coupled source). The ragged-tail completeness shared by both stages' +// field copies is a separate class, tracked under #73 / the aggcopy +// emitter choke-point. +fn copysrcnatsize(c: *cgen, src: *node) i32 = { + if (src == nil || src.type_ == nil) { + let msg: str = "#71: whole-struct copy source has no stamped tinfo\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let ti: *tinfo = tichase(src.type_: *tinfo); + if (ti == nil) { + let msg: str = "#71: whole-struct copy source tinfo chase yielded nil\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + return ti.size: i32; +}; + // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -33259,7 +33287,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -33485,7 +33513,7 @@ fn cgassign(c: *cgen, n: *node) void = { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -33772,7 +33800,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); @@ -34415,7 +34443,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("(SB), CX\n"); }; }; - let ssz: i32 = ssi.totsize; + let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t");