wcc: tuple-param ABI via SSE/GP arg cursors (#163)

Tuples were unhandled as parameters — no tuple arm in arg-push, arg-pop, or
callee-recv in either stage — so a tuple param fell to the 1-GP-word else and
dropped all but its first element (integer tuple params too; floats doubly
lost). Add tuple-param arms (SEND push+pop, callee RECV) across both stages,
reusing #164's per-element SysV classify with the 6-GP (DI,SI,DX,CX,R8,R9) +
8-SSE (X0-X7) arg cursors. A frame slot @tupargscr decouples the producing
call's return cursor from the overlapping arg cursor (capture-before-clobber).
Overflow (>6 GP / >8 SSE) fails loud (rule 7). Scoped to the N_CALL producer;
first-class tuple values (ident/literal) remain a separate unimplemented gap.

Gate-blind (the bootstrap passes no tuple params) — covered by table-driven
probe 905, which proves pre-fix element-drop and the loud-stop.
This commit is contained in:
2026-05-27 22:13:46 +09:00
parent 153c7b3b46
commit 0465c423c1
8 changed files with 986 additions and 3 deletions

View File

@@ -46,6 +46,17 @@ static int cg_retscr;
* @retscr single-slot convention + wwstage's `@tupfscr` '@'-prefix dedup;
* 0 means "not yet allocated". */
static int cg_tupfscr;
/* Per-fn @tupargscr staging slot (#163, single-slot SSoT). A tuple
* PASSED AS AN ARGUMENT (the param twin of #164's tuple return) is left
* by its producing call in the return-ABI cursor (AX/DX/CX/R8 + X0/X1);
* the SEND restages it into this slot positionally (tuple_store), then
* pushes the slot words onto the stack so the pop drains them into the
* SysV ARG cursor (DI/SI/.. + X0..X7). A frame slot decouples the
* return-class regs (which overlap the arg-class regs) from the arg
* placement. Reused per tuple arg (drained to the stack before the next
* arg); 0 means "not yet allocated", cg_tupargscr_sz the cached width. */
static int cg_tupargscr;
static int cg_tupargscr_sz;
/* Per-fn @-prefix scratch SSoT (task #26, follow-up to #15-cstage's
* @retscr). Pre-#26 each site allocated a labelseq-stamped fresh slot
* per call (mklabel "tagbase" / "tagscr" / "argscr" / "idxscr"); the
@@ -191,6 +202,22 @@ node_isslice(Node *n)
return n && type_isslice(n->type);
}
/* node_tuplearg — the underlying TY_TUPLE Type of a tuple-typed argument
* VALUE, else NULL. #163: scoped to an N_CALL producer — the only form
* that leaves a tuple in the return-ABI cursor (AX/DX/CX/R8 + X0/X1, per
* #164). A tuple ident / literal as a first-class value is a separate
* unimplemented gap (`let t = (1,2)` does not materialise a slot today),
* so the SEND restricts to the call form and loud-stops the rest rather
* than push stale registers (rule 7, never a silent drop). */
static Type *
node_tuplearg(Node *n)
{
if (n == NULL || n->kind != N_CALL) return NULL;
Type *t = n->type;
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
return (u && u->kind == TY_TUPLE) ? u : NULL;
}
/* #83: positional tuple register-return ABI. Tuple elements ride
* consecutive eightbytes over tuple_rseq[]; a slice/str rides its 3-word
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8, ty_str->size SSoT), a
@@ -5411,6 +5438,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
continue;
}
cgexpr(c, args[i], locals);
Type *tuparg_push = node_tuplearg(args[i]);
if (node_isfloat(args[i])) {
/* f32 spills 4B (MOVSS), f64 8B (MOVSD): the SysV
* float class drives the width per ref/qbe
@@ -5449,6 +5477,72 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (sz > 8)
ins1(c, A_PUSHQ, areg(D_DX));
ins1(c, A_PUSHQ, areg(D_AX));
} else if (tuparg_push) {
/* #163: tuple ARG (param twin of #164's return).
* cgexpr above left the tuple in the return-ABI
* cursor; restage it into @tupargscr by SysV class
* (tuple_store, the #164 helper), then push the slot
* words high→low so the pop drains slot+0 first into
* the ARG cursor. The frame slot decouples the
* return-class regs (AX/DX/CX/R8 + X0/X1) from the
* overlapping arg-class regs (DI/SI/.. + X0..X7). */
int gpcur = 0, ssecur = 0, eoff = 0, ef32;
int gptot = 0, sstot = 0, tsz = 0;
for (Tparam *p = tuparg_push->params; p; p = p->next) {
Type *pu = (p->type
&& p->type->kind == TY_NAMED)
? p->type->under : p->type;
int wide = pu && (pu->kind == TY_SLICE
|| pu->kind == TY_STR);
if (fld_isfloat(p->type, &ef32))
sstot++;
else
gptot += tuple_ebytes(wide);
/* slot stride per element (sum == tuple slot
* size); matches the wwstage slotsize() walk so
* the @tupargscr width + reverse-push count agree
* byte-for-byte. */
tsz += wide ? (int)pu->size : 8;
}
/* The producing call already satisfied #164's
* return caps; guard anyway (tuple_store indexes
* tuple_rseq[4] / tuple_sse_seq[2]). */
if (gptot > (int)(sizeof tuple_rseq
/ sizeof tuple_rseq[0])
|| sstot > (int)(sizeof tuple_sse_seq
/ sizeof tuple_sse_seq[0]))
fatal("tuple arg exceeds return-cursor ABI "
"capacity; see #163/#164");
if (cg_tupargscr == 0) {
cg_tupargscr = local_alloc(c, &locals,
"@tupargscr", tsz, cg_frame);
cg_tupargscr_sz = tsz;
} else if (tsz > cg_tupargscr_sz) {
fatal("cgcall: @tupargscr cached sz %d, "
"need %d (pinned offset can't grow; "
"#163)", cg_tupargscr_sz, tsz);
}
for (Tparam *p = tuparg_push->params; p; p = p->next) {
Type *pu = (p->type
&& p->type->kind == TY_NAMED)
? p->type->under : p->type;
int wide = pu && (pu->kind == TY_SLICE
|| pu->kind == TY_STR);
int isflt = fld_isfloat(p->type, &ef32);
tuple_store(c, p->type, wide, gpcur, ssecur,
cg_tupargscr + eoff);
if (isflt)
ssecur++;
else
gpcur += tuple_ebytes(wide);
eoff += wide ? (int)pu->size : 8;
}
for (int w = tsz - 8; w >= 0; w -= 8) {
ins2(c, A_MOVQ,
amem(D_BP, cg_tupargscr + w),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
} else {
ins1(c, A_PUSHQ, areg(D_AX));
}
@@ -5505,6 +5599,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
* the callee via positive offsets from BP. The caller is
* responsible for cleaning them up after CALL. */
int ii = (sret_call_sz > 0) ? 1 : 0, fi = 0, stackslots = 0;
Type *tu;
for (int i = 0; i < argcount; i++) {
if (widen[i]) {
/* Pop widened tagged slot into arg-register
@@ -5567,6 +5662,47 @@ cgexpr(Cg *c, Node *n, Local *locals)
else
stackslots++;
}
} else if ((tu = node_tuplearg(args[i])) != NULL) {
/* #163: drain the tuple's staged words (pushed
* slot+0 first) into the SysV arg cursor by SysV
* class — a float MOVSD/MOVSS off (SP) into the
* next XMM (X0..X7), everything else POPQ into the
* next INTEGER arg reg (DI/SI/..); a slice/str its
* 3-word {ptr,len,cap}. Reg overflow loud-stops
* (rule 7): the partial-spill stitch is out of
* scope (twin of #164's cap). */
int ef32;
for (Tparam *p = tu->params; p; p = p->next) {
Type *pu = (p->type
&& p->type->kind == TY_NAMED)
? p->type->under : p->type;
int wide = pu && (pu->kind == TY_SLICE
|| pu->kind == TY_STR);
if (fld_isfloat(p->type, &ef32)) {
if (fi >= 8)
fatal("tuple arg float "
"element overflows SSE "
"arg regs (X0..X7); "
"stitch out of scope, "
"see #163");
ins2(c, ef32 ? A_MOVSS : A_MOVSD,
amem(D_SP, 0),
areg(sysv_fargregs[fi]));
ins2(c, A_ADDQ, aimm(8),
areg(D_SP));
fi++;
continue;
}
int eb = tuple_ebytes(wide);
if (ii + eb > 6)
fatal("tuple arg element "
"overflows integer arg regs "
"(DI/SI/DX/CX/R8/R9); stitch "
"out of scope, see #163");
for (int k = 0; k < eb; k++)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
}
} else {
if (ii < 6) {
ins1(c, A_POPQ, areg(sysv_argregs[ii]));
@@ -8513,6 +8649,8 @@ cgfn(Cg *c, FILE *out, Node *fn)
cg_ret_type = fn->type ? fn->type->ret : NULL;
cg_retscr = 0;
cg_tupfscr = 0;
cg_tupargscr = 0;
cg_tupargscr_sz = 0;
cg_tagbase = 0;
cg_tagbase_sz = 0;
cg_tagscr = 0;
@@ -8575,6 +8713,55 @@ cgfn(Cg *c, FILE *out, Node *fn)
int is_tagged = tagged_sz > 0;
int isf = cg_isfloat(pt);
/* #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
* (DI/SI/..); a slice/str its 3-word {ptr,len,cap} header — and
* store each into the param's frame slot positionally (eoff
* steps by the element's slot width: a slice/str 24B, else 8B,
* matching the tuple-field-access offset walk + the SEND). Reg
* overflow loud-stops (rule 7), the partial-spill stitch out of
* scope (twin of #164's cap). Placed before the single-class
* eightbytes logic below, which can't model a mixed GP/SSE
* aggregate. */
if (pu && pu->kind == TY_TUPLE) {
int sz = (int)pu->size;
int off = localoff(c, &locals, p->str, sz, &frame);
int eoff = 0, ef32;
for (Tparam *te = pu->params; te; te = te->next) {
Type *teu = (te->type
&& te->type->kind == TY_NAMED)
? te->type->under : te->type;
int wide = teu && (teu->kind == TY_SLICE
|| teu->kind == TY_STR);
if (fld_isfloat(te->type, &ef32)) {
if (fargi >= 8)
fatal("tuple param float element "
"overflows SSE arg regs "
"(X0..X7); stitch out of "
"scope, see #163");
ins2(c, ef32 ? A_MOVSS : A_MOVSD,
areg(sysv_fargregs[fargi]),
amem(D_BP, off + eoff));
fargi++;
eoff += 8;
continue;
}
int eb = tuple_ebytes(wide);
if (argi + eb > 6)
fatal("tuple param element overflows "
"integer arg regs (DI/SI/DX/CX/R8/"
"R9); stitch out of scope, see #163");
for (int k = 0; k < eb; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + eoff + k * 8));
eoff += wide ? (int)teu->size : 8;
}
if (tp) tp = tp->next;
continue;
}
/* Args overflowing register classes live at positive offsets
* from BP (16 + i*8). We register them as Locals at those
* offsets, no spill needed. */