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:
@@ -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
|
||||
|
||||
@@ -2583,29 +2583,38 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = {
|
||||
};
|
||||
};
|
||||
if (rhs.kind == syntax.nkind.N_CALL) {
|
||||
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) {
|
||||
// ≤24B register RECV: the value arrives packed
|
||||
// in AX/DX/CX, so size by the maxalign-rounded
|
||||
// ABI size (cstage lu->size), not the natural
|
||||
// extent — see structabisize (#169).
|
||||
let lsz: i32 = structabisize(lsi);
|
||||
let tlm: i32 = lsz - (lsz / 8) * 8;
|
||||
if (lsz <= 24) {
|
||||
if (tlm == 0 || tlm == 1
|
||||
|| tlm == 2 || tlm == 4) {
|
||||
cgexpr(c, rhs);
|
||||
cgaggregstore(c, "BP", off, lsz, true);
|
||||
c.lastwasreturn = 0;
|
||||
return;
|
||||
//
|
||||
// #21/#224: size off the checker-STAMPED tinfo
|
||||
// (structabisizetn = tichase(tn.type_).size, the
|
||||
// maxalign-rounded ABI size = check.ww:2467),
|
||||
// NOT structlookup(tn.str). On a cross-module
|
||||
// same-leaf collision the inferred-let's tn.str
|
||||
// is a bare leaf that structlookup mis-resolves to
|
||||
// a FOREIGN same-leaf struct → the recv copied that
|
||||
// struct's word count (a 24B foreign over-copies a
|
||||
// 16B local, spilling into a neighbour slot). The
|
||||
// stamped tinfo carries the right size regardless of
|
||||
// collision; byte-id with structabisize on a
|
||||
// resolving lookup. Mirrors the sibling array arm
|
||||
// below (already tn.type_-keyed). cstage is
|
||||
// type-keyed (lu->size) — align ww UP.
|
||||
let lsz: i32 = structabisizetn(tn.type_: *syntax.tinfo);
|
||||
if (lsz > 0) {
|
||||
let tlm: i32 = lsz - (lsz / 8) * 8;
|
||||
if (lsz <= 24) {
|
||||
if (tlm == 0 || tlm == 1
|
||||
|| tlm == 2 || tlm == 4) {
|
||||
cgexpr(c, rhs);
|
||||
cgaggregstore(c, "BP", off, lsz, true);
|
||||
c.lastwasreturn = 0;
|
||||
return;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user