cgen: type-key the struct field-layout receiver arms via stamped tinfo (#31 c1)

wwstage cgen resolved struct field LAYOUT (offset + field type) by bare-leaf
name (structlookupchain / structlookup / letvarstructinfo) at nine field
READ / addr-of / let-copy receiver arms whose base is a *struct pointer, a
value struct, or a module global. Under a cross-module same-leaf collision
(two modules each exporting a `pair`, 16B vs 24B) the bare-leaf lookup
first-matches the wrong-size struct -> the field is read / addressed / copied
at the wrong offset with the wrong width. cstage type-keys off the stamped
node.type_ (type_chase_named(base->type)->fields) and is correct; this aligns
wwstage UP to it (ww-only change).

Route the nine arms off the stamped receiver tinfo: R1/R2 *struct reads via a
new shared choke-point cgptrfieldloadtf (the tinfo twin of cgptrfieldload);
A1/A2/A3 addr-of and W3 scalar global-store via tichase(recv.type_)->fields;
C1/W4a copy/size via structabisizetn(tichase(.type_)). Mirrors #21 (5ae6e34);
the *struct arms peel the pointee with tichase(.type_).sub, the faithful twin
of cstage type_chase_named(bu->sub).

The global value-struct arms (R2/R3/A3/W3/W4a) are converted-for-construction:
a global struct's type is always explicitly qualified, so the bare leaf already
resolved correctly and they cannot be reddened -- byte-id (cs.s==ww.s) is their
net, not a value pin. Labelled so the absent reddening pin is explained, not
silent.

Commit 1 of a 2-commit arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): closes the
field-LAYOUT read/copy/addr surface. Commit 2 converts the W1/W2/W5/W4b store
loops to a tinfo-native fill (cgstructlitfilltn + sretretsizetn) and closes the
in-loop nested sub-arms by construction. Part of the #224 name-keyed-cgen
cluster retirement.

Pin: test/wcc/797 value-asserts R1 ptr-read / C1 let-copy / A1/A2 addr, each
reddening under independent per-arm revert (a ratchet proves nothing for this
silent-capable class).
This commit is contained in:
2026-06-29 14:01:47 +09:00
parent 5ae6e3419e
commit eb28dcd5b7
4 changed files with 578 additions and 198 deletions

View File

@@ -2654,55 +2654,49 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = {
// via the str-init tail) — silent partial copy. Mirrors
// cstage cgen.c N_LET struct-ident branch (Task #32).
if (rhs.kind == syntax.nkind.N_IDENT) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TNAME) { sname = tn.str; };
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// memcpy run sizes on the maxalign-rounded ABI
// size — cstage N_LET sets sz = lu->size
// (cgen.c:7533, struct-IDENT branch :7869),
// check.c:760 SSoT. structnaturalsize would
// short struct{i64,i32} (natural 12, ABI 16)
// to MOVQ+MOVL where cstage writes MOVQ+MOVQ.
let lsz: i32 = structabisize(lsi);
if (lsz > 8) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
let soff: i32 = lc.off;
let ki: i32 = 0;
for (ki + 8 <= lsz) {
emitline("\tMOVQ\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
ki += 8;
};
if (ki < lsz) {
let tail: i32 = lsz - ki;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
// #31: size the local-to-local struct COPY off the COPY
// SOURCE's stamped tinfo (structabisizetn(rhs.type_)), NOT
// structlookup(tn.str). On a cross-module same-leaf collision
// the inferred-let's declared-type leaf (tn.str) mis-resolves
// to a FOREIGN same-leaf struct -> the memcpy run was sized off
// it (a 24B foreign over-reads a 16B source + over-writes the
// dst slot -- silent OOB, cs!=ww). rhs.type_ is the correct
// source struct; byte-id with structabisize on a resolving
// lookup. memcpy run = maxalign-rounded ABI size = cstage N_LET
// sz=lu->size (cgen.c:7533, struct-IDENT :7869, check.c:760 SSoT).
let lsz: i32 = structabisizetn(rhs.type_: *syntax.tinfo);
if (lsz > 8) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
let soff: i32 = lc.off;
let ki: i32 = 0;
for (ki + 8 <= lsz) {
emitline("\tMOVQ\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
ki += 8;
};
if (ki < lsz) {
let tail: i32 = lsz - ki;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};