w6c+wwstage: aggregate arg from any non-ident source via the closed addr machinery (#271) — close aggregate-arg family

Passing an aggregate BY VALUE as a call argument worked ONLY for a ≤16B
struct from an IDENT source; every non-ident source — CALL mk(), N_DOT
o.f, N_INDEX a[i], DEREF *p — and every array / >24B-struct (even as an
ident) fell to the scalar default: one PUSHQ for a multi-word aggregate,
stack-imbalancing against the type-based multi-word drain. cs!=ww, both
garbage (f(mk()) cs4/ww236, f(o.f) cs8/ww108, f(a[i]) cs4/ww28, f(*p)
cs4/ww140; arrays + 32B sret struct same).

The arg-pass twin of the #265/#268 let-init copy. A new aggregate-arg
push arm materialises the source into the arg convention: the source
ADDRESS in SI (ident LEAQ / deref operand / dotchainaddr #253 /
&base[i] spine #252-270) then its ceil(sz/8) words pushed high→low; a
CALL receives first — ≤24B in AX/DX/CX pushed straight, >24B sret'd
into a per-fn @aggargscr then pushed from there. The pop-forward drain
gained a matching array / >16B-struct arm and the callee prologue an
is_bigagg receive (ceil(sz/8) GP eightbytes), so caller and callee
agree on the multi-word layout. The ≤16B-struct-IDENT fast path is
untouched (byte-id preserved).

The new-arm exclusion is TYPE-keyed (the stamped tinfo, mirroring
cstage node_isstructarg over args[i]->type), not the name-keyed
structparamsize — a name-keyed gate re-opened the #211/#13 cross-module
same-leaf collision (784 symmetric: an 8B `sa.s` struct whose
name-resolution collides with `sb.s = *vtable` would miss the struct
fast path and wrongly enter the new arm, diverging from cstage's
1-word push). A float-bearing ≤16B struct from a non-ident source
loud-stops in both stages (the #165 SSE eightbyte transport the GP
push/drain can't model; out of scope). A const array/struct `def`
global as an aggregate arg is aligned DOWN to the leaner wwstage
(both loud-stop) per rule-10.

#110: cgen is compiler-imported by w6c + wwdump — main.combined.ww
regen'd for both.

949 rows: arg_{struct16,arr16,struct32}_{call,dot,idx,deref,ident},
full member readback (struct 16B reg-class + 32B sret-class + array
[4]u32, each non-ident source + ident control); byteid=1 throughout
(master both-broken-and-divergent → converge on the correct full
push, #263). All 111 dotbaseaddr + 3/3 784 pass; test-unit 241 green;
sizelint + smoke OK; the full w6c compiler source (214705 asm lines)
self-compiles cs==ww byte-id.
This commit is contained in:
2026-06-02 13:42:59 +09:00
parent 3c37b98164
commit 42dd70dc0c
7 changed files with 1204 additions and 38 deletions

View File

@@ -15976,6 +15976,93 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
};
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a
// non-ident source, OR any array, OR a struct > 16B. The arg twin
// of the #265/#268 let-init copy (mirror of cstage cgen.c #271 push
// arm): materialise the source ADDRESS in SI and push its ceil(sz/8)
// words high→low (the pop drains word0 into the first arg reg). A
// CALL source receives first — ≤24B in AX/DX/CX pushed straight,
// >24B sret'd into @aggargscr then pushed from there. Pre-fix every
// such source fell to the scalar default (one PUSHQ for a multi-word
// aggregate) and stack-imbalanced against the type-based drain.
let aggsz: i32 = aggargsizetn(arg.type_: *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.
let structident: bool = false;
if (arg.kind == nkind.N_IDENT) {
let st: *tinfo = arg.type_: *tinfo;
for (st != nil && st.kind == tykind.TY_NAMED) {
st = st.under;
};
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
if (st.size: i32 <= 16) { structident = true; };
}; };
};
if (!structident) {
if (aggargfloatstop(arg)) {
let msg: str = "#271/#165: float-bearing struct arg from a non-ident source needs SSE eightbyte transport (out of scope)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let nwords: i32 = (aggsz + 7) / 8;
if (arg.kind == nkind.N_CALL) {
if (callsretsize(c, arg) > 0) {
let scr: i32 = localadd(c, "@aggargscr",
aggsz, nil);
c.sretdestoff = scr;
cgexpr(c, arg);
c.sretdestoff = 0;
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((scr + k*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
} else {
// ≤24B: producer left AX=word0,
// DX=word1, CX=word2. Push high→low so
// the pop drains word0 first.
cgexpr(c, arg);
let k: i32 = nwords - 1;
for (k >= 0) {
if (k == 2) {
emitline("\tPUSHQ\tCX\n");
} else { if (k == 1) {
emitline("\tPUSHQ\tDX\n");
} else {
emitline("\tPUSHQ\tAX\n");
}; };
k -= 1;
};
};
return rest + nwords;
};
if (!aggargsrcaddr(c, arg, "SI")) {
let msg: str = "#271: aggregate arg from unsupported source kind\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((k*8): i64);
emitline("(SI), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
return rest + nwords;
};
};
// Float arg: cgexpr leaves the value in X0. Push 8 bytes from
// X0 via SUBQ+MOVSD so cgcall's pop side can drain into the
// XMM stream (X0..X7). f32 still occupies 8B on the stack —
@@ -17562,6 +17649,47 @@ fn structparamsize(c: *cgen, t: *node) i32 = {
return si.totsize;
};
// aggargsize — byte size of a by-value aggregate (struct OR array) call
// arg, else 0 (#271, mirror of cstage aggarg_size). The size axis the
// ≤16B-struct structparamsize carve-out doesn't cover: arrays of any
// size and structs > 16B. Reads the stamped tinfo size (the type table,
// byte-id with cstage Type.size).
fn aggargsizetn(t: *tinfo) i32 = {
if (t == nil) { return 0; };
let u: *tinfo = t;
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (u == nil) { return 0; };
if (u.kind == tykind.TY_STRUCT || u.kind == tykind.TY_ARRAY) {
return u.size: i32;
};
return 0;
};
fn nodeisaggarg(n: *node) bool = {
if (n == nil) { return false; };
return aggargsizetn(n.type_: *tinfo) > 0;
};
// aggargfloatstop — true iff the arg is a ≤16B struct with any float
// field (#271/#165). Such a struct from a non-ident source would need
// the SSE eightbyte transport the GP aggregate push/drain can't model;
// both stages loud-stop on it. Same predicate as the cstage Tfield
// fld_isfloat walk (cmd/w6c/cgen.c #271 push arm).
fn aggargfloatstop(n: *node) bool = {
if (n == nil) { return false; };
let st: *tinfo = n.type_: *tinfo;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st == nil) { return false; };
if (st.kind != tykind.TY_STRUCT) { return false; };
if (st.size: i32 > 16) { return false; };
let f: *tfield = st.fields;
for (f != nil) {
if (typeisfloat(f.type_)) { return true; };
f = f.tnext;
};
return false;
};
// structfloatclass — SysV per-eightbyte classification for the #165
// float-bearing-struct param case (param twin of #171's struct return;
// classifies per-eightbyte, not #163's per-element). Returns 0 when the
@@ -20626,6 +20754,92 @@ fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
return true;
};
// aggargsrcaddr — land the ADDRESS of an addressable aggregate (struct/
// array) call-arg source in dstreg (#271, mirror of cstage
// aggarg_srcaddr). Reuses the closed #265/#268 let-init-copy dispatch:
// local ident slot (LEAQ off(BP)), module-let global (LEAQ name(SB)),
// deref operand (cgexpr of the pointer), N_DOT field (dotchainaddr,
// #253), N_INDEX element of an N_IDENT array base (the &base[i] spine,
// #252/#270). Returns false for an uncovered source kind (caller loud-
// stops, rule 7). The CALL source is handled at the push site.
fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
if (src.kind == nkind.N_UN) {
if (src.op == tkind.TK_STAR) {
cgexpr(c, src.lhs);
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
};
if (src.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
// global value source. Gated to a module-`let` (letvartnode,
// the cstage let_islet twin); a const array/struct `def`
// aggregate ARG is untested + out of scope (#274; both stages
// loud-stop, rule-10 aligned).
if (letvartnode(c, src.str) != nil) {
emitline("\tLEAQ\t");
emitsymname(c, src.str);
emitline("(SB), ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
if (src.kind == nkind.N_DOT) {
return dotchainaddr(c, src, dst);
};
if (src.kind == nkind.N_INDEX) {
let base: *node = src.lhs;
let idx: *node = src.rhs;
if (base == nil) { return false; };
if (base.kind != nkind.N_IDENT) { return false; };
let bu: *tinfo = base.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
if (bu.kind != tykind.TY_ARRAY) { return false; };
let esz: i32 = 1;
if (bu.sub != nil) { esz = bu.sub.size: i32; };
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
let boff: *local = localfindnode(c, base.str);
if (boff != nil) {
emitline("\tLEAQ\t");
emitoff(boff.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, base.str);
emitline("(SB), BX\n");
};
emitline("\tADDQ\tBX, AX\n");
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
return false;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -24004,6 +24218,13 @@ fn cgcall(c: *cgen, n: *node) void = {
// words and shift intidx out of sync.
let tcs: i32 = taggedcallslot(c, a);
if (tcs > 0) { extra = tcs / 8 - 1; };
// #271: array / >16B-struct / non-ident 16B-struct
// aggregate arg — pushargsrev staged ceil(sz/8) words;
// drain exactly that many so intidx tracks per-arg
// (the ≤16B struct IDENT case is the stfc branch
// above). Mirror of cstage node_isaggarg drain arm.
let aggsz: i32 = aggargsizetn(a.type_: *tinfo);
if (aggsz > 0) { extra = (aggsz + 7) / 8 - 1; };
let words: i32 = 1 + extra;
let w: i32 = 0;
for (w < words) {
@@ -30870,18 +31091,66 @@ fn cgfnparams(c: *cgen, params: *node) void = {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw;
};};
} else {
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else { let aggsz2: i32 = aggargsizetn(p.lhs.type_: *tinfo);
if (aggsz2 > 0) {
// #271: array / >16B-struct by-value param —
// received as ceil(sz/8) GP eightbytes, the
// callee twin of the generalised aggregate-arg
// push. Mirror of the cstage is_bigagg arm
// (regs-fit / partial-stitch / stack-spill).
let nw2: i32 = (aggsz2 + 7) / 8;
if (idx + nw2 <= 6) {
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
let w: i32 = 0;
for (w < nw2) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
} else { if (idx < 6) {
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
let regs_left: i32 = 6 - idx;
let w: i32 = 0;
for (w < regs_left) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
for (w < nw2) {
emitline("\tMOVQ\t");
emitoff((16 + stkcursor*8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
stkcursor += 1;
w += 1;
};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw2;
};};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
};
};
};
};};};

View File

@@ -424,18 +424,66 @@ fn cgfnparams(c: *cgen, params: *node) void = {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw;
};};
} else {
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else { let aggsz2: i32 = aggargsizetn(p.lhs.type_: *tinfo);
if (aggsz2 > 0) {
// #271: array / >16B-struct by-value param —
// received as ceil(sz/8) GP eightbytes, the
// callee twin of the generalised aggregate-arg
// push. Mirror of the cstage is_bigagg arm
// (regs-fit / partial-stitch / stack-spill).
let nw2: i32 = (aggsz2 + 7) / 8;
if (idx + nw2 <= 6) {
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
let w: i32 = 0;
for (w < nw2) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
} else { if (idx < 6) {
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
let regs_left: i32 = 6 - idx;
let w: i32 = 0;
for (w < regs_left) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
for (w < nw2) {
emitline("\tMOVQ\t");
emitoff((16 + stkcursor*8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
stkcursor += 1;
w += 1;
};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw2;
};};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
};
};
};
};};};

View File

@@ -1128,6 +1128,92 @@ fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
return true;
};
// aggargsrcaddr — land the ADDRESS of an addressable aggregate (struct/
// array) call-arg source in dstreg (#271, mirror of cstage
// aggarg_srcaddr). Reuses the closed #265/#268 let-init-copy dispatch:
// local ident slot (LEAQ off(BP)), module-let global (LEAQ name(SB)),
// deref operand (cgexpr of the pointer), N_DOT field (dotchainaddr,
// #253), N_INDEX element of an N_IDENT array base (the &base[i] spine,
// #252/#270). Returns false for an uncovered source kind (caller loud-
// stops, rule 7). The CALL source is handled at the push site.
fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
if (src.kind == nkind.N_UN) {
if (src.op == tkind.TK_STAR) {
cgexpr(c, src.lhs);
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
};
if (src.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
// global value source. Gated to a module-`let` (letvartnode,
// the cstage let_islet twin); a const array/struct `def`
// aggregate ARG is untested + out of scope (#274; both stages
// loud-stop, rule-10 aligned).
if (letvartnode(c, src.str) != nil) {
emitline("\tLEAQ\t");
emitsymname(c, src.str);
emitline("(SB), ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
if (src.kind == nkind.N_DOT) {
return dotchainaddr(c, src, dst);
};
if (src.kind == nkind.N_INDEX) {
let base: *node = src.lhs;
let idx: *node = src.rhs;
if (base == nil) { return false; };
if (base.kind != nkind.N_IDENT) { return false; };
let bu: *tinfo = base.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
if (bu.kind != tykind.TY_ARRAY) { return false; };
let esz: i32 = 1;
if (bu.sub != nil) { esz = bu.sub.size: i32; };
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
let boff: *local = localfindnode(c, base.str);
if (boff != nil) {
emitline("\tLEAQ\t");
emitoff(boff.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, base.str);
emitline("(SB), BX\n");
};
emitline("\tADDQ\tBX, AX\n");
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
return false;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -4506,6 +4592,13 @@ fn cgcall(c: *cgen, n: *node) void = {
// words and shift intidx out of sync.
let tcs: i32 = taggedcallslot(c, a);
if (tcs > 0) { extra = tcs / 8 - 1; };
// #271: array / >16B-struct / non-ident 16B-struct
// aggregate arg — pushargsrev staged ceil(sz/8) words;
// drain exactly that many so intidx tracks per-arg
// (the ≤16B struct IDENT case is the stfc branch
// above). Mirror of cstage node_isaggarg drain arm.
let aggsz: i32 = aggargsizetn(a.type_: *tinfo);
if (aggsz > 0) { extra = (aggsz + 7) / 8 - 1; };
let words: i32 = 1 + extra;
let w: i32 = 0;
for (w < words) {

View File

@@ -466,6 +466,93 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
};
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a
// non-ident source, OR any array, OR a struct > 16B. The arg twin
// of the #265/#268 let-init copy (mirror of cstage cgen.c #271 push
// arm): materialise the source ADDRESS in SI and push its ceil(sz/8)
// words high→low (the pop drains word0 into the first arg reg). A
// CALL source receives first — ≤24B in AX/DX/CX pushed straight,
// >24B sret'd into @aggargscr then pushed from there. Pre-fix every
// such source fell to the scalar default (one PUSHQ for a multi-word
// aggregate) and stack-imbalanced against the type-based drain.
let aggsz: i32 = aggargsizetn(arg.type_: *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.
let structident: bool = false;
if (arg.kind == nkind.N_IDENT) {
let st: *tinfo = arg.type_: *tinfo;
for (st != nil && st.kind == tykind.TY_NAMED) {
st = st.under;
};
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
if (st.size: i32 <= 16) { structident = true; };
}; };
};
if (!structident) {
if (aggargfloatstop(arg)) {
let msg: str = "#271/#165: float-bearing struct arg from a non-ident source needs SSE eightbyte transport (out of scope)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let nwords: i32 = (aggsz + 7) / 8;
if (arg.kind == nkind.N_CALL) {
if (callsretsize(c, arg) > 0) {
let scr: i32 = localadd(c, "@aggargscr",
aggsz, nil);
c.sretdestoff = scr;
cgexpr(c, arg);
c.sretdestoff = 0;
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((scr + k*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
} else {
// ≤24B: producer left AX=word0,
// DX=word1, CX=word2. Push high→low so
// the pop drains word0 first.
cgexpr(c, arg);
let k: i32 = nwords - 1;
for (k >= 0) {
if (k == 2) {
emitline("\tPUSHQ\tCX\n");
} else { if (k == 1) {
emitline("\tPUSHQ\tDX\n");
} else {
emitline("\tPUSHQ\tAX\n");
}; };
k -= 1;
};
};
return rest + nwords;
};
if (!aggargsrcaddr(c, arg, "SI")) {
let msg: str = "#271: aggregate arg from unsupported source kind\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((k*8): i64);
emitline("(SI), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
return rest + nwords;
};
};
// Float arg: cgexpr leaves the value in X0. Push 8 bytes from
// X0 via SUBQ+MOVSD so cgcall's pop side can drain into the
// XMM stream (X0..X7). f32 still occupies 8B on the stack —
@@ -2052,6 +2139,47 @@ fn structparamsize(c: *cgen, t: *node) i32 = {
return si.totsize;
};
// aggargsize — byte size of a by-value aggregate (struct OR array) call
// arg, else 0 (#271, mirror of cstage aggarg_size). The size axis the
// ≤16B-struct structparamsize carve-out doesn't cover: arrays of any
// size and structs > 16B. Reads the stamped tinfo size (the type table,
// byte-id with cstage Type.size).
fn aggargsizetn(t: *tinfo) i32 = {
if (t == nil) { return 0; };
let u: *tinfo = t;
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (u == nil) { return 0; };
if (u.kind == tykind.TY_STRUCT || u.kind == tykind.TY_ARRAY) {
return u.size: i32;
};
return 0;
};
fn nodeisaggarg(n: *node) bool = {
if (n == nil) { return false; };
return aggargsizetn(n.type_: *tinfo) > 0;
};
// aggargfloatstop — true iff the arg is a ≤16B struct with any float
// field (#271/#165). Such a struct from a non-ident source would need
// the SSE eightbyte transport the GP aggregate push/drain can't model;
// both stages loud-stop on it. Same predicate as the cstage Tfield
// fld_isfloat walk (cmd/w6c/cgen.c #271 push arm).
fn aggargfloatstop(n: *node) bool = {
if (n == nil) { return false; };
let st: *tinfo = n.type_: *tinfo;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st == nil) { return false; };
if (st.kind != tykind.TY_STRUCT) { return false; };
if (st.size: i32 > 16) { return false; };
let f: *tfield = st.fields;
for (f != nil) {
if (typeisfloat(f.type_)) { return true; };
f = f.tnext;
};
return false;
};
// structfloatclass — SysV per-eightbyte classification for the #165
// float-bearing-struct param case (param twin of #171's struct return;
// classifies per-eightbyte, not #163's per-element). Returns 0 when the

View File

@@ -15976,6 +15976,93 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
};
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a
// non-ident source, OR any array, OR a struct > 16B. The arg twin
// of the #265/#268 let-init copy (mirror of cstage cgen.c #271 push
// arm): materialise the source ADDRESS in SI and push its ceil(sz/8)
// words high→low (the pop drains word0 into the first arg reg). A
// CALL source receives first — ≤24B in AX/DX/CX pushed straight,
// >24B sret'd into @aggargscr then pushed from there. Pre-fix every
// such source fell to the scalar default (one PUSHQ for a multi-word
// aggregate) and stack-imbalanced against the type-based drain.
let aggsz: i32 = aggargsizetn(arg.type_: *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.
let structident: bool = false;
if (arg.kind == nkind.N_IDENT) {
let st: *tinfo = arg.type_: *tinfo;
for (st != nil && st.kind == tykind.TY_NAMED) {
st = st.under;
};
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
if (st.size: i32 <= 16) { structident = true; };
}; };
};
if (!structident) {
if (aggargfloatstop(arg)) {
let msg: str = "#271/#165: float-bearing struct arg from a non-ident source needs SSE eightbyte transport (out of scope)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let nwords: i32 = (aggsz + 7) / 8;
if (arg.kind == nkind.N_CALL) {
if (callsretsize(c, arg) > 0) {
let scr: i32 = localadd(c, "@aggargscr",
aggsz, nil);
c.sretdestoff = scr;
cgexpr(c, arg);
c.sretdestoff = 0;
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((scr + k*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
} else {
// ≤24B: producer left AX=word0,
// DX=word1, CX=word2. Push high→low so
// the pop drains word0 first.
cgexpr(c, arg);
let k: i32 = nwords - 1;
for (k >= 0) {
if (k == 2) {
emitline("\tPUSHQ\tCX\n");
} else { if (k == 1) {
emitline("\tPUSHQ\tDX\n");
} else {
emitline("\tPUSHQ\tAX\n");
}; };
k -= 1;
};
};
return rest + nwords;
};
if (!aggargsrcaddr(c, arg, "SI")) {
let msg: str = "#271: aggregate arg from unsupported source kind\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((k*8): i64);
emitline("(SI), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
return rest + nwords;
};
};
// Float arg: cgexpr leaves the value in X0. Push 8 bytes from
// X0 via SUBQ+MOVSD so cgcall's pop side can drain into the
// XMM stream (X0..X7). f32 still occupies 8B on the stack —
@@ -17562,6 +17649,47 @@ fn structparamsize(c: *cgen, t: *node) i32 = {
return si.totsize;
};
// aggargsize — byte size of a by-value aggregate (struct OR array) call
// arg, else 0 (#271, mirror of cstage aggarg_size). The size axis the
// ≤16B-struct structparamsize carve-out doesn't cover: arrays of any
// size and structs > 16B. Reads the stamped tinfo size (the type table,
// byte-id with cstage Type.size).
fn aggargsizetn(t: *tinfo) i32 = {
if (t == nil) { return 0; };
let u: *tinfo = t;
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (u == nil) { return 0; };
if (u.kind == tykind.TY_STRUCT || u.kind == tykind.TY_ARRAY) {
return u.size: i32;
};
return 0;
};
fn nodeisaggarg(n: *node) bool = {
if (n == nil) { return false; };
return aggargsizetn(n.type_: *tinfo) > 0;
};
// aggargfloatstop — true iff the arg is a ≤16B struct with any float
// field (#271/#165). Such a struct from a non-ident source would need
// the SSE eightbyte transport the GP aggregate push/drain can't model;
// both stages loud-stop on it. Same predicate as the cstage Tfield
// fld_isfloat walk (cmd/w6c/cgen.c #271 push arm).
fn aggargfloatstop(n: *node) bool = {
if (n == nil) { return false; };
let st: *tinfo = n.type_: *tinfo;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st == nil) { return false; };
if (st.kind != tykind.TY_STRUCT) { return false; };
if (st.size: i32 > 16) { return false; };
let f: *tfield = st.fields;
for (f != nil) {
if (typeisfloat(f.type_)) { return true; };
f = f.tnext;
};
return false;
};
// structfloatclass — SysV per-eightbyte classification for the #165
// float-bearing-struct param case (param twin of #171's struct return;
// classifies per-eightbyte, not #163's per-element). Returns 0 when the
@@ -20626,6 +20754,92 @@ fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
return true;
};
// aggargsrcaddr — land the ADDRESS of an addressable aggregate (struct/
// array) call-arg source in dstreg (#271, mirror of cstage
// aggarg_srcaddr). Reuses the closed #265/#268 let-init-copy dispatch:
// local ident slot (LEAQ off(BP)), module-let global (LEAQ name(SB)),
// deref operand (cgexpr of the pointer), N_DOT field (dotchainaddr,
// #253), N_INDEX element of an N_IDENT array base (the &base[i] spine,
// #252/#270). Returns false for an uncovered source kind (caller loud-
// stops, rule 7). The CALL source is handled at the push site.
fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
if (src.kind == nkind.N_UN) {
if (src.op == tkind.TK_STAR) {
cgexpr(c, src.lhs);
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
};
if (src.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
// global value source. Gated to a module-`let` (letvartnode,
// the cstage let_islet twin); a const array/struct `def`
// aggregate ARG is untested + out of scope (#274; both stages
// loud-stop, rule-10 aligned).
if (letvartnode(c, src.str) != nil) {
emitline("\tLEAQ\t");
emitsymname(c, src.str);
emitline("(SB), ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
if (src.kind == nkind.N_DOT) {
return dotchainaddr(c, src, dst);
};
if (src.kind == nkind.N_INDEX) {
let base: *node = src.lhs;
let idx: *node = src.rhs;
if (base == nil) { return false; };
if (base.kind != nkind.N_IDENT) { return false; };
let bu: *tinfo = base.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
if (bu.kind != tykind.TY_ARRAY) { return false; };
let esz: i32 = 1;
if (bu.sub != nil) { esz = bu.sub.size: i32; };
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
let boff: *local = localfindnode(c, base.str);
if (boff != nil) {
emitline("\tLEAQ\t");
emitoff(boff.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, base.str);
emitline("(SB), BX\n");
};
emitline("\tADDQ\tBX, AX\n");
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
return false;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -24004,6 +24218,13 @@ fn cgcall(c: *cgen, n: *node) void = {
// words and shift intidx out of sync.
let tcs: i32 = taggedcallslot(c, a);
if (tcs > 0) { extra = tcs / 8 - 1; };
// #271: array / >16B-struct / non-ident 16B-struct
// aggregate arg — pushargsrev staged ceil(sz/8) words;
// drain exactly that many so intidx tracks per-arg
// (the ≤16B struct IDENT case is the stfc branch
// above). Mirror of cstage node_isaggarg drain arm.
let aggsz: i32 = aggargsizetn(a.type_: *tinfo);
if (aggsz > 0) { extra = (aggsz + 7) / 8 - 1; };
let words: i32 = 1 + extra;
let w: i32 = 0;
for (w < words) {
@@ -30870,18 +31091,66 @@ fn cgfnparams(c: *cgen, params: *node) void = {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw;
};};
} else {
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else { let aggsz2: i32 = aggargsizetn(p.lhs.type_: *tinfo);
if (aggsz2 > 0) {
// #271: array / >16B-struct by-value param —
// received as ceil(sz/8) GP eightbytes, the
// callee twin of the generalised aggregate-arg
// push. Mirror of the cstage is_bigagg arm
// (regs-fit / partial-stitch / stack-spill).
let nw2: i32 = (aggsz2 + 7) / 8;
if (idx + nw2 <= 6) {
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
let w: i32 = 0;
for (w < nw2) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
} else { if (idx < 6) {
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
let regs_left: i32 = 6 - idx;
let w: i32 = 0;
for (w < regs_left) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
for (w < nw2) {
emitline("\tMOVQ\t");
emitoff((16 + stkcursor*8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
stkcursor += 1;
w += 1;
};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw2;
};};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
};
};
};
};};};