diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index fe920239..51c86bb6 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1760,6 +1760,58 @@ cgexpr(Cg *c, Node *n, Local *locals) int is_ptr = u && u->kind == TY_PTR; int esz = (u && u->sub) ? (int)u->sub->size : 1; int elem_is_str = u && u->sub && type_isstr(u->sub); + Type *esub = u ? u->sub : NULL; + Type *esubu = (esub && esub->kind == TY_NAMED) + ? esub->under : esub; + int elem_tagged = esubu && esubu->kind == TY_TAGGED; + /* Tagged-union element: route widening through a + * scratch slot, then copy slot bytes to &arr[i]. + * Materialising into the scratch first lets us reuse + * the full cg_widen_tagged_store machinery — scalar / + * str / struct / subset payloads, tag remap, nullable + * fold — without duplicating it. The scratch lives in + * the function frame; no cleanup needed. */ + if ((is_arr || is_sl || is_ptr) && elem_tagged) { + int ssz = esz; + const char *scrn = mklabel(c, "idxscr"); + int scr = local_alloc(c, &locals, scrn, ssz, + cg_frame); + ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + for (int k = 0; k < ssz; k += 8) + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, scr + k)); + cg_widen_tagged_store(c, &locals, esubu, + n->rhs, scr, ssz); + /* Compute &arr[i] → BX. */ + cgexpr(c, n->lhs->rhs, locals); + if (ssz > 1) { + ins2(c, A_MOVQ, aimm(ssz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + if (base->kind == N_IDENT && is_arr) { + int boff = localfind(locals, base->str); + ins2(c, A_LEAQ, amem(D_BP, boff), + areg(D_BX)); + } else if (base->kind == N_IDENT) { + int boff = localfind(locals, base->str); + ins2(c, A_MOVQ, amem(D_BP, boff), + areg(D_BX)); + } else { + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, base, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + ins1(c, A_POPQ, areg(D_AX)); + } + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + /* Copy scratch slot → dest. */ + for (int k = 0; k < ssz; k += 8) { + ins2(c, A_MOVQ, amem(D_BP, scr + k), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BX, k)); + } + break; + } if (is_arr || is_sl || is_ptr) { cgexpr(c, n->rhs, locals); /* AX (and BX if str) */ /* str element: also stash len so we can store both */ @@ -3313,6 +3365,10 @@ cgexpr(Cg *c, Node *n, Local *locals) int esz = 1; if (u && u->sub) esz = (int)u->sub->size; if (u && u->kind == TY_STR) esz = 1; + Type *esub = u ? u->sub : NULL; + Type *esubu = (esub && esub->kind == TY_NAMED) + ? esub->under : esub; + int elem_tagged = esubu && esubu->kind == TY_TAGGED; if (n->lhs->kind == N_IDENT && u) { int off = localfind(locals, n->lhs->str); @@ -3337,6 +3393,22 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); break; } + /* tagged element: load slot words into (AX=tag, + * DX=val0, CX=val1) — matches the tagged-return ABI + * so let-init / match / call-arg paths consume it + * without spilling. Nullable folded element is one + * word in AX (caller treats it as a pointer). */ + if (elem_tagged) { + int ssz = (int)esubu->size; + if (ssz > 16) + ins2(c, A_MOVQ, amem(D_BX, 16), + areg(D_CX)); + if (ssz > 8) + ins2(c, A_MOVQ, amem(D_BX, 8), + areg(D_DX)); + ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); + break; + } int signed_elem = u && u->sub && ( u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || u->sub->kind == TY_I32); @@ -3368,6 +3440,19 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); break; } + /* tagged element via fallback base: AX holds the element + * address — copy to BX (the load into AX clobbers it), then + * load slot words. */ + if (elem_tagged) { + int ssz = (int)esubu->size; + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + if (ssz > 16) + ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX)); + if (ssz > 8) + ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); + break; + } { int signed_elem = u && u->sub && ( u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f910a7af..0fc60a28 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -5619,6 +5619,34 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { return rest + 1; }; if (widensz > 0) { + // Struct-payload widening into a tagged-union param uses + // @tagscr (zero + cgwidentaggedstore writes fields + tag, + // then push slot words high → low). Scalar / str go via + // the direct push fast path below — keeps wwstage's asm + // byte-identical to cstage for selfhost source. + let pname: str = rhsstructpayload(c, arg); + if (pname.len > 0) { + let ptype: *node = param.lhs; + let scroff: i32 = localadd(c, "@tagscr", 24, nil); + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < widensz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, ptype, arg, scroff, widensz); + let pp: i32 = widensz - 8; + for (pp >= 0) { + emitline("\tMOVQ\t"); + emitoff((scroff + pp): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + pp -= 8; + }; + return rest + widensz / 8; + }; cgexpr(c, arg); if (nodeisstr(c, arg)) { // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low @@ -6502,8 +6530,23 @@ fn slotsize(c: *cgen, typn: *node) i32 = { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; }; - }; + if (ps > 0) { esz = ps; } + else { + // Named struct / aliased type: size off + // the structinfo if present. + let si: *structinfo = structlookup(c, en); + if (si != nil) { esz = si.totsize; }; + }; + } else { if (elemn.kind == nkind.N_TTAGGED) { + // Tagged-union element: full slot (8 tag + + // padded max payload). Matches C cgen's + // resolve_type for `[N]TAGGED`. + esz = slotsize(c, elemn); + } else { if (elemn.kind == nkind.N_TPTR) { + esz = 8; + } else { if (elemn.kind == nkind.N_TSTRUCT) { + esz = slotsize(c, elemn); + }; }; }; }; }; return (esz: i64 * elen): i32; }; @@ -6971,21 +7014,40 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { if (rhs == nil) { return -1; }; let wantname: str = rhstargetname(c, rhs); if (wantname.len > 0) { - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (variantnamematch(v.str, wantname)) { return idx; }; - }; - v = v.next; - idx += 1; - }; + let r: i32 = flatvariantidx(c, tagged, wantname); + if (r >= 0) { return r; }; }; - // Fallback: by str-shape (resolves aliases). + // Fallback: by str-shape (resolves aliases). Walks the + // spread-flattened variant list so a `(...inner | str)` outer + // agrees with the (i32 | str) inner's str position. let wantstr: bool = nodeisstr(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + let ivisstr: bool = false; + if (iv.kind == nkind.N_TNAME) { + if (isstrtype(c, iv)) { ivisstr = true; }; + }; + if (ivisstr == wantstr) { return idx; }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; let visstr: bool = false; if (v.kind == nkind.N_TNAME) { if (isstrtype(c, v)) { visstr = true; }; @@ -6997,6 +7059,407 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { return -1; }; +// flatvariantidx — walk `tagged`'s variant list (with spread `...inner` +// expansion) and return the flat 0-based index where `want` matches. +// Mirrors check.c's spread flatten at type resolution: an outer +// `(...inner | T)` has the inner's variants inlined in declaration +// order, so the tag indices stay in sync between cstage (which +// resolves types upfront) and wwstage (which doesn't). Returns -1 if +// no variant matches. +fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { + if (tagged == nil) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + if (want.len == 0) { return -1; }; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + if (iv.kind == nkind.N_TNAME) { + if (variantnamematch(iv.str, want)) { + return idx; + }; + }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; + if (v.kind == nkind.N_TNAME) { + if (variantnamematch(v.str, want)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + return -1; +}; + +// cgwidentagremap — when widening from one tagged union to a wider one, +// rewrite the source's variant tag at slot_off+0 to use the destination's +// variant indices. No-op when src and dst index orders coincide. +// Mirrors cg_widen_tag_remap in cmd/w6c/cgen.c. +fn cgwidentagremap(c: *cgen, dst: *node, src: *node, slot_off: i32) void = { + if (dst == nil) { return; }; + if (src == nil) { return; }; + if (dst.kind != nkind.N_TTAGGED) { return; }; + if (src.kind != nkind.N_TTAGGED) { return; }; + let identity: bool = true; + let v: *node = src.list; + let idx: i32 = 0; + for (v != nil) { + let di: i32 = cgtagvariantidx(c, dst, v); + if (di < 0) { di = 0; }; + if (di != idx) { identity = false; v = nil; } + else { v = v.next; idx += 1; }; + }; + if (identity) { return; }; + let done: str = mklabel(c, "remap_done"); + emitline("\tMOVQ\t"); + emitoff(slot_off: i64); + emitline("(BP), AX\n"); + v = src.list; + idx = 0; + for (v != nil) { + let next: str = mklabel(c, "remap_next"); + let di: i32 = cgtagvariantidx(c, dst, v); + if (di < 0) { di = 0; }; + emitline("\tCMPQ\t$"); + emitint(idx: i64); + emitline(", AX\n"); + emitline("\tJNE\t"); + emitline(next); + emitline("\n"); + emitline("\tMOVQ\t$"); + emitint(di: i64); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + emitline("\tJMP\t"); + emitline(done); + emitline("\n"); + emitlabel(next); + v = v.next; + idx += 1; + }; + emitlabel(done); + return; +}; + +// rhsisstructpayload — is `src` a struct value (literal or local ident +// of a struct type)? Returns the struct name, or empty str. Only true +// when the name is registered in c.structs — `!void` / `!i32` aliases +// share the N_STRUCTLIT / N_TNAME shape but aren't structs, and must +// fall through to the scalar/str/tagged-source paths instead. +fn rhsstructpayload(c: *cgen, src: *node) str = { + let empty: str; + empty.ptr = nil; empty.len = 0; + if (src == nil) { return empty; }; + if (src.kind == nkind.N_STRUCTLIT) { + let trefn: *node = src.lhs; + if (trefn != nil) { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (trefn.kind == nkind.N_IDENT) { nm = trefn.str; }; + if (trefn.kind == nkind.N_TNAME) { nm = trefn.str; }; + if (nm.len > 0) { + if (structlookup(c, nm) != nil) { return nm; }; + }; + }; + return empty; + }; + if (src.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, src.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { + if (structlookup(c, tn.str) != nil) { + return tn.str; + }; + }; + }; + }; + }; + return empty; +}; + +// rhstaggedsource — return the tagged-type node for `src` when src is a +// tagged-typed local ident; nil otherwise. The slot-copy path uses this +// to walk variants for tag remap. +fn rhstaggedident(c: *cgen, src: *node) *node = { + if (src == nil) { return nil; }; + if (src.kind != nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, src.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (!istaggedtype(c, tn)) { return nil; }; + return resolvetagged(c, tn); +}; + +// rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX +// return ABI? True for N_CALL of a tagged-returning fn and N_INDEX of a +// tagged-element base. Used to decide whether cgexpr/spill works for the +// tagged-source branch of cgwidentaggedstore. +fn rhstaggedabicall(c: *cgen, src: *node) bool = { + if (src == nil) { return false; }; + if (src.kind == nkind.N_CALL) { + let callee: *node = src.lhs; + if (callee != nil) { + let calleename: str; + calleename.ptr = nil; calleename.len = 0; + if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; + if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + if (calleename.len > 0) { + let rt: *node = fnretlookup(c, calleename); + if (rt != nil) { + if (istaggedtype(c, rt)) { return true; }; + }; + }; + }; + return false; + }; + if (src.kind == nkind.N_INDEX) { + let base: *node = src.lhs; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bl: *local = localfindnode(c, base.str); + if (bl != nil) { + let btn: *node = bl.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + let elemt: *node = nil; + if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; + if (elemt != nil) { + if (istaggedtype(c, elemt)) { + return true; + }; + }; + }; + }; + }; + }; + }; + return false; +}; + +// cgwidentaggedstore — write tagged-union slot bytes for `src` into the +// slot at BP+slot_off, sized to slot_sz. Mirrors cg_widen_tagged_store +// in cmd/w6c/cgen.c. Branches by source shape: +// - nullable dst (8B slot): cgexpr → AX → slot+0. +// - tagged src ident: copy slot words, zero-pad, tag-remap. +// - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, +// spill words; no remap (callee already speaks dst tag order — or +// it doesn't, in which case the source is the wider one and remap +// would need a reversed direction we don't currently emit). +// - struct src (literal or ident): zero slot, write fields at +8+foff, +// tag last. +// - str src: tag@+0, ptr@+8, len@+16. +// - scalar src: tag@+0, value@+8. +fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { + let dt: *node = resolvetagged(c, dst); + if (dt == nil) { return; }; + // Nullable fold: one 8B word holding the pointer (or 0 for void). + if (isnullabletype(dst)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + // Tagged source ident: byte-copy slot words then tag-remap. + let st: *node = rhstaggedident(c, src); + if (st != nil) { + let lc: *local = localfindnode(c, src.str); + let ssz: i32 = slotsize(c, lc.tnode); + let soff: i32 = lc.off; + let k: i32 = 0; + for (k < ssz) { + emitline("\tMOVQ\t"); + emitoff((soff + k): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + k): i64); + emitline("(BP)\n"); + k += 8; + }; + if (ssz < slot_sz) { + emitline("\tXORQ\tAX, AX\n"); + let p: i32 = ssz; + for (p < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + p): i64); + emitline("(BP)\n"); + p += 8; + }; + }; + cgwidentagremap(c, dt, st, slot_off); + return; + }; + // Tagged source via AX/DX/CX register ABI (N_CALL, N_INDEX of + // tagged element). + if (rhstaggedabicall(c, src)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + if (slot_sz > 8) { + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + }; + if (slot_sz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((slot_off + 16): i64); + emitline("(BP)\n"); + }; + return; + }; + // Struct payload (literal or ident). + let sname: str = rhsstructpayload(c, src); + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + emitline("\tXORQ\tAX, AX\n"); + let zoff: i32 = 0; + for (zoff < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + zoff): i64); + emitline("(BP)\n"); + zoff += 8; + }; + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + if (src.kind == nkind.N_STRUCTLIT) { + let fnode: *node = src.list; + for (fnode != nil) { + if (fnode.kind == nkind.N_FIELD) { + let fname: str = fnode.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fname)) { + cgexpr(c, fnode.lhs); + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { + mov = "MOVSS"; + }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + } else { if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot_off + 8 + fi.foff + 8): i64); + emitline("(BP)\n"); + } else { + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + }; }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + fnode = fnode.next; + }; + } else { + // Struct ident source: byte-copy struct words to slot+8+k. + let lc: *local = localfindnode(c, src.str); + let soff: i32 = 0; + if (lc != nil) { soff = lc.off; }; + let stotal: i32 = si.totsize; + let ki: i32 = 0; + for (ki + 8 <= stotal) { + emitline("\tMOVQ\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8 + ki): i64); + emitline("(BP)\n"); + ki += 8; + }; + if (ki < stotal) { + let tail: i32 = stotal - ki; + let lop: str = "MOVQ"; + if (tail == 4) { lop = "MOVL"; } + else { if (tail == 1) { lop = "MOVB"; }; }; + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\t"); + emitline(lop); + emitline("\tAX, "); + emitoff((slot_off + 8 + ki): i64); + emitline("(BP)\n"); + }; + }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + }; + // Str payload. + if (nodeisstr(c, src)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot_off + 16): i64); + emitline("(BP)\n"); + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + // Scalar payload. + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; +}; + // MODULE: wcc // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // @@ -7112,7 +7575,7 @@ fn cgexpr(c: *cgen, n: *node) void = { // tagged-union type expression `tagged`. -1 if `tagged` isn't an // nkind.N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch // does inline; pulled out so `is` / `as` can reuse it. -fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { +fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; @@ -7120,16 +7583,7 @@ fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; if (want.len == 0) { return -1; }; - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (streq(v.str, want)) { return idx; }; - }; - v = v.next; - idx += 1; - }; - return -1; + return flatvariantidx(c, tagged, want); }; // cgtryprop — `e?` propagates the error variant up the stack. @@ -7246,7 +7700,7 @@ fn cgtypetest(c: *cgen, n: *node) void = { }; }; }; - let want: i32 = cgtagvariantidx(scrutt, n.rhs); + let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); @@ -7334,7 +7788,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; }; }; - let want: i32 = cgtagvariantidx(scrutt, n.rhs); + let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); emitline("\tMOVQ\t"); @@ -7566,6 +8020,43 @@ fn cgindex(c: *cgen, n: *node) void = { esz = indexbaseesz(c, base); };}; }; + // Tagged-union element: load slot words into (AX=tag, DX=val0, + // CX=val1) matching the tagged-return ABI so call-arg / let / + // match consumers see the same shape as a tagged-returning fn. + // Slot size = esz (8/16/24); nullable folded element is one + // word, which the fallthrough below handles via MOVQ AX. + let elem_tagged: bool = false; + let elem_slot_sz: i32 = esz; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bl: *local = baselocal; + let etn: *node = nil; + if (bl != nil) { + let btn: *node = bl.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + }; + } else { + let tn: *node = letvartnode(c, base.str); + if (tn != nil) { + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { etn = tn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = tn.lhs; }; + if (bk == nkind.N_TPTR) { etn = tn.lhs; }; + }; + }; + if (istaggedtype(c, etn)) { + if (!isnullabletype(etn)) { + elem_tagged = true; + elem_slot_sz = slotsize(c, etn); + esz = elem_slot_sz; + }; + }; + }; + }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -7584,6 +8075,16 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("(SB), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); + if (elem_tagged) { + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; if (esz == 16) { emitline("\tMOVQ\t8(BX), CX\n"); emitline("\tMOVQ\t(BX), AX\n"); @@ -7612,6 +8113,16 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); + if (elem_tagged) { + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; // str element (16B): load (ptr, len) into (AX, BX) so // the value flows through the str-rhs convention. if (esz == 16) { @@ -7633,6 +8144,19 @@ fn cgindex(c: *cgen, n: *node) void = { cgexpr(c, base); emitline("\tPOPQ\tBX\n"); emitline("\tADDQ\tBX, AX\n"); + if (elem_tagged) { + // AX holds the element address. Copy to BX (loading slot+0 + // into AX clobbers it), then read slot words. + emitline("\tMOVQ\tAX, BX\n"); + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; if (esz == 16) { emitline("\tMOVQ\t8(AX), BX\n"); emitline("\tMOVQ\t(AX), AX\n"); @@ -7747,15 +8271,16 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = resolvetagged(c, lc.tnode); }; } else { - // Non-ident scrutinee (call result, ?, etc.). Spill into a - // 24B `@match_spill` scratch slot and dispatch off it. - // Tagged returns follow the AX:DX:CX convention, so store - // all three words at +0/+8/+16; nullable returns are - // single-word (AX = ptr) and only read +0, so the extra - // stores are harmless. For N_CALL we recover the return - // type via fnretlookup so nullable dispatch can pick the - // pointer-vs-null discriminator. Mirrors C cgen's - // @match_spill path in cmd/w6c/cgen.c N_MATCH. + // Non-ident scrutinee (call result, arr[i], ?, etc.). + // Spill into a 24B `@match_spill` scratch slot and + // dispatch off it. Tagged returns (N_CALL) follow the + // AX:DX:CX convention; tagged-element loads (N_INDEX) + // after the cgindex fix produce the same triple. + // Nullable returns are single-word (AX = ptr); only +0 + // is read, so the extra stores are harmless. We + // recover the scrutinee type from fnretlookup (N_CALL) + // or the base local's array element type (N_INDEX) so + // dispatch can compute variant indices. scrutoff = localalloc(c, "@match_spill", 24, nil); if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; @@ -7770,6 +8295,27 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + if (scrut.kind == nkind.N_INDEX) { + let ibase: *node = scrut.lhs; + if (ibase != nil) { + if (ibase.kind == nkind.N_IDENT) { + let bl: *local = localfindnode(c, ibase.str); + let btn: *node = nil; + if (bl != nil) { btn = bl.tnode; } + else { btn = letvartnode(c, ibase.str); }; + if (btn != nil) { + let bk: nkind = btn.kind; + let etn: *node = nil; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + if (etn != nil) { + scrutt = resolvetagged(c, etn); + }; + }; + }; + }; + }; cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -7832,23 +8378,8 @@ fn cgmatch(c: *cgen, n: *node) void = { let patname: str; patname.ptr = nil; patname.len = 0; if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; - let v: *node = scrutt.list; - let idx: i32 = 0; - let found: bool = false; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (variantnamematch(v.str, patname)) { - want = idx; - found = true; - v = nil; - }; - }; - if (v != nil) { - v = v.next; - idx += 1; - }; - }; - if (!found) { want = 0; }; + let r: i32 = flatvariantidx(c, scrutt, patname); + if (r >= 0) { want = r; }; }; }; emitline("\tMOVQ\t"); @@ -9156,6 +9687,25 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Tagged-union local reassignment: `r = expr;` where r has a + // tagged-union type. Delegate to cgwidentaggedstore (same path + // as cglet's tagged-init). Covers nullable fold, tagged source, + // struct payload, str payload, scalar payload, with tag remap. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (n.op == tkind.TK_ASSIGN) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + let lsz: i32 = slotsize(c, lc.tnode); + cgwidentaggedstore(c, lc.tnode, + n.rhs, lc.off, lsz); + return; + }; + }; + }; + }; + }; // `*p = v` — deref-assign. Element width comes from the // pointer's declared type. Mirrors C cgen: eval rhs (AX, // and BX if str), push, eval pointer, pop value, store. @@ -9256,12 +9806,20 @@ fn cgassign(c: *cgen, n: *node) void = { let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; + let elemtn: *node = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); + let btn: *node = baselocal.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; + }; } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { @@ -9269,11 +9827,13 @@ fn cgassign(c: *cgen, n: *node) void = { isglobalarr = true; globalname = bn; esz = elemsizeof(tn); + elemtn = tn.lhs; }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeof(tn); + elemtn = tn.lhs; }; }; }; @@ -9281,6 +9841,80 @@ fn cgassign(c: *cgen, n: *node) void = { esz = indexbaseesz(c, base); };}; }; + // Tagged-union element: materialize source in a shared + // scratch slot via cgwidentaggedstore (handles struct / + // str / scalar / subset / nullable variants uniformly), + // then compute &arr[i] and byte-copy. The scratch + // (@tagscr) is reused across all tagged-arr stores in + // the function and counted once in scanlocals. + if (elemtn != nil) { + if (istaggedtype(c, elemtn)) { + let slot_sz: i32 = slotsize(c, elemtn); + let scroff: i32 = localadd(c, "@tagscr", + 24, nil); + // Pre-zero scratch (matches push helper). + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, elemtn, n.rhs, + scroff, slot_sz); + cgexpr(c, idx); + if (slot_sz > 1) { + emitline("\tMOVQ\t$"); + emitint(slot_sz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + let isarr: bool = false; + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isarr = true; + }; + }; + if (isarr) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { + emitline("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\tPOPQ\tAX\n"); + };};}; + emitline("\tADDQ\tAX, BX\n"); + let cc: i32 = 0; + for (cc < slot_sz) { + emitline("\tMOVQ\t"); + emitoff((scroff + cc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(cc: i64); + emitline("(BX)\n"); + cc += 8; + }; + return; + }; + }; cgexpr(c, n.rhs); // value → AX if (esz == 16) { emitline("\tPUSHQ\tBX\n"); }; emitline("\tPUSHQ\tAX\n"); @@ -10214,6 +10848,53 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; + // Struct payload or tagged-subset return — materialise + // the widened value in scratch via cgwidentaggedstore + // (handles tag remap and zero pad), then load AX/DX/CX + // from the slot. + let needswiden: bool = false; + if (!isnullabletype(c.fnret)) { + if (!forwardtagged) { + let sname: str = rhsstructpayload(c, rhs); + if (sname.len > 0) { needswiden = true; }; + if (rhstaggedident(c, rhs) != nil) { + needswiden = true; + }; + }; + }; + if (needswiden) { + let rsz: i32 = slotsize(c, c.fnret); + let scroff: i32 = localadd(c, "@tagscr", + 24, nil); + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < rsz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, c.fnret, rhs, scroff, + rsz); + emitline("\tMOVQ\t"); + emitoff(scroff: i64); + emitline("(BP), AX\n"); + if (rsz > 8) { + emitline("\tMOVQ\t"); + emitoff((scroff + 8): i64); + emitline("(BP), DX\n"); + }; + if (rsz > 16) { + emitline("\tMOVQ\t"); + emitoff((scroff + 16): i64); + emitline("(BP), CX\n"); + }; + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; cgexpr(c, rhs); if (isnullabletype(c.fnret)) { emitline("\tMOVQ\tBP, SP\n"); @@ -10299,75 +10980,12 @@ fn cglet(c: *cgen, n: *node) void = { let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; - // Tagged-union init: `let r: (T | E) = expr;`. - // - If rhs is a CALL to a fn returning tagged-union, - // the result is already in (AX=tag, DX=v0, CX=v1); - // just spill all three. - // - Otherwise rhs is a bare variant value: pack tag + - // value(s). + // Tagged-union init: delegate to cgwidentaggedstore, which + // handles nullable fold, tagged source (ident or AX/DX/CX + // ABI call), struct payload (literal/ident), str payload, + // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { - let nullable: bool = isnullabletype(tn); - let rhsreturnstagged: bool = false; - if (rhs.kind == nkind.N_CALL) { - let callee: *node = rhs.lhs; - if (callee != nil) { - let calleename: str; - calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; - if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(c, rt)) { rhsreturnstagged = true; }; - }; - }; - }; - cgexpr(c, rhs); - if (nullable) { - // Slot is one 8B word; AX is the pointer - // (or 0 for null/void). Same path whether - // the rhs is a call or a bare variant. - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - c.lastwasreturn = 0; - return; - }; - if (rhsreturnstagged) { - // Spill size/8 registers (tag + value words). - // Slots smaller than 24 don't carry a CX word. - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - if (sz > 16) { - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - }; - c.lastwasreturn = 0; - return; - }; - let tagidx: i32 = taggedvariantindex(c, tn, rhs); - if (tagidx < 0) { tagidx = 0; }; - if (nodeisstr(c, rhs)) { - emitline("\tMOVQ\tAX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - }; - emitline("\tMOVQ\t$"); - emitint(tagidx: i64); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); + cgwidentaggedstore(c, tn, rhs, off, sz); c.lastwasreturn = 0; return; }; @@ -11198,6 +11816,16 @@ fn scanlocals(c: *cgen, n: *node) i32 = { if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; total += sz; }; + // Carry the let's tnode into the stub so scanlocals can + // dispatch on type later in the walk — e.g. detecting + // `arr[i] = ...` where arr is a tagged-element array, + // which needs an @tagscr scratch slot reservation. + let stub: *local = localfindnode(c, n.str); + if (stub != nil) { + if (stub.tnode == nil) { + if (n.lhs != nil) { stub.tnode = n.lhs; }; + }; + }; }; // Multi-let from a tuple-returning call: each binding's size // comes from its annotated type (l.lhs) when present, else from @@ -11305,6 +11933,128 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Tagged-arr/slice index store needs a 24B scratch slot + // (`@tagscr`) for cgwidentaggedstore to materialise the source + // in before copying to the element address. Reserved once per + // function (dedup'd via scanseenmark) regardless of how many + // tagged-arr stores the body contains. + if (n.kind == nkind.N_ASSIGN) { + let alhs: *node = n.lhs; + if (alhs != nil) { + if (alhs.kind == nkind.N_INDEX) { + let abase: *node = alhs.lhs; + if (abase != nil) { + if (abase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, abase.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, abase.str); }; + if (btn != nil) { + let bk: nkind = btn.kind; + let etn: *node = nil; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + if (etn != nil) { + if (istaggedtype(c, etn)) { + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + }; + }; + }; + }; + // Tagged-union return with struct payload or tagged-subset + // source — cgreturn materialises in @tagscr then loads + // AX/DX/CX. Detect via the same rhsstructpayload predicate + // the cgen uses, so we only reserve when the cgen will + // actually emit a scratch-using path. `!void` / `!i32` + // aliases share N_STRUCTLIT shape but resolve to + // non-struct types — they fall through to scalar/str and + // don't need scratch. + if (n.kind == nkind.N_RETURN) { + if (c.fnret != nil) { + if (istaggedtype(c, c.fnret)) { + if (!isnullabletype(c.fnret)) { + let rhs: *node = n.lhs; + let needs: bool = false; + if (rhs != nil) { + let sn: str = rhsstructpayload(c, rhs); + if (sn.len > 0) { needs = true; }; + if (rhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + needs = true; + }; + }; + }; + }; + if (needs) { + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + }; + // Call-site struct-payload widening uses @tagscr — when the + // arg is a struct literal/ident and the callee's param is + // tagged, pushargsrev materialises in scratch and pushes. + // Scalar / str args take the direct-push fast path (no + // scratch). Tagged-typed ident args also skip widening (the + // slot is already laid out, so pushargsrev pushes slot words + // directly). Both fast paths agree with C cgen bytewise, so + // only struct-payload sites get a scratch reservation. + if (n.kind == nkind.N_CALL) { + let callee: *node = n.lhs; + let cnm: str; + cnm.ptr = nil; cnm.len = 0; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + }; + if (cnm.len > 0) { + let ps: *node = fnparamslookup(c, cnm); + let a: *node = n.list; + for (a != nil) { + if (ps == nil) { a = nil; } + else { + if (ps.kind == nkind.N_PARAM) { + let pt: *node = ps.lhs; + if (istaggedtype(c, pt)) { + if (!isnullabletype(pt)) { + let sn: str = rhsstructpayload(c, a); + if (sn.len > 0) { + let isidentstruct: bool = false; + if (a.kind == nkind.N_IDENT) { + // Struct ident as + // tagged arg — pushargsrev + // still routes through the + // scratch path. + isidentstruct = true; + }; + let _u: bool = isidentstruct; + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + if (a != nil) { + a = a.next; + ps = ps.next; + }; + }; + }; + }; + }; if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; if (n.cond != nil) { total += scanlocals(c, n.cond); }; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index e4936ae9..aad9c606 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -38,6 +38,16 @@ fn scanlocals(c: *cgen, n: *node) i32 = { if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; total += sz; }; + // Carry the let's tnode into the stub so scanlocals can + // dispatch on type later in the walk — e.g. detecting + // `arr[i] = ...` where arr is a tagged-element array, + // which needs an @tagscr scratch slot reservation. + let stub: *local = localfindnode(c, n.str); + if (stub != nil) { + if (stub.tnode == nil) { + if (n.lhs != nil) { stub.tnode = n.lhs; }; + }; + }; }; // Multi-let from a tuple-returning call: each binding's size // comes from its annotated type (l.lhs) when present, else from @@ -145,6 +155,128 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Tagged-arr/slice index store needs a 24B scratch slot + // (`@tagscr`) for cgwidentaggedstore to materialise the source + // in before copying to the element address. Reserved once per + // function (dedup'd via scanseenmark) regardless of how many + // tagged-arr stores the body contains. + if (n.kind == nkind.N_ASSIGN) { + let alhs: *node = n.lhs; + if (alhs != nil) { + if (alhs.kind == nkind.N_INDEX) { + let abase: *node = alhs.lhs; + if (abase != nil) { + if (abase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, abase.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, abase.str); }; + if (btn != nil) { + let bk: nkind = btn.kind; + let etn: *node = nil; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + if (etn != nil) { + if (istaggedtype(c, etn)) { + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + }; + }; + }; + }; + // Tagged-union return with struct payload or tagged-subset + // source — cgreturn materialises in @tagscr then loads + // AX/DX/CX. Detect via the same rhsstructpayload predicate + // the cgen uses, so we only reserve when the cgen will + // actually emit a scratch-using path. `!void` / `!i32` + // aliases share N_STRUCTLIT shape but resolve to + // non-struct types — they fall through to scalar/str and + // don't need scratch. + if (n.kind == nkind.N_RETURN) { + if (c.fnret != nil) { + if (istaggedtype(c, c.fnret)) { + if (!isnullabletype(c.fnret)) { + let rhs: *node = n.lhs; + let needs: bool = false; + if (rhs != nil) { + let sn: str = rhsstructpayload(c, rhs); + if (sn.len > 0) { needs = true; }; + if (rhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + needs = true; + }; + }; + }; + }; + if (needs) { + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + }; + // Call-site struct-payload widening uses @tagscr — when the + // arg is a struct literal/ident and the callee's param is + // tagged, pushargsrev materialises in scratch and pushes. + // Scalar / str args take the direct-push fast path (no + // scratch). Tagged-typed ident args also skip widening (the + // slot is already laid out, so pushargsrev pushes slot words + // directly). Both fast paths agree with C cgen bytewise, so + // only struct-payload sites get a scratch reservation. + if (n.kind == nkind.N_CALL) { + let callee: *node = n.lhs; + let cnm: str; + cnm.ptr = nil; cnm.len = 0; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + }; + if (cnm.len > 0) { + let ps: *node = fnparamslookup(c, cnm); + let a: *node = n.list; + for (a != nil) { + if (ps == nil) { a = nil; } + else { + if (ps.kind == nkind.N_PARAM) { + let pt: *node = ps.lhs; + if (istaggedtype(c, pt)) { + if (!isnullabletype(pt)) { + let sn: str = rhsstructpayload(c, a); + if (sn.len > 0) { + let isidentstruct: bool = false; + if (a.kind == nkind.N_IDENT) { + // Struct ident as + // tagged arg — pushargsrev + // still routes through the + // scratch path. + isidentstruct = true; + }; + let _u: bool = isidentstruct; + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + if (a != nil) { + a = a.next; + ps = ps.next; + }; + }; + }; + }; + }; if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; if (n.cond != nil) { total += scanlocals(c, n.cond); }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 8a22611c..80409b29 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -112,7 +112,7 @@ fn cgexpr(c: *cgen, n: *node) void = { // tagged-union type expression `tagged`. -1 if `tagged` isn't an // nkind.N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch // does inline; pulled out so `is` / `as` can reuse it. -fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { +fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; @@ -120,16 +120,7 @@ fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; if (want.len == 0) { return -1; }; - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (streq(v.str, want)) { return idx; }; - }; - v = v.next; - idx += 1; - }; - return -1; + return flatvariantidx(c, tagged, want); }; // cgtryprop — `e?` propagates the error variant up the stack. @@ -246,7 +237,7 @@ fn cgtypetest(c: *cgen, n: *node) void = { }; }; }; - let want: i32 = cgtagvariantidx(scrutt, n.rhs); + let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); @@ -334,7 +325,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; }; }; - let want: i32 = cgtagvariantidx(scrutt, n.rhs); + let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); emitline("\tMOVQ\t"); @@ -566,6 +557,43 @@ fn cgindex(c: *cgen, n: *node) void = { esz = indexbaseesz(c, base); };}; }; + // Tagged-union element: load slot words into (AX=tag, DX=val0, + // CX=val1) matching the tagged-return ABI so call-arg / let / + // match consumers see the same shape as a tagged-returning fn. + // Slot size = esz (8/16/24); nullable folded element is one + // word, which the fallthrough below handles via MOVQ AX. + let elem_tagged: bool = false; + let elem_slot_sz: i32 = esz; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bl: *local = baselocal; + let etn: *node = nil; + if (bl != nil) { + let btn: *node = bl.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + }; + } else { + let tn: *node = letvartnode(c, base.str); + if (tn != nil) { + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { etn = tn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = tn.lhs; }; + if (bk == nkind.N_TPTR) { etn = tn.lhs; }; + }; + }; + if (istaggedtype(c, etn)) { + if (!isnullabletype(etn)) { + elem_tagged = true; + elem_slot_sz = slotsize(c, etn); + esz = elem_slot_sz; + }; + }; + }; + }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -584,6 +612,16 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("(SB), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); + if (elem_tagged) { + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; if (esz == 16) { emitline("\tMOVQ\t8(BX), CX\n"); emitline("\tMOVQ\t(BX), AX\n"); @@ -612,6 +650,16 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); + if (elem_tagged) { + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; // str element (16B): load (ptr, len) into (AX, BX) so // the value flows through the str-rhs convention. if (esz == 16) { @@ -633,6 +681,19 @@ fn cgindex(c: *cgen, n: *node) void = { cgexpr(c, base); emitline("\tPOPQ\tBX\n"); emitline("\tADDQ\tBX, AX\n"); + if (elem_tagged) { + // AX holds the element address. Copy to BX (loading slot+0 + // into AX clobbers it), then read slot words. + emitline("\tMOVQ\tAX, BX\n"); + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; if (esz == 16) { emitline("\tMOVQ\t8(AX), BX\n"); emitline("\tMOVQ\t(AX), AX\n"); @@ -747,15 +808,16 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = resolvetagged(c, lc.tnode); }; } else { - // Non-ident scrutinee (call result, ?, etc.). Spill into a - // 24B `@match_spill` scratch slot and dispatch off it. - // Tagged returns follow the AX:DX:CX convention, so store - // all three words at +0/+8/+16; nullable returns are - // single-word (AX = ptr) and only read +0, so the extra - // stores are harmless. For N_CALL we recover the return - // type via fnretlookup so nullable dispatch can pick the - // pointer-vs-null discriminator. Mirrors C cgen's - // @match_spill path in cmd/w6c/cgen.c N_MATCH. + // Non-ident scrutinee (call result, arr[i], ?, etc.). + // Spill into a 24B `@match_spill` scratch slot and + // dispatch off it. Tagged returns (N_CALL) follow the + // AX:DX:CX convention; tagged-element loads (N_INDEX) + // after the cgindex fix produce the same triple. + // Nullable returns are single-word (AX = ptr); only +0 + // is read, so the extra stores are harmless. We + // recover the scrutinee type from fnretlookup (N_CALL) + // or the base local's array element type (N_INDEX) so + // dispatch can compute variant indices. scrutoff = localalloc(c, "@match_spill", 24, nil); if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; @@ -770,6 +832,27 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + if (scrut.kind == nkind.N_INDEX) { + let ibase: *node = scrut.lhs; + if (ibase != nil) { + if (ibase.kind == nkind.N_IDENT) { + let bl: *local = localfindnode(c, ibase.str); + let btn: *node = nil; + if (bl != nil) { btn = bl.tnode; } + else { btn = letvartnode(c, ibase.str); }; + if (btn != nil) { + let bk: nkind = btn.kind; + let etn: *node = nil; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + if (etn != nil) { + scrutt = resolvetagged(c, etn); + }; + }; + }; + }; + }; cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -832,23 +915,8 @@ fn cgmatch(c: *cgen, n: *node) void = { let patname: str; patname.ptr = nil; patname.len = 0; if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; - let v: *node = scrutt.list; - let idx: i32 = 0; - let found: bool = false; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (variantnamematch(v.str, patname)) { - want = idx; - found = true; - v = nil; - }; - }; - if (v != nil) { - v = v.next; - idx += 1; - }; - }; - if (!found) { want = 0; }; + let r: i32 = flatvariantidx(c, scrutt, patname); + if (r >= 0) { want = r; }; }; }; emitline("\tMOVQ\t"); @@ -2156,6 +2224,25 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Tagged-union local reassignment: `r = expr;` where r has a + // tagged-union type. Delegate to cgwidentaggedstore (same path + // as cglet's tagged-init). Covers nullable fold, tagged source, + // struct payload, str payload, scalar payload, with tag remap. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (n.op == tkind.TK_ASSIGN) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + let lsz: i32 = slotsize(c, lc.tnode); + cgwidentaggedstore(c, lc.tnode, + n.rhs, lc.off, lsz); + return; + }; + }; + }; + }; + }; // `*p = v` — deref-assign. Element width comes from the // pointer's declared type. Mirrors C cgen: eval rhs (AX, // and BX if str), push, eval pointer, pop value, store. @@ -2256,12 +2343,20 @@ fn cgassign(c: *cgen, n: *node) void = { let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; + let elemtn: *node = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); + let btn: *node = baselocal.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; + }; } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { @@ -2269,11 +2364,13 @@ fn cgassign(c: *cgen, n: *node) void = { isglobalarr = true; globalname = bn; esz = elemsizeof(tn); + elemtn = tn.lhs; }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeof(tn); + elemtn = tn.lhs; }; }; }; @@ -2281,6 +2378,80 @@ fn cgassign(c: *cgen, n: *node) void = { esz = indexbaseesz(c, base); };}; }; + // Tagged-union element: materialize source in a shared + // scratch slot via cgwidentaggedstore (handles struct / + // str / scalar / subset / nullable variants uniformly), + // then compute &arr[i] and byte-copy. The scratch + // (@tagscr) is reused across all tagged-arr stores in + // the function and counted once in scanlocals. + if (elemtn != nil) { + if (istaggedtype(c, elemtn)) { + let slot_sz: i32 = slotsize(c, elemtn); + let scroff: i32 = localadd(c, "@tagscr", + 24, nil); + // Pre-zero scratch (matches push helper). + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, elemtn, n.rhs, + scroff, slot_sz); + cgexpr(c, idx); + if (slot_sz > 1) { + emitline("\tMOVQ\t$"); + emitint(slot_sz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + let isarr: bool = false; + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isarr = true; + }; + }; + if (isarr) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { + emitline("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\tPOPQ\tAX\n"); + };};}; + emitline("\tADDQ\tAX, BX\n"); + let cc: i32 = 0; + for (cc < slot_sz) { + emitline("\tMOVQ\t"); + emitoff((scroff + cc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(cc: i64); + emitline("(BX)\n"); + cc += 8; + }; + return; + }; + }; cgexpr(c, n.rhs); // value → AX if (esz == 16) { emitline("\tPUSHQ\tBX\n"); }; emitline("\tPUSHQ\tAX\n"); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 64a261f3..816c2b7d 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -162,6 +162,53 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; + // Struct payload or tagged-subset return — materialise + // the widened value in scratch via cgwidentaggedstore + // (handles tag remap and zero pad), then load AX/DX/CX + // from the slot. + let needswiden: bool = false; + if (!isnullabletype(c.fnret)) { + if (!forwardtagged) { + let sname: str = rhsstructpayload(c, rhs); + if (sname.len > 0) { needswiden = true; }; + if (rhstaggedident(c, rhs) != nil) { + needswiden = true; + }; + }; + }; + if (needswiden) { + let rsz: i32 = slotsize(c, c.fnret); + let scroff: i32 = localadd(c, "@tagscr", + 24, nil); + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < rsz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, c.fnret, rhs, scroff, + rsz); + emitline("\tMOVQ\t"); + emitoff(scroff: i64); + emitline("(BP), AX\n"); + if (rsz > 8) { + emitline("\tMOVQ\t"); + emitoff((scroff + 8): i64); + emitline("(BP), DX\n"); + }; + if (rsz > 16) { + emitline("\tMOVQ\t"); + emitoff((scroff + 16): i64); + emitline("(BP), CX\n"); + }; + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; cgexpr(c, rhs); if (isnullabletype(c.fnret)) { emitline("\tMOVQ\tBP, SP\n"); @@ -247,75 +294,12 @@ fn cglet(c: *cgen, n: *node) void = { let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; - // Tagged-union init: `let r: (T | E) = expr;`. - // - If rhs is a CALL to a fn returning tagged-union, - // the result is already in (AX=tag, DX=v0, CX=v1); - // just spill all three. - // - Otherwise rhs is a bare variant value: pack tag + - // value(s). + // Tagged-union init: delegate to cgwidentaggedstore, which + // handles nullable fold, tagged source (ident or AX/DX/CX + // ABI call), struct payload (literal/ident), str payload, + // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { - let nullable: bool = isnullabletype(tn); - let rhsreturnstagged: bool = false; - if (rhs.kind == nkind.N_CALL) { - let callee: *node = rhs.lhs; - if (callee != nil) { - let calleename: str; - calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; - if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(c, rt)) { rhsreturnstagged = true; }; - }; - }; - }; - cgexpr(c, rhs); - if (nullable) { - // Slot is one 8B word; AX is the pointer - // (or 0 for null/void). Same path whether - // the rhs is a call or a bare variant. - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - c.lastwasreturn = 0; - return; - }; - if (rhsreturnstagged) { - // Spill size/8 registers (tag + value words). - // Slots smaller than 24 don't carry a CX word. - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - if (sz > 16) { - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - }; - c.lastwasreturn = 0; - return; - }; - let tagidx: i32 = taggedvariantindex(c, tn, rhs); - if (tagidx < 0) { tagidx = 0; }; - if (nodeisstr(c, rhs)) { - emitline("\tMOVQ\tAX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - }; - emitline("\tMOVQ\t$"); - emitint(tagidx: i64); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); + cgwidentaggedstore(c, tn, rhs, off, sz); c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index bb8d15be..a4066a6e 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -72,6 +72,34 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { return rest + 1; }; if (widensz > 0) { + // Struct-payload widening into a tagged-union param uses + // @tagscr (zero + cgwidentaggedstore writes fields + tag, + // then push slot words high → low). Scalar / str go via + // the direct push fast path below — keeps wwstage's asm + // byte-identical to cstage for selfhost source. + let pname: str = rhsstructpayload(c, arg); + if (pname.len > 0) { + let ptype: *node = param.lhs; + let scroff: i32 = localadd(c, "@tagscr", 24, nil); + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < widensz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, ptype, arg, scroff, widensz); + let pp: i32 = widensz - 8; + for (pp >= 0) { + emitline("\tMOVQ\t"); + emitoff((scroff + pp): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + pp -= 8; + }; + return rest + widensz / 8; + }; cgexpr(c, arg); if (nodeisstr(c, arg)) { // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low @@ -955,8 +983,23 @@ fn slotsize(c: *cgen, typn: *node) i32 = { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; }; - }; + if (ps > 0) { esz = ps; } + else { + // Named struct / aliased type: size off + // the structinfo if present. + let si: *structinfo = structlookup(c, en); + if (si != nil) { esz = si.totsize; }; + }; + } else { if (elemn.kind == nkind.N_TTAGGED) { + // Tagged-union element: full slot (8 tag + + // padded max payload). Matches C cgen's + // resolve_type for `[N]TAGGED`. + esz = slotsize(c, elemn); + } else { if (elemn.kind == nkind.N_TPTR) { + esz = 8; + } else { if (elemn.kind == nkind.N_TSTRUCT) { + esz = slotsize(c, elemn); + }; }; }; }; }; return (esz: i64 * elen): i32; }; @@ -1424,21 +1467,40 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { if (rhs == nil) { return -1; }; let wantname: str = rhstargetname(c, rhs); if (wantname.len > 0) { - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (variantnamematch(v.str, wantname)) { return idx; }; - }; - v = v.next; - idx += 1; - }; + let r: i32 = flatvariantidx(c, tagged, wantname); + if (r >= 0) { return r; }; }; - // Fallback: by str-shape (resolves aliases). + // Fallback: by str-shape (resolves aliases). Walks the + // spread-flattened variant list so a `(...inner | str)` outer + // agrees with the (i32 | str) inner's str position. let wantstr: bool = nodeisstr(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + let ivisstr: bool = false; + if (iv.kind == nkind.N_TNAME) { + if (isstrtype(c, iv)) { ivisstr = true; }; + }; + if (ivisstr == wantstr) { return idx; }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; let visstr: bool = false; if (v.kind == nkind.N_TNAME) { if (isstrtype(c, v)) { visstr = true; }; @@ -1449,3 +1511,404 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { }; return -1; }; + +// flatvariantidx — walk `tagged`'s variant list (with spread `...inner` +// expansion) and return the flat 0-based index where `want` matches. +// Mirrors check.c's spread flatten at type resolution: an outer +// `(...inner | T)` has the inner's variants inlined in declaration +// order, so the tag indices stay in sync between cstage (which +// resolves types upfront) and wwstage (which doesn't). Returns -1 if +// no variant matches. +fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { + if (tagged == nil) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + if (want.len == 0) { return -1; }; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + if (iv.kind == nkind.N_TNAME) { + if (variantnamematch(iv.str, want)) { + return idx; + }; + }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; + if (v.kind == nkind.N_TNAME) { + if (variantnamematch(v.str, want)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + return -1; +}; + +// cgwidentagremap — when widening from one tagged union to a wider one, +// rewrite the source's variant tag at slot_off+0 to use the destination's +// variant indices. No-op when src and dst index orders coincide. +// Mirrors cg_widen_tag_remap in cmd/w6c/cgen.c. +fn cgwidentagremap(c: *cgen, dst: *node, src: *node, slot_off: i32) void = { + if (dst == nil) { return; }; + if (src == nil) { return; }; + if (dst.kind != nkind.N_TTAGGED) { return; }; + if (src.kind != nkind.N_TTAGGED) { return; }; + let identity: bool = true; + let v: *node = src.list; + let idx: i32 = 0; + for (v != nil) { + let di: i32 = cgtagvariantidx(c, dst, v); + if (di < 0) { di = 0; }; + if (di != idx) { identity = false; v = nil; } + else { v = v.next; idx += 1; }; + }; + if (identity) { return; }; + let done: str = mklabel(c, "remap_done"); + emitline("\tMOVQ\t"); + emitoff(slot_off: i64); + emitline("(BP), AX\n"); + v = src.list; + idx = 0; + for (v != nil) { + let next: str = mklabel(c, "remap_next"); + let di: i32 = cgtagvariantidx(c, dst, v); + if (di < 0) { di = 0; }; + emitline("\tCMPQ\t$"); + emitint(idx: i64); + emitline(", AX\n"); + emitline("\tJNE\t"); + emitline(next); + emitline("\n"); + emitline("\tMOVQ\t$"); + emitint(di: i64); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + emitline("\tJMP\t"); + emitline(done); + emitline("\n"); + emitlabel(next); + v = v.next; + idx += 1; + }; + emitlabel(done); + return; +}; + +// rhsisstructpayload — is `src` a struct value (literal or local ident +// of a struct type)? Returns the struct name, or empty str. Only true +// when the name is registered in c.structs — `!void` / `!i32` aliases +// share the N_STRUCTLIT / N_TNAME shape but aren't structs, and must +// fall through to the scalar/str/tagged-source paths instead. +fn rhsstructpayload(c: *cgen, src: *node) str = { + let empty: str; + empty.ptr = nil; empty.len = 0; + if (src == nil) { return empty; }; + if (src.kind == nkind.N_STRUCTLIT) { + let trefn: *node = src.lhs; + if (trefn != nil) { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (trefn.kind == nkind.N_IDENT) { nm = trefn.str; }; + if (trefn.kind == nkind.N_TNAME) { nm = trefn.str; }; + if (nm.len > 0) { + if (structlookup(c, nm) != nil) { return nm; }; + }; + }; + return empty; + }; + if (src.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, src.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { + if (structlookup(c, tn.str) != nil) { + return tn.str; + }; + }; + }; + }; + }; + return empty; +}; + +// rhstaggedsource — return the tagged-type node for `src` when src is a +// tagged-typed local ident; nil otherwise. The slot-copy path uses this +// to walk variants for tag remap. +fn rhstaggedident(c: *cgen, src: *node) *node = { + if (src == nil) { return nil; }; + if (src.kind != nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, src.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (!istaggedtype(c, tn)) { return nil; }; + return resolvetagged(c, tn); +}; + +// rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX +// return ABI? True for N_CALL of a tagged-returning fn and N_INDEX of a +// tagged-element base. Used to decide whether cgexpr/spill works for the +// tagged-source branch of cgwidentaggedstore. +fn rhstaggedabicall(c: *cgen, src: *node) bool = { + if (src == nil) { return false; }; + if (src.kind == nkind.N_CALL) { + let callee: *node = src.lhs; + if (callee != nil) { + let calleename: str; + calleename.ptr = nil; calleename.len = 0; + if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; + if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + if (calleename.len > 0) { + let rt: *node = fnretlookup(c, calleename); + if (rt != nil) { + if (istaggedtype(c, rt)) { return true; }; + }; + }; + }; + return false; + }; + if (src.kind == nkind.N_INDEX) { + let base: *node = src.lhs; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bl: *local = localfindnode(c, base.str); + if (bl != nil) { + let btn: *node = bl.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + let elemt: *node = nil; + if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; + if (elemt != nil) { + if (istaggedtype(c, elemt)) { + return true; + }; + }; + }; + }; + }; + }; + }; + return false; +}; + +// cgwidentaggedstore — write tagged-union slot bytes for `src` into the +// slot at BP+slot_off, sized to slot_sz. Mirrors cg_widen_tagged_store +// in cmd/w6c/cgen.c. Branches by source shape: +// - nullable dst (8B slot): cgexpr → AX → slot+0. +// - tagged src ident: copy slot words, zero-pad, tag-remap. +// - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, +// spill words; no remap (callee already speaks dst tag order — or +// it doesn't, in which case the source is the wider one and remap +// would need a reversed direction we don't currently emit). +// - struct src (literal or ident): zero slot, write fields at +8+foff, +// tag last. +// - str src: tag@+0, ptr@+8, len@+16. +// - scalar src: tag@+0, value@+8. +fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { + let dt: *node = resolvetagged(c, dst); + if (dt == nil) { return; }; + // Nullable fold: one 8B word holding the pointer (or 0 for void). + if (isnullabletype(dst)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + // Tagged source ident: byte-copy slot words then tag-remap. + let st: *node = rhstaggedident(c, src); + if (st != nil) { + let lc: *local = localfindnode(c, src.str); + let ssz: i32 = slotsize(c, lc.tnode); + let soff: i32 = lc.off; + let k: i32 = 0; + for (k < ssz) { + emitline("\tMOVQ\t"); + emitoff((soff + k): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + k): i64); + emitline("(BP)\n"); + k += 8; + }; + if (ssz < slot_sz) { + emitline("\tXORQ\tAX, AX\n"); + let p: i32 = ssz; + for (p < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + p): i64); + emitline("(BP)\n"); + p += 8; + }; + }; + cgwidentagremap(c, dt, st, slot_off); + return; + }; + // Tagged source via AX/DX/CX register ABI (N_CALL, N_INDEX of + // tagged element). + if (rhstaggedabicall(c, src)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + if (slot_sz > 8) { + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + }; + if (slot_sz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((slot_off + 16): i64); + emitline("(BP)\n"); + }; + return; + }; + // Struct payload (literal or ident). + let sname: str = rhsstructpayload(c, src); + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + emitline("\tXORQ\tAX, AX\n"); + let zoff: i32 = 0; + for (zoff < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + zoff): i64); + emitline("(BP)\n"); + zoff += 8; + }; + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + if (src.kind == nkind.N_STRUCTLIT) { + let fnode: *node = src.list; + for (fnode != nil) { + if (fnode.kind == nkind.N_FIELD) { + let fname: str = fnode.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fname)) { + cgexpr(c, fnode.lhs); + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { + mov = "MOVSS"; + }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + } else { if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot_off + 8 + fi.foff + 8): i64); + emitline("(BP)\n"); + } else { + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + }; }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + fnode = fnode.next; + }; + } else { + // Struct ident source: byte-copy struct words to slot+8+k. + let lc: *local = localfindnode(c, src.str); + let soff: i32 = 0; + if (lc != nil) { soff = lc.off; }; + let stotal: i32 = si.totsize; + let ki: i32 = 0; + for (ki + 8 <= stotal) { + emitline("\tMOVQ\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8 + ki): i64); + emitline("(BP)\n"); + ki += 8; + }; + if (ki < stotal) { + let tail: i32 = stotal - ki; + let lop: str = "MOVQ"; + if (tail == 4) { lop = "MOVL"; } + else { if (tail == 1) { lop = "MOVB"; }; }; + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\t"); + emitline(lop); + emitline("\tAX, "); + emitoff((slot_off + 8 + ki): i64); + emitline("(BP)\n"); + }; + }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + }; + // Str payload. + if (nodeisstr(c, src)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot_off + 16): i64); + emitline("(BP)\n"); + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + // Scalar payload. + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; +}; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 99ca2ef3..e170a90e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -5619,6 +5619,34 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { return rest + 1; }; if (widensz > 0) { + // Struct-payload widening into a tagged-union param uses + // @tagscr (zero + cgwidentaggedstore writes fields + tag, + // then push slot words high → low). Scalar / str go via + // the direct push fast path below — keeps wwstage's asm + // byte-identical to cstage for selfhost source. + let pname: str = rhsstructpayload(c, arg); + if (pname.len > 0) { + let ptype: *node = param.lhs; + let scroff: i32 = localadd(c, "@tagscr", 24, nil); + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < widensz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, ptype, arg, scroff, widensz); + let pp: i32 = widensz - 8; + for (pp >= 0) { + emitline("\tMOVQ\t"); + emitoff((scroff + pp): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + pp -= 8; + }; + return rest + widensz / 8; + }; cgexpr(c, arg); if (nodeisstr(c, arg)) { // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low @@ -6502,8 +6530,23 @@ fn slotsize(c: *cgen, typn: *node) i32 = { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; }; - }; + if (ps > 0) { esz = ps; } + else { + // Named struct / aliased type: size off + // the structinfo if present. + let si: *structinfo = structlookup(c, en); + if (si != nil) { esz = si.totsize; }; + }; + } else { if (elemn.kind == nkind.N_TTAGGED) { + // Tagged-union element: full slot (8 tag + + // padded max payload). Matches C cgen's + // resolve_type for `[N]TAGGED`. + esz = slotsize(c, elemn); + } else { if (elemn.kind == nkind.N_TPTR) { + esz = 8; + } else { if (elemn.kind == nkind.N_TSTRUCT) { + esz = slotsize(c, elemn); + }; }; }; }; }; return (esz: i64 * elen): i32; }; @@ -6971,21 +7014,40 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { if (rhs == nil) { return -1; }; let wantname: str = rhstargetname(c, rhs); if (wantname.len > 0) { - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (variantnamematch(v.str, wantname)) { return idx; }; - }; - v = v.next; - idx += 1; - }; + let r: i32 = flatvariantidx(c, tagged, wantname); + if (r >= 0) { return r; }; }; - // Fallback: by str-shape (resolves aliases). + // Fallback: by str-shape (resolves aliases). Walks the + // spread-flattened variant list so a `(...inner | str)` outer + // agrees with the (i32 | str) inner's str position. let wantstr: bool = nodeisstr(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + let ivisstr: bool = false; + if (iv.kind == nkind.N_TNAME) { + if (isstrtype(c, iv)) { ivisstr = true; }; + }; + if (ivisstr == wantstr) { return idx; }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; let visstr: bool = false; if (v.kind == nkind.N_TNAME) { if (isstrtype(c, v)) { visstr = true; }; @@ -6997,6 +7059,407 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { return -1; }; +// flatvariantidx — walk `tagged`'s variant list (with spread `...inner` +// expansion) and return the flat 0-based index where `want` matches. +// Mirrors check.c's spread flatten at type resolution: an outer +// `(...inner | T)` has the inner's variants inlined in declaration +// order, so the tag indices stay in sync between cstage (which +// resolves types upfront) and wwstage (which doesn't). Returns -1 if +// no variant matches. +fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { + if (tagged == nil) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + if (want.len == 0) { return -1; }; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let isspread: bool = (v.op == tkind.TK_ELLIPSIS); + if (isspread) { + let inner: *node = v; + if (inner.kind == nkind.N_TNAME) { + let a: *node = aliaslookup(c, inner.str); + if (a != nil) { inner = a; }; + }; + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let iv: *node = inner.list; + for (iv != nil) { + if (iv.kind == nkind.N_TNAME) { + if (variantnamematch(iv.str, want)) { + return idx; + }; + }; + iv = iv.next; + idx += 1; + }; + v = v.next; + continue; + }; + }; + }; + if (v.kind == nkind.N_TNAME) { + if (variantnamematch(v.str, want)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + return -1; +}; + +// cgwidentagremap — when widening from one tagged union to a wider one, +// rewrite the source's variant tag at slot_off+0 to use the destination's +// variant indices. No-op when src and dst index orders coincide. +// Mirrors cg_widen_tag_remap in cmd/w6c/cgen.c. +fn cgwidentagremap(c: *cgen, dst: *node, src: *node, slot_off: i32) void = { + if (dst == nil) { return; }; + if (src == nil) { return; }; + if (dst.kind != nkind.N_TTAGGED) { return; }; + if (src.kind != nkind.N_TTAGGED) { return; }; + let identity: bool = true; + let v: *node = src.list; + let idx: i32 = 0; + for (v != nil) { + let di: i32 = cgtagvariantidx(c, dst, v); + if (di < 0) { di = 0; }; + if (di != idx) { identity = false; v = nil; } + else { v = v.next; idx += 1; }; + }; + if (identity) { return; }; + let done: str = mklabel(c, "remap_done"); + emitline("\tMOVQ\t"); + emitoff(slot_off: i64); + emitline("(BP), AX\n"); + v = src.list; + idx = 0; + for (v != nil) { + let next: str = mklabel(c, "remap_next"); + let di: i32 = cgtagvariantidx(c, dst, v); + if (di < 0) { di = 0; }; + emitline("\tCMPQ\t$"); + emitint(idx: i64); + emitline(", AX\n"); + emitline("\tJNE\t"); + emitline(next); + emitline("\n"); + emitline("\tMOVQ\t$"); + emitint(di: i64); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + emitline("\tJMP\t"); + emitline(done); + emitline("\n"); + emitlabel(next); + v = v.next; + idx += 1; + }; + emitlabel(done); + return; +}; + +// rhsisstructpayload — is `src` a struct value (literal or local ident +// of a struct type)? Returns the struct name, or empty str. Only true +// when the name is registered in c.structs — `!void` / `!i32` aliases +// share the N_STRUCTLIT / N_TNAME shape but aren't structs, and must +// fall through to the scalar/str/tagged-source paths instead. +fn rhsstructpayload(c: *cgen, src: *node) str = { + let empty: str; + empty.ptr = nil; empty.len = 0; + if (src == nil) { return empty; }; + if (src.kind == nkind.N_STRUCTLIT) { + let trefn: *node = src.lhs; + if (trefn != nil) { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (trefn.kind == nkind.N_IDENT) { nm = trefn.str; }; + if (trefn.kind == nkind.N_TNAME) { nm = trefn.str; }; + if (nm.len > 0) { + if (structlookup(c, nm) != nil) { return nm; }; + }; + }; + return empty; + }; + if (src.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, src.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { + if (structlookup(c, tn.str) != nil) { + return tn.str; + }; + }; + }; + }; + }; + return empty; +}; + +// rhstaggedsource — return the tagged-type node for `src` when src is a +// tagged-typed local ident; nil otherwise. The slot-copy path uses this +// to walk variants for tag remap. +fn rhstaggedident(c: *cgen, src: *node) *node = { + if (src == nil) { return nil; }; + if (src.kind != nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, src.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (!istaggedtype(c, tn)) { return nil; }; + return resolvetagged(c, tn); +}; + +// rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX +// return ABI? True for N_CALL of a tagged-returning fn and N_INDEX of a +// tagged-element base. Used to decide whether cgexpr/spill works for the +// tagged-source branch of cgwidentaggedstore. +fn rhstaggedabicall(c: *cgen, src: *node) bool = { + if (src == nil) { return false; }; + if (src.kind == nkind.N_CALL) { + let callee: *node = src.lhs; + if (callee != nil) { + let calleename: str; + calleename.ptr = nil; calleename.len = 0; + if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; + if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + if (calleename.len > 0) { + let rt: *node = fnretlookup(c, calleename); + if (rt != nil) { + if (istaggedtype(c, rt)) { return true; }; + }; + }; + }; + return false; + }; + if (src.kind == nkind.N_INDEX) { + let base: *node = src.lhs; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bl: *local = localfindnode(c, base.str); + if (bl != nil) { + let btn: *node = bl.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + let elemt: *node = nil; + if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; + if (elemt != nil) { + if (istaggedtype(c, elemt)) { + return true; + }; + }; + }; + }; + }; + }; + }; + return false; +}; + +// cgwidentaggedstore — write tagged-union slot bytes for `src` into the +// slot at BP+slot_off, sized to slot_sz. Mirrors cg_widen_tagged_store +// in cmd/w6c/cgen.c. Branches by source shape: +// - nullable dst (8B slot): cgexpr → AX → slot+0. +// - tagged src ident: copy slot words, zero-pad, tag-remap. +// - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, +// spill words; no remap (callee already speaks dst tag order — or +// it doesn't, in which case the source is the wider one and remap +// would need a reversed direction we don't currently emit). +// - struct src (literal or ident): zero slot, write fields at +8+foff, +// tag last. +// - str src: tag@+0, ptr@+8, len@+16. +// - scalar src: tag@+0, value@+8. +fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { + let dt: *node = resolvetagged(c, dst); + if (dt == nil) { return; }; + // Nullable fold: one 8B word holding the pointer (or 0 for void). + if (isnullabletype(dst)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + // Tagged source ident: byte-copy slot words then tag-remap. + let st: *node = rhstaggedident(c, src); + if (st != nil) { + let lc: *local = localfindnode(c, src.str); + let ssz: i32 = slotsize(c, lc.tnode); + let soff: i32 = lc.off; + let k: i32 = 0; + for (k < ssz) { + emitline("\tMOVQ\t"); + emitoff((soff + k): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + k): i64); + emitline("(BP)\n"); + k += 8; + }; + if (ssz < slot_sz) { + emitline("\tXORQ\tAX, AX\n"); + let p: i32 = ssz; + for (p < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + p): i64); + emitline("(BP)\n"); + p += 8; + }; + }; + cgwidentagremap(c, dt, st, slot_off); + return; + }; + // Tagged source via AX/DX/CX register ABI (N_CALL, N_INDEX of + // tagged element). + if (rhstaggedabicall(c, src)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + if (slot_sz > 8) { + emitline("\tMOVQ\tDX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + }; + if (slot_sz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((slot_off + 16): i64); + emitline("(BP)\n"); + }; + return; + }; + // Struct payload (literal or ident). + let sname: str = rhsstructpayload(c, src); + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + emitline("\tXORQ\tAX, AX\n"); + let zoff: i32 = 0; + for (zoff < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + zoff): i64); + emitline("(BP)\n"); + zoff += 8; + }; + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + if (src.kind == nkind.N_STRUCTLIT) { + let fnode: *node = src.list; + for (fnode != nil) { + if (fnode.kind == nkind.N_FIELD) { + let fname: str = fnode.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fname)) { + cgexpr(c, fnode.lhs); + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { + mov = "MOVSS"; + }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + } else { if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot_off + 8 + fi.foff + 8): i64); + emitline("(BP)\n"); + } else { + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitoff((slot_off + 8 + fi.foff): i64); + emitline("(BP)\n"); + }; }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + fnode = fnode.next; + }; + } else { + // Struct ident source: byte-copy struct words to slot+8+k. + let lc: *local = localfindnode(c, src.str); + let soff: i32 = 0; + if (lc != nil) { soff = lc.off; }; + let stotal: i32 = si.totsize; + let ki: i32 = 0; + for (ki + 8 <= stotal) { + emitline("\tMOVQ\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8 + ki): i64); + emitline("(BP)\n"); + ki += 8; + }; + if (ki < stotal) { + let tail: i32 = stotal - ki; + let lop: str = "MOVQ"; + if (tail == 4) { lop = "MOVL"; } + else { if (tail == 1) { lop = "MOVB"; }; }; + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\t"); + emitline(lop); + emitline("\tAX, "); + emitoff((slot_off + 8 + ki): i64); + emitline("(BP)\n"); + }; + }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + }; + // Str payload. + if (nodeisstr(c, src)) { + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((slot_off + 16): i64); + emitline("(BP)\n"); + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; + // Scalar payload. + cgexpr(c, src); + emitline("\tMOVQ\tAX, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + let tag: i32 = taggedvariantindex(c, dt, src); + if (tag < 0) { tag = 0; }; + emitline("\tMOVQ\t$"); + emitint(tag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; +}; + // MODULE: wcc // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // @@ -7112,7 +7575,7 @@ fn cgexpr(c: *cgen, n: *node) void = { // tagged-union type expression `tagged`. -1 if `tagged` isn't an // nkind.N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch // does inline; pulled out so `is` / `as` can reuse it. -fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { +fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; @@ -7120,16 +7583,7 @@ fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; if (want.len == 0) { return -1; }; - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (streq(v.str, want)) { return idx; }; - }; - v = v.next; - idx += 1; - }; - return -1; + return flatvariantidx(c, tagged, want); }; // cgtryprop — `e?` propagates the error variant up the stack. @@ -7246,7 +7700,7 @@ fn cgtypetest(c: *cgen, n: *node) void = { }; }; }; - let want: i32 = cgtagvariantidx(scrutt, n.rhs); + let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); @@ -7334,7 +7788,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; }; }; - let want: i32 = cgtagvariantidx(scrutt, n.rhs); + let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); emitline("\tMOVQ\t"); @@ -7566,6 +8020,43 @@ fn cgindex(c: *cgen, n: *node) void = { esz = indexbaseesz(c, base); };}; }; + // Tagged-union element: load slot words into (AX=tag, DX=val0, + // CX=val1) matching the tagged-return ABI so call-arg / let / + // match consumers see the same shape as a tagged-returning fn. + // Slot size = esz (8/16/24); nullable folded element is one + // word, which the fallthrough below handles via MOVQ AX. + let elem_tagged: bool = false; + let elem_slot_sz: i32 = esz; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bl: *local = baselocal; + let etn: *node = nil; + if (bl != nil) { + let btn: *node = bl.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + }; + } else { + let tn: *node = letvartnode(c, base.str); + if (tn != nil) { + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { etn = tn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = tn.lhs; }; + if (bk == nkind.N_TPTR) { etn = tn.lhs; }; + }; + }; + if (istaggedtype(c, etn)) { + if (!isnullabletype(etn)) { + elem_tagged = true; + elem_slot_sz = slotsize(c, etn); + esz = elem_slot_sz; + }; + }; + }; + }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -7584,6 +8075,16 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("(SB), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); + if (elem_tagged) { + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; if (esz == 16) { emitline("\tMOVQ\t8(BX), CX\n"); emitline("\tMOVQ\t(BX), AX\n"); @@ -7612,6 +8113,16 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); + if (elem_tagged) { + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; // str element (16B): load (ptr, len) into (AX, BX) so // the value flows through the str-rhs convention. if (esz == 16) { @@ -7633,6 +8144,19 @@ fn cgindex(c: *cgen, n: *node) void = { cgexpr(c, base); emitline("\tPOPQ\tBX\n"); emitline("\tADDQ\tBX, AX\n"); + if (elem_tagged) { + // AX holds the element address. Copy to BX (loading slot+0 + // into AX clobbers it), then read slot words. + emitline("\tMOVQ\tAX, BX\n"); + if (elem_slot_sz > 16) { + emitline("\tMOVQ\t16(BX), CX\n"); + }; + if (elem_slot_sz > 8) { + emitline("\tMOVQ\t8(BX), DX\n"); + }; + emitline("\tMOVQ\t(BX), AX\n"); + return; + }; if (esz == 16) { emitline("\tMOVQ\t8(AX), BX\n"); emitline("\tMOVQ\t(AX), AX\n"); @@ -7747,15 +8271,16 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = resolvetagged(c, lc.tnode); }; } else { - // Non-ident scrutinee (call result, ?, etc.). Spill into a - // 24B `@match_spill` scratch slot and dispatch off it. - // Tagged returns follow the AX:DX:CX convention, so store - // all three words at +0/+8/+16; nullable returns are - // single-word (AX = ptr) and only read +0, so the extra - // stores are harmless. For N_CALL we recover the return - // type via fnretlookup so nullable dispatch can pick the - // pointer-vs-null discriminator. Mirrors C cgen's - // @match_spill path in cmd/w6c/cgen.c N_MATCH. + // Non-ident scrutinee (call result, arr[i], ?, etc.). + // Spill into a 24B `@match_spill` scratch slot and + // dispatch off it. Tagged returns (N_CALL) follow the + // AX:DX:CX convention; tagged-element loads (N_INDEX) + // after the cgindex fix produce the same triple. + // Nullable returns are single-word (AX = ptr); only +0 + // is read, so the extra stores are harmless. We + // recover the scrutinee type from fnretlookup (N_CALL) + // or the base local's array element type (N_INDEX) so + // dispatch can compute variant indices. scrutoff = localalloc(c, "@match_spill", 24, nil); if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; @@ -7770,6 +8295,27 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + if (scrut.kind == nkind.N_INDEX) { + let ibase: *node = scrut.lhs; + if (ibase != nil) { + if (ibase.kind == nkind.N_IDENT) { + let bl: *local = localfindnode(c, ibase.str); + let btn: *node = nil; + if (bl != nil) { btn = bl.tnode; } + else { btn = letvartnode(c, ibase.str); }; + if (btn != nil) { + let bk: nkind = btn.kind; + let etn: *node = nil; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + if (etn != nil) { + scrutt = resolvetagged(c, etn); + }; + }; + }; + }; + }; cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -7832,23 +8378,8 @@ fn cgmatch(c: *cgen, n: *node) void = { let patname: str; patname.ptr = nil; patname.len = 0; if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; - let v: *node = scrutt.list; - let idx: i32 = 0; - let found: bool = false; - for (v != nil) { - if (v.kind == nkind.N_TNAME) { - if (variantnamematch(v.str, patname)) { - want = idx; - found = true; - v = nil; - }; - }; - if (v != nil) { - v = v.next; - idx += 1; - }; - }; - if (!found) { want = 0; }; + let r: i32 = flatvariantidx(c, scrutt, patname); + if (r >= 0) { want = r; }; }; }; emitline("\tMOVQ\t"); @@ -9156,6 +9687,25 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Tagged-union local reassignment: `r = expr;` where r has a + // tagged-union type. Delegate to cgwidentaggedstore (same path + // as cglet's tagged-init). Covers nullable fold, tagged source, + // struct payload, str payload, scalar payload, with tag remap. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (n.op == tkind.TK_ASSIGN) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + let lsz: i32 = slotsize(c, lc.tnode); + cgwidentaggedstore(c, lc.tnode, + n.rhs, lc.off, lsz); + return; + }; + }; + }; + }; + }; // `*p = v` — deref-assign. Element width comes from the // pointer's declared type. Mirrors C cgen: eval rhs (AX, // and BX if str), push, eval pointer, pop value, store. @@ -9256,12 +9806,20 @@ fn cgassign(c: *cgen, n: *node) void = { let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; + let elemtn: *node = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); + let btn: *node = baselocal.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; + }; } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { @@ -9269,11 +9827,13 @@ fn cgassign(c: *cgen, n: *node) void = { isglobalarr = true; globalname = bn; esz = elemsizeof(tn); + elemtn = tn.lhs; }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeof(tn); + elemtn = tn.lhs; }; }; }; @@ -9281,6 +9841,80 @@ fn cgassign(c: *cgen, n: *node) void = { esz = indexbaseesz(c, base); };}; }; + // Tagged-union element: materialize source in a shared + // scratch slot via cgwidentaggedstore (handles struct / + // str / scalar / subset / nullable variants uniformly), + // then compute &arr[i] and byte-copy. The scratch + // (@tagscr) is reused across all tagged-arr stores in + // the function and counted once in scanlocals. + if (elemtn != nil) { + if (istaggedtype(c, elemtn)) { + let slot_sz: i32 = slotsize(c, elemtn); + let scroff: i32 = localadd(c, "@tagscr", + 24, nil); + // Pre-zero scratch (matches push helper). + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, elemtn, n.rhs, + scroff, slot_sz); + cgexpr(c, idx); + if (slot_sz > 1) { + emitline("\tMOVQ\t$"); + emitint(slot_sz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + let isarr: bool = false; + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isarr = true; + }; + }; + if (isarr) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { + emitline("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\tPOPQ\tAX\n"); + };};}; + emitline("\tADDQ\tAX, BX\n"); + let cc: i32 = 0; + for (cc < slot_sz) { + emitline("\tMOVQ\t"); + emitoff((scroff + cc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(cc: i64); + emitline("(BX)\n"); + cc += 8; + }; + return; + }; + }; cgexpr(c, n.rhs); // value → AX if (esz == 16) { emitline("\tPUSHQ\tBX\n"); }; emitline("\tPUSHQ\tAX\n"); @@ -10214,6 +10848,53 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; + // Struct payload or tagged-subset return — materialise + // the widened value in scratch via cgwidentaggedstore + // (handles tag remap and zero pad), then load AX/DX/CX + // from the slot. + let needswiden: bool = false; + if (!isnullabletype(c.fnret)) { + if (!forwardtagged) { + let sname: str = rhsstructpayload(c, rhs); + if (sname.len > 0) { needswiden = true; }; + if (rhstaggedident(c, rhs) != nil) { + needswiden = true; + }; + }; + }; + if (needswiden) { + let rsz: i32 = slotsize(c, c.fnret); + let scroff: i32 = localadd(c, "@tagscr", + 24, nil); + emitline("\tXORQ\tAX, AX\n"); + let zz: i32 = 0; + for (zz < rsz) { + emitline("\tMOVQ\tAX, "); + emitoff((scroff + zz): i64); + emitline("(BP)\n"); + zz += 8; + }; + cgwidentaggedstore(c, c.fnret, rhs, scroff, + rsz); + emitline("\tMOVQ\t"); + emitoff(scroff: i64); + emitline("(BP), AX\n"); + if (rsz > 8) { + emitline("\tMOVQ\t"); + emitoff((scroff + 8): i64); + emitline("(BP), DX\n"); + }; + if (rsz > 16) { + emitline("\tMOVQ\t"); + emitoff((scroff + 16): i64); + emitline("(BP), CX\n"); + }; + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; cgexpr(c, rhs); if (isnullabletype(c.fnret)) { emitline("\tMOVQ\tBP, SP\n"); @@ -10299,75 +10980,12 @@ fn cglet(c: *cgen, n: *node) void = { let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; - // Tagged-union init: `let r: (T | E) = expr;`. - // - If rhs is a CALL to a fn returning tagged-union, - // the result is already in (AX=tag, DX=v0, CX=v1); - // just spill all three. - // - Otherwise rhs is a bare variant value: pack tag + - // value(s). + // Tagged-union init: delegate to cgwidentaggedstore, which + // handles nullable fold, tagged source (ident or AX/DX/CX + // ABI call), struct payload (literal/ident), str payload, + // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { - let nullable: bool = isnullabletype(tn); - let rhsreturnstagged: bool = false; - if (rhs.kind == nkind.N_CALL) { - let callee: *node = rhs.lhs; - if (callee != nil) { - let calleename: str; - calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; - if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(c, rt)) { rhsreturnstagged = true; }; - }; - }; - }; - cgexpr(c, rhs); - if (nullable) { - // Slot is one 8B word; AX is the pointer - // (or 0 for null/void). Same path whether - // the rhs is a call or a bare variant. - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - c.lastwasreturn = 0; - return; - }; - if (rhsreturnstagged) { - // Spill size/8 registers (tag + value words). - // Slots smaller than 24 don't carry a CX word. - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - if (sz > 16) { - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - }; - c.lastwasreturn = 0; - return; - }; - let tagidx: i32 = taggedvariantindex(c, tn, rhs); - if (tagidx < 0) { tagidx = 0; }; - if (nodeisstr(c, rhs)) { - emitline("\tMOVQ\tAX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - }; - emitline("\tMOVQ\t$"); - emitint(tagidx: i64); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); + cgwidentaggedstore(c, tn, rhs, off, sz); c.lastwasreturn = 0; return; }; @@ -11198,6 +11816,16 @@ fn scanlocals(c: *cgen, n: *node) i32 = { if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; total += sz; }; + // Carry the let's tnode into the stub so scanlocals can + // dispatch on type later in the walk — e.g. detecting + // `arr[i] = ...` where arr is a tagged-element array, + // which needs an @tagscr scratch slot reservation. + let stub: *local = localfindnode(c, n.str); + if (stub != nil) { + if (stub.tnode == nil) { + if (n.lhs != nil) { stub.tnode = n.lhs; }; + }; + }; }; // Multi-let from a tuple-returning call: each binding's size // comes from its annotated type (l.lhs) when present, else from @@ -11305,6 +11933,128 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Tagged-arr/slice index store needs a 24B scratch slot + // (`@tagscr`) for cgwidentaggedstore to materialise the source + // in before copying to the element address. Reserved once per + // function (dedup'd via scanseenmark) regardless of how many + // tagged-arr stores the body contains. + if (n.kind == nkind.N_ASSIGN) { + let alhs: *node = n.lhs; + if (alhs != nil) { + if (alhs.kind == nkind.N_INDEX) { + let abase: *node = alhs.lhs; + if (abase != nil) { + if (abase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, abase.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, abase.str); }; + if (btn != nil) { + let bk: nkind = btn.kind; + let etn: *node = nil; + if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; + if (bk == nkind.N_TPTR) { etn = btn.lhs; }; + if (etn != nil) { + if (istaggedtype(c, etn)) { + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + }; + }; + }; + }; + // Tagged-union return with struct payload or tagged-subset + // source — cgreturn materialises in @tagscr then loads + // AX/DX/CX. Detect via the same rhsstructpayload predicate + // the cgen uses, so we only reserve when the cgen will + // actually emit a scratch-using path. `!void` / `!i32` + // aliases share N_STRUCTLIT shape but resolve to + // non-struct types — they fall through to scalar/str and + // don't need scratch. + if (n.kind == nkind.N_RETURN) { + if (c.fnret != nil) { + if (istaggedtype(c, c.fnret)) { + if (!isnullabletype(c.fnret)) { + let rhs: *node = n.lhs; + let needs: bool = false; + if (rhs != nil) { + let sn: str = rhsstructpayload(c, rhs); + if (sn.len > 0) { needs = true; }; + if (rhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + needs = true; + }; + }; + }; + }; + if (needs) { + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + }; + // Call-site struct-payload widening uses @tagscr — when the + // arg is a struct literal/ident and the callee's param is + // tagged, pushargsrev materialises in scratch and pushes. + // Scalar / str args take the direct-push fast path (no + // scratch). Tagged-typed ident args also skip widening (the + // slot is already laid out, so pushargsrev pushes slot words + // directly). Both fast paths agree with C cgen bytewise, so + // only struct-payload sites get a scratch reservation. + if (n.kind == nkind.N_CALL) { + let callee: *node = n.lhs; + let cnm: str; + cnm.ptr = nil; cnm.len = 0; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + }; + if (cnm.len > 0) { + let ps: *node = fnparamslookup(c, cnm); + let a: *node = n.list; + for (a != nil) { + if (ps == nil) { a = nil; } + else { + if (ps.kind == nkind.N_PARAM) { + let pt: *node = ps.lhs; + if (istaggedtype(c, pt)) { + if (!isnullabletype(pt)) { + let sn: str = rhsstructpayload(c, a); + if (sn.len > 0) { + let isidentstruct: bool = false; + if (a.kind == nkind.N_IDENT) { + // Struct ident as + // tagged arg — pushargsrev + // still routes through the + // scratch path. + isidentstruct = true; + }; + let _u: bool = isidentstruct; + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + }; + }; + if (a != nil) { + a = a.next; + ps = ps.next; + }; + }; + }; + }; + }; if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; if (n.cond != nil) { total += scanlocals(c, n.cond); }; diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index b15cda92..0453a46e 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -955,6 +955,173 @@ static const struct row rows[] = { " };\n" " return -1;\n" "};", 42 }, + /* Tagged-union element in a fixed array — scalar+str variants. + * Store via N_INDEX widening, read+match through cgexpr N_INDEX + * tagged-slot load. 10 + len("hi")=2 + 5 = 17. */ + { "fn main() i32 = {\n" + " let arr: [3](i32 | str);\n" + " arr[0] = 10;\n" + " arr[1] = \"hi\";\n" + " arr[2] = 5;\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 3) {\n" + " match (arr[i]) {\n" + " case let v: i32 => s += v;\n" + " case let t: str => s += t.len: i32;\n" + " };\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};", 17 }, + /* Tagged-union array with struct payload variant. Struct fields + * are written at slot+8+field_off via cg_widen_tagged_store; the + * read side just copies slot bytes into AX/DX/CX for match. + * 5 + (7+11) + 2 + 9 = 34. */ + { "type pair = struct { a: i32, b: i32 };\n" + "fn main() i32 = {\n" + " let arr: [4](i32 | pair | str);\n" + " arr[0] = 5;\n" + " arr[1] = pair { a = 7, b = 11 };\n" + " arr[2] = \"yo\";\n" + " arr[3] = 9;\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 4) {\n" + " match (arr[i]) {\n" + " case let v: i32 => s += v;\n" + " case let p: pair => s += p.a + p.b;\n" + " case let t: str => s += t.len: i32;\n" + " };\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};", 34 }, + /* Slicing an array of tagged elements — the slice load path + * uses the fallback (non-ident base) N_INDEX which loads slot + * bytes from a computed address. 1 + 2 + 3 + 4 = 10. */ + { "fn main() i32 = {\n" + " let buf: [4](i32 | str);\n" + " buf[0] = 1;\n" + " buf[1] = \"ww\";\n" + " buf[2] = 3;\n" + " buf[3] = 4;\n" + " let xs: [](i32 | str) = buf[0:4];\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 4) {\n" + " match (xs[i]) {\n" + " case let v: i32 => s += v;\n" + " case let t: str => s += t.len: i32;\n" + " };\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};", 10 }, + /* Passing arr[i] (tagged element) as a tagged arg — cgexpr leaves + * the slot in AX/DX/CX which the call-site shuffle pushes onto + * the arg stack. 5 + 30 (len 3 * 10) + 7 = 42. */ + { "fn weight(v: (i32 | str)) i32 = {\n" + " match (v) {\n" + " case let n: i32 => return n;\n" + " case let s: str => return s.len: i32 * 10;\n" + " };\n" + " return 0;\n" + "};\n" + "fn main() i32 = {\n" + " let arr: [3](i32 | str);\n" + " arr[0] = 5;\n" + " arr[1] = \"abc\";\n" + " arr[2] = 7;\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 3) {\n" + " s += weight(arr[i]);\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};", 42 }, + /* let-init of a tagged local from arr[i] — the let path routes + * through cg_widen_tagged_store which, for a tagged source via + * cgexpr, spills AX/DX/CX into the slot. "ww!".len == 3. */ + { "fn main() i32 = {\n" + " let arr: [3](i32 | str);\n" + " arr[0] = 11;\n" + " arr[1] = \"ww!\";\n" + " arr[2] = 7;\n" + " let r: (i32 | str) = arr[1];\n" + " match (r) {\n" + " case let n: i32 => return n;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* Tagged-subset store into a wider tagged-union array element: + * source slot is (i32|str), dest element is (i32|str|u64). The + * store path materialises the subset in scratch then copies slot + * bytes — tag remap is a no-op here (variant order matches). + * arr[0]=9:i32, arr[1]="hi":str → 9 + 2 = 11. */ + { "type inner = (i32 | str);\n" + "fn main() i32 = {\n" + " let arr: [2](i32 | str | u64);\n" + " let v: inner = 9;\n" + " arr[0] = v;\n" + " let w: inner = \"hi\";\n" + " arr[1] = w;\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 2) {\n" + " match (arr[i]) {\n" + " case let n: i32 => s += n;\n" + " case let t: str => s += t.len: i32;\n" + " case let u: u64 => s += 100;\n" + " };\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};", 11 }, + /* Returning arr[i] from a fn whose return type matches the + * element. cgexpr leaves slot in AX/DX/CX; the return path + * forwards as-is. arr[1] = "abc" → str variant → .len == 3. */ + { "fn pick(i: i32) (i32 | str) = {\n" + " let arr: [2](i32 | str);\n" + " arr[0] = 21;\n" + " arr[1] = \"abc\";\n" + " return arr[i];\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i32 | str) = pick(1);\n" + " match (r) {\n" + " case let n: i32 => return n;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* Nullable folded element `(*T | void)` — the slot is one 8B + * pointer word; null is the void variant. Stores route through + * the nullable branch of cg_widen_tagged_store (single MOVQ at + * +0). 100 (nil) + 1 (non-nil) + 100 (nil) = 201. */ + { "fn pickptr(b: bool) *i32 = {\n" + " let x: i32 = 42;\n" + " if (b) { return &x; };\n" + " return nil;\n" + "};\n" + "fn main() i32 = {\n" + " let arr: [3](*i32 | void);\n" + " arr[0] = nil;\n" + " arr[1] = pickptr(true);\n" + " arr[2] = pickptr(false);\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 3) {\n" + " match (arr[i]) {\n" + " case let p: *i32 => s += 1;\n" + " case => s += 100;\n" + " };\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};", 201 }, /* Plan 9-style sentinel error idiom: `def NAME: error = "lit"` * inlines as the (ptr, len) pair at use sites. */ { "type error = str;\n"