From b5e76fb1fac4217421d50c5492d3fdd6e3eed7fb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 28 Jun 2026 15:16:52 +0900 Subject: [PATCH] cgen: widen the narrow-tail aggregate-register store to full sz<=24 at every routed site (#14) Completes the #14 close-by-construction begun by the helper extraction (e7fefa3): every <=24B aggregate register-store now routes through cg_agg_reg_store/cgaggregstore and handles all tail sizes. B/C/D/G fix a silent both-stage drop of a 3/5/6/7-byte tail (byte-id-blind: both stages dropped identically, so the gate could not see it). A/F enable a previously loud-rejected shape (a cgen backend gap, not a type rejection; harec accepts and lowers it). G (global g=f() array) routes symmetrically, dest_padded=false. The #11 arr[i].f scratch loops fold into the helper (dest_padded=true, byte-id zero-change), completing the grep-audit. Pins value-assert each eightbyte (the class is byte-id-blind) and redden under each stage's independent revert; site H's 3/5/6/7 let-receive stays a loud fatal (#22). --- cmd/w6c/cgen.c | 110 +++++++---------- selfhost/cmd/wcc/cgenexpr.ww | 182 +++++++++------------------- selfhost/cmd/wcc/cgenutil.ww | 26 ++-- test/lang/narrow_tail_widen_test.ww | 138 +++++++++++++++++++++ 4 files changed, 249 insertions(+), 207 deletions(-) create mode 100644 test/lang/narrow_tail_widen_test.ww diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 4c6f5217..b9be8429 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3507,17 +3507,13 @@ cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit, * fallthrough below silently dropped past the first * qword for any fsz > 8 (only AX got stored). * - * Sized stores: MOVQ for full 8B chunks plus a sized tail - * (MOVL/MOVW/MOVB) by `tail = fsz%8`. Mirrors #4's receive - * shape at the N_LET / N_ASSIGN call-rhs sites; the - * MOVW-for-tail==2 emission only fires on shapes that - * didn't compile before, so no #13 byte-identity concern. + * The choke-point stores MOVQ for full 8B chunks plus a + * sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`. * - * Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches #4's - * cgreturn ABI: >24B falls through (sret deferred); - * fsz%8 ∈ {3,5,6,7} would need shift-store and is also - * unsupported by #4 — falls through to the existing - * AX-only wrongness (consistent, tracked as follow-up). + * Guard `fsz <= 24` (#14 widened from {0,1,2,4}): the + * choke-point handles every in-cap tail incl. 3/5/6/7 (its + * non-padded scratch detour, dest_padded=0 — a struct-lit + * field is packed); >24B still falls through (sret deferred). * * INVARIANT: between cgexpr(N_CALL) and the AX/DX/CX * stores below, NO instruction may touch AX/DX/CX. The @@ -3525,9 +3521,7 @@ cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit, * added here will silently corrupt the return value. */ if (fu && fu->kind == TY_STRUCT && f->lhs && f->lhs->kind == N_CALL - && fsz <= 24 - && (fsz % 8 == 0 || fsz % 8 == 1 - || fsz % 8 == 2 || fsz % 8 == 4)) { + && fsz <= 24) { cgexpr(c, f->lhs, *locals_p); if (mode == DST_PTR_LOCAL) ins2(c, A_MOVQ, amem(D_BP, srcoff), @@ -5266,8 +5260,11 @@ cgexpr(Cg *c, Node *n, Local *locals) * {AX,DX,CX} producer-shift materialise. cgplaceaddr clobbers * AX/CX, so it runs BEFORE cgexpr(rhs) and the address is saved * across the call. In-cap struct field only (size<=24, #12 - * scope); a float-bearing / over-cap success variant loud-stops - * at the producer. Local/via-ptr single-dot stays on #12. */ + * scope, #14 widened from {0,1,2,4} — the choke-point handles + * every tail incl. 3/5/6/7 via its non-padded scratch detour, + * dest_padded=0 for a packed/chained field); a float-bearing / + * over-cap success variant loud-stops at the producer. + * Local/via-ptr single-dot stays on #12. */ if (n->op == TK_ASSIGN && n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && n->rhs && (n->rhs->kind == N_TRYUNW @@ -5285,9 +5282,7 @@ cgexpr(Cg *c, Node *n, Local *locals) Type *fu = type_chase_named(n->lhs->type); if ((chained || globalbase) && fu && fu->kind == TY_STRUCT - && (int)fu->size <= 24 - && (fu->size % 8 == 0 || fu->size % 8 == 1 - || fu->size % 8 == 2 || fu->size % 8 == 4)) { + && (int)fu->size <= 24) { int ssz = (int)fu->size; if (!cgplaceaddr(c, n->lhs, D_BX, locals)) fatal("#16: global/chained aggregate " @@ -5514,18 +5509,17 @@ cgexpr(Cg *c, Node *n, Local *locals) * / via-ptr base only; a global `g.f` unwrap stays on * its pre-existing path in BOTH stages (out of scope, * not regressed). Float/over-cap loud-stop at the - * producer. */ + * producer. #14 widened size<=24 from {0,1,2,4}: the + * choke-point handles every tail incl. 3/5/6/7 via its + * non-padded scratch detour (dest_padded=0 — a packed + * single-dot field). */ if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STRUCT && (int)str_fu->size <= 24 && n->rhs && (n->rhs->kind == N_CALL || ((n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP) - && !is_global)) - && (str_fu->size % 8 == 0 - || str_fu->size % 8 == 1 - || str_fu->size % 8 == 2 - || str_fu->size % 8 == 4)) { + && !is_global))) { int ssz = (int)str_fu->size; cgexpr(c, n->rhs, locals); int base_reg, base_disp; @@ -6002,33 +5996,16 @@ cgexpr(Cg *c, Node *n, Local *locals) * 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. */ + * has no SHRQ $imm). #10. #14: folded into the + * choke-point (dest_padded=1 — the scratch IS a + * ceil-8 slot so the tail eightbyte over-store + * lands in its pad; byte-identical to the prior + * inline form). Mirrors wwstage cgenexpr.ww. */ int scr11 = cg_tagscr_slot(c, &locals, fsz); cgexpr(c, n->rhs, locals); - int regs11[3] = { D_AX, D_DX, - D_CX }; - int full11 = fsz / 8; - int tail11 = fsz % 8; - for (int i11 = 0; - i11 < full11; i11++) - ins2(c, A_MOVQ, - areg(regs11[i11]), - amem(D_BP, scr11 - + i11 * 8)); - if (tail11 > 0) { - int op11 = (tail11 == 1) - ? A_MOVB : (tail11 - == 2) ? A_MOVW - : (tail11 == 4) - ? A_MOVL : A_MOVQ; - ins2(c, op11, - areg(regs11[full11]), - amem(D_BP, scr11 - + full11 * 8)); - } + cg_agg_reg_store(c, &locals, + D_BP, scr11, fsz, 1); cgexpr(c, idx, locals); if (esz > 1) { ins2(c, A_MOVQ, @@ -6736,12 +6713,14 @@ cgexpr(Cg *c, Node *n, Local *locals) * - N_STRUCTLIT (added with #5): field-by-field * store; via_cx reloads BX before each store * so cgexpr can clobber AX/BX between fields. + * #14 widened fsz<=24 from {0,1,2,4}: the choke- + * point handles every tail incl. 3/5/6/7 via its + * non-padded scratch detour (dest_padded=0 — a + * packed chained field). */ if (fu && fu->kind == TY_STRUCT && fsz <= 24 - && n->rhs && n->rhs->kind == N_CALL - && (fsz % 8 == 0 || fsz % 8 == 1 - || fsz % 8 == 2 || fsz % 8 == 4)) { + && n->rhs && n->rhs->kind == N_CALL) { cgexpr(c, n->rhs, locals); int base_reg, base_off; if (via_cx) { @@ -7943,11 +7922,13 @@ cgexpr(Cg *c, Node *n, Local *locals) break; } if (n->rhs && n->rhs->kind == N_CALL) { + /* #14 widened sz<=24 from {0,1,2,4}: the + * choke-point handles every in-cap tail incl. + * 3/5/6/7 via its dest_padded=0 scratch detour + * (always correct; kept symmetric with the + * wwstage twin's local struct/array reassign). */ if (off != 0 && au->kind != TY_TUPLE - && sz <= 24 - && (sz % 8 == 0 || sz % 8 == 1 - || sz % 8 == 2 - || sz % 8 == 4)) { + && sz <= 24) { cgexpr(c, n->rhs, locals); cg_agg_reg_store(c, &locals, D_BP, off, sz, 0); break; @@ -7971,19 +7952,12 @@ cgexpr(Cg *c, Node *n, Local *locals) * for this arm. */ cgexpr(c, n->rhs, locals); ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_DI)); - int regs[3] = { D_AX, D_DX, D_CX }; - int full = sz / 8; - int tail = sz % 8; - for (int i = 0; i < full; i++) - ins2(c, A_MOVQ, areg(regs[i]), - amem(D_DI, i * 8)); - if (tail > 0) { - int op = (tail == 4) ? A_MOVL - : (tail == 2) ? A_MOVW - : A_MOVB; - ins2(c, op, areg(regs[full]), - amem(D_DI, full * 8)); - } + /* #14: route through the choke-point so a + * 3/5/6/7 tail is no longer dropped to a lone + * MOVB (was silent both-stage). A global symbol + * is non-padded => dest_padded=0 (its scratch + * detour copies exactly `tail` bytes). */ + cg_agg_reg_store(c, &locals, D_DI, 0, sz, 0); break; } fatal("assign: aggregate call receive " diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 881433ff..08c47ce8 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -8471,19 +8471,17 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (fu != nil) { if (fu.kind == syntax.tykind.TY_STRUCT) { let ssz: i32 = fu.size: i32; if (ssz <= 24) { - let tlm: i32 = ssz - (ssz / 8) * 8; - if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { - if (!cgplaceaddr(c, lhs, "BX")) { - let m16: str = "#16: global/chained aggregate unwrap field dest unresolved (cgplaceaddr)\n"; - os.write(2, m16.ptr, m16.len: u64); - os.exit(1); - }; - emitline("\tPUSHQ\tBX\n"); - cgexpr(c, n.rhs); - emitline("\tPOPQ\tBX\n"); - cgaggregstore(c, "BX", 0, ssz, false); - return; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + if (!cgplaceaddr(c, lhs, "BX")) { + let m16: str = "#16: global/chained aggregate unwrap field dest unresolved (cgplaceaddr)\n"; + os.write(2, m16.ptr, m16.len: u64); + os.exit(1); }; + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + cgaggregstore(c, "BX", 0, ssz, false); + return; }; }; }; }; @@ -10205,37 +10203,11 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // materialise (:9100). #10; mirrors cstage cgen.c. let scr11: i32 = tagscradd(c, tsz); cgexpr(c, n.rhs); - // AX/DX/CX -> scratch (C2c materialise) - let full11: i32 = tsz / 8; - let tail11: i32 = tsz % 8; - let wi11: i32 = 0; - for (wi11 < full11) { - let rn11: str = "AX"; - if (wi11 == 1) { rn11 = "DX"; } - else { if (wi11 == 2) { rn11 = "CX"; }; }; - emitline("\tMOVQ\t"); - emitline(rn11); - emitline(", "); - emitoff((scr11 + wi11 * 8): i64); - emitline("(BP)\n"); - wi11 += 1; - }; - if (tail11 > 0) { - 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"; }; }; - emitline("\t"); - emitline(top11); - emitline("\t"); - emitline(treg11); - emitline(", "); - emitoff((scr11 + full11 * 8): i64); - emitline("(BP)\n"); - }; + // AX/DX/CX -> scratch: #14 folds this materialise into the choke- + // point (dest_padded=true; the scratch IS a ceil-8 slot so the + // tail eightbyte over-store lands in its pad — byte-identical to + // the prior inline form). + cgaggregstore(c, "BP", scr11, tsz, true); // &arr[i] -> BX (verbatim scalar arm) cgexpr(c, idx); if (esz > 1) { @@ -10620,16 +10592,13 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (ssi != nil) { let ssz: i32 = structabisize(ssi); if (ssz <= 24) { - let tlm: i32 = ssz - (ssz / 8) * 8; - if (tlm == 0 || tlm == 1 - || tlm == 2 || tlm == 4) { - cgexpr(c, n.rhs); - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); - cgaggregstore(c, "BX", fi.foff, ssz, false); - return; - }; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + cgexpr(c, n.rhs); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgaggregstore(c, "BX", fi.foff, ssz, false); + return; }; }; }; @@ -10842,13 +10811,10 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (ssi != nil) { let ssz: i32 = structabisize(ssi); if (ssz <= 24) { - let tlm: i32 = ssz - (ssz / 8) * 8; - if (tlm == 0 || tlm == 1 - || tlm == 2 || tlm == 4) { - cgexpr(c, n.rhs); - cgaggregstore(c, "BP", lc.off + fi.foff, ssz, false); - return; - }; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + cgexpr(c, n.rhs); + cgaggregstore(c, "BP", lc.off + fi.foff, ssz, false); + return; }; }; }; @@ -11106,16 +11072,13 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (ssi != nil) { let ssz: i32 = structabisize(ssi); if (ssz <= 24) { - let tlm: i32 = ssz - (ssz / 8) * 8; - if (tlm == 0 || tlm == 1 - || tlm == 2 || tlm == 4) { - cgexpr(c, n.rhs); - emitline("\tLEAQ\t"); - emitsymname(c, bn); - emitline("(SB), BX\n"); - cgaggregstore(c, "BX", fi.foff, ssz, false); - return; - }; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + cgexpr(c, n.rhs); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + cgaggregstore(c, "BX", fi.foff, ssz, false); + return; }; }; }; @@ -11778,24 +11741,21 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // (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 - || tlm == 2 || tlm == 4) { - cgexpr(c, n.rhs); - if (viacx) { - if (ptrroot) { - emitline("\tMOVQ\t"); - emitoff(rootoff: i64); - emitline("(BP), BX\n"); - } else { - emitline("\tLEAQ\t"); - emitsymname(c, rootname); - emitline("(SB), BX\n"); - }; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + cgexpr(c, n.rhs); + if (viacx) { + if (ptrroot) { + emitline("\tMOVQ\t"); + emitoff(rootoff: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, rootname); + emitline("(SB), BX\n"); }; - if (viacx) { cgaggregstore(c, "BX", totaloff, lsz, false); } else { cgaggregstore(c, "BP", rootoff + totaloff, lsz, false); }; - return; }; + if (viacx) { cgaggregstore(c, "BX", totaloff, lsz, false); } else { cgaggregstore(c, "BP", rootoff + totaloff, lsz, false); }; + return; }; }; }; @@ -12240,29 +12200,11 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), DI\n"); - let full: i32 = aggsz / 8; - let tail: i32 = aggsz - full * 8; - let i: i32 = 0; - for (i < full) { - emitline("\tMOVQ\t"); - emitline(tupreg(i)); - emitline(", "); - emitoff((i * 8): i64); - emitline("(DI)\n"); - i += 1; - }; - if (tail > 0) { - let top: str = "MOVB"; - if (tail == 4) { top = "MOVL"; }; - if (tail == 2) { top = "MOVW"; }; - emitline("\t"); - emitline(top); - emitline("\t"); - emitline(tupreg(full)); - emitline(", "); - emitoff((full * 8): i64); - emitline("(DI)\n"); - }; + // #14: route through the choke-point so a 3/5/6/7 tail is no + // longer dropped to a lone MOVB (was silent both-stage). A global + // symbol is non-padded => dest_padded=false (its scratch detour + // copies exactly `tail` bytes). + cgaggregstore(c, "DI", 0, aggsz, false); return; }; }; @@ -12534,13 +12476,10 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { }; let lcsz: i32 = lcnsz; if (lcsz <= 24) { - let tlm: i32 = lcsz - (lcsz / 8) * 8; - if (tlm == 0 || tlm == 1 - || tlm == 2 || tlm == 4) { - cgexpr(c, n.rhs); - cgaggregstore(c, "BP", off, lcsz, false); - return; - }; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + cgexpr(c, n.rhs); + cgaggregstore(c, "BP", off, lcsz, false); + return; }; }; }; @@ -12571,13 +12510,10 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { }; }; if (lcsz <= 24) { - let tlm: i32 = lcsz - (lcsz / 8) * 8; - if (tlm == 0 || tlm == 1 - || tlm == 2 || tlm == 4) { - cgexpr(c, n.rhs); - cgaggregstore(c, "BP", off, lcsz, false); - return; - }; + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. + cgexpr(c, n.rhs); + cgaggregstore(c, "BP", off, lcsz, false); + return; }; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 2748ea57..cd3eacde 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -5325,19 +5325,16 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *syntax.node, // silently dropped past the first qword for any // fsz > 8 (only AX got stored). // - // Sized stores: MOVQ for full 8B chunks plus a - // sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`. - // Mirror of cstage cg_structlit_fill's #20 branch. - // MOVW-for-tail==2 only fires on shapes that - // didn't compile before, so no #13 byte-identity - // concern. + // The choke-point stores MOVQ for full 8B chunks + // plus a sized tail (MOVL/MOVW/MOVB) by `tail = + // fsz%8`. Mirror of cstage cg_structlit_fill's #20 + // branch. // - // Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches - // #4's cgreturn ABI: >24B falls through (sret - // deferred); fsz%8 ∈ {3,5,6,7} would need shift- - // store and is also unsupported by #4 — falls - // through to the existing AX-only wrongness - // (consistent, tracked as follow-up). + // Guard `cfsz <= 24` (#14 widened from {0,1,2,4}): + // the choke-point handles every in-cap tail incl. + // 3/5/6/7 via its non-padded scratch detour + // (dest_padded=false — a struct-lit field is + // packed); >24B falls through (sret deferred). // // INVARIANT: between cgexpr(N_CALL) and the // AX/DX/CX stores below, NO instruction may touch @@ -5369,10 +5366,8 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *syntax.node, // MOVQ+MOVL where cstage // writes MOVQ+MOVQ. let cfsz: i32 = structabisize(csi); - let crem: i32 = cfsz - (cfsz / 8) * 8; if (cfsz <= 24) { - if (crem == 0 || crem == 1 - || crem == 2 || crem == 4) { + // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. cgexpr(c, fieldnode.lhs); if (mode == 1) { emitline("\tMOVQ\t"); @@ -5390,7 +5385,6 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *syntax.node, cgaggregstore(c, basereg, disp + fi.foff, cfsz, false); callwhole = true; }; - }; }; }; }; diff --git a/test/lang/narrow_tail_widen_test.ww b/test/lang/narrow_tail_widen_test.ww new file mode 100644 index 00000000..6b6966ca --- /dev/null +++ b/test/lang/narrow_tail_widen_test.ww @@ -0,0 +1,138 @@ +// narrow_tail_widen_test — #14 commit-2: the narrow-tail aggregate-register +// materialise choke-point (cg_agg_reg_store / cgaggregstore), now reached by +// the WIDENED gates. Before #14 these sites gated `tail%8 ∈ {0,1,2,4}` and a +// 3/5/6/7-byte aggregate tail fell through to a lone narrow MOV (a SILENT +// both-stage member drop — both cstage and wwstage emitted IDENTICALLY wrong +// asm, so byte-id was GREEN and BLIND). Widening each gate to the full +// `sz<=24` routes 3/5/6/7 through the choke-point's non-padded scratch detour +// (dest_padded=false: MOVQ the tail eightbyte into a ceil-8 pad, then an +// exact `tail`-byte aggcopy into the packed dest). +// +// Each @test POISON-SEEDS every dest eightbyte with a sentinel distinct from +// every mk() value, performs the aggregate store, then value-asserts each +// eightbyte INCLUDING the dropped 3/5/6/7 tail word. A drop leaves the poison +// (mismatch). Where the dest is a PACKED field/element (C, D) a trailing +// neighbour is sentinel-checked too: it pins dest_padded=FALSE (a wrong =true +// would over-store the tail eightbyte and clobber the neighbour). +// +// Heterogeneous shapes per site + #111 (row-array @test blocked) => inline +// @test fns, not a row table. A 14B struct/array (maxalign<=2) keeps a real +// tail-6: an i64-bearing field pads to 16B (tail 0) and never trips it. + +package narrow_tail_widen_test; + +type t14 = struct { v: [7]i16 }; // 14B: full=1, tail=6 +type boxed = struct { f: t14, guard: i16 }; // f@0 (14B), guard@14 +type nested = struct { m: boxed }; // o.m.f chained dot +type myerr = !i32; // site B unwrap error variant + +fn mk14() t14 = { + return t14 { v = [10i16, 20i16, 30i16, 40i16, 50i16, 60i16, 70i16] }; +}; +fn mk7arr() [7]i16 = { + return [10i16, 20i16, 30i16, 40i16, 50i16, 60i16, 70i16]; +}; +fn mk14res() (t14 | myerr) = { + return t14 { v = [10i16, 20i16, 30i16, 40i16, 50i16, 60i16, 70i16] }; +}; + +let gg: [7]i16 = [0i16, 0i16, 0i16, 0i16, 0i16, 0i16, 0i16]; // site G dest +let gb: boxed = boxed { // site B dest + f = t14 { v = [0i16, 0i16, 0i16, 0i16, 0i16, 0i16, 0i16] }, + guard = 0i16, +}; + +fn poison7(p: *[7]i16) void = { + let i: i32 = 0; + for (i < 7) { p[i] = 0x7f7fi16; i += 1; }; +}; + +// Site F (cstage cgen.c N_ASSIGN local-ident array `g = call`; ww cgenexpr.ww +// 12574). Whole local array reassign from an aggregate-returning call. +@test fn f_local_array() void = { + let g: [7]i16; + poison7(&g); + g = mk7arr(); + assert(g[0] == 10i16); + assert(g[3] == 40i16); + assert(g[4] == 50i16); // eb1 byte 0-1 + assert(g[5] == 60i16); // eb1 byte 2-3 — tail, dropped pre-#14 + assert(g[6] == 70i16); // eb1 byte 4-5 — tail, dropped pre-#14 +}; + +// Site F (struct twin; ww cgenexpr.ww 12537). Whole local struct reassign. +@test fn f_local_struct() void = { + let s: t14; + poison7(&s.v); + s = mk14(); + assert(s.v[0] == 10i16); + assert(s.v[4] == 50i16); + assert(s.v[5] == 60i16); // tail, dropped pre-#14 + assert(s.v[6] == 70i16); // tail, dropped pre-#14 +}; + +// Site G (cstage cgen.c N_ASSIGN global-ident array `g = call`, the inline +// loop now routed; ww cgenexpr.ww 12231). Whole GLOBAL array reassign — G +// had NO {0,1,2,4} gate, it dropped 3/5/6/7 via a lone MOVB. +@test fn g_global_array() void = { + poison7(&gg); + gg = mk7arr(); + assert(gg[0] == 10i16); + assert(gg[4] == 50i16); + assert(gg[5] == 60i16); // tail, dropped pre-#14 (MOVB wrote only byte 0) + assert(gg[6] == 70i16); // tail, dropped pre-#14 +}; + +// Site C (cstage cgen.c single-dot `b.f = call`; ww cgenexpr.ww 10845 local +// twin). PACKED field => guard neighbour pins dest_padded=false. +@test fn c_single_dot() void = { + let b: boxed; + poison7(&b.f.v); + b.guard = 0x55aai16; // neighbour sentinel + b.f = mk14(); + assert(b.f.v[0] == 10i16); + assert(b.f.v[4] == 50i16); + assert(b.f.v[5] == 60i16); // tail, dropped pre-#14 + assert(b.f.v[6] == 70i16); // tail, dropped pre-#14 + assert(b.guard == 0x55aai16); // intact: tail aggcopy is exactly 6 bytes +}; + +// Site D (cstage cgen.c chained-dot terminal `o.m.f = call`; ww cgenexpr.ww +// 11781). PACKED field of a nested struct => guard neighbour check. +@test fn d_chained_dot() void = { + let o: nested; + poison7(&o.m.f.v); + o.m.guard = 0x55aai16; + o.m.f = mk14(); + assert(o.m.f.v[0] == 10i16); + assert(o.m.f.v[4] == 50i16); + assert(o.m.f.v[5] == 60i16); // tail, dropped pre-#14 + assert(o.m.f.v[6] == 70i16); // tail, dropped pre-#14 + assert(o.m.guard == 0x55aai16); +}; + +// Site A (cstage cgen.c cg_structlit_fill nested-CALL field; ww cgenutil.ww +// cgstructlitfill 5388). A struct-LITERAL whose field is an aggregate call. +@test fn a_structlit_field() void = { + let x: boxed = boxed { f = mk14(), guard = 999i16 }; + assert(x.f.v[0] == 10i16); + assert(x.f.v[4] == 50i16); + assert(x.f.v[5] == 60i16); // tail, dropped pre-#14 + assert(x.f.v[6] == 70i16); // tail, dropped pre-#14 + assert(x.guard == 999i16); +}; + +// Site B (cstage cgen.c N_ASSIGN #16 global/chained `g.f = mk()!` unwrap; ww +// cgenexpr.ww 8483). A GLOBAL struct's field unwrap-receive — pre-#16 the +// generic single-word store DROPPED w1/w2; #16 routed it through the +// {AX,DX,CX} materialise, #14 widens its tail to 3/5/6/7. +@test fn b_global_unwrap() void = { + poison7(&gb.f.v); + gb.guard = 0x55aai16; + gb.f = mk14res()!; + assert(gb.f.v[0] == 10i16); + assert(gb.f.v[4] == 50i16); + assert(gb.f.v[5] == 60i16); // tail, dropped pre-#14 + assert(gb.f.v[6] == 70i16); // tail, dropped pre-#14 + assert(gb.guard == 0x55aai16); +};