wcc: multi-float tuple return via SSE cursor (#164, #107)

A multi-float tuple return mis-routed: SEND pushed a stale AX leaving the
float stranded in X0, while RECV (#105) read every float from X0 — so a
(f64,f64) return collided both floats. Add an SSE cursor [X0,X1] parallel to
the GP cursor [AX,DX,CX,R8], placing each element by its SysV class +
within-class index (ref/qbe/amd64/sysv.c retr), symmetric send/recv across
both stages, via a generic tuple_store/tupstore+tupsse helper that #171 will
reuse for struct-return convergence. (f64,f64,f64) = 3 SSE eightbytes exceeds
the 2-register cap and now fails loud (rule 7) rather than colliding.

Unifying the 16B and 32B whole-tuple-single-var branches onto the dual cursor
was required for f64+str coexistence; it also fixes a latent str-first
single-var bug (the old 32B branch read .ptr from DX while the send placed it
in AX). No str-first or 32B tuple exists in-tree, so integer paths stay
byte-identical (990-997 green).
This commit is contained in:
2026-05-27 21:01:25 +09:00
parent 028109513e
commit 153c7b3b46
5 changed files with 1102 additions and 572 deletions

View File

@@ -24189,6 +24189,17 @@ fn tupreg(i: i32) str = {
return "R8";
};
// #164 (#107): SSE half of the SysV dual register-class return. A float
// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the
// INTEGER row tupreg — a float lands in the next XMM regardless of its
// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},
// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage
// tuple_sse_seq (cmd/w6c/cgen.c).
fn tupsse(i: i32) str = {
if (i == 0) { return "X0"; };
return "X1";
};
fn tupebytes(wide: bool) i32 = {
if (wide) { return (tyslicesize() / 8i64): i32; };
return 1;
@@ -24229,31 +24240,34 @@ fn rettupleof(c: *cgen, rhs: *node) *node = {
// tupstore — store the tuple element at register-cursor `cur` into the
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
// cursor registers; a scalar stores 1 word. Byte-identical to the cstage
// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c).
fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a
// scalar stores 1 INTEGER word. The caller owns the dual cursor
// (validated + advanced). Byte-identical to the cstage tuple_store
// (cmd/w6c/cgen.c).
fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = {
if (wide) {
emitline("\tMOVQ\t");
emitline(tupreg(cur + 0));
emitline(tupreg(gpcur + 0));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 1));
emitline(tupreg(gpcur + 1));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 2));
emitline(tupreg(gpcur + 2));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
return;
};
// #105: an f64/f32 element rides X0 (the SSE return reg), not its
// integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and
// the FACE-Z field read sees it. X0 survives the reg->mem stores.
// Single-float scope; multi-float collides on X0 at RETURN (#107).
// #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
@@ -24286,13 +24300,15 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitline("\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
emitline("\tMOVQ\t");
emitline(tupreg(cur));
emitline(tupreg(gpcur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
@@ -24302,49 +24318,98 @@ fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// #83: positional per-element register-return (harec
// create_unpack_bindings, ref/harec/src/check.c:1354-1416). Each
// element rides consecutive eightbytes over [AX,DX,CX,R8]
// (tupreg); a slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8), cgexpr leaving it in (AX,BX,CX); a
// scalar rides 1 word in AX. Spill each element L->R, then pop
// into the cursor's registers in reverse so positional slot i
// lands in tupreg(i) — (scalar,str) keeps the historical AX +
// DX,CX,R8. The SAME cursor drives the receive sites. Over-
// capacity is a loud stop (return-ABI #10), never a silent drop.
// #83 / #164 (#107): positional register-return over a SysV
// dual class cursor (harec create_unpack_bindings, ref/harec/src/
// check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 =
// tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8]
// (tupreg) — a slice/str its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a
// scalar 1 word in AX. Integer words spill L->R to the stack and
// pop into the INTEGER cursor in reverse so positional slot i
// lands in tupreg(i) (byte-id with #83 when no float is present).
// Each float must spill X0 to @tupfscr as we walk, since a later
// element's cgexpr clobbers X0; after the integer pops the saved
// floats reload into X0/X1 by SSE index — INDEPENDENT of the
// INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows
// loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME
// class split drives the receive sites.
if (rhs.kind == nkind.N_TUPLE) {
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let e: *node = rhs.list;
for (e != nil) {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
total = total + tupebytes(wide);
e = e.next;
};
if (total > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage
// uses fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple return exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
e = rhs.list;
for (e != nil) {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
cgexpr(c, e);
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
if (wide) {
emitline("\tPUSHQ\tBX\n"); // .len
emitline("\tPUSHQ\tCX\n"); // .cap
if (isfloattype(c, e)) {
ssecount = ssecount + 1;
} else {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
gptotal = gptotal + tupebytes(wide);
};
e = e.next;
};
let i: i32 = total - 1;
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage
// uses fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (ssecount > ssecap) {
let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let fscr: i32 = 0;
if (ssecount > 0) {
fscr = localadd(c, "@tupfscr", ssecap * 8, nil);
};
let sseidx: i32 = 0;
e = rhs.list;
for (e != nil) {
let isflt: bool = isfloattype(c, e);
cgexpr(c, e);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, e)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((fscr + sseidx * 8): i64);
emitline("(BP)\n");
sseidx = sseidx + 1;
} else {
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
if (nodeisstr(c, e) || nodeisslice(c, e)) {
emitline("\tPUSHQ\tBX\n"); // .len
emitline("\tPUSHQ\tCX\n"); // .cap
};
};
e = e.next;
};
let i: i32 = gptotal - 1;
for (i >= 0) {
emitline("\tPOPQ\t");
emitline(tupreg(i));
emitline("\n");
i = i - 1;
};
let j: i32 = 0;
e = rhs.list;
for (e != nil) {
if (isfloattype(c, e)) {
let mov: str = "MOVSD";
if (isf32type(c, e)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((fscr + j * 8): i64);
emitline("(BP), ");
emitline(tupsse(j));
emitline("\n");
j = j + 1;
};
e = e.next;
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -24943,12 +25008,15 @@ fn cglet(c: *cgen, n: *node) void = {
return;
};
// 32B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len,
// R8 = str.cap. Layout is positional (str takes 24B at its
// position), so we route each register to the slot dictated by
// element type, not by AX/DX position. str IS []u8 (24B) → 32B
// tuple (#1/Phase 3, task #5).
// `let t: (str, scalar) = call()` (#105 / #164/#107). Each
// element rides its SysV class — a float its SSE cursor reg
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
// (tupreg), a slice/str its 3-word {ptr,len,cap} header over
// consecutive INTEGER cursor regs — on INDEPENDENT counters.
// tupstore routes each element from its real class into its
// positional slot (eoff steps by the element's slot size: a
// slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple
// (#1/Phase 3, task #5). Mirror of the cstage unified branch.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TTUPLE) {
let p0: *node = n.lhs.list;
@@ -24958,38 +25026,36 @@ fn cglet(c: *cgen, n: *node) void = {
let p1t: *node = nil;
if (p0 != nil) { p0t = p0.lhs; };
if (p1 != nil) { p1t = p1.lhs; };
let s0_is_str: bool = isstrtype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t);
let s0_is_str: bool = isstrtype(c, p0t)
|| isslicetype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t)
|| isslicetype(c, p1t);
if (p0 != nil) {
if (p1 != nil) {
if (s0_is_str != s1_is_str) {
cgexpr(c, rhs);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
let q: *node = n.lhs.list;
for (q != nil) {
let qt: *node = q.lhs;
let isflt: bool = isfloattype(c, qt);
let wide: bool = isstrtype(c, qt)
|| isslicetype(c, qt);
tupstore(c, gpcur, ssecur,
off + eoff, wide, qt);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
if (wide) {
eoff = eoff + (tyslicesize(): i32);
} else {
eoff = eoff + 8;
};
q = q.next;
};
c.lastwasreturn = 0;
return;
@@ -24998,54 +25064,35 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
};
// 16B tuple init from a function call. An integer word rides
// its integer cursor reg (AX, DX); a single f64/f32 word rides
// X0, the SSE return reg — the RETURN leaves the float in X0
// and pushes garbage through that word's integer slot, so a
// blanket MOVQ-from-integer spill stores garbage and the #103-
// FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill
// each word from its real class. Multi-float tuples collide on
// X0 at the RETURN (#107), out of scope. Mirror of cstage
// cgen.c. Without this branch a 16B tuple receive (any element
// mix) fell to the generic single-word store below and dropped
// word1 — silent loss of t.1 (#102).
// 16B tuple init from a function call (#105 / #164/#107). Each
// eightbyte rides its SysV class: a float its SSE cursor reg
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
// (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves
// floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ
// spill would store garbage where a float rode and the #103-
// FACE-Z field read (MOVSD-from-slot) would see it. tupstore
// routes each word from its real class; the same split drives
// the destructure / reassign sites. Without this branch a 16B
// tuple receive fell to the generic single-word store below and
// dropped word1 — silent loss of t.1 (#102).
let rt16: *node = rettupleof(c, rhs);
if (rt16 != nil && sz == 16) {
let q0: *node = rt16.list;
let q1: *node = nil;
if (q0 != nil) { q1 = q0.next; };
let q0t: *node = nil;
let q1t: *node = nil;
if (q0 != nil) { q0t = q0.lhs; };
if (q1 != nil) { q1t = q1.lhs; };
let e0f: bool = isfloattype(c, q0t);
let e1f: bool = isfloattype(c, q1t);
cgexpr(c, rhs);
if (e0f) {
let mov: str = "MOVSD";
if (isf32type(c, q0t)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (e1f) {
let mov: str = "MOVSD";
if (isf32type(c, q1t)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
let q: *node = rt16.list;
for (q != nil) {
let qt: *node = q.lhs;
let isflt: bool = isfloattype(c, qt);
tupstore(c, gpcur, ssecur, off + eoff, false, qt);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + 1;
};
eoff = eoff + 8;
q = q.next;
};
c.lastwasreturn = 0;
return;
@@ -25517,40 +25564,59 @@ fn cgmassign(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
let l: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = nil;
if (pt != nil) { tn = pt.lhs; };
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
gptotal = gptotal + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};
if (total > 4) { // AX,DX,CX,R8 capacity
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
// fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
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 cur: i32 = 0;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
l = n.list;
pt = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = nil;
if (pt != nil) { tn = pt.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let off: i32 = 0;
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
// harec `_` (off==0): skip the store but CONSUME the cursor
// slot so the next element stays aligned.
if (off != 0) {
tupstore(c, cur, off, wide, tn);
tupstore(c, gpcur, ssecur, off, wide, tn);
};
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
@@ -25585,7 +25651,9 @@ fn cgmlet(c: *cgen, n: *node) void = {
cgexpr(c, rhs);
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
let l: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
@@ -25594,20 +25662,30 @@ fn cgmlet(c: *cgen, n: *node) void = {
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
gptotal = gptotal + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};
if (total > 4) { // AX,DX,CX,R8 capacity
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
// fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
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 cur: i32 = 0;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
l = n.list;
pt = nil;
if (rettuple != nil) { pt = rettuple.list; };
@@ -25616,12 +25694,17 @@ fn cgmlet(c: *cgen, n: *node) void = {
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let sz: i32 = 8;
if (wide) { sz = tyslicesize(): i32; };
let off: i32 = localadd(c, l.str, sz, tn);
tupstore(c, cur, off, wide, tn);
cur = cur + tupebytes(wide);
tupstore(c, gpcur, ssecur, off, wide, tn);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};

View File

@@ -121,6 +121,17 @@ fn tupreg(i: i32) str = {
return "R8";
};
// #164 (#107): SSE half of the SysV dual register-class return. A float
// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the
// INTEGER row tupreg — a float lands in the next XMM regardless of its
// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},
// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage
// tuple_sse_seq (cmd/w6c/cgen.c).
fn tupsse(i: i32) str = {
if (i == 0) { return "X0"; };
return "X1";
};
fn tupebytes(wide: bool) i32 = {
if (wide) { return (tyslicesize() / 8i64): i32; };
return 1;
@@ -161,31 +172,34 @@ fn rettupleof(c: *cgen, rhs: *node) *node = {
// tupstore — store the tuple element at register-cursor `cur` into the
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
// cursor registers; a scalar stores 1 word. Byte-identical to the cstage
// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c).
fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a
// scalar stores 1 INTEGER word. The caller owns the dual cursor
// (validated + advanced). Byte-identical to the cstage tuple_store
// (cmd/w6c/cgen.c).
fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = {
if (wide) {
emitline("\tMOVQ\t");
emitline(tupreg(cur + 0));
emitline(tupreg(gpcur + 0));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 1));
emitline(tupreg(gpcur + 1));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 2));
emitline(tupreg(gpcur + 2));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
return;
};
// #105: an f64/f32 element rides X0 (the SSE return reg), not its
// integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and
// the FACE-Z field read sees it. X0 survives the reg->mem stores.
// Single-float scope; multi-float collides on X0 at RETURN (#107).
// #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
@@ -218,13 +232,15 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitline("\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
emitline("\tMOVQ\t");
emitline(tupreg(cur));
emitline(tupreg(gpcur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
@@ -234,49 +250,98 @@ fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// #83: positional per-element register-return (harec
// create_unpack_bindings, ref/harec/src/check.c:1354-1416). Each
// element rides consecutive eightbytes over [AX,DX,CX,R8]
// (tupreg); a slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8), cgexpr leaving it in (AX,BX,CX); a
// scalar rides 1 word in AX. Spill each element L->R, then pop
// into the cursor's registers in reverse so positional slot i
// lands in tupreg(i) — (scalar,str) keeps the historical AX +
// DX,CX,R8. The SAME cursor drives the receive sites. Over-
// capacity is a loud stop (return-ABI #10), never a silent drop.
// #83 / #164 (#107): positional register-return over a SysV
// dual class cursor (harec create_unpack_bindings, ref/harec/src/
// check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 =
// tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8]
// (tupreg) — a slice/str its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a
// scalar 1 word in AX. Integer words spill L->R to the stack and
// pop into the INTEGER cursor in reverse so positional slot i
// lands in tupreg(i) (byte-id with #83 when no float is present).
// Each float must spill X0 to @tupfscr as we walk, since a later
// element's cgexpr clobbers X0; after the integer pops the saved
// floats reload into X0/X1 by SSE index — INDEPENDENT of the
// INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows
// loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME
// class split drives the receive sites.
if (rhs.kind == nkind.N_TUPLE) {
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let e: *node = rhs.list;
for (e != nil) {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
total = total + tupebytes(wide);
e = e.next;
};
if (total > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage
// uses fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple return exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
e = rhs.list;
for (e != nil) {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
cgexpr(c, e);
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
if (wide) {
emitline("\tPUSHQ\tBX\n"); // .len
emitline("\tPUSHQ\tCX\n"); // .cap
if (isfloattype(c, e)) {
ssecount = ssecount + 1;
} else {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
gptotal = gptotal + tupebytes(wide);
};
e = e.next;
};
let i: i32 = total - 1;
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage
// uses fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (ssecount > ssecap) {
let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let fscr: i32 = 0;
if (ssecount > 0) {
fscr = localadd(c, "@tupfscr", ssecap * 8, nil);
};
let sseidx: i32 = 0;
e = rhs.list;
for (e != nil) {
let isflt: bool = isfloattype(c, e);
cgexpr(c, e);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, e)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((fscr + sseidx * 8): i64);
emitline("(BP)\n");
sseidx = sseidx + 1;
} else {
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
if (nodeisstr(c, e) || nodeisslice(c, e)) {
emitline("\tPUSHQ\tBX\n"); // .len
emitline("\tPUSHQ\tCX\n"); // .cap
};
};
e = e.next;
};
let i: i32 = gptotal - 1;
for (i >= 0) {
emitline("\tPOPQ\t");
emitline(tupreg(i));
emitline("\n");
i = i - 1;
};
let j: i32 = 0;
e = rhs.list;
for (e != nil) {
if (isfloattype(c, e)) {
let mov: str = "MOVSD";
if (isf32type(c, e)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((fscr + j * 8): i64);
emitline("(BP), ");
emitline(tupsse(j));
emitline("\n");
j = j + 1;
};
e = e.next;
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -875,12 +940,15 @@ fn cglet(c: *cgen, n: *node) void = {
return;
};
// 32B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len,
// R8 = str.cap. Layout is positional (str takes 24B at its
// position), so we route each register to the slot dictated by
// element type, not by AX/DX position. str IS []u8 (24B) → 32B
// tuple (#1/Phase 3, task #5).
// `let t: (str, scalar) = call()` (#105 / #164/#107). Each
// element rides its SysV class — a float its SSE cursor reg
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
// (tupreg), a slice/str its 3-word {ptr,len,cap} header over
// consecutive INTEGER cursor regs — on INDEPENDENT counters.
// tupstore routes each element from its real class into its
// positional slot (eoff steps by the element's slot size: a
// slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple
// (#1/Phase 3, task #5). Mirror of the cstage unified branch.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TTUPLE) {
let p0: *node = n.lhs.list;
@@ -890,38 +958,36 @@ fn cglet(c: *cgen, n: *node) void = {
let p1t: *node = nil;
if (p0 != nil) { p0t = p0.lhs; };
if (p1 != nil) { p1t = p1.lhs; };
let s0_is_str: bool = isstrtype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t);
let s0_is_str: bool = isstrtype(c, p0t)
|| isslicetype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t)
|| isslicetype(c, p1t);
if (p0 != nil) {
if (p1 != nil) {
if (s0_is_str != s1_is_str) {
cgexpr(c, rhs);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
let q: *node = n.lhs.list;
for (q != nil) {
let qt: *node = q.lhs;
let isflt: bool = isfloattype(c, qt);
let wide: bool = isstrtype(c, qt)
|| isslicetype(c, qt);
tupstore(c, gpcur, ssecur,
off + eoff, wide, qt);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
if (wide) {
eoff = eoff + (tyslicesize(): i32);
} else {
eoff = eoff + 8;
};
q = q.next;
};
c.lastwasreturn = 0;
return;
@@ -930,54 +996,35 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
};
// 16B tuple init from a function call. An integer word rides
// its integer cursor reg (AX, DX); a single f64/f32 word rides
// X0, the SSE return reg — the RETURN leaves the float in X0
// and pushes garbage through that word's integer slot, so a
// blanket MOVQ-from-integer spill stores garbage and the #103-
// FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill
// each word from its real class. Multi-float tuples collide on
// X0 at the RETURN (#107), out of scope. Mirror of cstage
// cgen.c. Without this branch a 16B tuple receive (any element
// mix) fell to the generic single-word store below and dropped
// word1 — silent loss of t.1 (#102).
// 16B tuple init from a function call (#105 / #164/#107). Each
// eightbyte rides its SysV class: a float its SSE cursor reg
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
// (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves
// floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ
// spill would store garbage where a float rode and the #103-
// FACE-Z field read (MOVSD-from-slot) would see it. tupstore
// routes each word from its real class; the same split drives
// the destructure / reassign sites. Without this branch a 16B
// tuple receive fell to the generic single-word store below and
// dropped word1 — silent loss of t.1 (#102).
let rt16: *node = rettupleof(c, rhs);
if (rt16 != nil && sz == 16) {
let q0: *node = rt16.list;
let q1: *node = nil;
if (q0 != nil) { q1 = q0.next; };
let q0t: *node = nil;
let q1t: *node = nil;
if (q0 != nil) { q0t = q0.lhs; };
if (q1 != nil) { q1t = q1.lhs; };
let e0f: bool = isfloattype(c, q0t);
let e1f: bool = isfloattype(c, q1t);
cgexpr(c, rhs);
if (e0f) {
let mov: str = "MOVSD";
if (isf32type(c, q0t)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (e1f) {
let mov: str = "MOVSD";
if (isf32type(c, q1t)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
let q: *node = rt16.list;
for (q != nil) {
let qt: *node = q.lhs;
let isflt: bool = isfloattype(c, qt);
tupstore(c, gpcur, ssecur, off + eoff, false, qt);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + 1;
};
eoff = eoff + 8;
q = q.next;
};
c.lastwasreturn = 0;
return;
@@ -1449,40 +1496,59 @@ fn cgmassign(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
let l: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = nil;
if (pt != nil) { tn = pt.lhs; };
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
gptotal = gptotal + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};
if (total > 4) { // AX,DX,CX,R8 capacity
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
// fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
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 cur: i32 = 0;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
l = n.list;
pt = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = nil;
if (pt != nil) { tn = pt.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let off: i32 = 0;
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
// harec `_` (off==0): skip the store but CONSUME the cursor
// slot so the next element stays aligned.
if (off != 0) {
tupstore(c, cur, off, wide, tn);
tupstore(c, gpcur, ssecur, off, wide, tn);
};
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
@@ -1517,7 +1583,9 @@ fn cgmlet(c: *cgen, n: *node) void = {
cgexpr(c, rhs);
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
let l: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
@@ -1526,20 +1594,30 @@ fn cgmlet(c: *cgen, n: *node) void = {
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
gptotal = gptotal + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};
if (total > 4) { // AX,DX,CX,R8 capacity
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
// fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
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 cur: i32 = 0;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
l = n.list;
pt = nil;
if (rettuple != nil) { pt = rettuple.list; };
@@ -1548,12 +1626,17 @@ fn cgmlet(c: *cgen, n: *node) void = {
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let sz: i32 = 8;
if (wide) { sz = tyslicesize(): i32; };
let off: i32 = localadd(c, l.str, sz, tn);
tupstore(c, cur, off, wide, tn);
cur = cur + tupebytes(wide);
tupstore(c, gpcur, ssecur, off, wide, tn);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};

View File

@@ -24189,6 +24189,17 @@ fn tupreg(i: i32) str = {
return "R8";
};
// #164 (#107): SSE half of the SysV dual register-class return. A float
// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the
// INTEGER row tupreg — a float lands in the next XMM regardless of its
// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},
// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage
// tuple_sse_seq (cmd/w6c/cgen.c).
fn tupsse(i: i32) str = {
if (i == 0) { return "X0"; };
return "X1";
};
fn tupebytes(wide: bool) i32 = {
if (wide) { return (tyslicesize() / 8i64): i32; };
return 1;
@@ -24229,31 +24240,34 @@ fn rettupleof(c: *cgen, rhs: *node) *node = {
// tupstore — store the tuple element at register-cursor `cur` into the
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
// cursor registers; a scalar stores 1 word. Byte-identical to the cstage
// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c).
fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a
// scalar stores 1 INTEGER word. The caller owns the dual cursor
// (validated + advanced). Byte-identical to the cstage tuple_store
// (cmd/w6c/cgen.c).
fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = {
if (wide) {
emitline("\tMOVQ\t");
emitline(tupreg(cur + 0));
emitline(tupreg(gpcur + 0));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 1));
emitline(tupreg(gpcur + 1));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 2));
emitline(tupreg(gpcur + 2));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
return;
};
// #105: an f64/f32 element rides X0 (the SSE return reg), not its
// integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and
// the FACE-Z field read sees it. X0 survives the reg->mem stores.
// Single-float scope; multi-float collides on X0 at RETURN (#107).
// #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
@@ -24286,13 +24300,15 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitline("\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
emitline("\tMOVQ\t");
emitline(tupreg(cur));
emitline(tupreg(gpcur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
@@ -24302,49 +24318,98 @@ fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// #83: positional per-element register-return (harec
// create_unpack_bindings, ref/harec/src/check.c:1354-1416). Each
// element rides consecutive eightbytes over [AX,DX,CX,R8]
// (tupreg); a slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8), cgexpr leaving it in (AX,BX,CX); a
// scalar rides 1 word in AX. Spill each element L->R, then pop
// into the cursor's registers in reverse so positional slot i
// lands in tupreg(i) — (scalar,str) keeps the historical AX +
// DX,CX,R8. The SAME cursor drives the receive sites. Over-
// capacity is a loud stop (return-ABI #10), never a silent drop.
// #83 / #164 (#107): positional register-return over a SysV
// dual class cursor (harec create_unpack_bindings, ref/harec/src/
// check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 =
// tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8]
// (tupreg) — a slice/str its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a
// scalar 1 word in AX. Integer words spill L->R to the stack and
// pop into the INTEGER cursor in reverse so positional slot i
// lands in tupreg(i) (byte-id with #83 when no float is present).
// Each float must spill X0 to @tupfscr as we walk, since a later
// element's cgexpr clobbers X0; after the integer pops the saved
// floats reload into X0/X1 by SSE index — INDEPENDENT of the
// INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows
// loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME
// class split drives the receive sites.
if (rhs.kind == nkind.N_TUPLE) {
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let e: *node = rhs.list;
for (e != nil) {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
total = total + tupebytes(wide);
e = e.next;
};
if (total > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage
// uses fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple return exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
e = rhs.list;
for (e != nil) {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
cgexpr(c, e);
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
if (wide) {
emitline("\tPUSHQ\tBX\n"); // .len
emitline("\tPUSHQ\tCX\n"); // .cap
if (isfloattype(c, e)) {
ssecount = ssecount + 1;
} else {
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
gptotal = gptotal + tupebytes(wide);
};
e = e.next;
};
let i: i32 = total - 1;
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage
// uses fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (ssecount > ssecap) {
let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let fscr: i32 = 0;
if (ssecount > 0) {
fscr = localadd(c, "@tupfscr", ssecap * 8, nil);
};
let sseidx: i32 = 0;
e = rhs.list;
for (e != nil) {
let isflt: bool = isfloattype(c, e);
cgexpr(c, e);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, e)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((fscr + sseidx * 8): i64);
emitline("(BP)\n");
sseidx = sseidx + 1;
} else {
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
if (nodeisstr(c, e) || nodeisslice(c, e)) {
emitline("\tPUSHQ\tBX\n"); // .len
emitline("\tPUSHQ\tCX\n"); // .cap
};
};
e = e.next;
};
let i: i32 = gptotal - 1;
for (i >= 0) {
emitline("\tPOPQ\t");
emitline(tupreg(i));
emitline("\n");
i = i - 1;
};
let j: i32 = 0;
e = rhs.list;
for (e != nil) {
if (isfloattype(c, e)) {
let mov: str = "MOVSD";
if (isf32type(c, e)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((fscr + j * 8): i64);
emitline("(BP), ");
emitline(tupsse(j));
emitline("\n");
j = j + 1;
};
e = e.next;
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -24943,12 +25008,15 @@ fn cglet(c: *cgen, n: *node) void = {
return;
};
// 32B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len,
// R8 = str.cap. Layout is positional (str takes 24B at its
// position), so we route each register to the slot dictated by
// element type, not by AX/DX position. str IS []u8 (24B) → 32B
// tuple (#1/Phase 3, task #5).
// `let t: (str, scalar) = call()` (#105 / #164/#107). Each
// element rides its SysV class — a float its SSE cursor reg
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
// (tupreg), a slice/str its 3-word {ptr,len,cap} header over
// consecutive INTEGER cursor regs — on INDEPENDENT counters.
// tupstore routes each element from its real class into its
// positional slot (eoff steps by the element's slot size: a
// slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple
// (#1/Phase 3, task #5). Mirror of the cstage unified branch.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TTUPLE) {
let p0: *node = n.lhs.list;
@@ -24958,38 +25026,36 @@ fn cglet(c: *cgen, n: *node) void = {
let p1t: *node = nil;
if (p0 != nil) { p0t = p0.lhs; };
if (p1 != nil) { p1t = p1.lhs; };
let s0_is_str: bool = isstrtype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t);
let s0_is_str: bool = isstrtype(c, p0t)
|| isslicetype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t)
|| isslicetype(c, p1t);
if (p0 != nil) {
if (p1 != nil) {
if (s0_is_str != s1_is_str) {
cgexpr(c, rhs);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
let q: *node = n.lhs.list;
for (q != nil) {
let qt: *node = q.lhs;
let isflt: bool = isfloattype(c, qt);
let wide: bool = isstrtype(c, qt)
|| isslicetype(c, qt);
tupstore(c, gpcur, ssecur,
off + eoff, wide, qt);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
if (wide) {
eoff = eoff + (tyslicesize(): i32);
} else {
eoff = eoff + 8;
};
q = q.next;
};
c.lastwasreturn = 0;
return;
@@ -24998,54 +25064,35 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
};
// 16B tuple init from a function call. An integer word rides
// its integer cursor reg (AX, DX); a single f64/f32 word rides
// X0, the SSE return reg — the RETURN leaves the float in X0
// and pushes garbage through that word's integer slot, so a
// blanket MOVQ-from-integer spill stores garbage and the #103-
// FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill
// each word from its real class. Multi-float tuples collide on
// X0 at the RETURN (#107), out of scope. Mirror of cstage
// cgen.c. Without this branch a 16B tuple receive (any element
// mix) fell to the generic single-word store below and dropped
// word1 — silent loss of t.1 (#102).
// 16B tuple init from a function call (#105 / #164/#107). Each
// eightbyte rides its SysV class: a float its SSE cursor reg
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
// (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves
// floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ
// spill would store garbage where a float rode and the #103-
// FACE-Z field read (MOVSD-from-slot) would see it. tupstore
// routes each word from its real class; the same split drives
// the destructure / reassign sites. Without this branch a 16B
// tuple receive fell to the generic single-word store below and
// dropped word1 — silent loss of t.1 (#102).
let rt16: *node = rettupleof(c, rhs);
if (rt16 != nil && sz == 16) {
let q0: *node = rt16.list;
let q1: *node = nil;
if (q0 != nil) { q1 = q0.next; };
let q0t: *node = nil;
let q1t: *node = nil;
if (q0 != nil) { q0t = q0.lhs; };
if (q1 != nil) { q1t = q1.lhs; };
let e0f: bool = isfloattype(c, q0t);
let e1f: bool = isfloattype(c, q1t);
cgexpr(c, rhs);
if (e0f) {
let mov: str = "MOVSD";
if (isf32type(c, q0t)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (e1f) {
let mov: str = "MOVSD";
if (isf32type(c, q1t)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
let q: *node = rt16.list;
for (q != nil) {
let qt: *node = q.lhs;
let isflt: bool = isfloattype(c, qt);
tupstore(c, gpcur, ssecur, off + eoff, false, qt);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + 1;
};
eoff = eoff + 8;
q = q.next;
};
c.lastwasreturn = 0;
return;
@@ -25517,40 +25564,59 @@ fn cgmassign(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
let l: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = nil;
if (pt != nil) { tn = pt.lhs; };
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
gptotal = gptotal + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};
if (total > 4) { // AX,DX,CX,R8 capacity
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
// fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
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 cur: i32 = 0;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
l = n.list;
pt = nil;
if (rettuple != nil) { pt = rettuple.list; };
for (l != nil) {
let tn: *node = nil;
if (pt != nil) { tn = pt.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let off: i32 = 0;
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
// harec `_` (off==0): skip the store but CONSUME the cursor
// slot so the next element stays aligned.
if (off != 0) {
tupstore(c, cur, off, wide, tn);
tupstore(c, gpcur, ssecur, off, wide, tn);
};
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
@@ -25585,7 +25651,9 @@ fn cgmlet(c: *cgen, n: *node) void = {
cgexpr(c, rhs);
let total: i32 = 0;
let ssecap: i32 = 2; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
let l: *node = n.list;
let pt: *node = nil;
if (rettuple != nil) { pt = rettuple.list; };
@@ -25594,20 +25662,30 @@ fn cgmlet(c: *cgen, n: *node) void = {
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
if (isfloattype(c, tn)) {
ssetotal = ssetotal + 1;
} else {
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
gptotal = gptotal + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};
if (total > 4) { // AX,DX,CX,R8 capacity
if (gptotal > 4) { // AX,DX,CX,R8 capacity
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
// fatal(), err.c) — surface, don't corrupt.
let msg: str = "tuple destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
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 cur: i32 = 0;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
l = n.list;
pt = nil;
if (rettuple != nil) { pt = rettuple.list; };
@@ -25616,12 +25694,17 @@ fn cgmlet(c: *cgen, n: *node) void = {
if (tn == nil) {
if (pt != nil) { tn = pt.lhs; };
};
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let sz: i32 = 8;
if (wide) { sz = tyslicesize(): i32; };
let off: i32 = localadd(c, l.str, sz, tn);
tupstore(c, cur, off, wide, tn);
cur = cur + tupebytes(wide);
tupstore(c, gpcur, ssecur, off, wide, tn);
if (isflt) {
ssecur = ssecur + 1;
} else {
gpcur = gpcur + tupebytes(wide);
};
l = l.next;
if (pt != nil) { pt = pt.next; };
};