From b3f4990979005de7dc3d7d893457dc3dbbbb1ecd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 27 Jun 2026 23:44:41 +0900 Subject: [PATCH] cgen: store the full register into padded scratch for a 3/5/6/7-byte aggregate tail (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmd/w6c/cgen.c | 45 ++++------ selfhost/cmd/wcc/cgenexpr.ww | 44 ++++------ test/lang/aggregate_tail_test.ww | 82 +++++++++++++++++++ .../data/idx_dot_aggret_subtail_loud/case.ww | 16 ---- .../data/idx_dot_aggret_subtail_run/case.ww | 29 +++++++ 5 files changed, 145 insertions(+), 71 deletions(-) create mode 100644 test/lang/aggregate_tail_test.ww delete mode 100644 test/wcc/data/idx_dot_aggret_subtail_loud/case.ww create mode 100644 test/wcc/data/idx_dot_aggret_subtail_run/case.ww diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 7c4a243d..b9a6f20c 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5866,29 +5866,16 @@ cgexpr(Cg *c, Node *n, Local *locals) "arr[i].f=mk() unwired (SSE " "return eightbyte; #171)"); if (fsz > 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 (:6764) 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 - * sibling of the over-cap/float stops - * above. Tails 0/1/2/4 are exact and flow - * through. Mirrors wwstage cgenexpr.ww. */ - int tl11 = fsz % 8; - if (tl11 == 3 || tl11 == 5 - || tl11 == 6 || tl11 == 7) - fatal("#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)"); + /* The sub-8 tail materialise stores the FULL + * 8-byte register (MOVQ) into a ceil-8-padded + * scratch (cg_tagscr_slot -> local_alloc rounds + * to 8): the over-stored high bytes land in the + * pad and the scratch->dest copy reads only fsz + * 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 (:7008). #10; mirrors wwstage + * cgenexpr.ww. */ int scr11 = cg_tagscr_slot(c, &locals, fsz); cgexpr(c, n->rhs, locals); @@ -5903,10 +5890,11 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(D_BP, scr11 + i11 * 8)); if (tail11 > 0) { - int op11 = (tail11 == 4) - ? A_MOVL : (tail11 + int op11 = (tail11 == 1) + ? A_MOVB : (tail11 == 2) ? A_MOVW - : A_MOVB; + : (tail11 == 4) + ? A_MOVL : A_MOVQ; ins2(c, op11, areg(regs11[full11]), amem(D_BP, scr11 @@ -7015,8 +7003,9 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(regs[i]), amem(D_BP, scr + i * 8)); if (tail > 0) { - int op = (tail == 4) ? A_MOVL - : (tail == 2) ? A_MOVW : A_MOVB; + int op = (tail == 1) ? A_MOVB + : (tail == 2) ? A_MOVW + : (tail == 4) ? A_MOVL : A_MOVQ; ins2(c, op, areg(regs[full]), amem(D_BP, scr + full * 8)); } diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 2218e174..62e3fe39 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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"; }; }; diff --git a/test/lang/aggregate_tail_test.ww b/test/lang/aggregate_tail_test.ww new file mode 100644 index 00000000..8ee235de --- /dev/null +++ b/test/lang/aggregate_tail_test.ww @@ -0,0 +1,82 @@ +// aggregate_tail_test — in-cap aggregate-receive sub-8 tail materialise (#10). +// An in-cap (<=24B) aggregate-returning CALL received into an INDEXED dest is a +// two-step: (1) materialise the AX/DX/CX return regs into a frame scratch, (2) +// word-copy scratch -> dest. Step 1's sub-8 TAIL stored a single narrow MOV +// picked by (tail==4)?MOVL:(tail==2)?MOVW:MOVB, so for tail in {3,5,6,7} it fell +// to MOVB: one byte written while the copy reads the FULL tail, so the high tail +// bytes stayed uninitialised => dropped members. Two arms shared the gap — the +// C2c whole-element arr[i]=mk() (SILENT both stages, byte-id-BLIND: both emitted +// IDENTICAL wrong asm) and the #11 field-of-indexed arr[i].f=mk() (LOUD-STOPped). +// The fix stores the FULL register (MOVQ) into the ceil-8-padded scratch; the +// over-stored bytes die in the pad and the copy reads only the real size. +// A repro element must be align-<=2 UNPADDED: [7]i16 = 14B keeps a real tail-6; +// an i64-bearing struct pads to 16B (tail 0) and never trips it. PRIMITIVE-only +// asserts (deref_callarg lineage): a dropped word fails. The C2c value assert is +// the SOLE tooth there (byte-id blind); the #11 case re-arms a compile-error +// loud-stop on revert. Distinct nonzero per-element values so a stale read (the +// fully-dropped members read uninitialised, 0 on a fresh frame) mismatches. + +package aggregate_tail_test; + +type s = struct { f: [7]i16, g: i16 }; // f at off 0 (14B tail-6), g at off 14 + +fn mk7() [7]i16 = { return [10i16, 20i16, 30i16, 40i16, 50i16, 60i16, 70i16]; }; +fn mk11() [11]u8 = { return [1u8,2u8,3u8,4u8,5u8,6u8,7u8,8u8,9u8,10u8,11u8]; }; +fn mk13() [13]u8 = { return [1u8,2u8,3u8,4u8,5u8,6u8,7u8,8u8,9u8,10u8,11u8,12u8,13u8]; }; +fn mk15() [15]u8 = { return [1u8,2u8,3u8,4u8,5u8,6u8,7u8,8u8,9u8,10u8,11u8,12u8,13u8,14u8,15u8]; }; + +@test fn c2c_whole_element() void = { + let arr: [3][7]i16; + arr[1] = mk7(); + assert(arr[1][0] == 10i16); + assert(arr[1][1] == 20i16); + assert(arr[1][2] == 30i16); + assert(arr[1][3] == 40i16); + assert(arr[1][4] == 50i16); // eb1 byte 0-1 + assert(arr[1][5] == 60i16); // eb1 byte 2-3 — dropped by the buggy MOVB tail + assert(arr[1][6] == 70i16); // eb1 byte 4-5 — dropped (fully-uninit tooth) +}; + +@test fn field_of_indexed() void = { + let arr: [3]s; + arr[1].g = 999i16; // neighbour set first + arr[1].f = mk7(); + assert(arr[1].f[0] == 10i16); + assert(arr[1].f[4] == 50i16); + assert(arr[1].f[5] == 60i16); + assert(arr[1].f[6] == 70i16); + assert(arr[1].g == 999i16); // tail MOVQ writes the padded SCRATCH, not g +}; + +// The STRUCT-field-of-indexed variant ({t14,pad} struct field) is NOT pinned here: +// #10's materialise is byte-id clean for it, but the [N]box local frame size +// diverges cs!=ww (task #9, slotsize-vs-natural) — benign (both stages correct) +// yet byte-id-RED, so it stays out of the T2 corpus. Positive cstage coverage of +// that shape lives in test/wcc/data/idx_dot_aggret_subtail_run (converted from the +// retired loud-stop tripwire). + +@test fn tail3() void = { + let a: [2][11]u8; + a[1] = mk11(); + assert(a[1][0] == 1u8); + assert(a[1][7] == 8u8); + assert(a[1][8] == 9u8); // tail byte 0 (MOVB wrote only this) + assert(a[1][9] == 10u8); // tail byte 1 — dropped + assert(a[1][10] == 11u8); // tail byte 2 — dropped +}; + +@test fn tail5() void = { + let a: [2][13]u8; + a[1] = mk13(); + assert(a[1][0] == 1u8); + assert(a[1][8] == 9u8); + assert(a[1][12] == 13u8); // tail byte 4 — dropped +}; + +@test fn tail7() void = { + let a: [2][15]u8; + a[1] = mk15(); + assert(a[1][0] == 1u8); + assert(a[1][8] == 9u8); + assert(a[1][14] == 15u8); // tail byte 6 — dropped +}; diff --git a/test/wcc/data/idx_dot_aggret_subtail_loud/case.ww b/test/wcc/data/idx_dot_aggret_subtail_loud/case.ww deleted file mode 100644 index 15ce6abb..00000000 --- a/test/wcc/data/idx_dot_aggret_subtail_loud/case.ww +++ /dev/null @@ -1,16 +0,0 @@ -//ww:error "3/5/6/7-byte sub-8 tail unwired" -// #11 tripwire: an in-cap aggregate-returning CALL into an aggregate field of -// an indexed element where the field has a 3/5/6/7-byte sub-8 tail (here 14B -// 7xi16, tail=6). The materialise's single narrow MOV stores only one tail byte -// while the copy reads the full tail -> a SILENT both-stage member drop. Both -// stages LOUD-STOP until a general register->scratch tail lands (rule 7, -// C2c-shared). SUCCESS = the loud-stop regressed to a silent miscompile. -package main; -type t14 = struct { a:i16,b:i16,c:i16,d:i16,e:i16,f:i16,g:i16 }; -type s14 = struct { x: t14, pad: i16 }; -fn mk() t14 = { return t14{a=1i16,b=2i16,c=3i16,d=4i16,e=5i16,f=6i16,g=7i16}; }; -export fn main() i32 = { - let arr: [2]s14; - arr[1].x = mk(); - return 0; -}; diff --git a/test/wcc/data/idx_dot_aggret_subtail_run/case.ww b/test/wcc/data/idx_dot_aggret_subtail_run/case.ww new file mode 100644 index 00000000..5e3ff14c --- /dev/null +++ b/test/wcc/data/idx_dot_aggret_subtail_run/case.ww @@ -0,0 +1,29 @@ +//ww:run +// #10 positive (converted from the retired idx_dot_aggret_subtail_loud tripwire): +// an in-cap aggregate-returning CALL into a 14B-tail-6 STRUCT field of an indexed +// element (arr[i].x = mk()). Pre-#10 both stages LOUD-STOPped this 3/5/6/7-tail +// materialise; #10 stores the FULL register into the ceil-8-padded scratch, so the +// over-stored bytes die in the pad and every member round-trips. main returns 0 +// only if all members AND the neighbour pad survived (else a distinct nonzero). +// cstage-only RUN by harness design (T1): this shape's [N]s14 local frame size +// diverges cs!=ww (task #9, slotsize-vs-natural, benign — both stages compute +// correct values) so it cannot enter the T2 byte-id corpus. Reverting #10 re-arms +// the loud-stop -> this case fails to compile (rc!=0) -> reddens. +package main; +type t14 = struct { a:i16,b:i16,c:i16,d:i16,e:i16,f:i16,g:i16 }; +type s14 = struct { x: t14, pad: i16 }; +fn mk() t14 = { return t14{a=10i16,b=20i16,c=30i16,d=40i16,e=50i16,f=60i16,g=70i16}; }; +export fn main() i32 = { + let arr: [2]s14; + arr[1].pad = 999i16; + arr[1].x = mk(); + if (arr[1].x.a != 10i16) { return 1; }; + if (arr[1].x.b != 20i16) { return 2; }; + if (arr[1].x.c != 30i16) { return 3; }; + if (arr[1].x.d != 40i16) { return 4; }; + if (arr[1].x.e != 50i16) { return 5; }; + if (arr[1].x.f != 60i16) { return 6; }; // tail byte 0-1 + if (arr[1].x.g != 70i16) { return 7; }; // tail byte 2-3 (dropped pre-#10) + if (arr[1].pad != 999i16) { return 8; }; // tail MOVQ must not smash pad + return 0; +};