wcc+w6c_ww: >48B tagged by-value args — MEMORY-class two-phase push (#38b)
Task #19 (the #38b residual surfaced by FC2 evidence): a tagged arg whose slot exceeds the 6-reg convention (>48B) is MEMORY-class per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). Caller stages the whole slot below every register-class word (two-phase push, rightmost-first, leftmost mem arg at 16(BP)); callee registers the param in place at positive BP offsets with zero prologue bytes; the merged slot count feeds the existing caller-cleanup ADDQ. Argument-side mirror of the #38 tagged-sret fix, same classify machinery (tagged_memarg_size / taggedmemargsize beside their register-class siblings). Pre-fix, the exact-typed arg loud-stopped on both stages, but WIDENING a concrete variant into a >48B param slipped the old guard silently — cstage pushed one scalar word while wwstage emitted an uncapped greedy stitch (wrong on both AND cs≠ww, gate-blind). Widen sources now route through the @tagscr scratch for mem slots. Loud boundaries kept (rule 7), each with its own diagnostic: sret-class tagged CALL result as mem-arg source (#40-family follow-up), global tagged let (task #25, broken at any size pre-existing), >48B variadic element, and mem-arg + register- overflow mixing (caller check + callee prologue mirror). Single commit: caller staging, callee receive, and both stages are one inseparable ABI class — landing any half alone breaks byte-id or runtime correctness (the #38 flip precedent); test/929 (15 table-driven rows: 56B/64B slots, widen-slip pin, source shapes, mixed orders both ways, two-mem call, 200k-call loop, 48B-boundary absence pin byte-id'd vs master, 5 reject rows pinning the exact per-guard diagnostic on both stages) rides with it.
This commit is contained in:
@@ -15831,31 +15831,124 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
|
||||
// tagged union and `arg`'s surface type is a concrete variant of it,
|
||||
// we materialise (tag, value-words, pad) for the parameter slot before
|
||||
// pushing — mirrors cmd/w6c/cgen.c's call-arg widening.
|
||||
fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
|
||||
//
|
||||
// #38b: cgcall walks the list TWICE — memphase=true first, staging
|
||||
// every MEMORY-class (>48B tagged) arg below all register-class
|
||||
// words, then memphase=false for the register classes. Each phase
|
||||
// skips the other's args; the return value counts only own-phase
|
||||
// slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per
|
||||
// ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit).
|
||||
fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
|
||||
if (arg == nil) { return 0; };
|
||||
let nextparam: *node = nil;
|
||||
if (param != nil) { nextparam = param.next; };
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam);
|
||||
// #38b residual (rule 7): a tagged arg slot past the 6-reg arg
|
||||
// capacity has no push shape — cstage tagged_arg_size returns 0
|
||||
// ("too large") and both stages fell to divergent silent pushes
|
||||
// (cs one word, ww slot words). Mirrors cstage cgcall's gate.
|
||||
{
|
||||
let a48: *tinfo = arg.type_: *tinfo;
|
||||
for (a48 != nil && a48.kind == tykind.TY_NAMED) {
|
||||
a48 = a48.under;
|
||||
};
|
||||
if (a48 != nil) {
|
||||
if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) {
|
||||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x
|
||||
// 8B words = the cstage tagged_arg_size cap.
|
||||
if (a48.size: i32 > 6 * 8) {
|
||||
let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n";
|
||||
os.write(2, m48.ptr, m48.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase);
|
||||
// #38b MEMORY-class detection: keyed off the declared param's
|
||||
// type (so widening into a >48B slot is caught), else the arg's
|
||||
// own stamped type (fn-ptr callee carries no param nodes).
|
||||
let memsz: i32 = 0;
|
||||
let memptype: *node = nil;
|
||||
if (param != nil) { if (param.kind == nkind.N_PARAM) {
|
||||
if (param.op != tkind.TK_ELLIPSIS) {
|
||||
memptype = param.lhs;
|
||||
if (memptype != nil) {
|
||||
memsz = taggedmemargsize(memptype.type_: *tinfo);
|
||||
};
|
||||
};
|
||||
}; };
|
||||
if (memsz == 0) {
|
||||
memsz = taggedmemargsize(arg.type_: *tinfo);
|
||||
};
|
||||
if (memphase != (memsz > 0)) { return rest; };
|
||||
if (memsz > 0) {
|
||||
// same-type check — mirror cstage's `(pu == au) ||
|
||||
// type_eq(p->type, at)` widen detection.
|
||||
let same: bool = false;
|
||||
let at: *tinfo = arg.type_: *tinfo;
|
||||
if (memptype != nil) {
|
||||
let pt: *tinfo = memptype.type_: *tinfo;
|
||||
let pu: *tinfo = pt;
|
||||
for (pu != nil && pu.kind == tykind.TY_NAMED) {
|
||||
pu = pu.under;
|
||||
};
|
||||
let au: *tinfo = at;
|
||||
for (au != nil && au.kind == tykind.TY_NAMED) {
|
||||
au = au.under;
|
||||
};
|
||||
if (pu != nil && pu == au) { same = true; };
|
||||
if (!same && pt != nil && at != nil) {
|
||||
if (typeeq(pt, at)) { same = true; };
|
||||
};
|
||||
} else {
|
||||
same = true;
|
||||
};
|
||||
if (!same) {
|
||||
// Widen via the @tagscr scratch for EVERY source
|
||||
// shape — the direct-push fast arms below stage
|
||||
// exactly 4 words, short of the memsz/8 the drain
|
||||
// accounts for (mirrors cstage cg_widen_tagged_push
|
||||
// dst_is_mem routing).
|
||||
let scroff: i32 = tagscradd(c, memsz);
|
||||
emitline("\tXORQ\tAX, AX\n");
|
||||
let zz: i32 = 0;
|
||||
for (zz < memsz) {
|
||||
emitline("\tMOVQ\tAX, ");
|
||||
emitoff((scroff + zz): i64);
|
||||
emitline("(BP)\n");
|
||||
zz += 8;
|
||||
};
|
||||
cgwidentaggedstore(c, memptype.type_: *tinfo, arg,
|
||||
"BP", scroff, memsz);
|
||||
let pp: i32 = memsz - 8;
|
||||
for (pp >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((scroff + pp): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
pp -= 8;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
// Exact type: raw slot words high→low from the value's
|
||||
// address (local slot, or any aggargsrcaddr-addressable
|
||||
// source: global let, N_DOT chain, array index, deref).
|
||||
// An exact-type CALL source is sret-class (>32B tagged
|
||||
// return) — its result is in memory behind a dest pointer,
|
||||
// not a register cursor; receive-then-push is the
|
||||
// #40-family follow-up.
|
||||
if (arg.kind == nkind.N_CALL) {
|
||||
let mc: str = "#38b: sret-class tagged call result as a >48B by-value arg unwired (#40-family follow-up)\n";
|
||||
os.write(2, mc.ptr, mc.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
if (arg.kind == nkind.N_IDENT) {
|
||||
let lc: *local = localfindnode(c, arg.str);
|
||||
if (lc != nil) {
|
||||
let w: i32 = memsz / 8 - 1;
|
||||
for (w >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((lc.off + w*8): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
w -= 1;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
};
|
||||
if (aggargsrcaddr(c, arg, "SI")) {
|
||||
let w: i32 = memsz / 8 - 1;
|
||||
for (w >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((w*8): i64);
|
||||
emitline("(SI), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
w -= 1;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
let mu: str = "#38b: >48B tagged arg from unsupported source kind (slice-element and rvalue sources unwired)\n";
|
||||
os.write(2, mu.ptr, mu.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
// Implicit widening from a concrete variant to a tagged-union
|
||||
// parameter slot. Skips when the arg is already a tagged local
|
||||
@@ -17922,6 +18015,26 @@ fn aggargsizetn(t: *tinfo) i32 = {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// taggedmemargsize — #38b: a tagged-union arg past the 6-reg register
|
||||
// convention (>48B slot, where the register transport's cap trips) is
|
||||
// MEMORY-class: the caller stages the whole slot on the outgoing stack
|
||||
// below every register-class word and the callee reads it in place at
|
||||
// positive BP offsets. Mirror of cstage tagged_memarg_size; ABI shape
|
||||
// per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). The
|
||||
// ≤48B register convention is pinned in-tree (test/926 boundary rows).
|
||||
fn taggedmemargsize(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_TAGGED) { return 0; };
|
||||
if (u.nullable != 0) { return 0; };
|
||||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x 8B words — the
|
||||
// same register-capacity constant as cstage tagged_arg_size.
|
||||
if (u.size: i32 <= 6 * 8) { return 0; };
|
||||
return u.size: i32;
|
||||
};
|
||||
|
||||
fn nodeisaggarg(n: *node) bool = {
|
||||
if (n == nil) { return false; };
|
||||
return aggargsizetn(n.type_: *tinfo) > 0;
|
||||
@@ -25054,6 +25167,17 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
if (esz < 1) { esz = 1; };
|
||||
// #38b: a >48B tagged variadic ELEMENT would
|
||||
// need the memory convention inside the vararg
|
||||
// gather buffer — unwired (rule 7). cstage twin
|
||||
// guards before its v_is_tagged gather.
|
||||
if (velem != nil) {
|
||||
if (taggedmemargsize(velem.type_: *tinfo) > 0) {
|
||||
let mv: str = "#38b: >48B tagged variadic element unwired\n";
|
||||
os.write(2, mv.ptr, mv.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
};
|
||||
let velemtagged: bool = istaggedtype(c, velem);
|
||||
let velemstr: bool = isstrtype(c, velem);
|
||||
let velemslice: bool = isslicetype(c, velem);
|
||||
@@ -25165,7 +25289,12 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams);
|
||||
// #38b: two-phase push — MEMORY-class (>48B tagged) args staged
|
||||
// first so they sit BELOW every register-class word; the pop loop
|
||||
// drains a strict prefix and never touches them. memwords feeds
|
||||
// the caller-cleanup ADDQ (with the mix guard below).
|
||||
let memwords: i32 = pushargsrev(c, n.list, calleeparams, true);
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false);
|
||||
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
|
||||
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
|
||||
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
|
||||
@@ -25201,9 +25330,31 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
if (sretcs > 0) { intidx = 1; };
|
||||
let fpidx: i32 = 0;
|
||||
let a: *node = n.list;
|
||||
let dparam: *node = calleeparams;
|
||||
let popped: i32 = 0;
|
||||
let stackslots: i32 = 0;
|
||||
for (a != nil) {
|
||||
// #38b: MEMORY-class arg — its words sit below the pop
|
||||
// region and stay on the stack for the callee; nothing to
|
||||
// drain. Same param-keyed-else-arg-keyed detection as
|
||||
// pushargsrev (a widened concrete arg is mem-class only
|
||||
// via its param).
|
||||
let dmemsz: i32 = 0;
|
||||
if (dparam != nil) { if (dparam.kind == nkind.N_PARAM) {
|
||||
if (dparam.op != tkind.TK_ELLIPSIS) {
|
||||
if (dparam.lhs != nil) {
|
||||
dmemsz = taggedmemargsize(dparam.lhs.type_: *tinfo);
|
||||
};
|
||||
};
|
||||
}; };
|
||||
if (dmemsz == 0) {
|
||||
dmemsz = taggedmemargsize(a.type_: *tinfo);
|
||||
};
|
||||
if (dmemsz > 0) {
|
||||
if (dparam != nil) { dparam = dparam.next; };
|
||||
a = a.next;
|
||||
continue;
|
||||
};
|
||||
let fk: i32 = 0;
|
||||
if (a != nil) {
|
||||
let at: *tinfo = a.type_: *tinfo;
|
||||
@@ -25353,6 +25504,7 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
if (dparam != nil) { dparam = dparam.next; };
|
||||
a = a.next;
|
||||
};
|
||||
// Drain any remaining slots that the arg-walker didn't account
|
||||
@@ -25371,6 +25523,17 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
// #38b: MEMORY-class args and register-overflow spill words cannot
|
||||
// coexist — the callee's positive-BP cursor walks params in
|
||||
// declaration order, but the residual region puts spill words
|
||||
// below every mem copy. Loud-stop (rule 7); cgfnparams holds the
|
||||
// mirror check. The merged count feeds the caller-cleanup ADDQ.
|
||||
if (memwords > 0 && stackslots > 0) {
|
||||
let mm: str = "#38b: >48B tagged arg mixed with register-overflow stack args unwired\n";
|
||||
os.write(2, mm.ptr, mm.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
stackslots += memwords;
|
||||
// `callee` is already in scope from line 2827; reuse it. Pre-#32
|
||||
// silent-redecl masked the second `let callee` here as a no-op
|
||||
// (same value, same fn-body scope post-#27).
|
||||
@@ -32549,6 +32712,9 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
// is registered with a *positive* offset pointing into the
|
||||
// caller's frame. Mirrors C cgen's cg_stack_arg_cursor.
|
||||
let stkcursor: i32 = 0;
|
||||
// #38b: words consumed by MEMORY-class (>48B tagged) params —
|
||||
// post-walk consistency check against stkcursor.
|
||||
let memwords: i32 = 0;
|
||||
for (p != nil) {
|
||||
if (p.kind == nkind.N_PARAM) {
|
||||
let nm: str = p.str;
|
||||
@@ -32743,7 +32909,19 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
if (istaggedtype(c, p.lhs)) {
|
||||
let slot: i32 = slotsize(c, p.lhs);
|
||||
let nw: i32 = slot / 8;
|
||||
if (idx + nw <= 6) {
|
||||
// #38b: MEMORY-class (>48B tagged) param — the
|
||||
// caller staged the whole slot below the return
|
||||
// address; read it in place at positive BP
|
||||
// offsets. No spill, no frame growth, zero
|
||||
// prologue bytes. Pre-fix this fell into the
|
||||
// greedy stitch arm below while cstage received
|
||||
// one scalar word (cs≠ww, silent).
|
||||
// ref/qbe/amd64/sysv.c:80-85 / :411-426.
|
||||
if (taggedmemargsize(p.lhs.type_: *tinfo) > 0) {
|
||||
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
||||
stkcursor += nw;
|
||||
memwords += nw;
|
||||
} else { if (idx + nw <= 6) {
|
||||
let off: i32 = localadd(c, nm, slot, p.lhs);
|
||||
let w: i32 = 0;
|
||||
for (w < nw) {
|
||||
@@ -32784,7 +32962,7 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
} else {
|
||||
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
||||
stkcursor += nw;
|
||||
};};
|
||||
};};};
|
||||
} else { if (isslicetype(c, p.lhs)) {
|
||||
if (idx + 3 <= 6) {
|
||||
let off: i32 = localadd(c, nm, tyslicesize(): i32, p.lhs);
|
||||
@@ -33004,6 +33182,17 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
};
|
||||
p = p.next;
|
||||
};
|
||||
// #38b: a MEMORY-class tagged param cannot coexist with stack-
|
||||
// spilled register-class params — both walk the same positive-BP
|
||||
// cursor in declaration order while the caller's residual region
|
||||
// puts spill words below every mem copy. Any non-mem cursor use
|
||||
// leaves stkcursor past the mem words. Mirror of the cgcall
|
||||
// caller-side check; loud-stop (rule 7).
|
||||
if (memwords > 0 && stkcursor != memwords) {
|
||||
let mp: str = "#38b: >48B tagged param mixed with stack-spilled params unwired\n";
|
||||
os.write(2, mp.ptr, mp.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
};
|
||||
|
||||
fn cgfn(c: *cgen, fn_: *node) void = {
|
||||
|
||||
@@ -35,6 +35,9 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
// is registered with a *positive* offset pointing into the
|
||||
// caller's frame. Mirrors C cgen's cg_stack_arg_cursor.
|
||||
let stkcursor: i32 = 0;
|
||||
// #38b: words consumed by MEMORY-class (>48B tagged) params —
|
||||
// post-walk consistency check against stkcursor.
|
||||
let memwords: i32 = 0;
|
||||
for (p != nil) {
|
||||
if (p.kind == nkind.N_PARAM) {
|
||||
let nm: str = p.str;
|
||||
@@ -229,7 +232,19 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
if (istaggedtype(c, p.lhs)) {
|
||||
let slot: i32 = slotsize(c, p.lhs);
|
||||
let nw: i32 = slot / 8;
|
||||
if (idx + nw <= 6) {
|
||||
// #38b: MEMORY-class (>48B tagged) param — the
|
||||
// caller staged the whole slot below the return
|
||||
// address; read it in place at positive BP
|
||||
// offsets. No spill, no frame growth, zero
|
||||
// prologue bytes. Pre-fix this fell into the
|
||||
// greedy stitch arm below while cstage received
|
||||
// one scalar word (cs≠ww, silent).
|
||||
// ref/qbe/amd64/sysv.c:80-85 / :411-426.
|
||||
if (taggedmemargsize(p.lhs.type_: *tinfo) > 0) {
|
||||
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
||||
stkcursor += nw;
|
||||
memwords += nw;
|
||||
} else { if (idx + nw <= 6) {
|
||||
let off: i32 = localadd(c, nm, slot, p.lhs);
|
||||
let w: i32 = 0;
|
||||
for (w < nw) {
|
||||
@@ -270,7 +285,7 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
} else {
|
||||
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
||||
stkcursor += nw;
|
||||
};};
|
||||
};};};
|
||||
} else { if (isslicetype(c, p.lhs)) {
|
||||
if (idx + 3 <= 6) {
|
||||
let off: i32 = localadd(c, nm, tyslicesize(): i32, p.lhs);
|
||||
@@ -490,6 +505,17 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
};
|
||||
p = p.next;
|
||||
};
|
||||
// #38b: a MEMORY-class tagged param cannot coexist with stack-
|
||||
// spilled register-class params — both walk the same positive-BP
|
||||
// cursor in declaration order while the caller's residual region
|
||||
// puts spill words below every mem copy. Any non-mem cursor use
|
||||
// leaves stkcursor past the mem words. Mirror of the cgcall
|
||||
// caller-side check; loud-stop (rule 7).
|
||||
if (memwords > 0 && stkcursor != memwords) {
|
||||
let mp: str = "#38b: >48B tagged param mixed with stack-spilled params unwired\n";
|
||||
os.write(2, mp.ptr, mp.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
};
|
||||
|
||||
fn cgfn(c: *cgen, fn_: *node) void = {
|
||||
|
||||
@@ -5144,6 +5144,17 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
if (esz < 1) { esz = 1; };
|
||||
// #38b: a >48B tagged variadic ELEMENT would
|
||||
// need the memory convention inside the vararg
|
||||
// gather buffer — unwired (rule 7). cstage twin
|
||||
// guards before its v_is_tagged gather.
|
||||
if (velem != nil) {
|
||||
if (taggedmemargsize(velem.type_: *tinfo) > 0) {
|
||||
let mv: str = "#38b: >48B tagged variadic element unwired\n";
|
||||
os.write(2, mv.ptr, mv.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
};
|
||||
let velemtagged: bool = istaggedtype(c, velem);
|
||||
let velemstr: bool = isstrtype(c, velem);
|
||||
let velemslice: bool = isslicetype(c, velem);
|
||||
@@ -5255,7 +5266,12 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams);
|
||||
// #38b: two-phase push — MEMORY-class (>48B tagged) args staged
|
||||
// first so they sit BELOW every register-class word; the pop loop
|
||||
// drains a strict prefix and never touches them. memwords feeds
|
||||
// the caller-cleanup ADDQ (with the mix guard below).
|
||||
let memwords: i32 = pushargsrev(c, n.list, calleeparams, true);
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false);
|
||||
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
|
||||
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
|
||||
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
|
||||
@@ -5291,9 +5307,31 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
if (sretcs > 0) { intidx = 1; };
|
||||
let fpidx: i32 = 0;
|
||||
let a: *node = n.list;
|
||||
let dparam: *node = calleeparams;
|
||||
let popped: i32 = 0;
|
||||
let stackslots: i32 = 0;
|
||||
for (a != nil) {
|
||||
// #38b: MEMORY-class arg — its words sit below the pop
|
||||
// region and stay on the stack for the callee; nothing to
|
||||
// drain. Same param-keyed-else-arg-keyed detection as
|
||||
// pushargsrev (a widened concrete arg is mem-class only
|
||||
// via its param).
|
||||
let dmemsz: i32 = 0;
|
||||
if (dparam != nil) { if (dparam.kind == nkind.N_PARAM) {
|
||||
if (dparam.op != tkind.TK_ELLIPSIS) {
|
||||
if (dparam.lhs != nil) {
|
||||
dmemsz = taggedmemargsize(dparam.lhs.type_: *tinfo);
|
||||
};
|
||||
};
|
||||
}; };
|
||||
if (dmemsz == 0) {
|
||||
dmemsz = taggedmemargsize(a.type_: *tinfo);
|
||||
};
|
||||
if (dmemsz > 0) {
|
||||
if (dparam != nil) { dparam = dparam.next; };
|
||||
a = a.next;
|
||||
continue;
|
||||
};
|
||||
let fk: i32 = 0;
|
||||
if (a != nil) {
|
||||
let at: *tinfo = a.type_: *tinfo;
|
||||
@@ -5443,6 +5481,7 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
if (dparam != nil) { dparam = dparam.next; };
|
||||
a = a.next;
|
||||
};
|
||||
// Drain any remaining slots that the arg-walker didn't account
|
||||
@@ -5461,6 +5500,17 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
// #38b: MEMORY-class args and register-overflow spill words cannot
|
||||
// coexist — the callee's positive-BP cursor walks params in
|
||||
// declaration order, but the residual region puts spill words
|
||||
// below every mem copy. Loud-stop (rule 7); cgfnparams holds the
|
||||
// mirror check. The merged count feeds the caller-cleanup ADDQ.
|
||||
if (memwords > 0 && stackslots > 0) {
|
||||
let mm: str = "#38b: >48B tagged arg mixed with register-overflow stack args unwired\n";
|
||||
os.write(2, mm.ptr, mm.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
stackslots += memwords;
|
||||
// `callee` is already in scope from line 2827; reuse it. Pre-#32
|
||||
// silent-redecl masked the second `let callee` here as a no-op
|
||||
// (same value, same fn-body scope post-#27).
|
||||
|
||||
@@ -99,31 +99,124 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
|
||||
// tagged union and `arg`'s surface type is a concrete variant of it,
|
||||
// we materialise (tag, value-words, pad) for the parameter slot before
|
||||
// pushing — mirrors cmd/w6c/cgen.c's call-arg widening.
|
||||
fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
|
||||
//
|
||||
// #38b: cgcall walks the list TWICE — memphase=true first, staging
|
||||
// every MEMORY-class (>48B tagged) arg below all register-class
|
||||
// words, then memphase=false for the register classes. Each phase
|
||||
// skips the other's args; the return value counts only own-phase
|
||||
// slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per
|
||||
// ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit).
|
||||
fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
|
||||
if (arg == nil) { return 0; };
|
||||
let nextparam: *node = nil;
|
||||
if (param != nil) { nextparam = param.next; };
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam);
|
||||
// #38b residual (rule 7): a tagged arg slot past the 6-reg arg
|
||||
// capacity has no push shape — cstage tagged_arg_size returns 0
|
||||
// ("too large") and both stages fell to divergent silent pushes
|
||||
// (cs one word, ww slot words). Mirrors cstage cgcall's gate.
|
||||
{
|
||||
let a48: *tinfo = arg.type_: *tinfo;
|
||||
for (a48 != nil && a48.kind == tykind.TY_NAMED) {
|
||||
a48 = a48.under;
|
||||
};
|
||||
if (a48 != nil) {
|
||||
if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) {
|
||||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x
|
||||
// 8B words = the cstage tagged_arg_size cap.
|
||||
if (a48.size: i32 > 6 * 8) {
|
||||
let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n";
|
||||
os.write(2, m48.ptr, m48.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase);
|
||||
// #38b MEMORY-class detection: keyed off the declared param's
|
||||
// type (so widening into a >48B slot is caught), else the arg's
|
||||
// own stamped type (fn-ptr callee carries no param nodes).
|
||||
let memsz: i32 = 0;
|
||||
let memptype: *node = nil;
|
||||
if (param != nil) { if (param.kind == nkind.N_PARAM) {
|
||||
if (param.op != tkind.TK_ELLIPSIS) {
|
||||
memptype = param.lhs;
|
||||
if (memptype != nil) {
|
||||
memsz = taggedmemargsize(memptype.type_: *tinfo);
|
||||
};
|
||||
};
|
||||
}; };
|
||||
if (memsz == 0) {
|
||||
memsz = taggedmemargsize(arg.type_: *tinfo);
|
||||
};
|
||||
if (memphase != (memsz > 0)) { return rest; };
|
||||
if (memsz > 0) {
|
||||
// same-type check — mirror cstage's `(pu == au) ||
|
||||
// type_eq(p->type, at)` widen detection.
|
||||
let same: bool = false;
|
||||
let at: *tinfo = arg.type_: *tinfo;
|
||||
if (memptype != nil) {
|
||||
let pt: *tinfo = memptype.type_: *tinfo;
|
||||
let pu: *tinfo = pt;
|
||||
for (pu != nil && pu.kind == tykind.TY_NAMED) {
|
||||
pu = pu.under;
|
||||
};
|
||||
let au: *tinfo = at;
|
||||
for (au != nil && au.kind == tykind.TY_NAMED) {
|
||||
au = au.under;
|
||||
};
|
||||
if (pu != nil && pu == au) { same = true; };
|
||||
if (!same && pt != nil && at != nil) {
|
||||
if (typeeq(pt, at)) { same = true; };
|
||||
};
|
||||
} else {
|
||||
same = true;
|
||||
};
|
||||
if (!same) {
|
||||
// Widen via the @tagscr scratch for EVERY source
|
||||
// shape — the direct-push fast arms below stage
|
||||
// exactly 4 words, short of the memsz/8 the drain
|
||||
// accounts for (mirrors cstage cg_widen_tagged_push
|
||||
// dst_is_mem routing).
|
||||
let scroff: i32 = tagscradd(c, memsz);
|
||||
emitline("\tXORQ\tAX, AX\n");
|
||||
let zz: i32 = 0;
|
||||
for (zz < memsz) {
|
||||
emitline("\tMOVQ\tAX, ");
|
||||
emitoff((scroff + zz): i64);
|
||||
emitline("(BP)\n");
|
||||
zz += 8;
|
||||
};
|
||||
cgwidentaggedstore(c, memptype.type_: *tinfo, arg,
|
||||
"BP", scroff, memsz);
|
||||
let pp: i32 = memsz - 8;
|
||||
for (pp >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((scroff + pp): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
pp -= 8;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
// Exact type: raw slot words high→low from the value's
|
||||
// address (local slot, or any aggargsrcaddr-addressable
|
||||
// source: global let, N_DOT chain, array index, deref).
|
||||
// An exact-type CALL source is sret-class (>32B tagged
|
||||
// return) — its result is in memory behind a dest pointer,
|
||||
// not a register cursor; receive-then-push is the
|
||||
// #40-family follow-up.
|
||||
if (arg.kind == nkind.N_CALL) {
|
||||
let mc: str = "#38b: sret-class tagged call result as a >48B by-value arg unwired (#40-family follow-up)\n";
|
||||
os.write(2, mc.ptr, mc.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
if (arg.kind == nkind.N_IDENT) {
|
||||
let lc: *local = localfindnode(c, arg.str);
|
||||
if (lc != nil) {
|
||||
let w: i32 = memsz / 8 - 1;
|
||||
for (w >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((lc.off + w*8): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
w -= 1;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
};
|
||||
if (aggargsrcaddr(c, arg, "SI")) {
|
||||
let w: i32 = memsz / 8 - 1;
|
||||
for (w >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((w*8): i64);
|
||||
emitline("(SI), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
w -= 1;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
let mu: str = "#38b: >48B tagged arg from unsupported source kind (slice-element and rvalue sources unwired)\n";
|
||||
os.write(2, mu.ptr, mu.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
// Implicit widening from a concrete variant to a tagged-union
|
||||
// parameter slot. Skips when the arg is already a tagged local
|
||||
@@ -2190,6 +2283,26 @@ fn aggargsizetn(t: *tinfo) i32 = {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// taggedmemargsize — #38b: a tagged-union arg past the 6-reg register
|
||||
// convention (>48B slot, where the register transport's cap trips) is
|
||||
// MEMORY-class: the caller stages the whole slot on the outgoing stack
|
||||
// below every register-class word and the callee reads it in place at
|
||||
// positive BP offsets. Mirror of cstage tagged_memarg_size; ABI shape
|
||||
// per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). The
|
||||
// ≤48B register convention is pinned in-tree (test/926 boundary rows).
|
||||
fn taggedmemargsize(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_TAGGED) { return 0; };
|
||||
if (u.nullable != 0) { return 0; };
|
||||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x 8B words — the
|
||||
// same register-capacity constant as cstage tagged_arg_size.
|
||||
if (u.size: i32 <= 6 * 8) { return 0; };
|
||||
return u.size: i32;
|
||||
};
|
||||
|
||||
fn nodeisaggarg(n: *node) bool = {
|
||||
if (n == nil) { return false; };
|
||||
return aggargsizetn(n.type_: *tinfo) > 0;
|
||||
|
||||
@@ -15831,31 +15831,124 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
|
||||
// tagged union and `arg`'s surface type is a concrete variant of it,
|
||||
// we materialise (tag, value-words, pad) for the parameter slot before
|
||||
// pushing — mirrors cmd/w6c/cgen.c's call-arg widening.
|
||||
fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
|
||||
//
|
||||
// #38b: cgcall walks the list TWICE — memphase=true first, staging
|
||||
// every MEMORY-class (>48B tagged) arg below all register-class
|
||||
// words, then memphase=false for the register classes. Each phase
|
||||
// skips the other's args; the return value counts only own-phase
|
||||
// slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per
|
||||
// ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit).
|
||||
fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
|
||||
if (arg == nil) { return 0; };
|
||||
let nextparam: *node = nil;
|
||||
if (param != nil) { nextparam = param.next; };
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam);
|
||||
// #38b residual (rule 7): a tagged arg slot past the 6-reg arg
|
||||
// capacity has no push shape — cstage tagged_arg_size returns 0
|
||||
// ("too large") and both stages fell to divergent silent pushes
|
||||
// (cs one word, ww slot words). Mirrors cstage cgcall's gate.
|
||||
{
|
||||
let a48: *tinfo = arg.type_: *tinfo;
|
||||
for (a48 != nil && a48.kind == tykind.TY_NAMED) {
|
||||
a48 = a48.under;
|
||||
};
|
||||
if (a48 != nil) {
|
||||
if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) {
|
||||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x
|
||||
// 8B words = the cstage tagged_arg_size cap.
|
||||
if (a48.size: i32 > 6 * 8) {
|
||||
let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n";
|
||||
os.write(2, m48.ptr, m48.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase);
|
||||
// #38b MEMORY-class detection: keyed off the declared param's
|
||||
// type (so widening into a >48B slot is caught), else the arg's
|
||||
// own stamped type (fn-ptr callee carries no param nodes).
|
||||
let memsz: i32 = 0;
|
||||
let memptype: *node = nil;
|
||||
if (param != nil) { if (param.kind == nkind.N_PARAM) {
|
||||
if (param.op != tkind.TK_ELLIPSIS) {
|
||||
memptype = param.lhs;
|
||||
if (memptype != nil) {
|
||||
memsz = taggedmemargsize(memptype.type_: *tinfo);
|
||||
};
|
||||
};
|
||||
}; };
|
||||
if (memsz == 0) {
|
||||
memsz = taggedmemargsize(arg.type_: *tinfo);
|
||||
};
|
||||
if (memphase != (memsz > 0)) { return rest; };
|
||||
if (memsz > 0) {
|
||||
// same-type check — mirror cstage's `(pu == au) ||
|
||||
// type_eq(p->type, at)` widen detection.
|
||||
let same: bool = false;
|
||||
let at: *tinfo = arg.type_: *tinfo;
|
||||
if (memptype != nil) {
|
||||
let pt: *tinfo = memptype.type_: *tinfo;
|
||||
let pu: *tinfo = pt;
|
||||
for (pu != nil && pu.kind == tykind.TY_NAMED) {
|
||||
pu = pu.under;
|
||||
};
|
||||
let au: *tinfo = at;
|
||||
for (au != nil && au.kind == tykind.TY_NAMED) {
|
||||
au = au.under;
|
||||
};
|
||||
if (pu != nil && pu == au) { same = true; };
|
||||
if (!same && pt != nil && at != nil) {
|
||||
if (typeeq(pt, at)) { same = true; };
|
||||
};
|
||||
} else {
|
||||
same = true;
|
||||
};
|
||||
if (!same) {
|
||||
// Widen via the @tagscr scratch for EVERY source
|
||||
// shape — the direct-push fast arms below stage
|
||||
// exactly 4 words, short of the memsz/8 the drain
|
||||
// accounts for (mirrors cstage cg_widen_tagged_push
|
||||
// dst_is_mem routing).
|
||||
let scroff: i32 = tagscradd(c, memsz);
|
||||
emitline("\tXORQ\tAX, AX\n");
|
||||
let zz: i32 = 0;
|
||||
for (zz < memsz) {
|
||||
emitline("\tMOVQ\tAX, ");
|
||||
emitoff((scroff + zz): i64);
|
||||
emitline("(BP)\n");
|
||||
zz += 8;
|
||||
};
|
||||
cgwidentaggedstore(c, memptype.type_: *tinfo, arg,
|
||||
"BP", scroff, memsz);
|
||||
let pp: i32 = memsz - 8;
|
||||
for (pp >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((scroff + pp): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
pp -= 8;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
// Exact type: raw slot words high→low from the value's
|
||||
// address (local slot, or any aggargsrcaddr-addressable
|
||||
// source: global let, N_DOT chain, array index, deref).
|
||||
// An exact-type CALL source is sret-class (>32B tagged
|
||||
// return) — its result is in memory behind a dest pointer,
|
||||
// not a register cursor; receive-then-push is the
|
||||
// #40-family follow-up.
|
||||
if (arg.kind == nkind.N_CALL) {
|
||||
let mc: str = "#38b: sret-class tagged call result as a >48B by-value arg unwired (#40-family follow-up)\n";
|
||||
os.write(2, mc.ptr, mc.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
if (arg.kind == nkind.N_IDENT) {
|
||||
let lc: *local = localfindnode(c, arg.str);
|
||||
if (lc != nil) {
|
||||
let w: i32 = memsz / 8 - 1;
|
||||
for (w >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((lc.off + w*8): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
w -= 1;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
};
|
||||
if (aggargsrcaddr(c, arg, "SI")) {
|
||||
let w: i32 = memsz / 8 - 1;
|
||||
for (w >= 0) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((w*8): i64);
|
||||
emitline("(SI), AX\n");
|
||||
emitline("\tPUSHQ\tAX\n");
|
||||
w -= 1;
|
||||
};
|
||||
return rest + memsz / 8;
|
||||
};
|
||||
let mu: str = "#38b: >48B tagged arg from unsupported source kind (slice-element and rvalue sources unwired)\n";
|
||||
os.write(2, mu.ptr, mu.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
// Implicit widening from a concrete variant to a tagged-union
|
||||
// parameter slot. Skips when the arg is already a tagged local
|
||||
@@ -17922,6 +18015,26 @@ fn aggargsizetn(t: *tinfo) i32 = {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// taggedmemargsize — #38b: a tagged-union arg past the 6-reg register
|
||||
// convention (>48B slot, where the register transport's cap trips) is
|
||||
// MEMORY-class: the caller stages the whole slot on the outgoing stack
|
||||
// below every register-class word and the callee reads it in place at
|
||||
// positive BP offsets. Mirror of cstage tagged_memarg_size; ABI shape
|
||||
// per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). The
|
||||
// ≤48B register convention is pinned in-tree (test/926 boundary rows).
|
||||
fn taggedmemargsize(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_TAGGED) { return 0; };
|
||||
if (u.nullable != 0) { return 0; };
|
||||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x 8B words — the
|
||||
// same register-capacity constant as cstage tagged_arg_size.
|
||||
if (u.size: i32 <= 6 * 8) { return 0; };
|
||||
return u.size: i32;
|
||||
};
|
||||
|
||||
fn nodeisaggarg(n: *node) bool = {
|
||||
if (n == nil) { return false; };
|
||||
return aggargsizetn(n.type_: *tinfo) > 0;
|
||||
@@ -25054,6 +25167,17 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
if (esz < 1) { esz = 1; };
|
||||
// #38b: a >48B tagged variadic ELEMENT would
|
||||
// need the memory convention inside the vararg
|
||||
// gather buffer — unwired (rule 7). cstage twin
|
||||
// guards before its v_is_tagged gather.
|
||||
if (velem != nil) {
|
||||
if (taggedmemargsize(velem.type_: *tinfo) > 0) {
|
||||
let mv: str = "#38b: >48B tagged variadic element unwired\n";
|
||||
os.write(2, mv.ptr, mv.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
};
|
||||
let velemtagged: bool = istaggedtype(c, velem);
|
||||
let velemstr: bool = isstrtype(c, velem);
|
||||
let velemslice: bool = isslicetype(c, velem);
|
||||
@@ -25165,7 +25289,12 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams);
|
||||
// #38b: two-phase push — MEMORY-class (>48B tagged) args staged
|
||||
// first so they sit BELOW every register-class word; the pop loop
|
||||
// drains a strict prefix and never touches them. memwords feeds
|
||||
// the caller-cleanup ADDQ (with the mix guard below).
|
||||
let memwords: i32 = pushargsrev(c, n.list, calleeparams, true);
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false);
|
||||
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
|
||||
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
|
||||
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
|
||||
@@ -25201,9 +25330,31 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
if (sretcs > 0) { intidx = 1; };
|
||||
let fpidx: i32 = 0;
|
||||
let a: *node = n.list;
|
||||
let dparam: *node = calleeparams;
|
||||
let popped: i32 = 0;
|
||||
let stackslots: i32 = 0;
|
||||
for (a != nil) {
|
||||
// #38b: MEMORY-class arg — its words sit below the pop
|
||||
// region and stay on the stack for the callee; nothing to
|
||||
// drain. Same param-keyed-else-arg-keyed detection as
|
||||
// pushargsrev (a widened concrete arg is mem-class only
|
||||
// via its param).
|
||||
let dmemsz: i32 = 0;
|
||||
if (dparam != nil) { if (dparam.kind == nkind.N_PARAM) {
|
||||
if (dparam.op != tkind.TK_ELLIPSIS) {
|
||||
if (dparam.lhs != nil) {
|
||||
dmemsz = taggedmemargsize(dparam.lhs.type_: *tinfo);
|
||||
};
|
||||
};
|
||||
}; };
|
||||
if (dmemsz == 0) {
|
||||
dmemsz = taggedmemargsize(a.type_: *tinfo);
|
||||
};
|
||||
if (dmemsz > 0) {
|
||||
if (dparam != nil) { dparam = dparam.next; };
|
||||
a = a.next;
|
||||
continue;
|
||||
};
|
||||
let fk: i32 = 0;
|
||||
if (a != nil) {
|
||||
let at: *tinfo = a.type_: *tinfo;
|
||||
@@ -25353,6 +25504,7 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
if (dparam != nil) { dparam = dparam.next; };
|
||||
a = a.next;
|
||||
};
|
||||
// Drain any remaining slots that the arg-walker didn't account
|
||||
@@ -25371,6 +25523,17 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
// #38b: MEMORY-class args and register-overflow spill words cannot
|
||||
// coexist — the callee's positive-BP cursor walks params in
|
||||
// declaration order, but the residual region puts spill words
|
||||
// below every mem copy. Loud-stop (rule 7); cgfnparams holds the
|
||||
// mirror check. The merged count feeds the caller-cleanup ADDQ.
|
||||
if (memwords > 0 && stackslots > 0) {
|
||||
let mm: str = "#38b: >48B tagged arg mixed with register-overflow stack args unwired\n";
|
||||
os.write(2, mm.ptr, mm.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
stackslots += memwords;
|
||||
// `callee` is already in scope from line 2827; reuse it. Pre-#32
|
||||
// silent-redecl masked the second `let callee` here as a no-op
|
||||
// (same value, same fn-body scope post-#27).
|
||||
@@ -32549,6 +32712,9 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
// is registered with a *positive* offset pointing into the
|
||||
// caller's frame. Mirrors C cgen's cg_stack_arg_cursor.
|
||||
let stkcursor: i32 = 0;
|
||||
// #38b: words consumed by MEMORY-class (>48B tagged) params —
|
||||
// post-walk consistency check against stkcursor.
|
||||
let memwords: i32 = 0;
|
||||
for (p != nil) {
|
||||
if (p.kind == nkind.N_PARAM) {
|
||||
let nm: str = p.str;
|
||||
@@ -32743,7 +32909,19 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
if (istaggedtype(c, p.lhs)) {
|
||||
let slot: i32 = slotsize(c, p.lhs);
|
||||
let nw: i32 = slot / 8;
|
||||
if (idx + nw <= 6) {
|
||||
// #38b: MEMORY-class (>48B tagged) param — the
|
||||
// caller staged the whole slot below the return
|
||||
// address; read it in place at positive BP
|
||||
// offsets. No spill, no frame growth, zero
|
||||
// prologue bytes. Pre-fix this fell into the
|
||||
// greedy stitch arm below while cstage received
|
||||
// one scalar word (cs≠ww, silent).
|
||||
// ref/qbe/amd64/sysv.c:80-85 / :411-426.
|
||||
if (taggedmemargsize(p.lhs.type_: *tinfo) > 0) {
|
||||
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
||||
stkcursor += nw;
|
||||
memwords += nw;
|
||||
} else { if (idx + nw <= 6) {
|
||||
let off: i32 = localadd(c, nm, slot, p.lhs);
|
||||
let w: i32 = 0;
|
||||
for (w < nw) {
|
||||
@@ -32784,7 +32962,7 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
} else {
|
||||
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
||||
stkcursor += nw;
|
||||
};};
|
||||
};};};
|
||||
} else { if (isslicetype(c, p.lhs)) {
|
||||
if (idx + 3 <= 6) {
|
||||
let off: i32 = localadd(c, nm, tyslicesize(): i32, p.lhs);
|
||||
@@ -33004,6 +33182,17 @@ fn cgfnparams(c: *cgen, params: *node) void = {
|
||||
};
|
||||
p = p.next;
|
||||
};
|
||||
// #38b: a MEMORY-class tagged param cannot coexist with stack-
|
||||
// spilled register-class params — both walk the same positive-BP
|
||||
// cursor in declaration order while the caller's residual region
|
||||
// puts spill words below every mem copy. Any non-mem cursor use
|
||||
// leaves stkcursor past the mem words. Mirror of the cgcall
|
||||
// caller-side check; loud-stop (rule 7).
|
||||
if (memwords > 0 && stkcursor != memwords) {
|
||||
let mp: str = "#38b: >48B tagged param mixed with stack-spilled params unwired\n";
|
||||
os.write(2, mp.ptr, mp.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
};
|
||||
|
||||
fn cgfn(c: *cgen, fn_: *node) void = {
|
||||
|
||||
Reference in New Issue
Block a user