Files
ww/selfhost/cmd/wcc/cgenstmt.ww
Hojun-Cho 46edb8db4a w6c+selfhost+lib: cgen quality batch + lib Hare-shape graduation
Six fixes across the toolchain, surfaced by lib/lisp porting work.

  1. f64 compound assigns (`acc += d`, `-=`, `*=`, `/=`). Both stages
     load slot → X1, OP X0 into X1, store back (ADDSD/SUBSD/MULSD/
     DIVSD are reg-reg only). Previous MOVSD-overwrite dropped the
     OP. Locals and top-level lets.

  2. Top-level `[N]u8` arrays + `&arr[i]`. let_emit_size grows a
     TY_ARRAY branch so zero-init DATAW lands; cgindex / N_INDEX
     store / `&base[i]` all detect a global array base and use
     LEAQ name(SB) instead of LEAQ (BP). TK_AMP no longer pre-
     evaluates the operand as a value-load — `&base[i]` computes
     base + i*esz directly. Unblocks Hare's static-buffer pattern:
     strconv.{u64,i64,f64}tos graduate to module-level `*_buf`
     arrays and return owned views.

  3. Cross-module `pkg.Enum.MEMBER`. Nested N_DOT chains that
     don't fold to a known shape now emit `MOVQ <leaf>(SB), AX`
     (mirrors the bare-IDENT unresolved fallback), so isolation
     probes — and the test 990 cgen-match floor — stay consistent
     across stages. strconv exposes `base` as a real `enum i32`;
     callers updated. The `main` exemption (linker entry-point
     keeps bare name even when not exported) mirrors C-side
     collectmods into selfhost cgendecl.

  4. Sum-typed parameter ABI. lib/bytes.{index,rindex} take
     `(u8 | []u8)` needle; lib/strings.byteindex / rbyteindex take
     `(str | rune)` needle (Hare-shaped; the byte-wise misnomer
     `index` is dropped). tagged_arg_size cap bumps to 48 (6 int
     regs), with a new partial-fit branch on the callee: when an
     N-word tagged arg overflows remaining regs, fill what fits and
     stitch the rest from positive BP offsets. scanlocals MCASE
     handles slice binds (24B) and walks each arm with a saved /
     restored seenmark set so two arms naming the same local each
     get their own slot — matches cstage's per-arm scope reset.

  5. 4-reg tagged-return ABI (AX=tag, DX=word0, CX=word1, R8=word2),
     up from 3 regs. Slice-payload variants (`([]T | E)`, slot 32B)
     round-trip ptr/len/cap end-to-end. Every receive site updates:
     let-init via cgwidentaggedstore, match scrutinee spill, cgindex
     tagged-element load (both N_IDENT and fallback bases),
     pushargsrev tagged-ident arg (reads word count from slot size),
     cgreturn slice variant in the shuffle path.

  6. `expr: TaggedAlias` is a widening, not a re-interpret. C cgen +
     selfhost cgwidentaggedstore peel an N_CAST whose destination IS
     the union — so cgexpr's natural shape (str: AX=ptr, BX=len;
     slice: AX=ptr, BX=len, CX=cap) is consumed by the matching
     concrete-variant branch instead of being misread as a tagged
     AX/DX/CX triple. Inner casts to a concrete variant (`7: i32`)
     keep their type for proper tag lookup. `[N]Alias` arrays
     resolve element size via slotsize + aliaslookup, and aliaslookup
     strips a `pkg.` prefix so cross-module references work.

lib/fmt grows `formattable = (i64 | str | bool | rune)` plus
`printv` / `printlnv` taking an explicit `[]formattable` slice (the
receive side of Hare's `args: formattable...`). Call-site variadic
gather isn't wired — callers either hand-build the slice or compose
strconv.i64tos + strings.concat.

700_e2e: 114 → 123 rows (f64 compound, top-level u8 arrays + `&buf[i]`,
pkg.Enum.MEMBER, sum-typed (str|rune) and (u8|[]u8) params, 4-reg
slice-return ABI, formattable array). 26/26 tests, bootstrap stable
through ww4.
2026-05-13 08:05:01 +09:00

1103 lines
30 KiB
Plaintext

// selfhost/cmd/wcc/cgenstmt.ww — split out of cgen.ww.
//
// cgstmt is a thin dispatcher over n.kind; each branch defers to a
// per-kind helper: cgblock, cgreturn, cgexprstmt, cglet, cgif, cgfor,
// cgmassign, cgbreak, cgcontinue.
//
// The expression generator (cgexpr) lives in cgenexpr.ww; the
// foundation (types, emit primitives, collect* tables, FFI/module
// maps) lives in cgen.ww.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
// ---- statement cgen --------------------------------------------------
fn cgstmt(c: *cgen, n: *node) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
if (k == nkind.N_BLOCK) { cgblock(c, n); return; };
if (k == nkind.N_RETURN) { cgreturn(c, n); return; };
if (k == nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; };
if (k == nkind.N_LET) { cglet(c, n); return; };
if (k == nkind.N_IF) { cgif(c, n); return; };
if (k == nkind.N_FOR) { cgfor(c, n); return; };
if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; };
if (k == nkind.N_SWITCH) { cgswitch(c, n); return; };
if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; };
if (k == nkind.N_MLET) { cgmlet(c, n); return; };
if (k == nkind.N_BREAK) { cgbreak(c, n); return; };
if (k == nkind.N_CONTINUE) { cgcontinue(c, n); return; };
if (k == nkind.N_YIELD) { cgyield(c, n); return; };
if (k == nkind.N_DEFER) {
if (c.defertop < DEFER_MAX) {
c.deferbuf[c.defertop] = n.lhs;
c.defertop += 1;
};
return;
};
c.lastwasreturn = 0;
};
fn cgyield(c: *cgen, n: *node) void = {
// Evaluate the value into AX (and BX for str), then JMP to the
// enclosing match's end label. Falls through silently if there
// is no active match — should be a checker error eventually.
if (n.lhs != nil) { cgexpr(c, n.lhs); };
if (c.yieldtop > 0) {
let tgt: str = c.yieldbuf[c.yieldtop - 1];
emitline("\tJMP\t");
emitline(tgt);
emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgblock(c: *cgen, n: *node) void = {
let s: *node = n.list;
for (s != nil) {
cgstmt(c, s);
s = s.next;
};
return;
};
// rundefers — emit cgexpr for every queued defer in LIFO order.
// Called from cgreturn and the cgfn implicit-return path.
fn rundefers(c: *cgen) void = {
let i: i32 = c.defertop - 1;
for (i >= 0) {
cgexpr(c, c.deferbuf[i]);
i -= 1;
};
return;
};
fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;`:
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len.
// 24B convention mirrors the tagged-union return below; receive
// sites destructure off the same regs regardless of position.
if (rhs.kind == nkind.N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
let v2: *node = v.next;
if (v2 != nil) {
let v0_is_str: bool = nodeisstr(c, v);
let v1_is_str: bool = nodeisstr(c, v2);
if ((v0_is_str || v1_is_str) && !(v0_is_str && v1_is_str)) {
let strn: *node = v;
let scaln: *node = v2;
if (v1_is_str) { strn = v2; scaln = v; };
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
} else {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
};
} else {
cgexpr(c, v);
};
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
// Tagged-union return: pack as (AX=tag, DX=value0, CX=value1).
// For str variant, cgexpr leaves (AX=ptr, BX=len), so we
// shuffle DX←AX (ptr) and CX←BX (len), then load tag.
// For other variants, cgexpr leaves AX, shuffle DX←AX.
// Nullable folded `(*T | void)`: just one word; AX is
// already the pointer (or 0). No shuffle, no tag.
if (istaggedtype(c, c.fnret)) {
// Forwarding a fallible call: `return f();` where f
// also returns a tagged union. The result is already
// in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag.
// Mirrors the rhsreturnstagged path in cglet and the
// !type_istagged guard in C cgen's N_RETURN.
let forwardtagged: 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)) { forwardtagged = true; };
};
};
};
// 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");
};
if (rsz > 24) {
emitline("\tMOVQ\t");
emitoff((scroff + 24): i64);
emitline("(BP), R8\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");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
if (forwardtagged) {
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisslice(c, rhs)) {
// cgexpr leaves (AX=ptr, BX=len, CX=cap).
// Shuffle into return ABI: DX=ptr, CX=len,
// R8=cap.
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else { if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else {
emitline("\tMOVQ\tAX, DX\n");
};};
emitline("\tMOVQ\t$");
if (idx < 0) { idx = 0; };
emitint(idx: i64);
emitline(", AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
cgexpr(c, rhs);
} else {
// Bare `return;` from a tagged-union-returning fn is
// the void variant: emit its tag. Payload is undefined
// (void has size 0). Otherwise zero AX for determinism.
if (istaggedtype(c, c.fnret)) {
if (isnullabletype(c.fnret)) {
// null = void variant; AX = 0.
emitline("\tMOVQ\t$0, AX\n");
} else {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
emitline("\tMOVQ\t$0, AX\n");
};
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).
// cgexpr leaves str in (AX, BX); shuffle BX→DX.
if (isstrtype(c, c.fnret)) {
emitline("\tMOVQ\tBX, DX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
fn cgexprstmt(c: *cgen, n: *node) void = {
if (n.lhs != nil) { cgexpr(c, n.lhs); };
c.lastwasreturn = 0;
return;
};
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = letslotsize(c, n);
// `let x = f()?` has no annotation but the cgen's struct-field
// paths need a tnode to dispatch off. Infer from f's tagged
// success variant — see inferletcalltype.
let tn: *node = n.lhs;
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
let off: i32 = localadd(c, nm, sz, tn);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
// 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)) {
cgwidentaggedstore(c, tn, rhs, off, sz);
c.lastwasreturn = 0;
return;
};
// 24B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len.
// Layout is positional, so we route each register to the
// slot dictated by element type, not by AX/DX position.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TTUPLE) {
let p0: *node = n.lhs.list;
let p1: *node = nil;
if (p0 != nil) { p1 = p0.next; };
let s0_is_str: bool = isstrtyperaw(p0);
let s1_is_str: bool = isstrtyperaw(p1);
if (p0 != nil) {
if (p1 != nil) {
if (s0_is_str != s1_is_str) {
cgexpr(c, rhs);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
// Walk elements in declaration order, store each at off + i*esz
// using the right width for the element type. Trailing `...`
// after the last value (an nkind.N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == nkind.N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry. When the literal carries
// op == tkind.TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == nkind.N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (trefn != nil) {
if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; }
else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; };
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == tkind.TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == nkind.N_FIELD) {
let fname: str = fieldnode.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fname)) {
cgexpr(c, fieldnode.lhs);
// f64/f32 struct-literal field init: cgexpr left
// the value in X0, store via MOVSD/MOVSS.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
} else {
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fieldnode = fieldnode.next;
};
c.lastwasreturn = 0;
return;
};
};
cgexpr(c, rhs);
// Float local: cgexpr leaves the value in X0. Spill via
// MOVSS (f32, 4B) or MOVSD (f64, 8B).
if (isfloattype(c, n.lhs)) {
let mov: str = "MOVSD";
if (isf32type(c, n.lhs)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
// str init: cgexpr also leaves len in BX; store both.
if (sz == 16) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
};
// slice init: ptr/len/cap in AX/BX/CX.
if (sz == 24) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
} else {
// Bare `let x: T;` with no initializer. C cgen
// (cmd/w6c/cgen.c:3317) zero-inits whenever the raw type size
// is 8: scalar primitives, pointers, fn/chan handles, plus 8B
// composites like `[8]bool`, `[2]i32`, `[4]i16`, `[1]i64`.
// Larger composites and `[N]T` with size != 8 are left for
// per-field writes.
if (typeis8byteprimitive(c, n.lhs)) {
emitline("\tMOVQ\t$0, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
c.lastwasreturn = 0;
return;
};
fn cgif(c: *cgen, n: *node) void = {
let els: str = mklabel(c, "else");
let endl: str = mklabel(c, "end");
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
if (n.els != nil) { emitline(els); }
else { emitline(endl); };
emitline("\n");
if (n.body != nil) { cgstmt(c, n.body); };
if (n.els != nil) {
emitline("\tJMP\t"); emitline(endl); emitline("\n");
emitlabel(els);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
fn cgfor(c: *cgen, n: *node) void = {
// Match C cgen's label scheme: <fn>_loop_N for the top,
// <fn>_endloop_N for the post-body merge. No separate cont
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
emitlabel(topl);
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
c.loopcontbuf[c.looptop] = topl;
c.looptop += 1;
if (n.body != nil) { cgstmt(c, n.body); };
c.looptop -= 1;
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
// Tuple-destructure assign: `a, b = call();`. The call's tuple
// return lands in (AX, DX); push DX to free it, store AX into
// the first lvalue, then pop DX into the second. Mirrors
// cmd/w6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// as C — no fixture uses >2 today).
fn cgmassign(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tPUSHQ\tDX\n");
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
if (l0 != nil) {
if (l0.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l0.str);
if (off != 0) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
emitline("\tPOPQ\tDX\n");
if (l1 != nil) {
if (l1.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l1.str);
if (off != 0) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
c.lastwasreturn = 0;
return;
};
// Multi-let from a tuple-returning call: `let n, s = call();` or
// `let (n, s) = call();`. wwstage has no checker, so each binding's
// type is taken from its explicit annotation (l.lhs) when present
// or inferred from the called fn's return-type tuple element.
//
// Per the AX:DX:CX return convention (mirrors C cgen nkind.N_MLET):
// (scalar, scalar) — AX → l0, DX → l1.
// (scalar, str) — AX → scalar slot, (DX, CX) → str slot
// as (.ptr, .len). Position-agnostic — the
// regs are routed by element type, not by AX/DX.
fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
let p0t: *node = nil;
let p1t: *node = nil;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == nkind.N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
let t0: *node = nil;
let t1: *node = nil;
if (l0 != nil) { t0 = l0.lhs; };
if (l1 != nil) { t1 = l1.lhs; };
if (t0 == nil) { t0 = p0t; };
if (t1 == nil) { t1 = p1t; };
let s0_is_str: bool = isstrtyperaw(t0);
let s1_is_str: bool = isstrtyperaw(t1);
cgexpr(c, rhs);
if (l0 != nil) {
if (l1 != nil) {
if (s0_is_str != s1_is_str) {
let sz0: i32 = 8;
let sz1: i32 = 8;
if (s0_is_str) { sz0 = 16; };
if (s1_is_str) { sz1 = 16; };
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
if (l0 != nil) {
let off: i32 = localadd(c, l0.str, 8, t0);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (l1 != nil) {
let off: i32 = localadd(c, l1.str, 8, t1);
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
// paramfieldsize — raw byte size of a tuple-field type. Mirrors the
// `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for
// i32/u32, 8 for i64/u64/*T/fn/slice-elt, 16 for str, default 8.
fn paramfieldsize(t: *node) i32 = {
if (t == nil) { return 8; };
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
};
return 8;
};
// paramissigned — does this primitive type need sign-extending on a
// narrow (4B) load? Mirrors C cgen's `binds[b].signed_field` flag.
fn paramissigned(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
let nm: str = t.str;
if (streq(nm, "i8")) { return true; };
if (streq(nm, "i16")) { return true; };
if (streq(nm, "i32")) { return true; };
return false;
};
// cgforrange — lower `for (let x .. slice) body` (and the tuple-
// destructure cousin `for (let (a, b) .. slice) body`). The body is
// wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`.
// Each iteration computes the element address `s.ptr + i*esz` and
// either loads the whole element into the named local or pulls each
// tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE
// byte-for-byte (label names + labelseq consumption order).
fn cgforrange(c: *cgen, n: *node) void = {
let slc: *node = n.lhs;
let slclocal: *local = nil;
let slctn: *node = nil;
if (slc != nil) {
if (slc.kind == nkind.N_IDENT) {
slclocal = localfindnode(c, slc.str);
if (slclocal != nil) { slctn = slclocal.tnode; };
};
};
// Element type — peek through TSLICE/TARRAY for the tuple param walk.
let elemt: *node = nil;
if (slctn != nil) {
let sk: nkind = slctn.kind;
if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; };
if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; };
};
// esz: raw elem byte size. For tuple-element slices `[](T0, T1)`,
// C cgen reads the resolved tuple's size (sum of raw param sizes,
// no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8.
// elemsizeof returns 8 for non-primitive elem, which would be
// wrong here — compute from the tuple param walk instead.
let esz: i32 = elemsizeof(slctn);
if (elemt != nil) {
if (elemt.kind == nkind.N_TTUPLE) {
let total: i32 = 0;
let p: *node = elemt.list;
for (p != nil) {
total += paramfieldsize(p);
p = p.next;
};
esz = total;
};
};
let destruct: bool = (n.list != nil);
// .rgi (counter) + .rgl (length) scratch slots.
let iname: str = mkscratchname(c, "rgi");
let lname: str = mkscratchname(c, "rgl");
let ioff: i32 = localalloc(c, iname, 8, nil);
let loff: i32 = localalloc(c, lname, 8, nil);
// Per-binding (up to 8 — matches the C array). Parallel arrays so
// we don't depend on local-struct cgen.
let bind_off: [8]i32;
let bind_sz: [8]i32;
let bind_foff: [8]i32;
let bind_signed: [8]bool;
let nbinds: i32 = 0;
if (destruct) {
let tp: *node = nil;
if (elemt != nil) {
if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; };
};
let field_off: i32 = 0;
let m: *node = n.list;
for (m != nil) {
if (nbinds >= 8) { m = nil; }
else {
let fsz: i32 = 8;
let signf: bool = false;
if (tp != nil) {
fsz = paramfieldsize(tp);
signf = paramissigned(tp);
};
let slot_sz: i32 = fsz;
if (slot_sz < 8) { slot_sz = 8; };
bind_sz[nbinds] = fsz;
bind_foff[nbinds] = field_off;
bind_signed[nbinds] = signf;
let bnm: str = m.str;
if (bnm.len > 0) {
bind_off[nbinds] = localadd(c, bnm, slot_sz, tp);
} else {
bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tp);
};
field_off += fsz;
nbinds += 1;
if (tp != nil) { tp = tp.next; };
m = m.next;
};
};
} else {
let slot_sz: i32 = esz;
if (slot_sz < 8) { slot_sz = 8; };
bind_sz[0] = esz;
bind_foff[0] = 0;
// Single-binding signed-narrow detection: mirror C which
// reads `u->sub->kind` for the elem type.
bind_signed[0] = false;
if (elemt != nil) {
bind_signed[0] = paramissigned(elemt);
};
if (n.str.len > 0) {
// Register with elem tnode so x.field on a loop
// var resolves through the standard local-typed
// path instead of falling into the SB fallback.
bind_off[0] = localadd(c, n.str, slot_sz, elemt);
} else {
bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt);
};
nbinds = 1;
};
// init: ioff(BP) = 0
emitline("\tMOVQ\t$0, ");
emitoff(ioff: i64);
emitline("(BP)\n");
// loff(BP) = len
let isarr: bool = false;
let isslicestr: bool = false;
if (slctn != nil) {
let tk: nkind = slctn.kind;
if (tk == nkind.N_TSLICE) { isslicestr = true; };
if (tk == nkind.N_TARRAY) { isarr = true; };
if (tk == nkind.N_TNAME) {
if (streq(slctn.str, "str")) { isslicestr = true; };
};
};
if (isslicestr) {
if (slc.kind == nkind.N_IDENT) {
if (slclocal != nil) {
emitline("\tMOVQ\t");
emitoff((slclocal.off + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(loff: i64);
emitline("(BP)\n");
};
};
} else { if (isarr) {
let alen: i64 = 0i64;
if (slctn.rhs != nil) {
if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; };
};
emitline("\tMOVQ\t$");
emitint(alen);
emitline(", ");
emitoff(loff: i64);
emitline("(BP)\n");
} else {
cgexpr(c, slc);
emitline("\tMOVQ\tAX, ");
emitoff(loff: i64);
emitline("(BP)\n");
};};
let loopl: str = mklabel(c, "rloop");
let endl: str = mklabel(c, "rend");
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "relseloop"); };
c.loopcontbuf[c.looptop] = loopl;
c.loopendbuf[c.looptop] = endl;
c.looptop += 1;
emitlabel(loopl);
emitline("\tMOVQ\t");
emitoff(ioff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff(loff: i64);
emitline("(BP), BX\n");
emitline("\tCMPQ\tBX, AX\n");
emitline("\tJGE\t"); emitline(naturall); emitline("\n");
// BX = base + i*esz
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (slc.kind == nkind.N_IDENT) {
if (slclocal != nil) {
if (isarr) {
emitline("\tLEAQ\t");
emitoff(slclocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(slclocal.off: i64);
emitline("(BP), BX\n");
};
};
};
emitline("\tADDQ\tAX, BX\n");
// Per-binding load from BX+foff.
let b: i32 = 0;
for (b < nbinds) {
let op: str = "MOVQ";
if (bind_sz[b] == 1) { op = "MOVZBQ"; }
else { if (bind_sz[b] == 4) {
if (bind_signed[b]) { op = "MOVSXD"; }
else { op = "MOVL"; };
};};
emitline("\t");
emitline(op);
emitline("\t");
emitoff(bind_foff[b]: i64);
emitline("(BX), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(bind_off[b]: i64);
emitline("(BP)\n");
b += 1;
};
if (n.body != nil) { cgstmt(c, n.body); };
c.looptop -= 1;
emitline("\tADDQ\t$1, ");
emitoff(ioff: i64);
emitline("(BP)\n");
emitline("\tJMP\t"); emitline(loopl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
// cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to
// a chain of compares against the scrutinee. Scrutinee lands in a
// fresh 8B local slot so case bodies can spill SP without losing it.
// Cases are tried top-to-bottom; the `case:` arm with no exprs is the
// default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c
// N_SWITCH: same labelseq consumption order so labels match byte-for-
// byte.
fn cgswitch(c: *cgen, n: *node) void = {
let swname: str = mkscratchname(c, "sw");
let sloff: i32 = localalloc(c, swname, 8, nil);
if (n.lhs != nil) { cgexpr(c, n.lhs); };
emitline("\tMOVQ\tAX, ");
emitoff(sloff: i64);
emitline("(BP)\n");
let endl: str = mklabel(c, "swend");
let defcase: *node = nil;
let cs: *node = n.list;
for (cs != nil) {
if (cs.list == nil) {
defcase = cs;
cs = cs.next;
continue;
};
let body: str = mklabel(c, "swcase");
let nxt: str = mklabel(c, "swnext");
let e: *node = cs.list;
for (e != nil) {
cgexpr(c, e);
emitline("\tMOVQ\t");
emitoff(sloff: i64);
emitline("(BP), BX\n");
emitline("\tCMPQ\tBX, AX\n");
emitline("\tJE\t");
emitline(body);
emitline("\n");
e = e.next;
};
emitline("\tJMP\t");
emitline(nxt);
emitline("\n");
emitlabel(body);
if (cs.body != nil) { cgstmt(c, cs.body); };
emitline("\tJMP\t");
emitline(endl);
emitline("\n");
emitlabel(nxt);
cs = cs.next;
};
if (defcase != nil) {
if (defcase.body != nil) { cgstmt(c, defcase.body); };
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
fn cgbreak(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopendbuf[c.looptop - 1];
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgcontinue(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopcontbuf[c.looptop - 1];
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
};
c.lastwasreturn = 0;
return;
};