package wcc; import os; import syntax; import strconv; fn cgstmt(c: *cgen, n: *syntax.node) void = { if (n == nil) { return; }; let k: syntax.nkind = n.kind; if (k == syntax.nkind.N_BLOCK) { cgblock(c, n); return; }; if (k == syntax.nkind.N_RETURN) { cgreturn(c, n); return; }; if (k == syntax.nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; }; if (k == syntax.nkind.N_LET) { cglet(c, n); return; }; if (k == syntax.nkind.N_IF) { cgif(c, n); return; }; if (k == syntax.nkind.N_FOR) { cgfor(c, n); return; }; if (k == syntax.nkind.N_FORRANGE) { cgforrange(c, n); return; }; if (k == syntax.nkind.N_SWITCH) { cgswitch(c, n); return; }; if (k == syntax.nkind.N_MASSIGN) { cgmassign(c, n); return; }; if (k == syntax.nkind.N_MLET) { cgmlet(c, n); return; }; if (k == syntax.nkind.N_BREAK) { cgbreak(c, n); return; }; if (k == syntax.nkind.N_CONTINUE) { cgcontinue(c, n); return; }; if (k == syntax.nkind.N_YIELD) { cgyield(c, n); return; }; if (k == syntax.nkind.N_DEFER) { // #40: at the cap, fail loud in BOTH stages rather than // silently drop the deferred call. cstage's DEFER_MAX was 32 // and also dropped silently past it; the runtime-correct target // is a hard stop at the shared cap (cgen.c twin fatals too). if (c.defertop >= DEFER_MAX) { let msg: str = "cgen: too many defers in one function\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; c.deferbuf[c.defertop] = n.lhs; c.defertop += 1; return; }; c.lastwasreturn = 0; }; fn cgyield(c: *cgen, n: *syntax.node) void = { // 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: *syntax.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: *syntax.node = n.list; for (s != nil) { cgstmt(c, s); s = s.next; }; c.locals = saved; return; }; 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"; }; // tupeslot — THE tuple element-stride accessor (#22): the slot a tuple // element occupies, in bytes. slot = roundup8(size(elem)), 8B a FLOOR // not a ceiling (user-ratified 2026-06-04): str/slice carry their 24B // header, a tagged element its full tag+payload box ((str,str)=48B // predates this; tagged was the one truncated >8B kind — the #237 // fieldslotsize precedent), narrow scalars pad UP to one 8B eightbyte. // Every tuple walk (cursor send/receive, t.N read, destructure, sret // classify, DATA emit) takes its stride and its eightbyte count // (eslot/8) from here — the per-site wide=(STR||SLICE)-else-8 // predicates this absorbs were the #22 neighbor-slot/zeros miscompile. // Checker twin: check.ww tupleelemslot / check.c N_TTUPLE; cstage twin: // tuple_eslot (cmd/w6c/cgen.c). fn tupeslot(ti: *syntax.tinfo) i32 = { let t: *syntax.tinfo = ti; t = tichase(t); if (t == nil) { return 8; }; if (t.kind == syntax.tykind.TY_VOID) { return 0; }; // a literal tuple's stamped element can be untyped_str (size 0) — // it occupies the str header slot (the C-t2 type_isstr lesson). if (t.kind == syntax.tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; if (t.kind == syntax.tykind.TY_STR || t.kind == syntax.tykind.TY_SLICE || t.kind == syntax.tykind.TY_TAGGED) { return ((t.size + 7u64) & ~7u64): i32; }; return 8; }; fn tupeslotn(n: *syntax.node) i32 = { if (n == nil) { return 8; }; return tupeslot(n.type_: *syntax.tinfo); }; // 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: *syntax.node) *syntax.node = { if (rhs == nil) { return nil; }; if (rhs.kind != syntax.nkind.N_CALL) { return nil; }; let callee: *syntax.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 == syntax.nkind.N_IDENT) { cnm = callee.str; cmod = c.curmod; }; if (callee.kind == syntax.nkind.N_DOT) { cnm = callee.str; if (callee.lhs != nil) { if (callee.lhs.kind == syntax.nkind.N_IDENT) { cmod = callee.lhs.str; }; }; }; if (cnm.len == 0) { return nil; }; let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod); if (rtyp == nil) { return nil; }; // #99 alias transparency: a NAMED tuple alias return (`fn f() // pair`) destructures like its base — peel to the N_TTUPLE (the // nodetuplearg alias-peel; without it the element walk saw no // tuple and the str len/cap stores were dropped). for (rtyp != nil && rtyp.kind == syntax.nkind.N_TNAME) { rtyp = aliaslookup(c, rtyp.str); }; if (rtyp == nil) { return nil; }; if (rtyp.kind != syntax.nkind.N_TTUPLE) { return nil; }; return rtyp; }; // nodetuplearg — the tuple node of a call ARG whose cgexpr fills the // return-ABI cursor (#163/#32, C-t2): an N_CALL or `?`/`!` unwrap (the // declared return / success variant via inferletcalltype), an N_IDENT // local (declared tnode, alias-peeled), or an N_TUPLE literal — returned // AS-IS, kind-discriminated at the walks (its elements are VALUE exprs, // classified the way cgtuplelittocursor classifies them, not type // nodes). Mirror of cstage node_tuplearg; the cgcall push site // loud-stops any other tuple-typed source shape (rule 7). rettupleof // stays N_CALL-scoped for the destructure/reassign receive sites. fn nodetuplearg(c: *cgen, a: *syntax.node) *syntax.node = { if (a == nil) { return nil; }; if (a.kind == syntax.nkind.N_TUPLE) { return a; }; if (a.kind == syntax.nkind.N_IDENT) { let lc: *local = localfindnode(c, a.str); if (lc == nil) { return nil; }; let tn: *syntax.node = lc.tnode; for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { tn = aliaslookup(c, tn.str); }; if (tn != nil) { if (tn.kind == syntax.nkind.N_TTUPLE) { return tn; }; }; return nil; }; let t: *syntax.node = inferletcalltype(c, a); if (t != nil) { if (t.kind == syntax.nkind.N_TTUPLE) { return t; }; }; return nil; }; // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A >8B element (slice/str 3-word // {ptr,len,cap} header, ref/hare/rt/ensure.ha:4-8; tagged tag+payload // box, #22) stores its eslot/8 words 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, eslot: i32, tn: *syntax.node) void = { if (eslot == 0) { return; }; // void element: the checker's 0-slot if (eslot > 8) { let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitline(tupreg(gpcur + k)); emitline(", "); emitoff((off + k * 8): i64); emitline("(BP)\n"); k += 1; }; 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 = syntax.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"); }; // tuplitgpwords — INTEGER cursor words an N_TUPLE literal element // occupies. MUST mirror the literal push arms (tuplitpushelem) exactly // — the count drives the POP fill, so a count/push skew silently // shifts every later element (#22 class). A float rides the SSE row // (0 GP words); str/slice push their 3-word header; a tagged element // its tupeslot/8 box words; a void element pushes nothing (the // checker's 0-slot); a scalar 1. Mirror of cstage tuple_lit_gpwords. // // #57: `dtn` is the DECLARED tuple element TYPE node (nil when the // consumer has none). The N_TUPLE literal's stamped type is // CONSTRUCTED from its elements, so a concrete rvalue under a // declared-TAGGED slot counted ONE word here while the receive walks // the declared eslot — the cursor shifted and every later element // read garbage. Declared-tagged keys the count on the DECLARED box. fn tuplitgpwords(c: *cgen, e: *syntax.node, dtn: *syntax.node) i32 = { if (dtn != nil) { if (istaggedtype(c, dtn)) { return tupeslotn(dtn) / 8; }; }; if (isfloattype(c, e)) { return 0; }; if (nodeisstr(c, e) || nodeisslice(c, e)) { return (tyslicesize() / 8i64): i32; }; let t: *syntax.tinfo = e.type_: *syntax.tinfo; t = tichase(t); if (t != nil && (t.kind == syntax.tykind.TY_TAGGED || t.kind == syntax.tykind.TY_VOID)) { return tupeslotn(e) / 8; }; return 1; }; // tuplitpushelem — evaluate one N_TUPLE literal element and push its // INTEGER cursor words L->R (the pop side fills tupreg in reverse). A // tagged element loads its box words straight from its local slot — // cgexpr's ident load is word0-only for tagged (every tagged consumer // reads memory), so the cursor fill must too. Mirror of cstage // tuple_lit_push_elem — count (tuplitgpwords) and push live or die // together. // // #57: a DECLARED-tagged element whose expr is a concrete rvalue // (`return (5: size, 9)` — cast, literal, call) skipped the widen // entirely: the stamped-keyed arm below saw a scalar and pushed ONE // word, the receiver read the declared box words — silent shift, both // stages, gate-blind (ken /tmp/ken57). Such an element now widens // into the shared tagged scratch (cgwidentaggedstore, the cgreturn // tagged-@retscr shape) and pushes the box words. A tagged->tagged // SUBSET element (eslot mismatch) needs a tag remap on the way into // the slot — loud (rule 7, the #23/#40 widening family). fn tuplitpushelem(c: *cgen, e: *syntax.node, dtn: *syntax.node) void = { let t: *syntax.tinfo = e.type_: *syntax.tinfo; t = tichase(t); let etagged: bool = false; if (t != nil) { if (t.kind == syntax.tykind.TY_TAGGED) { etagged = true; }; }; if (dtn != nil) { if (istaggedtype(c, dtn) && !etagged) { let eslot: i32 = tupeslotn(dtn); let scr: i32 = tagscradd(c, eslot); emitline("\tXORQ\tAX, AX\n"); let z: i32 = 0; for (z < eslot) { emitline("\tMOVQ\tAX, "); emitoff((scr + z): i64); emitline("(BP)\n"); z += 8; }; cgwidentaggedstore(c, dtn.type_: *syntax.tinfo, e, "BP", scr, eslot); let pk: i32 = 0; for (pk < eslot / 8) { emitline("\tMOVQ\t"); emitoff((scr + pk * 8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); pk += 1; }; return; }; if (istaggedtype(c, dtn) && etagged && tupeslotn(dtn) != tupeslotn(e)) { let m57: str = "#57: tagged tuple element widening into a wider declared union slot needs a tag remap (rule 7; the #23/#40 widening family)\n"; os.write(2, m57.ptr, m57.len: u64); os.exit(1); }; }; if (etagged) { let eslot: i32 = tupeslotn(e); let eoff: i32 = 0; if (e.kind == syntax.nkind.N_IDENT) { eoff = localfind(c, e.str); }; if (eoff == 0) { let m22: str = "#22a: tagged tuple element from a non-local source shape unwired (ident locals only; rule 7; call-source is task #41, widening #23, deref/cast #35)\n"; os.write(2, m22.ptr, m22.len: u64); os.exit(1); }; let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((eoff + k * 8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); k += 1; }; return; }; cgexpr(c, e); if (t != nil && t.kind == syntax.tykind.TY_VOID) { return; }; emitline("\tPUSHQ\tAX\n"); if (nodeisstr(c, e) || nodeisslice(c, e)) { emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tCX\n"); }; }; // cgtuplelittocursor — #241: materialise an N_TUPLE literal's elements into // the SysV register-return cursor (integer words L->R over tupreg AX/DX/CX/ // R8, floats over tupsse X0/X1, a slice/str's {ptr,len,cap} over three // consecutive INTEGER regs) — the SAME ABI a tuple-returning call leaves, // which every tuple consumer (tupstore at cgmlet/cgmassign) reads. cgexpr // otherwise falls to its `MOVQ $0, AX` default for a tuple, so a literal // rvalue tuple bound or destructured read garbage past word0. Byte-identical // extraction of cgreturn's in-register N_TUPLE arm (cgenstmt.ww), now shared // with cgexpr. Over-cap loud-stops (rule 7); a bare expression value can't // sret, so the >cap rvalue-tuple materialisation is the #10 follow-up. // // #57: `decl` is the consumer's DECLARED tuple TYPE node (N_TTUPLE, // nil when it has none — the bare cgexpr route). A declared-TAGGED // element gates the SSE row off (its payload may be float-stamped but // the BOX rides INTEGER eightbytes) and keys count + push on the // declared eslot — see tuplitgpwords / tuplitpushelem. Mirrors cstage // cg_tuple_lit_to_cursor's decl walk; decl.list nodes wrap the elem // type in .lhs (the c.fnret.list shape the over-cap arm walks). fn cgtuplelittocursor(c: *cgen, tuple: *syntax.node, decl: *syntax.node) void = { let dp0: *syntax.node = nil; if (decl != nil) { if (decl.kind == syntax.nkind.N_TTUPLE) { dp0 = decl.list; }; }; let ssecap: i32 = TUPLE_SSECAP; let gptotal: i32 = 0; let ssecount: i32 = 0; let dp: *syntax.node = dp0; let e: *syntax.node = tuple.list; for (e != nil) { let dtn: *syntax.node = nil; if (dp != nil) { dtn = dp.lhs; }; let dtagged: bool = false; if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; if (!dtagged && isfloattype(c, e)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tuplitgpwords(c, e, dtn); }; if (dp != nil) { dp = dp.next; }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { let msg: str = "tuple literal exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\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; dp = dp0; e = tuple.list; for (e != nil) { let dtn: *syntax.node = nil; if (dp != nil) { dtn = dp.lhs; }; let dtagged: bool = false; if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; let isflt: bool = !dtagged && isfloattype(c, e); if (isflt) { cgexpr(c, e); 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 { tuplitpushelem(c, e, dtn); }; if (dp != nil) { dp = dp.next; }; 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; dp = dp0; e = tuple.list; for (e != nil) { let dtn: *syntax.node = nil; if (dp != nil) { dtn = dp.lhs; }; let dtagged: bool = false; if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; if (!dtagged && 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; }; if (dp != nil) { dp = dp.next; }; e = e.next; }; }; // cgtupleslottocursor — #241: load a tuple already materialised in a BP- // relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound union // payload) into the SAME register cursor. The slot uses the register-ABI // stride the tuple-init / #242 destructure write (a scalar 8B, a slice/str // its 3-word header), NOT the packed t.N field layout (#238). All sources // are memory, so each word loads straight into its cursor reg. So `yield t` // / `return t` / `let q = t` over a tuple ident leave the whole tuple in the // cursor, not just word0 in AX. Over-cap loud-stops (rule 7; #10). Mirror of // cstage cg_tuple_slot_to_cursor. fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *syntax.tinfo) void = { let gptotal: i32 = 0; let ssecount: i32 = 0; let el: *syntax.ttupleelem = tu.tupleelems; for (el != nil) { let et: *syntax.tinfo = el.type_; et = tichase(et); if (et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tupeslot(el.type_) / 8; }; el = el.tnext; }; if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) { let msg: str = "tuple ident exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let gp: i32 = 0; let sse: i32 = 0; let foff: i32 = 0; el = tu.tupleelems; for (el != nil) { let et: *syntax.tinfo = el.type_; et = tichase(et); let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64); let eslot: i32 = tupeslot(el.type_); if (isflt) { let mov: str = "MOVSD"; if (et.kind == syntax.tykind.TY_F32) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((srcoff + foff): i64); emitline("(BP), "); emitline(tupsse(sse)); emitline("\n"); sse = sse + 1; foff += 8; } else { let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k * 8): i64); emitline("(BP), "); emitline(tupreg(gp + k)); emitline("\n"); k += 1; }; gp += eslot / 8; foff += eslot; }; el = el.tnext; }; }; // cgtaggedtuplepayloadshift — #241: a `?`-unwrapped tuple payload is an // rvalue tuple that must fill the register cursor. The tagged return leaves // AX=tag, DX=word0, CX=word1, R8=word2; the scalar/str unwrap lifts only // word0->AX, stranding word1+ in CX/R8. Shift the whole payload DOWN one // INTEGER reg so element i lands in tupreg(i). Float/slice/str payload // elements ride a different SysV class — loud-stop (rule 7; the per- // eightbyte tagged-tuple-payload classification is the #243 follow-up). // Mirror of cstage cg_tagged_tuple_payload_shift. fn cgtaggedtuplepayloadshift(c: *cgen, tup: *syntax.tinfo) void = { let words: i32 = 0; let el: *syntax.ttupleelem = tup.tupleelems; for (el != nil) { let et: *syntax.tinfo = el.type_; et = tichase(et); let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64); if (isflt || tupeslot(el.type_) != 8) { let msg: str = "tuple-in-union ? unwrap: float/slice/str/tagged payload element needs SysV per-eightbyte classification (see #243); only integer tuple payloads supported\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; words = words + 1; el = el.tnext; }; if (words > 3) { let msg: str = "tuple-in-union ? unwrap payload exceeds the 3 integer return regs past the tag; see #10/#243\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let i: i32 = 0; for (i < words) { emitline("\tMOVQ\t"); emitline(tupreg(i + 1)); emitline(", "); emitline(tupreg(i)); emitline("\n"); i = i + 1; }; }; fn cgreturn(c: *cgen, n: *syntax.node) void = { rundefers(c); let rhs: *syntax.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. // #242: a bare tuple return packs into the register cursor; a // tuple WRAPPED IN A TAGGED UNION must instead pack into the // union payload (tag + words) — fall through to the tagged path // below, which routes it via cgwidentaggedstore. Without this // guard the bare-tuple arm fired first and dropped the tag, // returning (AX=word0, DX=word1) with no tag word. if (rhs.kind == syntax.nkind.N_TUPLE && !istaggedtype(c, c.fnret)) { let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; // #57: count + push key on the DECLARED return-type // element (c.fnret.list) — the literal's stamped type // is element-constructed, so a declared-TAGGED // element's concrete rvalue counted 1 word and skipped // the widen while the caller's receive walks the // declared eslot (2 words sent for a 3-word shape; // ken /tmp/ken57 p8/p9). Same pt walk the over-cap arm // already does (#240/#22b). Mirrors cstage cgreturn. let rp0: *syntax.node = nil; if (c.fnret != nil) { if (c.fnret.kind == syntax.nkind.N_TTUPLE) { rp0 = c.fnret.list; }; }; let rp: *syntax.node = rp0; let e: *syntax.node = rhs.list; for (e != nil) { let rdtn: *syntax.node = nil; if (rp != nil) { rdtn = rp.lhs; }; let rdtag: bool = false; if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; if (!rdtag && isfloattype(c, e)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tuplitgpwords(c, e, rdtn); }; if (rp != nil) { rp = rp.next; }; e = e.next; }; // #22b: classify and emit MUST agree (the #10 SSoT note // at TUPLE_GPCAP). The over-cap DECISION rides // sretretsize on the DECLARED return type — the same // predicate the prologue (@sretarg) and the caller key // on. The expr-shape count above only pairs the in-cap // push/pop: a declared-tagged element whose expr is the // unwidened payload counts 1 word here vs 2+ declared // eightbytes, so the emit took the register path against // an sret-classified caller — silent garbage, both // stages, gate-blind (probe /tmp/i22b/p2). let overcap: bool = gptotal > TUPLE_GPCAP || ssecount > ssecap; if (c.fnret != nil) { overcap = sretretsize(c, c.fnret) > 0; }; if (overcap) { // #10 Fold A: over-cap tuple returns via sret. The // prologue wired @sretarg (sretretsize agrees on the // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), // holding the caller-prealloc dest. Store each element // through *(@sretarg) // at its packed layout offset (running sum of element // sizes from the return-type tuple node — the t.0/t.1 // positional layout), each at its natural width so a // narrow tail doesn't over-MOVQ (#169); the dest base is // reloaded into DX each step since a wide element's // cgexpr clobbers AX/BX/CX. Then reuse the struct-sret // epilogue. The CALL/receive side stays loud-stopped // (#10 Fold B). Byte-identical to cstage cgen.c // N_RETURN over-cap tuple arm. let saoff: i32 = localfind(c, "@sretarg"); let pt: *syntax.node = nil; if (c.fnret != nil) { pt = c.fnret.list; }; let we: *syntax.node = rhs.list; let foff: i32 = 0; for (we != nil) { let dt: *syntax.tinfo = nil; if (pt != nil) { dt = pt.lhs.type_: *syntax.tinfo; }; dt = tichase(dt); if (dt != nil && dt.kind == syntax.tykind.TY_TAGGED) { // #22b (task #28): MEMORY-class tagged // element — the whole box copies through // the sret pointer mem-to-mem from the // element's local slot. cgexpr can't // source it: the tagged ident load is // word0-only (every tagged consumer // reads memory) and the AX/DX/CX/R8 box // cursor would collide with the DX // dest-base reload. Ident-only, // mirroring tuplitpushelem; widening / // non-ident sources stay loud (#23/#40 // follow-ups). Mirror of cstage cgen.c // N_RETURN over-cap tagged arm. let eslot: i32 = tupeslotn(pt.lhs); let eu: *syntax.tinfo = we.type_: *syntax.tinfo; eu = tichase(eu); let eoff: i32 = 0; if (we.kind == syntax.nkind.N_IDENT && eu != nil) { if (eu.kind == syntax.tykind.TY_TAGGED && tupeslotn(we) == eslot) { eoff = localfind(c, we.str); }; }; if (eoff == 0) { let m22b: str = "#22b: tagged element in an over-cap (sret) tuple return from a non-ident or widening source unwired (ident locals only; rule 7; call-source is task #41, widening #23/#40)\n"; os.write(2, m22b.ptr, m22b.len: u64); os.exit(1); }; emitline("\tMOVQ\t"); emitoff(saoff: i64); emitline("(BP), DX\n"); let bk: i32 = 0; for (bk < eslot) { emitline("\tMOVQ\t"); emitoff((eoff + bk): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((foff + bk): i64, "DX"); emitline("\n"); bk += 8; }; foff += eslot; we = we.next; if (pt != nil) { pt = pt.next; }; continue; }; let isflt: bool = isfloattype(c, we); let wide: bool = nodeisstr(c, we) || nodeisslice(c, we); let esz: i32 = 8; if (pt != nil) { let eti: *syntax.tinfo = pt.lhs.type_: *syntax.tinfo; if (eti != nil) { esz = eti.size: i32; }; }; cgexpr(c, we); emitline("\tMOVQ\t"); emitoff(saoff: i64); emitline("(BP), DX\n"); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, we)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(foff: i64, "DX"); emitline("\n"); } else { if (wide) { emitline("\tMOVQ\tAX, "); emitdispreg(foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((foff + 16): i64, "DX"); emitline("\n"); } else { let sop: str = tnodestoreop(c, we, esz); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(foff: i64, "DX"); emitline("\n"); }; }; // C-t0/#22: the sret buffer is slot-laid like // every tuple home (checker size, t.N // reader, mlet receive agree) — the stride // is THE accessor's (a declared void // element's 0-slot included; the old // wide?esz:8 advanced 8 where every receive // walks 0). esz keeps the store WIDTH // natural. Mirrors cstage cgen.c N_RETURN // over-cap arm. if (pt != nil) { foff += tupeslotn(pt.lhs); } else { foff += tupeslotn(we); }; we = we.next; if (pt != nil) { pt = pt.next; }; }; emitline("\tMOVQ\t"); emitoff(saoff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; // rule-7 net: register-classified by the declared type // but the expr-shape count overflows the cursor — the // pops below would index past tupreg. Unreachable while // expr counts never exceed declared counts; loud, not // OOB, if a future shape breaks that. Mirrors cstage. if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { let mskew: str = "register-classified tuple return exceeds the cursor (classify/emit skew; rule 7, #22b)\n"; os.write(2, mskew.ptr, mskew.len: u64); os.exit(1); }; let fscr: i32 = 0; if (ssecount > 0) { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; rp = rp0; e = rhs.list; for (e != nil) { let rdtn: *syntax.node = nil; if (rp != nil) { rdtn = rp.lhs; }; let rdtag: bool = false; if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; let isflt: bool = !rdtag && isfloattype(c, e); if (isflt) { cgexpr(c, e); 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 { // scalar=AX; slice/str=AX,BX,CX; tagged // box from its slot or widened scratch // (tuplitpushelem) tuplitpushelem(c, e, rdtn); }; if (rp != nil) { rp = rp.next; }; 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; rp = rp0; e = rhs.list; for (e != nil) { let rdtn: *syntax.node = nil; if (rp != nil) { rdtn = rp.lhs; }; let rdtag: bool = false; if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; if (!rdtag && 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; }; if (rp != nil) { rp = rp.next; }; 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, R8=v2) — no shuffle, no // tag synthesis. Mirrors cstage cgen.c:8007 passthrough // = istagged && (vu == rt || type_eq(vt, cg_ret_type)). // TYPE-BASED predicate (was name-keyed via fnretlookupmod // IDENT/DOT-only) covers all callee shapes — including // deref-call N_UN(TK_STAR) per #201. Identity-on-peeled // handles the NAMED case (tinfocache memoizes per typedecl, // #191 lineage); the variant-pointer fallback handles the // anonymous case (each anonymous `(A|B)` decl gets its own // NAMED-less tinfo, so identity fails — e.g. cross-module // strings.byteindex returns the same anonymous (i32|void) // as bytes.index). Variant-pointer equality on the params // chain suffices because variants are primitives (single // tctx tinfo) or NAMED (per-decl identity); a full recursive // tinfo structural-eq helper is gated by #178. // #261: N_INDEX of a tagged element (`return x.o[i]`) and // N_DOT of a tagged field both materialize the full tagged // ABI shape via cgexpr (cgindex slot-copy / cgdot field-load, // AX=tag/DX=v0/...), exactly like an N_CALL of a tagged- // returning fn — so a same-type return forwards them // unchanged. cstage gates passthrough purely on the rhs type // (no kind filter, cgen.c:8845); without these kinds an // N_INDEX tagged-element return fell to the scalar-variant // shuffle (MOVQ AX,DX; MOVQ $0,AX), dropping the payload. let forwardtagged: bool = false; if ((rhs.kind == syntax.nkind.N_CALL || rhs.kind == syntax.nkind.N_INDEX || rhs.kind == syntax.nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { let ru: *syntax.tinfo = rhs.type_: *syntax.tinfo; ru = tichase(ru); let fu: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; fu = tichase(fu); if (ru != nil && fu != nil && ru.kind == syntax.tykind.TY_TAGGED && fu.kind == syntax.tykind.TY_TAGGED) { if (ru == fu) { forwardtagged = true; } else if (ru.nullable == fu.nullable) { let pa: *syntax.tparam = ru.params; let pb: *syntax.tparam = fu.params; let same: bool = true; for (pa != nil && pb != nil) { if (pa.type_ != pb.type_) { same = false; }; pa = pa.tnext; pb = pb.tnext; }; if (same && pa == nil && pb == nil) { forwardtagged = true; }; }; }; }; // #38b: sret-classified tagged return (slot > the // AX/DX/CX/R8 cursor) — write through *(@sretarg) and // return the dest pointer. Three shapes mirror cstage // cgen.c N_RETURN #38b: exact-type N_CALL forward // (c.sretforward), widening from a >32B tagged source // (#40 loud-stop), everything else through // cgwidentaggedstore's non-BP base. if (sretretsize(c, c.fnret) > 0) { let sa38v: i32 = localfind(c, "@sretarg"); if (forwardtagged && rhs.kind == syntax.nkind.N_CALL) { // exact-type N_CALL forward: inner sret's // into outer's dest; an N_INDEX/N_DOT // source routes through the widener's // #37 mem-read arm below instead. c.sretforward = 1; cgexpr(c, rhs); emitline("\tMOVQ\t"); emitoff(sa38v: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; let ru38: *syntax.tinfo = rhs.type_: *syntax.tinfo; ru38 = tichase(ru38); if (ru38 != nil) { // #37 wired the N_INDEX/N_DOT mem-read into // the widener; the remaining >32B kinds stay // loud. if (ru38.kind == syntax.tykind.TY_TAGGED && rhs.kind != syntax.nkind.N_IDENT && ru38.size: i32 > TUPLE_GPCAP * 8 && !taggedmemread(c, rhs)) { let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; os.write(2, m38e.ptr, m38e.len: u64); os.exit(1); }; }; emitline("\tMOVQ\t"); emitoff(sa38v: i64); emitline("(BP), BX\n"); cgwidentaggedstore(c, c.fnret.type_: *syntax.tinfo, rhs, "BX", 0, slotsize(c, c.fnret)); emitline("\tMOVQ\t"); emitoff(sa38v: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; // 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; }; // #242: a tuple variant packs into the union // payload via cgwidentaggedstore's TY_TUPLE arm. if (rhs.kind == syntax.nkind.N_TUPLE) { needswiden = true; }; // Family C (#35/#46): a mem-based tagged // read (`return *p`, any size) routes // through the widener's memread arm — // the cgexpr fall-through below wrapped // the un-deref'd POINTER as a scalar // payload (silent wrong). Mirrors // cstage cgreturn's widen-store route. if (taggedmemread(c, rhs)) { needswiden = true; }; // S1/#35: a GENUINE-WIDENING tagged source // (stamped type_ TY_TAGGED and != fnret; // exact-type rides forwardtagged/plain above) // routes through the widener — tag remap for // ident/call (#218), #35 widen-subset loud for // cast/dot. Mirrors cstage cgreturn istagged→ // cg_widen_tagged_store (cmd/w6c/cgen.c:13008- // 13011 → :2721). The ru1 != fu1 exclusion keeps // EXACT-type tagged casts on cstage's passthrough // (forwardtagged's own ru==fu equality) so byte-id // holds. let ru1: *syntax.tinfo = rhs.type_: *syntax.tinfo; ru1 = tichase(ru1); let fu1: *syntax.tinfo = nil; if (c.fnret != nil) { fu1 = c.fnret.type_: *syntax.tinfo; fu1 = tichase(fu1); }; if (ru1 != nil && fu1 != nil) { if (ru1.kind == syntax.tykind.TY_TAGGED && ru1 != fu1) { needswiden = true; }; // S3/#35: a SAME-TYPE tagged CAST (ru1 == fu1, // rhs an N_CAST) routes through the widener too. // cstage keeps the N_CAST out of the srcreg // passthrough (kind != N_CALL/INDEX/DOT) and the // widen branch peels the identity cast internally // (cg_widen_tagged_store cg_tagged_castpeel, // cgen.c:2553), so cs emits the scratch-widen (NOT // passthrough) for `return v: u` over an ident/ // call/dot source. ww's forwardtagged keys on // rhs.kind (N_CAST uncovered), so the same-type // cast fell to the scalar shuffle and synthesized // tag 0 (silent). The N_CAST guard mirrors cstage's // "N_CAST defeats srcreg"; peeling here instead // would reroute a call/dot source to passthrough // and break byte-id. if (rhs.kind == syntax.nkind.N_CAST && ru1.kind == syntax.tykind.TY_TAGGED && ru1 == fu1) { 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_: *syntax.tinfo, rhs, "BP", scroff, rsz); // Tagged-return ABI loads at most 4 eightbytes // (AX/DX/CX/R8). A union whose slot exceeds 32B // (tag + >3 payload words, e.g. a 32B struct // variant = 40B slot) drops its 5th+ word here — // SYMMETRICALLY with cstage, so byte-id holds and // the tag/early-word read paths are correct. The // dropped tail is #222 (the >4-eightbyte sret ABI // asymmetry); its real fix routes large unions // through a hidden-pointer sret on both paths. // Sound only while consumers never read the tail // (errno's tag/strerror path does not). 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: *syntax.tinfo = rhs.type_: *syntax.tinfo; if (syntax.typeisf32(rety)) { rfk = 1; } else { if (syntax.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"); }; };};}; if (idx < 0) { let mtag: str = "tagged return: no variant tag for return value (rule 7)\n"; os.write(2, mtag.ptr, mtag.len: u64); os.exit(1); }; 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; }; // 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 == syntax.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; // #272: >24B sret addressable-source closure — // N_DOT/N_INDEX/deref land their address in SI then // memcpy through *(@sretarg), mirroring cstage cgen.c // N_RETURN sret arm. N_ARRLIT >24B has no consumer // (loud-stops in cstage); not wired here. let addrsrc: bool = false; if (rhs.kind == syntax.nkind.N_IDENT) { okrhs = true; }; if (rhs.kind == syntax.nkind.N_STRUCTLIT) { okrhs = true; }; if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; }; if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; }; if (rhs.kind == syntax.nkind.N_UN) { if (rhs.op == syntax.tkind.TK_STAR) { okrhs = true; addrsrc = true; }; }; if (okrhs) { if (rhs.kind == syntax.nkind.N_STRUCTLIT) { // #63: the >24B sret RETURN twin of the :2421 // let-init fix. sretretsize chases the alias for // the size GATE (so this sret arm fires for a >24B // alias struct), but the field-fill resolved the // struct by a bare structlookup(c, sname): for an // alias-NAMED literal (`type biga = big; return // biga{...}`) sname is "biga", unregistered, so // sret_si was nil and the fill was SKIPPED — the // callee returned an uninitialised sret buffer // (SILENT wrong, runtime-0). structlookupchain chases // to the base struct; cs fills via the resolved // Type*, runtime-correct. let trefn: *syntax.node = rhs.lhs; let sret_si: *structinfo = structlookupchain(c, trefn); 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 { if (addrsrc) { if (!aggargsrcaddr(c, rhs, "SI")) { let m4: str = "#272: aggregate return from unsupported source kind\n"; os.write(2, m4.ptr, m4.len: u64); os.exit(1); }; emitline("\tMOVQ\t"); emitoff(sretargoff: i64); emitline("(BP), BX\n"); let k: i32 = 0; for (k + 8 <= scs) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 8; }; for (k + 4 <= scs) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 4; }; for (k < scs) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 1; }; } 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 == syntax.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; // #272: struct ≤24B addressable-source closure — // N_DOT/N_INDEX/deref memcpy into @retscr before the // shared structfloatclass tail (mirror cstage cgen.c). let addrsrc: bool = false; if (rhs.kind == syntax.nkind.N_IDENT) { okrhs = true; // #41 (#263 ww-runtime-correct): a module-global struct // source has no BP slot — route it through the addrsrc // memcpy (LEAQ g(SB),SI via aggargsrcaddr). Pre-fix the // rl==nil N_IDENT arm below emitted nothing → zeroed // @retscr. cstage copies frame garbage (cstage half #42). if (localfindnode(c, rhs.str) == nil) { addrsrc = true; }; }; if (rhs.kind == syntax.nkind.N_STRUCTLIT) { okrhs = true; }; if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; }; if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; }; if (rhs.kind == syntax.nkind.N_UN) { if (rhs.op == syntax.tkind.TK_STAR) { okrhs = true; addrsrc = 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 == syntax.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 { if (addrsrc) { // N_DOT / N_INDEX / deref: land src addr in SI, // then memcpy rsz bytes into @retscr (#265/#268 shape). if (!aggargsrcaddr(c, rhs, "SI")) { let m5: str = "#272: aggregate return from unsupported source kind\n"; os.write(2, m5.ptr, m5.len: u64); os.exit(1); }; let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= rsz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= rsz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= rsz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; } 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; }; }; }; }; // #267: array return-by-value SEND. >24B sret rides the sret // block above (scs = sretretsize keys it, N_IDENT word-copy / // N_CALL forward generic). ≤24B reg-class `return a;` (N_IDENT) // mirrors the struct ≤24B path: zero-pad a 24B scratch, word- // copy the array slot in, ship AX/DX/CX. Array natural size // (tinfo.size = sub.size*len) mirrors cstage rt->size. No // structfloatclass (pure-int arrays); N_CALL forward at reg- // class falls to the default cgexpr passthrough below. if (c.fnret != nil && c.fnret.kind == syntax.nkind.N_TARRAY) { // #272: array return-by-value source-shape closure. // Beyond the #267 N_IDENT word-copy, route N_ARRLIT // (literal fill), N_DOT/N_INDEX/deref (aggargsrcaddr + // memcpy) into @retscr — the mirror of cstage cgen.c // N_RETURN ≤24B arm. N_CALL stays on the cgexpr tail (the // callee already left AX/DX/CX). let arrok: bool = false; if (rhs.kind == syntax.nkind.N_IDENT) { arrok = true; }; if (rhs.kind == syntax.nkind.N_ARRLIT) { arrok = true; }; if (rhs.kind == syntax.nkind.N_DOT) { arrok = true; }; if (rhs.kind == syntax.nkind.N_INDEX) { arrok = true; }; if (rhs.kind == syntax.nkind.N_UN) { if (rhs.op == syntax.tkind.TK_STAR) { arrok = true; }; }; let ati: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; ati = tichase(ati); if (arrok && ati != nil) { let rsz: i32 = ati.size: i32; if (rsz <= 24) { 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 == syntax.nkind.N_IDENT) { let rl: *local = localfindnode(c, rhs.str); let roff: i32 = 0; if (rl != nil) { roff = rl.off; }; let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff((roff + 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((roff + 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((roff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; } else { if (rhs.kind == syntax.nkind.N_ARRLIT) { // scalar/float element fill; non-scalar // elements loud-stop (rule 7, no consumer). let esubti: *syntax.tinfo = nil; if (ati.sub != nil) { esubti = ati.sub; }; esubti = tichase(esubti); let esz: i32 = 8; if (esubti != nil) { esz = esubti.size: i32; }; let badel: bool = false; if (esubti != nil) { if (esubti.kind == syntax.tykind.TY_STRUCT) { badel = true; }; if (esubti.kind == syntax.tykind.TY_ARRAY) { badel = true; }; if (esubti.kind == syntax.tykind.TY_TUPLE) { badel = true; }; if (esubti.kind == syntax.tykind.TY_SLICE) { badel = true; }; if (esubti.kind == syntax.tykind.TY_STR) { badel = true; }; }; if (badel) { let m2: str = "#272: array-literal return with non-scalar element unsupported (rule 7, no consumer)\n"; os.write(2, m2.ptr, m2.len: u64); os.exit(1); }; let esub: *syntax.node = c.fnret.lhs; let isfl: bool = isfloattype(c, esub); let fmov: str = "MOVSD"; if (isf32type(c, esub)) { fmov = "MOVSS"; }; let op: str = "MOVQ"; if (esz == 1) { op = "MOVB"; } else { if (esz == 2) { op = "MOVW"; } else { if (esz == 4) { op = "MOVL"; }; }; }; let idx: i32 = 0; let repeat: bool = false; let e: *syntax.node = rhs.list; for (e != nil) { let isellip: bool = false; if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; isellip = true; }; }; if (isellip) { e = nil; } else { cgexpr(c, e); if (isfl) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); } else { emitline("\t"); emitline(op); emitline("\tAX, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); }; idx += 1; e = e.next; }; }; if (repeat) { let total: i32 = rsz / esz; for (idx < total) { if (isfl) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); } else { emitline("\t"); emitline(op); emitline("\tAX, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); }; idx += 1; }; }; } else { // N_DOT / N_INDEX / deref: land src addr in SI, // then memcpy rsz bytes into @retscr (#265/#268 // copy shape). Loud-stop unaddressable sources. if (!aggargsrcaddr(c, rhs, "SI")) { let m3: str = "#272: aggregate return from unsupported source kind\n"; os.write(2, m3.ptr, m3.len: u64); os.exit(1); }; let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= rsz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= rsz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= rsz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; }; }; 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; }; }; }; // #272 close-by-construction: addressable aggregate-return // sources (IDENT/STRUCTLIT/ARRLIT/DOT/INDEX/deref) all break in // the arms above; an aggregate N_CALL passes through cgexpr // (callee left AX/DX/CX). Any OTHER aggregate rvalue reaching // here would truncate to AX silently — loud-stop (rule 7), // mirroring cstage cgen.c N_RETURN. { // #277: key on the RESOLVED tinfo, not the syntactic node — a // NAMED-ALIAS aggregate return type (type a=[N]T / type a=struct) // presents as N_TNAME and is TY_ARRAY/TY_STRUCT only after the // alias chase, so the syntactic N_TARRAY/N_TNAME-structlookup arms // above never fire on it. Without this chase it would fall to the // scalar default = silent miscompile (cstage chases via // type_chase_named and stays correct). Loud-stop (rule 7) until // wwstage handles aliases via tinfo-kind dispatch (#277); the >24B // array-literal return (no consumer) also lands here (#276). let aggret: bool = false; if (c.fnret != nil) { let rti: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; rti = tichase(rti); if (rti != nil) { if (rti.kind == syntax.tykind.TY_ARRAY) { aggret = true; }; if (rti.kind == syntax.tykind.TY_STRUCT) { aggret = true; }; }; }; if (aggret && rhs.kind != syntax.nkind.N_CALL) { let m6: str = "#272/#276/#277: aggregate return reaches scalar default — unclosed shape (named-alias aggregate return or >24B array-literal; wwstage tinfo-dispatch deferred #277)\n"; os.write(2, m6.ptr, m6.len: u64); os.exit(1); }; }; 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)) { // #38b: an sret-classified tagged return (slot > the // AX/DX/CX/R8 cursor) writes the void-variant tag // through *(@sretarg) and returns the dest pointer — // the cursor can't carry the slot and the caller reads // memory. Mirrors cstage cgen.c N_RETURN bare arm. if (sretretsize(c, c.fnret) > 0) { let sa38: i32 = localfind(c, "@sretarg"); let vidx38: i32 = voidvariantindex(c.fnret); if (vidx38 < 0) { let mvd: str = "tagged return: no void variant (rule 7)\n"; os.write(2, mvd.ptr, mvd.len: u64); os.exit(1); }; emitline("\tMOVQ\t"); emitoff(sa38: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t$"); emitint(vidx38: i64); emitline(", (BX)\n"); emitline("\tMOVQ\t"); emitoff(sa38: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; 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) { let mvd: str = "tagged return: no void variant (rule 7)\n"; os.write(2, mvd.ptr, mvd.len: u64); os.exit(1); }; 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: *syntax.node) void = { if (n.lhs != nil) { cgexpr(c, n.lhs); }; c.lastwasreturn = 0; return; }; // cgarrlitfillbp — #31: fill the [count]T destination at BP-relative // `off` from an N_ARRLIT, extracted from the cglet array-init path so // the slice-borrow base materialisation (cgslice N_ARRLIT-base arm) // reuses the IDENTICAL element-store sequence — the frame-order / // store-op guarantee for rule-10 byte-id (ken). `arrtn` is the [count]T // type NODE (cglet n.lhs; cgslice the re-stamped tnode on arrlit.lhs, // #25); `rhs` the literal. Twin of cstage cg_arrlit_fill_bp. fn cgarrlitfillbp(c: *cgen, arrtn: *syntax.node, rhs: *syntax.node, off: i32) void = { let elemn: *syntax.node = arrtn.lhs; // #79 (#60 rider): alias-NAMED [count]T (`type A = [4]u32; let // a: A = [...]`) — arrtn is the N_TNAME leaf: elemn nil, esz // stayed the 8 sentinel and the per-element store strode MOVQ // over a stride-4 slot (saved-BP/RIP smash; masked when esz==8). // This is the STORE half of the #8 pair (the elemsizeofc READ // half chases the ELEMENT via idxeffti; alias-typed INDEXABLES // are chased at its call sites). Synthesise the element node off // the chased stamped sub — the cgforrange FC0 precedent — so the // prim/agg/slice/tagged/narrow dispatch below works unchanged; // stash alen for the `...` repeat bound (cstage cg_arrlit_fill_bp // receives the pre-chased bu and reads bu->alen). let aliasalen: i32 = -1; let ati79: *syntax.tinfo = arrtn.type_: *syntax.tinfo; if (ati79 != nil) { if (ati79.kind == syntax.tykind.TY_NAMED) { let au79: *syntax.tinfo = tichase(ati79); if (au79 != nil) { if (au79.kind == syntax.tykind.TY_ARRAY && au79.sub != nil) { let en79: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, arrtn.file, arrtn.line, arrtn.col); en79.str = au79.sub.name; en79.type_ = au79.sub: *void; elemn = en79; aliasalen = au79.alen: i32; };}; };}; let esz: i32 = 8; let isstrel: bool = false; if (elemn != nil) { if (elemn.kind == syntax.nkind.N_TNAME) { if (syntax.streq(elemn.str, "str")) { esz = primtypesize("str"): i32; isstrel = true; } else { let ps: i32 = aliasprimsize(c, elemn.str); if (ps > 0) { esz = ps; }; }; }; }; // #270-1c: an AGGREGATE (struct/array/tuple) element of // an array literal — the scalar per-element store below // writes only the first 8 bytes (unpopulated tail). Fill // each element slot from its literal (cgstructlitfillbp) // or source ident (word-copy). esz is the element's // natural size (cstage esub->size). let esubti: *syntax.tinfo = nil; if (elemn != nil) { esubti = elemn.type_: *syntax.tinfo; }; esubti = tichase(esubti); let isagg: bool = esubti != nil && (esubti.kind == syntax.tykind.TY_STRUCT || esubti.kind == syntax.tykind.TY_ARRAY || esubti.kind == syntax.tykind.TY_TUPLE); if (isagg) { esz = esubti.size: i32; }; // #20/#270 str-slice arm: a slice element (N_TSLICE) is // a 24B {ptr,len,cap} header — it matches no prim/str/agg // branch above, so esz stayed the 8 sentinel (wrong stride, // the -96-vs-80 cs!=ww frame divergence) and the scalar // store dropped .len/.cap. Size it from the stamped tinfo // and route it through the 3-word header store below. let isslicel: bool = esubti != nil && esubti.kind == syntax.tykind.TY_SLICE; if (isslicel) { esz = esubti.size: i32; }; // #12: a tagged-union element. NOT folded into isagg — // isagg's body word-copies/fatals and never boxes the // tag+payload; route through the cgwidentaggedstore // choke-point the N_LET tagged path (cgenstmt.ww:1627) // uses. esz must come from the stamped slot size (#8-class // trap, rule-13): the narrow override below only rescues // 1/2/4, so a tagged 16/24B element keeps the wrong 8 // sentinel stride without this. let istaggedel: bool = esubti != nil && esubti.kind == syntax.tykind.TY_TAGGED; if (istaggedel) { esz = esubti.size: i32; }; // #8: a named-narrow element (`[N]tk`, tk = enum i32) is // neither a builtin prim (primsize=0 above, so esz stayed // the 8 sentinel) nor an aggregate, so the scalar store kept // an 8B stride/MOVQ and overran the stride-4 frame slot — // smashing the saved BP / return addr (SEGFAULT). Mirror // cstage's uniform lu->sub->size (cgen.c:6387) and the // elemsizeofc read-side fix: take the stamped element tinfo's // size for a narrow scalar (1/2/4). Wider non-prim elements // (tagged/slice/str two-half) stay the documented follow-up // at :1742-1744 — the single-MOVx store below is scalar-only. if (!isstrel && !isagg && esz == 8 && esubti != nil) { let es: i32 = esubti.size: i32; if (es == 1 || es == 2 || es == 4) { esz = es; }; }; 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: *syntax.node = rhs.list; for (e != nil) { let isellip: bool = false; if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; isellip = true; }; }; if (isellip) { e = nil; } else { if (isagg) { if (e.kind == syntax.nkind.N_STRUCTLIT) { let esi: *structinfo = structlookupchain(c, elemn); cgstructlitfillbp(c, esi, e, off + idx * esz); } else { if (e.kind == syntax.nkind.N_IDENT) { let sl: *local = localfindnode(c, e.str); let soff: i32 = 0; if (sl != nil) { soff = sl.off; }; let kc: i32 = 0; for (kc + 8 <= esz) { emitline("\tMOVQ\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 8; }; if (kc + 4 <= esz) { emitline("\tMOVL\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 4; }; if (kc + 2 <= esz) { emitline("\tMOVW\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 2; }; if (kc + 1 <= esz) { emitline("\tMOVB\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 1; }; } else { let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; os.write(2, m1c.ptr, m1c.len: u64); os.exit(1); }; }; } else { if (istaggedel) { cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); } else { cgexpr(c, e); if (isstrel || isslicel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((off + idx * esz + 16): 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; }; }; if (repeat && isagg) { let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; os.write(2, m1cr.ptr, m1cr.len: u64); os.exit(1); }; // #12: `...` re-stores from AX, but cgwidentaggedstore consumed // the node and trashed AX — the repeat-fill would write garbage. // No consumer needs `[N]tagged=[x,...]`. if (repeat && istaggedel) { let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; os.write(2, m12r.ptr, m12r.len: u64); os.exit(1); }; // 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 (arrtn != nil) { if (arrtn.kind == syntax.nkind.N_TARRAY) { if (arrtn.rhs != nil) { if (arrtn.rhs.kind == syntax.nkind.N_INTLIT) { total = arrtn.rhs.uval: i32; }; }; }; }; // #79: alias arrtn has no length tnode — bound off the // chased tinfo (see the synthesis block at fn top). if (aliasalen >= 0) { total = aliasalen; }; for (idx < total) { if (isstrel || isslicel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((off + idx * esz + 16): 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; }; }; }; // #152: reserve the let's frame slot, emit its initializer against the // PRE-binding locals chain, then link the binding. A self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in // c.locals while cgletbody runs (Hare evals the init in the outer scope: // harec check.c clet runs cexpr before scope_define). localreserve bumps // the frame now so off + nested-let offsets stay stable. fn cglet(c: *cgen, n: *syntax.node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); let tn: *syntax.node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; let letloc: *local = localreserve(c, nm, sz, tn); cgletbody(c, n, letloc.off); letloc.lnext = c.locals; c.locals = letloc; }; // #152: cgletbody emits the initializer into the reserved slot `off`. // The wrapper cglet reserves the slot BEFORE this runs and links the // binding into c.locals only AFTER, so a self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn cgletbody(c: *cgen, n: *syntax.node, off: i32) 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: *syntax.node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; if (n.rhs != nil) { let rhs: *syntax.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: *syntax.node = nil; let viatryunw: bool = false; let viatryprop: bool = false; if (rhs.kind == syntax.nkind.N_TRYUNW) { if (rhs.lhs != nil) { if (rhs.lhs.kind == syntax.nkind.N_CALL) { scall = rhs.lhs; viatryunw = true; }; }; } else { if (rhs.kind == syntax.nkind.N_TRYPROP) { if (rhs.lhs != nil) { if (rhs.lhs.kind == syntax.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 == syntax.nkind.N_TSLICE && sz == tyslicesize(): i32) { let callee: *syntax.node = scall.lhs; let a0: *syntax.node = scall.list; let a1: *syntax.node = nil; let a2: *syntax.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 == syntax.nkind.N_IDENT && syntax.streq(callee.str, "alloc") && a0.kind == syntax.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: *syntax.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(allocresolve(c)); 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: *syntax.tinfo = nil; if (c.fnret != nil) { nti = c.fnret.type_: *syntax.tinfo; }; nti = tichase(nti); if (nti != nil) { if (nti.kind == syntax.tykind.TY_TAGGED) { let np: *syntax.tparam = nti.params; let nidx2: i32 = 0; for (np != nil) { let nvt: *syntax.tinfo = np.type_; if (nvt != nil) { if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; }; }; np = np.tnext; nidx2 += 1; }; }; }; if (nidx < 0) { let mnm: str = "tryprop: no nomem variant in fn return union (rule 7)\n"; os.write(2, mnm.ptr, mnm.len: u64); os.exit(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. // // #38b: an sret-classified tagged CALL result is in memory, // not the cursor — an exact-type receive falls through to the // generic sret receive below (the let's slot IS the dest); a // widening receive needs mem-to-mem tag-remap (#40, unwired). // Mirrors cstage cgen.c N_LET tagged arm. if (istaggedtype(c, tn)) { let letsret: i32 = 0; if (rhs.kind == syntax.nkind.N_CALL) { letsret = callsretsize(c, rhs); }; if (letsret == 0) { cgwidentaggedstore(c, tn.type_: *syntax.tinfo, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; let lru: *syntax.tinfo = rhs.type_: *syntax.tinfo; lru = tichase(lru); let llu: *syntax.tinfo = tn.type_: *syntax.tinfo; llu = tichase(llu); let exact38: bool = false; if (lru != nil && lru == llu) { exact38 = true; } else { if (syntax.typeeq(rhs.type_: *syntax.tinfo, tn.type_: *syntax.tinfo)) { exact38 = true; }; }; if (!exact38) { let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; os.write(2, m40c.ptr, m40c.len: u64); os.exit(1); }; // fall through to the generic sret receive below. }; // In-cap tuple initialiser (#105 / #164/#107): every in-cap // tuple receive routes here, keyed on the DECLARED TYPE's // register classify (sretretsize == 0, the shared SSoT) — // mirror of cstage cgen.c N_LET tuple arm. 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). Over-cap falls through to // the sret receive below (#240 — an over-cap receive via the // register cursor read garbage past R8). // // C-t1 (#33): the old keys were producer-SHAPE — the mixed // str/scalar arm required s0_is_str != s1_is_str (syntactic) // AND sz==16/32, the rt16 arm required an N_CALL rhs // (rettupleof) — so a scalar-scalar tuple LITERAL `(3, 4)` // matched neither and fell to the generic single-word store, // silently dropping word 1 (#209/#211-class syntactic-vs-type // keying). Alias-peel mirrors cstage's type_chase_named; the // unannotated `let t = f()` shape rides the inferletcalltype // tn above. let ttup: *syntax.node = tn; for (ttup != nil && ttup.kind == syntax.nkind.N_TNAME) { ttup = aliaslookup(c, ttup.str); }; if (ttup != nil) { if (ttup.kind == syntax.nkind.N_TTUPLE && sretretsize(c, ttup) == 0) { // #57: a tuple LITERAL rhs carries the DECLARED // type into the cursor fill — its stamped type // is element-constructed, so a declared-tagged // element's concrete rvalue skipped the widen // and the fill/receive cursor walks skewed // (let-twin of the return-position bug; probe // /tmp/p57/q1_let). Same emission as the cgexpr // route for every declared-tagged-free literal. if (rhs.kind == syntax.nkind.N_TUPLE) { cgtuplelittocursor(c, rhs, ttup); } else { cgexpr(c, rhs); }; let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; let q: *syntax.node = ttup.list; for (q != nil) { let qt: *syntax.node = q.lhs; let isflt: bool = isfloattype(c, qt); let eslot: i32 = tupeslotn(qt); tupstore(c, gpcur, ssecur, off + eoff, eslot, qt); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + eslot / 8; }; eoff = eoff + eslot; q = q.next; }; c.lastwasreturn = 0; return; }; // #22a (rule 7, ken R1) wwstage half: an OVER-CAP tuple // init whose rhs is not a CALL has no store path — only // the CALL shape rides the sret receive below; every // other rhs fell past ALL the store arms to NOTHING // (silent uninitialized-frame reads). cgexpr's cursor // materialisers loud most shapes, but their EXPR-shape // counts let a declared-tagged element's unwidened // payload (or a void literal) slip through in-cap // (probe /tmp/i22b/p7) — the let-twin of the #22b // classify/emit skew. Mirrors cstage cgen.c N_LET net. if (ttup.kind == syntax.nkind.N_TTUPLE && rhs.kind != syntax.nkind.N_CALL && sretretsize(c, ttup) > 0) { cgexpr(c, rhs); let mnet: str = "over-cap tuple initialiser from a non-call source unwired (see #10/#22b)\n"; os.write(2, mnet.ptr, mnet.len: u64); os.exit(1); }; }; // 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/slice element (24B = ptr+len+cap, post-#1) needs all 3 // words stored. cgstrlit / cgident leave it as (AX=ptr, BX=len, // CX=cap) and a single MOVQ from AX would leave .len/.cap 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/slice 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. The str/slice element now stores all 3 // words; [N]tagged element arrays still hit the gap, task #12.) if (rhs.kind == syntax.nkind.N_ARRLIT) { cgarrlitfillbp(c, n.lhs, rhs, off); 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 == syntax.nkind.N_STRUCTLIT) { // #63: an alias-NAMED struct literal (`type rep2 = rep; // let r = rep2{id=6}`) parses its type ref as N_IDENT/N_TNAME // "rep2", but only the base `rep` is registered — bare // structlookup(c, "rep2") returns nil, so the fill never // fired: the slot zeroed + the lit DROPPED (≤8B silent) or // fell to the :2920 LOUD (>8B). structlookupchain chases the // alias chain to the base struct, the #92/W2 SSoT already // adopted at cgenstmt:1974/:2687. cs chases via // type_chase_named, runtime-correct. let trefn: *syntax.node = rhs.lhs; let si: *structinfo = structlookupchain(c, trefn); 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 == syntax.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 == syntax.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 == syntax.nkind.N_CALL) { if (tn != nil) { if (tn.kind == syntax.nkind.N_TNAME) { // ≤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). // // #21/#224: size off the checker-STAMPED tinfo // (structabisizetn = tichase(tn.type_).size, the // maxalign-rounded ABI size = check.ww:2467), // NOT structlookup(tn.str). On a cross-module // same-leaf collision the inferred-let's tn.str // is a bare leaf that structlookup mis-resolves to // a FOREIGN same-leaf struct → the recv copied that // struct's word count (a 24B foreign over-copies a // 16B local, spilling into a neighbour slot). The // stamped tinfo carries the right size regardless of // collision; byte-id with structabisize on a // resolving lookup. Mirrors the sibling array arm // below (already tn.type_-keyed). cstage is // type-keyed (lu->size) — align ww UP. let lsz: i32 = structabisizetn(tn.type_: *syntax.tinfo); if (lsz > 0) { let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, rhs); cgaggregstore(c, "BP", off, lsz, true); c.lastwasreturn = 0; return; }; }; }; }; }; }; // #267: array return-by-value RECV ≤24B — `let c = mk()` // where mk returns an array. Arrays ride the struct reg-recv // path (AX/DX/CX, sized tail). >24B sret rides the sret recv // above (callsretsize keyed). Array natural size (tinfo.size // = sub.size*len) mirrors cstage lu->size. No structfloatclass // (pure-int element arrays). if (rhs.kind == syntax.nkind.N_CALL && tn != nil && tn.kind == syntax.nkind.N_TARRAY) { let ati: *syntax.tinfo = tn.type_: *syntax.tinfo; ati = tichase(ati); if (ati != nil) { let lsz: i32 = ati.size: i32; let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, rhs); cgaggregstore(c, "BP", off, lsz, true); 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 // ABI sizes that aren't 8-aligned (e.g. `struct // { i32, i32, i32 }`, maxalign 4 → ABI 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 == syntax.nkind.N_IDENT) { // #31: size the local-to-local struct COPY off the COPY // SOURCE's stamped tinfo (structabisizetn(rhs.type_)), NOT // structlookup(tn.str). On a cross-module same-leaf collision // the inferred-let's declared-type leaf (tn.str) mis-resolves // to a FOREIGN same-leaf struct -> the memcpy run was sized off // it (a 24B foreign over-reads a 16B source + over-writes the // dst slot -- silent OOB, cs!=ww). rhs.type_ is the correct // source struct; byte-id with structabisize on a resolving // lookup. memcpy run = maxalign-rounded ABI size = cstage N_LET // sz=lu->size (cgen.c:7533, struct-IDENT :7869, check.c:760 SSoT). let lsz: i32 = structabisizetn(rhs.type_: *syntax.tinfo); 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; }; }; }; // #265 fold-1/1b (#268): aggregate let-init copy from an // ADDRESSABLE rhs — `*p` (deref), an array ident `= s` // (struct-ident is the arm above), an N_DOT field `= o.i`, an // N_INDEX element `= a[i]`, T a struct/array >8B. ONE memcpy // loop fed by a per-rhs source-address setup landing the SOURCE // ADDRESS in SI; copy N bytes (the #254 non-slot-padded ABI // extent: structabisize for a struct, tinfo.size for an array) // slot→slot — a MOVQ run plus a sized MOVL/MOVW/MOVB tail. Pre- // fix array-ident/N_DOT truncated to the 8B scalar tail below // and N_INDEX scalar-loaded the element address (segfault). // Mirror of cstage cgen.c N_LET arm (rule-10); the by-value // RETURN ABI is fold-2 (#267). Source-addr setups reuse closed // machinery: LEAQ-slot (ident), the deref operand (cgexpr), // dotchainaddr (#253, N_DOT), the &base[i] spine (#252, // N_INDEX). let aggn: i32 = 0; let aggsi: *structinfo = structlookupchain(c, tn); if (aggsi != nil) { aggn = structabisize(aggsi); } else { let aggti: *syntax.tinfo = nil; if (tn != nil) { aggti = tn.type_: *syntax.tinfo; }; aggti = tichase(aggti); if (aggti != nil) { if (aggti.kind == syntax.tykind.TY_ARRAY) { aggn = aggti.size: i32; }; }; }; if (aggn > 8) { let havesrc: bool = false; if (rhs.kind == syntax.nkind.N_UN) { if (rhs.op == syntax.tkind.TK_STAR) { cgexpr(c, rhs.lhs); emitline("\tMOVQ\tAX, SI\n"); havesrc = true; }; }; if (!havesrc) { if (rhs.kind == syntax.nkind.N_IDENT) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), SI\n"); havesrc = true; } else { // rule-10: the addressable-def set must // equal cstage's let_islet || // def_isarraydef || def_isstructdef — // the laid-out-aggregate globals (#129 // A.2/A.3). Bare deflookup (any def) // over-copies struct-defs on wwstage // only; mirror the defisaddressable // pairing instead. let aggdtn: *syntax.node = defvartnode(c, rhs.str); let aggisdef: bool = defvarstructinfo(c, rhs.str) != nil; if (aggdtn != nil) { if (aggdtn.kind == syntax.nkind.N_TARRAY) { aggisdef = true; }; }; if (isletvar(c, rhs.str) || aggisdef) { emitline("\tLEAQ\t"); emitsymname(c, rhs.str); emitline("(SB), SI\n"); havesrc = true; }; }; }; }; if (!havesrc) { if (rhs.kind == syntax.nkind.N_DOT) { if (dotchainaddr(c, rhs, "SI")) { havesrc = true; }; }; }; if (!havesrc) { if (rhs.kind == syntax.nkind.N_INDEX) { let base: *syntax.node = rhs.lhs; let idx: *syntax.node = rhs.rhs; let bu: *syntax.tinfo = nil; if (base != nil) { bu = base.type_: *syntax.tinfo; }; bu = tichase(bu); if (base != nil && base.kind == syntax.nkind.N_IDENT && bu != nil && bu.kind == syntax.tykind.TY_ARRAY) { let esz: i32 = 1; if (bu.sub != nil) { esz = bu.sub.size: i32; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; let bl: *local = localfindnode(c, base.str); if (bl != nil) { emitline("\tLEAQ\t"); emitoff(bl.off: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), BX\n"); }; emitline("\tADDQ\tBX, AX\n"); emitline("\tMOVQ\tAX, SI\n"); havesrc = true; }; // #270-3a: the index BASE is an N_DOT // array-field (`x.arr[i]`) or a nested N_INDEX // (`a[i][j]`); the N_IDENT-base arm above missed // both, so the copy fell to the 8B truncation // below. Compute &base[idx]: scaled idx on the // stack, then &base via dotbaseaddr (N_DOT field // address) or the &abase[bidx] spine (nested // N_IDENT-array base), then add. if (!havesrc && base != nil && (base.kind == syntax.nkind.N_DOT || base.kind == syntax.nkind.N_INDEX)) { let esz2: i32 = 1; if (bu != nil && bu.sub != nil) { esz2 = bu.sub.size: i32; }; cgexpr(c, idx); if (esz2 > 1) { emitline("\tMOVQ\t$"); emitint(esz2: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tPUSHQ\tAX\n"); let baseok: bool = false; if (base.kind == syntax.nkind.N_DOT) { if (dotbaseaddr(c, base, "AX")) { baseok = true; }; } else { let ab: *syntax.node = base.lhs; let bidx: *syntax.node = base.rhs; let abu: *syntax.tinfo = nil; if (ab != nil) { abu = ab.type_: *syntax.tinfo; }; abu = tichase(abu); if (ab != nil && ab.kind == syntax.nkind.N_IDENT && abu != nil && abu.kind == syntax.tykind.TY_ARRAY) { let aesz: i32 = 1; if (abu.sub != nil) { aesz = abu.sub.size: i32; }; cgexpr(c, bidx); if (aesz > 1) { emitline("\tMOVQ\t$"); emitint(aesz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; let abl: *local = localfindnode(c, ab.str); if (abl != nil) { emitline("\tLEAQ\t"); emitoff(abl.off: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, ab.str); emitline("(SB), BX\n"); }; emitline("\tADDQ\tBX, AX\n"); baseok = true; }; }; emitline("\tPOPQ\tBX\n"); if (baseok) { emitline("\tADDQ\tBX, AX\n"); emitline("\tMOVQ\tAX, SI\n"); havesrc = true; }; }; }; }; // C4 (F5, task #7): the remaining ADDRESSABLE rhs // shapes — a slice-base element (`= xs[0]`; the arms // above have TY_ARRAY/N_DOT/N_INDEX bases but no // TY_SLICE base) and deref-spine leaves // (`= (*ts)[i].cap`) — resolve through cgplaceaddr // (the C1 resolver; enumerated arms dispatch first so // their asm is untouched). Pre-C4 these fell through // to the scalar default's 8B truncation while cstage // emitted NOTHING — gate-blind cs≠ww. if (!havesrc) { if (cgplaceaddr(c, rhs, "SI")) { havesrc = true; }; }; if (havesrc) { let k: i32 = 0; for (k + 8 <= aggn) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= aggn) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= aggn) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= aggn) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 1; }; c.lastwasreturn = 0; return; }; // #38b (rule 7): `?`/`!` over an sret-class call into // an aggregate let — keep the established #38b/#40 // loud-stop marker (mirror of cstage's pre-arm fatal, // cgen.c N_LET; pre-C4 this shape fell through to the // cgtryunw/cgtryprop gates, which the C4 tail below // now pre-empts in let position). if (rhs.kind == syntax.nkind.N_TRYUNW || rhs.kind == syntax.nkind.N_TRYPROP) { if (rhs.lhs != nil) { if (rhs.lhs.kind == syntax.nkind.N_CALL) { if (callsretsize(c, rhs.lhs) > 0) { let m38f: str = "#38b: `?`/`!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; os.write(2, m38f.ptr, m38f.len: u64); os.exit(1); }; }; }; }; // C4: nothing below this arm can initialise a >8B // struct/array slot — the scalar default's 8B store // was a silent truncation (rule 7). let mf5: str = "let: aggregate init from unhandled rhs shape (task #7/rule-7)\n"; os.write(2, mf5.ptr, mf5.len: u64); os.exit(1); }; 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. // #84 (user ruling, Go-zero): `[N]T` arrays zero-fill like // every other composite. They were excluded here, so a // dirtied-stack `let a: [3]int;` read garbage — BOTH stages, // both-wrong-IDENTICAL, gate-blind (#263). Dropping the // exclusion (cstage dropped `!TY_ARRAY` symmetrically) routes // arrays into the zsz>8 / zsz==8 arms below; an 8B array is // already caught by typeis8byteprimitive (TY_ARRAY size==8) → // single MOVQ $0, matching cstage's sz==8 store. // Zero-fill extent. cstage sizes the run on `lu->size` // (the natural ABI size from the type table, cgen.c:8397); // 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 nothing. Source the extent from the // type table's tinfo.size for a struct-typed let to converge; // slot allocation stays on `sz` (frame uses slot-padded slots). // #254: structabisize is NOT a sound ABI-size source here — it // sums fieldsize(), which slot-pads a nested value-struct field // to 8, so a sub-8 outer struct (e.g. `struct{struct{[4]u8}}`, // ABI 4) read 8 and emitted a stray MOVQ $0 cstage doesn't. // fieldsize / registerstruct / frame slot-padding stay // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { // #84: an array's zero-fill extent is its chased ABI // size (cstage `lu->size`), NOT the slot-padded sz from // letslotsize — a non-8-multiple array (e.g. [20]u8 = 20) // would over-zero MOVQ-rounded to 24 and diverge from // cstage's exact 20-byte run. Handles direct N_TARRAY and // alias-to-array (N_TNAME chasing through TY_NAMED) alike. let zti: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; zti = tichase(zti); if (zti != nil && zti.kind == syntax.tykind.TY_ARRAY) { zsz = zti.size: i32; } else { if (n.lhs.kind == syntax.nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { let ti: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; ti = tichase(ti); if (ti != nil) { zsz = ti.size: i32; }; }; }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (zsz == 8) { // #213: an 8B composite (single-field struct / tagged) is // neither an 8B primitive nor zsz>8, so it fell through // un-zeroed while cstage emits MOVQ $0 (cgen.c N_LET // `else if (sz == 8)`); a read-before-init then saw stack // garbage (cs!=ww byte-id + a latent garbage-read). Match // cstage's immediate MOVQ $0, checked BEFORE the run arm // below so an 8B slot stays one immediate store, not // XORQ+MOVQ (rule-10 byte-id). emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (zsz > 0) { // #16: the run arm was gated `zsz > 8`, so a SUB-8 // aggregate (`let c: [3]u8;` = 3, a 3-byte struct, etc.) // matched no arm and fell through un-zeroed — the exact // stack-garbage read ken's bytes verdict pinpointed // (ltrim_cases' `let c: [3]u8;`), BOTH stages, gate-blind // (#263). cstage widened its `!n->rhs && sz > 8` gate to // `sz > 0` symmetrically; the MOVL/MOVB tail already sizes // the run to any 1..7-byte extent. (`[0]T`, zsz == 0, needs // no stores — the lone XORQ is skipped, matching cstage.) 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: *syntax.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: *syntax.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"); }; // #42: bound the push. The buffers are sized exactly LOOP_MAX, so an // unguarded push at nesting depth LOOP_MAX+1 is an OOB heap write; // fail loud at the cap, both stages (cgen.c twin fatals too). if (c.looptop >= LOOP_MAX) { let msg: str = "cgen: loop nesting too deep\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; 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; }; fn cgmassign(c: *cgen, n: *syntax.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: *syntax.node = rettupleof(c, n.rhs); // #10 Fold B: over-cap tuple destructure REASSIGN. Same sret copy-out // as cgmlet but the slots already exist (localfind); a `_` / missing // binding (off == 0) SKIPS its store yet still ADVANCES foff so the // next element stays aligned (harec `_`). Byte-identical to the // cstage N_MASSIGN over-cap arm. let sretrecv: i32 = 0; if (n.rhs != nil) { if (n.rhs.kind == syntax.nkind.N_CALL) { sretrecv = callsretsize(c, n.rhs); }; }; // #64: a tuple-LITERAL rhs carries a DECLARED tuple type (built from // the lvalue binding types) into the cursor fill, so a declared-tagged // element's concrete rvalue widens into the box instead of riding the // decl-less stamped-keyed route — the #57 decl wire extended past // cgmlet/cgreturn to destructure-reassign. A `_` lvalue has no local // (no declared type node); its decl element stays nil and the // fill/receive fall back to the rhs literal element's own stamped type // for the cursor stride (harec `_` advance; pinned by the R3 control). let litrhs: bool = false; if (n.rhs != nil) { if (n.rhs.kind == syntax.nkind.N_TUPLE) { litrhs = true; }; }; let synthdecl: *syntax.node = nil; if (litrhs) { synthdecl = syntax.newnode(syntax.nkind.N_TTUPLE, n.rhs.file, n.rhs.line, n.rhs.col); let dtail: *syntax.node = nil; let lb0: *syntax.node = n.list; for (lb0 != nil) { let w: *syntax.node = syntax.newnode(syntax.nkind.N_TUPLE, n.rhs.file, n.rhs.line, n.rhs.col); w.lhs = nil; if (lb0.kind == syntax.nkind.N_IDENT) { let lc0: *local = localfindnode(c, lb0.str); if (lc0 != nil) { w.lhs = lc0.tnode; }; }; w.next = nil; if (dtail == nil) { synthdecl.list = w; } else { dtail.next = w; }; dtail = w; lb0 = lb0.next; }; cgtuplelittocursor(c, n.rhs, synthdecl); } else { if (n.rhs != nil) { cgexpr(c, n.rhs); }; }; if (sretrecv > 0) { let scr: i32 = localfind(c, "@sretscr"); let pt2: *syntax.node = nil; if (rettuple != nil) { pt2 = rettuple.list; }; let foff: i32 = 0; let lb: *syntax.node = n.list; for (lb != nil) { let tn: *syntax.node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); // #22b: the >8B copy-out keys on the ACCESSOR's slot // (str/slice header AND tagged box), not a str/slice // kind test — the tagged element took the scalar arm // (8B silent truncation; unreachable while the SEND // louded, live once #22b unwires it). Byte-id for // str/slice (esz == eslot == 24). Mirrors the cstage // N_MASSIGN sret arm + the R-1 all-three-routings lesson. let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *syntax.tinfo = pt2.lhs.type_: *syntax.tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let off: i32 = 0; if (lb.kind == syntax.nkind.N_IDENT) { off = localfind(c, lb.str); }; if (off != 0) { if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (eslot > 8) { let k: i32 = 0; for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; } else { let lop: str = tnodeloadop(c, tn, esz); let sop: str = tnodestoreop(c, tn, esz); emitline("\t"); emitline(lop); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; }; // C-t0: slot stride — must mirror the N_RETURN // over-cap SEND's buffer layout (cstage N_MASSIGN // twin strides tuple_eslot). foff += tupeslotn(tn); lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; c.lastwasreturn = 0; return; }; let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *syntax.node = n.list; let pt: *syntax.node = nil; if (rettuple != nil) { pt = rettuple.list; }; // #64: a tuple-LITERAL rhs keys element WIDTH on the DECLARED lvalue // type (synthdecl), not the rettuple (nil for a literal); a `_` slot // (declared type nil) falls back to the rhs literal element's own // stamped type for the cursor stride. let dp: *syntax.node = nil; let re: *syntax.node = nil; if (litrhs) { dp = synthdecl.list; re = n.rhs.list; }; for (l != nil) { let tn: *syntax.node = nil; if (litrhs) { if (dp != nil && dp.lhs != nil) { tn = dp.lhs; } else { tn = re; }; } else { if (pt != nil) { tn = pt.lhs; }; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; if (dp != nil) { dp = dp.next; }; if (re != nil) { re = re.next; }; }; if (gptotal > TUPLE_GPCAP) { // 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; }; dp = nil; re = nil; if (litrhs) { dp = synthdecl.list; re = n.rhs.list; }; for (l != nil) { let tn: *syntax.node = nil; if (litrhs) { if (dp != nil && dp.lhs != nil) { tn = dp.lhs; } else { tn = re; }; } else { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(tn); let off: i32 = 0; if (l.kind == syntax.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, eslot, tn); }; if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; if (dp != nil) { dp = dp.next; }; if (re != nil) { re = re.next; }; }; c.lastwasreturn = 0; return; }; fn cgmlet(c: *cgen, n: *syntax.node) void = { let rhs: *syntax.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: *syntax.node = rettupleof(c, rhs); // #10 Fold B: over-cap tuple destructure RECEIVE. The callee sret'd // the whole tuple into the @sretscr discard slot (cgcall sees // callsretsize > 0, no lvalue dest wired). Copy each element out to // its binding slot at the SAME packed offset the SEND wrote (foff += // element size — the t.0/t.1 layout), each at its NATURAL width // (#169). Byte-identical to the cstage N_MLET over-cap arm. let sretrecv: i32 = 0; if (rhs.kind == syntax.nkind.N_CALL) { sretrecv = callsretsize(c, rhs); }; // #242: rhs is a tuple already materialised in a local slot (a match- // bound union payload, `let (a,b)=t`), NOT a register-returning call. // cgexpr(tuple ident) loads only word0->AX, so the register cursor // path below reads DX/CX stale. Copy each element from the ident's // slot at the register-ABI 8B stride (24B for a slice/str header) — // the SAME layout the tagged construct + match payload-bind write. // Mirror of cstage cgen.c N_MLET tuple-ident arm. The binding element // types ride l.lhs (stamped by the checker's stamptuplebinds). if (rhs.kind == syntax.nkind.N_IDENT) { let rl: *local = localfindnode(c, rhs.str); if (rl != nil) { let rti: *syntax.tinfo = rhs.type_: *syntax.tinfo; rti = tichase(rti); if (rti != nil) { if (rti.kind == syntax.tykind.TY_TUPLE) { let srcoff: i32 = rl.off; let foff: i32 = 0; let lb: *syntax.node = n.list; for (lb != nil) { let tn: *syntax.node = lb.lhs; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; let eti: *syntax.tinfo = nil; if (tn != nil) { eti = tn.type_: *syntax.tinfo; }; if (eti != nil) { esz = eti.size: i32; }; let bsz: i32 = 8; if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((srcoff + foff): i64); emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (eslot > 8) { let k: i32 = 0; for (k < eslot) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; } else { let lop: str = tnodeloadop(c, tn, esz); let sop: str = tnodestoreop(c, tn, esz); emitline("\t"); emitline(lop); emitline("\t"); emitoff((srcoff + foff): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; foff += eslot; lb = lb.next; }; c.lastwasreturn = 0; return; }; }; }; }; cgexpr(c, rhs); if (sretrecv > 0) { let scr: i32 = localfind(c, "@sretscr"); let pt2: *syntax.node = nil; if (rettuple != nil) { pt2 = rettuple.list; }; let foff: i32 = 0; let lb: *syntax.node = n.list; for (lb != nil) { let tn: *syntax.node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *syntax.tinfo = pt2.lhs.type_: *syntax.tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let bsz: i32 = 8; if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (eslot > 8) { let k: i32 = 0; for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; } else { let lop: str = tnodeloadop(c, tn, esz); let sop: str = tnodestoreop(c, tn, esz); emitline("\t"); emitline(lop); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; foff += eslot; lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; c.lastwasreturn = 0; return; }; let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *syntax.node = n.list; let pt: *syntax.node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *syntax.node = l.lhs; if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; }; if (gptotal > TUPLE_GPCAP) { // 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: *syntax.node = l.lhs; if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(tn); let sz: i32 = 8; if (eslot > 8) { sz = eslot; }; let off: i32 = localadd(c, l.str, sz, tn); tupstore(c, gpcur, ssecur, off, eslot, tn); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + eslot / 8; }; 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, 24 for str/slice (str IS []u8, the slice // header SSoT), tuple → sum of its 8B-floored element slots, default 8. fn paramfieldsize(t: *syntax.node) i32 = { if (t == nil) { return 8; }; let k: syntax.nkind = t.kind; if (k == syntax.nkind.N_TPTR) { return 8; }; if (k == syntax.nkind.N_TFN) { return 8; }; if (k == syntax.nkind.N_TCHAN) { return 8; }; // #43 (F7-c4): a slice tuple-field carries the 24B header (ptr+len+ // cap), not the 8B scalar default. Without this arm the for-range // destructure over `[N]([]T, U)` strode the tuple at 8 not 24 and // read field-2 at the wrong offset (cs=42/ww=8, the cat-A repro). // tyslicesize() is the slice-header SSoT (rule-13); cstage reads the // same width via tp->type->size (cmd/w6c/cgen.c N_FORRANGE). if (k == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; }; // #53: a tagged-union tuple-field carries its full box (tag + widest // payload, slot-padded), NOT the 8B scalar default — a for-range // destructure binding sized 8 loaded only the tag word (cs=56/ww=9, the // cat-A repro). The box width is variant-dependent, so read it from the // checker-stamped tinfo (rule-13): cstage's tp->type->size reads the same // resolved width. The stamp already collapsed any alias, so this also // covers an N_TNAME field naming a tagged union (paramfieldsize's no-`c` // structural contract can't aliaslookup-chase the node). Mirrors the // N_TSLICE arm's type-table SSoT. let tgi: *syntax.tinfo = t.type_: *syntax.tinfo; if (tgi != nil) { tgi = tichase(tgi); if (tgi != nil && tgi.kind == syntax.tykind.TY_TAGGED) { return tgi.size: i32; }; }; // #43 (F7-c4): a nested tuple field sizes as the sum of its element // SLOTS — each element floored UP to one 8B eightbyte (str/slice keep // their 24B header), per the tuple-slot ruling and tupeslot's // roundup8. Recurse structurally so a tuple-of-tuple lands the same // stride cstage's tp->type->size computes. if (k == syntax.nkind.N_TTUPLE) { let total: i32 = 0; let dp: *syntax.node = t.list; for (dp != nil) { let esz: i32 = paramfieldsize(dp.lhs); total = total + (esz + 7) / 8 * 8; dp = dp.next; }; return total; }; if (k == syntax.nkind.N_TNAME) { let nm: str = t.str; if (syntax.streq(nm, "str")) { return primtypesize("str"): i32; }; // paramfieldsize is a STRUCTURAL // (no-`c`, no-chase) sizer by design — it takes a *node, not a // *cgen, so it cannot run aliasprimsize's aliaslookup chase // (threading c is the dormant #110). A bare primsize is correct // here, not the #101 narrow-alias bug shape. let ps: i32 = primsize(nm); if (ps > 0) { return ps; }; }; // rule-7: N_TARRAY (an array-typed tuple field) is intentionally not // sized here — and PROVABLY unreachable, not merely latent (#39). // paramfieldsize's only callers are tuple-field contexts (the N_TTUPLE // recursion above + the cgforrange destructure sizers below), and the // checker loud-REJECTS an array/struct/nested-tuple tuple element at // N_TTUPLE resolution (check.ww:2150, "composite element deferred to // task #60"; pinned both-stage by test 832_tuple_elem_overlong). So no // tuple field can carry an N_TARRAY type — this arm cannot be reached // until #60's inline-composite layout lands and lifts that gate. The 8B // fall-through is correct-by-vacuity; reopen WITH #60, adding the arm // (read t.type_.size, twin of the #53 tagged arm above). 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: *syntax.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: *syntax.node) void = { let slc: *syntax.node = n.lhs; let slclocal: *local = nil; let slctn: *syntax.node = nil; if (slc != nil) { if (slc.kind == syntax.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: *syntax.node = nil; if (slctn != nil) { let sk: syntax.nkind = slctn.kind; if (sk == syntax.nkind.N_TSLICE) { elemt = slctn.lhs; }; if (sk == syntax.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 == syntax.nkind.N_TNAME) { if (syntax.streq(slctn.str, "str")) { let sti: *syntax.tinfo = slctn.type_: *syntax.tinfo; if (sti != nil) { if (sti.sub != nil) { let u8n: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, slctn.file, slctn.line, slctn.col); u8n.str = "u8"; u8n.type_ = sti.sub: *void; elemt = u8n; }; }; }; }; }; // #60 (alias arc #5): alias-NAMED scrutinee (`let a: arr`, arr = // [4]int) — the tnode peek above sees only the N_TNAME leaf: // elemt nil, esz 1-sentinel, neither isarr nor isslicestr, so the // per-iteration base walked the array words as a POINTER (SEGV). // Chase the stamped tinfo (cstage N_FORRANGE u = type_chase_named // (slc->type) feeds esz/alen/base classify uniformly) and // synthesise the element node off .sub — the FC0 non-ident // precedent below. let rti60: *syntax.tinfo = nil; if (slctn != nil) { if (slctn.kind == syntax.nkind.N_TNAME) { let st60: *syntax.tinfo = slctn.type_: *syntax.tinfo; if (st60 != nil) { if (st60.kind == syntax.tykind.TY_NAMED) { rti60 = tichase(st60); }; }; }; }; if (rti60 != nil && elemt == nil) { if (rti60.sub != nil) { let en60: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, slctn.file, slctn.line, slctn.col); en60.str = rti60.sub.name; en60.type_ = rti60.sub: *void; elemt = en60; }; }; // 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. // C4 (task #7): elemsizeofc, not elemsizeof — a struct element // (`[]thread`, 16B) hit elemsizeof's 8-sentinel while cstage reads // the stamped slc->type sub size (IMULQ $8 vs $16, gate-blind // cs≠ww). elemsizeofc recovers the width from the stamped tinfo // (the #8 named-narrow precedent). let esz: i32 = elemsizeofc(c, slctn); // #60: alias-NAMED scrutinee — stride off the chased stamped // element (cstage esz = u->sub->size). if (rti60 != nil) { let es60: *syntax.tinfo = tichase(rti60.sub); if (es60 != nil) { esz = es60.size: i32; }; }; if (elemt != nil) { if (elemt.kind == syntax.nkind.N_TTUPLE) { let total: i32 = 0; let p: *syntax.node = elemt.list; for (p != nil) { total += paramfieldsize(p.lhs); p = p.next; }; esz = total; }; }; // C4 (FC0, task #7): a non-ident scrutinee (`re.charsets`) has no // local tnode — slctn is nil, so esz fell to 1 and the binding // registered typeless (cstage reads the stamped slc->type: esz 24, // slice-header readbacks → cs≠ww). Derive both from the checker- // stamped slc.type_ (tinfo SSoT, the #209/#211 discipline); the // synthesised N_TNAME carries the element tinfo so cgident's // str/slice/float keys read it like a declared local (the str→u8 // synthesis precedent above). if (slctn == nil && slc != nil) { let sti2: *syntax.tinfo = slc.type_: *syntax.tinfo; sti2 = tichase(sti2); if (sti2 != nil) { if (sti2.kind == syntax.tykind.TY_SLICE || sti2.kind == syntax.tykind.TY_STR || sti2.kind == syntax.tykind.TY_ARRAY) { if (sti2.sub != nil) { esz = sti2.sub.size: i32; let en: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, slc.file, slc.line, slc.col); en.str = sti2.sub.name; en.type_ = sti2.sub: *void; elemt = en; }; }; }; }; let destruct: bool = (n.list != nil); // .rgi (counter) + .rgl (length) scratch slots. #70: a NON-IDENT // slice/str base (field chain, indexed element, call) also needs // a .rgb base spill — pre-#70 the init stored cgexpr's AX (the // DATA POINTER — a slice-valued cgexpr leaves AX=ptr, BX=len, // CX=cap) into .rgl, and the per-iteration code had no non-ident // base arm, so the bound-reload BX doubled as the base: i was // compared against the POINTER and walked off the end // (regex.finish, SEGV on the first non-empty charsets; empty // slices coincidentally exited on ptr==0 — latent since fold 1, // byte-id both stages). A non-ident ARRAY base is loud (rule 7): // its cgexpr shape is not the slice header. 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); let baseoff: i32 = 0; if (slc != nil) { if (slc.kind != syntax.nkind.N_IDENT) { let stu70: *syntax.tinfo = slc.type_: *syntax.tinfo; stu70 = tichase(stu70); let arr70: bool = false; if (stu70 != nil) { if (stu70.kind == syntax.tykind.TY_ARRAY) { arr70 = true; }; }; if (arr70) { let m70: str = "for-range over a non-ident array base unwired (#70)\n"; os.write(2, m70.ptr, m70.len: u64); os.exit(1); }; // #11: cgexpr on a slice DEREF (*p) does not deliver // the AX/BX/CX header convention the spill assumes // (the deref-spine load family) — keep it LOUD until // #11 wires the deref load. if (slc.kind == syntax.nkind.N_UN) { if (slc.op == syntax.tkind.TK_STAR) { let m11: str = "for-range over a deref base unwired (#11)\n"; os.write(2, m11.ptr, m11.len: u64); os.exit(1); }; }; let bname: str = mkscratchname(c, "rgb"); baseoff = localalloc(c, bname, 8, nil); }; }; // #121 leg (c): for-range over a module-GLOBAL slice/str/array base // SEGV's today — the init + per-iteration base resolution below // assume a frame-local slot (localfindnode), so a global let/def base // reads saved-BP as the .ptr/.len. LOUD-STOP symmetric with cstage // cgen.c (byte-id-neutral; segfault→compile-error is pure // improvement). The fix (the N_INDEX isglobal base resolution ported // into the for-range spine) is a DISTINCT mechanism — filed as a #121 // sibling, off fold-6's path. if (slc != nil) { if (slc.kind == syntax.nkind.N_IDENT) { if (localfindnode(c, slc.str) == nil) { let isglob: bool = isletvar(c, slc.str); if (!isglob) { let gdtn: *syntax.node = defvartnode(c, slc.str); if (gdtn != nil) { if (gdtn.kind == syntax.nkind.N_TARRAY) { isglob = true; }; }; }; if (isglob) { let mc: str = "#121: for-range over a module-global slice/array base unwired (global-base resolution gap)\n"; os.write(2, mc.ptr, mc.len: u64); os.exit(1); }; }; }; }; // 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: *syntax.node = nil; if (elemt != nil) { if (elemt.kind == syntax.nkind.N_TTUPLE) { tp = elemt.list; }; }; let field_off: i32 = 0; let m: *syntax.node = n.list; for (m != nil) { // bind_* arrays are sized 8; a 9th binding silently // vanished pre-gate (rule 7 — the DEFER_MAX/LOOP_MAX // cap discipline). if (nbinds >= 8) { let mfr: str = "for-range destructure: more than 8 bindings (rule 7)\n"; os.write(2, mfr.ptr, mfr.len: u64); os.exit(1); } 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: *syntax.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; }; 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: syntax.nkind = slctn.kind; if (tk == syntax.nkind.N_TSLICE) { isslicestr = true; }; if (tk == syntax.nkind.N_TARRAY) { isarr = true; }; if (tk == syntax.nkind.N_TNAME) { if (syntax.streq(slctn.str, "str")) { isslicestr = true; }; }; }; // #60: alias-NAMED scrutinee — classify off the chased stamped // kind (cstage gates on u->kind TY_SLICE/TY_STR vs TY_ARRAY). if (rti60 != nil) { if (rti60.kind == syntax.tykind.TY_ARRAY) { isarr = true; }; if (rti60.kind == syntax.tykind.TY_SLICE) { isslicestr = true; }; if (rti60.kind == syntax.tykind.TY_STR) { isslicestr = true; }; }; if (isslicestr) { if (slc.kind == syntax.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 == syntax.nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; }; }; // #60: alias-NAMED scrutinee has no length tnode — bound off // the chased tinfo (cstage aimm(u->alen)). if (rti60 != nil) { alen = rti60.alen: i64; }; emitline("\tMOVQ\t$"); emitint(alen); emitline(", "); emitoff(loff: i64); emitline("(BP)\n"); } else { cgexpr(c, slc); if (baseoff != 0) { // #70: slice/str header from cgexpr is AX=ptr, // BX=len, CX=cap — bound is LEN; spill the base ptr // for the per-iteration element address. emitline("\tMOVQ\tBX, "); emitoff(loff: i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff(baseoff: i64); emitline("(BP)\n"); } else { // ident with unresolved type — legacy path, unchanged. 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"); // #42: bound the push. The buffers are sized exactly LOOP_MAX, so an // unguarded push at nesting depth LOOP_MAX+1 is an OOB heap write; // fail loud at the cap, both stages (cgen.c twin fatals too). if (c.looptop >= LOOP_MAX) { let msg: str = "cgen: loop nesting too deep\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; 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 == syntax.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"); }; }; } else { // #70: non-ident slice/str base — reload the spilled data // pointer (pre-#70 BX held the bound reload). emitline("\tMOVQ\t"); emitoff(baseoff: 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. // C4 (F5/FC0, task #7): a by-value AGGREGATE element (struct / // tuple / str/slice header, esz > 8) copies its FULL extent — the // single load word truncated it to 8B, so every field past word 0 // (str/slice .len/.cap included) read stale slot bytes // (regex.finish's 24B charset binding, gate-blind cs≠ww). Same // word-run + sized-tail idiom as the cglet aggregate copy. if (!destruct && esz > 8) { let k: i32 = 0; for (k + 8 <= esz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= esz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVL\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= esz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVW\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= esz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVB\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 1; }; } else { let b: i32 = 0; for (b < nbinds) { // #40 (#263): a str/slice/struct destructure binding // (24B header / aggregate, sz>8) copies its FULL extent // — the single load word truncated a slice binding to // its .ptr, dropping .len/.cap (both stages identically, // byte-id-WRONG; F7-c4 fixed only the STRIDE). Same // word-run + sized-tail idiom as the non-destructure // aggregate copy above. if (bind_sz[b] > 8) { let k: i32 = 0; for (k + 8 <= bind_sz[b]) { emitline("\tMOVQ\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= bind_sz[b]) { emitline("\tMOVL\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVL\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= bind_sz[b]) { emitline("\tMOVW\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVW\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= bind_sz[b]) { emitline("\tMOVB\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVB\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 1; }; b += 1; continue; }; 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: *syntax.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: *syntax.node = nil; let cs: *syntax.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: *syntax.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: *syntax.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: *syntax.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; };