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:
2026-06-04 09:36:15 +09:00
parent e3e6b5a820
commit 32063d0da0
8 changed files with 1398 additions and 84 deletions

View File

@@ -280,6 +280,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_sret_struct_return \
$(BIN)/test_sret_struct_return_run \
$(BIN)/test_tagged_sret_run \
$(BIN)/test_tagged_memarg_run \
$(BIN)/test_tagscr_sizes_run \
$(BIN)/test_is_nonident_run \
$(BIN)/test_match_nonident_idx_run \
@@ -1197,6 +1198,12 @@ $(BIN)/test_tagged_sret_run: test/wcc/926_tagged_sret_run.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_tagged_memarg_run: test/wcc/929_tagged_memarg_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_tagscr_sizes_run: test/wcc/926_tagscr_sizes_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \

View File

@@ -589,6 +589,23 @@ tagged_arg_size(Type *t)
return (int)t->size;
}
/* #38b: a tagged-union arg past the 6-reg register convention (>48B
* slot, where tagged_arg_size returns 0) 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.
* ABI shape per ref/qbe/amd64/sysv.c:80-85 (inmem aggregates) /
* :411-426 (stack blit, left-to-right offsets). The ≤48B register
* convention is pinned in-tree (test/926 boundary rows). */
static int
tagged_memarg_size(Type *t)
{
if (t == NULL) return 0;
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL || t->kind != TY_TAGGED || t->nullable) return 0;
if (t->size <= 48) return 0;
return (int)t->size;
}
/* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */
static int
type_isnullable(Type *t)
@@ -2597,7 +2614,11 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
int src_is_struct = su && su->kind == TY_STRUCT;
int src_is_tagged = su && su->kind == TY_TAGGED;
if (!src_is_struct && !src_is_tagged) {
/* #38b: a MEMORY-class (>48B) dst slot always routes through the
* scratch path — the str/slice fast arms push exactly 4 words,
* short of the slot's msz/8 the mem pre-pass accounts for. */
int dst_is_mem = tagged_memarg_size(dst) > 0;
if (!src_is_struct && !src_is_tagged && !dst_is_mem) {
/* Direct-push fast path: str / slice / scalar / pointer. */
cgexpr(c, src, *locals_p);
int tag = cg_tag_for_variant(du, st);
@@ -7033,6 +7054,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
int sloff = localoff(c, &locals,
slname, (int)vsu->size, cg_frame);
if (nvar > 0) {
/* #38b: a >48B tagged variadic ELEMENT
* would need the memory convention
* inside the vararg gather buffer —
* unwired (rule 7). */
if (velem &&
tagged_memarg_size(velem) > 0)
fatal("#38b: >48B tagged "
"variadic element "
"unwired");
int v_is_tagged = velem &&
tagged_arg_size(velem) > 0;
int v_is_str = type_isstr(velem);
@@ -7109,6 +7139,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
int widen[64] = {0};
int widen_sz[64] = {0};
Type *widen_param[64] = {0};
int memarg[64] = {0};
{
Tparam *p = callee_params;
for (int i = 0; i < argcount; i++) {
@@ -7127,14 +7158,88 @@ cgexpr(Cg *c, Node *n, Local *locals)
widen_param[i] = p->type;
}
}
/* #38b: MEMORY-class param (>48B tagged) —
* same widen detection, memory transport. */
int msz = tagged_memarg_size(p->type);
if (msz > 0) {
memarg[i] = msz;
Type *pu = (p->type && p->type->kind == TY_NAMED)
? p->type->under : p->type;
Type *au = (at && at->kind == TY_NAMED)
? at->under : at;
int same = (pu == au) || type_eq(p->type, at);
if (!same) {
widen[i] = 1;
widen_sz[i] = msz;
widen_param[i] = p->type;
}
}
p = p->next;
}
/* #38b: exact-type >48B tagged arg with no declared
* param to key off (fn-ptr callee, param-list
* mismatch) — MEMORY-class by the arg's own stamped
* type. */
for (int i = 0; i < argcount; i++)
if (!memarg[i] && args[i])
memarg[i] =
tagged_memarg_size(args[i]->type);
}
/* #38b MEMORY-class pre-pass: stage every >48B tagged arg on
* the stack BELOW all register-class words (rightmost-first,
* so the leftmost mem arg lands at the lowest address = the
* callee's first positive-BP cursor slot at 16(BP)). The pop
* loop below drains a strict prefix of the stack, so the mem
* copies are never popped; the caller-cleanup ADDQ reclaims
* them with the spill slots after CALL. Layout per
* ref/qbe/amd64/sysv.c:411-426 (stack blit, left-to-right). */
int memslots = 0;
for (int i = argcount - 1; i >= 0; i--) {
if (!memarg[i]) continue;
int msz = memarg[i];
if (widen[i]) {
cg_widen_tagged_push(c, &locals,
widen_param[i], args[i], widen_sz[i]);
memslots += widen_sz[i] / 8;
continue;
}
if (args[i]->kind == N_CALL)
fatal("#38b: sret-class tagged call result "
"as a >48B by-value arg unwired "
"(#40-family follow-up)");
if (args[i]->kind == N_IDENT) {
int off = localfind(locals, args[i]->str);
if (off != 0) {
for (int k = msz/8 - 1; k >= 0; k--) {
ins2(c, A_MOVQ,
amem(D_BP, off + k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
memslots += msz / 8;
continue;
}
}
if (aggarg_srcaddr(c, args[i], D_SI, locals)) {
for (int k = msz/8 - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_SI, k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
memslots += msz / 8;
continue;
}
fatal("#38b: >48B tagged arg from unsupported source "
"kind %d (slice-element and rvalue sources "
"unwired)", args[i]->kind);
}
/* eval right-to-left, push to stack. Each N_IDENT fast-path
* is guarded by !widen[i] so the tagged-union widening (which
* needs to synthesise tag + payload + pad) takes precedence
* over the verbatim slice/struct/tagged-ident loads below. */
for (int i = argcount - 1; i >= 0; i--) {
if (memarg[i]) /* #38b: staged by the mem pre-pass */
continue;
if (!widen[i] && node_isslice(args[i]) && args[i]->kind == N_IDENT) {
int off = localfind(locals, args[i]->str);
/* push cap, len, ptr (top) so pops give ptr,len,cap */
@@ -7392,20 +7497,6 @@ cgexpr(Cg *c, Node *n, Local *locals)
args[i], widen_sz[i]);
continue;
}
/* #38b residual (rule 7): a tagged arg slot past the
* 6-reg arg capacity has no push shape —
* tagged_arg_size returns 0 ("too large") and the
* scalar default silently pushed ONE word. Loud-stop;
* symmetric ww gate in pushargsrev. */
{
Type *au = type_chase_named(args[i]->type);
if (au && au->kind == TY_TAGGED
&& !au->nullable
&& tagged_arg_size(args[i]->type) == 0)
fatal("#38b: tagged arg exceeds the "
"register arg capacity (>48B "
"slot) — unwired");
}
cgexpr(c, args[i], locals);
Type *tuparg_push = node_tuplearg(args[i]);
if (node_isfloat(args[i])) {
@@ -7583,6 +7674,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
int ii = (sret_call_sz > 0) ? 1 : 0, fi = 0, stackslots = 0;
Type *tu;
for (int i = 0; i < argcount; i++) {
if (memarg[i]) /* #38b: stays on the stack */
continue;
if (widen[i]) {
/* Pop widened tagged slot into arg-register
* class — sized by the parameter's tagged slot,
@@ -7758,6 +7851,17 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
}
/* #38b: MEMORY-class args and register-overflow spill words
* cannot coexist — the callee's positive-BP cursor walks
* params in declaration order, but the caller's residual
* region puts spilled register-class words below every mem
* copy. Loud-stop (rule 7); the callee prologue holds the
* mirror check. The merged count feeds the caller-cleanup
* ADDQ after CALL. */
if (memslots > 0 && stackslots > 0)
fatal("#38b: >48B tagged arg mixed with register-"
"overflow stack args unwired");
stackslots += memslots;
/* sret hidden first-arg (#23): load &dest into RDI AFTER
* all user-arg pops have finished — the pop loop started
* its int-arg cursor at 1, so RDI was never written.
@@ -11614,6 +11718,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
* separately from integer DI/SI/DX/CX/R8/R9). */
int argi = (cg_sret_arg_off != 0) ? 1 : 0;
int fargi = 0;
int memparam_words = 0;
Tparam *tp = fn->type ? fn->type->params : NULL;
for (Node *p = fn->list; p; p = p->next) {
if (p->str == NULL || strcmp(p->str, "...") == 0) {
@@ -11636,6 +11741,23 @@ cgfn(Cg *c, FILE *out, Node *fn)
int is_tagged = tagged_sz > 0;
int isf = cg_isfloat(pt);
/* #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. ref/qbe/amd64/sysv.c:80-85 / :411-426. */
int mem_sz = tagged_memarg_size(pt);
if (mem_sz > 0) {
Local *l = amalloc(c->a, sizeof *l);
l->name = p->str;
l->off = 16 + cg_stack_arg_cursor * 8;
cg_stack_arg_cursor += mem_sz / 8;
memparam_words += mem_sz / 8;
l->next = locals;
locals = l;
if (tp) tp = tp->next;
continue;
}
/* #163: tuple PARAM receive (param twin of #164's return).
* Walk the tuple's elements over the SysV arg cursor — a float
* reads its XMM (X0..X7), everything else an INTEGER arg reg
@@ -11811,6 +11933,15 @@ cgfn(Cg *c, FILE *out, Node *fn)
}
if (tp) tp = tp->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 the cursor past the mem words. Mirror of the cgcall
* caller-side check. */
if (memparam_words > 0 && cg_stack_arg_cursor != memparam_words)
fatal("#38b: >48B tagged param mixed with stack-spilled "
"params unwired");
/* Iterate the fn body's statements directly rather than dispatching
* the outermost N_BLOCK through cgstmt — N_BLOCK now save/restores

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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).

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -0,0 +1,609 @@
/*
* 929_tagged_memarg_run — >48B tagged by-value ARGS (#38b): a tagged
* arg whose slot exceeds the 6-reg register convention (48B) is
* MEMORY-class — the caller stages the whole slot on the outgoing
* stack below every register-class word, the callee reads it in
* place at positive BP offsets, and the caller-cleanup ADDQ reclaims
* it after CALL. ABI shape per ref/qbe/amd64/sysv.c:80-85 (inmem) /
* :411-426 (stack blit).
*
* Pre-fix the exact-typed arg loud-stopped on both stages (the
* designed #38b guard), but the WIDENED concrete source into a >48B
* param slipped past the guard SILENTLY: cstage pushed one scalar
* word and received a 1-word scalar param while wwstage emitted an
* uncapped greedy stitch — silently wrong on both AND cs≠ww
* (gate-blind: no in-tree >48B call existed).
*
* Three checks per row:
* - byte-id: w6c vs w6c_ww .s identical (rule 10). Rows avoid the
* pre-existing match-on-tagged-struct-field divergence class by
* keeping payloads all-scalar; the inst-shaped row dispatches on
* the outer tag only.
* - asm markers: `needs` pins the mem cleanup ADDQ (the memory-
* class signature); `rejects` pins the 48B boundary row stays
* register-convention (no cleanup) — an off-by-one in
* tagged_memarg_size would flip every 48B-slot call in the tree.
* - runtime: build via ww / ww_ww and run; exit codes read the tag
* AND the late payload words (slot offsets +48/+56, past the old
* register cap) FIRST so their loss is the visible failure.
* Rows flagged `buildfail` must be rejected by BOTH stages with that
* row's EXACT #38b diagnostic — pinning WHICH guard fired (the
* remaining unwired sub-shapes: sret-class call source, global-let
* source, variadic element, register-overflow mixing caller- and
* callee-side).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row {
const char *label;
const char *src;
int want; /* expected exit code (run rows) */
const char *needs; /* .s must contain (NULL: skip) */
const char *rejects; /* .s must NOT contain (NULL: skip) */
int buildfail; /* 1: both stages must loud-stop */
const char *failmark; /* exact diagnostic both stages must emit
* — pins WHICH #38b guard fired, so one
* guard cannot silently cover for
* another's regression. */
};
/* 56B slot: 48B all-scalar payload + tag. m3 is the late word at slot
* offset +48 — dead under the old 6-reg cap. */
#define MEM56_TYPES \
"type t_lit = rune;\n" \
"type t_any = void;\n" \
"type t_rep = struct { id: size, origin: size, m0: size,\n" \
" m1: size, m2: size, m3: size };\n" \
"type t_u = (t_lit | t_any | t_rep);\n"
/* Branched callee: reads the tag AND the late payload words per arm,
* late word FIRST. A single-return callee would mask wrong word
* routing by coincidence. */
#define MEM56_PROBE \
"fn probe(a: t_u) i32 = {\n" \
" match (a) {\n" \
" case let l: t_lit => {\n" \
" if (l == 'x') { return 1; };\n" \
" return 91;\n" \
" };\n" \
" case t_any => { return 2; };\n" \
" case let r: t_rep => {\n" \
" if (r.m3 != 1234) { return 92; };\n" \
" if (r.m2 != 12) { return 93; };\n" \
" if (r.id != 7) { return 94; };\n" \
" return 3;\n" \
" };\n" \
" };\n" \
" return 99;\n" \
"};\n"
#define MEM56_REP_LIT \
"t_rep { id = 7, origin = 9, m0 = 10, m1 = 11, m2 = 12,\n" \
" m3 = 1234 }"
static const struct row rows[] = {
/* Exact-type local-ident source, every variant exercised. */
{ "mem56_ident_allvariants",
MEM56_TYPES
MEM56_PROBE
"export fn main() i32 = {\n"
" let a: t_u = ('x': t_lit);\n"
" if (probe(a) != 1) { return 1; };\n"
" let av: t_any;\n"
" let b: t_u = av;\n"
" if (probe(b) != 2) { return 2; };\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let cc: t_u = r;\n"
" if (probe(cc) != 3) { return 3; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* 64B slot — one word wider; late word at +56. */
{ "mem64_ident_lateword",
"type t_lit = rune;\n"
"type t_rep = struct { id: size, origin: size, m0: size,\n"
" m1: size, m2: size, m3: size, m4: size };\n"
"type t_u = (t_lit | t_rep);\n"
"fn probe(a: t_u) i32 = {\n"
" match (a) {\n"
" case let l: t_lit => {\n"
" if (l == 'y') { return 1; };\n"
" return 91;\n"
" };\n"
" case let r: t_rep => {\n"
" if (r.m4 != 7777) { return 92; };\n"
" if (r.m3 != 5) { return 93; };\n"
" return 2;\n"
" };\n"
" };\n"
" return 99;\n"
"};\n"
"export fn main() i32 = {\n"
" let a: t_u = ('y': t_lit);\n"
" if (probe(a) != 1) { return 1; };\n"
" let r: t_rep = t_rep { id = 1, origin = 2, m0 = 3,\n"
" m1 = 4, m2 = 5, m3 = 5, m4 = 7777 };\n"
" let b: t_u = r;\n"
" if (probe(b) != 2) { return 2; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$64, SP\n", NULL, 0, NULL },
/* Widened concrete sources at the call site — the sub-shape
* that slipped the old guard SILENTLY (cs 1-word scalar vs ww
* greedy stitch, both wrong). Scalar cast, void, struct local. */
{ "mem56_widen_concrete",
MEM56_TYPES
MEM56_PROBE
"export fn main() i32 = {\n"
" if (probe('x': t_lit) != 1) { return 1; };\n"
" let av: t_any;\n"
" if (probe(av) != 2) { return 2; };\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" if (probe(r) != 3) { return 3; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* Tagged SUBSET source widened into the 56B slot (tag remap). */
{ "mem56_subset_remap",
MEM56_TYPES
"type t_sub = (t_lit | t_any);\n"
MEM56_PROBE
"export fn main() i32 = {\n"
" let s: t_sub = ('x': t_lit);\n"
" if (probe(s) != 1) { return 1; };\n"
" let s2: t_sub = (void: t_any);\n"
" if (probe(s2) != 2) { return 2; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* Exact-type non-ident addressable sources: array element,
* struct field, pointer deref (the aggarg_srcaddr shapes). */
{ "mem56_srcshapes",
MEM56_TYPES
"type holder = struct { k: i64, u: t_u };\n"
MEM56_PROBE
"export fn main() i32 = {\n"
" let arr: [2]t_u = [\n"
" ('x': t_lit): t_u,\n"
" ('x': t_lit): t_u,\n"
" ];\n"
" if (probe(arr[1]) != 1) { return 1; };\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let h: holder = holder { k = 9, u = r };\n"
" if (probe(h.u) != 3) { return 2; };\n"
" let x: t_u = ('x': t_lit);\n"
" let p: *t_u = &x;\n"
" if (probe(*p) != 1) { return 3; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* Mem arg mixed with register-class args, both orders — the
* mem copy must not disturb the int/str register cursors. */
{ "mem56_mixed_orders",
MEM56_TYPES
"fn before(k: i64, s: str, a: t_u) i64 = {\n"
" if (k != 5) { return 91; };\n"
" if (s.len != 3) { return 92; };\n"
" match (a) {\n"
" case let r: t_rep => { return k + (r.m3: i64); };\n"
" case => { return 93; };\n"
" };\n"
" return 99;\n"
"};\n"
"fn after(a: t_u, k: i64, s: str) i64 = {\n"
" if (k != 6) { return 91; };\n"
" if (s.len != 3) { return 92; };\n"
" match (a) {\n"
" case let r: t_rep => { return k + (r.m3: i64); };\n"
" case => { return 93; };\n"
" };\n"
" return 99;\n"
"};\n"
"export fn main() i32 = {\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let a: t_u = r;\n"
" if (before(5, \"abc\", a) != 1239) { return 1; };\n"
" if (after(a, 6, \"abc\") != 1240) { return 2; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* Call in a tight loop: a leaked stack copy (missing cleanup)
* skews SP long before 200k iterations. */
{ "mem56_call_in_loop",
MEM56_TYPES
"fn grab(a: t_u) i64 = {\n"
" match (a) {\n"
" case let r: t_rep => { return r.m3: i64; };\n"
" case => { return -1; };\n"
" };\n"
" return -2;\n"
"};\n"
"export fn main() i32 = {\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let a: t_u = r;\n"
" let canary: i64 = 4242;\n"
" let sum: i64 = 0;\n"
" let i: i64 = 0;\n"
" for (i < 200000) {\n"
" sum += grab(a) - 1234;\n"
" i += 1;\n"
" };\n"
" if (canary != 4242) { return 2; };\n"
" if (sum != 0) { return 1; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* TWO mem args in one call (56B + 64B): pins the left-to-right
* outgoing layout (leftmost mem arg at 16(BP)). */
{ "mem56_twomem",
MEM56_TYPES
"type w_rep = struct { id: size, origin: size, m0: size,\n"
" m1: size, m2: size, m3: size, m4: size };\n"
"type w_u = (t_lit | w_rep);\n"
"fn both(a: t_u, b: w_u) i64 = {\n"
" let x: i64 = 0;\n"
" match (a) {\n"
" case let r: t_rep => { x = r.m3: i64; };\n"
" case => { return 91; };\n"
" };\n"
" match (b) {\n"
" case let r: w_rep => { return x + (r.m4: i64); };\n"
" case => { return 92; };\n"
" };\n"
" return 99;\n"
"};\n"
"export fn main() i32 = {\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let a: t_u = r;\n"
" let w: w_rep = w_rep { id = 1, origin = 2, m0 = 3,\n"
" m1 = 4, m2 = 5, m3 = 6, m4 = 2 };\n"
" let b: w_u = w;\n"
" if (both(a, b) != 1236) { return 1; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$120, SP\n", NULL, 0, NULL },
/* Inst-shaped row: the real lib/regex inst layout (48B
* inst_repeat payload with nested (void|size) fields = 56B
* slot). Outer-tag dispatch only — inner-field matches ride the
* pre-existing match-on-tagged-struct-field divergence class,
* out of scope here; the all-scalar rows above pin the late
* payload words. */
{ "mem56_inst_shape",
"type inst_lit = rune;\n"
"type inst_any = void;\n"
"type inst_repeat = struct { id: size, origin: size,\n"
" min: (void | size), max: (void | size) };\n"
"type inst = (inst_lit | inst_any | inst_repeat);\n"
"fn is_consuming(a: inst) bool = {\n"
" return a is inst_lit || a is inst_any;\n"
"};\n"
"fn rep_id(a: inst) i64 = {\n"
" match (a) {\n"
" case let r: inst_repeat => { return r.origin: i64; };\n"
" case => { return -1; };\n"
" };\n"
" return -2;\n"
"};\n"
"export fn main() i32 = {\n"
" let a: inst = ('x': inst_lit);\n"
" if (!is_consuming(a)) { return 1; };\n"
" let r: inst_repeat = inst_repeat { id = 7, origin = 9,\n"
" min = (11: size), max = (1234: size) };\n"
" let b: inst = r;\n"
" if (is_consuming(b)) { return 2; };\n"
" if (rep_id(b) != 9) { return 3; };\n"
" return 0;\n"
"};\n",
0, "\tADDQ\t$56, SP\n", NULL, 0, NULL },
/* BOUNDARY: 40B payload = EXACTLY 48B slot — must stay on the
* register convention (no mem cleanup ADDQ). An off-by-one in
* tagged_memarg_size flips every 48B-slot call in the tree. */
{ "boundary48_register",
"type t_lit = rune;\n"
"type t_rep = struct { id: size, origin: size, m0: size,\n"
" m1: size, m2: size };\n"
"type t_u = (t_lit | t_rep);\n"
"fn probe(a: t_u) i32 = {\n"
" match (a) {\n"
" case let l: t_lit => { return 1; };\n"
" case let r: t_rep => {\n"
" if (r.m2 != 12) { return 92; };\n"
" return 2;\n"
" };\n"
" };\n"
" return 99;\n"
"};\n"
"export fn main() i32 = {\n"
" let a: t_u = ('x': t_lit);\n"
" if (probe(a) != 1) { return 1; };\n"
" let r: t_rep = t_rep { id = 7, origin = 9, m0 = 10,\n"
" m1 = 11, m2 = 12 };\n"
" let b: t_u = r;\n"
" if (probe(b) != 2) { return 2; };\n"
" return 0;\n"
"};\n",
0, NULL, "\tADDQ\t$48, SP\n", 0, NULL },
/* LOUD-STOP: sret-class tagged call result in >48B argument
* position (memory result behind a dest pointer, not a cursor —
* receive-then-push is the #40-family follow-up). */
{ "fail_callsrc",
MEM56_TYPES
MEM56_PROBE
"fn mk() t_u = {\n"
" return ('x': t_lit);\n"
"};\n"
"export fn main() i32 = {\n"
" if (probe(mk()) != 1) { return 1; };\n"
" return 0;\n"
"};\n",
0, NULL, NULL, 1,
"#38b: sret-class tagged call result as a >48B by-value arg "
"unwired (#40-family follow-up)" },
/* LOUD-STOP: global-let source. Global tagged lets have no
* .data emission at ANY size (let_emit_size returns 0 for
* TY_TAGGED; the ≤48B arg path silently reads stack garbage at
* master — independent latent, filed as its own task). The >48B
* ARG path stops loud instead. No assign to g here — the global
* tagged ASSIGN is the other half of that independent bug and
* fails with its own (non-#38b) resolver diagnostic on wwstage
* post-cgplaceaddr. */
{ "fail_global_src",
MEM56_TYPES
"let g: t_u;\n"
MEM56_PROBE
"export fn main() i32 = {\n"
" if (probe(g) != 1) { return 1; };\n"
" return 0;\n"
"};\n",
/* marker stops before the source-kind number — cstage prints
* the numeric node kind, wwstage doesn't. */
0, NULL, NULL, 1,
"#38b: >48B tagged arg from unsupported source kind" },
/* LOUD-STOP: >48B tagged VARIADIC element (memory convention
* inside the vararg gather buffer — unwired). */
{ "fail_variadic_elem",
MEM56_TYPES
"fn v(xs: t_u...) i32 = {\n"
" return len(xs): i32;\n"
"};\n"
"export fn main() i32 = {\n"
" let a: t_u = ('x': t_lit);\n"
" if (v(a) != 1) { return 1; };\n"
" return 0;\n"
"};\n",
0, NULL, NULL, 1,
"#38b: >48B tagged variadic element unwired" },
/* LOUD-STOP: mem arg + register-overflow args in one call (7
* register-class words: 2 strs + int) — the positive-BP layouts
* collide. With the callee BEFORE main, its prologue mirror-
* check fires first (decl-order compilation); the caller-side
* twin is pinned by the next row. */
{ "fail_mem_plus_overflow",
MEM56_TYPES
"fn f(a: t_u, s1: str, s2: str, k: i64) i64 = {\n"
" match (a) {\n"
" case let r: t_rep => {\n"
" return (r.m3: i64) + (s1.len: i64) + (s2.len: i64) + k;\n"
" };\n"
" case => { return 91; };\n"
" };\n"
" return 99;\n"
"};\n"
"export fn main() i32 = {\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let a: t_u = r;\n"
" if (f(a, \"abc\", \"de\", 1) != 1240) { return 1; };\n"
" return 0;\n"
"};\n",
0, NULL, NULL, 1,
"#38b: >48B tagged param mixed with stack-spilled params "
"unwired" },
/* LOUD-STOP: same mixing, main BEFORE the callee — the CALL
* site compiles first, so the caller-side guard fires (the
* row above never reaches it). */
{ "fail_mem_plus_overflow_callsite",
MEM56_TYPES
"export fn main() i32 = {\n"
" let r: t_rep = " MEM56_REP_LIT ";\n"
" let a: t_u = r;\n"
" if (f(a, \"abc\", \"de\", 1) != 1240) { return 1; };\n"
" return 0;\n"
"};\n"
"fn f(a: t_u, s1: str, s2: str, k: i64) i64 = {\n"
" match (a) {\n"
" case let r: t_rep => {\n"
" return (r.m3: i64) + (s1.len: i64) + (s2.len: i64) + k;\n"
" };\n"
" case => { return 91; };\n"
" };\n"
" return 99;\n"
"};\n",
0, NULL, NULL, 1,
"#38b: >48B tagged arg mixed with register-overflow stack "
"args unwired" },
};
static const char *g_bin;
static int
compile_s(const char *tool, const char *src, const char *outpath,
const char *errpath)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "%s/%s %s > %s 2> %s",
g_bin, tool, src, outpath, errpath);
return runwait(cmd);
}
static int
file_eq(const char *a, const char *b)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cmp -s %s %s", a, b);
return runwait(cmd) == 0;
}
static int
file_has(const char *path, const char *needle)
{
FILE *f = fopen(path, "rb");
if (!f) return 0;
static char buf[1 << 20];
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = '\0';
return strstr(buf, needle) != NULL;
}
static int
run_driver(const char *driver, const char *src, const char *label)
{
char tmpdir[128], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/tmemarg_%d_d", getpid());
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s/%s build %s >/dev/null 2>&1",
tmpdir, g_bin, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
label, driver);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[256];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(outbin);
rmdir(tmpdir);
return got;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
static char absbin[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
g_bin = bin;
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char src[128], cs_s[128], ww_s[128];
char cs_e[128], ww_e[128];
snprintf(src, sizeof src, "/tmp/tmemarg_%d_%d.ww",
getpid(), i);
snprintf(cs_s, sizeof cs_s, "/tmp/tmemarg_%d_%d_cs.s",
getpid(), i);
snprintf(ww_s, sizeof ww_s, "/tmp/tmemarg_%d_%d_ww.s",
getpid(), i);
snprintf(cs_e, sizeof cs_e, "/tmp/tmemarg_%d_%d_cs.err",
getpid(), i);
snprintf(ww_e, sizeof ww_e, "/tmp/tmemarg_%d_%d_ww.err",
getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return 1;
fputs("package main;\n\n", f);
fputs(r->src, f);
fclose(f);
int cs_rc = compile_s("w6c", src, cs_s, cs_e);
int ww_rc = compile_s("w6c_ww", src, ww_s, ww_e);
if (r->buildfail) {
total++;
if (cs_rc == 0 || ww_rc == 0) {
fprintf(stderr, "FAIL row[%s]: loud-stop "
"expected, cstage rc=%d wwstage rc=%d\n",
r->label, cs_rc, ww_rc);
fail++;
} else if (!file_has(cs_e, r->failmark)
|| !file_has(ww_e, r->failmark)) {
/* the rejection must be THIS row's exact
* #38b loud-stop, not an unrelated error —
* or another guard — masquerading as
* coverage. */
fprintf(stderr, "FAIL row[%s]: rejected but "
"without the exact diagnostic \"%s\"\n",
r->label, r->failmark);
fail++;
}
unlink(src); unlink(cs_s); unlink(ww_s);
unlink(cs_e); unlink(ww_e);
continue;
}
total++;
if (cs_rc != 0 || ww_rc != 0) {
fprintf(stderr, "FAIL row[%s]: compile rc cs=%d "
"ww=%d\n", r->label, cs_rc, ww_rc);
fail++;
unlink(src); unlink(cs_s); unlink(ww_s);
unlink(cs_e); unlink(ww_e);
continue;
}
if (!file_eq(cs_s, ww_s)) {
fprintf(stderr, "FAIL row[%s]: cs != ww .s\n",
r->label);
fail++;
}
if (r->needs && !file_has(cs_s, r->needs)) {
fprintf(stderr, "FAIL row[%s]: expected mem-cleanup "
"marker missing from .s\n", r->label);
fail++;
}
if (r->rejects && file_has(cs_s, r->rejects)) {
fprintf(stderr, "FAIL row[%s]: boundary row emitted "
"the mem-cleanup marker (classifier off-by-one)\n",
r->label);
fail++;
}
int got_cs = run_driver("ww", src, r->label);
if (got_cs != r->want) {
fprintf(stderr, "FAIL row[%s] cstage: want %d "
"got %d\n", r->label, r->want, got_cs);
fail++;
}
char wwdrv[600];
snprintf(wwdrv, sizeof wwdrv, "%s/ww_ww", g_bin);
if (access(wwdrv, X_OK) == 0) {
int got_ww = run_driver("ww_ww", src, r->label);
if (got_ww != r->want) {
fprintf(stderr, "FAIL row[%s] wwstage: "
"want %d got %d\n",
r->label, r->want, got_ww);
fail++;
}
}
unlink(src); unlink(cs_s); unlink(ww_s);
unlink(cs_e); unlink(ww_e);
}
if (fail) {
fprintf(stderr, "tagged_memarg_run: %d/%d rows failed\n",
fail, total);
return 1;
}
printf("tagged_memarg_run: %d rows ok\n", total);
return 0;
}