cgen: store the full register into padded scratch for a 3/5/6/7-byte aggregate tail (#10)

The in-cap aggregate-receive materialise emitted a single narrow tail MOV that fell to MOVB for a 3/5/6/7-byte sub-8 tail, storing one byte while the scratch->dest copy read the full tail from uninitialised scratch — silently dropping members at the C2c whole-element arm (arr[i]=mk()) and loud-stopping at the #11 field arm. The scratch slot is ceil-8 padded (local_alloc/localadd round to 8) and the copy reads only tsz bytes, so flipping the tail default MOVB->MOVQ stores the full register harmlessly into the slot's own pad (in-bounds for in-cap <=24B); 1/2/4-byte tails stay byte-identical. Both stages symmetric. Removes the now-redundant #11 sub-8-tail loud-stop (keeps the float #165 and over-cap #234 loud-stops). The same narrow-tail materialise recurs at 6 other cstage sites (task #14).

Retires the obsolete idx_dot_aggret_subtail_loud //ww:error fixture (both stages now compile the case) and converts it to a positive cstage run-test; the struct-field shape is byte-id-divergent only via the pre-existing #9 frame-size bug, so the value pin uses array-field shapes. Value-asserting, reddens under each stage's independent revert.
This commit is contained in:
2026-06-27 23:44:41 +09:00
parent 5b33ea3f7d
commit b3f4990979
5 changed files with 145 additions and 71 deletions

View File

@@ -9127,9 +9127,10 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
wi += 1;
};
if (tail > 0) {
let top: str = "MOVB";
if (tail == 4) { top = "MOVL"; }
else { if (tail == 2) { top = "MOVW"; }; };
let top: str = "MOVQ";
if (tail == 1) { top = "MOVB"; }
else { if (tail == 2) { top = "MOVW"; }
else { if (tail == 4) { top = "MOVL"; }; }; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; }
else { if (full == 2) { treg = "CX"; }; };
@@ -10054,27 +10055,15 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
// would smash the next field (g at +12).
let tsz: i32 = fi.fsz;
if (tsz > 8) {
// A 3/5/6/7-byte sub-8 tail cannot
// be materialised by the single narrow
// MOV below — it stores ONE byte while
// the copy reads the full tail, dropping
// the rest from uninitialised scratch (a
// silent both-stage drop; empirically a
// 14B 7xi16 field loses f/g). The whole-
// element C2c sibling (:9100) shares this
// single-tail gap; until a general
// register->scratch tail (shift cascade)
// lands across BOTH sites, LOUD-STOP
// rather than silently drop — rule 7, the
// #11 sibling of the over-cap/float stops
// above. Tails 0/1/2/4 are exact and flow
// through. Mirrors cstage cgen.c.
let tl11: i32 = tsz % 8;
if (tl11 == 3 || tl11 == 5 || tl11 == 6 || tl11 == 7) {
let m11t: str = "#11: aggregate field receive arr[i].f=mk() with a 3/5/6/7-byte sub-8 tail unwired (materialise single-MOV under-stores; C2c-shared)\n";
os.write(2, m11t.ptr, m11t.len: u64);
os.exit(1);
};
// The sub-8 tail materialise stores the FULL
// 8-byte register (MOVQ) into a ceil-8-padded
// scratch (tagscradd -> localadd rounds to 8):
// the over-stored high bytes land in the pad and
// the scratch->dest copy reads only tsz bytes, so
// every in-cap tail (incl. 3/5/6/7) is exact
// without an immediate-shift cascade (w6a has no
// SHRQ $imm). Shares the C2c whole-element
// materialise (:9100). #10; mirrors cstage cgen.c.
let scr11: i32 = tagscradd(c, tsz);
cgexpr(c, n.rhs);
// AX/DX/CX -> scratch (C2c materialise)
@@ -10093,9 +10082,10 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
wi11 += 1;
};
if (tail11 > 0) {
let top11: str = "MOVB";
if (tail11 == 4) { top11 = "MOVL"; }
else { if (tail11 == 2) { top11 = "MOVW"; }; };
let top11: str = "MOVQ";
if (tail11 == 1) { top11 = "MOVB"; }
else { if (tail11 == 2) { top11 = "MOVW"; }
else { if (tail11 == 4) { top11 = "MOVL"; }; }; };
let treg11: str = "AX";
if (full11 == 1) { treg11 = "DX"; }
else { if (full11 == 2) { treg11 = "CX"; }; };