w6c+selfhost: widen concrete variant to tagged-union call arg

Tagged-union widening already fired for `let r: (str|rune) = "...";`,
`r = "...";`, and `return "..."` from a tagged-returning fn — but not
at call sites, so `fn f(x: (str|rune))` couldn't be called with a bare
str or rune. The arg was pushed as its own static type (2 words for
str, 1 for rune) while the callee's slot expected 3 (tag + payload).

C cgen: at the call boundary, look up the callee's declared param
type per arg. When the param is TY_TAGGED and the arg is a concrete
variant, materialise (tag, value-words, padding) sized to the param's
tagged_arg_size — then the existing pop-into-arg-regs logic picks it
up. Nullable `(*T | void)` collapses to a single 8B push.

selfhost: fnret now carries the params head alongside rtype (amalloc
bumped to 48); pushargsrev takes the matching param node and runs the
same widening sequence per arg. The pop drain in cgcall already
handled extra slot words, so no change needed on that side.

Verified with a smoke covering str/rune literals, typed locals,
pre-existing tagged-local pass-through, and nullable widening from a
raw pointer. Selfhost emits byte-identical asm to C cgen on the test.
This commit is contained in:
2026-05-13 04:44:03 +09:00
parent 6e7c9e0df4
commit 47d75d9b59
6 changed files with 374 additions and 15 deletions

View File

@@ -2064,6 +2064,35 @@ cgexpr(Cg *c, Node *n, Local *locals)
Node *args[16] = {0};
for (Node *a = n->list; a; a = a->next)
if (argcount < 16) args[argcount++] = a;
/* Resolve callee fn-type so we can match each arg against
* its declared parameter type — needed to detect implicit
* widening of a concrete variant into a tagged-union slot. */
Type *callee_t = n->lhs ? n->lhs->type : NULL;
Type *cu = (callee_t && callee_t->kind == TY_NAMED) ?
callee_t->under : callee_t;
Tparam *callee_params = (cu && cu->kind == TY_FN) ?
cu->params : NULL;
/* widen[i]: param is tagged, arg is a concrete variant.
* widen_sz[i]: param's tagged slot size (8/16/24).
* widen_param[i]: param type (for tag-index lookup). */
int widen[16] = {0};
int widen_sz[16] = {0};
Type *widen_param[16] = {0};
{
Tparam *p = callee_params;
for (int i = 0; i < argcount; i++) {
if (p == NULL) break;
Type *at = args[i] ? args[i]->type : NULL;
int psz = tagged_arg_size(p->type);
int arg_tagged = tagged_arg_size(at) > 0;
if (psz > 0 && !arg_tagged) {
widen[i] = 1;
widen_sz[i] = psz;
widen_param[i] = p->type;
}
p = p->next;
}
}
/* eval right-to-left, push to stack */
for (int i = argcount - 1; i >= 0; i--) {
if (node_isslice(args[i]) && args[i]->kind == N_IDENT) {
@@ -2150,6 +2179,46 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
continue;
}
if (widen[i]) {
/* Concrete → tagged-union widening at the call
* site. Mirrors the let/assign/return widening:
* synthesise tag from the arg's static variant
* type, evaluate the arg, lay it out as the
* parameter's tagged slot, then push high→low so
* pop drains tag first. */
int tag = cg_tag_for_variant(widen_param[i],
args[i]->type);
if (tag < 0) tag = 0;
int sz = widen_sz[i];
if (sz == 8) {
/* Nullable fold: the pointer value IS the
* discriminator — no separate tag word. */
cgexpr(c, args[i], locals);
ins1(c, A_PUSHQ, areg(D_AX));
continue;
}
cgexpr(c, args[i], locals);
if (node_isstr(args[i])) {
/* slot 24: [+0]=tag,[+8]=ptr,[+16]=len */
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
} else {
/* Scalar variant: single value word at +8.
* Pad a zero high word when the slot is 24B
* (some other variant of the union is 16B). */
if (sz > 16) {
ins2(c, A_XORQ, areg(D_DX),
areg(D_DX));
ins1(c, A_PUSHQ, areg(D_DX));
}
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
continue;
}
cgexpr(c, args[i], locals);
if (node_isfloat(args[i])) {
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
@@ -2179,6 +2248,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
* responsible for cleaning them up after CALL. */
int ii = 0, fi = 0, stackslots = 0;
for (int i = 0; i < argcount; i++) {
if (widen[i]) {
/* Pop widened tagged slot into arg-register
* class — sized by the parameter's tagged slot,
* not the arg's static type. */
int eb = widen_sz[i] / 8;
for (int k = 0; k < eb; k++) {
if (ii < 6)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
else
stackslots++;
}
continue;
}
if (node_isfloat(args[i])) {
if (fi < 8) {
ins2(c, A_MOVSD, amem(D_SP, 0),
@@ -2232,9 +2315,6 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* SysV: variadic callees require AL to hold the count of
* XMM regs used in the variable portion. We don't pass
* floats yet, so AL=0 covers every case we emit. */
Type *callee_t = n->lhs ? n->lhs->type : NULL;
Type *cu = (callee_t && callee_t->kind == TY_NAMED) ?
callee_t->under : callee_t;
if (cu && cu->kind == TY_FN && cu->variadic)
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
if (n->lhs->kind == N_IDENT) {