From 32063d0da046d62efdfe681afbd55850e99149f6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 09:36:15 +0900 Subject: [PATCH] =?UTF-8?q?wcc+w6c=5Fww:=20>48B=20tagged=20by-value=20args?= =?UTF-8?q?=20=E2=80=94=20MEMORY-class=20two-phase=20push=20(#38b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 + cmd/w6c/cgen.c | 161 ++++++- selfhost/cmd/w6c/main.combined.ww | 235 ++++++++++- selfhost/cmd/wcc/cgendecl.ww | 30 +- selfhost/cmd/wcc/cgenexpr.ww | 52 ++- selfhost/cmd/wcc/cgenutil.ww | 153 ++++++- selfhost/cmd/wwdump/main.combined.ww | 235 ++++++++++- test/wcc/929_tagged_memarg_run.c | 609 +++++++++++++++++++++++++++ 8 files changed, 1398 insertions(+), 84 deletions(-) create mode 100644 test/wcc/929_tagged_memarg_run.c diff --git a/Makefile b/Makefile index 62f05651..27fb1a1e 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 2813a074..c47c29ef 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 8ebc41d8..12b22d97 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 = { diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 341ff4b9..8bc450d3 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -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 = { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index ae6d01bf..acdd42fa 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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). diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 845d7425..860fad16 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4cf925ae..fea5a094 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 = { diff --git a/test/wcc/929_tagged_memarg_run.c b/test/wcc/929_tagged_memarg_run.c new file mode 100644 index 00000000..7a2b92da --- /dev/null +++ b/test/wcc/929_tagged_memarg_run.c @@ -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 +#include +#include +#include +#include +#include + +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; +}