Files
ww/selfhost/cmd/wcc/cgenstmt.ww
Hojun-Cho 5b212e51cf wcc/cgen: zero-init sub-8-byte bare lets, both stages; bytes test honest (#16-team)
A bare 'let x: T;' with 1 <= size(T) <= 7 matched no zero-fill arm in
either stage (8B and >8B were already zeroed) - 'let c: [3]u8;' read
stack garbage. User-ruled zero-value semantics: cstage gate sz>8 ->
sz>0; wwstage zsz==8 arm hoisted above the fill-run arm (required -
8B would otherwise route into the run and diverge) and run gate
zsz>0. New 840 pin: dirty-frame probe rows, dual-dim (run + cs/ww
byte-id); discriminators fail exit-154 on pre-fix binaries.

Fused with the lib/bytes test conversion (rule 11): either half alone
turns 967 red. The old exit(signalled+10) wrapped a real 1782-count
ltrim failure to exit 0 - green depended on the garbage. Converted to
assert form (completes the 35/35 @test conversion); ltrim rows keep
the bare 'let c: [3]u8;' as the consumer proof of the fix.
2026-06-10 20:08:58 +09:00

4220 lines
144 KiB
Plaintext

// selfhost/cmd/wcc/cgenstmt.ww — split out of cgen.ww.
//
// cgstmt is a thin dispatcher over n.kind; each branch defers to a
// per-kind helper: cgblock, cgreturn, cgexprstmt, cglet, cgif, cgfor,
// cgmassign, cgbreak, cgcontinue.
//
// The expression generator (cgexpr) lives in cgenexpr.ww; the
// foundation (types, emit primitives, collect* tables, FFI/module
// maps) lives in cgen.ww.
package wcc;
import os;
import ast;
import tok;
import typ;
import sym;
import strconv;
// ---- statement cgen --------------------------------------------------
fn cgstmt(c: *cgen, n: *node) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
if (k == nkind.N_BLOCK) { cgblock(c, n); return; };
if (k == nkind.N_RETURN) { cgreturn(c, n); return; };
if (k == nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; };
if (k == nkind.N_LET) { cglet(c, n); return; };
if (k == nkind.N_IF) { cgif(c, n); return; };
if (k == nkind.N_FOR) { cgfor(c, n); return; };
if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; };
if (k == nkind.N_SWITCH) { cgswitch(c, n); return; };
if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; };
if (k == nkind.N_MLET) { cgmlet(c, n); return; };
if (k == nkind.N_BREAK) { cgbreak(c, n); return; };
if (k == nkind.N_CONTINUE) { cgcontinue(c, n); return; };
if (k == nkind.N_YIELD) { cgyield(c, n); return; };
if (k == nkind.N_DEFER) {
if (c.defertop < DEFER_MAX) {
c.deferbuf[c.defertop] = n.lhs;
c.defertop += 1;
};
return;
};
c.lastwasreturn = 0;
};
fn cgyield(c: *cgen, n: *node) void = {
// Evaluate the value into AX (and BX for str), then JMP to the
// enclosing match's end label. Falls through silently if there
// is no active match — should be a checker error eventually.
if (n.lhs != nil) { cgexpr(c, n.lhs); };
if (c.yieldtop > 0) {
let tgt: str = c.yieldbuf[c.yieldtop - 1];
emitline("\tJMP\t");
emitline(tgt);
emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgblock(c: *cgen, n: *node) void = {
// Save/restore the locals head across the block (post-#27).
// Inner-scope `let` bindings prepend to c.locals via localadd;
// without this restore, the prepended stubs leak into sibling
// and ancestor scopes, and localfind (head-first) returns the
// inner binding's offset for an identifier that semantically
// belongs to the outer scope. The frame is left grown — we
// don't reclaim popped slots, matching cstage's lowering.
//
// cgfn iterates fn_.body.list directly to bypass this save/
// restore at the function's outermost block — defers (and the
// implicit-return epilogue) need locals intact.
let saved: *local = c.locals;
let s: *node = n.list;
for (s != nil) {
cgstmt(c, s);
s = s.next;
};
c.locals = saved;
return;
};
// rundefers — emit cgexpr for every queued defer in LIFO order.
// Called from cgreturn and the cgfn implicit-return path.
fn rundefers(c: *cgen) void = {
let i: i32 = c.defertop - 1;
for (i >= 0) {
cgexpr(c, c.deferbuf[i]);
i -= 1;
};
return;
};
// #83: positional tuple register-return ABI. Tuple elements ride
// consecutive eightbytes over [AX,DX,CX,R8] (tupreg by index); a
// slice/str rides its 3-word {ptr,len,cap} header (tyslicesize SSoT,
// ref/hare/rt/ensure.ha:4-8), a scalar rides 1. SEND (cgreturn) and
// RECEIVE (cgmlet/cgmassign) walk the SAME widths so element->register
// agrees — mirrors harec create_unpack_bindings
// (ref/harec/src/check.c:1354-1416). Capacity is 4 (AX,DX,CX,R8).
fn tupreg(i: i32) str = {
if (i == 0) { return "AX"; };
if (i == 1) { return "DX"; };
if (i == 2) { return "CX"; };
return "R8";
};
// #164 (#107): SSE half of the SysV dual register-class return. A float
// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the
// INTEGER row tupreg — a float lands in the next XMM regardless of its
// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},
// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage
// tuple_sse_seq (cmd/w6c/cgen.c).
fn tupsse(i: i32) str = {
if (i == 0) { return "X0"; };
return "X1";
};
// 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).
export fn tupeslot(ti: *tinfo) i32 = {
let t: *tinfo = ti;
t = tichase(t);
if (t == nil) { return 8; };
if (t.kind == 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 == tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; };
if (t.kind == tykind.TY_STR || t.kind == tykind.TY_SLICE ||
t.kind == tykind.TY_TAGGED) {
return ((t.size + 7u64) & ~7u64): i32;
};
return 8;
};
export fn tupeslotn(n: *node) i32 = {
if (n == nil) { return 8; };
return tupeslot(n.type_: *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: *node) *node = {
if (rhs == nil) { return nil; };
if (rhs.kind != nkind.N_CALL) { return nil; };
let callee: *node = rhs.lhs;
if (callee == nil) { return nil; };
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cnm = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cnm = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len == 0) { return nil; };
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp == nil) { return nil; };
if (rtyp.kind != nkind.N_TTUPLE) { return nil; };
return rtyp;
};
// 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: *node) *node = {
if (a == nil) { return nil; };
if (a.kind == nkind.N_TUPLE) { return a; };
if (a.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, a.str);
if (lc == nil) { return nil; };
let tn: *node = lc.tnode;
for (tn != nil && tn.kind == nkind.N_TNAME) {
tn = aliaslookup(c, tn.str);
};
if (tn != nil) {
if (tn.kind == nkind.N_TTUPLE) { return tn; };
};
return nil;
};
let t: *node = inferletcalltype(c, a);
if (t != nil) {
if (t.kind == 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: *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 = 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: *node, dtn: *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: *tinfo = e.type_: *tinfo;
t = tichase(t);
if (t != nil && (t.kind == tykind.TY_TAGGED ||
t.kind == 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: *node, dtn: *node) void = {
let t: *tinfo = e.type_: *tinfo;
t = tichase(t);
let etagged: bool = false;
if (t != nil) { if (t.kind == 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_: *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 == 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 == 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: *node, decl: *node) void = {
let dp0: *node = nil;
if (decl != nil) {
if (decl.kind == nkind.N_TTUPLE) { dp0 = decl.list; };
};
let ssecap: i32 = TUPLE_SSECAP;
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let dp: *node = dp0;
let e: *node = tuple.list;
for (e != nil) {
let dtn: *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: *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: *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: *tinfo) void = {
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let el: *ttupleelem = tu.tupleelems;
for (el != nil) {
let et: *tinfo = el.type_;
et = tichase(et);
if (et != nil && (et.kind == tykind.TY_F32 || et.kind == 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: *tinfo = el.type_;
et = tichase(et);
let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64);
let eslot: i32 = tupeslot(el.type_);
if (isflt) {
let mov: str = "MOVSD";
if (et.kind == 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: *tinfo) void = {
let words: i32 = 0;
let el: *ttupleelem = tup.tupleelems;
for (el != nil) {
let et: *tinfo = el.type_;
et = tichase(et);
let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == 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: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// #83 / #164 (#107): positional register-return over a SysV
// dual class cursor (harec create_unpack_bindings, ref/harec/src/
// check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 =
// tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8]
// (tupreg) — a slice/str its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a
// scalar 1 word in AX. Integer words spill L->R to the stack and
// pop into the INTEGER cursor in reverse so positional slot i
// lands in tupreg(i) (byte-id with #83 when no float is present).
// Each float must spill X0 to @tupfscr as we walk, since a later
// element's cgexpr clobbers X0; after the integer pops the saved
// floats reload into X0/X1 by SSE index — INDEPENDENT of the
// INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows
// loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME
// class split drives the receive sites.
// #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 == 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: *node = nil;
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TTUPLE) {
rp0 = c.fnret.list;
};
};
let rp: *node = rp0;
let e: *node = rhs.list;
for (e != nil) {
let rdtn: *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: *node = nil;
if (c.fnret != nil) { pt = c.fnret.list; };
let we: *node = rhs.list;
let foff: i32 = 0;
for (we != nil) {
let dt: *tinfo = nil;
if (pt != nil) { dt = pt.lhs.type_: *tinfo; };
dt = tichase(dt);
if (dt != nil && dt.kind == 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: *tinfo = we.type_: *tinfo;
eu = tichase(eu);
let eoff: i32 = 0;
if (we.kind == nkind.N_IDENT && eu != nil) {
if (eu.kind == 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: *tinfo = pt.lhs.type_: *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: *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: *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 == nkind.N_CALL || rhs.kind == nkind.N_INDEX || rhs.kind == nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) {
let ru: *tinfo = rhs.type_: *tinfo;
ru = tichase(ru);
let fu: *tinfo = c.fnret.type_: *tinfo;
fu = tichase(fu);
if (ru != nil && fu != nil && ru.kind == tykind.TY_TAGGED && fu.kind == tykind.TY_TAGGED) {
if (ru == fu) {
forwardtagged = true;
} else if (ru.nullable == fu.nullable) {
let pa: *tparam = ru.params;
let pb: *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 == 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: *tinfo = rhs.type_: *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 == tykind.TY_TAGGED
&& rhs.kind != 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_: *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 == 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: *tinfo = rhs.type_: *tinfo;
ru1 = tichase(ru1);
let fu1: *tinfo = nil;
if (c.fnret != nil) {
fu1 = c.fnret.type_: *tinfo;
fu1 = tichase(fu1);
};
if (ru1 != nil && fu1 != nil) {
if (ru1.kind == 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 == nkind.N_CAST && ru1.kind == 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_: *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: *tinfo = rhs.type_: *tinfo;
if (typeisf32(rety)) { rfk = 1; }
else { if (typeisfloat(rety)) { rfk = 2; }; };
};
if (nodeisslice(c, rhs)) {
// cgexpr leaves (AX=ptr, BX=len, CX=cap).
// Shuffle into return ABI: DX=ptr, CX=len,
// R8=cap.
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else { if (nodeisstr(c, rhs)) {
// str IS []u8: cgexpr leaves (AX=ptr, BX=len,
// CX=cap). Same shuffle as the slice arm above —
// DX=ptr, CX=len, R8=cap (#1/Phase 3).
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else { if (rfk != 0) {
// #157: float variant — cgexpr left the value
// in X0, not AX. No MOVQ-xmm->gp encoding, so
// bridge X0->DX through a stack slot (same arg-
// push idiom). Zero the slot first so the f32
// case (MOVSS writes only the low 4 bytes)
// leaves a deterministic high-4 — cs==ww byte-
// id, matching f64's MOVSD which fills all 8.
// The AX-independent spill also removes the
// stale-AX cs!=ww on multi-variant returns.
emitline("\tSUBQ\t$8, SP\n");
emitline("\tMOVQ\t$0, (SP)\n");
let mov: str = "MOVSD";
if (rfk == 1) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, (SP)\n");
emitline("\tMOVQ\t(SP), DX\n");
emitline("\tADDQ\t$8, SP\n");
if (rsz > 16) {
emitline("\tMOVQ\t$0, CX\n");
};
if (rsz > 24) {
emitline("\tMOVQ\t$0, R8\n");
};
} else {
emitline("\tMOVQ\tAX, DX\n");
// scalar fills DX only. Zero CX / R8 if dst
// covers slot+16 / slot+24.
if (rsz > 16) {
emitline("\tMOVQ\t$0, CX\n");
};
if (rsz > 24) {
emitline("\tMOVQ\t$0, R8\n");
};
};};};
emitline("\tMOVQ\t$");
if (idx < 0) { idx = 0; };
emitint(idx: i64);
emitline(", AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
// sret return (#23): plain TY_STRUCT > 24B. Callee writes
// through *(@sretarg) (the caller-prealloc dest saved at
// the prologue), then loads @sretarg into RAX and rets —
// the SysV "return the pointer" discipline. Two rhs shapes
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
let sretargoff: i32 = localfind(c, "@sretarg");
if (sretargoff != 0) {
let scs: i32 = sretretsize(c, c.fnret);
if (scs > 0) {
// sret return-forwarding (task #9 follow-up to
// #23): `return f();` where outer + inner both
// return the same >24B struct shape. Outer's
// @sretarg already holds its caller's prealloc
// dest; pass it to inner in RDI (set by cgcall
// via c.sretforward), inner writes directly
// there, inner's RAX (dest pointer) is already
// outer's return value. The trailing MOVQ
// @sretarg(BP), AX is redundant after inner's
// RET but kept for byte-id symmetry with the
// N_IDENT / N_STRUCTLIT arms below.
if (rhs.kind == nkind.N_CALL) {
c.sretforward = 1;
cgexpr(c, rhs);
emitline("\tMOVQ\t");
emitoff(sretargoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let okrhs: bool = false;
// #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 == nkind.N_IDENT) { okrhs = true; };
if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; };
if (rhs.kind == nkind.N_DOT) { okrhs = true; addrsrc = true; };
if (rhs.kind == nkind.N_INDEX) { okrhs = true; addrsrc = true; };
if (rhs.kind == nkind.N_UN) {
if (rhs.op == tkind.TK_STAR) { okrhs = true; addrsrc = true; };
};
if (okrhs) {
if (rhs.kind == 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: *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 == 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 == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
if (rhs.kind == nkind.N_DOT) { okrhs = true; addrsrc = true; };
if (rhs.kind == nkind.N_INDEX) { okrhs = true; addrsrc = true; };
if (rhs.kind == nkind.N_UN) {
if (rhs.op == 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 == 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 == 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 == nkind.N_IDENT) { arrok = true; };
if (rhs.kind == nkind.N_ARRLIT) { arrok = true; };
if (rhs.kind == nkind.N_DOT) { arrok = true; };
if (rhs.kind == nkind.N_INDEX) { arrok = true; };
if (rhs.kind == nkind.N_UN) {
if (rhs.op == tkind.TK_STAR) { arrok = true; };
};
let ati: *tinfo = c.fnret.type_: *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 == 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 == nkind.N_ARRLIT) {
// scalar/float element fill; non-scalar
// elements loud-stop (rule 7, no consumer).
let esubti: *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 == tykind.TY_STRUCT) { badel = true; };
if (esubti.kind == tykind.TY_ARRAY) { badel = true; };
if (esubti.kind == tykind.TY_TUPLE) { badel = true; };
if (esubti.kind == tykind.TY_SLICE) { badel = true; };
if (esubti.kind == 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: *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: *node = rhs.list;
for (e != nil) {
let isellip: bool = false;
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) { repeat = true; isellip = true; };
};
if (isellip) {
e = nil;
} else {
cgexpr(c, e);
if (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: *tinfo = c.fnret.type_: *tinfo;
rti = tichase(rti);
if (rti != nil) {
if (rti.kind == tykind.TY_ARRAY) { aggret = true; };
if (rti.kind == tykind.TY_STRUCT) { aggret = true; };
};
};
if (aggret && rhs.kind != 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) { vidx38 = 0; };
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) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
emitline("\tMOVQ\t$0, AX\n");
};
// str IS []u8: cgexpr leaves AX=ptr, BX=len, CX=cap — str now
// returns exactly like a slice, no AX:DX shuffle (#1/Phase 3).
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
fn cgexprstmt(c: *cgen, n: *node) void = {
if (n.lhs != nil) { cgexpr(c, n.lhs); };
c.lastwasreturn = 0;
return;
};
// 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: *node, rhs: *node, off: i32) void = {
let elemn: *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: *tinfo = arrtn.type_: *tinfo;
if (ati79 != nil) { if (ati79.kind == tykind.TY_NAMED) {
let au79: *tinfo = tichase(ati79);
if (au79 != nil) { if (au79.kind == tykind.TY_ARRAY
&& au79.sub != nil) {
let en79: *node = newnode(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 == nkind.N_TNAME) {
if (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: *tinfo = nil;
if (elemn != nil) { esubti = elemn.type_: *tinfo; };
esubti = tichase(esubti);
let isagg: bool = esubti != nil
&& (esubti.kind == tykind.TY_STRUCT
|| esubti.kind == tykind.TY_ARRAY
|| esubti.kind == 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 == 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 == 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: *node = rhs.list;
for (e != nil) {
let isellip: bool = false;
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
isellip = true;
};
};
if (isellip) {
e = nil;
} else {
if (isagg) {
if (e.kind == nkind.N_STRUCTLIT) {
let esi: *structinfo = structlookupchain(c, elemn);
cgstructlitfillbp(c, esi, e, off + idx * esz);
} else { if (e.kind == 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 == nkind.N_TARRAY) {
if (arrtn.rhs != nil) {
if (arrtn.rhs.kind == 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: *node) void = {
let nm: str = n.str;
let sz: i32 = letslotsize(c, n);
let tn: *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: *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: *node = n.lhs;
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
if (n.rhs != nil) {
let rhs: *node = n.rhs;
// `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45).
// Mirror of cstage cgen.c N_LET arrlit-empty branch: allocate
// n*esz bytes via rt_malloc, then build the {ptr, 0, n} slice
// header in the let slot. The `!`/`?` wraps the builtin's
// `([]T | nomem)` return; walk into the N_TRYUNW / N_TRYPROP
// to keep the direct-store fast path rather than falling
// through to cgalloc (which models scalar alloc and would
// land an 8B region and a junk slice header). `?` propagates
// nomem via AX = tag of nomem in c.fnret, then epilogue RET.
{
let scall: *node = nil;
let viatryunw: bool = false;
let viatryprop: bool = false;
if (rhs.kind == nkind.N_TRYUNW) {
if (rhs.lhs != nil) {
if (rhs.lhs.kind == nkind.N_CALL) {
scall = rhs.lhs;
viatryunw = true;
};
};
} else { if (rhs.kind == nkind.N_TRYPROP) {
if (rhs.lhs != nil) {
if (rhs.lhs.kind == nkind.N_CALL) {
scall = rhs.lhs;
viatryprop = true;
};
};
}; };
let shapeok: bool = false;
// #43: route the slice-shape size guard through SSoT.
// The N_TSLICE kind gate already discriminates here, so
// this is belt-and-suspenders, but the literal would
// silently miss after #1 if check.ww's astsize ever
// drifted from this dispatch.
if (scall != nil && tn != nil
&& tn.kind == nkind.N_TSLICE && sz == tyslicesize(): i32) {
let callee: *node = scall.lhs;
let a0: *node = scall.list;
let a1: *node = nil;
let a2: *node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil && a0 != nil && a1 != nil
&& a2 == nil) {
if (callee.kind == nkind.N_IDENT
&& streq(callee.str, "alloc")
&& a0.kind == nkind.N_ARRLIT
&& a0.list == nil) {
shapeok = true;
};
};
};
if (shapeok) {
// #32: cstage uses `lu->sub->size` (cgen.c:6387), so
// the element width must resolve struct/tagged/alias
// names too — not just primitives. elemsizeofc follows
// TNAME through structlookup/aliaslookup, matching the
// cstage path byte-identically. A bare primsize/slotsize
// fork would silently land esz=1 on `[]point`.
let esz: i32 = elemsizeofc(c, tn);
let count: *node = scall.list.next;
cgexpr(c, count);
emitline("\tPUSHQ\tAX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", BX\n");
emitline("\tIMULQ\tBX, AX\n");
};
emitline("\tMOVQ\tAX, DI\n");
emitline("\tCALL\t");
emitline(ffiresolve(c, "malloc"));
emitline("(SB)\n");
if (viatryunw) {
let okl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(okl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n");
emitline("\tMOVQ\t$60, AX\n");
emitline("\tSYSCALL\n");
emitlabel(okl);
};
if (viatryprop) {
// #45: null = nomem; propagate to the
// enclosing fn's tagged return. AX = tag
// of nomem variant in c.fnret, epilogue
// RETs to caller.
let okl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(okl);
emitline("\n");
// #66 Phase-N step 3: nomem propagation has no
// pattern node, so it can't ride the typeeq
// flatvariantidx path. cstage passes the ty_nomem
// singleton to cg_tag_for_variant; the wwstage cgen
// holds no tinfo singleton, so find the nomem
// variant by its NAMED name over tinfo.params.
let nidx: i32 = -1;
let nti: *tinfo = nil;
if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; };
nti = tichase(nti);
if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) {
let np: *tparam = nti.params;
let nidx2: i32 = 0;
for (np != nil) {
let nvt: *tinfo = np.type_;
if (nvt != nil) {
if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; };
};
np = np.tnext;
nidx2 += 1;
};
}; };
if (nidx < 0) { nidx = 1; };
emitline("\tMOVQ\t$");
emitint(nidx: i64);
emitline(", AX\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(okl);
};
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t$0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
};
// Tagged-union init: delegate to cgwidentaggedstore, which
// handles nullable fold, tagged source (ident or AX/DX/CX
// ABI call), struct payload (literal/ident), str payload,
// scalar payload — with tag remap for tagged-subset widening.
//
// #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 == nkind.N_CALL) {
letsret = callsretsize(c, rhs);
};
if (letsret == 0) {
cgwidentaggedstore(c, tn.type_: *tinfo, rhs,
"BP", off, sz);
c.lastwasreturn = 0;
return;
};
let lru: *tinfo = rhs.type_: *tinfo;
lru = tichase(lru);
let llu: *tinfo = tn.type_: *tinfo;
llu = tichase(llu);
let exact38: bool = false;
if (lru != nil && lru == llu) { exact38 = true; }
else {
if (typeeq(rhs.type_: *tinfo, tn.type_: *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: *node = tn;
for (ttup != nil && ttup.kind == nkind.N_TNAME) {
ttup = aliaslookup(c, ttup.str);
};
if (ttup != nil) {
if (ttup.kind == 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 == 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: *node = ttup.list;
for (q != nil) {
let qt: *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 == nkind.N_TTUPLE
&& rhs.kind != 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 == 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 == 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: *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 == nkind.N_CALL) {
let scs: i32 = callsretsize(c, rhs);
if (scs > 0) {
c.sretdestoff = off;
cgexpr(c, rhs);
c.sretdestoff = 0;
c.lastwasreturn = 0;
return;
};
};
// Whole-struct receive for sizes <=24B (call-result rhs).
// Counterpart of #4's cgreturn ABI: cgexpr leaves
// AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23],
// zero-padded to 24B by the producer.
//
// ASYMMETRY (do NOT mirror the sender): producer emits three
// uniform MOVQs into a zero-padded 24B scratch slot; the
// receiver writes only `sz` bytes — MOVQ for full 8B chunks
// plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
// struct size. Otherwise a trailing 1..7-byte chunk would
// overrun into the next local slot.
//
// Tail chunks in {3,5,6,7} (unreachable under WW struct
// alignment rules — field aligns force size%align==0) fall
// through to the generic scalar store rather than emit a
// stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch.
// #171a: float-bearing struct RECEIVE (return twin of #165's
// param recv). cgexpr leaves each float eightbyte in its SSE
// return reg (X0,X1 = tupsse) and each INT eightbyte in its
// INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors
// per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read
// from the next XMM regardless of its positional eightbyte
// (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's
// abisize is maxalign-rounded to a multiple of 8 (an f64
// forces align 8), so every eightbyte is a full word — the
// #169 sized tail is unreachable here. structfloatclass gates
// to qualifying structs; all-int + f32 fall to the GP recv
// below (byte-id / #171b).
if (rhs.kind == nkind.N_CALL && tn != nil) {
let sfc: i32 = structfloatclass(c, tn);
if (sfc != 0) {
cgexpr(c, rhs);
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitline(tupreg(gpcur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
gpcur += 1;
};
e += 1;
};
c.lastwasreturn = 0;
return;
};
};
if (rhs.kind == nkind.N_CALL) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) {
sname = tn.str;
};
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// ≤24B register RECV: the value arrives packed
// in AX/DX/CX, so size by the maxalign-rounded
// ABI size (cstage lu->size), not the natural
// extent — see structabisize (#169).
let lsz: i32 = structabisize(lsi);
let tlm: i32 = lsz - (lsz / 8) * 8;
if (lsz <= 24) {
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, rhs);
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
// #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 == nkind.N_CALL && tn != nil
&& tn.kind == nkind.N_TARRAY) {
let ati: *tinfo = tn.type_: *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);
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
// Struct ident copy: `let p2: T = p1;` where T is a struct
// >8B and rhs is a local ident. Per-qword MOVQ from src
// slot to dst slot, with a sized tail (MOVL/MOVB) for
// 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 == nkind.N_IDENT) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// memcpy run sizes on the maxalign-rounded ABI
// size — cstage N_LET sets sz = lu->size
// (cgen.c:7533, struct-IDENT branch :7869),
// check.c:760 SSoT. structnaturalsize would
// short struct{i64,i32} (natural 12, ABI 16)
// to MOVQ+MOVL where cstage writes MOVQ+MOVQ.
let lsz: i32 = structabisize(lsi);
if (lsz > 8) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
let soff: i32 = lc.off;
let ki: i32 = 0;
for (ki + 8 <= lsz) {
emitline("\tMOVQ\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
ki += 8;
};
if (ki < lsz) {
let tail: i32 = lsz - ki;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
// #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: *tinfo = nil;
if (tn != nil) { aggti = tn.type_: *tinfo; };
aggti = tichase(aggti);
if (aggti != nil) {
if (aggti.kind == tykind.TY_ARRAY) {
aggn = aggti.size: i32;
};
};
};
if (aggn > 8) {
let havesrc: bool = false;
if (rhs.kind == nkind.N_UN) {
if (rhs.op == tkind.TK_STAR) {
cgexpr(c, rhs.lhs);
emitline("\tMOVQ\tAX, SI\n");
havesrc = true;
};
};
if (!havesrc) { if (rhs.kind == 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: *node = defvartnode(c, rhs.str);
let aggisdef: bool = defvarstructinfo(c, rhs.str) != nil;
if (aggdtn != nil) {
if (aggdtn.kind == 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 == nkind.N_DOT) {
if (dotchainaddr(c, rhs, "SI")) {
havesrc = true;
};
}; };
if (!havesrc) { if (rhs.kind == nkind.N_INDEX) {
let base: *node = rhs.lhs;
let idx: *node = rhs.rhs;
let bu: *tinfo = nil;
if (base != nil) { bu = base.type_: *tinfo; };
bu = tichase(bu);
if (base != nil && base.kind == nkind.N_IDENT
&& bu != nil && bu.kind == 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 == nkind.N_DOT
|| base.kind == 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 == nkind.N_DOT) {
if (dotbaseaddr(c, base, "AX")) {
baseok = true;
};
} else {
let ab: *node = base.lhs;
let bidx: *node = base.rhs;
let abu: *tinfo = nil;
if (ab != nil) { abu = ab.type_: *tinfo; };
abu = tichase(abu);
if (ab != nil && ab.kind == nkind.N_IDENT
&& abu != nil && abu.kind == 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 == nkind.N_TRYUNW
|| rhs.kind == nkind.N_TRYPROP) {
if (rhs.lhs != nil) {
if (rhs.lhs.kind == 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: *tinfo = n.lhs.type_: *tinfo;
zti = tichase(zti);
if (zti != nil && zti.kind == tykind.TY_ARRAY) {
zsz = zti.size: i32;
} else { if (n.lhs.kind == nkind.N_TNAME) {
let szi: *structinfo = structlookupchain(c, n.lhs);
if (szi != nil) {
let ti: *tinfo = n.lhs.type_: *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: *node) void = {
let els: str = mklabel(c, "else");
let endl: str = mklabel(c, "end");
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
if (n.els != nil) { emitline(els); }
else { emitline(endl); };
emitline("\n");
if (n.body != nil) { cgstmt(c, n.body); };
if (n.els != nil) {
emitline("\tJMP\t"); emitline(endl); emitline("\n");
emitlabel(els);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
fn cgfor(c: *cgen, n: *node) void = {
// Match C cgen's label scheme: <fn>_loop_N for the top,
// <fn>_endloop_N for the post-body merge. No separate cont
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
// #138: `continue` in a 3-clause `for (init; cond; post)` must
// run the post-step before re-testing cond. Pre-fix the continue-
// target was `topl`, which SKIPPED the post-step → state never
// advanced → infinite loop. Allocate a dedicated `post` label
// only when there IS a post-step (`n.rhs != nil`); else keep
// continue → loop-top, byte-id with 1-clause for.
let conttgt: str = topl;
if (n.rhs != nil) { conttgt = mklabel(c, "post"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
emitlabel(topl);
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
c.loopcontbuf[c.looptop] = conttgt;
c.looptop += 1;
if (n.body != nil) { cgstmt(c, n.body); };
c.looptop -= 1;
if (n.rhs != nil) {
emitlabel(conttgt);
cgexpr(c, n.rhs);
};
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
// Tuple-destructure assign: `a, b = call();`. The call's tuple
// return lands in (AX, DX); push DX to free it, store AX into
// the first lvalue, then pop DX into the second. Mirrors
// cmd/w6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// as C — no fixture uses >2 today).
fn cgmassign(c: *cgen, n: *node) void = {
// #83: positional per-element destructure REASSIGN. Same cursor as
// cgmlet (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416), but the slots already exist
// (reassignment) so localfind them. wwstage has no checker, so each
// element's width comes from the called fn's return-type tuple
// element (N_TTUPLE param) walked in lockstep with the bindings; a
// slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes
// its register slot without storing (mirrors harec `_`). This bare-
// comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond
// Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
// #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 == 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 == nkind.N_TUPLE) { litrhs = true; }; };
let synthdecl: *node = nil;
if (litrhs) {
synthdecl = newnode(nkind.N_TTUPLE, n.rhs.file, n.rhs.line, n.rhs.col);
let dtail: *node = nil;
let lb0: *node = n.list;
for (lb0 != nil) {
let w: *node = newnode(nkind.N_TUPLE, n.rhs.file, n.rhs.line, n.rhs.col);
w.lhs = nil;
if (lb0.kind == 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: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *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: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let off: i32 = 0;
if (lb.kind == 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: *node = n.list;
let pt: *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: *node = nil;
let re: *node = nil;
if (litrhs) { dp = synthdecl.list; re = n.rhs.list; };
for (l != nil) {
let tn: *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: *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 == 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;
};
// Multi-let from a tuple-returning call: `let n, s = call();` or
// `let (n, s) = call();`. wwstage has no checker, so each binding's
// type is taken from its explicit annotation (l.lhs) when present
// or inferred from the called fn's return-type tuple element.
//
// Per the AX:DX:CX:R8 return convention (mirrors C cgen nkind.N_MLET):
// (scalar, scalar) — AX → l0, DX → l1.
// (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot
// as (.ptr, .len, .cap). Position-agnostic — the
// regs are routed by element type, not by AX/DX.
// str IS []u8 (24B): cap rides R8 (#1/Phase 3, task #5).
fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
// #83: positional per-element destructure let-binding. Same cursor
// as cgmassign (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416). wwstage has no checker, so each
// binding's type is its explicit annotation (l.lhs) when present,
// else the called fn's return-type tuple element (N_TTUPLE param)
// walked in lockstep. A slice/str rides its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
// #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 == 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 == nkind.N_IDENT) {
let rl: *local = localfindnode(c, rhs.str);
if (rl != nil) {
let rti: *tinfo = rl.tnode.type_: *tinfo;
rti = tichase(rti);
if (rti != nil) { if (rti.kind == tykind.TY_TUPLE) {
let srcoff: i32 = rl.off;
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = lb.lhs;
let isflt: bool = isfloattype(c, tn);
let eslot: i32 = tupeslotn(tn);
let esz: i32 = 8;
let eti: *tinfo = nil;
if (tn != nil) { eti = tn.type_: *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: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *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: *tinfo = pt2.lhs.type_: *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: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = l.lhs;
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
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: *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/slice-elt, 16 for str, default 8.
fn paramfieldsize(t: *node) i32 = {
if (t == nil) { return 8; };
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return primtypesize("str"): i32; };
// primsize-ok (#101/#109): 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; };
};
return 8;
};
// paramissigned — does this type need sign-extending on a sub-word
// (1/2/4B) load? Mirrors cstage's signed_field check via
// fieldissignedc (resolves TBANG / TENUM / alias chains).
fn paramissigned(c: *cgen, t: *node) bool = {
return fieldissignedc(c, t);
};
// cgforrange — lower `for (let x .. slice) body` (and the tuple-
// destructure cousin `for (let (a, b) .. slice) body`). The body is
// wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`.
// Each iteration computes the element address `s.ptr + i*esz` and
// either loads the whole element into the named local or pulls each
// tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE
// byte-for-byte (label names + labelseq consumption order).
fn cgforrange(c: *cgen, n: *node) void = {
let slc: *node = n.lhs;
let slclocal: *local = nil;
let slctn: *node = nil;
if (slc != nil) {
if (slc.kind == nkind.N_IDENT) {
slclocal = localfindnode(c, slc.str);
if (slclocal != nil) { slctn = slclocal.tnode; };
};
};
// Element type — peek through TSLICE/TARRAY for the tuple param walk.
let elemt: *node = nil;
if (slctn != nil) {
let sk: nkind = slctn.kind;
if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; };
if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; };
// str IS []u8 (F1: tystr.sub = tyu8). []u8 hands cgen a real
// u8 element node (slctn.lhs); a str scrutinee has none, so the
// loop var would register tnode=nil and read back as a wide
// MOVQ. Synthesise the u8 element off str.sub so the loop-var
// registration carries a u8 tnode and localloadop narrows the
// read-back to MOVZBQ on its own — aligning wwstage up to
// cstage, whose checker stamps the binding u8. Kind-gated so
// str's own type stays nominal.
if (sk == nkind.N_TNAME) {
if (streq(slctn.str, "str")) {
let sti: *tinfo = slctn.type_: *tinfo;
if (sti != nil) {
if (sti.sub != nil) {
let u8n: *node = newnode(nkind.N_TNAME, slctn.file, slctn.line, slctn.col);
u8n.str = "u8";
u8n.type_ = sti.sub: *void;
elemt = u8n;
};
};
};
};
};
// #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: *tinfo = nil;
if (slctn != nil) {
if (slctn.kind == nkind.N_TNAME) {
let st60: *tinfo = slctn.type_: *tinfo;
if (st60 != nil) {
if (st60.kind == tykind.TY_NAMED) { rti60 = tichase(st60); };
};
};
};
if (rti60 != nil && elemt == nil) {
if (rti60.sub != nil) {
let en60: *node = newnode(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: *tinfo = tichase(rti60.sub);
if (es60 != nil) { esz = es60.size: i32; };
};
if (elemt != nil) {
if (elemt.kind == nkind.N_TTUPLE) {
let total: i32 = 0;
let p: *node = elemt.list;
for (p != nil) {
total += paramfieldsize(p.lhs);
p = p.next;
};
esz = total;
};
};
// 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: *tinfo = slc.type_: *tinfo;
sti2 = tichase(sti2);
if (sti2 != nil) {
if (sti2.kind == tykind.TY_SLICE
|| sti2.kind == tykind.TY_STR
|| sti2.kind == tykind.TY_ARRAY) {
if (sti2.sub != nil) {
esz = sti2.sub.size: i32;
let en: *node = newnode(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 != nkind.N_IDENT) {
let stu70: *tinfo = slc.type_: *tinfo;
stu70 = tichase(stu70);
let arr70: bool = false;
if (stu70 != nil) {
if (stu70.kind == 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 == nkind.N_UN) {
if (slc.op == 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 == nkind.N_IDENT) {
if (localfindnode(c, slc.str) == nil) {
let isglob: bool = isletvar(c, slc.str);
if (!isglob) {
let gdtn: *node = defvartnode(c, slc.str);
if (gdtn != nil) {
if (gdtn.kind == 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: *node = nil;
if (elemt != nil) {
if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; };
};
let field_off: i32 = 0;
let m: *node = n.list;
for (m != nil) {
if (nbinds >= 8) { m = nil; }
else {
let fsz: i32 = 8;
let signf: bool = false;
// tp walks the N_TPARAM wrapper chain; tpt is the
// actual element type AST.
let tpt: *node = nil;
if (tp != nil) { tpt = tp.lhs; };
if (tpt != nil) {
fsz = paramfieldsize(tpt);
signf = paramissigned(c, tpt);
};
let slot_sz: i32 = fsz;
if (slot_sz < 8) { slot_sz = 8; };
bind_sz[nbinds] = fsz;
bind_foff[nbinds] = field_off;
bind_signed[nbinds] = signf;
let bnm: str = m.str;
if (bnm.len > 0) {
bind_off[nbinds] = localadd(c, bnm, slot_sz, tpt);
} else {
bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tpt);
};
field_off += fsz;
nbinds += 1;
if (tp != nil) { tp = tp.next; };
m = m.next;
};
};
} else {
let slot_sz: i32 = esz;
if (slot_sz < 8) { slot_sz = 8; };
bind_sz[0] = esz;
bind_foff[0] = 0;
// Single-binding signed-narrow detection: mirror C which
// reads `u->sub->kind` for the elem type.
bind_signed[0] = false;
if (elemt != nil) {
bind_signed[0] = paramissigned(c, elemt);
};
if (n.str.len > 0) {
// Register with elem tnode so x.field on a loop
// var resolves through the standard local-typed
// path instead of falling into the SB fallback.
bind_off[0] = localadd(c, n.str, slot_sz, elemt);
} else {
bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt);
};
nbinds = 1;
};
// init: ioff(BP) = 0
emitline("\tMOVQ\t$0, ");
emitoff(ioff: i64);
emitline("(BP)\n");
// loff(BP) = len
let isarr: bool = false;
let isslicestr: bool = false;
if (slctn != nil) {
let tk: nkind = slctn.kind;
if (tk == nkind.N_TSLICE) { isslicestr = true; };
if (tk == nkind.N_TARRAY) { isarr = true; };
if (tk == nkind.N_TNAME) {
if (streq(slctn.str, "str")) { isslicestr = true; };
};
};
// #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 == tykind.TY_ARRAY) { isarr = true; };
if (rti60.kind == tykind.TY_SLICE) { isslicestr = true; };
if (rti60.kind == tykind.TY_STR) { isslicestr = true; };
};
if (isslicestr) {
if (slc.kind == nkind.N_IDENT) {
if (slclocal != nil) {
emitline("\tMOVQ\t");
emitoff((slclocal.off + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(loff: i64);
emitline("(BP)\n");
};
};
} else { if (isarr) {
let alen: i64 = 0i64;
if (slctn.rhs != nil) {
if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; };
};
// #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");
c.loopcontbuf[c.looptop] = rpost;
c.loopendbuf[c.looptop] = endl;
c.looptop += 1;
emitlabel(loopl);
emitline("\tMOVQ\t");
emitoff(ioff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff(loff: i64);
emitline("(BP), BX\n");
emitline("\tCMPQ\tBX, AX\n");
emitline("\tJGE\t"); emitline(naturall); emitline("\n");
// BX = base + i*esz
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (slc.kind == nkind.N_IDENT) {
if (slclocal != nil) {
if (isarr) {
emitline("\tLEAQ\t");
emitoff(slclocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(slclocal.off: i64);
emitline("(BP), BX\n");
};
};
} 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) {
let op: str = loadopsz(bind_signed[b], bind_sz[b]);
emitline("\t");
emitline(op);
emitline("\t");
emitoff(bind_foff[b]: i64);
emitline("(BX), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(bind_off[b]: i64);
emitline("(BP)\n");
b += 1;
};
};
if (n.body != nil) { cgstmt(c, n.body); };
c.looptop -= 1;
emitlabel(rpost);
emitline("\tADDQ\t$1, ");
emitoff(ioff: i64);
emitline("(BP)\n");
emitline("\tJMP\t"); emitline(loopl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
// cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to
// a chain of compares against the scrutinee. Scrutinee lands in a
// fresh 8B local slot so case bodies can spill SP without losing it.
// Cases are tried top-to-bottom; the `case:` arm with no exprs is the
// default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c
// N_SWITCH: same labelseq consumption order so labels match byte-for-
// byte.
fn cgswitch(c: *cgen, n: *node) void = {
let swname: str = mkscratchname(c, "sw");
let sloff: i32 = localalloc(c, swname, 8, nil);
if (n.lhs != nil) { cgexpr(c, n.lhs); };
emitline("\tMOVQ\tAX, ");
emitoff(sloff: i64);
emitline("(BP)\n");
let endl: str = mklabel(c, "swend");
let defcase: *node = nil;
let cs: *node = n.list;
for (cs != nil) {
if (cs.list == nil) {
defcase = cs;
cs = cs.next;
continue;
};
let body: str = mklabel(c, "swcase");
let nxt: str = mklabel(c, "swnext");
let e: *node = cs.list;
for (e != nil) {
cgexpr(c, e);
emitline("\tMOVQ\t");
emitoff(sloff: i64);
emitline("(BP), BX\n");
emitline("\tCMPQ\tBX, AX\n");
emitline("\tJE\t");
emitline(body);
emitline("\n");
e = e.next;
};
emitline("\tJMP\t");
emitline(nxt);
emitline("\n");
emitlabel(body);
if (cs.body != nil) { cgstmt(c, cs.body); };
emitline("\tJMP\t");
emitline(endl);
emitline("\n");
emitlabel(nxt);
cs = cs.next;
};
if (defcase != nil) {
if (defcase.body != nil) { cgstmt(c, defcase.body); };
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
fn cgbreak(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopendbuf[c.looptop - 1];
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgcontinue(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopcontbuf[c.looptop - 1];
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
};
c.lastwasreturn = 0;
return;
};