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:
161
cmd/w6c/cgen.c
161
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
|
||||
|
||||
Reference in New Issue
Block a user