// 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: i32 = n.kind; if (k == N_BLOCK) { cgblock(c, n); return; }; if (k == N_RETURN) { cgreturn(c, n); return; }; if (k == N_EXPRSTMT) { cgexprstmt(c, n); return; }; if (k == N_LET) { cglet(c, n); return; }; if (k == N_IF) { cgif(c, n); return; }; if (k == N_FOR) { cgfor(c, n); return; }; if (k == N_MASSIGN) { cgmassign(c, n); return; }; if (k == N_MLET) { cgmlet(c, n); return; }; if (k == N_BREAK) { cgbreak(c, n); return; }; if (k == N_CONTINUE) { cgcontinue(c, n); return; }; if (k == N_YIELD) { cgyield(c, n); return; }; if (k == 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 == 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.fnret)) { cgexpr(c, rhs); if (isnullabletype(c.fnret)) { 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 (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.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 off: i32 = localadd(c, nm, sz, n.lhs); 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). if (istaggedtype(n.lhs)) { let nullable: bool = isnullabletype(n.lhs); let rhsreturnstagged: bool = false; if (rhs.kind == N_CALL) { let callee: *node = rhs.lhs; if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; if (callee.kind == N_IDENT) { calleename = callee.str; }; if (callee.kind == N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); if (istaggedtype(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, n.lhs, 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"); 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 == 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 N_FIELD with str=="...") fills the // remaining slots up to the declared length with that value. if (rhs.kind == N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == 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 == 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 == N_TARRAY) { if (n.lhs.rhs != nil) { if (n.lhs.rhs.kind == 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 == TK_ELLIPSIS (autofill marker from the parser), the // entire slot is zero-filled first so unmentioned fields // read as 0. if (rhs.kind == N_STRUCTLIT) { let trefn: *node = rhs.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (trefn != nil) { if (trefn.kind == N_IDENT) { sname = trefn.str; } else { if (trefn.kind == N_TNAME) { sname = trefn.str; }; }; }; let si: *structinfo = structlookup(c, sname); if (si != nil) { if (rhs.op == 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 == 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); 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); 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:2181-2183) zero-inits only when // the underlying type's natural size is 8 — pointers, // i64/u64, function pointers, ints. Structs/arrays/ // slices/strings/tagged/tuples are left for per-field // writes. ww's slotsize pads struct slots up to 8, // so we can't just check sz == 8: walk the type AST // directly to make the same call. 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: _loop_N for the top, // _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 == 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 == 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 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 == N_CALL) { let callee: *node = rhs.lhs; if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; if (callee.kind == N_IDENT) { cnm = callee.str; }; if (callee.kind == N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); if (rt != nil) { if (rt.kind == 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; }; 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; };