diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 6190aa37..fe920239 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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)); diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 0003d499..82ea3faa 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 5e75d06d..2886ea0e 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -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; diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index 80a28fb1..19dbd138 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -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. */ diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index 6f738fe9..c09e23a1 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -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; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3c810829..f910a7af 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 98ca125c..99ca2ef3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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; diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 6d2367d2..b15cda92 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -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"