diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 3082b2ab..c8ec1094 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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++) { diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 82ea3faa..1e546c01 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 2886ea0e..2f65ff36 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -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 */ diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index 19dbd138..dc165922 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -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; } diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 3d655a0b..94626eb8 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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 { diff --git a/lib/CLAUDE.md b/lib/CLAUDE.md index 3bd3cf89..b527dbb1 100644 --- a/lib/CLAUDE.md +++ b/lib/CLAUDE.md @@ -26,18 +26,14 @@ Signatures mirror Hare too, modulo: buffer (`strings.dup`, `strings.concat`) still return an owned `str` that callers free via `os.free(r.ptr, r.len: u64)`. -- Call-site variadic sugar (`fmt::println(42)`) doesn't land yet. - The receive side does — `fmt.formattable` is a tagged union of - the printable scalar types, and `fmt.printv` / `fmt.printlnv` - take an explicit `[]formattable` slice. Until the call-site - gather is implemented, callers either hand-build the slice: - let args: [2]fmt.formattable; - args[0] = "count: ": fmt.formattable; - args[1] = 42i64: fmt.formattable; - fmt.printlnv(args[0:2]); - or compose to a single str via strconv.i64tos + strings.concat: - fmt.println(strconv.i64tos(42, strconv.base.DEC)); - `lib/fmt` is intentionally print-string-only — no `printf`-family. +- Call-site variadic sugar matches Hare. `fn f(args: T...)` declares + a Hare-style variadic; call sites either gather N args into a + fresh `[]T` (`fmt.println(42, "hi", true)`) or forward an existing + slice with `xs...` (`fprintln(fd, args...)`). The bare `T...` form + in tagged unions still means spread-flatten (`(...inner | E)`); + the two uses don't overlap because `T...` only attaches to a + *param* decl. `lib/fmt` is intentionally print-string-only — no + `printf`-family. - `(T | U)` sum-typed parameters dispatch via `match` inside the callee. `strings.byteindex(haystack: str, needle: (str | rune))`, diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index 384d7555..77a33dd1 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -1,37 +1,30 @@ -// fmt — formatting writers. Goes through os.write to a file -// descriptor. Hare's variadic call-site sugar (`fmt::println(42)` -// gathering args into a `[]formattable`) isn't wired yet; until then, -// callers either: -// -// 1. Hand-build the slice: -// let args: [2]formattable; -// args[0] = 42i64: formattable; -// args[1] = " hi": formattable; -// fmt.print(args[0:2]); -// -// 2. Compose a single str via strconv.i64tos / strings.concat: -// fmt.println(strconv.i64tos(42, strconv.base.DEC)); -// -// `print(s: str)` keeps the single-string form for the common case. +// fmt — formatting writers. Mirrors Hare's lib/fmt subset that fmt- +// prints values via [[io::handle]]-style fd writers. Call sites take +// Hare's variadic shape: `fmt.println(42, "hi", true)` gathers the +// args into a `[]formattable` slice; wrappers forward via `args...`. use os; use strconv; use strings; -// formattable — tagged union of types fmt can render. Mirrors -// Hare's `fmt::formattable = (...types::numeric | uintptr | str | -// rune | bool | nullable *opaque | void)`, narrowed to the set ww -// actually has codegen for. Slot size is 24B (8 tag + 16 str -// payload). +// formattable — tagged union of types fmt can render. Mirrors Hare's +// `fmt::formattable = (...types::numeric | uintptr | str | rune | +// bool | nullable *opaque | void)`, narrowed to the set ww actually +// has codegen for. Slot size is 24B (8 tag + 16 str payload). export type formattable = (i64 | str | bool | rune); -// vprint — write the formatted form of each element of `args` to -// `fd`. Returns total bytes written or the first negative os.write -// result. -fn vprint(fd: i32, args: []formattable) i64 = { +// fprint — write the formatted form of each `args` element to `fd`, +// separated by spaces. Returns total bytes written or the first +// negative os.write result. Hare's separator-by-space matches. +export fn fprint(fd: i32, args: formattable...) i64 = { let total: i64 = 0; let i: i32 = 0; for (i < args.len) { + if (i > 0) { + let r: i64 = os.write(fd, " ".ptr, 1u64); + if (r < 0) { return r; }; + total += r; + }; match (args[i]) { case let n: i64 => { let s: str = strconv.i64tos(n, strconv.base.DEC); @@ -64,55 +57,40 @@ fn vprint(fd: i32, args: []formattable) i64 = { return total; }; -// print(s: str) — single-string form for the common case. The -// variadic-style `print(args: []formattable)` lives as `printv` -// until call-site sugar lands. -export fn print(s: str) i64 = { - return os.write(1, s.ptr, s.len: u64); -}; - -// printv — Hare-shaped `print(args: formattable...)` modulo the -// call-site sugar. Callers pass an explicit `[]formattable` slice. -export fn printv(args: []formattable) i64 = { - return vprint(1, args); -}; - -export fn println(s: str) i64 = { - let n: i64 = os.write(1, s.ptr, s.len: u64); - if (n < 0) { return n; }; - let m: i64 = os.write(1, "\n".ptr, 1u64); - if (m < 0) { return m; }; - return n + m; -}; - -// printlnv — like printv but adds a trailing newline. -export fn printlnv(args: []formattable) i64 = { - let n: i64 = vprint(1, args); - if (n < 0) { return n; }; - let m: i64 = os.write(1, "\n".ptr, 1u64); - if (m < 0) { return m; }; - return n + m; -}; - -// errorln — write a message to stderr with a trailing newline. -export fn errorln(s: str) i64 = { - let n: i64 = os.write(2, s.ptr, s.len: u64); - if (n < 0) { return n; }; - let m: i64 = os.write(2, "\n".ptr, 1u64); - if (m < 0) { return m; }; - return n + m; -}; - -// fprint / fprintln — same as print/println but on an arbitrary fd. -// Used by the compiler to write to its -o output file. -export fn fprint(fd: i32, s: str) i64 = { - return os.write(fd, s.ptr, s.len: u64); -}; - -export fn fprintln(fd: i32, s: str) i64 = { - let n: i64 = os.write(fd, s.ptr, s.len: u64); +// fprintln — fprint plus a trailing newline. +export fn fprintln(fd: i32, args: formattable...) i64 = { + let n: i64 = fprint(fd, args...); if (n < 0) { return n; }; let m: i64 = os.write(fd, "\n".ptr, 1u64); if (m < 0) { return m; }; return n + m; }; + +// print / println — fprint / fprintln on stdout. Direct counterparts +// of Hare's fmt::print / fmt::println. +export fn print(args: formattable...) i64 = { + return fprint(1, args...); +}; + +export fn println(args: formattable...) i64 = { + return fprintln(1, args...); +}; + +// errorln — fprintln on stderr. Hare's `fmt::error` (without -ln) is +// skipped here: the bare `error` name collides with strconv's +// `type error = !(invalid | overflow)` under the driver's flat +// concatenation namespace. Callers wanting the no-newline form use +// `fprint(2, args...)` directly. +export fn errorln(args: formattable...) i64 = { + return fprintln(2, args...); +}; + +// fatal — errorln then exit(255). `never` return marks the bottom +// type so flow-control checks treat callers as terminated. The +// fprintln result is dropped as an expression statement (Hare's +// `_ = fprintln(...)` wouldn't add safety here — process exit +// follows immediately). +export fn fatal(args: formattable...) never = { + fprintln(2, args...); + os.exit(255); +}; diff --git a/lib/ww/parse/decl.ww b/lib/ww/parse/decl.ww index a2f5dc7c..fc2f153d 100644 --- a/lib/ww/parse/decl.ww +++ b/lib/ww/parse/decl.ww @@ -104,8 +104,17 @@ fn parseparams(p: *parser) *node = { n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); n.lhs = parsetype(p); + // Hare-style variadic: `name: T...`. Marker on n.op so check + // promotes the param's type to []T and call sites gather / + // forward. Mirrors cmd/wcc/parse.c parseparams. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + n.op = tkind.TK_ELLIPSIS; + }; if (head == nil) { head = n; tail = n; } else { tail.next = n; tail = n; }; + if (n.op == tkind.TK_ELLIPSIS) { + break; // variadic must be the last param + }; if (!accepttok(p, tkind.TK_COMMA)) { break; }; if (p.curkind == tkind.TK_RPAREN) { break; }; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 51727a4c..d60ff2a8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -3639,8 +3639,17 @@ fn parseparams(p: *parser) *node = { n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); n.lhs = parsetype(p); + // Hare-style variadic: `name: T...`. Marker on n.op so check + // promotes the param's type to []T and call sites gather / + // forward. Mirrors cmd/wcc/parse.c parseparams. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + n.op = tkind.TK_ELLIPSIS; + }; if (head == nil) { head = n; tail = n; } else { tail.next = n; tail = n; }; + if (n.op == tkind.TK_ELLIPSIS) { + break; // variadic must be the last param + }; if (!accepttok(p, tkind.TK_COMMA)) { break; }; if (p.curkind == tkind.TK_RPAREN) { break; }; }; @@ -5568,6 +5577,81 @@ use typ; use sym; use strconv; +// ---- variadic-call helpers (Hare-style `T...` param) ----------------- + +// slicewrap — synthesise an N_TSLICE node wrapping the given element +// type AST. Used by the Hare-style variadic path so the local entry +// for the param (callee side) and the call-site slice descriptor +// (caller side) both advertise their effective type as []ELEM — +// every isslicetype / nodeisslice check then succeeds naturally. +fn slicewrap(c: *cgen, elem: *node) *node = { + let s: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0); + s.lhs = elem; + return s; +}; + +// findvariadicparam — walk a param-list head and return the variadic +// param node (the one with op == TK_ELLIPSIS) plus the count of +// non-variadic params before it. Returns nil/0 when no variadic. +// nfixed_out cannot be nil. +fn findvariadicparam(ps: *node, nfixed_out: *i32) *node = { + *nfixed_out = 0; + let p: *node = ps; + for (p != nil) { + if (p.kind == nkind.N_PARAM) { + if (p.op == tkind.TK_ELLIPSIS) { + return p; + }; + *nfixed_out += 1; + }; + p = p.next; + }; + return nil; +}; + +// callee_variadic_param — convenience wrapper: looks up the callee +// by name and finds its variadic param + nfixed. Returns nil if the +// callee isn't registered or has no variadic param. +fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { + *nfixed_out = 0; + if (callee == nil) { return nil; }; + let cnm: str; + cnm.ptr = nil; cnm.len = 0; + if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + if (cnm.len == 0) { return nil; }; + let ps: *node = fnparamslookup(c, cnm); + return findvariadicparam(ps, nfixed_out); +}; + +// mkvarargname — fresh local-slot name "". Used for +// the per-variadic-call scratch buffers (`@vararg_d_N` for the +// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor) +// where N is recorded on the N_CALL node at scanlocals time so both +// the prologue reservation and the call-site emission agree. +fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = { + let buf: [128]u8; + let i: i32 = 0; + let j: i32 = 0; + for (j < prefix.len) { + buf[i] = prefix[j]; + i += 1; j += 1; + }; + let ns: str = strconv.i64tos(seq: i64, strconv.base.DEC); + let n: i32 = ns.len; + let dk: i32 = 0; + for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { p[k] = buf[k]; k += 1; }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + // ---- expression cgen ------------------------------------------------- // pushargsrev — recursively walks the arg list, evaluates rightmost @@ -5593,21 +5677,31 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { let widentag: i32 = 0; if (param != nil) { if (param.kind == nkind.N_PARAM) { - let ptype: *node = param.lhs; - if (istaggedtype(c, ptype)) { - let aistagged: bool = false; - if (arg.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, arg.str); - if (lc != nil) { - aistagged = istaggedtype(c, lc.tnode); + // Hare-style variadic `T...`: effective param type is + // []T (slice). The arg here is the synthesised slice + // descriptor (or a forwarded `xs...` slice), not a + // value of T being widened into a tagged slot — skip + // the widening detection so the slice-ident fast path + // at the bottom of pushargsrev gets the push. + if (param.op == tkind.TK_ELLIPSIS) { + widensz = 0; + } else { + let ptype: *node = param.lhs; + if (istaggedtype(c, ptype)) { + let aistagged: bool = false; + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + aistagged = istaggedtype(c, lc.tnode); + }; + }; + if (!aistagged) { + widensz = slotsize(c, ptype); + let tagged: *node = resolvetagged(c, ptype); + let t: i32 = taggedvariantindex(c, tagged, arg); + if (t < 0) { t = 0; }; + widentag = t; }; - }; - if (!aistagged) { - widensz = slotsize(c, ptype); - let tagged: *node = resolvetagged(c, ptype); - let t: i32 = taggedvariantindex(c, tagged, arg); - if (t < 0) { t = 0; }; - widentag = t; }; }; }; @@ -7035,6 +7129,16 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { return nm; }; if (rhs.kind == nkind.N_STRLIT) { return "str"; }; + if (rhs.kind == nkind.N_TRUE) { return "bool"; }; + if (rhs.kind == nkind.N_FALSE) { return "bool"; }; + if (rhs.kind == nkind.N_RUNELIT) { return "rune"; }; + if (rhs.kind == nkind.N_INTLIT) { + // Typed int literal (`42i64`, `3u8`): suffix names the + // concrete variant so flatvariantidx finds it. Untyped + // literals (tsuffix=="") fall through to the isstr scan. + let s: str = rhs.tsuffix; + if (s.len > 0) { return s; }; + }; // `T{}` carries its type name on the lhs N_IDENT — the parser // builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`. // Needed so `return eof{};` (variant of a tagged union) resolves @@ -9735,6 +9839,147 @@ fn cgcall(c: *cgen, n: *node) void = { calleeparams = fnparamslookup(c, callee.str); }; }; + // Hare-style variadic last param: gather N tail args into a + // frame-resident [N]T (`@vararg_d_`) plus a 24B slice + // descriptor (`@vararg_sl_`), then splice a synthesised + // N_IDENT pointing at the descriptor into n.list so the rest + // of the call machinery sees one slice slot for the variadic. + // Forwarding shape (`xs...`) skips the gather: the spread's + // inner slice expression replaces the wrapper in place. Empty + // (no trailing args) writes a {nil, 0, 0} descriptor. The seq + // matches the one scanlocals stamped on n.uval. + { + let nfixed_v: i32 = 0; + let varp: *node = callee_variadic_param(c, callee, &nfixed_v); + if (varp != nil) { + let nargs0: i32 = 0; + let aw: *node = n.list; + for (aw != nil) { nargs0 += 1; aw = aw.next; }; + let nvar: i32 = nargs0 - nfixed_v; + if (nvar < 0) { nvar = 0; }; + let forwarding: bool = false; + if (nvar == 1) { + let aaf: *node = n.list; + let kk: i32 = 0; + for (kk < nfixed_v) { + aaf = aaf.next; + kk += 1; + }; + if (aaf != nil) { + if (aaf.kind == nkind.N_SPREAD) { + forwarding = true; + }; + }; + }; + if (forwarding) { + let prev: *node = nil; + let cur2: *node = n.list; + let kk2: i32 = 0; + for (kk2 < nfixed_v) { + prev = cur2; + cur2 = cur2.next; + kk2 += 1; + }; + let inner: *node = cur2.lhs; + if (inner != nil) { inner.next = nil; }; + if (prev == nil) { n.list = inner; } + else { prev.next = inner; }; + } else { + let seq: i32 = n.uval: i32; + let dname: str = mkvarargname(c, "@vararg_d_", seq); + let sname: str = mkvarargname(c, "@vararg_sl_", seq); + let esz: i32 = slotsize(c, varp.lhs); + if (esz < 1) { esz = 1; }; + let velemtagged: bool = istaggedtype(c, varp.lhs); + let velemstr: bool = isstrtype(c, varp.lhs); + let velemslice: bool = isslicetype(c, varp.lhs); + let doff: i32 = 0; + if (nvar > 0) { + doff = localadd(c, dname, nvar * esz, nil); + }; + let soff: i32 = localadd(c, sname, 24, + slicewrap(c, varp.lhs)); + let aa2: *node = n.list; + let kk3: i32 = 0; + for (kk3 < nfixed_v) { + aa2 = aa2.next; + kk3 += 1; + }; + let j: i32 = 0; + let prevarg: *node = n.list; + if (nfixed_v == 0) { prevarg = nil; } + else { + let kk4: i32 = 0; + for (kk4 < nfixed_v - 1) { + prevarg = prevarg.next; + kk4 += 1; + }; + }; + for (aa2 != nil) { + let slot: i32 = doff + j * esz; + if (velemtagged) { + cgwidentaggedstore(c, varp.lhs, + aa2, slot, esz); + } else { if (velemstr) { + cgexpr(c, aa2); + emitline("\tMOVQ\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot + 8): i64); + emitline("(BP)\n"); + } else { if (velemslice) { + cgexpr(c, aa2); + emitline("\tMOVQ\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((slot + 16): i64); + emitline("(BP)\n"); + } else { + cgexpr(c, aa2); + let op: str = "MOVQ"; + if (esz == 1) { op = "MOVB"; } + else { if (esz == 4) { op = "MOVL"; }; }; + emitline("\t"); + emitline(op); + emitline("\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + }; }; }; + j += 1; + aa2 = aa2.next; + }; + if (nvar > 0) { + emitline("\tLEAQ\t"); + emitoff(doff: i64); + emitline("(BP), AX\n"); + } else { + emitline("\tXORQ\tAX, AX\n"); + }; + emitline("\tMOVQ\tAX, "); + emitoff(soff: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\t$"); + emitint(nvar: i64); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((soff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tAX, "); + emitoff((soff + 16): i64); + emitline("(BP)\n"); + let sn: *node = newnode(c.a, nkind.N_IDENT, + "", 0, 0); + sn.str = sname; + if (prevarg == nil) { n.list = sn; } + else { prevarg.next = sn; }; + }; + }; + }; let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else @@ -12393,6 +12638,50 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Hare-style variadic call: reserve @vararg_d_ for the + // element data and @vararg_sl_ for the 24B slice + // descriptor. The seq is recorded on the N_CALL node so + // cgcall picks the same names regardless of walk order + // (scanlocals descends LTR; pushargsrev evaluates RTL). + let nfixed: i32 = 0; + let varp: *node = callee_variadic_param(c, n.lhs, &nfixed); + if (varp != nil) { + let nargs: i32 = 0; + let aw: *node = n.list; + for (aw != nil) { nargs += 1; aw = aw.next; }; + let nvar: i32 = nargs - nfixed; + if (nvar < 0) { nvar = 0; }; + let forwarding: bool = false; + if (nvar == 1) { + let aa: *node = n.list; + let k0: i32 = 0; + for (k0 < nfixed) { aa = aa.next; k0 += 1; }; + if (aa != nil) { + if (aa.kind == nkind.N_SPREAD) { + forwarding = true; + }; + }; + }; + if (!forwarding) { + let seq: i32 = c.varargseq; + n.uval = seq: u64; + c.varargseq += 1; + let esz: i32 = slotsize(c, varp.lhs); + if (esz < 1) { esz = 1; }; + let dname: str = mkvarargname(c, "@vararg_d_", seq); + let sname: str = mkvarargname(c, "@vararg_sl_", seq); + if (nvar > 0) { + if (!scanseenmark(c, dname)) { + let dsz: i32 = nvar * esz; + if ((dsz & 7) != 0) { + dsz = (dsz + 7) & ~7; + }; + total += dsz; + }; + }; + if (!scanseenmark(c, sname)) { total += 24; }; + }; + }; }; if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; @@ -12424,6 +12713,39 @@ fn cgfnparams(c: *cgen, params: *node) void = { for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; + // Hare-style variadic `T...`: callee receives a []T + // slice (3 register words / 24B). Mirror the slice- + // param spill below but use a synthesised TSLICE + // tnode so body references see the slot as a slice. + if (p.op == tkind.TK_ELLIPSIS) { + let tn: *node = slicewrap(c, p.lhs); + if (idx + 3 <= 6) { + let off: i32 = localadd(c, nm, 24, tn); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { + localaddstack(c, nm, tn, 16 + stkcursor*8); + stkcursor += 3; + }; + p = p.next; + continue; + }; if (isfloattype(c, p.lhs)) { // Float param: SysV uses the XMM stream // (X0..X7). 8B (f64) or 4B (f32) slot. @@ -12597,15 +12919,20 @@ fn cgfn(c: *cgen, fn_: *node) void = { let frame: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } + // Hare-style variadic `T...`: param is []T inside + // the callee, so it occupies a 24B slice slot. + if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } + else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; + else { frame += 8; }; }; }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; }; + c.varargseq = 0; if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; + c.varargseq = 0; // Drop the stubs so emission rebuilds c.locals with real offsets. c.locals = nil; if ((frame & 15) != 0) { @@ -13022,6 +13349,11 @@ type cgen = struct { yieldbuf: *str, // stack of match end labels for yield defertop: i32, deferbuf: **node, // stack of deferred exprs (LIFO at return) + // Variadic-call gather state. scanlocals walks the body in pre- + // order DFS and assigns per-call scratch names `@vararg_d_N` / + // `@vararg_sl_N` using this counter; cgcall resets and walks in + // the same order so the names line up at emission time. + varargseq: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -13042,6 +13374,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.frame = 0; c.lastwasreturn = 0; c.labelseq = 0; + c.varargseq = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index ad36afc2..d3f773cf 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -349,6 +349,11 @@ type cgen = struct { yieldbuf: *str, // stack of match end labels for yield defertop: i32, deferbuf: **node, // stack of deferred exprs (LIFO at return) + // Variadic-call gather state. scanlocals walks the body in pre- + // order DFS and assigns per-call scratch names `@vararg_d_N` / + // `@vararg_sl_N` using this counter; cgcall resets and walks in + // the same order so the names line up at emission time. + varargseq: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -369,6 +374,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.frame = 0; c.lastwasreturn = 0; c.labelseq = 0; + c.varargseq = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 7c4690bd..878ecb82 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -290,6 +290,50 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Hare-style variadic call: reserve @vararg_d_ for the + // element data and @vararg_sl_ for the 24B slice + // descriptor. The seq is recorded on the N_CALL node so + // cgcall picks the same names regardless of walk order + // (scanlocals descends LTR; pushargsrev evaluates RTL). + let nfixed: i32 = 0; + let varp: *node = callee_variadic_param(c, n.lhs, &nfixed); + if (varp != nil) { + let nargs: i32 = 0; + let aw: *node = n.list; + for (aw != nil) { nargs += 1; aw = aw.next; }; + let nvar: i32 = nargs - nfixed; + if (nvar < 0) { nvar = 0; }; + let forwarding: bool = false; + if (nvar == 1) { + let aa: *node = n.list; + let k0: i32 = 0; + for (k0 < nfixed) { aa = aa.next; k0 += 1; }; + if (aa != nil) { + if (aa.kind == nkind.N_SPREAD) { + forwarding = true; + }; + }; + }; + if (!forwarding) { + let seq: i32 = c.varargseq; + n.uval = seq: u64; + c.varargseq += 1; + let esz: i32 = slotsize(c, varp.lhs); + if (esz < 1) { esz = 1; }; + let dname: str = mkvarargname(c, "@vararg_d_", seq); + let sname: str = mkvarargname(c, "@vararg_sl_", seq); + if (nvar > 0) { + if (!scanseenmark(c, dname)) { + let dsz: i32 = nvar * esz; + if ((dsz & 7) != 0) { + dsz = (dsz + 7) & ~7; + }; + total += dsz; + }; + }; + if (!scanseenmark(c, sname)) { total += 24; }; + }; + }; }; if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; @@ -321,6 +365,39 @@ fn cgfnparams(c: *cgen, params: *node) void = { for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; + // Hare-style variadic `T...`: callee receives a []T + // slice (3 register words / 24B). Mirror the slice- + // param spill below but use a synthesised TSLICE + // tnode so body references see the slot as a slice. + if (p.op == tkind.TK_ELLIPSIS) { + let tn: *node = slicewrap(c, p.lhs); + if (idx + 3 <= 6) { + let off: i32 = localadd(c, nm, 24, tn); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { + localaddstack(c, nm, tn, 16 + stkcursor*8); + stkcursor += 3; + }; + p = p.next; + continue; + }; if (isfloattype(c, p.lhs)) { // Float param: SysV uses the XMM stream // (X0..X7). 8B (f64) or 4B (f32) slot. @@ -494,15 +571,20 @@ fn cgfn(c: *cgen, fn_: *node) void = { let frame: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } + // Hare-style variadic `T...`: param is []T inside + // the callee, so it occupies a 24B slice slot. + if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } + else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; + else { frame += 8; }; }; }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; }; + c.varargseq = 0; if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; + c.varargseq = 0; // Drop the stubs so emission rebuilds c.locals with real offsets. c.locals = nil; if ((frame & 15) != 0) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 85300316..3e2eaa09 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2142,6 +2142,147 @@ fn cgcall(c: *cgen, n: *node) void = { calleeparams = fnparamslookup(c, callee.str); }; }; + // Hare-style variadic last param: gather N tail args into a + // frame-resident [N]T (`@vararg_d_`) plus a 24B slice + // descriptor (`@vararg_sl_`), then splice a synthesised + // N_IDENT pointing at the descriptor into n.list so the rest + // of the call machinery sees one slice slot for the variadic. + // Forwarding shape (`xs...`) skips the gather: the spread's + // inner slice expression replaces the wrapper in place. Empty + // (no trailing args) writes a {nil, 0, 0} descriptor. The seq + // matches the one scanlocals stamped on n.uval. + { + let nfixed_v: i32 = 0; + let varp: *node = callee_variadic_param(c, callee, &nfixed_v); + if (varp != nil) { + let nargs0: i32 = 0; + let aw: *node = n.list; + for (aw != nil) { nargs0 += 1; aw = aw.next; }; + let nvar: i32 = nargs0 - nfixed_v; + if (nvar < 0) { nvar = 0; }; + let forwarding: bool = false; + if (nvar == 1) { + let aaf: *node = n.list; + let kk: i32 = 0; + for (kk < nfixed_v) { + aaf = aaf.next; + kk += 1; + }; + if (aaf != nil) { + if (aaf.kind == nkind.N_SPREAD) { + forwarding = true; + }; + }; + }; + if (forwarding) { + let prev: *node = nil; + let cur2: *node = n.list; + let kk2: i32 = 0; + for (kk2 < nfixed_v) { + prev = cur2; + cur2 = cur2.next; + kk2 += 1; + }; + let inner: *node = cur2.lhs; + if (inner != nil) { inner.next = nil; }; + if (prev == nil) { n.list = inner; } + else { prev.next = inner; }; + } else { + let seq: i32 = n.uval: i32; + let dname: str = mkvarargname(c, "@vararg_d_", seq); + let sname: str = mkvarargname(c, "@vararg_sl_", seq); + let esz: i32 = slotsize(c, varp.lhs); + if (esz < 1) { esz = 1; }; + let velemtagged: bool = istaggedtype(c, varp.lhs); + let velemstr: bool = isstrtype(c, varp.lhs); + let velemslice: bool = isslicetype(c, varp.lhs); + let doff: i32 = 0; + if (nvar > 0) { + doff = localadd(c, dname, nvar * esz, nil); + }; + let soff: i32 = localadd(c, sname, 24, + slicewrap(c, varp.lhs)); + let aa2: *node = n.list; + let kk3: i32 = 0; + for (kk3 < nfixed_v) { + aa2 = aa2.next; + kk3 += 1; + }; + let j: i32 = 0; + let prevarg: *node = n.list; + if (nfixed_v == 0) { prevarg = nil; } + else { + let kk4: i32 = 0; + for (kk4 < nfixed_v - 1) { + prevarg = prevarg.next; + kk4 += 1; + }; + }; + for (aa2 != nil) { + let slot: i32 = doff + j * esz; + if (velemtagged) { + cgwidentaggedstore(c, varp.lhs, + aa2, slot, esz); + } else { if (velemstr) { + cgexpr(c, aa2); + emitline("\tMOVQ\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot + 8): i64); + emitline("(BP)\n"); + } else { if (velemslice) { + cgexpr(c, aa2); + emitline("\tMOVQ\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((slot + 16): i64); + emitline("(BP)\n"); + } else { + cgexpr(c, aa2); + let op: str = "MOVQ"; + if (esz == 1) { op = "MOVB"; } + else { if (esz == 4) { op = "MOVL"; }; }; + emitline("\t"); + emitline(op); + emitline("\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + }; }; }; + j += 1; + aa2 = aa2.next; + }; + if (nvar > 0) { + emitline("\tLEAQ\t"); + emitoff(doff: i64); + emitline("(BP), AX\n"); + } else { + emitline("\tXORQ\tAX, AX\n"); + }; + emitline("\tMOVQ\tAX, "); + emitoff(soff: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\t$"); + emitint(nvar: i64); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((soff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tAX, "); + emitoff((soff + 16): i64); + emitline("(BP)\n"); + let sn: *node = newnode(c.a, nkind.N_IDENT, + "", 0, 0); + sn.str = sname; + if (prevarg == nil) { n.list = sn; } + else { prevarg.next = sn; }; + }; + }; + }; let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index e73a7c23..3db9e2df 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -20,6 +20,81 @@ use typ; use sym; use strconv; +// ---- variadic-call helpers (Hare-style `T...` param) ----------------- + +// slicewrap — synthesise an N_TSLICE node wrapping the given element +// type AST. Used by the Hare-style variadic path so the local entry +// for the param (callee side) and the call-site slice descriptor +// (caller side) both advertise their effective type as []ELEM — +// every isslicetype / nodeisslice check then succeeds naturally. +fn slicewrap(c: *cgen, elem: *node) *node = { + let s: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0); + s.lhs = elem; + return s; +}; + +// findvariadicparam — walk a param-list head and return the variadic +// param node (the one with op == TK_ELLIPSIS) plus the count of +// non-variadic params before it. Returns nil/0 when no variadic. +// nfixed_out cannot be nil. +fn findvariadicparam(ps: *node, nfixed_out: *i32) *node = { + *nfixed_out = 0; + let p: *node = ps; + for (p != nil) { + if (p.kind == nkind.N_PARAM) { + if (p.op == tkind.TK_ELLIPSIS) { + return p; + }; + *nfixed_out += 1; + }; + p = p.next; + }; + return nil; +}; + +// callee_variadic_param — convenience wrapper: looks up the callee +// by name and finds its variadic param + nfixed. Returns nil if the +// callee isn't registered or has no variadic param. +fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { + *nfixed_out = 0; + if (callee == nil) { return nil; }; + let cnm: str; + cnm.ptr = nil; cnm.len = 0; + if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + if (cnm.len == 0) { return nil; }; + let ps: *node = fnparamslookup(c, cnm); + return findvariadicparam(ps, nfixed_out); +}; + +// mkvarargname — fresh local-slot name "". Used for +// the per-variadic-call scratch buffers (`@vararg_d_N` for the +// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor) +// where N is recorded on the N_CALL node at scanlocals time so both +// the prologue reservation and the call-site emission agree. +fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = { + let buf: [128]u8; + let i: i32 = 0; + let j: i32 = 0; + for (j < prefix.len) { + buf[i] = prefix[j]; + i += 1; j += 1; + }; + let ns: str = strconv.i64tos(seq: i64, strconv.base.DEC); + let n: i32 = ns.len; + let dk: i32 = 0; + for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { p[k] = buf[k]; k += 1; }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + // ---- expression cgen ------------------------------------------------- // pushargsrev — recursively walks the arg list, evaluates rightmost @@ -45,21 +120,31 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { let widentag: i32 = 0; if (param != nil) { if (param.kind == nkind.N_PARAM) { - let ptype: *node = param.lhs; - if (istaggedtype(c, ptype)) { - let aistagged: bool = false; - if (arg.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, arg.str); - if (lc != nil) { - aistagged = istaggedtype(c, lc.tnode); + // Hare-style variadic `T...`: effective param type is + // []T (slice). The arg here is the synthesised slice + // descriptor (or a forwarded `xs...` slice), not a + // value of T being widened into a tagged slot — skip + // the widening detection so the slice-ident fast path + // at the bottom of pushargsrev gets the push. + if (param.op == tkind.TK_ELLIPSIS) { + widensz = 0; + } else { + let ptype: *node = param.lhs; + if (istaggedtype(c, ptype)) { + let aistagged: bool = false; + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + aistagged = istaggedtype(c, lc.tnode); + }; + }; + if (!aistagged) { + widensz = slotsize(c, ptype); + let tagged: *node = resolvetagged(c, ptype); + let t: i32 = taggedvariantindex(c, tagged, arg); + if (t < 0) { t = 0; }; + widentag = t; }; - }; - if (!aistagged) { - widensz = slotsize(c, ptype); - let tagged: *node = resolvetagged(c, ptype); - let t: i32 = taggedvariantindex(c, tagged, arg); - if (t < 0) { t = 0; }; - widentag = t; }; }; }; @@ -1487,6 +1572,16 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { return nm; }; if (rhs.kind == nkind.N_STRLIT) { return "str"; }; + if (rhs.kind == nkind.N_TRUE) { return "bool"; }; + if (rhs.kind == nkind.N_FALSE) { return "bool"; }; + if (rhs.kind == nkind.N_RUNELIT) { return "rune"; }; + if (rhs.kind == nkind.N_INTLIT) { + // Typed int literal (`42i64`, `3u8`): suffix names the + // concrete variant so flatvariantidx finds it. Untyped + // literals (tsuffix=="") fall through to the isstr scan. + let s: str = rhs.tsuffix; + if (s.len > 0) { return s; }; + }; // `T{}` carries its type name on the lhs N_IDENT — the parser // builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`. // Needed so `return eof{};` (variant of a tagged union) resolves diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f385d3b9..d2a742cb 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -3639,8 +3639,17 @@ fn parseparams(p: *parser) *node = { n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); n.lhs = parsetype(p); + // Hare-style variadic: `name: T...`. Marker on n.op so check + // promotes the param's type to []T and call sites gather / + // forward. Mirrors cmd/wcc/parse.c parseparams. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + n.op = tkind.TK_ELLIPSIS; + }; if (head == nil) { head = n; tail = n; } else { tail.next = n; tail = n; }; + if (n.op == tkind.TK_ELLIPSIS) { + break; // variadic must be the last param + }; if (!accepttok(p, tkind.TK_COMMA)) { break; }; if (p.curkind == tkind.TK_RPAREN) { break; }; }; @@ -5568,6 +5577,81 @@ use typ; use sym; use strconv; +// ---- variadic-call helpers (Hare-style `T...` param) ----------------- + +// slicewrap — synthesise an N_TSLICE node wrapping the given element +// type AST. Used by the Hare-style variadic path so the local entry +// for the param (callee side) and the call-site slice descriptor +// (caller side) both advertise their effective type as []ELEM — +// every isslicetype / nodeisslice check then succeeds naturally. +fn slicewrap(c: *cgen, elem: *node) *node = { + let s: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0); + s.lhs = elem; + return s; +}; + +// findvariadicparam — walk a param-list head and return the variadic +// param node (the one with op == TK_ELLIPSIS) plus the count of +// non-variadic params before it. Returns nil/0 when no variadic. +// nfixed_out cannot be nil. +fn findvariadicparam(ps: *node, nfixed_out: *i32) *node = { + *nfixed_out = 0; + let p: *node = ps; + for (p != nil) { + if (p.kind == nkind.N_PARAM) { + if (p.op == tkind.TK_ELLIPSIS) { + return p; + }; + *nfixed_out += 1; + }; + p = p.next; + }; + return nil; +}; + +// callee_variadic_param — convenience wrapper: looks up the callee +// by name and finds its variadic param + nfixed. Returns nil if the +// callee isn't registered or has no variadic param. +fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { + *nfixed_out = 0; + if (callee == nil) { return nil; }; + let cnm: str; + cnm.ptr = nil; cnm.len = 0; + if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + if (cnm.len == 0) { return nil; }; + let ps: *node = fnparamslookup(c, cnm); + return findvariadicparam(ps, nfixed_out); +}; + +// mkvarargname — fresh local-slot name "". Used for +// the per-variadic-call scratch buffers (`@vararg_d_N` for the +// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor) +// where N is recorded on the N_CALL node at scanlocals time so both +// the prologue reservation and the call-site emission agree. +fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = { + let buf: [128]u8; + let i: i32 = 0; + let j: i32 = 0; + for (j < prefix.len) { + buf[i] = prefix[j]; + i += 1; j += 1; + }; + let ns: str = strconv.i64tos(seq: i64, strconv.base.DEC); + let n: i32 = ns.len; + let dk: i32 = 0; + for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { p[k] = buf[k]; k += 1; }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + // ---- expression cgen ------------------------------------------------- // pushargsrev — recursively walks the arg list, evaluates rightmost @@ -5593,21 +5677,31 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { let widentag: i32 = 0; if (param != nil) { if (param.kind == nkind.N_PARAM) { - let ptype: *node = param.lhs; - if (istaggedtype(c, ptype)) { - let aistagged: bool = false; - if (arg.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, arg.str); - if (lc != nil) { - aistagged = istaggedtype(c, lc.tnode); + // Hare-style variadic `T...`: effective param type is + // []T (slice). The arg here is the synthesised slice + // descriptor (or a forwarded `xs...` slice), not a + // value of T being widened into a tagged slot — skip + // the widening detection so the slice-ident fast path + // at the bottom of pushargsrev gets the push. + if (param.op == tkind.TK_ELLIPSIS) { + widensz = 0; + } else { + let ptype: *node = param.lhs; + if (istaggedtype(c, ptype)) { + let aistagged: bool = false; + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + aistagged = istaggedtype(c, lc.tnode); + }; + }; + if (!aistagged) { + widensz = slotsize(c, ptype); + let tagged: *node = resolvetagged(c, ptype); + let t: i32 = taggedvariantindex(c, tagged, arg); + if (t < 0) { t = 0; }; + widentag = t; }; - }; - if (!aistagged) { - widensz = slotsize(c, ptype); - let tagged: *node = resolvetagged(c, ptype); - let t: i32 = taggedvariantindex(c, tagged, arg); - if (t < 0) { t = 0; }; - widentag = t; }; }; }; @@ -7035,6 +7129,16 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { return nm; }; if (rhs.kind == nkind.N_STRLIT) { return "str"; }; + if (rhs.kind == nkind.N_TRUE) { return "bool"; }; + if (rhs.kind == nkind.N_FALSE) { return "bool"; }; + if (rhs.kind == nkind.N_RUNELIT) { return "rune"; }; + if (rhs.kind == nkind.N_INTLIT) { + // Typed int literal (`42i64`, `3u8`): suffix names the + // concrete variant so flatvariantidx finds it. Untyped + // literals (tsuffix=="") fall through to the isstr scan. + let s: str = rhs.tsuffix; + if (s.len > 0) { return s; }; + }; // `T{}` carries its type name on the lhs N_IDENT — the parser // builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`. // Needed so `return eof{};` (variant of a tagged union) resolves @@ -9735,6 +9839,147 @@ fn cgcall(c: *cgen, n: *node) void = { calleeparams = fnparamslookup(c, callee.str); }; }; + // Hare-style variadic last param: gather N tail args into a + // frame-resident [N]T (`@vararg_d_`) plus a 24B slice + // descriptor (`@vararg_sl_`), then splice a synthesised + // N_IDENT pointing at the descriptor into n.list so the rest + // of the call machinery sees one slice slot for the variadic. + // Forwarding shape (`xs...`) skips the gather: the spread's + // inner slice expression replaces the wrapper in place. Empty + // (no trailing args) writes a {nil, 0, 0} descriptor. The seq + // matches the one scanlocals stamped on n.uval. + { + let nfixed_v: i32 = 0; + let varp: *node = callee_variadic_param(c, callee, &nfixed_v); + if (varp != nil) { + let nargs0: i32 = 0; + let aw: *node = n.list; + for (aw != nil) { nargs0 += 1; aw = aw.next; }; + let nvar: i32 = nargs0 - nfixed_v; + if (nvar < 0) { nvar = 0; }; + let forwarding: bool = false; + if (nvar == 1) { + let aaf: *node = n.list; + let kk: i32 = 0; + for (kk < nfixed_v) { + aaf = aaf.next; + kk += 1; + }; + if (aaf != nil) { + if (aaf.kind == nkind.N_SPREAD) { + forwarding = true; + }; + }; + }; + if (forwarding) { + let prev: *node = nil; + let cur2: *node = n.list; + let kk2: i32 = 0; + for (kk2 < nfixed_v) { + prev = cur2; + cur2 = cur2.next; + kk2 += 1; + }; + let inner: *node = cur2.lhs; + if (inner != nil) { inner.next = nil; }; + if (prev == nil) { n.list = inner; } + else { prev.next = inner; }; + } else { + let seq: i32 = n.uval: i32; + let dname: str = mkvarargname(c, "@vararg_d_", seq); + let sname: str = mkvarargname(c, "@vararg_sl_", seq); + let esz: i32 = slotsize(c, varp.lhs); + if (esz < 1) { esz = 1; }; + let velemtagged: bool = istaggedtype(c, varp.lhs); + let velemstr: bool = isstrtype(c, varp.lhs); + let velemslice: bool = isslicetype(c, varp.lhs); + let doff: i32 = 0; + if (nvar > 0) { + doff = localadd(c, dname, nvar * esz, nil); + }; + let soff: i32 = localadd(c, sname, 24, + slicewrap(c, varp.lhs)); + let aa2: *node = n.list; + let kk3: i32 = 0; + for (kk3 < nfixed_v) { + aa2 = aa2.next; + kk3 += 1; + }; + let j: i32 = 0; + let prevarg: *node = n.list; + if (nfixed_v == 0) { prevarg = nil; } + else { + let kk4: i32 = 0; + for (kk4 < nfixed_v - 1) { + prevarg = prevarg.next; + kk4 += 1; + }; + }; + for (aa2 != nil) { + let slot: i32 = doff + j * esz; + if (velemtagged) { + cgwidentaggedstore(c, varp.lhs, + aa2, slot, esz); + } else { if (velemstr) { + cgexpr(c, aa2); + emitline("\tMOVQ\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot + 8): i64); + emitline("(BP)\n"); + } else { if (velemslice) { + cgexpr(c, aa2); + emitline("\tMOVQ\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((slot + 16): i64); + emitline("(BP)\n"); + } else { + cgexpr(c, aa2); + let op: str = "MOVQ"; + if (esz == 1) { op = "MOVB"; } + else { if (esz == 4) { op = "MOVL"; }; }; + emitline("\t"); + emitline(op); + emitline("\tAX, "); + emitoff(slot: i64); + emitline("(BP)\n"); + }; }; }; + j += 1; + aa2 = aa2.next; + }; + if (nvar > 0) { + emitline("\tLEAQ\t"); + emitoff(doff: i64); + emitline("(BP), AX\n"); + } else { + emitline("\tXORQ\tAX, AX\n"); + }; + emitline("\tMOVQ\tAX, "); + emitoff(soff: i64); + emitline("(BP)\n"); + emitline("\tMOVQ\t$"); + emitint(nvar: i64); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((soff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tAX, "); + emitoff((soff + 16): i64); + emitline("(BP)\n"); + let sn: *node = newnode(c.a, nkind.N_IDENT, + "", 0, 0); + sn.str = sname; + if (prevarg == nil) { n.list = sn; } + else { prevarg.next = sn; }; + }; + }; + }; let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else @@ -12393,6 +12638,50 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Hare-style variadic call: reserve @vararg_d_ for the + // element data and @vararg_sl_ for the 24B slice + // descriptor. The seq is recorded on the N_CALL node so + // cgcall picks the same names regardless of walk order + // (scanlocals descends LTR; pushargsrev evaluates RTL). + let nfixed: i32 = 0; + let varp: *node = callee_variadic_param(c, n.lhs, &nfixed); + if (varp != nil) { + let nargs: i32 = 0; + let aw: *node = n.list; + for (aw != nil) { nargs += 1; aw = aw.next; }; + let nvar: i32 = nargs - nfixed; + if (nvar < 0) { nvar = 0; }; + let forwarding: bool = false; + if (nvar == 1) { + let aa: *node = n.list; + let k0: i32 = 0; + for (k0 < nfixed) { aa = aa.next; k0 += 1; }; + if (aa != nil) { + if (aa.kind == nkind.N_SPREAD) { + forwarding = true; + }; + }; + }; + if (!forwarding) { + let seq: i32 = c.varargseq; + n.uval = seq: u64; + c.varargseq += 1; + let esz: i32 = slotsize(c, varp.lhs); + if (esz < 1) { esz = 1; }; + let dname: str = mkvarargname(c, "@vararg_d_", seq); + let sname: str = mkvarargname(c, "@vararg_sl_", seq); + if (nvar > 0) { + if (!scanseenmark(c, dname)) { + let dsz: i32 = nvar * esz; + if ((dsz & 7) != 0) { + dsz = (dsz + 7) & ~7; + }; + total += dsz; + }; + }; + if (!scanseenmark(c, sname)) { total += 24; }; + }; + }; }; if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; @@ -12424,6 +12713,39 @@ fn cgfnparams(c: *cgen, params: *node) void = { for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; + // Hare-style variadic `T...`: callee receives a []T + // slice (3 register words / 24B). Mirror the slice- + // param spill below but use a synthesised TSLICE + // tnode so body references see the slot as a slice. + if (p.op == tkind.TK_ELLIPSIS) { + let tn: *node = slicewrap(c, p.lhs); + if (idx + 3 <= 6) { + let off: i32 = localadd(c, nm, 24, tn); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { + localaddstack(c, nm, tn, 16 + stkcursor*8); + stkcursor += 3; + }; + p = p.next; + continue; + }; if (isfloattype(c, p.lhs)) { // Float param: SysV uses the XMM stream // (X0..X7). 8B (f64) or 4B (f32) slot. @@ -12597,15 +12919,20 @@ fn cgfn(c: *cgen, fn_: *node) void = { let frame: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } + // Hare-style variadic `T...`: param is []T inside + // the callee, so it occupies a 24B slice slot. + if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } + else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; + else { frame += 8; }; }; }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; }; + c.varargseq = 0; if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; + c.varargseq = 0; // Drop the stubs so emission rebuilds c.locals with real offsets. c.locals = nil; if ((frame & 15) != 0) { @@ -13022,6 +13349,11 @@ type cgen = struct { yieldbuf: *str, // stack of match end labels for yield defertop: i32, deferbuf: **node, // stack of deferred exprs (LIFO at return) + // Variadic-call gather state. scanlocals walks the body in pre- + // order DFS and assigns per-call scratch names `@vararg_d_N` / + // `@vararg_sl_N` using this counter; cgcall resets and walks in + // the same order so the names line up at emission time. + varargseq: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -13042,6 +13374,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.frame = 0; c.lastwasreturn = 0; c.labelseq = 0; + c.varargseq = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 10f3fe37..0ecb6171 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -1558,6 +1558,75 @@ static const struct row rows[] = { " };\n" " return s;\n" "};", 42 }, /* 1 + 2 + 39 = 42 */ + /* Hare-style variadic gather: `args: T...` declares an N-arg + * variadic; the call-site materialises N values into a fresh + * `[N]T` and synthesises a {ptr,len,cap} slice for the param. + * Plain element type (i64): no widening, MOVQ-per-element. */ + { "fn sum(args: i64...) i64 = {\n" + " let s: i64 = 0i64;\n" + " let i: i32 = 0;\n" + " for (i < args.len) { s += args[i]; i += 1; };\n" + " return s;\n" + "};\n" + "fn main() i32 = {\n" + " return sum(1i64, 2i64, 3i64, 7i64, 9i64, 20i64): i32;\n" + "};", 42 }, + /* Variadic with zero args: empty-slice descriptor {nil,0,0}. + * Confirms the gather path doesn't crash on N=0. */ + { "fn sum(args: i64...) i64 = {\n" + " let s: i64 = 0i64;\n" + " let i: i32 = 0;\n" + " for (i < args.len) { s += args[i]; i += 1; };\n" + " return s;\n" + "};\n" + "fn main() i32 = {\n" + " let a: i64 = sum();\n" + " let b: i64 = sum(42i64);\n" + " return (a + b): i32;\n" + "};", 42 }, + /* Variadic with tagged-union element type: each gathered arg + * widens to the variant's slot shape (tag@+0, payload@+8). The + * runtime match-dispatch reads (i64=1)+(str.len=2)+(bool=39)=42. */ + { "type formattable = (i64 | str | bool);\n" + "fn sumtag(args: formattable...) i64 = {\n" + " let s: i64 = 0i64;\n" + " let i: i32 = 0;\n" + " for (i < args.len) {\n" + " match (args[i]) {\n" + " case let n: i64 => s += n;\n" + " case let v: str => s += v.len: i64;\n" + " case let b: bool => { if (b) { s += 39i64; }; };\n" + " };\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};\n" + "fn main() i32 = { return sumtag(1i64, \"hi\", true): i32; };", 42 }, + /* Variadic forwarding: `wrap(args...)` passes the local slice + * directly to `sum`, no re-gather. Mirrors Hare's wrapper shape + * (`fn println(args: formattable...) = fprintln(os.stdout, args...)`). */ + { "fn sum(args: i64...) i64 = {\n" + " let s: i64 = 0i64;\n" + " let i: i32 = 0;\n" + " for (i < args.len) { s += args[i]; i += 1; };\n" + " return s;\n" + "};\n" + "fn wrap(prefix: i64, args: i64...) i64 = {\n" + " return prefix + sum(args...);\n" + "};\n" + "fn main() i32 = {\n" + " return wrap(2i64, 1i64, 2i64, 3i64, 4i64, 5i64, 7i64, 18i64): i32;\n" + "};", 42 }, + /* lib/fmt user-side: `fmt.println(args: formattable...)` gathers + * mixed-type args at the call site. End-to-end exercises the + * lib/fmt graduation: the wrapper-chain `println → fprintln → + * fprint` is itself variadic-forwarding, so this validates both + * gather (at main) and `args...` forward (inside lib/fmt). The + * exit code is bytes printed (`hello 7\n` = 8). */ + { "use fmt;\n" + "fn main() i32 = {\n" + " return fmt.println(\"hello\", 7i64): i32;\n" + "};", 8 }, { NULL, 0 } };