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

@@ -844,8 +844,18 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool,
// `cgexpr(c, arg)` + scalar PUSHQ AX — only the first
// 8B word made it across, and the callee's second-arg
// slots picked up the wrong neighbour's value.
let stsz: i32 = structparamsize(c, lc.tnode);
if (stsz > 0) {
// #21/#224: COUNT the push off the checker-STAMPED tinfo
// (structabisizetn), not the name-keyed structparamsize(lc.tnode).
// On a cross-module same-leaf collision the inferred-let's tnode
// is a bare leaf that structlookup mis-resolves (returns 0 or a
// foreign struct >16B → 0), so the fast path was skipped and the
// arg dropped to the scalar single-PUSHQ default — word1 lost.
// cstage counts via struct_arg_size(args[i]->type) (TYPE-keyed),
// never colliding; align ww UP. The <=16B gate keeps >16B structs
// + arrays on the #271 aggregate arm below (the fast path emits
// at most 2 words).
let stsz: i32 = structabisizetn(arg.type_: *syntax.tinfo);
if (stsz > 0 && stsz <= 16) {
if (stsz > 8) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
@@ -951,14 +961,15 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool,
// aggregate) and stack-imbalanced against the type-based drain.
let aggsz: i32 = aggargsizetn(arg.type_: *syntax.tinfo);
if (aggsz > 0) {
// Exclude a ≤16B-struct IDENT — it owns the structparamsize
// fast path above (or, when a cross-module same-leaf collision
// makes the name-keyed structparamsize miss it, the scalar
// default below, byte-id with cstage's 1-word struct push;
// #784/#223). The exclusion is TYPE-keyed via the stamped
// tinfo, mirroring cstage node_isstructarg (struct_arg_size on
// args[i]->type) — a name-keyed gate here re-opens the #211/#13
// name-keyed divergence the cstage type gate doesn't have.
// Exclude a ≤16B-struct IDENT — it owns the structabisizetn
// fast path above. That path is now TYPE-keyed (#21/#224): a
// cross-module same-leaf collision no longer misses (the prior
// name-keyed structparamsize returned 0 → the arg dropped to the
// scalar default → word1 lost; #784/#223). The exclusion is
// TYPE-keyed via the stamped tinfo, mirroring cstage
// node_isstructarg (struct_arg_size on args[i]->type) — a
// name-keyed gate here re-opens the #211/#13 name-keyed
// divergence the cstage type gate doesn't have.
let structident: bool = false;
if (arg.kind == syntax.nkind.N_IDENT) {
let st: *syntax.tinfo = arg.type_: *syntax.tinfo;
@@ -2046,6 +2057,28 @@ fn structabisize(si: *structinfo) i32 = {
return (n + maxaln - 1) & ~(maxaln - 1);
};
// structabisizetn — the maxalign-rounded ABI size of a by-value STRUCT,
// read off the checker-STAMPED tinfo (.size), else 0. The tinfo twin of
// structabisize(*structinfo): check.ww:2467 computes the struct's
// `r.size = (off+maxalign-1)&~(maxalign-1)` — identical to structabisize's
// formula — so `tichase(t).size` IS the ABI size, byte-id with the
// name-keyed structabisize on a resolving lookup. The #21/#224 choke-point
// for the caller-side register-ABI sites (struct-arg push, inferred-let
// struct call-result recv) that previously keyed the COUNT through a
// name-keyed structlookup/structparamsize: a cross-module same-leaf
// collision makes that lookup return 0 (or a foreign struct's size), so the
// push under-counted and the recv over-copied. The stamped tinfo carries
// the right size regardless of leaf collision (the documented #211/#13/#784
// name-keyed cluster; align wwstage UP to cstage's type-keyed struct_arg_size
// / lu->size). STRUCT-only — arrays own their own #271/#267 arms.
fn structabisizetn(t: *syntax.tinfo) i32 = {
if (t == nil) { return 0; };
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return 0; };
if (u.kind == syntax.tykind.TY_STRUCT) { return u.size: i32; };
return 0;
};
// sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B,
// return its natural size; else 0. Tagged unions, tuples, str,
// slices, scalars route through their existing register-return ABIs