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

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