w6c+selfhost+lib: Hare-style variadic call sites

Param-decl `name: T...` (Tparam.variadic=1, type []T), call-site
gather of N args into a fresh `[N]T`, forward via `xs...`, full
selfhost mirror, and lib/fmt graduated to the Hare shape.

Frontend:
  - parse: `T...` after a param's type stamps Node.op=TK_ELLIPSIS
    and breaks out (variadic must be last).
  - check: resolve_type N_TFN / build_fn_type wrap the param type
    as []T and set tp->variadic. N_CALL accepts either a tail of
    args assignable to T (gather) or a single `xs...` spread of
    []T (forward); both bypass the "too many args" check on the
    variadic slot.
  - type: type_eq compares Tparam.variadic.

Cgen (cstage):
  - call site: when the callee has a variadic last param,
    materialise the tail args into a frame-resident `[N]T` via
    localoff, write a 24B slice descriptor (ptr,len,cap), and
    splice a synthesised N_IDENT into args[] so the downstream
    widen/eval/pop loops see one slice slot. Tagged-element types
    route each store through cg_widen_tagged_store. Forwarding
    skips gather: the N_SPREAD wrapper is replaced with its inner
    slice expression. Empty form writes {nil,0,0}. args[] / widen[]
    bump from 16 to 64 to accommodate Hare's mixed-arg printers.

Selfhost mirror:
  - lib/ww/parse: `T...` mark on N_PARAM.op.
  - cgen: varargseq counter on Cg; scanlocals reserves
    @vararg_d_N + @vararg_sl_N per variadic call (seq recorded on
    N_CALL.uval so cgcall picks the same names). cgcall does the
    same gather/forward and N_IDENT splice. cgfnparams treats
    variadic params as 24B slice slots via a synthesised TSLICE
    tnode. pushargsrev skips the tagged-widen detection for
    variadic params (effective type is []T, not tagged).
  - rhstargetname now recognises N_TRUE/N_FALSE/N_RUNELIT and
    typed N_INTLIT so the variant-tag lookup finds bool/rune/iN
    variants instead of falling through to "first non-str" (which
    misassigned tag 0 to bool in tagged unions like formattable).

lib/fmt graduated: print/println/fprint/fprintln/errorln/fatal
take `args: formattable...`. Bare `error` (no -ln) is skipped —
the leaf name collides with strconv's `type error = !(invalid |
overflow)` under the driver's flat namespace.

Tests: 5 new e2e rows (plain gather, zero-arg, tagged element,
forwarding, fmt.println end-to-end). lib/CLAUDE.md workaround
paragraph replaced with the Hare-shape description.
This commit is contained in:
2026-05-13 08:56:01 +09:00
parent 46edb8db4a
commit b6cf68f2b8
15 changed files with 1327 additions and 140 deletions

View File

@@ -2546,11 +2546,14 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
/* up to 6 integer + 8 float args via SysV registers.
* str args occupy two integer eightbytes (ptr, len). */
* str args occupy two integer eightbytes (ptr, len). The
* arg-buffer cap accommodates Hare-style variadic gather
* (`fmt.println(a, b, c, ...)`) where N args of element
* type T fold into a single []T slice slot below. */
int argcount = 0;
Node *args[16] = {0};
Node *args[64] = {0};
for (Node *a = n->list; a; a = a->next)
if (argcount < 16) args[argcount++] = a;
if (argcount < 64) 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. */
@@ -2559,15 +2562,101 @@ cgexpr(Cg *c, Node *n, Local *locals)
callee_t->under : callee_t;
Tparam *callee_params = (cu && cu->kind == TY_FN) ?
cu->params : NULL;
/* Hare-style variadic last param: gather N tail args into a
* stack-resident []T or forward an `xs...` spread, then
* splice in a single slice arg so the downstream widen/push/
* pop machinery sees one 24B slice slot for the variadic.
*
* Forward shape: `f(... , xs...)` becomes `f(... , xs)`.
* Gather shape: `f(... , e0, e1, eN)` materialises e0..eN
* into a frame-resident `[N]T` (widening each element when T
* is a tagged union), writes a 24B slice descriptor
* {ptr=&data, len=N, cap=N}, and replaces the tail args with
* an N_IDENT pointing at the descriptor. Empty form
* (`f(...)` with no variadic args) writes {0, 0, 0}. */
{
int nfixed = 0;
Tparam *var_p = NULL;
for (Tparam *p = callee_params; p; p = p->next) {
if (p->variadic) { var_p = p; break; }
nfixed++;
}
if (var_p != NULL) {
int nvar = argcount - nfixed;
if (nvar < 0) nvar = 0;
int forwarding = (nvar == 1 && args[nfixed] &&
args[nfixed]->kind == N_SPREAD);
if (forwarding) {
args[nfixed] = args[nfixed]->lhs;
argcount = nfixed + 1;
} else {
Type *vst = var_p->type;
Type *vsu = (vst && vst->kind == TY_NAMED)
? vst->under : vst;
Type *velem = (vsu && vsu->kind == TY_SLICE)
? vsu->sub : NULL;
int esz = (velem && velem->size)
? (int)velem->size : 8;
const char *slname = mklabel(c, "vararg_sl");
int sloff = localoff(c, &locals,
slname, 24, cg_frame);
int doff = 0;
if (nvar > 0) {
const char *dname = mklabel(c, "vararg_d");
doff = localoff(c, &locals,
dname, nvar * esz, cg_frame);
int v_is_tagged = velem &&
tagged_arg_size(velem) > 0;
for (int j = 0; j < nvar; j++) {
Node *a = args[nfixed + j];
int slot = doff + j * esz;
if (v_is_tagged) {
cg_widen_tagged_store(c,
&locals, velem,
a, slot, esz);
continue;
}
cgexpr(c, a, locals);
int op = A_MOVQ;
if (esz == 1) op = A_MOVB;
else if (esz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX),
amem(D_BP, slot));
}
}
if (nvar > 0)
ins2(c, A_LEAQ,
amem(D_BP, doff),
areg(D_AX));
else
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sloff + 0));
ins2(c, A_MOVQ, aimm(nvar),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sloff + 8));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sloff + 16));
Node *sn = newnode(c->a, N_IDENT, n->pos);
sn->str = slname;
sn->strlen = 0;
sn->type = vst;
args[nfixed] = sn;
argcount = nfixed + 1;
}
}
}
/* widen[i]: param is tagged and arg needs re-layout.
* - arg is a concrete variant (str/struct/scalar) — wrap
* in the param's slot shape.
* - arg is itself a tagged union of a subset/different
* variant set — copy the slot words and remap the tag.
* Identical types pass through unchanged. */
int widen[16] = {0};
int widen_sz[16] = {0};
Type *widen_param[16] = {0};
int widen[64] = {0};
int widen_sz[64] = {0};
Type *widen_param[64] = {0};
{
Tparam *p = callee_params;
for (int i = 0; i < argcount; i++) {

View File

@@ -404,7 +404,17 @@ resolve_type(Checker *c, Node *n)
}
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->name = p->str;
tp->type = resolve_type(c, p->lhs);
Type *pt = resolve_type(c, p->lhs);
/* Hare-style `T...` (marked on the param node via
* Node.op == TK_ELLIPSIS): the param's effective type
* inside the callee is []T, and call sites either
* gather N args of type T or forward an `xs...` slice. */
if (p->op == TK_ELLIPSIS) {
tp->variadic = 1;
tp->type = type_slice(c->a, pt);
} else {
tp->type = pt;
}
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
@@ -962,12 +972,38 @@ cexpr(Checker *c, Node *n)
err(c, n->pos, "too many arguments");
continue;
}
/* Hare-style variadic param: every remaining arg either
* - flows into the gather (assignable to element T), or
* - is a single `xs...` spread of `[]T` (forwarding).
* Don't advance p — the variadic slot absorbs the tail. */
if (p->variadic) {
Type *elem = (p->type && p->type->kind == TY_SLICE)
? p->type->sub : ty_err;
if (a->kind == N_SPREAD) {
if (at != ty_err && p->type != ty_err &&
!type_assignable(p->type, at))
err(c, a->pos,
"spread arg: %s not assignable to %s",
type_name(c->a, at),
type_name(c->a, p->type));
if (a->next != NULL)
err(c, a->pos,
"spread arg must be the last");
} else if (elem != ty_err && at != ty_err) {
if (!type_assignable(elem, at))
err(c, a->pos,
"variadic arg: %s not assignable to %s",
type_name(c->a, at),
type_name(c->a, elem));
}
continue;
}
if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err)
err(c, a->pos, "argument type %s not assignable to %s",
type_name(c->a, at), type_name(c->a, p->type));
p = p->next;
}
if (p != NULL)
if (p != NULL && !p->variadic)
err(c, n->pos, "not enough arguments");
return n->type = u->ret ? u->ret : ty_void;
}
@@ -1514,7 +1550,14 @@ build_fn_type(Checker *c, Node *fn)
}
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->name = p->str;
tp->type = resolve_type(c, p->lhs);
Type *pt = resolve_type(c, p->lhs);
/* Hare-style `T...` — see resolve_type N_TFN. */
if (p->op == TK_ELLIPSIS) {
tp->variadic = 1;
tp->type = type_slice(c->a, pt);
} else {
tp->type = pt;
}
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;

View File

@@ -135,9 +135,18 @@ parseparams(Parser *p)
n->strlen = 0;
n->lhs = parsetype(p);
}
/* Hare-style variadic: `name: T...`. The trailing `...`
* after the type promotes the param's type to []T at type-
* resolution time; call sites gather N args or forward a
* `xs...` spread. Marked on n->op so check.c and selfhost
* recognise it without needing a new Node kind. */
if (accept(p, TK_ELLIPSIS))
n->op = TK_ELLIPSIS;
if (head == NULL) head = n;
else tail->next = n;
tail = n;
if (n->op == TK_ELLIPSIS)
break; /* Hare-style variadic must be the last param */
if (!accept(p, TK_COMMA))
break;
if (p->cur.kind == TK_RPAREN) /* trailing comma */

View File

@@ -219,6 +219,7 @@ type_eq(Type *a, Type *b)
if (!type_eq(a->ret, b->ret)) return 0;
Tparam *pa = a->params, *pb = b->params;
while (pa && pb) {
if (pa->variadic != pb->variadic) return 0;
if (!type_eq(pa->type, pb->type)) return 0;
pa = pa->next; pb = pb->next;
}

View File

@@ -403,6 +403,9 @@ struct Tparam {
const char *name;
Type *type;
Tparam *next;
int variadic; /* Hare-style `T...` — `type` is []T,
* call site gathers / forwards. Distinct
* from Type.variadic (C-style FFI `...`). */
};
struct Type {