cgen: N-ary tuple destructure positional store + loud-stop (both stages, #83)

Replace the str-only XOR (e0_is_str ^ e1_is_str) at the tuple send
(N_RETURN) and receive (N_MLET/N_MASSIGN) sites with a positional
per-element register cursor, mirroring harec create_unpack_bindings
(ref/harec/src/check.c:1354-1416). Each element rides consecutive
eightbytes over [AX,DX,CX,R8]; a slice/str rides its 3-word
{ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), a scalar rides 1.
Send and receive walk the SAME type-table widths so element->register
agrees. This routes []u8 elements through the 3-word path (the XOR was
slice-blind, dropping len+cap to the scalar fallback) and closes the
pre-existing (scalar,slice) cs!=ww divergence by construction. cstage
and wwstage emit byte-identical asm.

Both receive sites derive each element's width from the rhs tuple's
element types (n->rhs->type->params / the callee return type) -- the
SAME producer view the send site walks -- NOT the binding type: a `_`
lvalue is an N_IDENT with empty str the checker never type-stamps, so a
binding-typed width mis-sized a wide `_` and desynced the cursor for the
next element (cstage read DX, wwstage R8). harec `_` skips the store but
CONSUMES its tuple offset; the cursor advance honours that.

Loud-stop (rule 7): the register file holds 4 eightbytes; a tuple whose
elements sum to >4 (([]u8,[]u8)/(str,str)=6) cannot be register-returned,
so the send site aborts at compile time citing the return-ABI capacity
(#10) rather than silently miscompiling. The receive loop guards the
same predicate (defense-in-depth). Routed through each stage's EXISTING
pinned-fatal idiom: cstage fatal() (cmd/wcc/err.c), wwstage the inline
os.write(2,...)+os.exit(1) at cgen.ww:604 -- no new diagnostics path.

N_MASSIGN (`a,b=f()`, bare comma, pre-declared) is a retained
ww-EXTENSION beyond Hare's binding-only tuple-unpack (Go/rob-pike
multi-assign, rule-9 carve-out); the loop covers it identically to
N_MLET.

Test 945_tuple_nary_destructure_run: (i64,[]u8)+(i64,str) store+read
len/cap for both N_MLET and N_MASSIGN, a single-str control, a wide-
first blank `_,a=f()` row (the cursor-desync discriminator), and a
([]u8,[]u8) row asserting the loud BUILDERR carries the cited
diagnostic; dual ww/ww_ww drivers.
This commit is contained in:
2026-05-25 09:34:05 +09:00
parent 1304db8871
commit 5a0427ef32
6 changed files with 1116 additions and 896 deletions

View File

@@ -20062,47 +20062,134 @@ fn rundefers(c: *cgen) void = {
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";
};
fn tupebytes(wide: bool) i32 = {
if (wide) { return (tyslicesize() / 8i64): i32; };
return 1;
};
// rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil).
// wwstage has no checker, so the receive sites read each tuple element's
// width from the called fn's declared return type. Mirrors the callee
// resolution shared by cgmlet/cgmassign.
fn rettupleof(c: *cgen, rhs: *node) *node = {
if (rhs == nil) { return nil; };
if (rhs.kind != nkind.N_CALL) { return nil; };
let callee: *node = rhs.lhs;
if (callee == nil) { return nil; };
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cnm = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cnm = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len == 0) { return nil; };
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp == nil) { return nil; };
if (rtyp.kind != nkind.N_TTUPLE) { return nil; };
return rtyp;
};
// tupstore — store the tuple element at register-cursor `cur` into the
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
// cursor registers; a scalar stores 1 word. Byte-identical to the cstage
// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c).
fn tupstore(cur: i32, off: i32, wide: bool) void = {
if (wide) {
emitline("\tMOVQ\t");
emitline(tupreg(cur + 0));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 1));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 2));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
return;
};
emitline("\tMOVQ\t");
emitline(tupreg(cur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
};
fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;`, word-indexed AX→DX→CX→R8 (the
// SAME register sequence as the tagged-union return below; the
// tuple just fills it positionally):
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len, R8 = str.cap.
// str IS []u8 (24B) → 32B tuple; cap rides R8, matching the
// tagged-union return that already uses R8 for slot+24
// (#1/Phase 3, task #5). Receive sites destructure off the
// same regs regardless of position.
// #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.
if (rhs.kind == nkind.N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
let v2: *node = v.next;
if (v2 != nil) {
let v0_is_str: bool = nodeisstr(c, v);
let v1_is_str: bool = nodeisstr(c, v2);
if ((v0_is_str || v1_is_str) && !(v0_is_str && v1_is_str)) {
let strn: *node = v;
let scaln: *node = v2;
if (v1_is_str) { strn = v2; scaln = v; };
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
} else {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
};
} else {
cgexpr(c, v);
let total: 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
};
e = e.next;
};
let i: i32 = total - 1;
for (i >= 0) {
emitline("\tPOPQ\t");
emitline(tupreg(i));
emitline("\n");
i = i - 1;
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
@@ -21152,131 +21239,58 @@ fn cgfor(c: *cgen, n: *node) void = {
// 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 = {
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
// one-str only; two-str destructure is gap (task #22). The str
// tuple element's 24B slot already exists (reassignment), so
// localfind it and mirror cgmlet's (DX,CX,R8)->(.ptr,.len,.cap)
// routing; the bare scalar fallback below would store only DX and
// drop len/cap. wwstage has no checker, so str-ness comes from the
// called fn's return-type tuple element (as in cgmlet).
let p0t: *node = nil;
let p1t: *node = nil;
let rhs: *node = n.rhs;
if (rhs != nil) {
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != 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) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTUPLE) {
let pp: *node = rtyp.list;
if (pp != nil) {
p0t = pp.lhs;
if (pp.next != nil) {
p1t = pp.next.lhs;
};
};
};
};
};
};
};
};
let s0_is_str: bool = isstrtype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t);
// #83: positional per-element destructure REASSIGN. Same cursor as
// cgmlet (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416), but the slots already exist
// (reassignment) so localfind them. wwstage has no checker, so each
// element's width comes from the called fn's return-type tuple
// element (N_TTUPLE param) walked in lockstep with the bindings; a
// slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes
// its register slot without storing (mirrors harec `_`). This bare-
// comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond
// Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
if (n.rhs != nil) { cgexpr(c, n.rhs); };
if (l0 != nil) {
if (l1 != nil) {
if (l0.kind == nkind.N_IDENT) {
if (l1.kind == nkind.N_IDENT) {
if (s0_is_str != s1_is_str) {
let off0: i32 = localfind(c, l0.str);
let off1: i32 = localfind(c, l1.str);
if (off0 != 0) {
if (off1 != 0) {
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
};
let total: 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);
l = l.next;
if (pt != nil) { pt = pt.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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
emitline("\tPUSHQ\tDX\n");
if (l0 != nil) {
if (l0.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l0.str);
if (off != 0) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
emitline("\tPOPQ\tDX\n");
if (l1 != nil) {
if (l1.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l1.str);
if (off != 0) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
let cur: 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 wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let off: i32 = 0;
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
if (off != 0) {
tupstore(cur, off, wide);
};
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
c.lastwasreturn = 0;
return;
@@ -21297,115 +21311,57 @@ fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
let p0t: *node = nil;
let p1t: *node = nil;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != 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) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTUPLE) {
let pp: *node = rtyp.list;
if (pp != nil) {
p0t = pp.lhs;
if (pp.next != nil) {
p1t = pp.next.lhs;
};
};
};
};
};
};
};
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
let t0: *node = nil;
let t1: *node = nil;
if (l0 != nil) { t0 = l0.lhs; };
if (l1 != nil) { t1 = l1.lhs; };
if (t0 == nil) { t0 = p0t; };
if (t1 == nil) { t1 = p1t; };
let s0_is_str: bool = isstrtype(c, t0);
let s1_is_str: bool = isstrtype(c, t1);
// #83: positional per-element destructure let-binding. Same cursor
// as cgmassign (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416). wwstage has no checker, so each
// binding's type is its explicit annotation (l.lhs) when present,
// else the called fn's return-type tuple element (N_TTUPLE param)
// walked in lockstep. A slice/str rides its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
cgexpr(c, rhs);
if (l0 != nil) {
if (l1 != nil) {
if (s0_is_str != s1_is_str) {
let sz0: i32 = 8;
let sz1: i32 = 8;
if (s0_is_str) { sz0 = primtypesize("str"): i32; };
if (s1_is_str) { sz1 = primtypesize("str"): i32; };
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
let total: 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; };
};
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (l0 != nil) {
let off: i32 = localadd(c, l0.str, 8, t0);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (l1 != nil) {
let off: i32 = localadd(c, l1.str, 8, t1);
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
let cur: 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 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(cur, off, wide);
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
c.lastwasreturn = 0;
return;

View File

@@ -107,47 +107,134 @@ fn rundefers(c: *cgen) void = {
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";
};
fn tupebytes(wide: bool) i32 = {
if (wide) { return (tyslicesize() / 8i64): i32; };
return 1;
};
// rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil).
// wwstage has no checker, so the receive sites read each tuple element's
// width from the called fn's declared return type. Mirrors the callee
// resolution shared by cgmlet/cgmassign.
fn rettupleof(c: *cgen, rhs: *node) *node = {
if (rhs == nil) { return nil; };
if (rhs.kind != nkind.N_CALL) { return nil; };
let callee: *node = rhs.lhs;
if (callee == nil) { return nil; };
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cnm = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cnm = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len == 0) { return nil; };
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp == nil) { return nil; };
if (rtyp.kind != nkind.N_TTUPLE) { return nil; };
return rtyp;
};
// tupstore — store the tuple element at register-cursor `cur` into the
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
// cursor registers; a scalar stores 1 word. Byte-identical to the cstage
// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c).
fn tupstore(cur: i32, off: i32, wide: bool) void = {
if (wide) {
emitline("\tMOVQ\t");
emitline(tupreg(cur + 0));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 1));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 2));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
return;
};
emitline("\tMOVQ\t");
emitline(tupreg(cur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
};
fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;`, word-indexed AX→DX→CX→R8 (the
// SAME register sequence as the tagged-union return below; the
// tuple just fills it positionally):
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len, R8 = str.cap.
// str IS []u8 (24B) → 32B tuple; cap rides R8, matching the
// tagged-union return that already uses R8 for slot+24
// (#1/Phase 3, task #5). Receive sites destructure off the
// same regs regardless of position.
// #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.
if (rhs.kind == nkind.N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
let v2: *node = v.next;
if (v2 != nil) {
let v0_is_str: bool = nodeisstr(c, v);
let v1_is_str: bool = nodeisstr(c, v2);
if ((v0_is_str || v1_is_str) && !(v0_is_str && v1_is_str)) {
let strn: *node = v;
let scaln: *node = v2;
if (v1_is_str) { strn = v2; scaln = v; };
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
} else {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
};
} else {
cgexpr(c, v);
let total: 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
};
e = e.next;
};
let i: i32 = total - 1;
for (i >= 0) {
emitline("\tPOPQ\t");
emitline(tupreg(i));
emitline("\n");
i = i - 1;
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
@@ -1197,131 +1284,58 @@ fn cgfor(c: *cgen, n: *node) void = {
// 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 = {
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
// one-str only; two-str destructure is gap (task #22). The str
// tuple element's 24B slot already exists (reassignment), so
// localfind it and mirror cgmlet's (DX,CX,R8)->(.ptr,.len,.cap)
// routing; the bare scalar fallback below would store only DX and
// drop len/cap. wwstage has no checker, so str-ness comes from the
// called fn's return-type tuple element (as in cgmlet).
let p0t: *node = nil;
let p1t: *node = nil;
let rhs: *node = n.rhs;
if (rhs != nil) {
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != 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) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTUPLE) {
let pp: *node = rtyp.list;
if (pp != nil) {
p0t = pp.lhs;
if (pp.next != nil) {
p1t = pp.next.lhs;
};
};
};
};
};
};
};
};
let s0_is_str: bool = isstrtype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t);
// #83: positional per-element destructure REASSIGN. Same cursor as
// cgmlet (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416), but the slots already exist
// (reassignment) so localfind them. wwstage has no checker, so each
// element's width comes from the called fn's return-type tuple
// element (N_TTUPLE param) walked in lockstep with the bindings; a
// slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes
// its register slot without storing (mirrors harec `_`). This bare-
// comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond
// Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
if (n.rhs != nil) { cgexpr(c, n.rhs); };
if (l0 != nil) {
if (l1 != nil) {
if (l0.kind == nkind.N_IDENT) {
if (l1.kind == nkind.N_IDENT) {
if (s0_is_str != s1_is_str) {
let off0: i32 = localfind(c, l0.str);
let off1: i32 = localfind(c, l1.str);
if (off0 != 0) {
if (off1 != 0) {
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
};
let total: 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);
l = l.next;
if (pt != nil) { pt = pt.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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
emitline("\tPUSHQ\tDX\n");
if (l0 != nil) {
if (l0.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l0.str);
if (off != 0) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
emitline("\tPOPQ\tDX\n");
if (l1 != nil) {
if (l1.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l1.str);
if (off != 0) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
let cur: 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 wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let off: i32 = 0;
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
if (off != 0) {
tupstore(cur, off, wide);
};
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
c.lastwasreturn = 0;
return;
@@ -1342,115 +1356,57 @@ fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
let p0t: *node = nil;
let p1t: *node = nil;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != 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) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTUPLE) {
let pp: *node = rtyp.list;
if (pp != nil) {
p0t = pp.lhs;
if (pp.next != nil) {
p1t = pp.next.lhs;
};
};
};
};
};
};
};
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
let t0: *node = nil;
let t1: *node = nil;
if (l0 != nil) { t0 = l0.lhs; };
if (l1 != nil) { t1 = l1.lhs; };
if (t0 == nil) { t0 = p0t; };
if (t1 == nil) { t1 = p1t; };
let s0_is_str: bool = isstrtype(c, t0);
let s1_is_str: bool = isstrtype(c, t1);
// #83: positional per-element destructure let-binding. Same cursor
// as cgmassign (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416). wwstage has no checker, so each
// binding's type is its explicit annotation (l.lhs) when present,
// else the called fn's return-type tuple element (N_TTUPLE param)
// walked in lockstep. A slice/str rides its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
cgexpr(c, rhs);
if (l0 != nil) {
if (l1 != nil) {
if (s0_is_str != s1_is_str) {
let sz0: i32 = 8;
let sz1: i32 = 8;
if (s0_is_str) { sz0 = primtypesize("str"): i32; };
if (s1_is_str) { sz1 = primtypesize("str"): i32; };
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
let total: 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; };
};
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (l0 != nil) {
let off: i32 = localadd(c, l0.str, 8, t0);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (l1 != nil) {
let off: i32 = localadd(c, l1.str, 8, t1);
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
let cur: 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 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(cur, off, wide);
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
c.lastwasreturn = 0;
return;

View File

@@ -20062,47 +20062,134 @@ fn rundefers(c: *cgen) void = {
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";
};
fn tupebytes(wide: bool) i32 = {
if (wide) { return (tyslicesize() / 8i64): i32; };
return 1;
};
// rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil).
// wwstage has no checker, so the receive sites read each tuple element's
// width from the called fn's declared return type. Mirrors the callee
// resolution shared by cgmlet/cgmassign.
fn rettupleof(c: *cgen, rhs: *node) *node = {
if (rhs == nil) { return nil; };
if (rhs.kind != nkind.N_CALL) { return nil; };
let callee: *node = rhs.lhs;
if (callee == nil) { return nil; };
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cnm = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cnm = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len == 0) { return nil; };
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp == nil) { return nil; };
if (rtyp.kind != nkind.N_TTUPLE) { return nil; };
return rtyp;
};
// tupstore — store the tuple element at register-cursor `cur` into the
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
// cursor registers; a scalar stores 1 word. Byte-identical to the cstage
// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c).
fn tupstore(cur: i32, off: i32, wide: bool) void = {
if (wide) {
emitline("\tMOVQ\t");
emitline(tupreg(cur + 0));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 1));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\t");
emitline(tupreg(cur + 2));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
return;
};
emitline("\tMOVQ\t");
emitline(tupreg(cur));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
};
fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;`, word-indexed AX→DX→CX→R8 (the
// SAME register sequence as the tagged-union return below; the
// tuple just fills it positionally):
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len, R8 = str.cap.
// str IS []u8 (24B) → 32B tuple; cap rides R8, matching the
// tagged-union return that already uses R8 for slot+24
// (#1/Phase 3, task #5). Receive sites destructure off the
// same regs regardless of position.
// #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.
if (rhs.kind == nkind.N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
let v2: *node = v.next;
if (v2 != nil) {
let v0_is_str: bool = nodeisstr(c, v);
let v1_is_str: bool = nodeisstr(c, v2);
if ((v0_is_str || v1_is_str) && !(v0_is_str && v1_is_str)) {
let strn: *node = v;
let scaln: *node = v2;
if (v1_is_str) { strn = v2; scaln = v; };
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
} else {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
};
} else {
cgexpr(c, v);
let total: 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
};
e = e.next;
};
let i: i32 = total - 1;
for (i >= 0) {
emitline("\tPOPQ\t");
emitline(tupreg(i));
emitline("\n");
i = i - 1;
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
@@ -21152,131 +21239,58 @@ fn cgfor(c: *cgen, n: *node) void = {
// 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 = {
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
// one-str only; two-str destructure is gap (task #22). The str
// tuple element's 24B slot already exists (reassignment), so
// localfind it and mirror cgmlet's (DX,CX,R8)->(.ptr,.len,.cap)
// routing; the bare scalar fallback below would store only DX and
// drop len/cap. wwstage has no checker, so str-ness comes from the
// called fn's return-type tuple element (as in cgmlet).
let p0t: *node = nil;
let p1t: *node = nil;
let rhs: *node = n.rhs;
if (rhs != nil) {
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != 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) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTUPLE) {
let pp: *node = rtyp.list;
if (pp != nil) {
p0t = pp.lhs;
if (pp.next != nil) {
p1t = pp.next.lhs;
};
};
};
};
};
};
};
};
let s0_is_str: bool = isstrtype(c, p0t);
let s1_is_str: bool = isstrtype(c, p1t);
// #83: positional per-element destructure REASSIGN. Same cursor as
// cgmlet (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416), but the slots already exist
// (reassignment) so localfind them. wwstage has no checker, so each
// element's width comes from the called fn's return-type tuple
// element (N_TTUPLE param) walked in lockstep with the bindings; a
// slice/str rides its 3-word {ptr,len,cap} header
// (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes
// its register slot without storing (mirrors harec `_`). This bare-
// comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond
// Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
if (n.rhs != nil) { cgexpr(c, n.rhs); };
if (l0 != nil) {
if (l1 != nil) {
if (l0.kind == nkind.N_IDENT) {
if (l1.kind == nkind.N_IDENT) {
if (s0_is_str != s1_is_str) {
let off0: i32 = localfind(c, l0.str);
let off1: i32 = localfind(c, l1.str);
if (off0 != 0) {
if (off1 != 0) {
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
};
let total: 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);
l = l.next;
if (pt != nil) { pt = pt.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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
emitline("\tPUSHQ\tDX\n");
if (l0 != nil) {
if (l0.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l0.str);
if (off != 0) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
emitline("\tPOPQ\tDX\n");
if (l1 != nil) {
if (l1.kind == nkind.N_IDENT) {
let off: i32 = localfind(c, l1.str);
if (off != 0) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
let cur: 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 wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let off: i32 = 0;
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
if (off != 0) {
tupstore(cur, off, wide);
};
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
c.lastwasreturn = 0;
return;
@@ -21297,115 +21311,57 @@ fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
let p0t: *node = nil;
let p1t: *node = nil;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != 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) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTUPLE) {
let pp: *node = rtyp.list;
if (pp != nil) {
p0t = pp.lhs;
if (pp.next != nil) {
p1t = pp.next.lhs;
};
};
};
};
};
};
};
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
let t0: *node = nil;
let t1: *node = nil;
if (l0 != nil) { t0 = l0.lhs; };
if (l1 != nil) { t1 = l1.lhs; };
if (t0 == nil) { t0 = p0t; };
if (t1 == nil) { t1 = p1t; };
let s0_is_str: bool = isstrtype(c, t0);
let s1_is_str: bool = isstrtype(c, t1);
// #83: positional per-element destructure let-binding. Same cursor
// as cgmassign (and cgreturn; harec create_unpack_bindings,
// ref/harec/src/check.c:1354-1416). wwstage has no checker, so each
// binding's type is its explicit annotation (l.lhs) when present,
// else the called fn's return-type tuple element (N_TTUPLE param)
// walked in lockstep. A slice/str rides its 3-word {ptr,len,cap}
// header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
cgexpr(c, rhs);
if (l0 != nil) {
if (l1 != nil) {
if (s0_is_str != s1_is_str) {
let sz0: i32 = 8;
let sz1: i32 = 8;
if (s0_is_str) { sz0 = primtypesize("str"): i32; };
if (s1_is_str) { sz1 = primtypesize("str"): i32; };
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
let total: 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; };
};
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
total = total + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (l0 != nil) {
let off: i32 = localadd(c, l0.str, 8, t0);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (l1 != nil) {
let off: i32 = localadd(c, l1.str, 8, t1);
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
let cur: 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 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(cur, off, wide);
cur = cur + tupebytes(wide);
l = l.next;
if (pt != nil) { pt = pt.next; };
};
c.lastwasreturn = 0;
return;