cgen: type-key wwstage struct-layout at 3 sites via stamped tinfo (#21)

wwstage cgen resolved struct LAYOUT by bare-leaf name (structlookup /
structparamsize) at three caller-side sites — the by-value arg push
(cgenutil), the let-receive copy width (cgenstmt), and the field-read
offset (cgenexpr). Under a cross-module same-leaf collision (two modules
each exporting a `pair`, 16B vs 24B) the name lookup first-matches the
WRONG type, so the push dropped the 2nd eightbyte, the receive over-copied,
and the field read the wrong offset. cstage type-keys off the stamped
tinfo and is correct; this aligns wwstage UP to it (ww-only change).

Route all three sites through the stamped node.type_ via a new
structabisizetn(*tinfo) accessor (push + receive) and tichase(type_).fields
(field-read, structlookupchain removed). One commit (rule-11 carve-out):
the collision drives all three at once and no per-site fixture isolates, so
it cannot bisect-split. A scoped slice of the #209/#211 name-keyed-cgen
cluster retirement; the cgdot *struct-ptr/global and let-copy siblings stay
name-keyed and are filed (#31).

New table-driven test 793_xmod_struct_argpush_collide_run (4 scenarios:
push/recv/field over 16B and 12B tails) reddens under a revert of the three
cgen files. Full make test green (336 passed); make sizelint clean.
This commit is contained in:
2026-06-29 11:45:08 +09:00
parent 7b9488706b
commit 5ae6e3419e
5 changed files with 435 additions and 69 deletions

View File

@@ -3512,9 +3512,13 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
// same-module struct (a genuine struct-value receiver); a
// same-module alias keeps peeling; a foreign leaf (in
// neither registry for c.curmod) falls back to the prior
// any-module heuristic. The broader cross-module same-leaf
// STRUCT name-keying at the direct-struct arm below is
// filed separately as #224 (not FLIP-triggered).
// any-module heuristic. #21/#224: the direct-struct arm
// below no longer name-keys — it resolves field offsets off
// the stamped struct tinfo (dotlhs.type_), closing the
// cross-module same-leaf STRUCT mis-read there. This peel +
// same-module break still scopes the *struct (N_TPTR) and
// array arms, which remain structlookupchain-keyed (out of
// the #21 scoped slice).
for (tn != nil && tn.kind == syntax.nkind.N_TNAME) {
if (structsamemod(c, tn.str) != nil) { break; };
let nx: *syntax.node = aliassamemod(c, tn.str);
@@ -3567,69 +3571,77 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
};
// Direct struct local: field load at off+foff.
if (lkind == syntax.nkind.N_TNAME) {
// structlookupchain walks the alias chain on
// miss so a transitively-aliased struct (`type
// b = a; a = struct`) still resolves to the
// underlying fieldinfo (#22).
let si: *structinfo = structlookupchain(c, tn);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (syntax.streq(fn_, fld)) {
// tagged-union field: emit the AX=tag,
// DX=word0, CX=word1[, R8=word2] load
// sequence so the match / let-init /
// call-arg consumers see the same shape
// as a tagged-returning fn. Pre-#28 fell
// through to the scalar fieldloadop and
// only AX (tag) was loaded — payload
// words came from whatever the caller
// left in DX/CX/R8.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
// #21/#224: resolve the field OFFSET + field type off
// the checker-STAMPED struct tinfo
// (tichase(dotlhs.type_).fields), NOT the name-keyed
// structlookupchain(tn). On a cross-module same-leaf
// collision lc.tnode is a bare leaf that structlookup
// mis-resolves to a FOREIGN same-leaf struct → fields
// read at the WRONG offsets / wrong load-op (the #224
// direct-struct arm flagged at the peel-loop comment
// above). The stamped tinfo carries the right layout
// regardless of leaf collision; mirrors cstage's
// `t->fields` walk (cgen.c N_DOT, type-keyed) — align
// ww UP. tfield {name, type_, offset} is the tinfo twin
// of fieldinfo {fname, tnode, foff}; the dispatch keys
// off syntax.typeis* on the field tinfo, byte-id with
// the prior is*type(fi.tnode)=typeis*(fi.tnode.type_).
let sbu: *syntax.tinfo = nil;
if (dotlhs != nil) { sbu = tichase(dotlhs.type_: *syntax.tinfo); };
if (sbu != nil) { if (sbu.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sbu.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
// tagged-union field: AX=tag, DX=word0,
// CX=word1[, R8=word2]; slot = ti.size
// (slotsize's TAGGED arm, cgenutil.ww:2680).
if (syntax.typeistagged(ftraw)) {
let ftc: *syntax.tinfo = tichase(ftraw);
let tsz: i32 = 0;
if (ftc != nil) { tsz = ftc.size: i32; };
cgloadtaggedfield(c, "BP",
lc.off + fi.foff, tsz, true);
lc.off + foff, tsz, true);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap}
// as a slice field: load (ptr, len, cap)
// into (AX, BX, CX). Base is BP so no
// aliasing — order doesn't matter. str
// folds onto the slice arm (#1/Phase 3
// collapse; cite cstage cgen.c N_DOT S1).
if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
// str IS []u8 — 3-word {ptr,len,cap} into
// (AX,BX,CX). Base is BP so order is harmless.
if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff): i64);
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 8): i64);
emitoff((lc.off + foff + 8): i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 16): i64);
emitoff((lc.off + foff + 16): i64);
emitline("(BP), CX\n");
} else { if (isfloattype(c, fi.tnode)) {
} else { if (syntax.typeisfloat(ftraw)) {
// f64/f32 field: route through X0.
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((lc.off + fi.foff): i64);
emitoff((lc.off + foff): i64);
emitline("(BP), X0\n");
} else {
let op: str = fieldloadop(c, fi);
let ftc: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (ftc != nil) { fsz = ftc.size: i32; };
let op: str = loadopsz(syntax.typeissigned(ftraw), fsz);
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + fi.foff): i64);
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
}; };
return;
};
fi = fi.finext;
tf = tf.tnext;
};
};
}; };
};
// Array pseudo-fields: `.ptr` is the array's
// address (LEAQ); `.len` is the static element