// 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. package wcc; import os; import ast; import tok; import typ; import sym; import 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 = { // Save/restore the locals head across the block (post-#27). // Inner-scope `let` bindings prepend to c.locals via localadd; // without this restore, the prepended stubs leak into sibling // and ancestor scopes, and localfind (head-first) returns the // inner binding's offset for an identifier that semantically // belongs to the outer scope. The frame is left grown — we // don't reclaim popped slots, matching cstage's lowering. // // cgfn iterates fn_.body.list directly to bypass this save/ // restore at the function's outermost block — defers (and the // implicit-return epilogue) need locals intact. let saved: *local = c.locals; let s: *node = n.list; for (s != nil) { cgstmt(c, s); s = s.next; }; c.locals = saved; 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; }; // #83: positional tuple register-return ABI. Tuple elements ride // consecutive eightbytes over [AX,DX,CX,R8] (tupreg by index); a // slice/str rides its 3-word {ptr,len,cap} header (tyslicesize SSoT, // ref/hare/rt/ensure.ha:4-8), a scalar rides 1. SEND (cgreturn) and // RECEIVE (cgmlet/cgmassign) walk the SAME widths so element->register // agrees — mirrors harec create_unpack_bindings // (ref/harec/src/check.c:1354-1416). Capacity is 4 (AX,DX,CX,R8). fn tupreg(i: i32) str = { if (i == 0) { return "AX"; }; if (i == 1) { return "DX"; }; if (i == 2) { return "CX"; }; return "R8"; }; // #164 (#107): SSE half of the SysV dual register-class return. A float // element rides the SSE row [X0,X1] on a counter INDEPENDENT of the // INTEGER row tupreg — a float lands in the next XMM regardless of its // positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX}, // {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage // tuple_sse_seq (cmd/w6c/cgen.c). fn tupsse(i: i32) str = { if (i == 0) { return "X0"; }; return "X1"; }; fn tupebytes(wide: bool) i32 = { if (wide) { return (tyslicesize() / 8i64): i32; }; return 1; }; // rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil). // wwstage has no checker, so the receive sites read each tuple element's // width from the called fn's declared return type. Mirrors the callee // resolution shared by cgmlet/cgmassign. fn rettupleof(c: *cgen, rhs: *node) *node = { if (rhs == nil) { return nil; }; if (rhs.kind != nkind.N_CALL) { return nil; }; let callee: *node = rhs.lhs; if (callee == nil) { return nil; }; let cnm: str; cnm.ptr = nil; cnm.len = 0; let cmod: str; cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; cmod = c.curmod; }; if (callee.kind == nkind.N_DOT) { cnm = callee.str; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { cmod = callee.lhs.str; }; }; }; if (cnm.len == 0) { return nil; }; let rtyp: *node = fnretlookupmod(c, cnm, cmod); if (rtyp == nil) { return nil; }; if (rtyp.kind != nkind.N_TTUPLE) { return nil; }; return rtyp; }; // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive // INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a // scalar stores 1 INTEGER word. The caller owns the dual cursor // (validated + advanced). Byte-identical to the cstage tuple_store // (cmd/w6c/cgen.c). fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); emitline(tupreg(gpcur + 0)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); emitline(tupreg(gpcur + 1)); emitline(", "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); emitline(tupreg(gpcur + 2)); emitline(", "); emitoff((off + 16): i64); emitline("(BP)\n"); return; }; // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg // (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else // the slot gets garbage and the FACE-Z field read sees it. The SSE // regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105 // single-float byte-id; idx1=X1 is the #107 multi-float extension. if (isfloattype(c, tn)) { // #121 (Package B) RESIDUAL sibling-evidence guard, pin form. // In destructure mode tn IS the tuple-element-type-AST node // (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value // stored" rides X0 with no separate AST. isfloattype(c, tn) at // the branch head already implies tn.type_!=nil (typeisfloat is // false on nil), so this assertion is structurally unreachable // today — RETAINED to PIN the contract: "the float-store branch // requires a stamped slot." Catches a future change that opens // this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple // element binding where the destructure stamp didn't land — // #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/ // 1475 + asserttyped file:line at check.ww:3340-3344. if (tn != nil) { if (tn.type_ == nil) { let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at "; os.write(2, msg.ptr, msg.len: u64); if (tn.file.len > 0) { os.write(2, tn.file.ptr, tn.file.len: u64); os.write(2, ":".ptr, 1u64); let ls: str = strconv.i32tos(tn.line, strconv.base.DEC); os.write(2, ls.ptr, ls.len: u64); os.write(2, " ".ptr, 1u64); }; let kn: str = nkname(tn.kind); os.write(2, kn.ptr, kn.len: u64); os.write(2, "\n".ptr, 1u64); os.exit(1); }; }; let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitline(tupsse(ssecur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\tMOVQ\t"); emitline(tupreg(gpcur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); }; fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; if (rhs != nil) { // #83 / #164 (#107): positional register-return over a SysV // dual class cursor (harec create_unpack_bindings, ref/harec/src/ // check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 = // tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8] // (tupreg) — a slice/str its 3-word {ptr,len,cap} header // (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a // scalar 1 word in AX. Integer words spill L->R to the stack and // pop into the INTEGER cursor in reverse so positional slot i // lands in tupreg(i) (byte-id with #83 when no float is present). // Each float must spill X0 to @tupfscr as we walk, since a later // element's cgexpr clobbers X0; after the integer pops the saved // floats reload into X0/X1 by SSE index — INDEPENDENT of the // INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows // loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME // class split drives the receive sites. if (rhs.kind == nkind.N_TUPLE) { let ssecap: i32 = 2; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; let e: *node = rhs.list; for (e != nil) { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); gptotal = gptotal + tupebytes(wide); }; e = e.next; }; if (gptotal > 4) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage // uses fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (ssecount > ssecap) { let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let fscr: i32 = 0; if (ssecount > 0) { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; e = rhs.list; for (e != nil) { let isflt: bool = isfloattype(c, e); cgexpr(c, e); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((fscr + sseidx * 8): i64); emitline("(BP)\n"); sseidx = sseidx + 1; } else { emitline("\tPUSHQ\tAX\n"); // scalar / .ptr if (nodeisstr(c, e) || nodeisslice(c, e)) { emitline("\tPUSHQ\tBX\n"); // .len emitline("\tPUSHQ\tCX\n"); // .cap }; }; e = e.next; }; let i: i32 = gptotal - 1; for (i >= 0) { emitline("\tPOPQ\t"); emitline(tupreg(i)); emitline("\n"); i = i - 1; }; let j: i32 = 0; e = rhs.list; for (e != nil) { if (isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((fscr + j * 8): i64); emitline("(BP), "); emitline(tupsse(j)); emitline("\n"); j = j + 1; }; e = e.next; }; 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; let cmod: str; cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { calleename = callee.str; cmod = c.curmod; }; if (callee.kind == nkind.N_DOT) { calleename = callee.str; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { cmod = callee.lhs.str; }; }; }; if (calleename.len > 0) { let rtyp: *node = fnretlookupmod(c, calleename, cmod); if (istaggedtype(c, rtyp)) { 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); // @retscr (not @tagscr) for the return materialise // path. Cstage cmd/w6c/cgen.c cgreturn uses // `@retscr` here and reserves the @tagscr SSoT // for arg-widen / non-BP-base store / N_INDEX // tagged-element write. Sharing the name in a fn // that BOTH returns a 32B tagged AND pushes a // smaller tagged arg fatals localadd's @-prefix // size-grow guard (rule 7); routing returns // through their own slot keeps each cache // monotonic. Hardcoding 24 truncated 32B-slot // returns and overwrote adjacent locals during // the pre-zero loop (#38). let scroff: i32 = localadd(c, "@retscr", rsz, 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.type_: *tinfo, rhs, "BP", 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); // Tagged-return ABI: AX=tag, DX=word0, CX=word1, // R8=word2. Receiver (cgwidentaggedstore call-source // arm) writes AX/DX/CX/R8 unconditionally sized by the // dst slot; unused ABI words must be zeroed here so a // stale CX/R8 from the caller (e.g. a slice-stride // IMULQ before the call) does not land in slot+16 / // slot+24. (Task #18.) let rsz: i32 = slotsize(c, c.fnret); // Value-class read off the checker stamp (rhs.type_) — // the SSoT cstage reads via node_isfloat / type_isf32. let rfk: i32 = 0; if (rhs != nil) { let rety: *tinfo = rhs.type_: *tinfo; if (typeisf32(rety)) { rfk = 1; } else { if (typeisfloat(rety)) { rfk = 2; }; }; }; 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)) { // str IS []u8: cgexpr leaves (AX=ptr, BX=len, // CX=cap). Same shuffle as the slice arm above — // DX=ptr, CX=len, R8=cap (#1/Phase 3). emitline("\tMOVQ\tCX, R8\n"); emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); } else { if (rfk != 0) { // #157: float variant — cgexpr left the value // in X0, not AX. No MOVQ-xmm->gp encoding, so // bridge X0->DX through a stack slot (same arg- // push idiom). Zero the slot first so the f32 // case (MOVSS writes only the low 4 bytes) // leaves a deterministic high-4 — cs==ww byte- // id, matching f64's MOVSD which fills all 8. // The AX-independent spill also removes the // stale-AX cs!=ww on multi-variant returns. emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let mov: str = "MOVSD"; if (rfk == 1) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); emitline("\tMOVQ\t(SP), DX\n"); emitline("\tADDQ\t$8, SP\n"); if (rsz > 16) { emitline("\tMOVQ\t$0, CX\n"); }; if (rsz > 24) { emitline("\tMOVQ\t$0, R8\n"); }; } else { emitline("\tMOVQ\tAX, DX\n"); // scalar fills DX only. Zero CX / R8 if dst // covers slot+16 / slot+24. if (rsz > 16) { emitline("\tMOVQ\t$0, CX\n"); }; if (rsz > 24) { emitline("\tMOVQ\t$0, R8\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; }; // sret return (#23): plain TY_STRUCT > 24B. Callee writes // through *(@sretarg) (the caller-prealloc dest saved at // the prologue), then loads @sretarg into RAX and rets — // the SysV "return the pointer" discipline. Two rhs shapes // are wired: N_IDENT (word-copy from rhs slot to *(dest)) // and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL). let sretargoff: i32 = localfind(c, "@sretarg"); if (sretargoff != 0) { let scs: i32 = sretretsize(c, c.fnret); if (scs > 0) { // sret return-forwarding (task #9 follow-up to // #23): `return f();` where outer + inner both // return the same >24B struct shape. Outer's // @sretarg already holds its caller's prealloc // dest; pass it to inner in RDI (set by cgcall // via c.sretforward), inner writes directly // there, inner's RAX (dest pointer) is already // outer's return value. The trailing MOVQ // @sretarg(BP), AX is redundant after inner's // RET but kept for byte-id symmetry with the // N_IDENT / N_STRUCTLIT arms below. if (rhs.kind == nkind.N_CALL) { c.sretforward = 1; cgexpr(c, rhs); emitline("\tMOVQ\t"); emitoff(sretargoff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; }; if (okrhs) { 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 sret_si: *structinfo = structlookup(c, sname); if (sret_si != nil) { let emptys: str; emptys.ptr = nil; emptys.len = 0; // mode=1 (PTR_LOCAL): base reg = BX, // reloaded from @sretarg(BP) before // each field store. disp = 0 because // the dest pointer IS the struct base. cgstructlitfill(c, sret_si, rhs, 1, sretargoff, emptys, 0); }; } else { let rl: *local = localfindnode(c, rhs.str); if (rl != nil) { emitline("\tMOVQ\t"); emitoff(sretargoff: i64); emitline("(BP), BX\n"); let k: i32 = 0; for (k + 8 <= scs) { emitline("\tMOVQ\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 8; }; for (k + 4 <= scs) { emitline("\tMOVL\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 4; }; for (k < scs) { emitline("\tMOVB\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 1; }; }; }; // sret return: RAX = dest pointer. emitline("\tMOVQ\t"); emitoff(sretargoff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; }; }; // Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7], // DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c // N_RETURN TY_STRUCT branch. Two rhs shapes are wired: // N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT // (field-by-field store at scratch+foff, with tagged fields // delegated to cgwidentaggedstore). Call-result chain return // is deferred to #5's receive side. Sizes > 24B route through // the sret arm above. let rname: str; rname.ptr = nil; rname.len = 0; if (c.fnret != nil) { if (c.fnret.kind == nkind.N_TNAME) { rname = c.fnret.str; }; }; if (rname.len > 0) { let rsi: *structinfo = structlookup(c, rname); if (rsi != nil) { // ≤24B register RETURN: cstage sizes by rt->size // (maxalign-rounded), not the slot-padded totsize // (round-to-8) — see structabisize (#169). let rsz: i32 = structabisize(rsi); if (rsz <= 24) { let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; }; if (okrhs) { let scroff: i32 = localadd(c, "@retscr", 24, nil); emitline("\tXORQ\tAX, AX\n"); emitline("\tMOVQ\tAX, "); emitoff(scroff: i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + 16): i64); emitline("(BP)\n"); if (rhs.kind == nkind.N_STRUCTLIT) { // Delegate to the shared BP-relative // structlit fill helper. Same store // sequence the inline pre-#17 walk // emitted (tagged + float + scalar), // plus nested struct-typed structlit // values recurse instead of dropping // trailing bytes. cgstructlitfillbp(c, rsi, rhs, scroff); } else { // N_IDENT: word-copy from rhs slot // to scratch. Whole 8B words via // MOVQ; tail via MOVL/MOVB so we // read no further than the source // slot's declared size. let rl: *local = localfindnode(c, rhs.str); if (rl != nil) { let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 8; }; for (k + 4 <= rsz) { emitline("\tMOVL\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 4; }; for (k < rsz) { emitline("\tMOVB\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; }; }; // #171a: float-bearing struct RETURN (return // twin of #165's param recv). A qualifying // struct's float eightbytes ride the SSE return // row (X0,X1 = tupsse), its INT eightbytes the // INTEGER return row (AX,DX = tupreg), on // INDEPENDENT cursors per SysV (ref/qbe/amd64/ // sysv.c retr) — so a float lands in the next // XMM regardless of its positional eightbyte // (struct{f64,i32}: e0→X0, e1→AX, NOT DX). The // scratch is zero-padded to 24B so a full MOVQ // on a trailing INT eightbyte reads no garbage // (the #169 sized tail is a RECV concern). // structfloatclass gates to qualifying structs; // all-int + f32 keep the AX/DX/CX transport // (byte-id / #171b). let sfc: i32 = structfloatclass(c, c.fnret); if (sfc != 0) { let nb: i32 = sfc & 15; let gpcur: i32 = 0; let ssecur: i32 = 0; let e: i32 = 0; for (e < nb) { let issse: bool = (sfc & (16 << e)) != 0; if (issse) { emitline("\tMOVSD\t"); emitoff((scroff + e*8): i64); emitline("(BP), "); emitline(tupsse(ssecur)); emitline("\n"); ssecur += 1; } else { emitline("\tMOVQ\t"); emitoff((scroff + e*8): i64); emitline("(BP), "); emitline(tupreg(gpcur)); emitline("\n"); gpcur += 1; }; e += 1; }; } else { emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((scroff + 8): i64); emitline("(BP), DX\n"); 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); } 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"); }; // str IS []u8: cgexpr leaves AX=ptr, BX=len, CX=cap — str now // returns exactly like a slice, no AX:DX shuffle (#1/Phase 3). 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; // `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45). // Mirror of cstage cgen.c N_LET arrlit-empty branch: allocate // n*esz bytes via rt_malloc, then build the {ptr, 0, n} slice // header in the let slot. The `!`/`?` wraps the builtin's // `([]T | nomem)` return; walk into the N_TRYUNW / N_TRYPROP // to keep the direct-store fast path rather than falling // through to cgalloc (which models scalar alloc and would // land an 8B region and a junk slice header). `?` propagates // nomem via AX = tag of nomem in c.fnret, then epilogue RET. { let scall: *node = nil; let viatryunw: bool = false; let viatryprop: bool = false; if (rhs.kind == nkind.N_TRYUNW) { if (rhs.lhs != nil) { if (rhs.lhs.kind == nkind.N_CALL) { scall = rhs.lhs; viatryunw = true; }; }; } else { if (rhs.kind == nkind.N_TRYPROP) { if (rhs.lhs != nil) { if (rhs.lhs.kind == nkind.N_CALL) { scall = rhs.lhs; viatryprop = true; }; }; }; }; let shapeok: bool = false; // #43: route the slice-shape size guard through SSoT. // The N_TSLICE kind gate already discriminates here, so // this is belt-and-suspenders, but the literal would // silently miss after #1 if check.ww's astsize ever // drifted from this dispatch. if (scall != nil && tn != nil && tn.kind == nkind.N_TSLICE && sz == tyslicesize(): i32) { let callee: *node = scall.lhs; let a0: *node = scall.list; let a1: *node = nil; let a2: *node = nil; if (a0 != nil) { a1 = a0.next; }; if (a1 != nil) { a2 = a1.next; }; if (callee != nil && a0 != nil && a1 != nil && a2 == nil) { if (callee.kind == nkind.N_IDENT && streq(callee.str, "alloc") && a0.kind == nkind.N_ARRLIT && a0.list == nil) { shapeok = true; }; }; }; if (shapeok) { // #32: cstage uses `lu->sub->size` (cgen.c:6387), so // the element width must resolve struct/tagged/alias // names too — not just primitives. elemsizeofc follows // TNAME through structlookup/aliaslookup, matching the // cstage path byte-identically. A bare primsize/slotsize // fork would silently land esz=1 on `[]point`. let esz: i32 = elemsizeofc(c, tn); let count: *node = scall.list.next; cgexpr(c, count); emitline("\tPUSHQ\tAX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", BX\n"); emitline("\tIMULQ\tBX, AX\n"); }; emitline("\tMOVQ\tAX, DI\n"); emitline("\tCALL\t"); emitline(ffiresolve(c, "malloc")); emitline("(SB)\n"); if (viatryunw) { let okl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); emitline("\tMOVQ\t$1, DI\n"); emitline("\tMOVQ\t$60, AX\n"); emitline("\tSYSCALL\n"); emitlabel(okl); }; if (viatryprop) { // #45: null = nomem; propagate to the // enclosing fn's tagged return. AX = tag // of nomem variant in c.fnret, epilogue // RETs to caller. let okl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); // #66 Phase-N step 3: nomem propagation has no // pattern node, so it can't ride the typeeq // flatvariantidx path. cstage passes the ty_nomem // singleton to cg_tag_for_variant; the wwstage cgen // holds no tinfo singleton, so find the nomem // variant by its NAMED name over tinfo.params. let nidx: i32 = -1; let nti: *tinfo = nil; if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; }; for (nti != nil && nti.kind == tykind.TY_NAMED) { nti = nti.under; }; if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) { let np: *tparam = nti.params; let nidx2: i32 = 0; for (np != nil) { let nvt: *tinfo = np.type_; if (nvt != nil) { if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; }; }; np = np.tnext; nidx2 += 1; }; }; }; if (nidx < 0) { nidx = 1; }; emitline("\tMOVQ\t$"); emitint(nidx: i64); emitline(", AX\n"); emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(okl); }; emitline("\tPOPQ\tBX\n"); emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t$0, "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + 16): i64); emitline("(BP)\n"); c.lastwasreturn = 0; return; }; }; // 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.type_: *tinfo, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; // 32B tuple init for `let t: (scalar, str) = call()` / // `let t: (str, scalar) = call()` (#105 / #164/#107). Each // element rides its SysV class — a float its SSE cursor reg // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg // (tupreg), a slice/str its 3-word {ptr,len,cap} header over // consecutive INTEGER cursor regs — on INDEPENDENT counters. // tupstore routes each element from its real class into its // positional slot (eoff steps by the element's slot size: a // slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple // (#1/Phase 3, task #5). Mirror of the cstage unified branch. 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 p0t: *node = nil; let p1t: *node = nil; if (p0 != nil) { p0t = p0.lhs; }; if (p1 != nil) { p1t = p1.lhs; }; let s0_is_str: bool = isstrtype(c, p0t) || isslicetype(c, p0t); let s1_is_str: bool = isstrtype(c, p1t) || isslicetype(c, p1t); if (p0 != nil) { if (p1 != nil) { if (s0_is_str != s1_is_str) { cgexpr(c, rhs); let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; let q: *node = n.lhs.list; for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); let wide: bool = isstrtype(c, qt) || isslicetype(c, qt); tupstore(c, gpcur, ssecur, off + eoff, wide, qt); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + tupebytes(wide); }; if (wide) { eoff = eoff + (tyslicesize(): i32); } else { eoff = eoff + 8; }; q = q.next; }; c.lastwasreturn = 0; return; }; }; }; }; }; // 16B tuple init from a function call (#105 / #164/#107). Each // eightbyte rides its SysV class: a float its SSE cursor reg // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg // (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves // floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ // spill would store garbage where a float rode and the #103- // FACE-Z field read (MOVSD-from-slot) would see it. tupstore // routes each word from its real class; the same split drives // the destructure / reassign sites. Without this branch a 16B // tuple receive fell to the generic single-word store below and // dropped word1 — silent loss of t.1 (#102). let rt16: *node = rettupleof(c, rhs); if (rt16 != nil && sz == 16) { cgexpr(c, rhs); let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; let q: *node = rt16.list; for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); tupstore(c, gpcur, ssecur, off + eoff, false, qt); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + 1; }; eoff = eoff + 8; q = q.next; }; 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. // // str element (16B = ptr+len) needs both halves stored. cgstrlit // / cgident leave a str as (AX=ptr, BX=len) and a single MOVQ // from AX would leave .len as whatever the stack held — silent // miscompile. Worse, primsize("str") returns 0 so esz would fall // back to 8, also collapsing the per-element stride (element i+1 // would overwrite element i's would-be .len half). Detect the // str-element case up front so both esz and the store path are // right. (primsize's default-to-8-on-zero pattern is brittle for // composites generally; same gap blocks slice / struct / tuple / // tagged element arrays — tracked as a follow-up.) if (rhs.kind == nkind.N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; let isstrel: bool = false; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { if (streq(elemn.str, "str")) { esz = primtypesize("str"): i32; isstrel = true; } else { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; }; let mop: str = tnodestoreop(c, elemn, esz); // float element → store FROM X0 (MOVSS/MOVSD): cgexpr // leaves a float in X0 and for f32 the #104 CVTSD2SS // narrowing only touches X0; the AX store (mop) would // write the raw double low-bits, garbage for f32 (#122, // mirrors cstage cgen.c:6889 arr-lit float store). let isfloatel: bool = isfloattype(c, elemn); let fmov: str = "MOVSD"; if (isf32type(c, elemn)) { fmov = "MOVSS"; }; let idx: i32 = 0; let repeat: bool = false; let e: *node = rhs.list; for (e != nil) { let isellip: bool = false; if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; isellip = true; }; }; if (isellip) { e = nil; } else { cgexpr(c, e); if (isstrel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); } else { if (isfloatel) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); } else { emitline("\t"); emitline(mop); emitline("\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); }; }; idx += 1; e = e.next; }; }; // AX (and BX for str) 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) { if (isstrel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); } else { if (isfloatel) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); } else { 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=...};`. // Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS // autofill + per-field walk, with nested struct-typed structlit // values recursing into the helper instead of landing only AX // (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET // structlit branch. 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) { cgstructlitfillbp(c, si, rhs, off); c.lastwasreturn = 0; return; }; }; // sret receive (#23): plain TY_STRUCT > 24B from a call. // The let's own slot IS the caller-prealloc dest; the // nested cgexpr → cgcall path emits `LEAQ off(BP), DI` // before the CALL and the callee writes through it. No // AX/DX/CX shuffle; AX returns the dest pointer per SysV // sret discipline (irrelevant here). if (rhs.kind == nkind.N_CALL) { let scs: i32 = callsretsize(c, rhs); if (scs > 0) { c.sretdestoff = off; cgexpr(c, rhs); c.sretdestoff = 0; c.lastwasreturn = 0; return; }; }; // Whole-struct receive for sizes <=24B (call-result rhs). // Counterpart of #4's cgreturn ABI: cgexpr leaves // AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], // zero-padded to 24B by the producer. // // ASYMMETRY (do NOT mirror the sender): producer emits three // uniform MOVQs into a zero-padded 24B scratch slot; the // receiver writes only `sz` bytes — MOVQ for full 8B chunks // plus a sized tail (MOVL/MOVW/MOVB) by the *declared* // struct size. Otherwise a trailing 1..7-byte chunk would // overrun into the next local slot. // // Tail chunks in {3,5,6,7} (unreachable under WW struct // alignment rules — field aligns force size%align==0) fall // through to the generic scalar store rather than emit a // stomping MOVQ tail. Sizes >24B also fall through (sret // deferred, same constraint as #4). Mirrors the cstage // cgen.c N_LET receive branch. // #171a: float-bearing struct RECEIVE (return twin of #165's // param recv). cgexpr leaves each float eightbyte in its SSE // return reg (X0,X1 = tupsse) and each INT eightbyte in its // INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors // per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read // from the next XMM regardless of its positional eightbyte // (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's // abisize is maxalign-rounded to a multiple of 8 (an f64 // forces align 8), so every eightbyte is a full word — the // #169 sized tail is unreachable here. structfloatclass gates // to qualifying structs; all-int + f32 fall to the GP recv // below (byte-id / #171b). if (rhs.kind == nkind.N_CALL && tn != nil) { let sfc: i32 = structfloatclass(c, tn); if (sfc != 0) { cgexpr(c, rhs); let nb: i32 = sfc & 15; let gpcur: i32 = 0; let ssecur: i32 = 0; let e: i32 = 0; for (e < nb) { let issse: bool = (sfc & (16 << e)) != 0; if (issse) { emitline("\tMOVSD\t"); emitline(tupsse(ssecur)); emitline(", "); emitoff((off + e*8): i64); emitline("(BP)\n"); ssecur += 1; } else { emitline("\tMOVQ\t"); emitline(tupreg(gpcur)); emitline(", "); emitoff((off + e*8): i64); emitline("(BP)\n"); gpcur += 1; }; e += 1; }; c.lastwasreturn = 0; return; }; }; if (rhs.kind == nkind.N_CALL) { let sname: str; sname.ptr = nil; sname.len = 0; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; }; if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { // ≤24B register RECV: the value arrives packed // in AX/DX/CX, so size by the maxalign-rounded // ABI size (cstage lu->size), not the natural // extent — see structabisize (#169). let lsz: i32 = structabisize(lsi); let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, rhs); let full: i32 = lsz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((off + i * 8): i64); emitline("(BP)\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((off + full * 8): i64); emitline("(BP)\n"); }; c.lastwasreturn = 0; return; }; }; }; }; }; // Struct ident copy: `let p2: T = p1;` where T is a struct // >8B and rhs is a local ident. Per-qword MOVQ from src // slot to dst slot, with a sized tail (MOVL/MOVB) for // natural sizes that aren't 8-aligned (e.g. `struct // { i32, i32, i32 }` is 12B). Pre-fix this path fell // through to `cgexpr + MOVQ AX, off(BP)` which stored // only the first qword (and a stale BX for sz==16 lets // via the str-init tail) — silent partial copy. Mirrors // cstage cgen.c N_LET struct-ident branch (Task #32). if (rhs.kind == nkind.N_IDENT) { let sname: str; sname.ptr = nil; sname.len = 0; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; }; if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { let lsz: i32 = structnaturalsize(lsi); if (lsz > 8) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { let soff: i32 = lc.off; let ki: i32 = 0; for (ki + 8 <= lsz) { emitline("\tMOVQ\t"); emitoff((soff + ki): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + ki): i64); emitline("(BP)\n"); ki += 8; }; if (ki < lsz) { let tail: i32 = lsz - 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((off + ki): i64); emitline("(BP)\n"); }; 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 IS []u8: cgexpr leaves (ptr,len,cap) in AX/BX/CX; store // all three, same as the slice arm below (#1/Phase 3). // #60: gate by kind too — under #1's str=24 bump, sizeof(str) // and sizeof(slice) collide, so a bare `sz ==` check fires // both branches for one let. Mirrors cstage cgen.c's // `type_isstr(lt) && sz == ty_str->size` shape. if (isstrtype(c, tn) && sz == primtypesize("str"): i32) { emitline("\tMOVQ\tBX, "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((off + 16): i64); emitline("(BP)\n"); }; // slice init: ptr/len/cap in AX/BX/CX. Same kind+size gate as // the str arm — without the kind check this fires on a str let // once sz==24 (#60). if (isslicetype(c, tn) && sz == tyslicesize(): i32) { 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 N_LET no-rhs branch) zero-inits in two // shapes: // - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.): // single `MOVQ $0, off(BP)`. // - multi-word composites (str/slice/tuple/struct/tagged): // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot // so reads after the bare let see {0...} rather than // stack garbage. // `[N]T` arrays of size != 8 keep the per-index-write // contract — they're left uninit. let isarr: bool = false; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; }; // Zero-fill extent. cstage sizes the run on `lu->size` // (the maxalign-rounded ABI size, check.c:760); wwstage's // `sz` from letslotsize is slot-padded (round-to-8), so a // struct with maxalign<8 and a sub-8 tail would over-zero // MOVQ where cstage emits MOVL/MOVB. Read structabisize // for a struct-typed let to converge; slot allocation // stays on `sz` (frame uses slot-padded slots). let zsz: i32 = sz; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { zsz = structabisize(szi); }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (!isarr) { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; for (zi + 8 <= zsz) { emitline("\tMOVQ\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 8; }; for (zi + 4 <= zsz) { emitline("\tMOVL\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 4; }; for (zi < zsz) { emitline("\tMOVB\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 1; }; }; }; }; }; 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"); }; // #138: `continue` in a 3-clause `for (init; cond; post)` must // run the post-step before re-testing cond. Pre-fix the continue- // target was `topl`, which SKIPPED the post-step → state never // advanced → infinite loop. Allocate a dedicated `post` label // only when there IS a post-step (`n.rhs != nil`); else keep // continue → loop-top, byte-id with 1-clause for. let conttgt: str = topl; if (n.rhs != nil) { conttgt = mklabel(c, "post"); }; 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] = conttgt; c.looptop += 1; if (n.body != nil) { cgstmt(c, n.body); }; c.looptop -= 1; if (n.rhs != nil) { emitlabel(conttgt); 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 = { // #83: positional per-element destructure REASSIGN. Same cursor as // cgmlet (and cgreturn; harec create_unpack_bindings, // ref/harec/src/check.c:1354-1416), but the slots already exist // (reassignment) so localfind them. wwstage has no checker, so each // element's width comes from the called fn's return-type tuple // element (N_TTUPLE param) walked in lockstep with the bindings; a // slice/str rides its 3-word {ptr,len,cap} header // (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes // its register slot without storing (mirrors harec `_`). This bare- // comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond // Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike // multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops. let rettuple: *node = rettupleof(c, n.rhs); if (n.rhs != nil) { cgexpr(c, n.rhs); }; let ssecap: i32 = 2; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); gptotal = gptotal + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; if (gptotal > 4) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (ssetotal > ssecap) { let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let gpcur: i32 = 0; let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; // harec `_` (off==0): skip the store but CONSUME the cursor // slot so the next element stays aligned. if (off != 0) { tupstore(c, gpcur, ssecur, off, wide, tn); }; if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; 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:R8 return convention (mirrors C cgen nkind.N_MLET): // (scalar, scalar) — AX → l0, DX → l1. // (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot // as (.ptr, .len, .cap). Position-agnostic — the // regs are routed by element type, not by AX/DX. // str IS []u8 (24B): cap rides R8 (#1/Phase 3, task #5). fn cgmlet(c: *cgen, n: *node) void = { let rhs: *node = n.rhs; if (rhs == nil) { return; }; // #83: positional per-element destructure let-binding. Same cursor // as cgmassign (and cgreturn; harec create_unpack_bindings, // ref/harec/src/check.c:1354-1416). wwstage has no checker, so each // binding's type is its explicit annotation (l.lhs) when present, // else the called fn's return-type tuple element (N_TTUPLE param) // walked in lockstep. A slice/str rides its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a // scalar rides 1 word into an 8B slot. Over-capacity loud-stops. let rettuple: *node = rettupleof(c, rhs); cgexpr(c, rhs); let ssecap: i32 = 2; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = l.lhs; if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); gptotal = gptotal + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; if (gptotal > 4) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (ssetotal > ssecap) { let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let gpcur: i32 = 0; let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = l.lhs; if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); tupstore(c, gpcur, ssecur, off, wide, tn); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; 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 primtypesize("str"): i32; }; let ps: i32 = primsize(nm); if (ps > 0) { return ps; }; }; return 8; }; // paramissigned — does this type need sign-extending on a sub-word // (1/2/4B) load? Mirrors cstage's signed_field check via // fieldissignedc (resolves TBANG / TENUM / alias chains). fn paramissigned(c: *cgen, t: *node) bool = { return fieldissignedc(c, t); }; // 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; }; // str IS []u8 (F1: tystr.sub = tyu8). []u8 hands cgen a real // u8 element node (slctn.lhs); a str scrutinee has none, so the // loop var would register tnode=nil and read back as a wide // MOVQ. Synthesise the u8 element off str.sub so the loop-var // registration carries a u8 tnode and localloadop narrows the // read-back to MOVZBQ on its own — aligning wwstage up to // cstage, whose checker stamps the binding u8. Kind-gated so // str's own type stays nominal. if (sk == nkind.N_TNAME) { if (streq(slctn.str, "str")) { let sti: *tinfo = slctn.type_: *tinfo; if (sti != nil) { if (sti.sub != nil) { let u8n: *node = newnode(nkind.N_TNAME, slctn.file, slctn.line, slctn.col); u8n.str = "u8"; u8n.type_ = sti.sub: *void; elemt = u8n; }; }; }; }; }; // 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.lhs); 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; // tp walks the N_TPARAM wrapper chain; tpt is the // actual element type AST. let tpt: *node = nil; if (tp != nil) { tpt = tp.lhs; }; if (tpt != nil) { fsz = paramfieldsize(tpt); signf = paramissigned(c, tpt); }; 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, tpt); } else { bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tpt); }; 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(c, 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"); }; // #138 (range form): `continue` must run the implicit `i+=1` // post-step before re-testing the bound. Pre-fix cont = loopl // (top), skipping the ADDQ $1, ioff below — infinite loop on // the value that triggered continue. Dedicated `rpost` label. let rpost: str = mklabel(c, "rpost"); c.loopcontbuf[c.looptop] = rpost; 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. Signedness comes from bind_signed // (set via paramissigned → fieldissignedc), so enum-aliased narrows // pick the right MOVS*Q without a literal-name gate. let b: i32 = 0; for (b < nbinds) { let op: str = loadopsz(bind_signed[b], bind_sz[b]); 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; emitlabel(rpost); 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; };