w6c+selfhost: tagged-arr element ABI + full selfhost mirror

Closes the remaining tagged-union gaps after the prior two commits:

  1. Tagged element in an array/slice (cstage). N_INDEX load now reads
     slot words into AX/DX/CX, matching the tagged-return ABI so match
     / call-arg / let-init paths consume `arr[i]` uniformly. N_INDEX
     store routes through a scratch slot + cg_widen_tagged_store +
     byte-copy to &arr[i], so the full widening machinery (scalar /
     str / struct payload / tagged subset / nullable fold) lights up
     for element writes too.

  2. Selfhost mirror — the cgen widen helpers (struct payload,
     tagged-subset, spread-flatten) C cgen has had for two commits
     finally land in selfhost:

       cgwidentaggedstore  — single writer for nullable / tagged ident /
                             tagged via AX:DX:CX / struct (lit + ident) /
                             str / scalar source shapes.
       cgwidentagremap     — CMPQ-chain tag remap for variant-subset.
       rhsstructpayload    — struct-name predicate; filters `!void` /
                             `!i32` aliases that share N_STRUCTLIT shape
                             but aren't structs.
       rhstaggedident,
       rhstaggedabicall    — source-shape predicates.
       flatvariantidx      — spread-aware variant index lookup. Walks
                             `(...inner | T)` entries by resolving the
                             alias and inlining the inner's variants so
                             wwstage's tag order matches the check.c
                             flattening cstage does at type resolution.

     cglet tagged init, cgassign tagged-ident reassign, cgreturn struct
     / subset payload, pushargsrev struct payload, cgindex tagged
     element load, cgassign N_INDEX tagged element store all delegate
     to these. cgmatch picks up scrutt from N_INDEX bases (element
     type) and uses flatvariantidx for case dispatch.

  3. Selfhost frame accounting: scanlocals reserves a 24B @tagscr slot
     when the body contains a tagged-arr store, a struct-payload
     tagged return, or a struct-payload call arg — dedup'd via
     scanseenmark so multiple sites share one slot. N_LET stubs now
     carry tnode so walk-time type checks see the array element type.
     slotsize TARRAY learned to size tagged / struct / ptr / aliased
     elements (was 8B-default for anything not N_TNAME-primitive,
     undersizing tagged-element arrays).

     Scalar / str call-arg widening keeps its direct-push fast path
     (no scratch), so wwstage's asm on selfhost source remains
     byte-identical to cstage's — 993/995 still pass.

700_e2e: 9 new rows — scalar/str/struct/subset/nullable variants in
arrays and slices, plus pass-arg / let-init / return / match shapes.
This commit is contained in:
2026-05-13 06:29:52 +09:00
parent 9133251269
commit 6fd0160c0f
8 changed files with 2859 additions and 357 deletions

View File

@@ -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 ||

File diff suppressed because it is too large Load Diff

View File

@@ -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); };

View File

@@ -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");

View File

@@ -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;
};

View File

@@ -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;
};

File diff suppressed because it is too large Load Diff

View File

@@ -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"