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).
This commit is contained in:
2026-06-28 15:16:52 +09:00
parent e7fefa3eb7
commit b5e76fb1fa
4 changed files with 249 additions and 207 deletions

View File

@@ -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 "