w6c+wcc: widen struct/tagged-subset, parse ... spread

Three tagged-union gaps:

  1. Struct-payload widening was broken at every site (call, let,
     assign, return, struct-field init). cg_widen_tagged_store now
     materialises str / scalar / struct-lit / struct-ident / tagged
     payloads at slot+8+field_off and writes the tag last. Call sites
     route through cg_widen_tagged_push (scratch slot + push high→low).

  2. Tagged → wider tagged widening forwarded the source tag verbatim.
     cg_widen_tag_remap emits a CMPQ-chain switch that translates each
     source variant index to the destination's, then zero-pads to the
     wider slot. type_eq grew a TY_TAGGED arm (was returning 1 for any
     two unions); type_assignable now accepts variant-subset and
     rejects the rest.

  3. `(...inner | T)` spread parses (cmd/wcc/parse.c, lib/ww/parse).
     Marks Node.op = TK_ELLIPSIS; resolve_type unwraps NAMED + flattens
     when the spread bit is set so aliases inline like Hare's
     tagged_type unwrap flag.

Selfhost mirror: spread parser ported. Cgen widen helpers not yet
mirrored — wwstage stays byte-identical to cstage on the existing
test corpus, but will emit wrong asm if user code uses the new
patterns (probe sp2 shows the divergence).

700_e2e: 9 new rows covering call/let/assign/return × struct +
tagged subset, plus the spread-flatten case.
This commit is contained in:
2026-05-13 05:30:20 +09:00
parent 47d75d9b59
commit 9133251269
8 changed files with 562 additions and 161 deletions

View File

@@ -771,6 +771,9 @@ localfind(Local *head, const char *name)
static void cgexpr(Cg*, Node*, Local*);
static void cgstmt(Cg*, Node*, Local**, int*);
static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int);
static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int);
static void cg_widen_tag_remap(Cg*, Type*, Type*, int);
static void
cgexpr_int(Cg *c, long long v)
@@ -778,6 +781,232 @@ cgexpr_int(Cg *c, long long v)
ins2(c, A_MOVQ, aimm(v), areg(D_AX));
}
/* cg_widen_tag_remap — when widening from one tagged union to another,
* rewrite the source's variant tag at BP+slot_off+0 to use the dst
* union's variant indices. No-op when src and dst index orders coincide.
*
* Mirrors Hare's tagged-subset assignment: a value of type (A|B) flows
* into (A|B|C) by re-tagging the discriminator to the position the
* variant occupies in the wider union. Both must already match by
* cg_variant_match — the checker enforces that.
*
* Emits a CMPQ-chain switch over the source tag because w6a has no
* CMOVQ encoding. The chain is linear in nvariants; in practice tagged
* unions are small. */
static void
cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off)
{
if (du == NULL || du->kind != TY_TAGGED) return;
if (su == NULL || su->kind != TY_TAGGED) return;
int identity = 1, idx = 0;
for (Tparam *p = su->params; p; p = p->next, idx++) {
int di = cg_tag_for_variant(du, p->type);
if (di < 0) di = 0;
if (di != idx) { identity = 0; break; }
}
if (identity) return;
const char *done = mklabel(c, "remap_done");
ins2(c, A_MOVQ, amem(D_BP, slot_off + 0), areg(D_AX));
idx = 0;
for (Tparam *p = su->params; p; p = p->next, idx++) {
const char *next = mklabel(c, "remap_next");
int di = cg_tag_for_variant(du, p->type);
if (di < 0) di = 0;
ins2(c, A_CMPQ, aimm(idx), areg(D_AX));
ins1(c, A_JNE, abranch(next));
ins2(c, A_MOVQ, aimm(di), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
ins1(c, A_JMP, abranch(done));
label(c, next);
}
label(c, done);
}
/* cg_widen_tagged_store — write the tagged-union slot bytes for `src`
* into BP+slot_off, sized to `sz` (8 for nullable fold, else 16/24+).
* Used by call-site widening (via cg_widen_tagged_push) and by the
* let/assign/return/struct-field-init paths.
*
* Branches by source shape (tagged_arg_size > 0 source counts as a
* tagged subset — possibly with different variant indices):
* - nullable: dst is folded (*T|void); store pointer at +0.
* - tagged ident: byte-copy slot words then remap tag at +0.
* - tagged expression: cgexpr leaves AX=tag, DX=val0, [CX=val1] —
* spill into slot then remap.
* - struct ident: zero-fill, byte-copy struct words to +8.
* - struct literal: zero-fill, store each field at slot+8+field_off.
* - str: cgexpr leaves AX=ptr, BX=len.
* - scalar: cgexpr leaves AX; store at +8 with zero pad. */
static void
cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
int slot_off, int sz)
{
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
if (du == NULL || du->kind != TY_TAGGED) return;
if (du->nullable) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
return;
}
Type *st = src ? src->type : NULL;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
/* Tagged → tagged subset: copy slot words then tag-remap. */
if (su && su->kind == TY_TAGGED) {
int ssz = (int)su->size;
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot_off + k));
}
} else {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot_off + 0));
if (ssz > 8)
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, slot_off + 8));
if (ssz > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, slot_off + 16));
}
if (ssz < sz) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = ssz; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot_off + k));
}
cg_widen_tag_remap(c, du, su, slot_off);
return;
}
/* Struct payload: zero the whole slot, then write fields/words
* at slot+8+ — keeping the tag word at slot+0 from the zero-fill,
* then patch it with the variant tag. */
if (su && su->kind == TY_STRUCT) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot_off + k));
int tag = cg_tag_for_variant(du, st);
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
int ssz = (int)su->size;
int k = 0;
while (k + 8 <= ssz) {
ins2(c, A_MOVQ, amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot_off + 8 + k));
k += 8;
}
if (k < ssz) {
/* Tail word: load with the right width to
* avoid stepping past the source slot. The
* zero-fill above means trailing slop is
* already clean. */
int tail = ssz - k;
int lop = (tail == 4) ? A_MOVL :
(tail == 1 ? A_MOVB : A_MOVQ);
ins2(c, lop,
amem(D_BP, soff + k), areg(D_AX));
ins2(c, lop, areg(D_AX),
amem(D_BP, slot_off + 8 + k));
}
} else if (src->kind == N_STRUCTLIT) {
for (Node *f = src->list; f; f = f->next) {
u64 foff = 0;
int fsz = 8;
Type *ftype = NULL;
for (Tfield *fl = su->fields; fl; fl = fl->next) {
if (strcmp(fl->name, f->str) == 0) {
foff = fl->offset;
fsz = (int)(fl->type ? fl->type->size : 8);
ftype = fl->type;
break;
}
}
cgexpr(c, f->lhs, *locals_p);
int sl_isf32 = 0;
if (fld_isfloat(ftype, &sl_isf32)) {
int mov = sl_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov, areg(D_X0),
amem(D_BP, slot_off + 8 + (int)foff));
continue;
}
Type *fu = (ftype && ftype->kind == TY_NAMED)
? ftype->under : ftype;
if (fu && fu->kind == TY_STR) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot_off + 8 + (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, slot_off + 8 + (int)foff + 8));
continue;
}
int op = A_MOVQ;
if (fsz == 1) op = A_MOVB;
else if (fsz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX),
amem(D_BP, slot_off + 8 + (int)foff));
}
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, slot_off + 0));
return;
}
/* str payload: AX=ptr, BX=len from cgexpr. */
if (type_isstr(st) || (su && su->kind == TY_STR)) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, slot_off + 16));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, slot_off + 0));
return;
}
/* Scalar / pointer / etc. The high slot word (when sz > 16) is
* left untouched here — match dispatches on the tag word first
* and only the str branch reads slot+16, so leaving the pad
* uninitialised in let/assign matches the pre-refactor asm.
* cg_widen_tagged_push pre-zeroes the scratch slot before
* calling us, so the call-site push still sees clean pad. */
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, slot_off + 0));
}
/* cg_widen_tagged_push — call-site widening. Materialise the tagged
* value in a stack scratch slot then push slot words high→low so the
* arg-register pop drain sees tag first, then payload words. */
static void
cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
{
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
if (du && du->nullable) {
/* Single 8B slot: just push the pointer/null. */
cgexpr(c, src, *locals_p);
ins1(c, A_PUSHQ, areg(D_AX));
return;
}
const char *scr_name = mklabel(c, "argscr");
int scr = local_alloc(c, locals_p, scr_name, sz, cg_frame);
/* Zero the scratch slot first so any pad word the store path
* leaves untouched (scalar variant in a >16B slot, struct payload
* shorter than the slot's value area) reads as 0 on the callee.
* The store path then writes the variant bytes over the zeros. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
cg_widen_tagged_store(c, locals_p, dst, src, scr, sz);
int nwords = sz / 8;
for (int k = nwords - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
}
static void
cgexpr(Cg *c, Node *n, Local *locals)
{
@@ -1570,9 +1799,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
}
/* Plain `r = expr;` where r is a tagged-union local. Mirrors
* the let-init path: forward AX:DX[:CX] when rhs is itself
* tagged, else synthesise the tag from rhs's static type. */
/* Plain `r = expr;` where r is a tagged-union local.
* Delegates to cg_widen_tagged_store: covers nullable fold,
* tagged→tagged (with tag remap), struct payload (ident or
* literal), str payload, and scalar payload. */
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
&& n->lhs->type) {
Type *lt = n->lhs->type;
@@ -1580,31 +1810,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (lu && lu->kind == TY_TAGGED) {
int off = localfind(locals, n->lhs->str);
if (off == 0) break;
Type *rt = n->rhs ? n->rhs->type : NULL;
if (type_istagged(rt)) {
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, off + 8));
if (lu->size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, off + 16));
} else {
int tag = cg_tag_for_variant(lu, rt);
cgexpr(c, n->rhs, locals);
if (type_isstr(rt)) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, off + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + 8));
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, off + 0));
}
cg_widen_tagged_store(c, &locals, lu, n->rhs,
off, (int)lu->size);
break;
}
}
@@ -2072,9 +2279,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
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). */
/* 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};
@@ -2084,18 +2294,27 @@ cgexpr(Cg *c, Node *n, Local *locals)
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;
if (psz > 0) {
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] = psz;
widen_param[i] = p->type;
}
}
p = p->next;
}
}
/* eval right-to-left, push to stack */
/* 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 (node_isslice(args[i]) && args[i]->kind == N_IDENT) {
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 */
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX));
@@ -2153,7 +2372,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */
continue;
}
if (node_isstructarg(args[i]) && args[i]->kind == N_IDENT) {
if (!widen[i] && node_isstructarg(args[i]) && args[i]->kind == N_IDENT) {
/* load qword(s) directly from the struct's slot */
int off = localfind(locals, args[i]->str);
int sz = struct_arg_size(args[i]->type);
@@ -2165,7 +2384,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_AX));
continue;
}
if (node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) {
if (!widen[i] && node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) {
/* Tagged-union: push each 8B word from the slot.
* High word goes first so the popper drains them
* in low→high order into the arg-register class. */
@@ -2182,41 +2401,25 @@ cgexpr(Cg *c, Node *n, Local *locals)
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));
}
* lay out the value in the parameter's slot
* shape, then push high→low so pop drains tag
* first.
*
* Branches by source shape:
* - nullable (sz==8): pointer IS the disc.
* - str: tag@+0, ptr@+8, len@+16.
* - struct ident: copy struct words then
* prepend tag, zero-pad to slot size.
* - struct literal: materialise via a stack
* scratch slot — store each field at its
* struct-relative offset (with the +8 tag
* shift), zero-fill, then push from slot.
* - tagged source: load src slot words, remap
* the tag word via cg_widen_tag_remap, pad
* to wider dst slot, push.
* - scalar: tag@+0, value@+8, optional pad. */
cg_widen_tagged_push(c, &locals, widen_param[i],
args[i], widen_sz[i]);
continue;
}
cgexpr(c, args[i], locals);
@@ -3327,47 +3530,13 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
break;
}
}
/* Tagged-union initialiser. Two shapes:
* 1) rhs already produces a tagged-union value (e.g. a fn
* call returning (T | E)). cgexpr leaves AX=tag,
* DX=value0[, CX=value1] — copy each into the slot.
* 2) rhs is a bare variant value (e.g. `let r: (i64|i32) = 7`).
* Synthesise the tag from rhs's static type and store it
* alongside the value. str-typed rhs flows as
* (AX=ptr, BX=len) so we store both halves.
*
* Nullable folded `(*T | void)`: the slot is a single 8B
* pointer. Both the value-from-call and bare-variant paths
* simplify to "spill AX". void variant stores 0; *T variant
* stores the pointer. */
/* Tagged-union initialiser. Delegates to cg_widen_tagged_store,
* which handles nullable fold, tagged→tagged (with tag remap
* when variant indices differ), struct payload (ident or
* literal — field-by-field at slot+8+field_off), str payload,
* and scalar payload (with zero-pad to the slot size). */
if (n->rhs && lu && lu->kind == TY_TAGGED) {
Type *rt = n->rhs->type;
if (lu->nullable) {
cgexpr(c, n->rhs, *locals);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + 0));
} else if (type_istagged(rt)) {
cgexpr(c, n->rhs, *locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8));
if (lu->size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, off + 16));
} else {
int tag = cg_tag_for_variant(lu, rt);
cgexpr(c, n->rhs, *locals);
if (type_isstr(rt)) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, off + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + 8));
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, off + 0));
}
cg_widen_tagged_store(c, locals, lu, n->rhs, off, sz);
break;
}
/* slice expression initialiser: build a {ptr, len, cap} header
@@ -3472,37 +3641,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
break;
}
}
/* Tagged-union field: synthesise tag from
* f->lhs's static type and store value bytes
* (1 or 2 words for ≤8B/str variants). */
/* Tagged-union field: delegate to the shared
* widening writer. Handles str, scalar, struct
* literal/ident payload, and tagged-subset
* forwarding (with tag remap). The field's slot
* starts at off+foff inside the struct slot. */
Type *fu = (ft && ft->kind == TY_NAMED)
? ft->under : ft;
if (fu && fu->kind == TY_TAGGED) {
Type *vt = f->lhs ? f->lhs->type : NULL;
int tag = cg_tag_for_variant(fu, vt);
cgexpr(c, f->lhs, *locals);
if (type_isstr(vt)) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + (int)foff + 8));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, off + (int)foff + 16));
} else if (type_istagged(vt)) {
/* forwarding a tagged value */
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, off + (int)foff + 8));
if (fu->size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, off + (int)foff + 16));
/* AX already holds the tag */
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + (int)foff + 0));
continue;
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + (int)foff + 8));
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, off + (int)foff + 0));
cg_widen_tagged_store(c, locals, fu,
f->lhs, off + (int)foff,
(int)fu->size);
continue;
}
cgexpr(c, f->lhs, *locals);
@@ -3615,18 +3764,25 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
if (rt->kind == TY_NAMED) rt = rt->under;
if (rt && rt->kind == TY_TAGGED) {
Type *vt = n->lhs->type;
cgexpr(c, n->lhs, *locals);
Type *vu = (vt && vt->kind == TY_NAMED)
? vt->under : vt;
int istagged = vu && vu->kind == TY_TAGGED;
int passthrough = istagged && (vu == rt ||
type_eq(vt, cg_ret_type));
int isstruct = vu && vu->kind == TY_STRUCT;
if (rt->nullable) {
/* AX already holds the pointer (or
* 0 if the value was `nil` / `void`).
* No tag word, no shuffle. */
} else if (!type_istagged(vt)) {
cgexpr(c, n->lhs, *locals);
} else if (passthrough) {
/* same tagged type: forward AX/DX/CX. */
cgexpr(c, n->lhs, *locals);
} else if (!istagged && !isstruct) {
/* str / scalar variant: synthesise the
* tag in AX and shuffle the value into
* DX[/CX]. Direct register path keeps
* the asm short — no scratch slot. */
int tag = cg_tag_for_variant(rt, vt);
cgexpr(c, n->lhs, *locals);
if (type_isstr(vt)) {
/* AX=ptr, BX=len → DX=ptr,
* CX=len, AX=tag. Move BX
* before AX since the tag
* MOVQ trashes AX. */
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
@@ -3637,6 +3793,33 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
areg(D_AX));
} else {
/* Struct variant or tagged-subset:
* materialise the widened value in a
* scratch slot, then load AX/DX/CX
* from the slot. Struct literal: field
* stores; struct ident: word copy;
* tagged subset: copy + tag remap. */
int sz = (int)rt->size;
const char *scrn = mklabel(c, "retscr");
int scr = local_alloc(c, locals, scrn,
sz, cg_frame);
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + k));
cg_widen_tagged_store(c, locals, rt,
n->lhs, scr, sz);
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
areg(D_AX));
if (sz > 8)
ins2(c, A_MOVQ,
amem(D_BP, scr + 8),
areg(D_DX));
if (sz > 16)
ins2(c, A_MOVQ,
amem(D_BP, scr + 16),
areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));

View File

@@ -323,9 +323,16 @@ resolve_type(Checker *c, Node *n)
for (Node *e = n->list; e; e = e->next) {
Type *vt = resolve_type(c, e);
if (vt == ty_never) continue;
if (vt && vt->kind == TY_TAGGED) {
/* flatten anonymous nested tagged */
for (Tparam *src = vt->params; src; src = src->next) {
int spread = (e->op == TK_ELLIPSIS);
/* `...inner` spread: flatten the variants of the
* (possibly NAMED) inner tagged union into the
* enclosing union — matches Hare's parse-time
* unwrap flag on each tagged_type entry. */
Type *vu = spread && vt && vt->kind == TY_NAMED
? vt->under : vt;
if (vu && vu->kind == TY_TAGGED &&
(spread || vt->kind == TY_TAGGED)) {
for (Tparam *src = vu->params; src; src = src->next) {
Type *st = src->type;
if (st == ty_never) continue;
if (variant_present(head, st)) continue;

View File

@@ -292,14 +292,27 @@ parsetype(Parser *p)
/* Three forms inside the parens:
* (T) — parenthesised single type
* (T, T2, ...) — tuple type
* (T | T2 | ...) — tagged-union type (Hare-style sum) */
* (T | T2 | ...) — tagged-union type (Hare-style sum)
*
* Each tagged variant may be prefixed with `...` to mark
* a spread: when the variant resolves to another tagged
* union its variants are flattened into the enclosing
* union. We tag the spread on Node.op = TK_ELLIPSIS so
* resolve_type can distinguish intent (today the checker
* flattens any nested tagged unconditionally, matching
* Hare's structural-equivalence rule, but the marker is
* preserved for future nominal handling). */
advance(p);
int first_spread = accept(p, TK_ELLIPSIS);
Node *first = parsetype(p);
if (first_spread) first->op = TK_ELLIPSIS;
if (accept(p, TK_PIPE)) {
Node *t = newnode(p->a, N_TTAGGED, pp);
Node *head = first, *tail = first;
for (;;) {
int spread = accept(p, TK_ELLIPSIS);
Node *e = parsetype(p);
if (spread) e->op = TK_ELLIPSIS;
tail->next = e;
tail = e;
if (!accept(p, TK_PIPE)) break;
@@ -308,6 +321,10 @@ parsetype(Parser *p)
t->list = head;
return t;
}
if (first_spread) {
errorf(pp, "spread '...' only valid before tagged-union variants");
p->errs++;
}
if (!accept(p, TK_COMMA)) {
expect(p, TK_RPAREN);
return first;

View File

@@ -243,6 +243,19 @@ type_eq(Type *a, Type *b)
}
return pa == NULL && pb == NULL;
}
case TY_TAGGED: {
/* Tagged unions are structurally equal iff variant lists
* match position-by-position. Nullable fold is a per-Type
* flag, so equal-up-to-fold types compare not-equal here —
* the caller can unwrap intentionally if needed. */
if (a->nullable != b->nullable) return 0;
Tparam *pa = a->params, *pb = b->params;
while (pa && pb) {
if (!type_eq(pa->type, pb->type)) return 0;
pa = pa->next; pb = pb->next;
}
return pa == NULL && pb == NULL;
}
default: return 1; /* primitives */
}
}
@@ -255,11 +268,14 @@ type_assignable(Type *dst, Type *src)
if (src == ty_never) return 1; /* bottom flows into anything */
if (type_eq(dst, src)) return 1;
/* Tagged-union variant inclusion: src is one of dst's variants.
* Checked before the untyped branch so untyped literals (e.g.
* 0, "msg") flow through to a variant's typed slot. Unwraps a
* named alias on either side so `type result = (T | E);` also
* accepts variants and the inverse. */
/* Tagged-union assignment:
* - concrete → tagged: src must match one of dst's variants.
* - tagged → tagged: src is assignable when every variant of
* src appears as a variant of dst (Hare-style subset). Tag
* remap at the use site handles different variant indices.
* Checked before the untyped branch so untyped literals flow
* through to a variant's typed slot. Unwraps a NAMED alias on
* either side so `type result = (T|E);` accepts variants too. */
{
Type *du = (dst->kind == TY_NAMED) ? dst->under : dst;
Type *su = (src->kind == TY_NAMED) ? src->under : src;
@@ -269,6 +285,18 @@ type_assignable(Type *dst, Type *src)
if (type_assignable(p->type, src)) return 1;
return 0;
}
if (du && du->kind == TY_TAGGED &&
su && su->kind == TY_TAGGED) {
for (Tparam *sp = su->params; sp; sp = sp->next) {
int ok = 0;
for (Tparam *dp = du->params; dp; dp = dp->next)
if (type_eq(dp->type, sp->type)) {
ok = 1; break;
}
if (!ok) return 0;
}
return 1;
}
}
/* Untyped → typed: only if the typed kind can hold the value. */

View File

@@ -255,14 +255,24 @@ fn parsetype(p: *parser) *node = {
if (p.curkind == tkind.TK_LPAREN) {
// (T) or (T, T, ...) or (T | T | ...)
//
// Each tagged variant may be prefixed with `...` to mark a
// spread — when the variant resolves to another tagged union
// its variants are flattened into the enclosing union. We
// tag the spread on node.op = TK_ELLIPSIS so resolve_type
// can distinguish intent. Mirrors C parsetype.
advance(p);
let firstspread: bool = accepttok(p, tkind.TK_ELLIPSIS);
let first: *node = parsetype(p);
if (firstspread) { first.op = tkind.TK_ELLIPSIS; };
if (accepttok(p, tkind.TK_PIPE)) {
let n: *node = newnode(p.a, nkind.N_TTAGGED, pf, pl, pc);
let head: *node = first;
let tail: *node = first;
for (true) {
let spread: bool = accepttok(p, tkind.TK_ELLIPSIS);
let e: *node = parsetype(p);
if (spread) { e.op = tkind.TK_ELLIPSIS; };
tail.next = e;
tail = e;
if (!accepttok(p, tkind.TK_PIPE)) { break; };
@@ -271,6 +281,9 @@ fn parsetype(p: *parser) *node = {
n.list = head;
return n;
};
if (firstspread) {
errmsg(p, "spread '...' only valid before tagged-union variants");
};
if (!accepttok(p, tkind.TK_COMMA)) {
expecttok(p, tkind.TK_RPAREN, "expected ')' after parenthesised type");
return first;

View File

@@ -3952,14 +3952,24 @@ fn parsetype(p: *parser) *node = {
if (p.curkind == tkind.TK_LPAREN) {
// (T) or (T, T, ...) or (T | T | ...)
//
// Each tagged variant may be prefixed with `...` to mark a
// spread — when the variant resolves to another tagged union
// its variants are flattened into the enclosing union. We
// tag the spread on node.op = TK_ELLIPSIS so resolve_type
// can distinguish intent. Mirrors C parsetype.
advance(p);
let firstspread: bool = accepttok(p, tkind.TK_ELLIPSIS);
let first: *node = parsetype(p);
if (firstspread) { first.op = tkind.TK_ELLIPSIS; };
if (accepttok(p, tkind.TK_PIPE)) {
let n: *node = newnode(p.a, nkind.N_TTAGGED, pf, pl, pc);
let head: *node = first;
let tail: *node = first;
for (true) {
let spread: bool = accepttok(p, tkind.TK_ELLIPSIS);
let e: *node = parsetype(p);
if (spread) { e.op = tkind.TK_ELLIPSIS; };
tail.next = e;
tail = e;
if (!accepttok(p, tkind.TK_PIPE)) { break; };
@@ -3968,6 +3978,9 @@ fn parsetype(p: *parser) *node = {
n.list = head;
return n;
};
if (firstspread) {
errmsg(p, "spread '...' only valid before tagged-union variants");
};
if (!accepttok(p, tkind.TK_COMMA)) {
expecttok(p, tkind.TK_RPAREN, "expected ')' after parenthesised type");
return first;

View File

@@ -3952,14 +3952,24 @@ fn parsetype(p: *parser) *node = {
if (p.curkind == tkind.TK_LPAREN) {
// (T) or (T, T, ...) or (T | T | ...)
//
// Each tagged variant may be prefixed with `...` to mark a
// spread — when the variant resolves to another tagged union
// its variants are flattened into the enclosing union. We
// tag the spread on node.op = TK_ELLIPSIS so resolve_type
// can distinguish intent. Mirrors C parsetype.
advance(p);
let firstspread: bool = accepttok(p, tkind.TK_ELLIPSIS);
let first: *node = parsetype(p);
if (firstspread) { first.op = tkind.TK_ELLIPSIS; };
if (accepttok(p, tkind.TK_PIPE)) {
let n: *node = newnode(p.a, nkind.N_TTAGGED, pf, pl, pc);
let head: *node = first;
let tail: *node = first;
for (true) {
let spread: bool = accepttok(p, tkind.TK_ELLIPSIS);
let e: *node = parsetype(p);
if (spread) { e.op = tkind.TK_ELLIPSIS; };
tail.next = e;
tail = e;
if (!accepttok(p, tkind.TK_PIPE)) { break; };
@@ -3968,6 +3978,9 @@ fn parsetype(p: *parser) *node = {
n.list = head;
return n;
};
if (firstspread) {
errmsg(p, "spread '...' only valid before tagged-union variants");
};
if (!accepttok(p, tkind.TK_COMMA)) {
expecttok(p, tkind.TK_RPAREN, "expected ')' after parenthesised type");
return first;

View File

@@ -828,6 +828,133 @@ static const struct row rows[] = {
" };\n"
" return 0;\n"
"};", 3 },
/* Struct variant of a tagged union at the call site. The arg is
* an N_STRUCTLIT, the param is (str|point). Widening at the call
* site must zero-fill the scratch slot, store each field at
* slot+8+field_off, then push the slot words high→low. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn classify(r: (str | point)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};\n"
"fn main() i32 = {\n"
" return classify(point { x = 10, y = 20 });\n"
"};", 30 },
/* Struct variant passed as a typed local. Widening copies the
* struct words from the local into the scratch slot at +8. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn classify(r: (str | point)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};\n"
"fn main() i32 = {\n"
" let p: point = point { x = 11, y = 22 };\n"
" return classify(p);\n"
"};", 33 },
/* let-init of a tagged-union local from a struct literal: the
* field stores go into slot+8+field_off in-place; tag patched
* last. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn main() i32 = {\n"
" let r: (str | point) = point { x = 7, y = 35 };\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};", 42 },
/* Reassign a tagged-union local to a struct literal. Same path
* as let-init but writing into an already-allocated slot. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn main() i32 = {\n"
" let r: (str | point) = \"init\";\n"
" r = point { x = 100, y = 23 };\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};", 123 },
/* Return a struct variant of the fn's tagged return type. The
* scratch-slot path materialises the struct payload then loads
* AX/DX/CX from it. */
{ "type point = struct { x: i32, y: i32 };\n"
"fn make() (str | point) = {\n"
" return point { x = 12, y = 30 };\n"
"};\n"
"fn main() i32 = {\n"
" let r: (str | point) = make();\n"
" match (r) {\n"
" case let s: str => return 0 - s.len: i32;\n"
" case let p: point => return p.x + p.y;\n"
" };\n"
" return -1;\n"
"};", 42 },
/* Widen a smaller tagged union to a wider one across slot sizes
* AND remapped variant indices. (i32 | rune) is 16B with i32 at
* tag 0; (str | i32 | rune) is 24B with i32 at tag 1. The widen
* path copies the slot words, zero-pads to 24B, then runs a
* CMPQ-chain switch to remap src tag 0 → dst tag 1. */
{ "fn classify(r: (str | i32 | rune)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 1;\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32 + 100;\n"
" };\n"
" return 0;\n"
"};\n"
"fn main() i32 = {\n"
" let inner: (i32 | rune) = 42: i32;\n"
" return classify(inner);\n"
"};", 42 },
/* Same shape but the rune variant of inner exercises the tag
* remap from src tag 1 → dst tag 2. The rune literal needs an
* explicit `: rune` cast — `'A'` is an untyped rune and the
* variant search picks the first variant that accepts it (i32,
* which also accepts untyped runes). 'A' = 65 + 100 = 165. */
{ "fn classify(r: (str | i32 | rune)) i32 = {\n"
" match (r) {\n"
" case let s: str => return 1;\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32 + 100;\n"
" };\n"
" return 0;\n"
"};\n"
"fn main() i32 = {\n"
" let inner: (i32 | rune) = 'A': rune;\n"
" return classify(inner);\n"
"};", 165 },
/* let-init of a wider tagged union from a smaller-tagged local. */
{ "fn main() i32 = {\n"
" let inner: (i32 | rune) = 42: i32;\n"
" let r: (str | i32 | rune) = inner;\n"
" match (r) {\n"
" case let s: str => return 1;\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32 + 100;\n"
" };\n"
" return 0;\n"
"};", 42 },
/* Spread variant in tagged-union type — `(...inner | T)`. The
* checker flattens the spread's variants into the enclosing
* union so `outer` has variants {i32, rune, str}. */
{ "type inner = (i32 | rune);\n"
"type outer = (...inner | str);\n"
"fn main() i32 = {\n"
" let r: outer = 42: i32;\n"
" match (r) {\n"
" case let n: i32 => return n;\n"
" case let c: rune => return c: i32;\n"
" case let s: str => return 0;\n"
" };\n"
" return -1;\n"
"};", 42 },
/* Plan 9-style sentinel error idiom: `def NAME: error = "lit"`
* inlines as the (ptr, len) pair at use sites. */
{ "type error = str;\n"