C-cgen-side nullable folding landed in f4efaac. This commit catches
the selfhost cgen up so a wwstage-compiled binary produces the same
ABI for `(*T | void)`.
- cgenutil: isnullabletype(), nullableptrtag() helpers shaped to
the selfhost cgen's AST-only world view (it doesn't carry a Type
with a .nullable flag — it walks N_TTAGGED node lists). slotsize
returns 8 for nullable.
- cgenexpr cgmatch: nullable arm uses pointer-vs-null discriminator
and binds only the *T case (void has size 0).
- cgenstmt cglet: nullable target spills only AX (no tag word, no
value-word DX/CX).
- cgenstmt cgreturn: nullable return passes AX through with no
shuffle; bare `return;` emits AX = 0 (void encoding).
Verified end-to-end: a fn returning `(*i32 | void)` compiled by the
wwstage cgen produces the same exit code as the C-cgen build. The
995 fixed-point gate stays green — no selfhost source uses nullable
yet, so the existing tagged paths are still byte-identical.
688 lines
18 KiB
Plaintext
688 lines
18 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.
|
|
|
|
use os;
|
|
use mem;
|
|
use ast;
|
|
use tok;
|
|
use typ;
|
|
use sym;
|
|
use strconv;
|
|
|
|
// ---- statement cgen --------------------------------------------------
|
|
|
|
fn cgstmt(c: *cgen, n: *node) void = {
|
|
if (n == nil) { return; };
|
|
let k: i32 = n.kind;
|
|
|
|
if (k == N_BLOCK) { cgblock(c, n); return; };
|
|
|
|
if (k == N_RETURN) { cgreturn(c, n); return; };
|
|
|
|
if (k == N_EXPRSTMT) { cgexprstmt(c, n); return; };
|
|
|
|
if (k == N_LET) { cglet(c, n); return; };
|
|
|
|
if (k == N_IF) { cgif(c, n); return; };
|
|
|
|
if (k == N_FOR) { cgfor(c, n); return; };
|
|
|
|
if (k == N_MASSIGN) { cgmassign(c, n); return; };
|
|
|
|
if (k == N_MLET) { cgmlet(c, n); return; };
|
|
|
|
if (k == N_BREAK) { cgbreak(c, n); return; };
|
|
if (k == N_CONTINUE) { cgcontinue(c, n); return; };
|
|
|
|
c.lastwasreturn = 0;
|
|
};
|
|
|
|
fn cgblock(c: *cgen, n: *node) void = {
|
|
let s: *node = n.list;
|
|
for (s != nil) {
|
|
cgstmt(c, s);
|
|
s = s.next;
|
|
};
|
|
return;
|
|
};
|
|
|
|
fn cgreturn(c: *cgen, n: *node) void = {
|
|
let rhs: *node = n.lhs;
|
|
if (rhs != nil) {
|
|
// Tuple return `return a, b;`:
|
|
// (scalar, scalar) — AX = v0, DX = v1.
|
|
// (scalar, str) / (str, scalar) — AX = scalar elem,
|
|
// DX = str.ptr, CX = str.len.
|
|
// 24B convention mirrors the tagged-union return below; receive
|
|
// sites destructure off the same regs regardless of position.
|
|
if (rhs.kind == 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\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);
|
|
};
|
|
};
|
|
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.fnret)) {
|
|
cgexpr(c, rhs);
|
|
if (isnullabletype(c.fnret)) {
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
|
|
if (nodeisstr(c, rhs)) {
|
|
emitline("\tMOVQ\tBX, CX\n");
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
} else {
|
|
emitline("\tMOVQ\tAX, DX\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;
|
|
};
|
|
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.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");
|
|
};
|
|
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).
|
|
// cgexpr leaves str in (AX, BX); shuffle BX→DX.
|
|
if (isstrtype(c, c.fnret)) {
|
|
emitline("\tMOVQ\tBX, DX\n");
|
|
};
|
|
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 off: i32 = localadd(c, nm, sz, n.lhs);
|
|
if (n.rhs != nil) {
|
|
let rhs: *node = n.rhs;
|
|
// Tagged-union init: `let r: (T | E) = expr;`.
|
|
// - If rhs is a CALL to a fn returning tagged-union,
|
|
// the result is already in (AX=tag, DX=v0, CX=v1);
|
|
// just spill all three.
|
|
// - Otherwise rhs is a bare variant value: pack tag +
|
|
// value(s).
|
|
if (istaggedtype(n.lhs)) {
|
|
let nullable: bool = isnullabletype(n.lhs);
|
|
let rhsreturnstagged: bool = false;
|
|
if (rhs.kind == N_CALL) {
|
|
let callee: *node = rhs.lhs;
|
|
if (callee != nil) {
|
|
let calleename: str;
|
|
calleename.ptr = nil; calleename.len = 0;
|
|
if (callee.kind == N_IDENT) { calleename = callee.str; };
|
|
if (callee.kind == N_DOT) { calleename = callee.str; };
|
|
if (calleename.len > 0) {
|
|
let rt: *node = fnretlookup(c, calleename);
|
|
if (istaggedtype(rt)) { rhsreturnstagged = true; };
|
|
};
|
|
};
|
|
};
|
|
cgexpr(c, rhs);
|
|
if (nullable) {
|
|
// Slot is one 8B word; AX is the pointer
|
|
// (or 0 for null/void). Same path whether
|
|
// the rhs is a call or a bare variant.
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
if (rhsreturnstagged) {
|
|
// Spill size/8 registers (tag + value words).
|
|
// Slots smaller than 24 don't carry a CX word.
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tDX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
if (sz > 16) {
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
let tagidx: i32 = taggedvariantindex(c, n.lhs, rhs);
|
|
if (tagidx < 0) { tagidx = 0; };
|
|
if (nodeisstr(c, rhs)) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
emitline("\tMOVQ\t$");
|
|
emitint(tagidx: i64);
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// 24B tuple init for `let t: (scalar, str) = call()` /
|
|
// `let t: (str, scalar) = call()`. Per the AX:DX:CX return
|
|
// convention: AX = scalar elem, DX = str.ptr, CX = str.len.
|
|
// Layout is positional, so we route each register to the
|
|
// slot dictated by element type, not by AX/DX position.
|
|
if (n.lhs != nil) {
|
|
if (n.lhs.kind == N_TTUPLE) {
|
|
let p0: *node = n.lhs.list;
|
|
let p1: *node = nil;
|
|
if (p0 != nil) { p1 = p0.next; };
|
|
let s0_is_str: bool = isstrtyperaw(p0);
|
|
let s1_is_str: bool = isstrtyperaw(p1);
|
|
if (p0 != nil) {
|
|
if (p1 != nil) {
|
|
if (s0_is_str != s1_is_str) {
|
|
cgexpr(c, rhs);
|
|
if (s0_is_str) {
|
|
emitline("\tMOVQ\tDX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tDX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
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 N_FIELD with str=="...") fills the
|
|
// remaining slots up to the declared length with that value.
|
|
if (rhs.kind == N_ARRLIT) {
|
|
let elemn: *node = n.lhs.lhs;
|
|
let esz: i32 = 8;
|
|
if (elemn != nil) {
|
|
if (elemn.kind == N_TNAME) {
|
|
let ps: i32 = primsize(elemn.str);
|
|
if (ps > 0) { esz = ps; };
|
|
};
|
|
};
|
|
let mop: str = "MOVQ";
|
|
if (esz == 1) { mop = "MOVB"; }
|
|
else { if (esz == 4) { mop = "MOVL"; }; };
|
|
let idx: i32 = 0;
|
|
let repeat: bool = false;
|
|
let e: *node = rhs.list;
|
|
for (e != nil) {
|
|
if (e.kind == N_FIELD) {
|
|
if (streq(e.str, "...")) {
|
|
repeat = true;
|
|
e = nil;
|
|
} else {
|
|
cgexpr(c, e);
|
|
emitline("\t");
|
|
emitline(mop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
e = e.next;
|
|
};
|
|
} else {
|
|
cgexpr(c, e);
|
|
emitline("\t");
|
|
emitline(mop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
e = e.next;
|
|
};
|
|
};
|
|
// AX 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 == N_TARRAY) {
|
|
if (n.lhs.rhs != nil) {
|
|
if (n.lhs.rhs.kind == N_INTLIT) {
|
|
total = n.lhs.rhs.uval: i32;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
for (idx < total) {
|
|
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=...};`.
|
|
// For each field in the lit, evaluate its value and store at
|
|
// the field's offset within the slot. Field-name → offset
|
|
// from the struct registry. When the literal carries
|
|
// op == TK_ELLIPSIS (autofill marker from the parser), the
|
|
// entire slot is zero-filled first so unmentioned fields
|
|
// read as 0.
|
|
if (rhs.kind == N_STRUCTLIT) {
|
|
let trefn: *node = rhs.lhs;
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (trefn != nil) {
|
|
if (trefn.kind == N_IDENT) { sname = trefn.str; }
|
|
else { if (trefn.kind == N_TNAME) { sname = trefn.str; }; };
|
|
};
|
|
let si: *structinfo = structlookup(c, sname);
|
|
if (si != nil) {
|
|
if (rhs.op == TK_ELLIPSIS) {
|
|
let total: i32 = si.totsize;
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
let zi: i32 = 0;
|
|
for (zi + 8 <= total) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 8;
|
|
};
|
|
for (zi + 4 <= total) {
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 4;
|
|
};
|
|
for (zi < total) {
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 1;
|
|
};
|
|
};
|
|
let fieldnode: *node = rhs.list;
|
|
for (fieldnode != nil) {
|
|
if (fieldnode.kind == N_FIELD) {
|
|
let fname: str = fieldnode.str;
|
|
let fi: *fieldinfo = si.fields;
|
|
for (fi != nil) {
|
|
let fn_: str = fi.fname;
|
|
if (streq(fn_, fname)) {
|
|
cgexpr(c, fieldnode.lhs);
|
|
let sop: str = fieldstoreop(fi);
|
|
emitline("\t");
|
|
emitline(sop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + fi.foff): i64);
|
|
emitline("(BP)\n");
|
|
fi = nil;
|
|
} else {
|
|
fi = fi.finext;
|
|
};
|
|
};
|
|
};
|
|
fieldnode = fieldnode.next;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
cgexpr(c, rhs);
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
// str init: cgexpr also leaves len in BX; store both.
|
|
if (sz == 16) {
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
// slice init: ptr/len/cap in AX/BX/CX.
|
|
if (sz == 24) {
|
|
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:2181-2183) zero-inits only when
|
|
// the underlying type's natural size is 8 — pointers,
|
|
// i64/u64, function pointers, ints. Structs/arrays/
|
|
// slices/strings/tagged/tuples are left for per-field
|
|
// writes. ww's slotsize pads struct slots up to 8,
|
|
// so we can't just check sz == 8: walk the type AST
|
|
// directly to make the same call.
|
|
if (typeis8byteprimitive(c, n.lhs)) {
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
};
|
|
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"); };
|
|
|
|
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] = topl;
|
|
c.looptop += 1;
|
|
|
|
if (n.body != nil) { cgstmt(c, n.body); };
|
|
|
|
c.looptop -= 1;
|
|
|
|
if (n.rhs != nil) { 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 = {
|
|
if (n.rhs != nil) { cgexpr(c, n.rhs); };
|
|
emitline("\tPUSHQ\tDX\n");
|
|
let l0: *node = n.list;
|
|
let l1: *node = nil;
|
|
if (l0 != nil) { l1 = l0.next; };
|
|
if (l0 != nil) {
|
|
if (l0.kind == 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 == N_IDENT) {
|
|
let off: i32 = localfind(c, l1.str);
|
|
if (off != 0) {
|
|
emitline("\tMOVQ\tDX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
};
|
|
};
|
|
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 return convention (mirrors C cgen N_MLET):
|
|
// (scalar, scalar) — AX → l0, DX → l1.
|
|
// (scalar, str) — AX → scalar slot, (DX, CX) → str slot
|
|
// as (.ptr, .len). Position-agnostic — the
|
|
// regs are routed by element type, not by AX/DX.
|
|
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 == N_CALL) {
|
|
let callee: *node = rhs.lhs;
|
|
if (callee != nil) {
|
|
let cnm: str;
|
|
cnm.ptr = nil; cnm.len = 0;
|
|
if (callee.kind == N_IDENT) { cnm = callee.str; };
|
|
if (callee.kind == N_DOT) { cnm = callee.str; };
|
|
if (cnm.len > 0) {
|
|
let rt: *node = fnretlookup(c, cnm);
|
|
if (rt != nil) {
|
|
if (rt.kind == N_TTUPLE) {
|
|
p0t = rt.list;
|
|
if (p0t != nil) { p1t = p0t.next; };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
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 = isstrtyperaw(t0);
|
|
let s1_is_str: bool = isstrtyperaw(t1);
|
|
|
|
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 = 16; };
|
|
if (s1_is_str) { sz1 = 16; };
|
|
let off0: i32 = localadd(c, l0.str, sz0, t0);
|
|
let off1: i32 = localadd(c, l1.str, sz1, t1);
|
|
if (s0_is_str) {
|
|
emitline("\tMOVQ\tDX, ");
|
|
emitoff(off0: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off0 + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off1: i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
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");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
|
|
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");
|
|
};
|
|
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;
|
|
};
|
|
|
|
|