cgen: store all eightbytes when an in-cap aggregate call returns into a field of an indexed element (#11)

The arr[i].f=mk() assign arm had no aggregate-field sub-arm, so a by-value aggregate field receive fell to the scalar default (one MOVQ, dropping DX/CX) — silent on BOTH stages (byte-id blind). Add a dual-site symmetric in-cap N_CALL arm mirroring C2c (c83a340): scratch-first materialise AX/DX/CX, then word-copy to (fi.foff+k*8) within &arr[i], sizing from the natural field size fi.fsz (not slotsize). Rule-7 LOUD-STOP for the three cases the in-cap GP path cannot transport: over-cap sret (#11c/#234), a float-bearing field whose eightbyte classifies SSE (#11/#165), and a 3/5/6/7-byte sub-8 tail the single narrow tail MOV cannot express (the general cascade tail is the shared C2c/#11 follow-up, task #10). Value-asserting pins (poison-seeded, redden under each stage's independent revert) plus cfail pins for the three loud-stops.

Contained to the indexed base + in-cap call rhs; arr[i].f=src (#11b) and over-cap (#11c) are separate.
This commit is contained in:
2026-06-27 18:49:03 +09:00
parent b8d55a729e
commit a0e330b283
7 changed files with 522 additions and 0 deletions

View File

@@ -9984,6 +9984,185 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
emitline("\n");
return;
};
// #11: an in-cap aggregate-returning CALL into
// an AGGREGATE field of an indexed element
// `arr[i].f = mk()`. The scalar default below
// stores only AX (eb0), dropping DX/CX — a
// SILENT both-stage field-drop, the field-of-
// indexed twin of C2c's whole-element
// arr[i]=mk() arm (cgenexpr.ww :9100). Scratch-
// first materialise of the AX/DX/CX return (not
// a PUSHQ spill — keeps the CALL at the frame's
// 16B alignment and survives an idx that itself
// contains a call), reuse the scalar arm's
// &arr[i]->BX address computation verbatim, then
// word-copy scratch to fi.foff(BX). tsz from the
// type table (fieldsize, rule 13). In-cap only
// (callsretsize==0); over-cap sret-into-field
// LOUD-STOPS (#11c/#234, task #8) and a float-
// bearing aggregate LOUD-STOPS (#165/#171 — a
// pure-float return eightbyte rides X0/X1 which
// the GP AX/DX/CX cursor cannot read). Mirrors
// cstage cgen.c.
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL) {
if (callsretsize(c, n.rhs) > 0) {
let m11o: str = "#11c/#234: over-cap (sret) aggregate field receive arr[i].f=mk() unwired (cs!=ww; task #8)\n";
os.write(2, m11o.ptr, m11o.len: u64);
os.exit(1);
};
// structfloatclass mirrors the return-
// side SSE routing (cgenstmt.ww #171a);
// a field typed DIRECTLY as a tuple
// misses it (non-N_TNAME) yet the bare-
// tuple return routes floats to tupsse —
// guard it too so neither stage silently
// stores X0 garbage through the GP cursor.
let sse11: bool = structfloatclass(c, fi.tnode) != 0;
if (!sse11) {
let rt11: *syntax.node = resolvetype(c, fi.tnode);
if (rt11 != nil) { if (rt11.kind == syntax.nkind.N_TTUPLE) {
let q11: *syntax.node = rt11.list;
for (q11 != nil) {
if (isfloattype(c, q11.lhs)) { sse11 = true; };
q11 = q11.next;
};
};};
};
if (sse11) {
let m11f: str = "#11/#165: float-bearing aggregate field receive arr[i].f=mk() unwired (SSE return eightbyte; #171)\n";
os.write(2, m11f.ptr, m11f.len: u64);
os.exit(1);
};
// fi.fsz is the field's NATURAL size
// (tf.type_.size, cgenutil.ww :2736) —
// the byte-id twin of cstage's
// fsz=ft->size. fieldsize() returns the
// slot-PADDED size (16 for a 12B
// 3×i32), which both diverges from
// cstage AND a full-MOVQ tail on it
// 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);
};
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 = "MOVB";
if (tail11 == 4) { top11 = "MOVL"; }
else { if (tail11 == 2) { top11 = "MOVW"; }; };
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");
};
// &arr[i] -> BX (verbatim scalar arm)
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (baseisarray) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tAX, BX\n");
if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); };
// word-copy scratch -> fi.foff(BX),
// tail-aware (C2c copy); foff on every
// eightbyte, sub-8 tail stays sub-8.
let kc11: i32 = 0;
for (kc11 + 8 <= tsz) {
emitline("\tMOVQ\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 8;
};
if (kc11 + 4 <= tsz) {
emitline("\tMOVL\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 4;
};
if (kc11 + 2 <= tsz) {
emitline("\tMOVW\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 2;
};
if (kc11 + 1 <= tsz) {
emitline("\tMOVB\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 1;
};
return;
};
// tsz<=8 in-cap aggregate returns wholly
// in AX; the scalar default's MOVQ/MOVL AX
// store is the correct 1-word receive.
};
// scalar plain `=`
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");