cgen: #49 aggregate-ASSIGN word0-only family — one mem-to-mem funnel (cg_aggcopy), both stages

Whole-aggregate reassignment `b = a` fell to the N_ASSIGN scalar tail
and copied ONE MOVQ — word 0 of any struct/array/tuple — in BOTH
stages, byte-identical, gate-blind (ken f49_min asm proof; latent
because lib style is let-init, whose #265/#268 copy is full-width).
Same class at three more positions: struct-lit FIELD init from an
ident source (`outer{.., r = r}`, the #38 non-tagged half), the deref
place `*p = s` (#31-A), and the module-let global `g = a` / `g = pt{..}`.

Fix: extract the C1.25 assign-resolver word-copy tail verbatim into
cg_aggcopy/aggcopy — the ONE place-resolved (SI)->(BX) aggregate copy
— and wire it at the N_ASSIGN ident-aggregate arm (local + global),
the deref-place divert into the existing resolver aggregate arm, and
the structlit-fill aggregate-field arm, all fed by aggarg_srcaddr
(the closed #265/#268 dispatch). The new arms key on the FULL alias
chase (type_chase_named / chased stamped tinfo, the #22 precedent) in
BOTH stages — the region's single-peel `lu`/`fu` would miss
`type b = a; type a = struct` on cstage while the wwstage twin fired
(ken R1, gA3b: master cs ran the word0 corruption, exit 2; now 0).
Non-addressable aggregate rhs (tuple-lit, unhandled call shapes) dies
LOUD (rule 7) instead of silently truncating: #31-E `*p = (3,4)` and
#31-G's deref flavor `*p = mk()` are now loud both stages (the INDEX
flavor `a[i] = mk()` stays in the legacy INDEX arm — receive
machinery, not this funnel; still filed under #31). #31-B rides: the
cstage-only <=24B gate before cg_structlit_fill_bp is lifted (the
wwstage twin never gated — a >24B literal reassign was
cs-zero/ww-filled, rule-10 break). Global structlit reassign rides
the existing DST_GLOBAL fill machinery.

Unsplit (rule 11): the assign arm, fill arm and deref divert all
route through the one new funnel (cg_aggcopy + aggarg_srcaddr) in
both stages; splitting by site or by stage would ship a transient
cs!=ww (gate-red) or a funnel with no consumer.

941 t2_reject_chain_arg: the row's tuple-LITERAL field fill now louds
at the #49 fill arm before reaching the pinned ARG-site reject; the
fill switches to an ident source (newly working via the fill arm) so
the original arg-site pin still fires.

test/wcc/812_agg_assign_width.c: 17 runtime-readback rows (the only
oracle for a gate-blind class) + per-row asm byte-id; every row fails
at 7545bf7 (ken matrix f49_min/f49b/f49c/fA_16b/f38b + gA3b/gA6 +
impl-A probes; reviewer-A re-probed 5 rows + the gA2 12B shape at a
master git-archive scratch). Alias rows use FIELD-WISE init: the
struct-LIT spelling louds earlier at the pre-existing task-#7
aggregate-let bound on wwstage (the #5 alias-arc's hole, not this
funnel's). Reviewer-A amendment (test-only, K5 self-certify): add
the ken-gA2 odd-size row (12B {u32,u32,u32}, maxalign 4 — pins the
MOVL tail; master both stages exit 3) and gA4's neighbor guards on
the deref row, completing ken's validated matrix in the committed
suite.
This commit is contained in:
2026-06-05 05:51:02 +09:00
parent 7545bf7dcd
commit 4c46d3afde
8 changed files with 1425 additions and 194 deletions

View File

@@ -18578,6 +18578,20 @@ fn istaggedtype(c: *cgen, t: *node) bool = {
return typeistagged(t.type_: *tinfo);
};
// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo
// (the #49 funnel predicate; #209/#211 discipline — never tnode
// names). Mirror of cstage's chase-then-kind test at the fill /
// assign aggregate arms.
fn tnodeisagg(t: *node) bool = {
if (t == nil) { return false; };
let u: *tinfo = t.type_: *tinfo;
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (u == nil) { return false; };
if (u.kind == tykind.TY_STRUCT) { return true; };
if (u.kind == tykind.TY_ARRAY) { return true; };
return u.kind == tykind.TY_TUPLE;
};
// isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage
// cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across
// cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-
@@ -20506,6 +20520,59 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
};
};
fi = nil;
} else if (tnodeisagg(fi.tnode)) {
// #49 (f38b/x5f-h): an aggregate (struct/array/
// tuple) field from an ADDRESSABLE source expr —
// `outer{.., r = r}` — fell to the scalar tail
// below and stored word0 only. Funnel: source
// address via aggargsrcaddr (SI), field address
// via LEAQ/ADDQ (BX — loaded AFTER the source
// walk, which clobbers BX/AX), then aggcopy.
// Width = the checker-STAMPED tinfo size (the
// cstage fl->type->size SSoT; fi.fsz is the
// slot-padded extent and skews on maxalign<8).
// Non-addressable aggregate sources (tuple-lit,
// >24B/odd-tail call) die loud — pre-#49 the
// same silent word0 (rule 7). Mirror of cstage
// cg_structlit_fill #49 arm.
if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) {
let m49g: str = "structlit fill: aggregate field '";
os.write(2, m49g.ptr, m49g.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n";
os.write(2, m49h.ptr, m49h.len: u64);
os.exit(1);
};
if (mode == 0) {
emitline("\tLEAQ\t");
emitoff((disp + fi.foff): i64);
emitline("(BP), BX\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (disp + fi.foff != 0) {
emitline("\tADDQ\t$");
emitint((disp + fi.foff): i64);
emitline(", BX\n");
};
};
let agsz49: i32 = 0;
if (fi.tnode != nil) {
let agti49: *tinfo = fi.tnode.type_: *tinfo;
for (agti49 != nil && agti49.kind == tykind.TY_NAMED) {
agti49 = agti49.under;
};
if (agti49 != nil) { agsz49 = agti49.size: i32; };
};
aggcopy(c, agsz49);
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered
@@ -21952,6 +22019,53 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
return false;
};
// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz
// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail.
// Extracted verbatim from the C1.25 assign-resolver tail so every
// aggregate copy position (resolver field store, #49 ident reassign,
// #49 structlit fill-field) funnels through one loop — close-by-
// construction, no per-site width logic to skew. Mirror of cstage
// cg_aggcopy.
fn aggcopy(c: *cgen, sz: i32) void = {
let k: i32 = 0;
for (k + 8 <= sz) {
emitline("\tMOVQ\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 8;
};
if (k + 4 <= sz) {
emitline("\tMOVL\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 4;
};
if (k + 2 <= sz) {
emitline("\tMOVW\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 2;
};
if (k + 1 <= sz) {
emitline("\tMOVB\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 1;
};
};
// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue)
// expression into dstreg; returns true when the shape is wired, false
// otherwise (the caller loud-stops — rule 7, never a silent drop).
@@ -27588,6 +27702,29 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// #49 (#31-A fold): an aggregate pointee diverts the whole
// deref-assign to the resolver aggregate arm below — the scalar
// tail there stored ONE word of `*p = s` (#31-A); tuple-lit
// (#31-E) and call (#31-G) rhs now die loud there instead of
// silently truncating. str/slice pointees keep their 3-word arm
// (byte-id-pinned). Mirror of cstage deref_agg.
let derefagg: bool = false;
if (lhs != nil) {
if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR
&& n.op == tkind.TK_ASSIGN) {
let du: *tinfo = lhs.type_: *tinfo;
for (du != nil && du.kind == tykind.TY_NAMED) {
du = du.under;
};
if (du != nil) {
if (du.kind == tykind.TY_STRUCT
|| du.kind == tykind.TY_ARRAY
|| du.kind == tykind.TY_TUPLE) {
derefagg = true;
};
};
};
};
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an nkind.N_IDENT with empty str
// (planted by parseprimary on the tkind.TK_UNDER token).
@@ -27707,7 +27844,8 @@ fn cgassign(c: *cgen, n: *node) void = {
if (lhs != nil) {
if (lhs.kind == nkind.N_UN) {
if (lhs.op == tkind.TK_STAR) {
if (n.op == tkind.TK_ASSIGN && !placeslit) {
if (n.op == tkind.TK_ASSIGN && !placeslit
&& !derefagg) {
let inner: *node = lhs.lhs;
let elemstr: bool = false;
let elemfloat: bool = false;
@@ -30439,6 +30577,56 @@ fn cgassign(c: *cgen, n: *node) void = {
return;
};
};
// #49 (global twin): aggregate module-let reassign —
// `g = a` / `g = pt{...}`. Same funnel as the local
// arm: structlit → mode-2 (DST_GLOBAL) fill; call →
// loud (#276: ≤24B struct global receive was a
// documented symmetric fall-through, now loud);
// addressable rhs → aggargsrcaddr + LEAQ g(SB), BX
// + aggcopy. Pre-#49 every shape fell to the scalar
// tail below: one MOVQ AX, g(SB).
if (n.op == tkind.TK_ASSIGN && lvftn != nil) {
let gau: *tinfo = lvftn.type_: *tinfo;
for (gau != nil && gau.kind == tykind.TY_NAMED) {
gau = gau.under;
};
if (gau != nil) {
if (gau.kind == tykind.TY_STRUCT
|| gau.kind == tykind.TY_ARRAY
|| gau.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
let gsi49: *structinfo = nil;
if (lvftn.kind == nkind.N_TNAME) {
gsi49 = structlookup(c, lvftn.str);
};
if (gsi49 == nil) {
// wwstage-only bail (anonymous
// type; the @placescr precedent).
let m49d: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49d.ptr, m49d.len: u64);
os.exit(1);
};
cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0);
return;
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49e.ptr, m49e.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
aggcopy(c, gau.size: i32);
return;
};
let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49f.ptr, m49f.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the
@@ -30817,6 +31005,60 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("(BP)\n");
return;
};
// #49: aggregate (struct/array/tuple) IDENT
// reassignment — any rhs shape that missed the
// dedicated arms above (struct-lit fill, call
// receive, sret) funnels through the ONE mem-to-mem
// copy (aggargsrcaddr → SI, dst → BX, aggcopy), or
// dies loud. Pre-#49 it fell to the scalar tail
// below and word0-copied `b = a` (cstage cgen.c
// N_ASSIGN aggregate-ident twin). Kind keys off the
// checker-STAMPED tinfo (#209/#211 discipline).
if (n.op == tkind.TK_ASSIGN) {
let agu: *tinfo = nil;
if (lcn != nil) {
if (lcn.tnode != nil) {
agu = lcn.tnode.type_: *tinfo;
};
};
for (agu != nil && agu.kind == tykind.TY_NAMED) {
agu = agu.under;
};
if (agu != nil) {
if (agu.kind == tykind.TY_STRUCT
|| agu.kind == tykind.TY_ARRAY
|| agu.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
// wwstage-only bail: the fill is
// structinfo-keyed; a literal
// reaching past the name-keyed arm
// above has no registry entry
// (anonymous struct type). Loud,
// rule 7 (the resolver @placescr
// precedent); cstage fills from
// Type directly.
let m49a: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49a.ptr, m49a.len: u64);
os.exit(1);
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49b.ptr, m49b.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), BX\n");
aggcopy(c, agu.size: i32);
return;
};
let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49c.ptr, m49c.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
emitline("\tMOVQ\tAX, ");
@@ -31067,43 +31309,7 @@ fn cgassign(c: *cgen, n: *node) void = {
os.write(2, mau.ptr, mau.len: u64);
os.exit(1);
};
let kc: i32 = 0;
for (kc + 8 <= fsz) {
emitline("\tMOVQ\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 8;
};
if (kc + 4 <= fsz) {
emitline("\tMOVL\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 4;
};
if (kc + 2 <= fsz) {
emitline("\tMOVW\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 2;
};
if (kc + 1 <= fsz) {
emitline("\tMOVB\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 1;
};
aggcopy(c, fsz);
return;
};
};

View File

@@ -1374,6 +1374,53 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
return false;
};
// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz
// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail.
// Extracted verbatim from the C1.25 assign-resolver tail so every
// aggregate copy position (resolver field store, #49 ident reassign,
// #49 structlit fill-field) funnels through one loop — close-by-
// construction, no per-site width logic to skew. Mirror of cstage
// cg_aggcopy.
fn aggcopy(c: *cgen, sz: i32) void = {
let k: i32 = 0;
for (k + 8 <= sz) {
emitline("\tMOVQ\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 8;
};
if (k + 4 <= sz) {
emitline("\tMOVL\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 4;
};
if (k + 2 <= sz) {
emitline("\tMOVW\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 2;
};
if (k + 1 <= sz) {
emitline("\tMOVB\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 1;
};
};
// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue)
// expression into dstreg; returns true when the shape is wired, false
// otherwise (the caller loud-stops — rule 7, never a silent drop).
@@ -7010,6 +7057,29 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// #49 (#31-A fold): an aggregate pointee diverts the whole
// deref-assign to the resolver aggregate arm below — the scalar
// tail there stored ONE word of `*p = s` (#31-A); tuple-lit
// (#31-E) and call (#31-G) rhs now die loud there instead of
// silently truncating. str/slice pointees keep their 3-word arm
// (byte-id-pinned). Mirror of cstage deref_agg.
let derefagg: bool = false;
if (lhs != nil) {
if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR
&& n.op == tkind.TK_ASSIGN) {
let du: *tinfo = lhs.type_: *tinfo;
for (du != nil && du.kind == tykind.TY_NAMED) {
du = du.under;
};
if (du != nil) {
if (du.kind == tykind.TY_STRUCT
|| du.kind == tykind.TY_ARRAY
|| du.kind == tykind.TY_TUPLE) {
derefagg = true;
};
};
};
};
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an nkind.N_IDENT with empty str
// (planted by parseprimary on the tkind.TK_UNDER token).
@@ -7129,7 +7199,8 @@ fn cgassign(c: *cgen, n: *node) void = {
if (lhs != nil) {
if (lhs.kind == nkind.N_UN) {
if (lhs.op == tkind.TK_STAR) {
if (n.op == tkind.TK_ASSIGN && !placeslit) {
if (n.op == tkind.TK_ASSIGN && !placeslit
&& !derefagg) {
let inner: *node = lhs.lhs;
let elemstr: bool = false;
let elemfloat: bool = false;
@@ -9861,6 +9932,56 @@ fn cgassign(c: *cgen, n: *node) void = {
return;
};
};
// #49 (global twin): aggregate module-let reassign —
// `g = a` / `g = pt{...}`. Same funnel as the local
// arm: structlit → mode-2 (DST_GLOBAL) fill; call →
// loud (#276: ≤24B struct global receive was a
// documented symmetric fall-through, now loud);
// addressable rhs → aggargsrcaddr + LEAQ g(SB), BX
// + aggcopy. Pre-#49 every shape fell to the scalar
// tail below: one MOVQ AX, g(SB).
if (n.op == tkind.TK_ASSIGN && lvftn != nil) {
let gau: *tinfo = lvftn.type_: *tinfo;
for (gau != nil && gau.kind == tykind.TY_NAMED) {
gau = gau.under;
};
if (gau != nil) {
if (gau.kind == tykind.TY_STRUCT
|| gau.kind == tykind.TY_ARRAY
|| gau.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
let gsi49: *structinfo = nil;
if (lvftn.kind == nkind.N_TNAME) {
gsi49 = structlookup(c, lvftn.str);
};
if (gsi49 == nil) {
// wwstage-only bail (anonymous
// type; the @placescr precedent).
let m49d: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49d.ptr, m49d.len: u64);
os.exit(1);
};
cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0);
return;
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49e.ptr, m49e.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
aggcopy(c, gau.size: i32);
return;
};
let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49f.ptr, m49f.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the
@@ -10239,6 +10360,60 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("(BP)\n");
return;
};
// #49: aggregate (struct/array/tuple) IDENT
// reassignment — any rhs shape that missed the
// dedicated arms above (struct-lit fill, call
// receive, sret) funnels through the ONE mem-to-mem
// copy (aggargsrcaddr → SI, dst → BX, aggcopy), or
// dies loud. Pre-#49 it fell to the scalar tail
// below and word0-copied `b = a` (cstage cgen.c
// N_ASSIGN aggregate-ident twin). Kind keys off the
// checker-STAMPED tinfo (#209/#211 discipline).
if (n.op == tkind.TK_ASSIGN) {
let agu: *tinfo = nil;
if (lcn != nil) {
if (lcn.tnode != nil) {
agu = lcn.tnode.type_: *tinfo;
};
};
for (agu != nil && agu.kind == tykind.TY_NAMED) {
agu = agu.under;
};
if (agu != nil) {
if (agu.kind == tykind.TY_STRUCT
|| agu.kind == tykind.TY_ARRAY
|| agu.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
// wwstage-only bail: the fill is
// structinfo-keyed; a literal
// reaching past the name-keyed arm
// above has no registry entry
// (anonymous struct type). Loud,
// rule 7 (the resolver @placescr
// precedent); cstage fills from
// Type directly.
let m49a: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49a.ptr, m49a.len: u64);
os.exit(1);
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49b.ptr, m49b.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), BX\n");
aggcopy(c, agu.size: i32);
return;
};
let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49c.ptr, m49c.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
emitline("\tMOVQ\tAX, ");
@@ -10489,43 +10664,7 @@ fn cgassign(c: *cgen, n: *node) void = {
os.write(2, mau.ptr, mau.len: u64);
os.exit(1);
};
let kc: i32 = 0;
for (kc + 8 <= fsz) {
emitline("\tMOVQ\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 8;
};
if (kc + 4 <= fsz) {
emitline("\tMOVL\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 4;
};
if (kc + 2 <= fsz) {
emitline("\tMOVW\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 2;
};
if (kc + 1 <= fsz) {
emitline("\tMOVB\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 1;
};
aggcopy(c, fsz);
return;
};
};

View File

@@ -2568,6 +2568,20 @@ fn istaggedtype(c: *cgen, t: *node) bool = {
return typeistagged(t.type_: *tinfo);
};
// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo
// (the #49 funnel predicate; #209/#211 discipline — never tnode
// names). Mirror of cstage's chase-then-kind test at the fill /
// assign aggregate arms.
fn tnodeisagg(t: *node) bool = {
if (t == nil) { return false; };
let u: *tinfo = t.type_: *tinfo;
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (u == nil) { return false; };
if (u.kind == tykind.TY_STRUCT) { return true; };
if (u.kind == tykind.TY_ARRAY) { return true; };
return u.kind == tykind.TY_TUPLE;
};
// isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage
// cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across
// cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-
@@ -4496,6 +4510,59 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
};
};
fi = nil;
} else if (tnodeisagg(fi.tnode)) {
// #49 (f38b/x5f-h): an aggregate (struct/array/
// tuple) field from an ADDRESSABLE source expr —
// `outer{.., r = r}` — fell to the scalar tail
// below and stored word0 only. Funnel: source
// address via aggargsrcaddr (SI), field address
// via LEAQ/ADDQ (BX — loaded AFTER the source
// walk, which clobbers BX/AX), then aggcopy.
// Width = the checker-STAMPED tinfo size (the
// cstage fl->type->size SSoT; fi.fsz is the
// slot-padded extent and skews on maxalign<8).
// Non-addressable aggregate sources (tuple-lit,
// >24B/odd-tail call) die loud — pre-#49 the
// same silent word0 (rule 7). Mirror of cstage
// cg_structlit_fill #49 arm.
if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) {
let m49g: str = "structlit fill: aggregate field '";
os.write(2, m49g.ptr, m49g.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n";
os.write(2, m49h.ptr, m49h.len: u64);
os.exit(1);
};
if (mode == 0) {
emitline("\tLEAQ\t");
emitoff((disp + fi.foff): i64);
emitline("(BP), BX\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (disp + fi.foff != 0) {
emitline("\tADDQ\t$");
emitint((disp + fi.foff): i64);
emitline(", BX\n");
};
};
let agsz49: i32 = 0;
if (fi.tnode != nil) {
let agti49: *tinfo = fi.tnode.type_: *tinfo;
for (agti49 != nil && agti49.kind == tykind.TY_NAMED) {
agti49 = agti49.under;
};
if (agti49 != nil) { agsz49 = agti49.size: i32; };
};
aggcopy(c, agsz49);
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered

View File

@@ -18578,6 +18578,20 @@ fn istaggedtype(c: *cgen, t: *node) bool = {
return typeistagged(t.type_: *tinfo);
};
// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo
// (the #49 funnel predicate; #209/#211 discipline — never tnode
// names). Mirror of cstage's chase-then-kind test at the fill /
// assign aggregate arms.
fn tnodeisagg(t: *node) bool = {
if (t == nil) { return false; };
let u: *tinfo = t.type_: *tinfo;
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (u == nil) { return false; };
if (u.kind == tykind.TY_STRUCT) { return true; };
if (u.kind == tykind.TY_ARRAY) { return true; };
return u.kind == tykind.TY_TUPLE;
};
// isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage
// cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across
// cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-
@@ -20506,6 +20520,59 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
};
};
fi = nil;
} else if (tnodeisagg(fi.tnode)) {
// #49 (f38b/x5f-h): an aggregate (struct/array/
// tuple) field from an ADDRESSABLE source expr —
// `outer{.., r = r}` — fell to the scalar tail
// below and stored word0 only. Funnel: source
// address via aggargsrcaddr (SI), field address
// via LEAQ/ADDQ (BX — loaded AFTER the source
// walk, which clobbers BX/AX), then aggcopy.
// Width = the checker-STAMPED tinfo size (the
// cstage fl->type->size SSoT; fi.fsz is the
// slot-padded extent and skews on maxalign<8).
// Non-addressable aggregate sources (tuple-lit,
// >24B/odd-tail call) die loud — pre-#49 the
// same silent word0 (rule 7). Mirror of cstage
// cg_structlit_fill #49 arm.
if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) {
let m49g: str = "structlit fill: aggregate field '";
os.write(2, m49g.ptr, m49g.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n";
os.write(2, m49h.ptr, m49h.len: u64);
os.exit(1);
};
if (mode == 0) {
emitline("\tLEAQ\t");
emitoff((disp + fi.foff): i64);
emitline("(BP), BX\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (disp + fi.foff != 0) {
emitline("\tADDQ\t$");
emitint((disp + fi.foff): i64);
emitline(", BX\n");
};
};
let agsz49: i32 = 0;
if (fi.tnode != nil) {
let agti49: *tinfo = fi.tnode.type_: *tinfo;
for (agti49 != nil && agti49.kind == tykind.TY_NAMED) {
agti49 = agti49.under;
};
if (agti49 != nil) { agsz49 = agti49.size: i32; };
};
aggcopy(c, agsz49);
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered
@@ -21952,6 +22019,53 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
return false;
};
// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz
// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail.
// Extracted verbatim from the C1.25 assign-resolver tail so every
// aggregate copy position (resolver field store, #49 ident reassign,
// #49 structlit fill-field) funnels through one loop — close-by-
// construction, no per-site width logic to skew. Mirror of cstage
// cg_aggcopy.
fn aggcopy(c: *cgen, sz: i32) void = {
let k: i32 = 0;
for (k + 8 <= sz) {
emitline("\tMOVQ\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 8;
};
if (k + 4 <= sz) {
emitline("\tMOVL\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 4;
};
if (k + 2 <= sz) {
emitline("\tMOVW\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 2;
};
if (k + 1 <= sz) {
emitline("\tMOVB\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 1;
};
};
// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue)
// expression into dstreg; returns true when the shape is wired, false
// otherwise (the caller loud-stops — rule 7, never a silent drop).
@@ -27588,6 +27702,29 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// #49 (#31-A fold): an aggregate pointee diverts the whole
// deref-assign to the resolver aggregate arm below — the scalar
// tail there stored ONE word of `*p = s` (#31-A); tuple-lit
// (#31-E) and call (#31-G) rhs now die loud there instead of
// silently truncating. str/slice pointees keep their 3-word arm
// (byte-id-pinned). Mirror of cstage deref_agg.
let derefagg: bool = false;
if (lhs != nil) {
if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR
&& n.op == tkind.TK_ASSIGN) {
let du: *tinfo = lhs.type_: *tinfo;
for (du != nil && du.kind == tykind.TY_NAMED) {
du = du.under;
};
if (du != nil) {
if (du.kind == tykind.TY_STRUCT
|| du.kind == tykind.TY_ARRAY
|| du.kind == tykind.TY_TUPLE) {
derefagg = true;
};
};
};
};
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an nkind.N_IDENT with empty str
// (planted by parseprimary on the tkind.TK_UNDER token).
@@ -27707,7 +27844,8 @@ fn cgassign(c: *cgen, n: *node) void = {
if (lhs != nil) {
if (lhs.kind == nkind.N_UN) {
if (lhs.op == tkind.TK_STAR) {
if (n.op == tkind.TK_ASSIGN && !placeslit) {
if (n.op == tkind.TK_ASSIGN && !placeslit
&& !derefagg) {
let inner: *node = lhs.lhs;
let elemstr: bool = false;
let elemfloat: bool = false;
@@ -30439,6 +30577,56 @@ fn cgassign(c: *cgen, n: *node) void = {
return;
};
};
// #49 (global twin): aggregate module-let reassign —
// `g = a` / `g = pt{...}`. Same funnel as the local
// arm: structlit → mode-2 (DST_GLOBAL) fill; call →
// loud (#276: ≤24B struct global receive was a
// documented symmetric fall-through, now loud);
// addressable rhs → aggargsrcaddr + LEAQ g(SB), BX
// + aggcopy. Pre-#49 every shape fell to the scalar
// tail below: one MOVQ AX, g(SB).
if (n.op == tkind.TK_ASSIGN && lvftn != nil) {
let gau: *tinfo = lvftn.type_: *tinfo;
for (gau != nil && gau.kind == tykind.TY_NAMED) {
gau = gau.under;
};
if (gau != nil) {
if (gau.kind == tykind.TY_STRUCT
|| gau.kind == tykind.TY_ARRAY
|| gau.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
let gsi49: *structinfo = nil;
if (lvftn.kind == nkind.N_TNAME) {
gsi49 = structlookup(c, lvftn.str);
};
if (gsi49 == nil) {
// wwstage-only bail (anonymous
// type; the @placescr precedent).
let m49d: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49d.ptr, m49d.len: u64);
os.exit(1);
};
cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0);
return;
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49e.ptr, m49e.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
aggcopy(c, gau.size: i32);
return;
};
let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49f.ptr, m49f.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the
@@ -30817,6 +31005,60 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("(BP)\n");
return;
};
// #49: aggregate (struct/array/tuple) IDENT
// reassignment — any rhs shape that missed the
// dedicated arms above (struct-lit fill, call
// receive, sret) funnels through the ONE mem-to-mem
// copy (aggargsrcaddr → SI, dst → BX, aggcopy), or
// dies loud. Pre-#49 it fell to the scalar tail
// below and word0-copied `b = a` (cstage cgen.c
// N_ASSIGN aggregate-ident twin). Kind keys off the
// checker-STAMPED tinfo (#209/#211 discipline).
if (n.op == tkind.TK_ASSIGN) {
let agu: *tinfo = nil;
if (lcn != nil) {
if (lcn.tnode != nil) {
agu = lcn.tnode.type_: *tinfo;
};
};
for (agu != nil && agu.kind == tykind.TY_NAMED) {
agu = agu.under;
};
if (agu != nil) {
if (agu.kind == tykind.TY_STRUCT
|| agu.kind == tykind.TY_ARRAY
|| agu.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
// wwstage-only bail: the fill is
// structinfo-keyed; a literal
// reaching past the name-keyed arm
// above has no registry entry
// (anonymous struct type). Loud,
// rule 7 (the resolver @placescr
// precedent); cstage fills from
// Type directly.
let m49a: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49a.ptr, m49a.len: u64);
os.exit(1);
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49b.ptr, m49b.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), BX\n");
aggcopy(c, agu.size: i32);
return;
};
let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49c.ptr, m49c.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
emitline("\tMOVQ\tAX, ");
@@ -31067,43 +31309,7 @@ fn cgassign(c: *cgen, n: *node) void = {
os.write(2, mau.ptr, mau.len: u64);
os.exit(1);
};
let kc: i32 = 0;
for (kc + 8 <= fsz) {
emitline("\tMOVQ\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 8;
};
if (kc + 4 <= fsz) {
emitline("\tMOVL\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 4;
};
if (kc + 2 <= fsz) {
emitline("\tMOVW\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 2;
};
if (kc + 1 <= fsz) {
emitline("\tMOVB\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 1;
};
aggcopy(c, fsz);
return;
};
};