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

@@ -5810,6 +5810,178 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_BX, foff + 8));
break;
}
/* #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
* (cgen.c :6764). 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 foff(BX). In-cap only
* (cg_sret_retsize==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 wwstage
* cgenexpr.ww. */
if (n->op == TK_ASSIGN && n->rhs
&& n->rhs->kind == N_CALL) {
if (cg_sret_retsize(ft) > 0)
fatal("#11c/#234: over-cap "
"(sret) aggregate field "
"receive arr[i].f=mk() "
"unwired (cs!=ww; task #8)");
/* struct_float_class mirrors the
* return-side SSE routing (#171a); a
* field typed DIRECTLY as a tuple misses
* it yet the bare-tuple return routes
* floats to tuple_sse_seq — guard it too
* so neither stage silently stores X0
* garbage through the GP cursor. */
int sclass11[2];
int sse11 = struct_float_class(ft,
sclass11) > 0;
if (!sse11) {
Type *tu11 =
type_chase_named(ft);
if (tu11 && tu11->kind
== TY_TUPLE)
for (Tparam *p11 =
tu11->params; p11;
p11 = p11->next) {
int f32_11;
if (fld_isfloat(
p11->type,
&f32_11))
sse11 = 1;
}
}
if (sse11)
fatal("#11/#165: float-bearing "
"aggregate field receive "
"arr[i].f=mk() unwired (SSE "
"return eightbyte; #171)");
if (fsz > 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 (:6764) 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
* sibling of the over-cap/float stops
* above. Tails 0/1/2/4 are exact and flow
* through. Mirrors wwstage cgenexpr.ww. */
int tl11 = fsz % 8;
if (tl11 == 3 || tl11 == 5
|| tl11 == 6 || tl11 == 7)
fatal("#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)");
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 == 4)
? A_MOVL : (tail11
== 2) ? A_MOVW
: A_MOVB;
ins2(c, op11,
areg(regs11[full11]),
amem(D_BP, scr11
+ full11 * 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));
int k11 = 0;
for (; k11 + 8 <= fsz;
k11 += 8) {
ins2(c, A_MOVQ,
amem(D_BP, scr11
+ k11), areg(D_AX));
ins2(c, A_MOVQ,
areg(D_AX),
amem(D_BX,
foff + k11));
}
if (k11 + 4 <= fsz) {
ins2(c, A_MOVL,
amem(D_BP, scr11
+ k11), areg(D_AX));
ins2(c, A_MOVL,
areg(D_AX),
amem(D_BX,
foff + k11));
k11 += 4;
}
if (k11 + 2 <= fsz) {
ins2(c, A_MOVW,
amem(D_BP, scr11
+ k11), areg(D_AX));
ins2(c, A_MOVW,
areg(D_AX),
amem(D_BX,
foff + k11));
k11 += 2;
}
if (k11 + 1 <= fsz) {
ins2(c, A_MOVB,
amem(D_BP, scr11
+ k11), areg(D_AX));
ins2(c, A_MOVB,
areg(D_AX),
amem(D_BX,
foff + k11));
k11 += 1;
}
break;
}
/* fsz<=8 in-cap aggregate returns
* wholly in AX; the scalar default's
* single store is the correct receive. */
}
if (n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ,

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");

View File

@@ -0,0 +1,43 @@
// idx_dot_aggret_recv_runonly_test — #11 sub-8-tail anti-clobber through a
// LOCAL [N]<struct-with-sub-8-tail-field> array. The fix's MOVL (not MOVQ) tail
// + foff-on-every-eightbyte are VALUE-verified here: the dropped eb1 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 (S slot-padded to
// 24 -> [2]S=48), cstage by natural size (S=16 -> 32) — that PREDATES and is
// INDEPENDENT of #11: `let arr:[2]S; return arr[1].g` already diverges $32 vs
// $48 with no call involved. The #11 receive INSTRUCTIONS are byte-identical
// (both stages emit the MOVL tail); only the container's frame SIZE differs.
// The byte-id twin (idx_dot_aggret_recv_test) covers the same sub-8-tail shape
// via global-backed bases, where the slotsize bug does not bite.
package idx_dot_aggret_recv_runonly_test;
type t12 = struct { a: i32, b: i32, c: i32 };
type s12 = struct { f: t12, g: i32 };
fn mk12() t12 = { return t12{a=10i32, b=20i32, c=30i32}; };
fn one() i64 = { return 1i64; };
@test fn subtail_local_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}];
a[1].f = mk12();
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_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}];
a[one()].f = mk12();
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,83 @@
// idx_dot_aggret_recv_test — #11: an in-cap aggregate-returning CALL received
// into an AGGREGATE field of an INDEXED element `arr[i].f = mk()`. Pre-fix the
// N_DOT(N_INDEX) assign arm had no case for a struct/array/tuple field filled
// from an in-cap (<=24B, AX/DX/CX-return) call rhs: it fell to the 1-word
// scalar default — only AX (eb0) stored, DX/CX dropped. BOTH stages emitted
// byte-IDENTICAL wrong asm (gate-blind, the #263 both-wrong form). The field-
// of-indexed twin of C2c (idx_aggret_recv_test) — a DIFFERENT cgassign arm.
// 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
// then reads 9 and fails. Covers 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, 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), independent of #11. T2 keeps
// the cs==ww net.
package idx_dot_aggret_recv_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 s16 = struct { f: t16, g: i64 };
type s24 = struct { f: t24, g: i64 };
type sfnf = struct { g: i64, f: t16 };
type s12 = struct { f: t12, g: i32 };
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}];
fn mk16() t16 = { return t16{a=40i64, b=20i64}; };
fn mk24() t24 = { return t24{a=100i64, b=20i64, c=3i64}; };
fn mk12() t12 = { return t12{a=10i32, b=20i32, c=30i32}; };
fn one() i64 = { return 1i64; };
// full=2, [N]S value-array local, const index, foff=0
@test fn struct16_local_const() void = {
let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}];
a[1].f = mk16();
assert(a[1].f.a == 40i64);
assert(a[1].f.b == 20i64);
assert(a[1].g == 9i64);
assert(a[0].f.a == 9i64);
};
// full=3 (CX word), [N]S value-array local, runtime index, foff=0
@test fn struct24_local_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}];
a[one()].f = mk24();
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, const index
@test fn fnotfirst_local() void = {
let a: [2]sfnf = [sfnf{g=9i64,f=t16{a=9i64,b=9i64}}, sfnf{g=9i64,f=t16{a=9i64,b=9i64}}];
a[1].f = mk16();
assert(a[1].f.a == 40i64);
assert(a[1].f.b == 20i64);
assert(a[1].g == 9i64);
};
// sub-8-tail (12B, MOVL tail), *[N]S -> global, const index
@test fn subtail_ptr_const() void = {
let p: *[2]s12 = &g12p;
p[1].f = mk12();
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), []S -> global, runtime index
@test fn subtail_slice_runtime() void = {
let sl: []s12 = g12s[0:2];
sl[one()].f = mk12();
assert(g12s[1].f.a == 10i32);
assert(g12s[1].f.b == 20i32);
assert(g12s[1].f.c == 30i32);
assert(g12s[1].g == 9i32);
};

View File

@@ -0,0 +1,14 @@
//ww:error "float-bearing aggregate field receive"
// #11/#165 tripwire: an in-cap aggregate-returning CALL into a FLOAT-bearing
// aggregate field of an indexed element. The return routes a float eightbyte to
// X0/X1, which the GP AX/DX/CX materialise cursor cannot read -> both stages
// LOUD-STOP (#171). SUCCESS = the float guard regressed to silent GP-garbage.
package main;
type ft = struct { x: f64, y: i64 };
type sf = struct { f: ft, g: i64 };
fn mk() ft = { return ft{x=1.5f64, y=7i64}; };
export fn main() i32 = {
let arr: [2]sf;
arr[1].f = mk();
return 0;
};

View File

@@ -0,0 +1,15 @@
//ww:error "over-cap (sret) aggregate field receive"
// #11c/#234 tripwire: an OVER-cap (>24B, sret-returning) CALL into an aggregate
// field of an indexed element. The result is written via a dest pointer, not the
// AX/DX/CX cursor, so the in-cap materialise cannot handle it -> both stages
// LOUD-STOP until the const-idx sret-into-field path is wired (task #8, rule 7).
// SUCCESS = the over-cap stop regressed to a 1-word silent drop (cs!=ww).
package main;
type big = struct { a:i64, b:i64, c:i64, d:i64 };
type s = struct { f: big, g: i64 };
fn mk() big = { return big{a=1i64,b=2i64,c=3i64,d=4i64}; };
export fn main() i32 = {
let arr: [2]s;
arr[1].f = mk();
return 0;
};

View File

@@ -0,0 +1,16 @@
//ww:error "3/5/6/7-byte sub-8 tail unwired"
// #11 tripwire: an in-cap aggregate-returning CALL into an aggregate field of
// an indexed element where the field has a 3/5/6/7-byte sub-8 tail (here 14B
// 7xi16, tail=6). The materialise's single narrow MOV stores only one tail byte
// while the copy reads the full tail -> a SILENT both-stage member drop. Both
// stages LOUD-STOP until a general register->scratch tail lands (rule 7,
// C2c-shared). SUCCESS = the loud-stop regressed to a silent miscompile.
package main;
type t14 = struct { a:i16,b:i16,c:i16,d:i16,e:i16,f:i16,g:i16 };
type s14 = struct { x: t14, pad: i16 };
fn mk() t14 = { return t14{a=1i16,b=2i16,c=3i16,d=4i16,e=5i16,f=6i16,g=7i16}; };
export fn main() i32 = {
let arr: [2]s14;
arr[1].x = mk();
return 0;
};