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

@@ -5982,6 +5982,57 @@ cgexpr(Cg *c, Node *n, Local *locals)
* wholly in AX; the scalar default's * wholly in AX; the scalar default's
* single store is the correct receive. */ * single store is the correct 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
* cg_aggcopy transports EVERY byte: a sub-8
* tail 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 + foff, then funnel
* through aggarg_srcaddr (src -> SI) +
* cg_aggcopy — the ONE copy emitter the non-
* indexed bases use (DRY). fsz natural
* (ft->size). Mirrors wwstage cgenexpr.ww. */
if (n->op == TK_ASSIGN && n->rhs
&& n->rhs->kind != N_CALL && fu
&& (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY
|| fu->kind == TY_TUPLE) && fsz > 8) {
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
if (is_arr)
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
else
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
if (viaptr)
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX));
if (foff != 0)
ins2(c, A_ADDQ, aimm(foff), areg(D_BX));
/* spill dest across the source-address
* resolution (the #270-1b order:
* aggarg_srcaddr clobbers BX). */
ins1(c, A_PUSHQ, areg(D_BX));
if (!aggarg_srcaddr(c, n->rhs, D_SI, locals))
fatal("#11b: aggregate field receive "
"arr[i].f=src - source shape unwired "
"(rule-7)");
ins1(c, A_POPQ, areg(D_BX));
cg_aggcopy(c, fsz);
break;
}
if (n->op == TK_ASSIGN) { if (n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals); cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, ins1(c, A_PUSHQ,

View File

@@ -10163,6 +10163,78 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
// in AX; the scalar default's MOVQ/MOVL AX // in AX; the scalar default's MOVQ/MOVL AX
// store is the correct 1-word receive. // 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 `=` // scalar plain `=`
cgexpr(c, n.rhs); cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n"); emitline("\tPUSHQ\tAX\n");

View File

@@ -0,0 +1,43 @@
// idx_dot_src_aggregate_runonly_test — #11b sub-8-tail anti-clobber through a
// LOCAL [N]<struct-with-sub-8-tail-field> array. The fix's MOVL (not MOVQ) tail
// is VALUE-verified here: the dropped tail would read the poison 9, and a tail
// WIDENED to MOVQ would write bytes 8..15 and smash the adjacent g at +12 — both
// caught by asserting f.c AND g. Exit-correct under BOTH stages.
//
// _runonly (excluded from the test-lang-byteid T2 gate) because a local
// [N]<sub-8-tail-struct> trips a SEPARATE PRE-EXISTING let-array slotsize cs!=ww
// FRAME divergence — ww sizes the array element by slotsize (s12 slot-padded),
// cstage by natural size — that PREDATES and is INDEPENDENT of #11b: empirically
// the #11b receive INSTRUCTIONS are byte-identical (both stages emit `MOVQ
// (SI),AX` then the `MOVL 8(SI),AX` tail); only the container's frame SIZE and
// the BP-relative offsets shift ($48 vs $64). The byte-id twin
// (idx_dot_src_aggregate_test) covers the same sub-8-tail shape via global-
// backed bases, where the slotsize bug does not bite (task #9).
package idx_dot_src_aggregate_runonly_test;
type t12 = struct { a: i32, b: i32, c: i32 };
type s12 = struct { f: t12, g: i32 };
fn one() i64 = { return 1i64; };
@test fn subtail_local_ident_const() void = {
let a: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}];
let src: t12 = t12{a=10i32, b=20i32, c=30i32};
a[1].f = src;
assert(a[1].f.a == 10i32);
assert(a[1].f.b == 20i32);
assert(a[1].f.c == 30i32);
assert(a[1].g == 9i32);
assert(a[0].f.a == 9i32);
};
@test fn subtail_local_ident_runtime() void = {
let a: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}];
let src: t12 = t12{a=10i32, b=20i32, c=30i32};
a[one()].f = src;
assert(a[1].f.a == 10i32);
assert(a[1].f.b == 20i32);
assert(a[1].f.c == 30i32);
assert(a[1].g == 9i32);
};

View File

@@ -0,0 +1,136 @@
// idx_dot_src_aggregate_test — #11b: a NON-call AGGREGATE source assigned into
// an AGGREGATE field of an INDEXED element `arr[i].f = src` (src an ident or a
// field `.g`, NOT a function call). Pre-fix the N_DOT(N_INDEX) assign arm had no
// aggregate-source case: it fell to the 1-word scalar default — cgexpr loaded
// only the source's FIRST word into AX and stored ONE word, dropping the rest.
// BOTH stages emitted byte-IDENTICAL wrong asm (gate-blind, the #263 both-wrong
// form) — the non-call twin of #11 (idx_dot_aggret_recv_test, the in-cap CALL
// source). Unlike #11's GP AX/DX/CX cursor, the source here 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 the fix needs NO tail/float/over-
// cap loud-stop (those #11 stops were register-cursor artefacts). Each @test
// POISON-seeds the field + its neighbour with sentinel 9 (distinct from 0 AND
// every expected value) and asserts EVERY member: a dropped word reads 9 and
// fails. Covers IDENT and DOT sources, full=2/full=3 (CX word), the sub-8-tail
// MOVL row (anti-clobber: the 12B field's tail MUST stay MOVL — a widened MOVQ
// would smash the adjacent g at +12), foff!=0, a float-bearing field, and bases
// [N]S / *[N]S / []S with const + runtime index. The sub-8-tail rows use a
// GLOBAL-backed base: a LOCAL [N]<sub-8-tail-struct> array trips a SEPARATE
// PRE-EXISTING let-array slotsize cs!=ww FRAME divergence (see the _runonly
// twin; task #9), independent of #11b. T2 keeps the cs==ww net.
package idx_dot_src_aggregate_test;
type t16 = struct { a: i64, b: i64 };
type t24 = struct { a: i64, b: i64, c: i64 };
type t12 = struct { a: i32, b: i32, c: i32 };
type ft = struct { x: f64, y: i64 };
type s16 = struct { f: t16, g: i64 };
type s24 = struct { f: t24, g: i64 };
type sfnf = struct { g: i64, f: t16 };
type sf = struct { f: ft, g: i64 };
type s12 = struct { f: t12, g: i32 };
type w16 = struct { pad: i64, inner: t16 };
type w12 = struct { pad: i64, inner: t12 };
let g12p: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}];
let g12s: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}];
let g12d: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}];
// full=2, [N]S value-array local, IDENT src, const index, foff=0
@test fn struct16_local_ident_const() void = {
let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}];
let src: t16 = t16{a=40i64, b=20i64};
a[1].f = src;
assert(a[1].f.a == 40i64);
assert(a[1].f.b == 20i64);
assert(a[1].g == 9i64);
assert(a[0].f.a == 9i64);
};
// full=2, [N]S local, DOT src (`w.inner`), const index, foff=0
@test fn struct16_local_dot_const() void = {
let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}];
let w: w16 = w16{pad=7i64, inner=t16{a=40i64, b=20i64}};
a[1].f = w.inner;
assert(a[1].f.a == 40i64);
assert(a[1].f.b == 20i64);
assert(a[1].g == 9i64);
};
// full=3 (CX word), [N]S local, IDENT src, runtime index, foff=0
@test fn struct24_local_ident_runtime() void = {
let a: [2]s24 = [s24{f=t24{a=9i64,b=9i64,c=9i64},g=9i64}, s24{f=t24{a=9i64,b=9i64,c=9i64},g=9i64}];
let src: t24 = t24{a=100i64, b=20i64, c=3i64};
a[one()].f = src;
assert(a[1].f.a == 100i64);
assert(a[1].f.b == 20i64);
assert(a[1].f.c == 3i64);
assert(a[1].g == 9i64);
};
// foff != 0 (f at offset 8), [N]S local, IDENT src, const index
@test fn fnotfirst_local_ident() void = {
let a: [2]sfnf = [sfnf{g=9i64,f=t16{a=9i64,b=9i64}}, sfnf{g=9i64,f=t16{a=9i64,b=9i64}}];
let src: t16 = t16{a=40i64, b=20i64};
a[1].f = src;
assert(a[1].f.a == 40i64);
assert(a[1].f.b == 20i64);
assert(a[1].g == 9i64);
};
// float-bearing field (mem-to-mem copies float bits verbatim — no loud-stop),
// [N]S local, IDENT src, const index
@test fn float_local_ident() void = {
let a: [2]sf = [sf{f=ft{x=9.0f64,y=9i64},g=9i64}, sf{f=ft{x=9.0f64,y=9i64},g=9i64}];
let src: ft = ft{x=1.5f64, y=7i64};
a[1].f = src;
assert(a[1].f.x == 1.5f64);
assert(a[1].f.y == 7i64);
assert(a[1].g == 9i64);
};
// sub-8-tail (12B, MOVL tail), IDENT src, *[N]S -> global, const index
@test fn subtail_ptr_ident_const() void = {
let p: *[2]s12 = &g12p;
let src: t12 = t12{a=10i32, b=20i32, c=30i32};
p[1].f = src;
assert(g12p[1].f.a == 10i32);
assert(g12p[1].f.b == 20i32);
assert(g12p[1].f.c == 30i32);
assert(g12p[1].g == 9i32);
};
// sub-8-tail (12B, MOVL tail), IDENT src, []S -> global, runtime index
@test fn subtail_slice_ident_runtime() void = {
let sl: []s12 = g12s[0:2];
let src: t12 = t12{a=10i32, b=20i32, c=30i32};
sl[one()].f = src;
assert(g12s[1].f.a == 10i32);
assert(g12s[1].f.b == 20i32);
assert(g12s[1].f.c == 30i32);
assert(g12s[1].g == 9i32);
};
// INDEX src (`srcarr[0]`), [N]S local, const index, foff=0
@test fn struct16_local_index_src() void = {
let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}];
let srcarr: [2]t16 = [t16{a=40i64, b=20i64}, t16{a=99i64, b=99i64}];
a[1].f = srcarr[0];
assert(a[1].f.a == 40i64);
assert(a[1].f.b == 20i64);
assert(a[1].g == 9i64);
};
// sub-8-tail (12B, MOVL tail), DOT src (`w.inner`), *[N]S -> global, const index
@test fn subtail_ptr_dot_const() void = {
let p: *[2]s12 = &g12d;
let w: w12 = w12{pad=7i64, inner=t12{a=10i32, b=20i32, c=30i32}};
p[1].f = w.inner;
assert(g12d[1].f.a == 10i32);
assert(g12d[1].f.b == 20i32);
assert(g12d[1].f.c == 30i32);
assert(g12d[1].g == 9i32);
};
fn one() i64 = { return 1i64; };