wwstage struct-return RECV and RETURN used unrounded / round-to-8 sizes where
cstage uses the maxalign-rounded lu->size / rt->size, so a struct with maxalign
8 and a sub-8 tail (e.g. struct{i64,i32}) — or a maxalign<8 struct on the
return path — unpacked with a different trailing-word width (MOVL vs MOVQ)
between stages. Value-correct either way, but a cs!=ww asm divergence.
Add a dedicated structabisize = round(natural, maxalign) used only at the two
register-ABI sites. structnaturalsize stays unrounded: cstage's >24B sret and
memory-move path (cgen.c:8150, Task #33) genuinely uses the unrounded natural
size, so the two are different sizes — rounding the shared metric breaks 995.
maxalign derives from each field's tinfo.align (mirrors cstage check.c:708),
not an fsz ladder (a ladder over-rounds composite [N]u8 fields).
Gate-blind (no bootstrap struct hits the maxalign-8+tail shape) — the
discriminator is the cs==ww .s byte-cmp; covered by probe 698.
2016 lines
63 KiB
Plaintext
2016 lines
63 KiB
Plaintext
// selfhost/cmd/wcc/cgenstmt.ww — split out of cgen.ww.
|
|
//
|
|
// cgstmt is a thin dispatcher over n.kind; each branch defers to a
|
|
// per-kind helper: cgblock, cgreturn, cgexprstmt, cglet, cgif, cgfor,
|
|
// cgmassign, cgbreak, cgcontinue.
|
|
//
|
|
// The expression generator (cgexpr) lives in cgenexpr.ww; the
|
|
// foundation (types, emit primitives, collect* tables, FFI/module
|
|
// maps) lives in cgen.ww.
|
|
|
|
package wcc;
|
|
|
|
import os;
|
|
import ast;
|
|
import tok;
|
|
import typ;
|
|
import sym;
|
|
import strconv;
|
|
|
|
// ---- statement cgen --------------------------------------------------
|
|
|
|
fn cgstmt(c: *cgen, n: *node) void = {
|
|
if (n == nil) { return; };
|
|
let k: nkind = n.kind;
|
|
|
|
if (k == nkind.N_BLOCK) { cgblock(c, n); return; };
|
|
|
|
if (k == nkind.N_RETURN) { cgreturn(c, n); return; };
|
|
|
|
if (k == nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; };
|
|
|
|
if (k == nkind.N_LET) { cglet(c, n); return; };
|
|
|
|
if (k == nkind.N_IF) { cgif(c, n); return; };
|
|
|
|
if (k == nkind.N_FOR) { cgfor(c, n); return; };
|
|
|
|
if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; };
|
|
|
|
if (k == nkind.N_SWITCH) { cgswitch(c, n); return; };
|
|
|
|
if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; };
|
|
|
|
if (k == nkind.N_MLET) { cgmlet(c, n); return; };
|
|
|
|
if (k == nkind.N_BREAK) { cgbreak(c, n); return; };
|
|
if (k == nkind.N_CONTINUE) { cgcontinue(c, n); return; };
|
|
|
|
if (k == nkind.N_YIELD) { cgyield(c, n); return; };
|
|
|
|
if (k == nkind.N_DEFER) {
|
|
if (c.defertop < DEFER_MAX) {
|
|
c.deferbuf[c.defertop] = n.lhs;
|
|
c.defertop += 1;
|
|
};
|
|
return;
|
|
};
|
|
|
|
c.lastwasreturn = 0;
|
|
};
|
|
|
|
fn cgyield(c: *cgen, n: *node) void = {
|
|
// Evaluate the value into AX (and BX for str), then JMP to the
|
|
// enclosing match's end label. Falls through silently if there
|
|
// is no active match — should be a checker error eventually.
|
|
if (n.lhs != nil) { cgexpr(c, n.lhs); };
|
|
if (c.yieldtop > 0) {
|
|
let tgt: str = c.yieldbuf[c.yieldtop - 1];
|
|
emitline("\tJMP\t");
|
|
emitline(tgt);
|
|
emitline("\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cgblock(c: *cgen, n: *node) void = {
|
|
// Save/restore the locals head across the block (post-#27).
|
|
// Inner-scope `let` bindings prepend to c.locals via localadd;
|
|
// without this restore, the prepended stubs leak into sibling
|
|
// and ancestor scopes, and localfind (head-first) returns the
|
|
// inner binding's offset for an identifier that semantically
|
|
// belongs to the outer scope. The frame is left grown — we
|
|
// don't reclaim popped slots, matching cstage's lowering.
|
|
//
|
|
// cgfn iterates fn_.body.list directly to bypass this save/
|
|
// restore at the function's outermost block — defers (and the
|
|
// implicit-return epilogue) need locals intact.
|
|
let saved: *local = c.locals;
|
|
let s: *node = n.list;
|
|
for (s != nil) {
|
|
cgstmt(c, s);
|
|
s = s.next;
|
|
};
|
|
c.locals = saved;
|
|
return;
|
|
};
|
|
|
|
// rundefers — emit cgexpr for every queued defer in LIFO order.
|
|
// Called from cgreturn and the cgfn implicit-return path.
|
|
fn rundefers(c: *cgen) void = {
|
|
let i: i32 = c.defertop - 1;
|
|
for (i >= 0) {
|
|
cgexpr(c, c.deferbuf[i]);
|
|
i -= 1;
|
|
};
|
|
return;
|
|
};
|
|
|
|
// #83: positional tuple register-return ABI. Tuple elements ride
|
|
// consecutive eightbytes over [AX,DX,CX,R8] (tupreg by index); a
|
|
// slice/str rides its 3-word {ptr,len,cap} header (tyslicesize SSoT,
|
|
// ref/hare/rt/ensure.ha:4-8), a scalar rides 1. SEND (cgreturn) and
|
|
// RECEIVE (cgmlet/cgmassign) walk the SAME widths so element->register
|
|
// agrees — mirrors harec create_unpack_bindings
|
|
// (ref/harec/src/check.c:1354-1416). Capacity is 4 (AX,DX,CX,R8).
|
|
fn tupreg(i: i32) str = {
|
|
if (i == 0) { return "AX"; };
|
|
if (i == 1) { return "DX"; };
|
|
if (i == 2) { return "CX"; };
|
|
return "R8";
|
|
};
|
|
|
|
// #164 (#107): SSE half of the SysV dual register-class return. A float
|
|
// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the
|
|
// INTEGER row tupreg — a float lands in the next XMM regardless of its
|
|
// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},
|
|
// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage
|
|
// tuple_sse_seq (cmd/w6c/cgen.c).
|
|
fn tupsse(i: i32) str = {
|
|
if (i == 0) { return "X0"; };
|
|
return "X1";
|
|
};
|
|
|
|
fn tupebytes(wide: bool) i32 = {
|
|
if (wide) { return (tyslicesize() / 8i64): i32; };
|
|
return 1;
|
|
};
|
|
|
|
// rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil).
|
|
// wwstage has no checker, so the receive sites read each tuple element's
|
|
// width from the called fn's declared return type. Mirrors the callee
|
|
// resolution shared by cgmlet/cgmassign.
|
|
fn rettupleof(c: *cgen, rhs: *node) *node = {
|
|
if (rhs == nil) { return nil; };
|
|
if (rhs.kind != nkind.N_CALL) { return nil; };
|
|
let callee: *node = rhs.lhs;
|
|
if (callee == nil) { return nil; };
|
|
let cnm: str;
|
|
cnm.ptr = nil; cnm.len = 0;
|
|
let cmod: str;
|
|
cmod.ptr = nil; cmod.len = 0;
|
|
if (callee.kind == nkind.N_IDENT) {
|
|
cnm = callee.str;
|
|
cmod = c.curmod;
|
|
};
|
|
if (callee.kind == nkind.N_DOT) {
|
|
cnm = callee.str;
|
|
if (callee.lhs != nil) {
|
|
if (callee.lhs.kind == nkind.N_IDENT) {
|
|
cmod = callee.lhs.str;
|
|
};
|
|
};
|
|
};
|
|
if (cnm.len == 0) { return nil; };
|
|
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
|
|
if (rtyp == nil) { return nil; };
|
|
if (rtyp.kind != nkind.N_TTUPLE) { return nil; };
|
|
return rtyp;
|
|
};
|
|
|
|
// tupstore — store the tuple element at register-cursor `cur` into the
|
|
// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap}
|
|
// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive
|
|
// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a
|
|
// scalar stores 1 INTEGER word. The caller owns the dual cursor
|
|
// (validated + advanced). Byte-identical to the cstage tuple_store
|
|
// (cmd/w6c/cgen.c).
|
|
fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = {
|
|
if (wide) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur + 0));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur + 1));
|
|
emitline(", ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur + 2));
|
|
emitline(", ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
return;
|
|
};
|
|
// #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg
|
|
// (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else
|
|
// the slot gets garbage and the FACE-Z field read sees it. The SSE
|
|
// regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105
|
|
// single-float byte-id; idx1=X1 is the #107 multi-float extension.
|
|
if (isfloattype(c, tn)) {
|
|
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
|
|
// In destructure mode tn IS the tuple-element-type-AST node
|
|
// (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value
|
|
// stored" rides X0 with no separate AST. isfloattype(c, tn) at
|
|
// the branch head already implies tn.type_!=nil (typeisfloat is
|
|
// false on nil), so this assertion is structurally unreachable
|
|
// today — RETAINED to PIN the contract: "the float-store branch
|
|
// requires a stamped slot." Catches a future change that opens
|
|
// this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple
|
|
// element binding where the destructure stamp didn't land —
|
|
// #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/
|
|
// 1475 + asserttyped file:line at check.ww:3340-3344.
|
|
if (tn != nil) { if (tn.type_ == nil) {
|
|
let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at ";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
if (tn.file.len > 0) {
|
|
os.write(2, tn.file.ptr, tn.file.len: u64);
|
|
os.write(2, ":".ptr, 1u64);
|
|
let ls: str = strconv.i32tos(tn.line, strconv.base.DEC);
|
|
os.write(2, ls.ptr, ls.len: u64);
|
|
os.write(2, " ".ptr, 1u64);
|
|
};
|
|
let kn: str = nkname(tn.kind);
|
|
os.write(2, kn.ptr, kn.len: u64);
|
|
os.write(2, "\n".ptr, 1u64);
|
|
os.exit(1);
|
|
}; };
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, tn)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\t");
|
|
emitline(tupsse(ssecur));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
return;
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
|
|
fn cgreturn(c: *cgen, n: *node) void = {
|
|
rundefers(c);
|
|
let rhs: *node = n.lhs;
|
|
if (rhs != nil) {
|
|
// #83 / #164 (#107): positional register-return over a SysV
|
|
// dual class cursor (harec create_unpack_bindings, ref/harec/src/
|
|
// check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 =
|
|
// tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8]
|
|
// (tupreg) — a slice/str its 3-word {ptr,len,cap} header
|
|
// (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a
|
|
// scalar 1 word in AX. Integer words spill L->R to the stack and
|
|
// pop into the INTEGER cursor in reverse so positional slot i
|
|
// lands in tupreg(i) (byte-id with #83 when no float is present).
|
|
// Each float must spill X0 to @tupfscr as we walk, since a later
|
|
// element's cgexpr clobbers X0; after the integer pops the saved
|
|
// floats reload into X0/X1 by SSE index — INDEPENDENT of the
|
|
// INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows
|
|
// loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME
|
|
// class split drives the receive sites.
|
|
if (rhs.kind == nkind.N_TUPLE) {
|
|
let ssecap: i32 = 2; // X0,X1 per SysV
|
|
let gptotal: i32 = 0;
|
|
let ssecount: i32 = 0;
|
|
let e: *node = rhs.list;
|
|
for (e != nil) {
|
|
if (isfloattype(c, e)) {
|
|
ssecount = ssecount + 1;
|
|
} else {
|
|
let wide: bool = nodeisstr(c, e) || nodeisslice(c, e);
|
|
gptotal = gptotal + tupebytes(wide);
|
|
};
|
|
e = e.next;
|
|
};
|
|
if (gptotal > 4) { // AX,DX,CX,R8 capacity
|
|
// pinned loud-stop, inline like cgen.ww:604 (cstage
|
|
// uses fatal(), err.c) — surface, don't corrupt.
|
|
let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
if (ssecount > ssecap) {
|
|
let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let fscr: i32 = 0;
|
|
if (ssecount > 0) {
|
|
fscr = localadd(c, "@tupfscr", ssecap * 8, nil);
|
|
};
|
|
let sseidx: i32 = 0;
|
|
e = rhs.list;
|
|
for (e != nil) {
|
|
let isflt: bool = isfloattype(c, e);
|
|
cgexpr(c, e);
|
|
if (isflt) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, e)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, ");
|
|
emitoff((fscr + sseidx * 8): i64);
|
|
emitline("(BP)\n");
|
|
sseidx = sseidx + 1;
|
|
} else {
|
|
emitline("\tPUSHQ\tAX\n"); // scalar / .ptr
|
|
if (nodeisstr(c, e) || nodeisslice(c, e)) {
|
|
emitline("\tPUSHQ\tBX\n"); // .len
|
|
emitline("\tPUSHQ\tCX\n"); // .cap
|
|
};
|
|
};
|
|
e = e.next;
|
|
};
|
|
let i: i32 = gptotal - 1;
|
|
for (i >= 0) {
|
|
emitline("\tPOPQ\t");
|
|
emitline(tupreg(i));
|
|
emitline("\n");
|
|
i = i - 1;
|
|
};
|
|
let j: i32 = 0;
|
|
e = rhs.list;
|
|
for (e != nil) {
|
|
if (isfloattype(c, e)) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, e)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\t");
|
|
emitoff((fscr + j * 8): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupsse(j));
|
|
emitline("\n");
|
|
j = j + 1;
|
|
};
|
|
e = e.next;
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
// Tagged-union return: pack as (AX=tag, DX=value0, CX=value1).
|
|
// For str variant, cgexpr leaves (AX=ptr, BX=len), so we
|
|
// shuffle DX←AX (ptr) and CX←BX (len), then load tag.
|
|
// For other variants, cgexpr leaves AX, shuffle DX←AX.
|
|
// Nullable folded `(*T | void)`: just one word; AX is
|
|
// already the pointer (or 0). No shuffle, no tag.
|
|
if (istaggedtype(c, c.fnret)) {
|
|
// Forwarding a fallible call: `return f();` where f
|
|
// also returns a tagged union. The result is already
|
|
// in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag.
|
|
// Mirrors the rhsreturnstagged path in cglet and the
|
|
// !type_istagged guard in C cgen's N_RETURN.
|
|
let forwardtagged: bool = false;
|
|
if (rhs.kind == nkind.N_CALL) {
|
|
let callee: *node = rhs.lhs;
|
|
if (callee != nil) {
|
|
let calleename: str;
|
|
calleename.ptr = nil; calleename.len = 0;
|
|
let cmod: str;
|
|
cmod.ptr = nil; cmod.len = 0;
|
|
if (callee.kind == nkind.N_IDENT) {
|
|
calleename = callee.str;
|
|
cmod = c.curmod;
|
|
};
|
|
if (callee.kind == nkind.N_DOT) {
|
|
calleename = callee.str;
|
|
if (callee.lhs != nil) {
|
|
if (callee.lhs.kind == nkind.N_IDENT) {
|
|
cmod = callee.lhs.str;
|
|
};
|
|
};
|
|
};
|
|
if (calleename.len > 0) {
|
|
let rtyp: *node = fnretlookupmod(c, calleename, cmod);
|
|
if (istaggedtype(c, rtyp)) { forwardtagged = true; };
|
|
};
|
|
};
|
|
};
|
|
// Struct payload or tagged-subset return — materialise
|
|
// the widened value in scratch via cgwidentaggedstore
|
|
// (handles tag remap and zero pad), then load AX/DX/CX
|
|
// from the slot.
|
|
let needswiden: bool = false;
|
|
if (!isnullabletype(c.fnret)) {
|
|
if (!forwardtagged) {
|
|
let sname: str = rhsstructpayload(c, rhs);
|
|
if (sname.len > 0) { needswiden = true; };
|
|
if (rhstaggedident(c, rhs) != nil) {
|
|
needswiden = true;
|
|
};
|
|
};
|
|
};
|
|
if (needswiden) {
|
|
let rsz: i32 = slotsize(c, c.fnret);
|
|
// @retscr (not @tagscr) for the return materialise
|
|
// path. Cstage cmd/w6c/cgen.c cgreturn uses
|
|
// `@retscr` here and reserves the @tagscr SSoT
|
|
// for arg-widen / non-BP-base store / N_INDEX
|
|
// tagged-element write. Sharing the name in a fn
|
|
// that BOTH returns a 32B tagged AND pushes a
|
|
// smaller tagged arg fatals localadd's @-prefix
|
|
// size-grow guard (rule 7); routing returns
|
|
// through their own slot keeps each cache
|
|
// monotonic. Hardcoding 24 truncated 32B-slot
|
|
// returns and overwrote adjacent locals during
|
|
// the pre-zero loop (#38).
|
|
let scroff: i32 = localadd(c, "@retscr", rsz, nil);
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
let zz: i32 = 0;
|
|
for (zz < rsz) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + zz): i64);
|
|
emitline("(BP)\n");
|
|
zz += 8;
|
|
};
|
|
cgwidentaggedstore(c, c.fnret.type_: *tinfo, rhs, "BP",
|
|
scroff, rsz);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP), AX\n");
|
|
if (rsz > 8) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP), DX\n");
|
|
};
|
|
if (rsz > 16) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP), CX\n");
|
|
};
|
|
if (rsz > 24) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 24): i64);
|
|
emitline("(BP), R8\n");
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
cgexpr(c, rhs);
|
|
if (isnullabletype(c.fnret)) {
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
if (forwardtagged) {
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
|
|
// Tagged-return ABI: AX=tag, DX=word0, CX=word1,
|
|
// R8=word2. Receiver (cgwidentaggedstore call-source
|
|
// arm) writes AX/DX/CX/R8 unconditionally sized by the
|
|
// dst slot; unused ABI words must be zeroed here so a
|
|
// stale CX/R8 from the caller (e.g. a slice-stride
|
|
// IMULQ before the call) does not land in slot+16 /
|
|
// slot+24. (Task #18.)
|
|
let rsz: i32 = slotsize(c, c.fnret);
|
|
if (nodeisslice(c, rhs)) {
|
|
// cgexpr leaves (AX=ptr, BX=len, CX=cap).
|
|
// Shuffle into return ABI: DX=ptr, CX=len,
|
|
// R8=cap.
|
|
emitline("\tMOVQ\tCX, R8\n");
|
|
emitline("\tMOVQ\tBX, CX\n");
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
} else { if (nodeisstr(c, rhs)) {
|
|
// str IS []u8: cgexpr leaves (AX=ptr, BX=len,
|
|
// CX=cap). Same shuffle as the slice arm above —
|
|
// DX=ptr, CX=len, R8=cap (#1/Phase 3).
|
|
emitline("\tMOVQ\tCX, R8\n");
|
|
emitline("\tMOVQ\tBX, CX\n");
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
} else { if (exprfloatkind(c, rhs) != 0) {
|
|
// #157: float variant — cgexpr left the value
|
|
// in X0, not AX. No MOVQ-xmm->gp encoding, so
|
|
// bridge X0->DX through a stack slot (same arg-
|
|
// push idiom). Zero the slot first so the f32
|
|
// case (MOVSS writes only the low 4 bytes)
|
|
// leaves a deterministic high-4 — cs==ww byte-
|
|
// id, matching f64's MOVSD which fills all 8.
|
|
// The AX-independent spill also removes the
|
|
// stale-AX cs!=ww on multi-variant returns.
|
|
emitline("\tSUBQ\t$8, SP\n");
|
|
emitline("\tMOVQ\t$0, (SP)\n");
|
|
let mov: str = "MOVSD";
|
|
if (exprfloatkind(c, rhs) == 1) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, (SP)\n");
|
|
emitline("\tMOVQ\t(SP), DX\n");
|
|
emitline("\tADDQ\t$8, SP\n");
|
|
if (rsz > 16) {
|
|
emitline("\tMOVQ\t$0, CX\n");
|
|
};
|
|
if (rsz > 24) {
|
|
emitline("\tMOVQ\t$0, R8\n");
|
|
};
|
|
} else {
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
// scalar fills DX only. Zero CX / R8 if dst
|
|
// covers slot+16 / slot+24.
|
|
if (rsz > 16) {
|
|
emitline("\tMOVQ\t$0, CX\n");
|
|
};
|
|
if (rsz > 24) {
|
|
emitline("\tMOVQ\t$0, R8\n");
|
|
};
|
|
};};};
|
|
emitline("\tMOVQ\t$");
|
|
if (idx < 0) { idx = 0; };
|
|
emitint(idx: i64);
|
|
emitline(", AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
// sret return (#23): plain TY_STRUCT > 24B. Callee writes
|
|
// through *(@sretarg) (the caller-prealloc dest saved at
|
|
// the prologue), then loads @sretarg into RAX and rets —
|
|
// the SysV "return the pointer" discipline. Two rhs shapes
|
|
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
|
|
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
|
|
let sretargoff: i32 = localfind(c, "@sretarg");
|
|
if (sretargoff != 0) {
|
|
let scs: i32 = sretretsize(c, c.fnret);
|
|
if (scs > 0) {
|
|
// sret return-forwarding (task #9 follow-up to
|
|
// #23): `return f();` where outer + inner both
|
|
// return the same >24B struct shape. Outer's
|
|
// @sretarg already holds its caller's prealloc
|
|
// dest; pass it to inner in RDI (set by cgcall
|
|
// via c.sretforward), inner writes directly
|
|
// there, inner's RAX (dest pointer) is already
|
|
// outer's return value. The trailing MOVQ
|
|
// @sretarg(BP), AX is redundant after inner's
|
|
// RET but kept for byte-id symmetry with the
|
|
// N_IDENT / N_STRUCTLIT arms below.
|
|
if (rhs.kind == nkind.N_CALL) {
|
|
c.sretforward = 1;
|
|
cgexpr(c, rhs);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
let okrhs: bool = false;
|
|
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
|
if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; };
|
|
if (okrhs) {
|
|
if (rhs.kind == nkind.N_STRUCTLIT) {
|
|
let trefn: *node = rhs.lhs;
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (trefn != nil) {
|
|
if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; }
|
|
else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; };
|
|
};
|
|
let sret_si: *structinfo = structlookup(c, sname);
|
|
if (sret_si != nil) {
|
|
let emptys: str;
|
|
emptys.ptr = nil; emptys.len = 0;
|
|
// mode=1 (PTR_LOCAL): base reg = BX,
|
|
// reloaded from @sretarg(BP) before
|
|
// each field store. disp = 0 because
|
|
// the dest pointer IS the struct base.
|
|
cgstructlitfill(c, sret_si, rhs,
|
|
1, sretargoff, emptys,
|
|
0, scs);
|
|
};
|
|
} else {
|
|
let rl: *local = localfindnode(c, rhs.str);
|
|
if (rl != nil) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), BX\n");
|
|
let k: i32 = 0;
|
|
for (k + 8 <= scs) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 8;
|
|
};
|
|
for (k + 4 <= scs) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 4;
|
|
};
|
|
for (k < scs) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 1;
|
|
};
|
|
};
|
|
};
|
|
// sret return: RAX = dest pointer.
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
// Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7],
|
|
// DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c
|
|
// N_RETURN TY_STRUCT branch. Two rhs shapes are wired:
|
|
// N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT
|
|
// (field-by-field store at scratch+foff, with tagged fields
|
|
// delegated to cgwidentaggedstore). Call-result chain return
|
|
// is deferred to #5's receive side. Sizes > 24B route through
|
|
// the sret arm above.
|
|
let rname: str;
|
|
rname.ptr = nil; rname.len = 0;
|
|
if (c.fnret != nil) {
|
|
if (c.fnret.kind == nkind.N_TNAME) {
|
|
rname = c.fnret.str;
|
|
};
|
|
};
|
|
if (rname.len > 0) {
|
|
let rsi: *structinfo = structlookup(c, rname);
|
|
if (rsi != nil) {
|
|
// ≤24B register RETURN: cstage sizes by rt->size
|
|
// (maxalign-rounded), not the slot-padded totsize
|
|
// (round-to-8) — see structabisize (#169).
|
|
let rsz: i32 = structabisize(rsi);
|
|
if (rsz <= 24) {
|
|
let okrhs: bool = false;
|
|
if (rhs.kind == nkind.N_IDENT) {
|
|
okrhs = true;
|
|
};
|
|
if (rhs.kind == nkind.N_STRUCTLIT) {
|
|
okrhs = true;
|
|
};
|
|
if (okrhs) {
|
|
let scroff: i32 = localadd(c,
|
|
"@retscr", 24, nil);
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP)\n");
|
|
if (rhs.kind == nkind.N_STRUCTLIT) {
|
|
// Delegate to the shared BP-relative
|
|
// structlit fill helper. Same store
|
|
// sequence the inline pre-#17 walk
|
|
// emitted (tagged + float + scalar),
|
|
// plus nested struct-typed structlit
|
|
// values recurse instead of dropping
|
|
// trailing bytes.
|
|
cgstructlitfillbp(c, rsi, rhs, scroff);
|
|
} else {
|
|
// N_IDENT: word-copy from rhs slot
|
|
// to scratch. Whole 8B words via
|
|
// MOVQ; tail via MOVL/MOVB so we
|
|
// read no further than the source
|
|
// slot's declared size.
|
|
let rl: *local = localfindnode(c, rhs.str);
|
|
if (rl != nil) {
|
|
let k: i32 = 0;
|
|
for (k + 8 <= rsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
for (k + 4 <= rsz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
for (k < rsz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
};
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP), DX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP), CX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
cgexpr(c, rhs);
|
|
} else {
|
|
// Bare `return;` from a tagged-union-returning fn is
|
|
// the void variant: emit its tag. Payload is undefined
|
|
// (void has size 0). Otherwise zero AX for determinism.
|
|
if (istaggedtype(c, c.fnret)) {
|
|
if (isnullabletype(c.fnret)) {
|
|
// null = void variant; AX = 0.
|
|
emitline("\tMOVQ\t$0, AX\n");
|
|
} else {
|
|
let idx: i32 = voidvariantindex(c.fnret);
|
|
if (idx < 0) { idx = 0; };
|
|
emitline("\tMOVQ\t$");
|
|
emitint(idx: i64);
|
|
emitline(", AX\n");
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
emitline("\tMOVQ\t$0, AX\n");
|
|
};
|
|
// str IS []u8: cgexpr leaves AX=ptr, BX=len, CX=cap — str now
|
|
// returns exactly like a slice, no AX:DX shuffle (#1/Phase 3).
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
|
|
fn cgexprstmt(c: *cgen, n: *node) void = {
|
|
if (n.lhs != nil) { cgexpr(c, n.lhs); };
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cglet(c: *cgen, n: *node) void = {
|
|
let nm: str = n.str;
|
|
let sz: i32 = letslotsize(c, n);
|
|
// `let x = f()?` has no annotation but the cgen's struct-field
|
|
// paths need a tnode to dispatch off. Infer from f's tagged
|
|
// success variant — see inferletcalltype.
|
|
let tn: *node = n.lhs;
|
|
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
|
let off: i32 = localadd(c, nm, sz, tn);
|
|
if (n.rhs != nil) {
|
|
let rhs: *node = n.rhs;
|
|
// `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45).
|
|
// Mirror of cstage cgen.c N_LET arrlit-empty branch: allocate
|
|
// n*esz bytes via rt_malloc, then build the {ptr, 0, n} slice
|
|
// header in the let slot. The `!`/`?` wraps the builtin's
|
|
// `([]T | nomem)` return; walk into the N_TRYUNW / N_TRYPROP
|
|
// to keep the direct-store fast path rather than falling
|
|
// through to cgalloc (which models scalar alloc and would
|
|
// land an 8B region and a junk slice header). `?` propagates
|
|
// nomem via AX = tag of nomem in c.fnret, then epilogue RET.
|
|
{
|
|
let scall: *node = nil;
|
|
let viatryunw: bool = false;
|
|
let viatryprop: bool = false;
|
|
if (rhs.kind == nkind.N_TRYUNW) {
|
|
if (rhs.lhs != nil) {
|
|
if (rhs.lhs.kind == nkind.N_CALL) {
|
|
scall = rhs.lhs;
|
|
viatryunw = true;
|
|
};
|
|
};
|
|
} else { if (rhs.kind == nkind.N_TRYPROP) {
|
|
if (rhs.lhs != nil) {
|
|
if (rhs.lhs.kind == nkind.N_CALL) {
|
|
scall = rhs.lhs;
|
|
viatryprop = true;
|
|
};
|
|
};
|
|
}; };
|
|
let shapeok: bool = false;
|
|
// #43: route the slice-shape size guard through SSoT.
|
|
// The N_TSLICE kind gate already discriminates here, so
|
|
// this is belt-and-suspenders, but the literal would
|
|
// silently miss after #1 if check.ww's astsize ever
|
|
// drifted from this dispatch.
|
|
if (scall != nil && tn != nil
|
|
&& tn.kind == nkind.N_TSLICE && sz == tyslicesize(): i32) {
|
|
let callee: *node = scall.lhs;
|
|
let a0: *node = scall.list;
|
|
let a1: *node = nil;
|
|
let a2: *node = nil;
|
|
if (a0 != nil) { a1 = a0.next; };
|
|
if (a1 != nil) { a2 = a1.next; };
|
|
if (callee != nil && a0 != nil && a1 != nil
|
|
&& a2 == nil) {
|
|
if (callee.kind == nkind.N_IDENT
|
|
&& streq(callee.str, "alloc")
|
|
&& a0.kind == nkind.N_ARRLIT
|
|
&& a0.list == nil) {
|
|
shapeok = true;
|
|
};
|
|
};
|
|
};
|
|
if (shapeok) {
|
|
// #32: cstage uses `lu->sub->size` (cgen.c:6387), so
|
|
// the element width must resolve struct/tagged/alias
|
|
// names too — not just primitives. elemsizeofc follows
|
|
// TNAME through structlookup/aliaslookup, matching the
|
|
// cstage path byte-identically. A bare primsize/slotsize
|
|
// fork would silently land esz=1 on `[]point`.
|
|
let esz: i32 = elemsizeofc(c, tn);
|
|
let count: *node = scall.list.next;
|
|
cgexpr(c, count);
|
|
emitline("\tPUSHQ\tAX\n");
|
|
if (esz > 1) {
|
|
emitline("\tMOVQ\t$");
|
|
emitint(esz: i64);
|
|
emitline(", BX\n");
|
|
emitline("\tIMULQ\tBX, AX\n");
|
|
};
|
|
emitline("\tMOVQ\tAX, DI\n");
|
|
emitline("\tCALL\t");
|
|
emitline(ffiresolve(c, "malloc"));
|
|
emitline("(SB)\n");
|
|
if (viatryunw) {
|
|
let okl: str = mklabel(c, "tryunw_ok");
|
|
emitline("\tCMPQ\t$0, AX\n");
|
|
emitline("\tJNE\t");
|
|
emitline(okl);
|
|
emitline("\n");
|
|
emitline("\tMOVQ\t$1, DI\n");
|
|
emitline("\tMOVQ\t$60, AX\n");
|
|
emitline("\tSYSCALL\n");
|
|
emitlabel(okl);
|
|
};
|
|
if (viatryprop) {
|
|
// #45: null = nomem; propagate to the
|
|
// enclosing fn's tagged return. AX = tag
|
|
// of nomem variant in c.fnret, epilogue
|
|
// RETs to caller.
|
|
let okl: str = mklabel(c, "tryprop_ok");
|
|
emitline("\tCMPQ\t$0, AX\n");
|
|
emitline("\tJNE\t");
|
|
emitline(okl);
|
|
emitline("\n");
|
|
// #66 Phase-N step 3: nomem propagation has no
|
|
// pattern node, so it can't ride the typeeq
|
|
// flatvariantidx path. cstage passes the ty_nomem
|
|
// singleton to cg_tag_for_variant; the wwstage cgen
|
|
// holds no tinfo singleton, so find the nomem
|
|
// variant by its NAMED name over tinfo.params.
|
|
let nidx: i32 = -1;
|
|
let nti: *tinfo = nil;
|
|
if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; };
|
|
for (nti != nil && nti.kind == tykind.TY_NAMED) { nti = nti.under; };
|
|
if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) {
|
|
let np: *tparam = nti.params;
|
|
let nidx2: i32 = 0;
|
|
for (np != nil) {
|
|
let nvt: *tinfo = np.type_;
|
|
if (nvt != nil) {
|
|
if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; };
|
|
};
|
|
np = np.tnext;
|
|
nidx2 += 1;
|
|
};
|
|
}; };
|
|
if (nidx < 0) { nidx = 1; };
|
|
emitline("\tMOVQ\t$");
|
|
emitint(nidx: i64);
|
|
emitline(", AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
|
|
emitlabel(okl);
|
|
};
|
|
emitline("\tPOPQ\tBX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
// Tagged-union init: delegate to cgwidentaggedstore, which
|
|
// handles nullable fold, tagged source (ident or AX/DX/CX
|
|
// ABI call), struct payload (literal/ident), str payload,
|
|
// scalar payload — with tag remap for tagged-subset widening.
|
|
if (istaggedtype(c, tn)) {
|
|
cgwidentaggedstore(c, tn.type_: *tinfo, rhs, "BP", off, sz);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// 32B tuple init for `let t: (scalar, str) = call()` /
|
|
// `let t: (str, scalar) = call()` (#105 / #164/#107). Each
|
|
// element rides its SysV class — a float its SSE cursor reg
|
|
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
|
|
// (tupreg), a slice/str its 3-word {ptr,len,cap} header over
|
|
// consecutive INTEGER cursor regs — on INDEPENDENT counters.
|
|
// tupstore routes each element from its real class into its
|
|
// positional slot (eoff steps by the element's slot size: a
|
|
// slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple
|
|
// (#1/Phase 3, task #5). Mirror of the cstage unified branch.
|
|
if (n.lhs != nil) {
|
|
if (n.lhs.kind == nkind.N_TTUPLE) {
|
|
let p0: *node = n.lhs.list;
|
|
let p1: *node = nil;
|
|
if (p0 != nil) { p1 = p0.next; };
|
|
let p0t: *node = nil;
|
|
let p1t: *node = nil;
|
|
if (p0 != nil) { p0t = p0.lhs; };
|
|
if (p1 != nil) { p1t = p1.lhs; };
|
|
let s0_is_str: bool = isstrtype(c, p0t)
|
|
|| isslicetype(c, p0t);
|
|
let s1_is_str: bool = isstrtype(c, p1t)
|
|
|| isslicetype(c, p1t);
|
|
if (p0 != nil) {
|
|
if (p1 != nil) {
|
|
if (s0_is_str != s1_is_str) {
|
|
cgexpr(c, rhs);
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
let eoff: i32 = 0;
|
|
let q: *node = n.lhs.list;
|
|
for (q != nil) {
|
|
let qt: *node = q.lhs;
|
|
let isflt: bool = isfloattype(c, qt);
|
|
let wide: bool = isstrtype(c, qt)
|
|
|| isslicetype(c, qt);
|
|
tupstore(c, gpcur, ssecur,
|
|
off + eoff, wide, qt);
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + tupebytes(wide);
|
|
};
|
|
if (wide) {
|
|
eoff = eoff + (tyslicesize(): i32);
|
|
} else {
|
|
eoff = eoff + 8;
|
|
};
|
|
q = q.next;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// 16B tuple init from a function call (#105 / #164/#107). Each
|
|
// eightbyte rides its SysV class: a float its SSE cursor reg
|
|
// (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg
|
|
// (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves
|
|
// floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ
|
|
// spill would store garbage where a float rode and the #103-
|
|
// FACE-Z field read (MOVSD-from-slot) would see it. tupstore
|
|
// routes each word from its real class; the same split drives
|
|
// the destructure / reassign sites. Without this branch a 16B
|
|
// tuple receive fell to the generic single-word store below and
|
|
// dropped word1 — silent loss of t.1 (#102).
|
|
let rt16: *node = rettupleof(c, rhs);
|
|
if (rt16 != nil && sz == 16) {
|
|
cgexpr(c, rhs);
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
let eoff: i32 = 0;
|
|
let q: *node = rt16.list;
|
|
for (q != nil) {
|
|
let qt: *node = q.lhs;
|
|
let isflt: bool = isfloattype(c, qt);
|
|
tupstore(c, gpcur, ssecur, off + eoff, false, qt);
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + 1;
|
|
};
|
|
eoff = eoff + 8;
|
|
q = q.next;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
|
|
// Walk elements in declaration order, store each at off + i*esz
|
|
// using the right width for the element type. Trailing `...`
|
|
// after the last value (an nkind.N_FIELD with str=="...") fills the
|
|
// remaining slots up to the declared length with that value.
|
|
//
|
|
// str element (16B = ptr+len) needs both halves stored. cgstrlit
|
|
// / cgident leave a str as (AX=ptr, BX=len) and a single MOVQ
|
|
// from AX would leave .len as whatever the stack held — silent
|
|
// miscompile. Worse, primsize("str") returns 0 so esz would fall
|
|
// back to 8, also collapsing the per-element stride (element i+1
|
|
// would overwrite element i's would-be .len half). Detect the
|
|
// str-element case up front so both esz and the store path are
|
|
// right. (primsize's default-to-8-on-zero pattern is brittle for
|
|
// composites generally; same gap blocks slice / struct / tuple /
|
|
// tagged element arrays — tracked as a follow-up.)
|
|
if (rhs.kind == nkind.N_ARRLIT) {
|
|
let elemn: *node = n.lhs.lhs;
|
|
let esz: i32 = 8;
|
|
let isstrel: bool = false;
|
|
if (elemn != nil) {
|
|
if (elemn.kind == nkind.N_TNAME) {
|
|
if (streq(elemn.str, "str")) {
|
|
esz = primtypesize("str"): i32;
|
|
isstrel = true;
|
|
} else {
|
|
let ps: i32 = primsize(elemn.str);
|
|
if (ps > 0) { esz = ps; };
|
|
};
|
|
};
|
|
};
|
|
let mop: str = tnodestoreop(c, elemn, esz);
|
|
// float element → store FROM X0 (MOVSS/MOVSD): cgexpr
|
|
// leaves a float in X0 and for f32 the #104 CVTSD2SS
|
|
// narrowing only touches X0; the AX store (mop) would
|
|
// write the raw double low-bits, garbage for f32 (#122,
|
|
// mirrors cstage cgen.c:6889 arr-lit float store).
|
|
let isfloatel: bool = isfloattype(c, elemn);
|
|
let fmov: str = "MOVSD";
|
|
if (isf32type(c, elemn)) { fmov = "MOVSS"; };
|
|
let idx: i32 = 0;
|
|
let repeat: bool = false;
|
|
let e: *node = rhs.list;
|
|
for (e != nil) {
|
|
let isellip: bool = false;
|
|
if (e.kind == nkind.N_FIELD) {
|
|
if (streq(e.str, "...")) {
|
|
repeat = true;
|
|
isellip = true;
|
|
};
|
|
};
|
|
if (isellip) {
|
|
e = nil;
|
|
} else {
|
|
cgexpr(c, e);
|
|
if (isstrel) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + idx * esz + 8): i64);
|
|
emitline("(BP)\n");
|
|
} else { if (isfloatel) {
|
|
emitline("\t");
|
|
emitline(fmov);
|
|
emitline("\tX0, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\t");
|
|
emitline(mop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
}; };
|
|
idx += 1;
|
|
e = e.next;
|
|
};
|
|
};
|
|
// AX (and BX for str) still holds the last stored value;
|
|
// fill remaining slots up to the declared length with it.
|
|
if (repeat) {
|
|
let total: i32 = idx;
|
|
if (n.lhs != nil) {
|
|
if (n.lhs.kind == nkind.N_TARRAY) {
|
|
if (n.lhs.rhs != nil) {
|
|
if (n.lhs.rhs.kind == nkind.N_INTLIT) {
|
|
total = n.lhs.rhs.uval: i32;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
for (idx < total) {
|
|
if (isstrel) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + idx * esz + 8): i64);
|
|
emitline("(BP)\n");
|
|
} else { if (isfloatel) {
|
|
emitline("\t");
|
|
emitline(fmov);
|
|
emitline("\tX0, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\t");
|
|
emitline(mop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
}; };
|
|
idx += 1;
|
|
};
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// Struct literal init: `let p: point = point{x=..., y=...};`.
|
|
// Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS
|
|
// autofill + per-field walk, with nested struct-typed structlit
|
|
// values recursing into the helper instead of landing only AX
|
|
// (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET
|
|
// structlit branch.
|
|
if (rhs.kind == nkind.N_STRUCTLIT) {
|
|
let trefn: *node = rhs.lhs;
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (trefn != nil) {
|
|
if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; }
|
|
else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; };
|
|
};
|
|
let si: *structinfo = structlookup(c, sname);
|
|
if (si != nil) {
|
|
cgstructlitfillbp(c, si, rhs, off);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
// sret receive (#23): plain TY_STRUCT > 24B from a call.
|
|
// The let's own slot IS the caller-prealloc dest; the
|
|
// nested cgexpr → cgcall path emits `LEAQ off(BP), DI`
|
|
// before the CALL and the callee writes through it. No
|
|
// AX/DX/CX shuffle; AX returns the dest pointer per SysV
|
|
// sret discipline (irrelevant here).
|
|
if (rhs.kind == nkind.N_CALL) {
|
|
let scs: i32 = callsretsize(c, rhs);
|
|
if (scs > 0) {
|
|
c.sretdestoff = off;
|
|
cgexpr(c, rhs);
|
|
c.sretdestoff = 0;
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
// Whole-struct receive for sizes <=24B (call-result rhs).
|
|
// Counterpart of #4's cgreturn ABI: cgexpr leaves
|
|
// AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23],
|
|
// zero-padded to 24B by the producer.
|
|
//
|
|
// ASYMMETRY (do NOT mirror the sender): producer emits three
|
|
// uniform MOVQs into a zero-padded 24B scratch slot; the
|
|
// receiver writes only `sz` bytes — MOVQ for full 8B chunks
|
|
// plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
|
|
// struct size. Otherwise a trailing 1..7-byte chunk would
|
|
// overrun into the next local slot.
|
|
//
|
|
// Tail chunks in {3,5,6,7} (unreachable under WW struct
|
|
// alignment rules — field aligns force size%align==0) fall
|
|
// through to the generic scalar store rather than emit a
|
|
// stomping MOVQ tail. Sizes >24B also fall through (sret
|
|
// deferred, same constraint as #4). Mirrors the cstage
|
|
// cgen.c N_LET receive branch.
|
|
if (rhs.kind == nkind.N_CALL) {
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (tn != nil) {
|
|
if (tn.kind == nkind.N_TNAME) {
|
|
sname = tn.str;
|
|
};
|
|
};
|
|
if (sname.len > 0) {
|
|
let lsi: *structinfo = structlookup(c, sname);
|
|
if (lsi != nil) {
|
|
// ≤24B register RECV: the value arrives packed
|
|
// in AX/DX/CX, so size by the maxalign-rounded
|
|
// ABI size (cstage lu->size), not the natural
|
|
// extent — see structabisize (#169).
|
|
let lsz: i32 = structabisize(lsi);
|
|
let tlm: i32 = lsz - (lsz / 8) * 8;
|
|
if (lsz <= 24) {
|
|
if (tlm == 0 || tlm == 1
|
|
|| tlm == 2 || tlm == 4) {
|
|
cgexpr(c, rhs);
|
|
let full: i32 = lsz / 8;
|
|
let i: i32 = 0;
|
|
for (i < full) {
|
|
let reg: str = "AX";
|
|
if (i == 1) { reg = "DX"; };
|
|
if (i == 2) { reg = "CX"; };
|
|
emitline("\tMOVQ\t");
|
|
emitline(reg);
|
|
emitline(", ");
|
|
emitoff((off + i * 8): i64);
|
|
emitline("(BP)\n");
|
|
i += 1;
|
|
};
|
|
if (tlm > 0) {
|
|
let top: str = "MOVB";
|
|
if (tlm == 4) { top = "MOVL"; };
|
|
if (tlm == 2) { top = "MOVW"; };
|
|
let treg: str = "AX";
|
|
if (full == 1) { treg = "DX"; };
|
|
if (full == 2) { treg = "CX"; };
|
|
emitline("\t");
|
|
emitline(top);
|
|
emitline("\t");
|
|
emitline(treg);
|
|
emitline(", ");
|
|
emitoff((off + full * 8): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// Struct ident copy: `let p2: T = p1;` where T is a struct
|
|
// >8B and rhs is a local ident. Per-qword MOVQ from src
|
|
// slot to dst slot, with a sized tail (MOVL/MOVB) for
|
|
// natural sizes that aren't 8-aligned (e.g. `struct
|
|
// { i32, i32, i32 }` is 12B). Pre-fix this path fell
|
|
// through to `cgexpr + MOVQ AX, off(BP)` which stored
|
|
// only the first qword (and a stale BX for sz==16 lets
|
|
// via the str-init tail) — silent partial copy. Mirrors
|
|
// cstage cgen.c N_LET struct-ident branch (Task #32).
|
|
if (rhs.kind == nkind.N_IDENT) {
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (tn != nil) {
|
|
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
|
|
};
|
|
if (sname.len > 0) {
|
|
let lsi: *structinfo = structlookup(c, sname);
|
|
if (lsi != nil) {
|
|
let lsz: i32 = structnaturalsize(lsi);
|
|
if (lsz > 8) {
|
|
let lc: *local = localfindnode(c, rhs.str);
|
|
if (lc != nil) {
|
|
let soff: i32 = lc.off;
|
|
let ki: i32 = 0;
|
|
for (ki + 8 <= lsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((soff + ki): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + ki): i64);
|
|
emitline("(BP)\n");
|
|
ki += 8;
|
|
};
|
|
if (ki < lsz) {
|
|
let tail: i32 = lsz - ki;
|
|
let lop: str = "MOVQ";
|
|
if (tail == 4) { lop = "MOVL"; }
|
|
else { if (tail == 1) { lop = "MOVB"; }; };
|
|
emitline("\t");
|
|
emitline(lop);
|
|
emitline("\t");
|
|
emitoff((soff + ki): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\t");
|
|
emitline(lop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + ki): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
cgexpr(c, rhs);
|
|
// Float local: cgexpr leaves the value in X0. Spill via
|
|
// MOVSS (f32, 4B) or MOVSD (f64, 8B).
|
|
if (isfloattype(c, n.lhs)) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, n.lhs)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
// str IS []u8: cgexpr leaves (ptr,len,cap) in AX/BX/CX; store
|
|
// all three, same as the slice arm below (#1/Phase 3).
|
|
// #60: gate by kind too — under #1's str=24 bump, sizeof(str)
|
|
// and sizeof(slice) collide, so a bare `sz ==` check fires
|
|
// both branches for one let. Mirrors cstage cgen.c's
|
|
// `type_isstr(lt) && sz == ty_str->size` shape.
|
|
if (isstrtype(c, tn) && sz == primtypesize("str"): i32) {
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
// slice init: ptr/len/cap in AX/BX/CX. Same kind+size gate as
|
|
// the str arm — without the kind check this fires on a str let
|
|
// once sz==24 (#60).
|
|
if (isslicetype(c, tn) && sz == tyslicesize(): i32) {
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
} else {
|
|
// Bare `let x: T;` with no initializer. C cgen
|
|
// (cmd/w6c/cgen.c N_LET no-rhs branch) zero-inits in two
|
|
// shapes:
|
|
// - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.):
|
|
// single `MOVQ $0, off(BP)`.
|
|
// - multi-word composites (str/slice/tuple/struct/tagged):
|
|
// `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot
|
|
// so reads after the bare let see {0...} rather than
|
|
// stack garbage.
|
|
// `[N]T` arrays of size != 8 keep the per-index-write
|
|
// contract — they're left uninit.
|
|
let isarr: bool = false;
|
|
if (n.lhs != nil) {
|
|
if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; };
|
|
};
|
|
if (typeis8byteprimitive(c, n.lhs)) {
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
} else { if (!isarr) { if (sz > 8) {
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
let zi: i32 = 0;
|
|
for (zi + 8 <= sz) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 8;
|
|
};
|
|
for (zi + 4 <= sz) {
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 4;
|
|
};
|
|
for (zi < sz) {
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 1;
|
|
};
|
|
}; }; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cgif(c: *cgen, n: *node) void = {
|
|
let els: str = mklabel(c, "else");
|
|
let endl: str = mklabel(c, "end");
|
|
cgexpr(c, n.cond);
|
|
emitline("\tCMPQ\t$0, AX\n");
|
|
emitline("\tJE\t");
|
|
if (n.els != nil) { emitline(els); }
|
|
else { emitline(endl); };
|
|
emitline("\n");
|
|
if (n.body != nil) { cgstmt(c, n.body); };
|
|
if (n.els != nil) {
|
|
emitline("\tJMP\t"); emitline(endl); emitline("\n");
|
|
emitlabel(els);
|
|
cgstmt(c, n.els);
|
|
};
|
|
emitlabel(endl);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cgfor(c: *cgen, n: *node) void = {
|
|
// Match C cgen's label scheme: <fn>_loop_N for the top,
|
|
// <fn>_endloop_N for the post-body merge. No separate cont
|
|
// label when there's no post-expression.
|
|
let topl: str = mklabel(c, "loop");
|
|
let endl: str = mklabel(c, "endloop");
|
|
// `else` runs at natural cond-false exit; break skips it. When
|
|
// present, branch the cond-fail edge to a separate natural_exit
|
|
// label so the else body sits between it and the break target.
|
|
let naturall: str = endl;
|
|
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
|
|
// #138: `continue` in a 3-clause `for (init; cond; post)` must
|
|
// run the post-step before re-testing cond. Pre-fix the continue-
|
|
// target was `topl`, which SKIPPED the post-step → state never
|
|
// advanced → infinite loop. Allocate a dedicated `post` label
|
|
// only when there IS a post-step (`n.rhs != nil`); else keep
|
|
// continue → loop-top, byte-id with 1-clause for.
|
|
let conttgt: str = topl;
|
|
if (n.rhs != nil) { conttgt = mklabel(c, "post"); };
|
|
|
|
if (n.lhs != nil) { cgstmt(c, n.lhs); };
|
|
|
|
emitlabel(topl);
|
|
if (n.cond != nil) {
|
|
cgexpr(c, n.cond);
|
|
emitline("\tCMPQ\t$0, AX\n");
|
|
emitline("\tJE\t"); emitline(naturall); emitline("\n");
|
|
};
|
|
|
|
c.loopendbuf[c.looptop] = endl;
|
|
c.loopcontbuf[c.looptop] = conttgt;
|
|
c.looptop += 1;
|
|
|
|
if (n.body != nil) { cgstmt(c, n.body); };
|
|
|
|
c.looptop -= 1;
|
|
|
|
if (n.rhs != nil) {
|
|
emitlabel(conttgt);
|
|
cgexpr(c, n.rhs);
|
|
};
|
|
emitline("\tJMP\t"); emitline(topl); emitline("\n");
|
|
if (n.els != nil) {
|
|
emitlabel(naturall);
|
|
cgstmt(c, n.els);
|
|
};
|
|
emitlabel(endl);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// Tuple-destructure assign: `a, b = call();`. The call's tuple
|
|
// return lands in (AX, DX); push DX to free it, store AX into
|
|
// the first lvalue, then pop DX into the second. Mirrors
|
|
// cmd/w6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
|
|
// as C — no fixture uses >2 today).
|
|
fn cgmassign(c: *cgen, n: *node) void = {
|
|
// #83: positional per-element destructure REASSIGN. Same cursor as
|
|
// cgmlet (and cgreturn; harec create_unpack_bindings,
|
|
// ref/harec/src/check.c:1354-1416), but the slots already exist
|
|
// (reassignment) so localfind them. wwstage has no checker, so each
|
|
// element's width comes from the called fn's return-type tuple
|
|
// element (N_TTUPLE param) walked in lockstep with the bindings; a
|
|
// slice/str rides its 3-word {ptr,len,cap} header
|
|
// (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes
|
|
// its register slot without storing (mirrors harec `_`). This bare-
|
|
// comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond
|
|
// Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike
|
|
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
|
|
let rettuple: *node = rettupleof(c, n.rhs);
|
|
|
|
if (n.rhs != nil) { cgexpr(c, n.rhs); };
|
|
|
|
let ssecap: i32 = 2; // X0,X1 per SysV
|
|
let gptotal: i32 = 0;
|
|
let ssetotal: i32 = 0;
|
|
let l: *node = n.list;
|
|
let pt: *node = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
for (l != nil) {
|
|
let tn: *node = nil;
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
if (isfloattype(c, tn)) {
|
|
ssetotal = ssetotal + 1;
|
|
} else {
|
|
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
|
|
gptotal = gptotal + tupebytes(wide);
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
if (gptotal > 4) { // AX,DX,CX,R8 capacity
|
|
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
|
|
// fatal(), err.c) — surface, don't corrupt.
|
|
let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
if (ssetotal > ssecap) {
|
|
let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
l = n.list;
|
|
pt = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
for (l != nil) {
|
|
let tn: *node = nil;
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
let isflt: bool = isfloattype(c, tn);
|
|
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
|
|
let off: i32 = 0;
|
|
if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); };
|
|
// harec `_` (off==0): skip the store but CONSUME the cursor
|
|
// slot so the next element stays aligned.
|
|
if (off != 0) {
|
|
tupstore(c, gpcur, ssecur, off, wide, tn);
|
|
};
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + tupebytes(wide);
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// Multi-let from a tuple-returning call: `let n, s = call();` or
|
|
// `let (n, s) = call();`. wwstage has no checker, so each binding's
|
|
// type is taken from its explicit annotation (l.lhs) when present
|
|
// or inferred from the called fn's return-type tuple element.
|
|
//
|
|
// Per the AX:DX:CX:R8 return convention (mirrors C cgen nkind.N_MLET):
|
|
// (scalar, scalar) — AX → l0, DX → l1.
|
|
// (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot
|
|
// as (.ptr, .len, .cap). Position-agnostic — the
|
|
// regs are routed by element type, not by AX/DX.
|
|
// str IS []u8 (24B): cap rides R8 (#1/Phase 3, task #5).
|
|
fn cgmlet(c: *cgen, n: *node) void = {
|
|
let rhs: *node = n.rhs;
|
|
if (rhs == nil) { return; };
|
|
|
|
// #83: positional per-element destructure let-binding. Same cursor
|
|
// as cgmassign (and cgreturn; harec create_unpack_bindings,
|
|
// ref/harec/src/check.c:1354-1416). wwstage has no checker, so each
|
|
// binding's type is its explicit annotation (l.lhs) when present,
|
|
// else the called fn's return-type tuple element (N_TTUPLE param)
|
|
// walked in lockstep. A slice/str rides its 3-word {ptr,len,cap}
|
|
// header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a
|
|
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
|
|
let rettuple: *node = rettupleof(c, rhs);
|
|
|
|
cgexpr(c, rhs);
|
|
|
|
let ssecap: i32 = 2; // X0,X1 per SysV
|
|
let gptotal: i32 = 0;
|
|
let ssetotal: i32 = 0;
|
|
let l: *node = n.list;
|
|
let pt: *node = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
for (l != nil) {
|
|
let tn: *node = l.lhs;
|
|
if (tn == nil) {
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
};
|
|
if (isfloattype(c, tn)) {
|
|
ssetotal = ssetotal + 1;
|
|
} else {
|
|
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
|
|
gptotal = gptotal + tupebytes(wide);
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
if (gptotal > 4) { // AX,DX,CX,R8 capacity
|
|
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
|
|
// fatal(), err.c) — surface, don't corrupt.
|
|
let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
if (ssetotal > ssecap) {
|
|
let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
l = n.list;
|
|
pt = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
for (l != nil) {
|
|
let tn: *node = l.lhs;
|
|
if (tn == nil) {
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
};
|
|
let isflt: bool = isfloattype(c, tn);
|
|
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
|
|
let sz: i32 = 8;
|
|
if (wide) { sz = tyslicesize(): i32; };
|
|
let off: i32 = localadd(c, l.str, sz, tn);
|
|
tupstore(c, gpcur, ssecur, off, wide, tn);
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + tupebytes(wide);
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// paramfieldsize — raw byte size of a tuple-field type. Mirrors the
|
|
// `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for
|
|
// i32/u32, 8 for i64/u64/*T/fn/slice-elt, 16 for str, default 8.
|
|
fn paramfieldsize(t: *node) i32 = {
|
|
if (t == nil) { return 8; };
|
|
let k: nkind = t.kind;
|
|
if (k == nkind.N_TPTR) { return 8; };
|
|
if (k == nkind.N_TFN) { return 8; };
|
|
if (k == nkind.N_TCHAN) { return 8; };
|
|
if (k == nkind.N_TNAME) {
|
|
let nm: str = t.str;
|
|
if (streq(nm, "str")) { return primtypesize("str"): i32; };
|
|
let ps: i32 = primsize(nm);
|
|
if (ps > 0) { return ps; };
|
|
};
|
|
return 8;
|
|
};
|
|
|
|
// paramissigned — does this type need sign-extending on a sub-word
|
|
// (1/2/4B) load? Mirrors cstage's signed_field check via
|
|
// fieldissignedc (resolves TBANG / TENUM / alias chains).
|
|
fn paramissigned(c: *cgen, t: *node) bool = {
|
|
return fieldissignedc(c, t);
|
|
};
|
|
|
|
// cgforrange — lower `for (let x .. slice) body` (and the tuple-
|
|
// destructure cousin `for (let (a, b) .. slice) body`). The body is
|
|
// wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`.
|
|
// Each iteration computes the element address `s.ptr + i*esz` and
|
|
// either loads the whole element into the named local or pulls each
|
|
// tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE
|
|
// byte-for-byte (label names + labelseq consumption order).
|
|
fn cgforrange(c: *cgen, n: *node) void = {
|
|
let slc: *node = n.lhs;
|
|
let slclocal: *local = nil;
|
|
let slctn: *node = nil;
|
|
if (slc != nil) {
|
|
if (slc.kind == nkind.N_IDENT) {
|
|
slclocal = localfindnode(c, slc.str);
|
|
if (slclocal != nil) { slctn = slclocal.tnode; };
|
|
};
|
|
};
|
|
// Element type — peek through TSLICE/TARRAY for the tuple param walk.
|
|
let elemt: *node = nil;
|
|
if (slctn != nil) {
|
|
let sk: nkind = slctn.kind;
|
|
if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; };
|
|
if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; };
|
|
// str IS []u8 (F1: tystr.sub = tyu8). []u8 hands cgen a real
|
|
// u8 element node (slctn.lhs); a str scrutinee has none, so the
|
|
// loop var would register tnode=nil and read back as a wide
|
|
// MOVQ. Synthesise the u8 element off str.sub so the loop-var
|
|
// registration carries a u8 tnode and localloadop narrows the
|
|
// read-back to MOVZBQ on its own — aligning wwstage up to
|
|
// cstage, whose checker stamps the binding u8. Kind-gated so
|
|
// str's own type stays nominal.
|
|
if (sk == nkind.N_TNAME) {
|
|
if (streq(slctn.str, "str")) {
|
|
let sti: *tinfo = slctn.type_: *tinfo;
|
|
if (sti != nil) {
|
|
if (sti.sub != nil) {
|
|
let u8n: *node = newnode(nkind.N_TNAME, slctn.file, slctn.line, slctn.col);
|
|
u8n.str = "u8";
|
|
u8n.type_ = sti.sub: *void;
|
|
elemt = u8n;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// esz: raw elem byte size. For tuple-element slices `[](T0, T1)`,
|
|
// C cgen reads the resolved tuple's size (sum of raw param sizes,
|
|
// no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8.
|
|
// elemsizeof returns 8 for non-primitive elem, which would be
|
|
// wrong here — compute from the tuple param walk instead.
|
|
let esz: i32 = elemsizeof(slctn);
|
|
if (elemt != nil) {
|
|
if (elemt.kind == nkind.N_TTUPLE) {
|
|
let total: i32 = 0;
|
|
let p: *node = elemt.list;
|
|
for (p != nil) {
|
|
total += paramfieldsize(p.lhs);
|
|
p = p.next;
|
|
};
|
|
esz = total;
|
|
};
|
|
};
|
|
let destruct: bool = (n.list != nil);
|
|
|
|
// .rgi (counter) + .rgl (length) scratch slots.
|
|
let iname: str = mkscratchname(c, "rgi");
|
|
let lname: str = mkscratchname(c, "rgl");
|
|
let ioff: i32 = localalloc(c, iname, 8, nil);
|
|
let loff: i32 = localalloc(c, lname, 8, nil);
|
|
|
|
// Per-binding (up to 8 — matches the C array). Parallel arrays so
|
|
// we don't depend on local-struct cgen.
|
|
let bind_off: [8]i32;
|
|
let bind_sz: [8]i32;
|
|
let bind_foff: [8]i32;
|
|
let bind_signed: [8]bool;
|
|
let nbinds: i32 = 0;
|
|
|
|
if (destruct) {
|
|
let tp: *node = nil;
|
|
if (elemt != nil) {
|
|
if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; };
|
|
};
|
|
let field_off: i32 = 0;
|
|
let m: *node = n.list;
|
|
for (m != nil) {
|
|
if (nbinds >= 8) { m = nil; }
|
|
else {
|
|
let fsz: i32 = 8;
|
|
let signf: bool = false;
|
|
// tp walks the N_TPARAM wrapper chain; tpt is the
|
|
// actual element type AST.
|
|
let tpt: *node = nil;
|
|
if (tp != nil) { tpt = tp.lhs; };
|
|
if (tpt != nil) {
|
|
fsz = paramfieldsize(tpt);
|
|
signf = paramissigned(c, tpt);
|
|
};
|
|
let slot_sz: i32 = fsz;
|
|
if (slot_sz < 8) { slot_sz = 8; };
|
|
bind_sz[nbinds] = fsz;
|
|
bind_foff[nbinds] = field_off;
|
|
bind_signed[nbinds] = signf;
|
|
let bnm: str = m.str;
|
|
if (bnm.len > 0) {
|
|
bind_off[nbinds] = localadd(c, bnm, slot_sz, tpt);
|
|
} else {
|
|
bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tpt);
|
|
};
|
|
field_off += fsz;
|
|
nbinds += 1;
|
|
if (tp != nil) { tp = tp.next; };
|
|
m = m.next;
|
|
};
|
|
};
|
|
} else {
|
|
let slot_sz: i32 = esz;
|
|
if (slot_sz < 8) { slot_sz = 8; };
|
|
bind_sz[0] = esz;
|
|
bind_foff[0] = 0;
|
|
// Single-binding signed-narrow detection: mirror C which
|
|
// reads `u->sub->kind` for the elem type.
|
|
bind_signed[0] = false;
|
|
if (elemt != nil) {
|
|
bind_signed[0] = paramissigned(c, elemt);
|
|
};
|
|
if (n.str.len > 0) {
|
|
// Register with elem tnode so x.field on a loop
|
|
// var resolves through the standard local-typed
|
|
// path instead of falling into the SB fallback.
|
|
bind_off[0] = localadd(c, n.str, slot_sz, elemt);
|
|
} else {
|
|
bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt);
|
|
};
|
|
nbinds = 1;
|
|
};
|
|
|
|
// init: ioff(BP) = 0
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff(ioff: i64);
|
|
emitline("(BP)\n");
|
|
|
|
// loff(BP) = len
|
|
let isarr: bool = false;
|
|
let isslicestr: bool = false;
|
|
if (slctn != nil) {
|
|
let tk: nkind = slctn.kind;
|
|
if (tk == nkind.N_TSLICE) { isslicestr = true; };
|
|
if (tk == nkind.N_TARRAY) { isarr = true; };
|
|
if (tk == nkind.N_TNAME) {
|
|
if (streq(slctn.str, "str")) { isslicestr = true; };
|
|
};
|
|
};
|
|
if (isslicestr) {
|
|
if (slc.kind == nkind.N_IDENT) {
|
|
if (slclocal != nil) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((slclocal.off + 8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(loff: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
};
|
|
} else { if (isarr) {
|
|
let alen: i64 = 0i64;
|
|
if (slctn.rhs != nil) {
|
|
if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; };
|
|
};
|
|
emitline("\tMOVQ\t$");
|
|
emitint(alen);
|
|
emitline(", ");
|
|
emitoff(loff: i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
cgexpr(c, slc);
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(loff: i64);
|
|
emitline("(BP)\n");
|
|
};};
|
|
|
|
let loopl: str = mklabel(c, "rloop");
|
|
let endl: str = mklabel(c, "rend");
|
|
let naturall: str = endl;
|
|
if (n.els != nil) { naturall = mklabel(c, "relseloop"); };
|
|
// #138 (range form): `continue` must run the implicit `i+=1`
|
|
// post-step before re-testing the bound. Pre-fix cont = loopl
|
|
// (top), skipping the ADDQ $1, ioff below — infinite loop on
|
|
// the value that triggered continue. Dedicated `rpost` label.
|
|
let rpost: str = mklabel(c, "rpost");
|
|
|
|
c.loopcontbuf[c.looptop] = rpost;
|
|
c.loopendbuf[c.looptop] = endl;
|
|
c.looptop += 1;
|
|
|
|
emitlabel(loopl);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(ioff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff(loff: i64);
|
|
emitline("(BP), BX\n");
|
|
emitline("\tCMPQ\tBX, AX\n");
|
|
emitline("\tJGE\t"); emitline(naturall); emitline("\n");
|
|
|
|
// BX = base + i*esz
|
|
if (esz > 1) {
|
|
emitline("\tMOVQ\t$");
|
|
emitint(esz: i64);
|
|
emitline(", CX\n");
|
|
emitline("\tIMULQ\tCX, AX\n");
|
|
};
|
|
if (slc.kind == nkind.N_IDENT) {
|
|
if (slclocal != nil) {
|
|
if (isarr) {
|
|
emitline("\tLEAQ\t");
|
|
emitoff(slclocal.off: i64);
|
|
emitline("(BP), BX\n");
|
|
} else {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(slclocal.off: i64);
|
|
emitline("(BP), BX\n");
|
|
};
|
|
};
|
|
};
|
|
emitline("\tADDQ\tAX, BX\n");
|
|
|
|
// Per-binding load from BX+foff. Signedness comes from bind_signed
|
|
// (set via paramissigned → fieldissignedc), so enum-aliased narrows
|
|
// pick the right MOVS*Q without a literal-name gate.
|
|
let b: i32 = 0;
|
|
for (b < nbinds) {
|
|
let op: str = loadopsz(bind_signed[b], bind_sz[b]);
|
|
emitline("\t");
|
|
emitline(op);
|
|
emitline("\t");
|
|
emitoff(bind_foff[b]: i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(bind_off[b]: i64);
|
|
emitline("(BP)\n");
|
|
b += 1;
|
|
};
|
|
|
|
if (n.body != nil) { cgstmt(c, n.body); };
|
|
|
|
c.looptop -= 1;
|
|
|
|
emitlabel(rpost);
|
|
emitline("\tADDQ\t$1, ");
|
|
emitoff(ioff: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tJMP\t"); emitline(loopl); emitline("\n");
|
|
if (n.els != nil) {
|
|
emitlabel(naturall);
|
|
cgstmt(c, n.els);
|
|
};
|
|
emitlabel(endl);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to
|
|
// a chain of compares against the scrutinee. Scrutinee lands in a
|
|
// fresh 8B local slot so case bodies can spill SP without losing it.
|
|
// Cases are tried top-to-bottom; the `case:` arm with no exprs is the
|
|
// default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c
|
|
// N_SWITCH: same labelseq consumption order so labels match byte-for-
|
|
// byte.
|
|
fn cgswitch(c: *cgen, n: *node) void = {
|
|
let swname: str = mkscratchname(c, "sw");
|
|
let sloff: i32 = localalloc(c, swname, 8, nil);
|
|
|
|
if (n.lhs != nil) { cgexpr(c, n.lhs); };
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(sloff: i64);
|
|
emitline("(BP)\n");
|
|
|
|
let endl: str = mklabel(c, "swend");
|
|
let defcase: *node = nil;
|
|
|
|
let cs: *node = n.list;
|
|
for (cs != nil) {
|
|
if (cs.list == nil) {
|
|
defcase = cs;
|
|
cs = cs.next;
|
|
continue;
|
|
};
|
|
let body: str = mklabel(c, "swcase");
|
|
let nxt: str = mklabel(c, "swnext");
|
|
let e: *node = cs.list;
|
|
for (e != nil) {
|
|
cgexpr(c, e);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sloff: i64);
|
|
emitline("(BP), BX\n");
|
|
emitline("\tCMPQ\tBX, AX\n");
|
|
emitline("\tJE\t");
|
|
emitline(body);
|
|
emitline("\n");
|
|
e = e.next;
|
|
};
|
|
emitline("\tJMP\t");
|
|
emitline(nxt);
|
|
emitline("\n");
|
|
emitlabel(body);
|
|
if (cs.body != nil) { cgstmt(c, cs.body); };
|
|
emitline("\tJMP\t");
|
|
emitline(endl);
|
|
emitline("\n");
|
|
emitlabel(nxt);
|
|
cs = cs.next;
|
|
};
|
|
if (defcase != nil) {
|
|
if (defcase.body != nil) { cgstmt(c, defcase.body); };
|
|
};
|
|
emitlabel(endl);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cgbreak(c: *cgen, n: *node) void = {
|
|
if (c.looptop > 0) {
|
|
let lbl: str = c.loopendbuf[c.looptop - 1];
|
|
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cgcontinue(c: *cgen, n: *node) void = {
|
|
if (c.looptop > 0) {
|
|
let lbl: str = c.loopcontbuf[c.looptop - 1];
|
|
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
|