cgen: copy all eightbytes when a non-call aggregate assigns into a field of an indexed element (#11b)

The arr[i].f=src legacy assign block enumerated scalar field-type arms then fell to a 1-word scalar default, so a non-call aggregate source (ident/dot/index) cgexpr'd only its first word into AX and stored one eightbyte — silent on BOTH stages (byte-id blind). The non-indexed bases (local/deref/chained/global) reach the general assign resolver's canonical aggargsrcaddr+aggcopy; the indexed arm short-circuited before it. Route the indexed base through the block's own proven &arr[i] spine into the same aggargsrcaddr+aggcopy emitters (DRY — no third copy), dual-site symmetric. Unlike #11's in-cap arm, the source is a memory address so aggcopy is a pure memcpy: float bits and the sub-8 tail transport verbatim, no loud-stop needed. Did not fall through to the general resolver because its cgplaceaddr N_INDEX arm rejects a *[N]S (TY_PTR) base (latent resolver gap, filed separately).

Contained to the indexed base + non-call aggregate-field rhs; value-asserting pins redden under each stage's independent revert.
This commit is contained in:
2026-06-27 19:40:04 +09:00
parent 737126ed69
commit 3719ff1c64
4 changed files with 302 additions and 0 deletions

View File

@@ -10163,6 +10163,78 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
// in AX; the scalar default's MOVQ/MOVL AX
// store is the correct 1-word receive.
};
// #11b: a NON-call AGGREGATE source into an aggregate
// field of an indexed element `arr[i].f = src` (src an
// ident / .g / index). The scalar default below loads
// only the source's FIRST word into AX and stores ONE
// word — dropping the rest (a SILENT both-stage member
// drop, the non-call twin of the #11 in-cap CALL arm
// above; byte-id blind). Unlike #11's GP AX/DX/CX cursor
// the source is a MEMORY address, so the shared mem-to-
// mem aggcopy transports EVERY byte: a sub-8 tail (MOVL/
// MOVW/MOVB) and float bits copy verbatim, so NO tail/
// float/over-cap loud-stop is needed here (those #11
// stops were register-cursor artefacts). Reuse the
// block's own &arr[i] spine (proven for [N]S / *[N]S /
// []S by the sibling arms) -> BX + fi.foff, then funnel
// through aggargsrcaddr (src -> SI) + aggcopy — the ONE
// copy emitter the non-indexed bases use (DRY, rule 8).
// tsz natural (fi.fsz, type table). Mirrors cstage cgen.c.
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind != syntax.nkind.N_CALL) {
let fk11b: *syntax.tinfo = tichase(fi.tnode.type_: *syntax.tinfo);
let isagg11b: bool = false;
if (fk11b != nil) {
if (fk11b.kind == syntax.tykind.TY_STRUCT
|| fk11b.kind == syntax.tykind.TY_ARRAY
|| fk11b.kind == syntax.tykind.TY_TUPLE) {
isagg11b = true;
};
};
if (isagg11b) {
let tsz11b: i32 = fi.fsz;
if (tsz11b > 8) {
// &arr[i].f -> BX (verbatim sibling-arm spine)
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"); };
if (fi.foff != 0) {
emitline("\tADDQ\t$");
emitint(fi.foff: i64);
emitline(", BX\n");
};
// spill dest across the source-address resolution
// (the #270-1b order: aggargsrcaddr clobbers BX).
emitline("\tPUSHQ\tBX\n");
if (!aggargsrcaddr(c, n.rhs, "SI")) {
let m11b: str = "#11b: aggregate field receive arr[i].f=src - source shape unwired (rule-7)\n";
os.write(2, m11b.ptr, m11b.len: u64);
os.exit(1);
};
emitline("\tPOPQ\tBX\n");
aggcopy(c, tsz11b);
return;
};
// tsz<=8 aggregate: one word, the scalar default's
// single store is the correct copy.
};
};
// scalar plain `=`
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");