cgen: convert the struct-store loop arms to a tinfo-native fill (#31 c2)

The W1/W2/W5/W4b store / structlit-fill arms still resolved struct field
LAYOUT by bare-leaf name (structlookupchain / structlookup / letvarstructinfo)
inside the per-field store loop -- the write-side twin of commit-1's read fix.
Under a cross-module same-leaf collision (two modules each exporting a `pair`,
16B vs 24B) the field is stored at the wrong offset/width; an interior 2-byte
field store silently corrupted neighbours. cstage walks the resolved Type*
(never a name) and is correct; this aligns wwstage UP (ww-only change).

Convert each store loop to a tinfo-native fill. Two new helpers in cgenutil.ww:
sretretsizetn (a verbatim port of cstage cg_sret_retsize -- 4 kind-branches,
no TY_BANG, dropping the #129 module-juggling that was pure name-keying
compensation, redundant once the lookup walks resolved tinfo) and
cgstructlitfilltn (the tinfo-native twin of cgstructlitfill, recursing on
tichase(tf.type_).fields with NO name lookup at any nesting level). Supporting
twins storeopsz / tfieldlookup / cgdotfieldhardstoptn. Because the fill never
bottoms out on a name, the W4b "decisive wall" (a structlit that needed a
(sname,smod)-keyed structinfo the stamped leaf-only tinfo couldn't supply)
dissolves by construction, with no module-aware tinfo->structinfo bridge.

This CLOSES the nine in-loop nested sub-arms (N_CALL / N_STRUCTLIT / N_IDENT
field receive) by construction -- the tf walk has no name lookup. The
non-reddenable global arms (W5/W4b) are converted-for-construction: a qualified
global decl resolves its bare leaf correctly today, so byte-id (cs.s==ww.s) is
their net, not a value pin. `!T` is kind-preserving in both stages
(resolve_type / tinfofornode), so a `!struct` sret sizes as the inner struct,
never TAGGED -- the #129 drop changes no size.

Completes the 2-commit #31 arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): the
#224 name-keyed receiver-layout cluster is now closed by construction. The
residual nested-field surface (the shared cgstructlitfill kept for its non-#31
callers + the cgenstmt/cgenutil let-receive/fill family) is tracked as #32.

Pins: test/wcc/797 value-asserts ptr-WRITE (W1) and val-WRITE (W2), each
reddening under independent per-arm revert; nestfill_box drives
cgstructlitfilltn's nested recursion (bootstrap-uncovered -- the corpus has no
struct-literal field store -- so the value pin is its only net).
This commit is contained in:
2026-06-29 15:19:41 +09:00
parent eb28dcd5b7
commit 5bfcd8bd6b
3 changed files with 813 additions and 185 deletions

View File

@@ -1695,6 +1695,19 @@ fn loadopsz(sigd: bool, sz: i32) str = {
return "MOVQ";
};
// storeopsz — store op when the size has already been resolved
// upstream off a stamped tinfo (the #31 receiver-layout store choke-
// point). Size-only twin of fieldstoreop's body {1->MOVB, 2->MOVW,
// 4->MOVL, else MOVQ}; the store side carries no signedness. Preserves
// the #15 sub-8 guard (a 2B tail field stores MOVW, not a slot-wide
// MOVQ into saved BP).
fn storeopsz(sz: i32) str = {
if (sz == 1) { return "MOVB"; };
if (sz == 2) { return "MOVW"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// localloadop — read instruction for a scalar local/let load. Same
// dispatch as fieldloadop, but keyed on the value's own tnode.type_.
// Lets the caller emit MOVSXD/MOVSWQ/MOVSBQ on a signed-narrow slot
@@ -2214,6 +2227,80 @@ export fn sretretsize(c: *cgen, t: *syntax.node) i32 = {
return n;
};
// tfieldlookup — the named field `fld` in struct tinfo `sti`'s stamped
// field chain, else nil. The #31 receiver-layout store choke-point: the
// tfield twin of the name-keyed `for (fi != nil)` fieldinfo walk. A
// cross-module same-leaf collision that mis-resolved structlookupchain
// cannot reach a stamped tinfo, so every field-OFFSET/width/kind keyed
// off the returned tf is collision-immune.
fn tfieldlookup(sti: *syntax.tinfo, fld: str) *syntax.tfield = {
if (sti == nil) { return nil; };
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) { return tf; };
tf = tf.tnext;
};
return nil;
};
// sretretsizetn — verbatim port of cstage cg_sret_retsize (cmd/w6c/
// cgen.c:380) keyed on the checker-STAMPED *tinfo. The tinfo twin of
// sretretsize, the #31 store-loop choke-point (the sret hard-stop /
// sret-receive field sub-arms). Walks resolved tinfo, so the #129
// aliassamemod/module-juggling sretretsize carries — pure name-keying
// compensation for structlookup's any-module fallback — is NOT needed
// and absent: tichase(t) peels NAMED then falls through all four kind
// tests to 0, exactly what #129 reconstructed by name (cstage sees a
// resolved Type*, never a name). There is NO TY_BANG tinfo kind — `!T`
// folds into TY_TAGGED's per-variant iserror (typ.ww:82-90), so there
// is no error-peel branch; mirror cstage's four branches exactly. `c`
// kept for call-site symmetry with sretretsize (unused: tinfo carries
// the module-independent layout).
fn sretretsizetn(c: *cgen, t: *syntax.tinfo) i32 = {
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return 0; };
if (u.kind == syntax.tykind.TY_STRUCT) {
if (u.size: i32 <= 24) { return 0; };
return u.size: i32;
};
// #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
// eightbytes; nullable folds to one ptr word. Mirrors cstage
// cg_sret_retsize TY_TAGGED arm.
if (u.kind == syntax.tykind.TY_TAGGED) {
if (u.nullable != 0) { return 0; };
if (u.size: i32 <= TUPLE_GPCAP * 8) { return 0; };
return u.size: i32;
};
// #267: arrays ride the struct-return ABI (≤24 reg / >24 sret).
if (u.kind == syntax.tykind.TY_ARRAY) {
if (u.size: i32 <= 24) { return 0; };
return u.size: i32;
};
// #10: over-cap tuple → sret. Walk the positional element chain
// over the SAME caps the SEND/receive use (a float = 1 SSE
// eightbyte; str/slice/tagged ride their box-rounded GP stride via
// tupeslot; a scalar 1 GP word). Mirrors cstage cg_sret_retsize
// TY_TUPLE arm; tupleelems is the tinfo positional chain.
if (u.kind == syntax.tykind.TY_TUPLE) {
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let te: *syntax.ttupleelem = u.tupleelems;
for (te != nil) {
if (syntax.typeisfloat(te.type_)) {
ssecount = ssecount + 1;
} else {
gptotal = gptotal + tupeslot(te.type_) / 8;
};
te = te.tnext;
};
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) {
return u.size: i32;
};
return 0;
};
return 0;
};
// callsretsize — if N_CALL `n`'s callee returns a plain TY_STRUCT
// > 24B, return its natural size; else 0. Wraps sretretsize over the
// callee's resolved return type, used by cglet / cgassign receive
@@ -5738,3 +5825,419 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *syntax.node, bpoff: i32) v
if (si == nil) { return; };
cgstructlitfill(c, si, lit, 0, 0, "", bpoff);
};
// cgstructlitfilltn — tinfo-native twin of cgstructlitfill, the #31
// receiver-layout fill choke-point. Walks the struct's checker-STAMPED
// tfield chain (lu.fields) and keys every per-field decision off the
// resolved tf.type_ (kind / size / signedness) — NO structlookup, NO
// fi.tnode names. Nested struct fields recurse on tichase(tf.type_), so
// a cross-module same-leaf collision (the #224 cluster) cannot reach a
// name lookup at any nesting level. Faithful mirror of cstage
// cg_structlit_fill, which recurses on type_chase_named(fl->type)
// (cgen.c:3179) over resolved Type*. Emission (mode reloads, register
// staging, store ordering) is byte-identical to cgstructlitfill; only
// the LAYOUT source flips from si/fieldinfo to lu/tfield. tinfo.size
// equals structabisize/fi.fsz for a struct field (check.ww:2468 ABI
// formula; NAMED.size==under.size at :2234), so the size-keyed stores
// match. Modes: 0=DST_BP, 1=DST_PTR_LOCAL(srcoff), 2=DST_GLOBAL(srcname),
// 3=DST_PTR_SP. Routed by the W1/W2/W5 N_STRUCTLIT sub-arms + W4b.
fn cgstructlitfilltn(c: *cgen, lu: *syntax.tinfo, lit: *syntax.node,
mode: i32, srcoff: i32, srcname: str,
disp: i32) void = {
let s: *syntax.tinfo = tichase(lu);
if (s == nil) { return; };
if (s.kind != syntax.tykind.TY_STRUCT) { return; };
let basereg: str = "BP";
if (mode != 0) { basereg = "BX"; };
let totsize: i32 = s.size: i32;
if (lit.op == syntax.tkind.TK_ELLIPSIS) {
// `..., ...` autofill — zero the entire slot first so
// unmentioned fields read as 0. Sized stores: 8/4/1. For
// non-BP modes, reload BX once before the loop.
emitline("\tXORQ\tAX, AX\n");
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= totsize) {
emitline("\tMOVQ\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 8;
};
for (zi + 4 <= totsize) {
emitline("\tMOVL\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 4;
};
for (zi < totsize) {
emitline("\tMOVB\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 1;
};
};
let fieldnode: *syntax.node = lit.list;
for (fieldnode != nil) {
if (fieldnode.kind == syntax.nkind.N_FIELD) {
let fname: str = fieldnode.str;
let tf: *syntax.tfield = tfieldlookup(s, fname);
if (tf != nil) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
let fu: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (fu != nil) { fsz = fu.size: i32; };
// Tagged-union field: delegate to the shared
// widening writer (str/scalar/struct-lit/ident
// payload + tagged-subset tag remap). For non-BP
// modes, reload BX first.
if (syntax.typeistagged(ftraw)) {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
cgwidentaggedstore(c, ftraw,
fieldnode.lhs, basereg,
disp + foff, fsz);
} else {
// Nested struct-typed structlit value: recurse at
// the field's offset off the resolved field tinfo
// (NO structlookup). Pre-#17/#18 the cgexpr-then-
// store tail would land AX = first qword and the
// rest silently stayed zero.
let nested: bool = false;
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == syntax.nkind.N_STRUCTLIT) {
if (fu != nil) {
if (fu.kind == syntax.tykind.TY_STRUCT) {
cgstructlitfilltn(c, fu,
fieldnode.lhs,
mode, srcoff, srcname,
disp + foff);
nested = true;
};
};
};
};
// Nested struct-typed CALL value (#20): cgexpr
// leaves AX/DX/CX per #4's cgreturn ABI; the
// choke-point stores full 8B chunks + a sized tail.
// Guard fu.size <= 24 (#14: every in-cap tail incl.
// 3/5/6/7); >24B falls through (sret deferred).
// INVARIANT: between cgexpr(N_CALL) and the AX/DX/CX
// stores, only the BX reload may intervene.
let callwhole: bool = false;
if (!nested) {
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == syntax.nkind.N_CALL) {
if (fu != nil) {
if (fu.kind == syntax.tykind.TY_STRUCT) {
let cfsz: i32 = fu.size: i32;
if (cfsz <= 24) {
cgexpr(c, fieldnode.lhs);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
cgaggregstore(c, basereg, disp + foff, cfsz, false);
callwhole = true;
};
};
};
};
};
};
if (nested) {
// recursed above
} else if (callwhole) {
// stored above
} else if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
// str IS []u8: 3-word {ptr,len,cap}. cgexpr
// leaves AX/BX/CX; for non-BP modes the dst
// base goes in DX to dodge BX=len / CX=cap.
cgexpr(c, fieldnode.lhs);
if (mode == 0) {
emitline("\tMOVQ\tAX, ");
emitoff((disp + foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((disp + foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((disp + foff + 16): i64);
emitline("(BP)\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), DX\n");
} else { if (mode == 3) {
emitline("\tMOVQ\t(SP), DX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), DX\n");
}; };
emitline("\tMOVQ\tAX, ");
emitdispreg((disp + foff): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((disp + foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((disp + foff + 16): i64, "DX");
emitline("\n");
};
} else if (fu != nil && fu.kind == syntax.tykind.TY_ARRAY
&& fieldnode.lhs != nil
&& fieldnode.lhs.kind == syntax.nkind.N_ARRLIT) {
// #249: array field from an N_ARRLIT. element-
// wise store at disp+foff+i*esz off the array
// element tinfo (fu.sub). int/float elements
// only; str/slice/struct/tagged elements are the
// N_LET multi-word gap — loud rule-7 (cstage
// cg_structlit_fill array arm twin).
let eti: *syntax.tinfo = tichase(fu.sub);
let isstructel: bool = false;
if (eti != nil) {
if (eti.kind == syntax.tykind.TY_STRUCT) { isstructel = true; };
};
if (syntax.typeisstr(fu.sub) || syntax.typeisslice(fu.sub)
|| isstructel || syntax.typeistagged(fu.sub)) {
let e1: str = "ww: struct-literal array field '";
os.write(2, e1.ptr, e1.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let e2: str = "' has a str/slice/struct/tagged element — multi-word element store out of #249 scope (N_LET array-init gap)\n";
os.write(2, e2.ptr, e2.len: u64);
os.exit(1);
};
let esz: i32 = 8;
if (eti != nil) { esz = eti.size: i32; };
let mop: str = storeopsz(esz);
let isfloatel: bool = syntax.typeisfloat(fu.sub);
let fmov: str = "MOVSD";
if (syntax.typeisf32(fu.sub)) { fmov = "MOVSS"; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *syntax.node = fieldnode.lhs.list;
for (e != nil) {
let isellip: bool = false;
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
repeat = true;
isellip = true;
};
};
if (isellip) {
e = nil;
} else {
cgexpr(c, e);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let eoff: i32 = disp + foff + idx * esz;
if (isfloatel) {
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
} else {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
};
if (mode == 0) {
emitoff(eoff: i64);
emitline("(BP)\n");
} else {
emitdispreg(eoff: i64, basereg);
emitline("\n");
};
idx += 1;
e = e.next;
};
};
if (repeat) {
let total: i32 = fu.alen: i32;
for (idx < total) {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let eoff: i32 = disp + foff + idx * esz;
if (isfloatel) {
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
} else {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
};
if (mode == 0) {
emitoff(eoff: i64);
emitline("(BP)\n");
} else {
emitdispreg(eoff: i64, basereg);
emitline("\n");
};
idx += 1;
};
};
} else if (fu != nil
&& (fu.kind == syntax.tykind.TY_STRUCT
|| fu.kind == syntax.tykind.TY_ARRAY
|| fu.kind == syntax.tykind.TY_TUPLE)) {
// #49: aggregate (struct/array/tuple) field from
// an ADDRESSABLE source expr — `outer{.., r = r}`.
// Funnel: source addr via aggargsrcaddr (SI),
// field addr via LEAQ/ADDQ (BX, AFTER the source
// walk clobbers BX/AX), then aggcopy. Width = the
// resolved field tinfo size. Non-addressable
// sources die loud (rule 7). cstage #49 arm twin.
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 + foff): i64);
emitline("(BP), BX\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
} else { if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
}; };
if (disp + foff != 0) {
emitline("\tADDQ\t$");
emitint((disp + foff): i64);
emitline(", BX\n");
};
};
aggcopy(c, fsz);
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered BX;
// reload it before the store.
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
if (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
if (mode == 0) {
emitoff((disp + foff): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + foff): i64, basereg);
emitline("\n");
};
} else {
// Full field-width store {1->MOVB, 2->MOVW,
// 4->MOVL, else MOVQ}; the #15 sub-8 guard.
let op: str = storeopsz(fsz);
emitline("\t");
emitline(op);
emitline("\tAX, ");
if (mode == 0) {
emitoff((disp + foff): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + foff): i64, basereg);
emitline("\n");
};
};
};
};
};
};
fieldnode = fieldnode.next;
};
};