After the frontend consolidated into one syntax package (#74), wcc still referenced syntax symbols unqualified — residue of the old flat combined namespace, where bare refs resolved by accident. Under separate compilation Hare and Go both require the package qualifier, so those bare refs would not sep-resolve. Qualify every wcc reference to a syntax type, function, or enum member as syntax.X across the seven syntax-importing files. Resolution-only: the resolved symbol and emitted code are unchanged, so the two combined.ww regenerate textually but all five _ww binaries hold byte-for-byte. The struct-literal sites resolve via #76. This makes w6c fully separate-compilable.
4346 lines
152 KiB
Plaintext
4346 lines
152 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 syntax;
|
|
import strconv;
|
|
|
|
// ---- statement cgen --------------------------------------------------
|
|
|
|
fn cgstmt(c: *cgen, n: *syntax.node) void = {
|
|
if (n == nil) { return; };
|
|
let k: syntax.nkind = n.kind;
|
|
|
|
if (k == syntax.nkind.N_BLOCK) { cgblock(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_RETURN) { cgreturn(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_LET) { cglet(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_IF) { cgif(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_FOR) { cgfor(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_FORRANGE) { cgforrange(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_SWITCH) { cgswitch(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_MASSIGN) { cgmassign(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_MLET) { cgmlet(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_BREAK) { cgbreak(c, n); return; };
|
|
if (k == syntax.nkind.N_CONTINUE) { cgcontinue(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_YIELD) { cgyield(c, n); return; };
|
|
|
|
if (k == syntax.nkind.N_DEFER) {
|
|
// #40: at the cap, fail loud in BOTH stages rather than
|
|
// silently drop the deferred call. cstage's DEFER_MAX was 32
|
|
// and also dropped silently past it; the runtime-correct target
|
|
// is a hard stop at the shared cap (cgen.c twin fatals too).
|
|
if (c.defertop >= DEFER_MAX) {
|
|
let msg: str = "cgen: too many defers in one function\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
c.deferbuf[c.defertop] = n.lhs;
|
|
c.defertop += 1;
|
|
return;
|
|
};
|
|
|
|
c.lastwasreturn = 0;
|
|
};
|
|
|
|
fn cgyield(c: *cgen, n: *syntax.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: *syntax.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: *syntax.node = n.list;
|
|
for (s != nil) {
|
|
cgstmt(c, s);
|
|
s = s.next;
|
|
};
|
|
c.locals = saved;
|
|
return;
|
|
};
|
|
|
|
// rundefers — emit cgexpr for every queued defer in LIFO order.
|
|
// Called from cgreturn and the cgfn implicit-return path.
|
|
fn rundefers(c: *cgen) void = {
|
|
let i: i32 = c.defertop - 1;
|
|
for (i >= 0) {
|
|
cgexpr(c, c.deferbuf[i]);
|
|
i -= 1;
|
|
};
|
|
return;
|
|
};
|
|
|
|
// #83: positional tuple register-return ABI. Tuple elements ride
|
|
// consecutive eightbytes over [AX,DX,CX,R8] (tupreg by index); a
|
|
// slice/str rides its 3-word {ptr,len,cap} header (tyslicesize SSoT,
|
|
// ref/hare/rt/ensure.ha:4-8), a scalar rides 1. SEND (cgreturn) and
|
|
// RECEIVE (cgmlet/cgmassign) walk the SAME widths so element->register
|
|
// agrees — mirrors harec create_unpack_bindings
|
|
// (ref/harec/src/check.c:1354-1416). Capacity is 4 (AX,DX,CX,R8).
|
|
fn tupreg(i: i32) str = {
|
|
if (i == 0) { return "AX"; };
|
|
if (i == 1) { return "DX"; };
|
|
if (i == 2) { return "CX"; };
|
|
return "R8";
|
|
};
|
|
|
|
// #164 (#107): SSE half of the SysV dual register-class return. A float
|
|
// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the
|
|
// INTEGER row tupreg — a float lands in the next XMM regardless of its
|
|
// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},
|
|
// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage
|
|
// tuple_sse_seq (cmd/w6c/cgen.c).
|
|
fn tupsse(i: i32) str = {
|
|
if (i == 0) { return "X0"; };
|
|
return "X1";
|
|
};
|
|
|
|
// tupeslot — THE tuple element-stride accessor (#22): the slot a tuple
|
|
// element occupies, in bytes. slot = roundup8(size(elem)), 8B a FLOOR
|
|
// not a ceiling (user-ratified 2026-06-04): str/slice carry their 24B
|
|
// header, a tagged element its full tag+payload box ((str,str)=48B
|
|
// predates this; tagged was the one truncated >8B kind — the #237
|
|
// fieldslotsize precedent), narrow scalars pad UP to one 8B eightbyte.
|
|
// Every tuple walk (cursor send/receive, t.N read, destructure, sret
|
|
// classify, DATA emit) takes its stride and its eightbyte count
|
|
// (eslot/8) from here — the per-site wide=(STR||SLICE)-else-8
|
|
// predicates this absorbs were the #22 neighbor-slot/zeros miscompile.
|
|
// Checker twin: check.ww tupleelemslot / check.c N_TTUPLE; cstage twin:
|
|
// tuple_eslot (cmd/w6c/cgen.c).
|
|
export fn tupeslot(ti: *syntax.tinfo) i32 = {
|
|
let t: *syntax.tinfo = ti;
|
|
t = tichase(t);
|
|
if (t == nil) { return 8; };
|
|
if (t.kind == syntax.tykind.TY_VOID) { return 0; };
|
|
// a literal tuple's stamped element can be untyped_str (size 0) —
|
|
// it occupies the str header slot (the C-t2 type_isstr lesson).
|
|
if (t.kind == syntax.tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; };
|
|
if (t.kind == syntax.tykind.TY_STR || t.kind == syntax.tykind.TY_SLICE ||
|
|
t.kind == syntax.tykind.TY_TAGGED) {
|
|
return ((t.size + 7u64) & ~7u64): i32;
|
|
};
|
|
return 8;
|
|
};
|
|
|
|
export fn tupeslotn(n: *syntax.node) i32 = {
|
|
if (n == nil) { return 8; };
|
|
return tupeslot(n.type_: *syntax.tinfo);
|
|
};
|
|
|
|
// rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil).
|
|
// wwstage has no checker, so the receive sites read each tuple element's
|
|
// width from the called fn's declared return type. Mirrors the callee
|
|
// resolution shared by cgmlet/cgmassign.
|
|
fn rettupleof(c: *cgen, rhs: *syntax.node) *syntax.node = {
|
|
if (rhs == nil) { return nil; };
|
|
if (rhs.kind != syntax.nkind.N_CALL) { return nil; };
|
|
let callee: *syntax.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 == syntax.nkind.N_IDENT) {
|
|
cnm = callee.str;
|
|
cmod = c.curmod;
|
|
};
|
|
if (callee.kind == syntax.nkind.N_DOT) {
|
|
cnm = callee.str;
|
|
if (callee.lhs != nil) {
|
|
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
|
cmod = callee.lhs.str;
|
|
};
|
|
};
|
|
};
|
|
if (cnm.len == 0) { return nil; };
|
|
let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod);
|
|
if (rtyp == nil) { return nil; };
|
|
if (rtyp.kind != syntax.nkind.N_TTUPLE) { return nil; };
|
|
return rtyp;
|
|
};
|
|
|
|
// nodetuplearg — the tuple node of a call ARG whose cgexpr fills the
|
|
// return-ABI cursor (#163/#32, C-t2): an N_CALL or `?`/`!` unwrap (the
|
|
// declared return / success variant via inferletcalltype), an N_IDENT
|
|
// local (declared tnode, alias-peeled), or an N_TUPLE literal — returned
|
|
// AS-IS, kind-discriminated at the walks (its elements are VALUE exprs,
|
|
// classified the way cgtuplelittocursor classifies them, not type
|
|
// nodes). Mirror of cstage node_tuplearg; the cgcall push site
|
|
// loud-stops any other tuple-typed source shape (rule 7). rettupleof
|
|
// stays N_CALL-scoped for the destructure/reassign receive sites.
|
|
fn nodetuplearg(c: *cgen, a: *syntax.node) *syntax.node = {
|
|
if (a == nil) { return nil; };
|
|
if (a.kind == syntax.nkind.N_TUPLE) { return a; };
|
|
if (a.kind == syntax.nkind.N_IDENT) {
|
|
let lc: *local = localfindnode(c, a.str);
|
|
if (lc == nil) { return nil; };
|
|
let tn: *syntax.node = lc.tnode;
|
|
for (tn != nil && tn.kind == syntax.nkind.N_TNAME) {
|
|
tn = aliaslookup(c, tn.str);
|
|
};
|
|
if (tn != nil) {
|
|
if (tn.kind == syntax.nkind.N_TTUPLE) { return tn; };
|
|
};
|
|
return nil;
|
|
};
|
|
let t: *syntax.node = inferletcalltype(c, a);
|
|
if (t != nil) {
|
|
if (t.kind == syntax.nkind.N_TTUPLE) { return t; };
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// tupstore — store the tuple element at register-cursor `cur` into the
|
|
// BP-relative slot at `off`. A >8B element (slice/str 3-word
|
|
// {ptr,len,cap} header, ref/hare/rt/ensure.ha:4-8; tagged tag+payload
|
|
// box, #22) stores its eslot/8 words from consecutive INTEGER cursor
|
|
// registers; a float rides the SSE cursor (X0,X1); a scalar stores 1
|
|
// INTEGER word. The caller owns the dual cursor (validated +
|
|
// advanced). Byte-identical to the cstage tuple_store (cmd/w6c/cgen.c).
|
|
fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *syntax.node) void = {
|
|
if (eslot == 0) { return; }; // void element: the checker's 0-slot
|
|
if (eslot > 8) {
|
|
let k: i32 = 0;
|
|
for (k < eslot / 8) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur + k));
|
|
emitline(", ");
|
|
emitoff((off + k * 8): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
return;
|
|
};
|
|
// #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg
|
|
// (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else
|
|
// the slot gets garbage and the FACE-Z field read sees it. The SSE
|
|
// regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105
|
|
// single-float byte-id; idx1=X1 is the #107 multi-float extension.
|
|
if (isfloattype(c, tn)) {
|
|
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
|
|
// In destructure mode tn IS the tuple-element-type-AST node
|
|
// (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value
|
|
// stored" rides X0 with no separate AST. isfloattype(c, tn) at
|
|
// the branch head already implies tn.type_!=nil (typeisfloat is
|
|
// false on nil), so this assertion is structurally unreachable
|
|
// today — RETAINED to PIN the contract: "the float-store branch
|
|
// requires a stamped slot." Catches a future change that opens
|
|
// this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple
|
|
// element binding where the destructure stamp didn't land —
|
|
// #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/
|
|
// 1475 + asserttyped file:line at check.ww:3340-3344.
|
|
if (tn != nil) { if (tn.type_ == nil) {
|
|
let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at ";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
if (tn.file.len > 0) {
|
|
os.write(2, tn.file.ptr, tn.file.len: u64);
|
|
os.write(2, ":".ptr, 1u64);
|
|
let ls: str = strconv.i32tos(tn.line, strconv.base.DEC);
|
|
os.write(2, ls.ptr, ls.len: u64);
|
|
os.write(2, " ".ptr, 1u64);
|
|
};
|
|
let kn: str = syntax.nkname(tn.kind);
|
|
os.write(2, kn.ptr, kn.len: u64);
|
|
os.write(2, "\n".ptr, 1u64);
|
|
os.exit(1);
|
|
}; };
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, tn)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\t");
|
|
emitline(tupsse(ssecur));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
return;
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
|
|
// tuplitgpwords — INTEGER cursor words an N_TUPLE literal element
|
|
// occupies. MUST mirror the literal push arms (tuplitpushelem) exactly
|
|
// — the count drives the POP fill, so a count/push skew silently
|
|
// shifts every later element (#22 class). A float rides the SSE row
|
|
// (0 GP words); str/slice push their 3-word header; a tagged element
|
|
// its tupeslot/8 box words; a void element pushes nothing (the
|
|
// checker's 0-slot); a scalar 1. Mirror of cstage tuple_lit_gpwords.
|
|
//
|
|
// #57: `dtn` is the DECLARED tuple element TYPE node (nil when the
|
|
// consumer has none). The N_TUPLE literal's stamped type is
|
|
// CONSTRUCTED from its elements, so a concrete rvalue under a
|
|
// declared-TAGGED slot counted ONE word here while the receive walks
|
|
// the declared eslot — the cursor shifted and every later element
|
|
// read garbage. Declared-tagged keys the count on the DECLARED box.
|
|
fn tuplitgpwords(c: *cgen, e: *syntax.node, dtn: *syntax.node) i32 = {
|
|
if (dtn != nil) {
|
|
if (istaggedtype(c, dtn)) { return tupeslotn(dtn) / 8; };
|
|
};
|
|
if (isfloattype(c, e)) { return 0; };
|
|
if (nodeisstr(c, e) || nodeisslice(c, e)) {
|
|
return (tyslicesize() / 8i64): i32;
|
|
};
|
|
let t: *syntax.tinfo = e.type_: *syntax.tinfo;
|
|
t = tichase(t);
|
|
if (t != nil && (t.kind == syntax.tykind.TY_TAGGED ||
|
|
t.kind == syntax.tykind.TY_VOID)) {
|
|
return tupeslotn(e) / 8;
|
|
};
|
|
return 1;
|
|
};
|
|
|
|
// tuplitpushelem — evaluate one N_TUPLE literal element and push its
|
|
// INTEGER cursor words L->R (the pop side fills tupreg in reverse). A
|
|
// tagged element loads its box words straight from its local slot —
|
|
// cgexpr's ident load is word0-only for tagged (every tagged consumer
|
|
// reads memory), so the cursor fill must too. Mirror of cstage
|
|
// tuple_lit_push_elem — count (tuplitgpwords) and push live or die
|
|
// together.
|
|
//
|
|
// #57: a DECLARED-tagged element whose expr is a concrete rvalue
|
|
// (`return (5: size, 9)` — cast, literal, call) skipped the widen
|
|
// entirely: the stamped-keyed arm below saw a scalar and pushed ONE
|
|
// word, the receiver read the declared box words — silent shift, both
|
|
// stages, gate-blind (ken /tmp/ken57). Such an element now widens
|
|
// into the shared tagged scratch (cgwidentaggedstore, the cgreturn
|
|
// tagged-@retscr shape) and pushes the box words. A tagged->tagged
|
|
// SUBSET element (eslot mismatch) needs a tag remap on the way into
|
|
// the slot — loud (rule 7, the #23/#40 widening family).
|
|
fn tuplitpushelem(c: *cgen, e: *syntax.node, dtn: *syntax.node) void = {
|
|
let t: *syntax.tinfo = e.type_: *syntax.tinfo;
|
|
t = tichase(t);
|
|
let etagged: bool = false;
|
|
if (t != nil) { if (t.kind == syntax.tykind.TY_TAGGED) { etagged = true; }; };
|
|
if (dtn != nil) {
|
|
if (istaggedtype(c, dtn) && !etagged) {
|
|
let eslot: i32 = tupeslotn(dtn);
|
|
let scr: i32 = tagscradd(c, eslot);
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
let z: i32 = 0;
|
|
for (z < eslot) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scr + z): i64);
|
|
emitline("(BP)\n");
|
|
z += 8;
|
|
};
|
|
cgwidentaggedstore(c, dtn.type_: *syntax.tinfo, e,
|
|
"BP", scr, eslot);
|
|
let pk: i32 = 0;
|
|
for (pk < eslot / 8) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scr + pk * 8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tPUSHQ\tAX\n");
|
|
pk += 1;
|
|
};
|
|
return;
|
|
};
|
|
if (istaggedtype(c, dtn) && etagged
|
|
&& tupeslotn(dtn) != tupeslotn(e)) {
|
|
let m57: str = "#57: tagged tuple element widening into a wider declared union slot needs a tag remap (rule 7; the #23/#40 widening family)\n";
|
|
os.write(2, m57.ptr, m57.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
if (etagged) {
|
|
let eslot: i32 = tupeslotn(e);
|
|
let eoff: i32 = 0;
|
|
if (e.kind == syntax.nkind.N_IDENT) { eoff = localfind(c, e.str); };
|
|
if (eoff == 0) {
|
|
let m22: str = "#22a: tagged tuple element from a non-local source shape unwired (ident locals only; rule 7; call-source is task #41, widening #23, deref/cast #35)\n";
|
|
os.write(2, m22.ptr, m22.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let k: i32 = 0;
|
|
for (k < eslot / 8) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((eoff + k * 8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tPUSHQ\tAX\n");
|
|
k += 1;
|
|
};
|
|
return;
|
|
};
|
|
cgexpr(c, e);
|
|
if (t != nil && t.kind == syntax.tykind.TY_VOID) { return; };
|
|
emitline("\tPUSHQ\tAX\n");
|
|
if (nodeisstr(c, e) || nodeisslice(c, e)) {
|
|
emitline("\tPUSHQ\tBX\n");
|
|
emitline("\tPUSHQ\tCX\n");
|
|
};
|
|
};
|
|
|
|
// cgtuplelittocursor — #241: materialise an N_TUPLE literal's elements into
|
|
// the SysV register-return cursor (integer words L->R over tupreg AX/DX/CX/
|
|
// R8, floats over tupsse X0/X1, a slice/str's {ptr,len,cap} over three
|
|
// consecutive INTEGER regs) — the SAME ABI a tuple-returning call leaves,
|
|
// which every tuple consumer (tupstore at cgmlet/cgmassign) reads. cgexpr
|
|
// otherwise falls to its `MOVQ $0, AX` default for a tuple, so a literal
|
|
// rvalue tuple bound or destructured read garbage past word0. Byte-identical
|
|
// extraction of cgreturn's in-register N_TUPLE arm (cgenstmt.ww), now shared
|
|
// with cgexpr. Over-cap loud-stops (rule 7); a bare expression value can't
|
|
// sret, so the >cap rvalue-tuple materialisation is the #10 follow-up.
|
|
//
|
|
// #57: `decl` is the consumer's DECLARED tuple TYPE node (N_TTUPLE,
|
|
// nil when it has none — the bare cgexpr route). A declared-TAGGED
|
|
// element gates the SSE row off (its payload may be float-stamped but
|
|
// the BOX rides INTEGER eightbytes) and keys count + push on the
|
|
// declared eslot — see tuplitgpwords / tuplitpushelem. Mirrors cstage
|
|
// cg_tuple_lit_to_cursor's decl walk; decl.list nodes wrap the elem
|
|
// type in .lhs (the c.fnret.list shape the over-cap arm walks).
|
|
fn cgtuplelittocursor(c: *cgen, tuple: *syntax.node, decl: *syntax.node) void = {
|
|
let dp0: *syntax.node = nil;
|
|
if (decl != nil) {
|
|
if (decl.kind == syntax.nkind.N_TTUPLE) { dp0 = decl.list; };
|
|
};
|
|
let ssecap: i32 = TUPLE_SSECAP;
|
|
let gptotal: i32 = 0;
|
|
let ssecount: i32 = 0;
|
|
let dp: *syntax.node = dp0;
|
|
let e: *syntax.node = tuple.list;
|
|
for (e != nil) {
|
|
let dtn: *syntax.node = nil;
|
|
if (dp != nil) { dtn = dp.lhs; };
|
|
let dtagged: bool = false;
|
|
if (dtn != nil) { dtagged = istaggedtype(c, dtn); };
|
|
if (!dtagged && isfloattype(c, e)) {
|
|
ssecount = ssecount + 1;
|
|
} else {
|
|
gptotal = gptotal + tuplitgpwords(c, e, dtn);
|
|
};
|
|
if (dp != nil) { dp = dp.next; };
|
|
e = e.next;
|
|
};
|
|
if (gptotal > TUPLE_GPCAP || ssecount > ssecap) {
|
|
let msg: str = "tuple literal exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let fscr: i32 = 0;
|
|
if (ssecount > 0) {
|
|
fscr = localadd(c, "@tupfscr", ssecap * 8, nil);
|
|
};
|
|
let sseidx: i32 = 0;
|
|
dp = dp0;
|
|
e = tuple.list;
|
|
for (e != nil) {
|
|
let dtn: *syntax.node = nil;
|
|
if (dp != nil) { dtn = dp.lhs; };
|
|
let dtagged: bool = false;
|
|
if (dtn != nil) { dtagged = istaggedtype(c, dtn); };
|
|
let isflt: bool = !dtagged && isfloattype(c, e);
|
|
if (isflt) {
|
|
cgexpr(c, e);
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, e)) { mov = "MOVSS"; };
|
|
emitline("\t"); emitline(mov); emitline("\tX0, ");
|
|
emitoff((fscr + sseidx * 8): i64);
|
|
emitline("(BP)\n");
|
|
sseidx = sseidx + 1;
|
|
} else {
|
|
tuplitpushelem(c, e, dtn);
|
|
};
|
|
if (dp != nil) { dp = dp.next; };
|
|
e = e.next;
|
|
};
|
|
let i: i32 = gptotal - 1;
|
|
for (i >= 0) {
|
|
emitline("\tPOPQ\t");
|
|
emitline(tupreg(i));
|
|
emitline("\n");
|
|
i = i - 1;
|
|
};
|
|
let j: i32 = 0;
|
|
dp = dp0;
|
|
e = tuple.list;
|
|
for (e != nil) {
|
|
let dtn: *syntax.node = nil;
|
|
if (dp != nil) { dtn = dp.lhs; };
|
|
let dtagged: bool = false;
|
|
if (dtn != nil) { dtagged = istaggedtype(c, dtn); };
|
|
if (!dtagged && isfloattype(c, e)) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, e)) { mov = "MOVSS"; };
|
|
emitline("\t"); emitline(mov); emitline("\t");
|
|
emitoff((fscr + j * 8): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupsse(j));
|
|
emitline("\n");
|
|
j = j + 1;
|
|
};
|
|
if (dp != nil) { dp = dp.next; };
|
|
e = e.next;
|
|
};
|
|
};
|
|
|
|
// cgtupleslottocursor — #241: load a tuple already materialised in a BP-
|
|
// relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound union
|
|
// payload) into the SAME register cursor. The slot uses the register-ABI
|
|
// stride the tuple-init / #242 destructure write (a scalar 8B, a slice/str
|
|
// its 3-word header), NOT the packed t.N field layout (#238). All sources
|
|
// are memory, so each word loads straight into its cursor reg. So `yield t`
|
|
// / `return t` / `let q = t` over a tuple ident leave the whole tuple in the
|
|
// cursor, not just word0 in AX. Over-cap loud-stops (rule 7; #10). Mirror of
|
|
// cstage cg_tuple_slot_to_cursor.
|
|
fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *syntax.tinfo) void = {
|
|
let gptotal: i32 = 0;
|
|
let ssecount: i32 = 0;
|
|
let el: *syntax.ttupleelem = tu.tupleelems;
|
|
for (el != nil) {
|
|
let et: *syntax.tinfo = el.type_;
|
|
et = tichase(et);
|
|
if (et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64)) {
|
|
ssecount = ssecount + 1;
|
|
} else {
|
|
gptotal = gptotal + tupeslot(el.type_) / 8;
|
|
};
|
|
el = el.tnext;
|
|
};
|
|
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) {
|
|
let msg: str = "tuple ident exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let gp: i32 = 0;
|
|
let sse: i32 = 0;
|
|
let foff: i32 = 0;
|
|
el = tu.tupleelems;
|
|
for (el != nil) {
|
|
let et: *syntax.tinfo = el.type_;
|
|
et = tichase(et);
|
|
let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64);
|
|
let eslot: i32 = tupeslot(el.type_);
|
|
if (isflt) {
|
|
let mov: str = "MOVSD";
|
|
if (et.kind == syntax.tykind.TY_F32) { mov = "MOVSS"; };
|
|
emitline("\t"); emitline(mov); emitline("\t");
|
|
emitoff((srcoff + foff): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupsse(sse));
|
|
emitline("\n");
|
|
sse = sse + 1;
|
|
foff += 8;
|
|
} else {
|
|
let k: i32 = 0;
|
|
for (k < eslot / 8) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((srcoff + foff + k * 8): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupreg(gp + k));
|
|
emitline("\n");
|
|
k += 1;
|
|
};
|
|
gp += eslot / 8;
|
|
foff += eslot;
|
|
};
|
|
el = el.tnext;
|
|
};
|
|
};
|
|
|
|
// cgtaggedtuplepayloadshift — #241: a `?`-unwrapped tuple payload is an
|
|
// rvalue tuple that must fill the register cursor. The tagged return leaves
|
|
// AX=tag, DX=word0, CX=word1, R8=word2; the scalar/str unwrap lifts only
|
|
// word0->AX, stranding word1+ in CX/R8. Shift the whole payload DOWN one
|
|
// INTEGER reg so element i lands in tupreg(i). Float/slice/str payload
|
|
// elements ride a different SysV class — loud-stop (rule 7; the per-
|
|
// eightbyte tagged-tuple-payload classification is the #243 follow-up).
|
|
// Mirror of cstage cg_tagged_tuple_payload_shift.
|
|
fn cgtaggedtuplepayloadshift(c: *cgen, tup: *syntax.tinfo) void = {
|
|
let words: i32 = 0;
|
|
let el: *syntax.ttupleelem = tup.tupleelems;
|
|
for (el != nil) {
|
|
let et: *syntax.tinfo = el.type_;
|
|
et = tichase(et);
|
|
let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64);
|
|
if (isflt || tupeslot(el.type_) != 8) {
|
|
let msg: str = "tuple-in-union ? unwrap: float/slice/str/tagged payload element needs SysV per-eightbyte classification (see #243); only integer tuple payloads supported\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
words = words + 1;
|
|
el = el.tnext;
|
|
};
|
|
if (words > 3) {
|
|
let msg: str = "tuple-in-union ? unwrap payload exceeds the 3 integer return regs past the tag; see #10/#243\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let i: i32 = 0;
|
|
for (i < words) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(i + 1));
|
|
emitline(", ");
|
|
emitline(tupreg(i));
|
|
emitline("\n");
|
|
i = i + 1;
|
|
};
|
|
};
|
|
|
|
fn cgreturn(c: *cgen, n: *syntax.node) void = {
|
|
rundefers(c);
|
|
let rhs: *syntax.node = n.lhs;
|
|
if (rhs != nil) {
|
|
// #83 / #164 (#107): positional register-return over a SysV
|
|
// dual class cursor (harec create_unpack_bindings, ref/harec/src/
|
|
// check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 =
|
|
// tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8]
|
|
// (tupreg) — a slice/str its 3-word {ptr,len,cap} header
|
|
// (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a
|
|
// scalar 1 word in AX. Integer words spill L->R to the stack and
|
|
// pop into the INTEGER cursor in reverse so positional slot i
|
|
// lands in tupreg(i) (byte-id with #83 when no float is present).
|
|
// Each float must spill X0 to @tupfscr as we walk, since a later
|
|
// element's cgexpr clobbers X0; after the integer pops the saved
|
|
// floats reload into X0/X1 by SSE index — INDEPENDENT of the
|
|
// INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows
|
|
// loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME
|
|
// class split drives the receive sites.
|
|
// #242: a bare tuple return packs into the register cursor; a
|
|
// tuple WRAPPED IN A TAGGED UNION must instead pack into the
|
|
// union payload (tag + words) — fall through to the tagged path
|
|
// below, which routes it via cgwidentaggedstore. Without this
|
|
// guard the bare-tuple arm fired first and dropped the tag,
|
|
// returning (AX=word0, DX=word1) with no tag word.
|
|
if (rhs.kind == syntax.nkind.N_TUPLE && !istaggedtype(c, c.fnret)) {
|
|
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
|
|
let gptotal: i32 = 0;
|
|
let ssecount: i32 = 0;
|
|
// #57: count + push key on the DECLARED return-type
|
|
// element (c.fnret.list) — the literal's stamped type
|
|
// is element-constructed, so a declared-TAGGED
|
|
// element's concrete rvalue counted 1 word and skipped
|
|
// the widen while the caller's receive walks the
|
|
// declared eslot (2 words sent for a 3-word shape;
|
|
// ken /tmp/ken57 p8/p9). Same pt walk the over-cap arm
|
|
// already does (#240/#22b). Mirrors cstage cgreturn.
|
|
let rp0: *syntax.node = nil;
|
|
if (c.fnret != nil) {
|
|
if (c.fnret.kind == syntax.nkind.N_TTUPLE) {
|
|
rp0 = c.fnret.list;
|
|
};
|
|
};
|
|
let rp: *syntax.node = rp0;
|
|
let e: *syntax.node = rhs.list;
|
|
for (e != nil) {
|
|
let rdtn: *syntax.node = nil;
|
|
if (rp != nil) { rdtn = rp.lhs; };
|
|
let rdtag: bool = false;
|
|
if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); };
|
|
if (!rdtag && isfloattype(c, e)) {
|
|
ssecount = ssecount + 1;
|
|
} else {
|
|
gptotal = gptotal + tuplitgpwords(c, e, rdtn);
|
|
};
|
|
if (rp != nil) { rp = rp.next; };
|
|
e = e.next;
|
|
};
|
|
// #22b: classify and emit MUST agree (the #10 SSoT note
|
|
// at TUPLE_GPCAP). The over-cap DECISION rides
|
|
// sretretsize on the DECLARED return type — the same
|
|
// predicate the prologue (@sretarg) and the caller key
|
|
// on. The expr-shape count above only pairs the in-cap
|
|
// push/pop: a declared-tagged element whose expr is the
|
|
// unwidened payload counts 1 word here vs 2+ declared
|
|
// eightbytes, so the emit took the register path against
|
|
// an sret-classified caller — silent garbage, both
|
|
// stages, gate-blind (probe /tmp/i22b/p2).
|
|
let overcap: bool = gptotal > TUPLE_GPCAP || ssecount > ssecap;
|
|
if (c.fnret != nil) {
|
|
overcap = sretretsize(c, c.fnret) > 0;
|
|
};
|
|
if (overcap) {
|
|
// #10 Fold A: over-cap tuple returns via sret. The
|
|
// prologue wired @sretarg (sretretsize agrees on the
|
|
// caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT),
|
|
// holding the caller-prealloc dest. Store each element
|
|
// through *(@sretarg)
|
|
// at its packed layout offset (running sum of element
|
|
// sizes from the return-type tuple node — the t.0/t.1
|
|
// positional layout), each at its natural width so a
|
|
// narrow tail doesn't over-MOVQ (#169); the dest base is
|
|
// reloaded into DX each step since a wide element's
|
|
// cgexpr clobbers AX/BX/CX. Then reuse the struct-sret
|
|
// epilogue. The CALL/receive side stays loud-stopped
|
|
// (#10 Fold B). Byte-identical to cstage cgen.c
|
|
// N_RETURN over-cap tuple arm.
|
|
let saoff: i32 = localfind(c, "@sretarg");
|
|
let pt: *syntax.node = nil;
|
|
if (c.fnret != nil) { pt = c.fnret.list; };
|
|
let we: *syntax.node = rhs.list;
|
|
let foff: i32 = 0;
|
|
for (we != nil) {
|
|
let dt: *syntax.tinfo = nil;
|
|
if (pt != nil) { dt = pt.lhs.type_: *syntax.tinfo; };
|
|
dt = tichase(dt);
|
|
if (dt != nil && dt.kind == syntax.tykind.TY_TAGGED) {
|
|
// #22b (task #28): MEMORY-class tagged
|
|
// element — the whole box copies through
|
|
// the sret pointer mem-to-mem from the
|
|
// element's local slot. cgexpr can't
|
|
// source it: the tagged ident load is
|
|
// word0-only (every tagged consumer
|
|
// reads memory) and the AX/DX/CX/R8 box
|
|
// cursor would collide with the DX
|
|
// dest-base reload. Ident-only,
|
|
// mirroring tuplitpushelem; widening /
|
|
// non-ident sources stay loud (#23/#40
|
|
// follow-ups). Mirror of cstage cgen.c
|
|
// N_RETURN over-cap tagged arm.
|
|
let eslot: i32 = tupeslotn(pt.lhs);
|
|
let eu: *syntax.tinfo = we.type_: *syntax.tinfo;
|
|
eu = tichase(eu);
|
|
let eoff: i32 = 0;
|
|
if (we.kind == syntax.nkind.N_IDENT && eu != nil) {
|
|
if (eu.kind == syntax.tykind.TY_TAGGED && tupeslotn(we) == eslot) {
|
|
eoff = localfind(c, we.str);
|
|
};
|
|
};
|
|
if (eoff == 0) {
|
|
let m22b: str = "#22b: tagged element in an over-cap (sret) tuple return from a non-ident or widening source unwired (ident locals only; rule 7; call-source is task #41, widening #23/#40)\n";
|
|
os.write(2, m22b.ptr, m22b.len: u64);
|
|
os.exit(1);
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitoff(saoff: i64);
|
|
emitline("(BP), DX\n");
|
|
let bk: i32 = 0;
|
|
for (bk < eslot) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((eoff + bk): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitdispreg((foff + bk): i64, "DX");
|
|
emitline("\n");
|
|
bk += 8;
|
|
};
|
|
foff += eslot;
|
|
we = we.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
continue;
|
|
};
|
|
let isflt: bool = isfloattype(c, we);
|
|
let wide: bool = nodeisstr(c, we) || nodeisslice(c, we);
|
|
let esz: i32 = 8;
|
|
if (pt != nil) {
|
|
let eti: *syntax.tinfo = pt.lhs.type_: *syntax.tinfo;
|
|
if (eti != nil) { esz = eti.size: i32; };
|
|
};
|
|
cgexpr(c, we);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(saoff: i64);
|
|
emitline("(BP), DX\n");
|
|
if (isflt) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, we)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, ");
|
|
emitdispreg(foff: i64, "DX");
|
|
emitline("\n");
|
|
} else {
|
|
if (wide) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitdispreg(foff: i64, "DX");
|
|
emitline("\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitdispreg((foff + 8): i64, "DX");
|
|
emitline("\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitdispreg((foff + 16): i64, "DX");
|
|
emitline("\n");
|
|
} else {
|
|
let sop: str = tnodestoreop(c, we, esz);
|
|
emitline("\t");
|
|
emitline(sop);
|
|
emitline("\tAX, ");
|
|
emitdispreg(foff: i64, "DX");
|
|
emitline("\n");
|
|
};
|
|
};
|
|
// C-t0/#22: the sret buffer is slot-laid like
|
|
// every tuple home (checker size, t.N
|
|
// reader, mlet receive agree) — the stride
|
|
// is THE accessor's (a declared void
|
|
// element's 0-slot included; the old
|
|
// wide?esz:8 advanced 8 where every receive
|
|
// walks 0). esz keeps the store WIDTH
|
|
// natural. Mirrors cstage cgen.c N_RETURN
|
|
// over-cap arm.
|
|
if (pt != nil) { foff += tupeslotn(pt.lhs); }
|
|
else { foff += tupeslotn(we); };
|
|
we = we.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitoff(saoff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
// rule-7 net: register-classified by the declared type
|
|
// but the expr-shape count overflows the cursor — the
|
|
// pops below would index past tupreg. Unreachable while
|
|
// expr counts never exceed declared counts; loud, not
|
|
// OOB, if a future shape breaks that. Mirrors cstage.
|
|
if (gptotal > TUPLE_GPCAP || ssecount > ssecap) {
|
|
let mskew: str = "register-classified tuple return exceeds the cursor (classify/emit skew; rule 7, #22b)\n";
|
|
os.write(2, mskew.ptr, mskew.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let fscr: i32 = 0;
|
|
if (ssecount > 0) {
|
|
fscr = localadd(c, "@tupfscr", ssecap * 8, nil);
|
|
};
|
|
let sseidx: i32 = 0;
|
|
rp = rp0;
|
|
e = rhs.list;
|
|
for (e != nil) {
|
|
let rdtn: *syntax.node = nil;
|
|
if (rp != nil) { rdtn = rp.lhs; };
|
|
let rdtag: bool = false;
|
|
if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); };
|
|
let isflt: bool = !rdtag && isfloattype(c, e);
|
|
if (isflt) {
|
|
cgexpr(c, e);
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, e)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, ");
|
|
emitoff((fscr + sseidx * 8): i64);
|
|
emitline("(BP)\n");
|
|
sseidx = sseidx + 1;
|
|
} else {
|
|
// scalar=AX; slice/str=AX,BX,CX; tagged
|
|
// box from its slot or widened scratch
|
|
// (tuplitpushelem)
|
|
tuplitpushelem(c, e, rdtn);
|
|
};
|
|
if (rp != nil) { rp = rp.next; };
|
|
e = e.next;
|
|
};
|
|
let i: i32 = gptotal - 1;
|
|
for (i >= 0) {
|
|
emitline("\tPOPQ\t");
|
|
emitline(tupreg(i));
|
|
emitline("\n");
|
|
i = i - 1;
|
|
};
|
|
let j: i32 = 0;
|
|
rp = rp0;
|
|
e = rhs.list;
|
|
for (e != nil) {
|
|
let rdtn: *syntax.node = nil;
|
|
if (rp != nil) { rdtn = rp.lhs; };
|
|
let rdtag: bool = false;
|
|
if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); };
|
|
if (!rdtag && isfloattype(c, e)) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, e)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\t");
|
|
emitoff((fscr + j * 8): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupsse(j));
|
|
emitline("\n");
|
|
j = j + 1;
|
|
};
|
|
if (rp != nil) { rp = rp.next; };
|
|
e = e.next;
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
// Tagged-union return: pack as (AX=tag, DX=value0, CX=value1).
|
|
// For str variant, cgexpr leaves (AX=ptr, BX=len), so we
|
|
// shuffle DX←AX (ptr) and CX←BX (len), then load tag.
|
|
// For other variants, cgexpr leaves AX, shuffle DX←AX.
|
|
// Nullable folded `(*T | void)`: just one word; AX is
|
|
// already the pointer (or 0). No shuffle, no tag.
|
|
if (istaggedtype(c, c.fnret)) {
|
|
// Forwarding a fallible call: `return f();` where f
|
|
// also returns a tagged union. The result is already
|
|
// in (AX=tag, DX=v0, CX=v1, R8=v2) — no shuffle, no
|
|
// tag synthesis. Mirrors cstage cgen.c:8007 passthrough
|
|
// = istagged && (vu == rt || type_eq(vt, cg_ret_type)).
|
|
// TYPE-BASED predicate (was name-keyed via fnretlookupmod
|
|
// IDENT/DOT-only) covers all callee shapes — including
|
|
// deref-call N_UN(TK_STAR) per #201. Identity-on-peeled
|
|
// handles the NAMED case (tinfocache memoizes per typedecl,
|
|
// #191 lineage); the variant-pointer fallback handles the
|
|
// anonymous case (each anonymous `(A|B)` decl gets its own
|
|
// NAMED-less tinfo, so identity fails — e.g. cross-module
|
|
// strings.byteindex returns the same anonymous (i32|void)
|
|
// as bytes.index). Variant-pointer equality on the params
|
|
// chain suffices because variants are primitives (single
|
|
// tctx tinfo) or NAMED (per-decl identity); a full recursive
|
|
// tinfo structural-eq helper is gated by #178.
|
|
// #261: N_INDEX of a tagged element (`return x.o[i]`) and
|
|
// N_DOT of a tagged field both materialize the full tagged
|
|
// ABI shape via cgexpr (cgindex slot-copy / cgdot field-load,
|
|
// AX=tag/DX=v0/...), exactly like an N_CALL of a tagged-
|
|
// returning fn — so a same-type return forwards them
|
|
// unchanged. cstage gates passthrough purely on the rhs type
|
|
// (no kind filter, cgen.c:8845); without these kinds an
|
|
// N_INDEX tagged-element return fell to the scalar-variant
|
|
// shuffle (MOVQ AX,DX; MOVQ $0,AX), dropping the payload.
|
|
let forwardtagged: bool = false;
|
|
if ((rhs.kind == syntax.nkind.N_CALL || rhs.kind == syntax.nkind.N_INDEX || rhs.kind == syntax.nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) {
|
|
let ru: *syntax.tinfo = rhs.type_: *syntax.tinfo;
|
|
ru = tichase(ru);
|
|
let fu: *syntax.tinfo = c.fnret.type_: *syntax.tinfo;
|
|
fu = tichase(fu);
|
|
if (ru != nil && fu != nil && ru.kind == syntax.tykind.TY_TAGGED && fu.kind == syntax.tykind.TY_TAGGED) {
|
|
if (ru == fu) {
|
|
forwardtagged = true;
|
|
} else if (ru.nullable == fu.nullable) {
|
|
let pa: *syntax.tparam = ru.params;
|
|
let pb: *syntax.tparam = fu.params;
|
|
let same: bool = true;
|
|
for (pa != nil && pb != nil) {
|
|
if (pa.type_ != pb.type_) { same = false; };
|
|
pa = pa.tnext;
|
|
pb = pb.tnext;
|
|
};
|
|
if (same && pa == nil && pb == nil) {
|
|
forwardtagged = true;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// #38b: sret-classified tagged return (slot > the
|
|
// AX/DX/CX/R8 cursor) — write through *(@sretarg) and
|
|
// return the dest pointer. Three shapes mirror cstage
|
|
// cgen.c N_RETURN #38b: exact-type N_CALL forward
|
|
// (c.sretforward), widening from a >32B tagged source
|
|
// (#40 loud-stop), everything else through
|
|
// cgwidentaggedstore's non-BP base.
|
|
if (sretretsize(c, c.fnret) > 0) {
|
|
let sa38v: i32 = localfind(c, "@sretarg");
|
|
if (forwardtagged && rhs.kind == syntax.nkind.N_CALL) {
|
|
// exact-type N_CALL forward: inner sret's
|
|
// into outer's dest; an N_INDEX/N_DOT
|
|
// source routes through the widener's
|
|
// #37 mem-read arm below instead.
|
|
c.sretforward = 1;
|
|
cgexpr(c, rhs);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sa38v: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
let ru38: *syntax.tinfo = rhs.type_: *syntax.tinfo;
|
|
ru38 = tichase(ru38);
|
|
if (ru38 != nil) {
|
|
// #37 wired the N_INDEX/N_DOT mem-read into
|
|
// the widener; the remaining >32B kinds stay
|
|
// loud.
|
|
if (ru38.kind == syntax.tykind.TY_TAGGED
|
|
&& rhs.kind != syntax.nkind.N_IDENT
|
|
&& ru38.size: i32 > TUPLE_GPCAP * 8
|
|
&& !taggedmemread(c, rhs)) {
|
|
let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n";
|
|
os.write(2, m38e.ptr, m38e.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sa38v: i64);
|
|
emitline("(BP), BX\n");
|
|
cgwidentaggedstore(c, c.fnret.type_: *syntax.tinfo, rhs,
|
|
"BX", 0, slotsize(c, c.fnret));
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sa38v: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
// Struct payload or tagged-subset return — materialise
|
|
// the widened value in scratch via cgwidentaggedstore
|
|
// (handles tag remap and zero pad), then load AX/DX/CX
|
|
// from the slot.
|
|
let needswiden: bool = false;
|
|
if (!isnullabletype(c.fnret)) {
|
|
if (!forwardtagged) {
|
|
let sname: str = rhsstructpayload(c, rhs);
|
|
if (sname.len > 0) { needswiden = true; };
|
|
if (rhstaggedident(c, rhs) != nil) {
|
|
needswiden = true;
|
|
};
|
|
// #242: a tuple variant packs into the union
|
|
// payload via cgwidentaggedstore's TY_TUPLE arm.
|
|
if (rhs.kind == syntax.nkind.N_TUPLE) {
|
|
needswiden = true;
|
|
};
|
|
// Family C (#35/#46): a mem-based tagged
|
|
// read (`return *p`, any size) routes
|
|
// through the widener's memread arm —
|
|
// the cgexpr fall-through below wrapped
|
|
// the un-deref'd POINTER as a scalar
|
|
// payload (silent wrong). Mirrors
|
|
// cstage cgreturn's widen-store route.
|
|
if (taggedmemread(c, rhs)) {
|
|
needswiden = true;
|
|
};
|
|
// S1/#35: a GENUINE-WIDENING tagged source
|
|
// (stamped type_ TY_TAGGED and != fnret;
|
|
// exact-type rides forwardtagged/plain above)
|
|
// routes through the widener — tag remap for
|
|
// ident/call (#218), #35 widen-subset loud for
|
|
// cast/dot. Mirrors cstage cgreturn istagged→
|
|
// cg_widen_tagged_store (cmd/w6c/cgen.c:13008-
|
|
// 13011 → :2721). The ru1 != fu1 exclusion keeps
|
|
// EXACT-type tagged casts on cstage's passthrough
|
|
// (forwardtagged's own ru==fu equality) so byte-id
|
|
// holds.
|
|
let ru1: *syntax.tinfo = rhs.type_: *syntax.tinfo;
|
|
ru1 = tichase(ru1);
|
|
let fu1: *syntax.tinfo = nil;
|
|
if (c.fnret != nil) {
|
|
fu1 = c.fnret.type_: *syntax.tinfo;
|
|
fu1 = tichase(fu1);
|
|
};
|
|
if (ru1 != nil && fu1 != nil) {
|
|
if (ru1.kind == syntax.tykind.TY_TAGGED && ru1 != fu1) {
|
|
needswiden = true;
|
|
};
|
|
// S3/#35: a SAME-TYPE tagged CAST (ru1 == fu1,
|
|
// rhs an N_CAST) routes through the widener too.
|
|
// cstage keeps the N_CAST out of the srcreg
|
|
// passthrough (kind != N_CALL/INDEX/DOT) and the
|
|
// widen branch peels the identity cast internally
|
|
// (cg_widen_tagged_store cg_tagged_castpeel,
|
|
// cgen.c:2553), so cs emits the scratch-widen (NOT
|
|
// passthrough) for `return v: u` over an ident/
|
|
// call/dot source. ww's forwardtagged keys on
|
|
// rhs.kind (N_CAST uncovered), so the same-type
|
|
// cast fell to the scalar shuffle and synthesized
|
|
// tag 0 (silent). The N_CAST guard mirrors cstage's
|
|
// "N_CAST defeats srcreg"; peeling here instead
|
|
// would reroute a call/dot source to passthrough
|
|
// and break byte-id.
|
|
if (rhs.kind == syntax.nkind.N_CAST && ru1.kind == syntax.tykind.TY_TAGGED && ru1 == fu1) {
|
|
needswiden = true;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
if (needswiden) {
|
|
let rsz: i32 = slotsize(c, c.fnret);
|
|
// @retscr (not @tagscr) for the return materialise
|
|
// path. Cstage cmd/w6c/cgen.c cgreturn uses
|
|
// `@retscr` here and reserves the @tagscr SSoT
|
|
// for arg-widen / non-BP-base store / N_INDEX
|
|
// tagged-element write. Sharing the name in a fn
|
|
// that BOTH returns a 32B tagged AND pushes a
|
|
// smaller tagged arg fatals localadd's @-prefix
|
|
// size-grow guard (rule 7); routing returns
|
|
// through their own slot keeps each cache
|
|
// monotonic. Hardcoding 24 truncated 32B-slot
|
|
// returns and overwrote adjacent locals during
|
|
// the pre-zero loop (#38).
|
|
let scroff: i32 = localadd(c, "@retscr", rsz, nil);
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
let zz: i32 = 0;
|
|
for (zz < rsz) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + zz): i64);
|
|
emitline("(BP)\n");
|
|
zz += 8;
|
|
};
|
|
cgwidentaggedstore(c, c.fnret.type_: *syntax.tinfo, rhs, "BP",
|
|
scroff, rsz);
|
|
// Tagged-return ABI loads at most 4 eightbytes
|
|
// (AX/DX/CX/R8). A union whose slot exceeds 32B
|
|
// (tag + >3 payload words, e.g. a 32B struct
|
|
// variant = 40B slot) drops its 5th+ word here —
|
|
// SYMMETRICALLY with cstage, so byte-id holds and
|
|
// the tag/early-word read paths are correct. The
|
|
// dropped tail is #222 (the >4-eightbyte sret ABI
|
|
// asymmetry); its real fix routes large unions
|
|
// through a hidden-pointer sret on both paths.
|
|
// Sound only while consumers never read the tail
|
|
// (errno's tag/strerror path does not).
|
|
emitline("\tMOVQ\t");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP), AX\n");
|
|
if (rsz > 8) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP), DX\n");
|
|
};
|
|
if (rsz > 16) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP), CX\n");
|
|
};
|
|
if (rsz > 24) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 24): i64);
|
|
emitline("(BP), R8\n");
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
cgexpr(c, rhs);
|
|
if (isnullabletype(c.fnret)) {
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
if (forwardtagged) {
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
|
|
// Tagged-return ABI: AX=tag, DX=word0, CX=word1,
|
|
// R8=word2. Receiver (cgwidentaggedstore call-source
|
|
// arm) writes AX/DX/CX/R8 unconditionally sized by the
|
|
// dst slot; unused ABI words must be zeroed here so a
|
|
// stale CX/R8 from the caller (e.g. a slice-stride
|
|
// IMULQ before the call) does not land in slot+16 /
|
|
// slot+24. (Task #18.)
|
|
let rsz: i32 = slotsize(c, c.fnret);
|
|
// Value-class read off the checker stamp (rhs.type_) —
|
|
// the SSoT cstage reads via node_isfloat / type_isf32.
|
|
let rfk: i32 = 0;
|
|
if (rhs != nil) {
|
|
let rety: *syntax.tinfo = rhs.type_: *syntax.tinfo;
|
|
if (syntax.typeisf32(rety)) { rfk = 1; }
|
|
else { if (syntax.typeisfloat(rety)) { rfk = 2; }; };
|
|
};
|
|
if (nodeisslice(c, rhs)) {
|
|
// cgexpr leaves (AX=ptr, BX=len, CX=cap).
|
|
// Shuffle into return ABI: DX=ptr, CX=len,
|
|
// R8=cap.
|
|
emitline("\tMOVQ\tCX, R8\n");
|
|
emitline("\tMOVQ\tBX, CX\n");
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
} else { if (nodeisstr(c, rhs)) {
|
|
// str IS []u8: cgexpr leaves (AX=ptr, BX=len,
|
|
// CX=cap). Same shuffle as the slice arm above —
|
|
// DX=ptr, CX=len, R8=cap (#1/Phase 3).
|
|
emitline("\tMOVQ\tCX, R8\n");
|
|
emitline("\tMOVQ\tBX, CX\n");
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
} else { if (rfk != 0) {
|
|
// #157: float variant — cgexpr left the value
|
|
// in X0, not AX. No MOVQ-xmm->gp encoding, so
|
|
// bridge X0->DX through a stack slot (same arg-
|
|
// push idiom). Zero the slot first so the f32
|
|
// case (MOVSS writes only the low 4 bytes)
|
|
// leaves a deterministic high-4 — cs==ww byte-
|
|
// id, matching f64's MOVSD which fills all 8.
|
|
// The AX-independent spill also removes the
|
|
// stale-AX cs!=ww on multi-variant returns.
|
|
emitline("\tSUBQ\t$8, SP\n");
|
|
emitline("\tMOVQ\t$0, (SP)\n");
|
|
let mov: str = "MOVSD";
|
|
if (rfk == 1) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, (SP)\n");
|
|
emitline("\tMOVQ\t(SP), DX\n");
|
|
emitline("\tADDQ\t$8, SP\n");
|
|
if (rsz > 16) {
|
|
emitline("\tMOVQ\t$0, CX\n");
|
|
};
|
|
if (rsz > 24) {
|
|
emitline("\tMOVQ\t$0, R8\n");
|
|
};
|
|
} else {
|
|
emitline("\tMOVQ\tAX, DX\n");
|
|
// scalar fills DX only. Zero CX / R8 if dst
|
|
// covers slot+16 / slot+24.
|
|
if (rsz > 16) {
|
|
emitline("\tMOVQ\t$0, CX\n");
|
|
};
|
|
if (rsz > 24) {
|
|
emitline("\tMOVQ\t$0, R8\n");
|
|
};
|
|
};};};
|
|
emitline("\tMOVQ\t$");
|
|
if (idx < 0) { idx = 0; };
|
|
emitint(idx: i64);
|
|
emitline(", AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
// sret return (#23): plain TY_STRUCT > 24B. Callee writes
|
|
// through *(@sretarg) (the caller-prealloc dest saved at
|
|
// the prologue), then loads @sretarg into RAX and rets —
|
|
// the SysV "return the pointer" discipline. Two rhs shapes
|
|
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
|
|
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
|
|
let sretargoff: i32 = localfind(c, "@sretarg");
|
|
if (sretargoff != 0) {
|
|
let scs: i32 = sretretsize(c, c.fnret);
|
|
if (scs > 0) {
|
|
// sret return-forwarding (task #9 follow-up to
|
|
// #23): `return f();` where outer + inner both
|
|
// return the same >24B struct shape. Outer's
|
|
// @sretarg already holds its caller's prealloc
|
|
// dest; pass it to inner in RDI (set by cgcall
|
|
// via c.sretforward), inner writes directly
|
|
// there, inner's RAX (dest pointer) is already
|
|
// outer's return value. The trailing MOVQ
|
|
// @sretarg(BP), AX is redundant after inner's
|
|
// RET but kept for byte-id symmetry with the
|
|
// N_IDENT / N_STRUCTLIT arms below.
|
|
if (rhs.kind == syntax.nkind.N_CALL) {
|
|
c.sretforward = 1;
|
|
cgexpr(c, rhs);
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
let okrhs: bool = false;
|
|
// #272: >24B sret addressable-source closure —
|
|
// N_DOT/N_INDEX/deref land their address in SI then
|
|
// memcpy through *(@sretarg), mirroring cstage cgen.c
|
|
// N_RETURN sret arm. N_ARRLIT >24B has no consumer
|
|
// (loud-stops in cstage); not wired here.
|
|
let addrsrc: bool = false;
|
|
if (rhs.kind == syntax.nkind.N_IDENT) { okrhs = true; };
|
|
if (rhs.kind == syntax.nkind.N_STRUCTLIT) { okrhs = true; };
|
|
if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; };
|
|
if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; };
|
|
if (rhs.kind == syntax.nkind.N_UN) {
|
|
if (rhs.op == syntax.tkind.TK_STAR) { okrhs = true; addrsrc = true; };
|
|
};
|
|
if (okrhs) {
|
|
if (rhs.kind == syntax.nkind.N_STRUCTLIT) {
|
|
// #63: the >24B sret RETURN twin of the :2421
|
|
// let-init fix. sretretsize chases the alias for
|
|
// the size GATE (so this sret arm fires for a >24B
|
|
// alias struct), but the field-fill resolved the
|
|
// struct by a bare structlookup(c, sname): for an
|
|
// alias-NAMED literal (`type biga = big; return
|
|
// biga{...}`) sname is "biga", unregistered, so
|
|
// sret_si was nil and the fill was SKIPPED — the
|
|
// callee returned an uninitialised sret buffer
|
|
// (SILENT wrong, runtime-0). structlookupchain chases
|
|
// to the base struct; cs fills via the resolved
|
|
// Type*, runtime-correct.
|
|
let trefn: *syntax.node = rhs.lhs;
|
|
let sret_si: *structinfo = structlookupchain(c, trefn);
|
|
if (sret_si != nil) {
|
|
let emptys: str;
|
|
emptys.ptr = nil; emptys.len = 0;
|
|
// mode=1 (PTR_LOCAL): base reg = BX,
|
|
// reloaded from @sretarg(BP) before
|
|
// each field store. disp = 0 because
|
|
// the dest pointer IS the struct base.
|
|
cgstructlitfill(c, sret_si, rhs,
|
|
1, sretargoff, emptys,
|
|
0);
|
|
};
|
|
} else { if (addrsrc) {
|
|
if (!aggargsrcaddr(c, rhs, "SI")) {
|
|
let m4: str = "#272: aggregate return from unsupported source kind\n";
|
|
os.write(2, m4.ptr, m4.len: u64);
|
|
os.exit(1);
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), BX\n");
|
|
let k: i32 = 0;
|
|
for (k + 8 <= scs) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 8;
|
|
};
|
|
for (k + 4 <= scs) {
|
|
emitline("\tMOVL\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 4;
|
|
};
|
|
for (k < scs) {
|
|
emitline("\tMOVB\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 1;
|
|
};
|
|
} else {
|
|
let rl: *local = localfindnode(c, rhs.str);
|
|
if (rl != nil) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), BX\n");
|
|
let k: i32 = 0;
|
|
for (k + 8 <= scs) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 8;
|
|
};
|
|
for (k + 4 <= scs) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 4;
|
|
};
|
|
for (k < scs) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff(k: i64);
|
|
emitline("(BX)\n");
|
|
k += 1;
|
|
};
|
|
};
|
|
}; };
|
|
// sret return: RAX = dest pointer.
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sretargoff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
// Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7],
|
|
// DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c
|
|
// N_RETURN TY_STRUCT branch. Two rhs shapes are wired:
|
|
// N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT
|
|
// (field-by-field store at scratch+foff, with tagged fields
|
|
// delegated to cgwidentaggedstore). Call-result chain return
|
|
// is deferred to #5's receive side. Sizes > 24B route through
|
|
// the sret arm above.
|
|
let rname: str;
|
|
rname.ptr = nil; rname.len = 0;
|
|
if (c.fnret != nil) {
|
|
if (c.fnret.kind == syntax.nkind.N_TNAME) {
|
|
rname = c.fnret.str;
|
|
};
|
|
};
|
|
if (rname.len > 0) {
|
|
let rsi: *structinfo = structlookup(c, rname);
|
|
if (rsi != nil) {
|
|
// ≤24B register RETURN: cstage sizes by rt->size
|
|
// (maxalign-rounded), not the slot-padded totsize
|
|
// (round-to-8) — see structabisize (#169).
|
|
let rsz: i32 = structabisize(rsi);
|
|
if (rsz <= 24) {
|
|
let okrhs: bool = false;
|
|
// #272: struct ≤24B addressable-source closure —
|
|
// N_DOT/N_INDEX/deref memcpy into @retscr before the
|
|
// shared structfloatclass tail (mirror cstage cgen.c).
|
|
let addrsrc: bool = false;
|
|
if (rhs.kind == syntax.nkind.N_IDENT) {
|
|
okrhs = true;
|
|
// #41 (#263 ww-runtime-correct): a module-global struct
|
|
// source has no BP slot — route it through the addrsrc
|
|
// memcpy (LEAQ g(SB),SI via aggargsrcaddr). Pre-fix the
|
|
// rl==nil N_IDENT arm below emitted nothing → zeroed
|
|
// @retscr. cstage copies frame garbage (cstage half #42).
|
|
if (localfindnode(c, rhs.str) == nil) {
|
|
addrsrc = true;
|
|
};
|
|
};
|
|
if (rhs.kind == syntax.nkind.N_STRUCTLIT) {
|
|
okrhs = true;
|
|
};
|
|
if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; };
|
|
if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; };
|
|
if (rhs.kind == syntax.nkind.N_UN) {
|
|
if (rhs.op == syntax.tkind.TK_STAR) { okrhs = true; addrsrc = true; };
|
|
};
|
|
if (okrhs) {
|
|
let scroff: i32 = localadd(c,
|
|
"@retscr", 24, nil);
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP)\n");
|
|
if (rhs.kind == syntax.nkind.N_STRUCTLIT) {
|
|
// Delegate to the shared BP-relative
|
|
// structlit fill helper. Same store
|
|
// sequence the inline pre-#17 walk
|
|
// emitted (tagged + float + scalar),
|
|
// plus nested struct-typed structlit
|
|
// values recurse instead of dropping
|
|
// trailing bytes.
|
|
cgstructlitfillbp(c, rsi, rhs, scroff);
|
|
} else { if (addrsrc) {
|
|
// N_DOT / N_INDEX / deref: land src addr in SI,
|
|
// then memcpy rsz bytes into @retscr (#265/#268 shape).
|
|
if (!aggargsrcaddr(c, rhs, "SI")) {
|
|
let m5: str = "#272: aggregate return from unsupported source kind\n";
|
|
os.write(2, m5.ptr, m5.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let k: i32 = 0;
|
|
for (k + 8 <= rsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
if (k + 4 <= rsz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
if (k + 2 <= rsz) {
|
|
emitline("\tMOVW\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVW\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 2;
|
|
};
|
|
if (k + 1 <= rsz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
} else {
|
|
// N_IDENT: word-copy from rhs slot
|
|
// to scratch. Whole 8B words via
|
|
// MOVQ; tail via MOVL/MOVB so we
|
|
// read no further than the source
|
|
// slot's declared size.
|
|
let rl: *local = localfindnode(c, rhs.str);
|
|
if (rl != nil) {
|
|
let k: i32 = 0;
|
|
for (k + 8 <= rsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
for (k + 4 <= rsz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
for (k < rsz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((rl.off + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
};
|
|
}; };
|
|
// #171a: float-bearing struct RETURN (return
|
|
// twin of #165's param recv). A qualifying
|
|
// struct's float eightbytes ride the SSE return
|
|
// row (X0,X1 = tupsse), its INT eightbytes the
|
|
// INTEGER return row (AX,DX = tupreg), on
|
|
// INDEPENDENT cursors per SysV (ref/qbe/amd64/
|
|
// sysv.c retr) — so a float lands in the next
|
|
// XMM regardless of its positional eightbyte
|
|
// (struct{f64,i32}: e0→X0, e1→AX, NOT DX). The
|
|
// scratch is zero-padded to 24B so a full MOVQ
|
|
// on a trailing INT eightbyte reads no garbage
|
|
// (the #169 sized tail is a RECV concern).
|
|
// structfloatclass gates to qualifying structs;
|
|
// all-int + f32 keep the AX/DX/CX transport
|
|
// (byte-id / #171b).
|
|
let sfc: i32 = structfloatclass(c, c.fnret);
|
|
if (sfc != 0) {
|
|
let nb: i32 = sfc & 15;
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
let e: i32 = 0;
|
|
for (e < nb) {
|
|
let issse: bool = (sfc & (16 << e)) != 0;
|
|
if (issse) {
|
|
emitline("\tMOVSD\t");
|
|
emitoff((scroff + e*8): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupsse(ssecur));
|
|
emitline("\n");
|
|
ssecur += 1;
|
|
} else {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + e*8): i64);
|
|
emitline("(BP), ");
|
|
emitline(tupreg(gpcur));
|
|
emitline("\n");
|
|
gpcur += 1;
|
|
};
|
|
e += 1;
|
|
};
|
|
} else {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP), DX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP), CX\n");
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// #267: array return-by-value SEND. >24B sret rides the sret
|
|
// block above (scs = sretretsize keys it, N_IDENT word-copy /
|
|
// N_CALL forward generic). ≤24B reg-class `return a;` (N_IDENT)
|
|
// mirrors the struct ≤24B path: zero-pad a 24B scratch, word-
|
|
// copy the array slot in, ship AX/DX/CX. Array natural size
|
|
// (tinfo.size = sub.size*len) mirrors cstage rt->size. No
|
|
// structfloatclass (pure-int arrays); N_CALL forward at reg-
|
|
// class falls to the default cgexpr passthrough below.
|
|
if (c.fnret != nil && c.fnret.kind == syntax.nkind.N_TARRAY) {
|
|
// #272: array return-by-value source-shape closure.
|
|
// Beyond the #267 N_IDENT word-copy, route N_ARRLIT
|
|
// (literal fill), N_DOT/N_INDEX/deref (aggargsrcaddr +
|
|
// memcpy) into @retscr — the mirror of cstage cgen.c
|
|
// N_RETURN ≤24B arm. N_CALL stays on the cgexpr tail (the
|
|
// callee already left AX/DX/CX).
|
|
let arrok: bool = false;
|
|
if (rhs.kind == syntax.nkind.N_IDENT) { arrok = true; };
|
|
if (rhs.kind == syntax.nkind.N_ARRLIT) { arrok = true; };
|
|
if (rhs.kind == syntax.nkind.N_DOT) { arrok = true; };
|
|
if (rhs.kind == syntax.nkind.N_INDEX) { arrok = true; };
|
|
if (rhs.kind == syntax.nkind.N_UN) {
|
|
if (rhs.op == syntax.tkind.TK_STAR) { arrok = true; };
|
|
};
|
|
let ati: *syntax.tinfo = c.fnret.type_: *syntax.tinfo;
|
|
ati = tichase(ati);
|
|
if (arrok && ati != nil) {
|
|
let rsz: i32 = ati.size: i32;
|
|
if (rsz <= 24) {
|
|
let scroff: i32 = localadd(c, "@retscr", 24, nil);
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP)\n");
|
|
if (rhs.kind == syntax.nkind.N_IDENT) {
|
|
let rl: *local = localfindnode(c, rhs.str);
|
|
let roff: i32 = 0;
|
|
if (rl != nil) { roff = rl.off; };
|
|
let k: i32 = 0;
|
|
for (k + 8 <= rsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((roff + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
for (k + 4 <= rsz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((roff + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
for (k < rsz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((roff + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
} else { if (rhs.kind == syntax.nkind.N_ARRLIT) {
|
|
// scalar/float element fill; non-scalar
|
|
// elements loud-stop (rule 7, no consumer).
|
|
let esubti: *syntax.tinfo = nil;
|
|
if (ati.sub != nil) { esubti = ati.sub; };
|
|
esubti = tichase(esubti);
|
|
let esz: i32 = 8;
|
|
if (esubti != nil) { esz = esubti.size: i32; };
|
|
let badel: bool = false;
|
|
if (esubti != nil) {
|
|
if (esubti.kind == syntax.tykind.TY_STRUCT) { badel = true; };
|
|
if (esubti.kind == syntax.tykind.TY_ARRAY) { badel = true; };
|
|
if (esubti.kind == syntax.tykind.TY_TUPLE) { badel = true; };
|
|
if (esubti.kind == syntax.tykind.TY_SLICE) { badel = true; };
|
|
if (esubti.kind == syntax.tykind.TY_STR) { badel = true; };
|
|
};
|
|
if (badel) {
|
|
let m2: str = "#272: array-literal return with non-scalar element unsupported (rule 7, no consumer)\n";
|
|
os.write(2, m2.ptr, m2.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let esub: *syntax.node = c.fnret.lhs;
|
|
let isfl: bool = isfloattype(c, esub);
|
|
let fmov: str = "MOVSD";
|
|
if (isf32type(c, esub)) { fmov = "MOVSS"; };
|
|
let op: str = "MOVQ";
|
|
if (esz == 1) { op = "MOVB"; } else { if (esz == 2) { op = "MOVW"; } else { if (esz == 4) { op = "MOVL"; }; }; };
|
|
let idx: i32 = 0;
|
|
let repeat: bool = false;
|
|
let e: *syntax.node = rhs.list;
|
|
for (e != nil) {
|
|
let isellip: bool = false;
|
|
if (e.kind == syntax.nkind.N_FIELD) {
|
|
if (syntax.streq(e.str, "...")) { repeat = true; isellip = true; };
|
|
};
|
|
if (isellip) {
|
|
e = nil;
|
|
} else {
|
|
cgexpr(c, e);
|
|
if (isfl) {
|
|
emitline("\t");
|
|
emitline(fmov);
|
|
emitline("\tX0, ");
|
|
emitoff((scroff + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\t");
|
|
emitline(op);
|
|
emitline("\tAX, ");
|
|
emitoff((scroff + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
idx += 1;
|
|
e = e.next;
|
|
};
|
|
};
|
|
if (repeat) {
|
|
let total: i32 = rsz / esz;
|
|
for (idx < total) {
|
|
if (isfl) {
|
|
emitline("\t");
|
|
emitline(fmov);
|
|
emitline("\tX0, ");
|
|
emitoff((scroff + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\t");
|
|
emitline(op);
|
|
emitline("\tAX, ");
|
|
emitoff((scroff + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
idx += 1;
|
|
};
|
|
};
|
|
} else {
|
|
// N_DOT / N_INDEX / deref: land src addr in SI,
|
|
// then memcpy rsz bytes into @retscr (#265/#268
|
|
// copy shape). Loud-stop unaddressable sources.
|
|
if (!aggargsrcaddr(c, rhs, "SI")) {
|
|
let m3: str = "#272: aggregate return from unsupported source kind\n";
|
|
os.write(2, m3.ptr, m3.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let k: i32 = 0;
|
|
for (k + 8 <= rsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
if (k + 4 <= rsz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
if (k + 2 <= rsz) {
|
|
emitline("\tMOVW\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVW\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 2;
|
|
};
|
|
if (k + 1 <= rsz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((scroff + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
}; };
|
|
emitline("\tMOVQ\t");
|
|
emitoff(scroff: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 8): i64);
|
|
emitline("(BP), DX\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scroff + 16): i64);
|
|
emitline("(BP), CX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
// #272 close-by-construction: addressable aggregate-return
|
|
// sources (IDENT/STRUCTLIT/ARRLIT/DOT/INDEX/deref) all break in
|
|
// the arms above; an aggregate N_CALL passes through cgexpr
|
|
// (callee left AX/DX/CX). Any OTHER aggregate rvalue reaching
|
|
// here would truncate to AX silently — loud-stop (rule 7),
|
|
// mirroring cstage cgen.c N_RETURN.
|
|
{
|
|
// #277: key on the RESOLVED tinfo, not the syntactic node — a
|
|
// NAMED-ALIAS aggregate return type (type a=[N]T / type a=struct)
|
|
// presents as N_TNAME and is TY_ARRAY/TY_STRUCT only after the
|
|
// alias chase, so the syntactic N_TARRAY/N_TNAME-structlookup arms
|
|
// above never fire on it. Without this chase it would fall to the
|
|
// scalar default = silent miscompile (cstage chases via
|
|
// type_chase_named and stays correct). Loud-stop (rule 7) until
|
|
// wwstage handles aliases via tinfo-kind dispatch (#277); the >24B
|
|
// array-literal return (no consumer) also lands here (#276).
|
|
let aggret: bool = false;
|
|
if (c.fnret != nil) {
|
|
let rti: *syntax.tinfo = c.fnret.type_: *syntax.tinfo;
|
|
rti = tichase(rti);
|
|
if (rti != nil) {
|
|
if (rti.kind == syntax.tykind.TY_ARRAY) { aggret = true; };
|
|
if (rti.kind == syntax.tykind.TY_STRUCT) { aggret = true; };
|
|
};
|
|
};
|
|
if (aggret && rhs.kind != syntax.nkind.N_CALL) {
|
|
let m6: str = "#272/#276/#277: aggregate return reaches scalar default — unclosed shape (named-alias aggregate return or >24B array-literal; wwstage tinfo-dispatch deferred #277)\n";
|
|
os.write(2, m6.ptr, m6.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
cgexpr(c, rhs);
|
|
} else {
|
|
// Bare `return;` from a tagged-union-returning fn is
|
|
// the void variant: emit its tag. Payload is undefined
|
|
// (void has size 0). Otherwise zero AX for determinism.
|
|
if (istaggedtype(c, c.fnret)) {
|
|
// #38b: an sret-classified tagged return (slot > the
|
|
// AX/DX/CX/R8 cursor) writes the void-variant tag
|
|
// through *(@sretarg) and returns the dest pointer —
|
|
// the cursor can't carry the slot and the caller reads
|
|
// memory. Mirrors cstage cgen.c N_RETURN bare arm.
|
|
if (sretretsize(c, c.fnret) > 0) {
|
|
let sa38: i32 = localfind(c, "@sretarg");
|
|
let vidx38: i32 = voidvariantindex(c.fnret);
|
|
if (vidx38 < 0) { vidx38 = 0; };
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sa38: i64);
|
|
emitline("(BP), BX\n");
|
|
emitline("\tMOVQ\t$");
|
|
emitint(vidx38: i64);
|
|
emitline(", (BX)\n");
|
|
emitline("\tMOVQ\t");
|
|
emitoff(sa38: i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
if (isnullabletype(c.fnret)) {
|
|
// null = void variant; AX = 0.
|
|
emitline("\tMOVQ\t$0, AX\n");
|
|
} else {
|
|
let idx: i32 = voidvariantindex(c.fnret);
|
|
if (idx < 0) { idx = 0; };
|
|
emitline("\tMOVQ\t$");
|
|
emitint(idx: i64);
|
|
emitline(", AX\n");
|
|
};
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
emitline("\tMOVQ\t$0, AX\n");
|
|
};
|
|
// str IS []u8: cgexpr leaves AX=ptr, BX=len, CX=cap — str now
|
|
// returns exactly like a slice, no AX:DX shuffle (#1/Phase 3).
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
c.lastwasreturn = 1;
|
|
return;
|
|
};
|
|
|
|
fn cgexprstmt(c: *cgen, n: *syntax.node) void = {
|
|
if (n.lhs != nil) { cgexpr(c, n.lhs); };
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// cgarrlitfillbp — #31: fill the [count]T destination at BP-relative
|
|
// `off` from an N_ARRLIT, extracted from the cglet array-init path so
|
|
// the slice-borrow base materialisation (cgslice N_ARRLIT-base arm)
|
|
// reuses the IDENTICAL element-store sequence — the frame-order /
|
|
// store-op guarantee for rule-10 byte-id (ken). `arrtn` is the [count]T
|
|
// type NODE (cglet n.lhs; cgslice the re-stamped tnode on arrlit.lhs,
|
|
// #25); `rhs` the literal. Twin of cstage cg_arrlit_fill_bp.
|
|
fn cgarrlitfillbp(c: *cgen, arrtn: *syntax.node, rhs: *syntax.node, off: i32) void = {
|
|
let elemn: *syntax.node = arrtn.lhs;
|
|
// #79 (#60 rider): alias-NAMED [count]T (`type A = [4]u32; let
|
|
// a: A = [...]`) — arrtn is the N_TNAME leaf: elemn nil, esz
|
|
// stayed the 8 sentinel and the per-element store strode MOVQ
|
|
// over a stride-4 slot (saved-BP/RIP smash; masked when esz==8).
|
|
// This is the STORE half of the #8 pair (the elemsizeofc READ
|
|
// half chases the ELEMENT via idxeffti; alias-typed INDEXABLES
|
|
// are chased at its call sites). Synthesise the element node off
|
|
// the chased stamped sub — the cgforrange FC0 precedent — so the
|
|
// prim/agg/slice/tagged/narrow dispatch below works unchanged;
|
|
// stash alen for the `...` repeat bound (cstage cg_arrlit_fill_bp
|
|
// receives the pre-chased bu and reads bu->alen).
|
|
let aliasalen: i32 = -1;
|
|
let ati79: *syntax.tinfo = arrtn.type_: *syntax.tinfo;
|
|
if (ati79 != nil) { if (ati79.kind == syntax.tykind.TY_NAMED) {
|
|
let au79: *syntax.tinfo = tichase(ati79);
|
|
if (au79 != nil) { if (au79.kind == syntax.tykind.TY_ARRAY
|
|
&& au79.sub != nil) {
|
|
let en79: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, arrtn.file, arrtn.line, arrtn.col);
|
|
en79.str = au79.sub.name;
|
|
en79.type_ = au79.sub: *void;
|
|
elemn = en79;
|
|
aliasalen = au79.alen: i32;
|
|
};};
|
|
};};
|
|
let esz: i32 = 8;
|
|
let isstrel: bool = false;
|
|
if (elemn != nil) {
|
|
if (elemn.kind == syntax.nkind.N_TNAME) {
|
|
if (syntax.streq(elemn.str, "str")) {
|
|
esz = primtypesize("str"): i32;
|
|
isstrel = true;
|
|
} else {
|
|
let ps: i32 = aliasprimsize(c, elemn.str);
|
|
if (ps > 0) { esz = ps; };
|
|
};
|
|
};
|
|
};
|
|
// #270-1c: an AGGREGATE (struct/array/tuple) element of
|
|
// an array literal — the scalar per-element store below
|
|
// writes only the first 8 bytes (unpopulated tail). Fill
|
|
// each element slot from its literal (cgstructlitfillbp)
|
|
// or source ident (word-copy). esz is the element's
|
|
// natural size (cstage esub->size).
|
|
let esubti: *syntax.tinfo = nil;
|
|
if (elemn != nil) { esubti = elemn.type_: *syntax.tinfo; };
|
|
esubti = tichase(esubti);
|
|
let isagg: bool = esubti != nil
|
|
&& (esubti.kind == syntax.tykind.TY_STRUCT
|
|
|| esubti.kind == syntax.tykind.TY_ARRAY
|
|
|| esubti.kind == syntax.tykind.TY_TUPLE);
|
|
if (isagg) { esz = esubti.size: i32; };
|
|
// #20/#270 str-slice arm: a slice element (N_TSLICE) is
|
|
// a 24B {ptr,len,cap} header — it matches no prim/str/agg
|
|
// branch above, so esz stayed the 8 sentinel (wrong stride,
|
|
// the -96-vs-80 cs!=ww frame divergence) and the scalar
|
|
// store dropped .len/.cap. Size it from the stamped tinfo
|
|
// and route it through the 3-word header store below.
|
|
let isslicel: bool = esubti != nil
|
|
&& esubti.kind == syntax.tykind.TY_SLICE;
|
|
if (isslicel) { esz = esubti.size: i32; };
|
|
// #12: a tagged-union element. NOT folded into isagg —
|
|
// isagg's body word-copies/fatals and never boxes the
|
|
// tag+payload; route through the cgwidentaggedstore
|
|
// choke-point the N_LET tagged path (cgenstmt.ww:1627)
|
|
// uses. esz must come from the stamped slot size (#8-class
|
|
// trap, rule-13): the narrow override below only rescues
|
|
// 1/2/4, so a tagged 16/24B element keeps the wrong 8
|
|
// sentinel stride without this.
|
|
let istaggedel: bool = esubti != nil
|
|
&& esubti.kind == syntax.tykind.TY_TAGGED;
|
|
if (istaggedel) { esz = esubti.size: i32; };
|
|
// #8: a named-narrow element (`[N]tk`, tk = enum i32) is
|
|
// neither a builtin prim (primsize=0 above, so esz stayed
|
|
// the 8 sentinel) nor an aggregate, so the scalar store kept
|
|
// an 8B stride/MOVQ and overran the stride-4 frame slot —
|
|
// smashing the saved BP / return addr (SEGFAULT). Mirror
|
|
// cstage's uniform lu->sub->size (cgen.c:6387) and the
|
|
// elemsizeofc read-side fix: take the stamped element tinfo's
|
|
// size for a narrow scalar (1/2/4). Wider non-prim elements
|
|
// (tagged/slice/str two-half) stay the documented follow-up
|
|
// at :1742-1744 — the single-MOVx store below is scalar-only.
|
|
if (!isstrel && !isagg && esz == 8 && esubti != nil) {
|
|
let es: i32 = esubti.size: i32;
|
|
if (es == 1 || es == 2 || es == 4) { esz = es; };
|
|
};
|
|
let mop: str = tnodestoreop(c, elemn, esz);
|
|
// float element → store FROM X0 (MOVSS/MOVSD): cgexpr
|
|
// leaves a float in X0 and for f32 the #104 CVTSD2SS
|
|
// narrowing only touches X0; the AX store (mop) would
|
|
// write the raw double low-bits, garbage for f32 (#122,
|
|
// mirrors cstage cgen.c:6889 arr-lit float store).
|
|
let isfloatel: bool = isfloattype(c, elemn);
|
|
let fmov: str = "MOVSD";
|
|
if (isf32type(c, elemn)) { fmov = "MOVSS"; };
|
|
let idx: i32 = 0;
|
|
let repeat: bool = false;
|
|
let e: *syntax.node = rhs.list;
|
|
for (e != nil) {
|
|
let isellip: bool = false;
|
|
if (e.kind == syntax.nkind.N_FIELD) {
|
|
if (syntax.streq(e.str, "...")) {
|
|
repeat = true;
|
|
isellip = true;
|
|
};
|
|
};
|
|
if (isellip) {
|
|
e = nil;
|
|
} else {
|
|
if (isagg) {
|
|
if (e.kind == syntax.nkind.N_STRUCTLIT) {
|
|
let esi: *structinfo = structlookupchain(c, elemn);
|
|
cgstructlitfillbp(c, esi, e, off + idx * esz);
|
|
} else { if (e.kind == syntax.nkind.N_IDENT) {
|
|
let sl: *local = localfindnode(c, e.str);
|
|
let soff: i32 = 0;
|
|
if (sl != nil) { soff = sl.off; };
|
|
let kc: i32 = 0;
|
|
for (kc + 8 <= esz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((soff + kc): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + idx * esz + kc): i64);
|
|
emitline("(BP)\n");
|
|
kc += 8;
|
|
};
|
|
if (kc + 4 <= esz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((soff + kc): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((off + idx * esz + kc): i64);
|
|
emitline("(BP)\n");
|
|
kc += 4;
|
|
};
|
|
if (kc + 2 <= esz) {
|
|
emitline("\tMOVW\t");
|
|
emitoff((soff + kc): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVW\tAX, ");
|
|
emitoff((off + idx * esz + kc): i64);
|
|
emitline("(BP)\n");
|
|
kc += 2;
|
|
};
|
|
if (kc + 1 <= esz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((soff + kc): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((off + idx * esz + kc): i64);
|
|
emitline("(BP)\n");
|
|
kc += 1;
|
|
};
|
|
} else {
|
|
let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n";
|
|
os.write(2, m1c.ptr, m1c.len: u64);
|
|
os.exit(1);
|
|
}; };
|
|
} else { if (istaggedel) {
|
|
cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz);
|
|
} else {
|
|
cgexpr(c, e);
|
|
if (isstrel || isslicel) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + idx * esz + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + idx * esz + 16): i64);
|
|
emitline("(BP)\n");
|
|
} else { if (isfloatel) {
|
|
emitline("\t");
|
|
emitline(fmov);
|
|
emitline("\tX0, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\t");
|
|
emitline(mop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
}; };
|
|
}; };
|
|
idx += 1;
|
|
e = e.next;
|
|
};
|
|
};
|
|
if (repeat && isagg) {
|
|
let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n";
|
|
os.write(2, m1cr.ptr, m1cr.len: u64);
|
|
os.exit(1);
|
|
};
|
|
// #12: `...` re-stores from AX, but cgwidentaggedstore consumed
|
|
// the node and trashed AX — the repeat-fill would write garbage.
|
|
// No consumer needs `[N]tagged=[x,...]`.
|
|
if (repeat && istaggedel) {
|
|
let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n";
|
|
os.write(2, m12r.ptr, m12r.len: u64);
|
|
os.exit(1);
|
|
};
|
|
// AX (and BX for str) still holds the last stored value;
|
|
// fill remaining slots up to the declared length with it.
|
|
if (repeat) {
|
|
let total: i32 = idx;
|
|
if (arrtn != nil) {
|
|
if (arrtn.kind == syntax.nkind.N_TARRAY) {
|
|
if (arrtn.rhs != nil) {
|
|
if (arrtn.rhs.kind == syntax.nkind.N_INTLIT) {
|
|
total = arrtn.rhs.uval: i32;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// #79: alias arrtn has no length tnode — bound off the
|
|
// chased tinfo (see the synthesis block at fn top).
|
|
if (aliasalen >= 0) { total = aliasalen; };
|
|
for (idx < total) {
|
|
if (isstrel || isslicel) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + idx * esz + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + idx * esz + 16): i64);
|
|
emitline("(BP)\n");
|
|
} else { if (isfloatel) {
|
|
emitline("\t");
|
|
emitline(fmov);
|
|
emitline("\tX0, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
emitline("\t");
|
|
emitline(mop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + idx * esz): i64);
|
|
emitline("(BP)\n");
|
|
}; };
|
|
idx += 1;
|
|
};
|
|
};
|
|
};
|
|
|
|
// #152: reserve the let's frame slot, emit its initializer against the
|
|
// PRE-binding locals chain, then link the binding. A self-shadowing init
|
|
// (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in
|
|
// c.locals while cgletbody runs (Hare evals the init in the outer scope:
|
|
// harec check.c clet runs cexpr before scope_define). localreserve bumps
|
|
// the frame now so off + nested-let offsets stay stable.
|
|
fn cglet(c: *cgen, n: *syntax.node) void = {
|
|
let nm: str = n.str;
|
|
let sz: i32 = letslotsize(c, n);
|
|
let tn: *syntax.node = n.lhs;
|
|
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
|
let letloc: *local = localreserve(c, nm, sz, tn);
|
|
cgletbody(c, n, letloc.off);
|
|
letloc.lnext = c.locals;
|
|
c.locals = letloc;
|
|
};
|
|
|
|
// #152: cgletbody emits the initializer into the reserved slot `off`.
|
|
// The wrapper cglet reserves the slot BEFORE this runs and links the
|
|
// binding into c.locals only AFTER, so a self-shadowing init
|
|
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
|
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
|
fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = {
|
|
let nm: str = n.str;
|
|
let sz: i32 = letslotsize(c, n);
|
|
// `let x = f()?` has no annotation but the cgen's struct-field
|
|
// paths need a tnode to dispatch off. Infer from f's tagged
|
|
// success variant — see inferletcalltype.
|
|
let tn: *syntax.node = n.lhs;
|
|
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
|
if (n.rhs != nil) {
|
|
let rhs: *syntax.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: *syntax.node = nil;
|
|
let viatryunw: bool = false;
|
|
let viatryprop: bool = false;
|
|
if (rhs.kind == syntax.nkind.N_TRYUNW) {
|
|
if (rhs.lhs != nil) {
|
|
if (rhs.lhs.kind == syntax.nkind.N_CALL) {
|
|
scall = rhs.lhs;
|
|
viatryunw = true;
|
|
};
|
|
};
|
|
} else { if (rhs.kind == syntax.nkind.N_TRYPROP) {
|
|
if (rhs.lhs != nil) {
|
|
if (rhs.lhs.kind == syntax.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 == syntax.nkind.N_TSLICE && sz == tyslicesize(): i32) {
|
|
let callee: *syntax.node = scall.lhs;
|
|
let a0: *syntax.node = scall.list;
|
|
let a1: *syntax.node = nil;
|
|
let a2: *syntax.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 == syntax.nkind.N_IDENT
|
|
&& syntax.streq(callee.str, "alloc")
|
|
&& a0.kind == syntax.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: *syntax.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: *syntax.tinfo = nil;
|
|
if (c.fnret != nil) { nti = c.fnret.type_: *syntax.tinfo; };
|
|
nti = tichase(nti);
|
|
if (nti != nil) { if (nti.kind == syntax.tykind.TY_TAGGED) {
|
|
let np: *syntax.tparam = nti.params;
|
|
let nidx2: i32 = 0;
|
|
for (np != nil) {
|
|
let nvt: *syntax.tinfo = np.type_;
|
|
if (nvt != nil) {
|
|
if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; };
|
|
};
|
|
np = np.tnext;
|
|
nidx2 += 1;
|
|
};
|
|
}; };
|
|
if (nidx < 0) { nidx = 1; };
|
|
emitline("\tMOVQ\t$");
|
|
emitint(nidx: i64);
|
|
emitline(", AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
|
|
emitlabel(okl);
|
|
};
|
|
emitline("\tPOPQ\tBX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
// Tagged-union init: delegate to cgwidentaggedstore, which
|
|
// handles nullable fold, tagged source (ident or AX/DX/CX
|
|
// ABI call), struct payload (literal/ident), str payload,
|
|
// scalar payload — with tag remap for tagged-subset widening.
|
|
//
|
|
// #38b: an sret-classified tagged CALL result is in memory,
|
|
// not the cursor — an exact-type receive falls through to the
|
|
// generic sret receive below (the let's slot IS the dest); a
|
|
// widening receive needs mem-to-mem tag-remap (#40, unwired).
|
|
// Mirrors cstage cgen.c N_LET tagged arm.
|
|
if (istaggedtype(c, tn)) {
|
|
let letsret: i32 = 0;
|
|
if (rhs.kind == syntax.nkind.N_CALL) {
|
|
letsret = callsretsize(c, rhs);
|
|
};
|
|
if (letsret == 0) {
|
|
cgwidentaggedstore(c, tn.type_: *syntax.tinfo, rhs,
|
|
"BP", off, sz);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
let lru: *syntax.tinfo = rhs.type_: *syntax.tinfo;
|
|
lru = tichase(lru);
|
|
let llu: *syntax.tinfo = tn.type_: *syntax.tinfo;
|
|
llu = tichase(llu);
|
|
let exact38: bool = false;
|
|
if (lru != nil && lru == llu) { exact38 = true; }
|
|
else {
|
|
if (syntax.typeeq(rhs.type_: *syntax.tinfo, tn.type_: *syntax.tinfo)) {
|
|
exact38 = true;
|
|
};
|
|
};
|
|
if (!exact38) {
|
|
let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n";
|
|
os.write(2, m40c.ptr, m40c.len: u64);
|
|
os.exit(1);
|
|
};
|
|
// fall through to the generic sret receive below.
|
|
};
|
|
// In-cap tuple initialiser (#105 / #164/#107): every in-cap
|
|
// tuple receive routes here, keyed on the DECLARED TYPE's
|
|
// register classify (sretretsize == 0, the shared SSoT) —
|
|
// mirror of cstage cgen.c N_LET tuple arm. Each element rides
|
|
// its SysV class — a float its SSE cursor reg (X0,X1 =
|
|
// tupsse), an integer/ptr word its INTEGER cursor reg
|
|
// (tupreg), a slice/str its 3-word {ptr,len,cap} header over
|
|
// consecutive INTEGER cursor regs — on INDEPENDENT counters.
|
|
// tupstore routes each element from its real class into its
|
|
// positional slot (eoff steps by the element's slot size: a
|
|
// slice/str takes its 24B header). Over-cap falls through to
|
|
// the sret receive below (#240 — an over-cap receive via the
|
|
// register cursor read garbage past R8).
|
|
//
|
|
// C-t1 (#33): the old keys were producer-SHAPE — the mixed
|
|
// str/scalar arm required s0_is_str != s1_is_str (syntactic)
|
|
// AND sz==16/32, the rt16 arm required an N_CALL rhs
|
|
// (rettupleof) — so a scalar-scalar tuple LITERAL `(3, 4)`
|
|
// matched neither and fell to the generic single-word store,
|
|
// silently dropping word 1 (#209/#211-class syntactic-vs-type
|
|
// keying). Alias-peel mirrors cstage's type_chase_named; the
|
|
// unannotated `let t = f()` shape rides the inferletcalltype
|
|
// tn above.
|
|
let ttup: *syntax.node = tn;
|
|
for (ttup != nil && ttup.kind == syntax.nkind.N_TNAME) {
|
|
ttup = aliaslookup(c, ttup.str);
|
|
};
|
|
if (ttup != nil) {
|
|
if (ttup.kind == syntax.nkind.N_TTUPLE
|
|
&& sretretsize(c, ttup) == 0) {
|
|
// #57: a tuple LITERAL rhs carries the DECLARED
|
|
// type into the cursor fill — its stamped type
|
|
// is element-constructed, so a declared-tagged
|
|
// element's concrete rvalue skipped the widen
|
|
// and the fill/receive cursor walks skewed
|
|
// (let-twin of the return-position bug; probe
|
|
// /tmp/p57/q1_let). Same emission as the cgexpr
|
|
// route for every declared-tagged-free literal.
|
|
if (rhs.kind == syntax.nkind.N_TUPLE) {
|
|
cgtuplelittocursor(c, rhs, ttup);
|
|
} else {
|
|
cgexpr(c, rhs);
|
|
};
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
let eoff: i32 = 0;
|
|
let q: *syntax.node = ttup.list;
|
|
for (q != nil) {
|
|
let qt: *syntax.node = q.lhs;
|
|
let isflt: bool = isfloattype(c, qt);
|
|
let eslot: i32 = tupeslotn(qt);
|
|
tupstore(c, gpcur, ssecur,
|
|
off + eoff, eslot, qt);
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + eslot / 8;
|
|
};
|
|
eoff = eoff + eslot;
|
|
q = q.next;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// #22a (rule 7, ken R1) wwstage half: an OVER-CAP tuple
|
|
// init whose rhs is not a CALL has no store path — only
|
|
// the CALL shape rides the sret receive below; every
|
|
// other rhs fell past ALL the store arms to NOTHING
|
|
// (silent uninitialized-frame reads). cgexpr's cursor
|
|
// materialisers loud most shapes, but their EXPR-shape
|
|
// counts let a declared-tagged element's unwidened
|
|
// payload (or a void literal) slip through in-cap
|
|
// (probe /tmp/i22b/p7) — the let-twin of the #22b
|
|
// classify/emit skew. Mirrors cstage cgen.c N_LET net.
|
|
if (ttup.kind == syntax.nkind.N_TTUPLE
|
|
&& rhs.kind != syntax.nkind.N_CALL
|
|
&& sretretsize(c, ttup) > 0) {
|
|
cgexpr(c, rhs);
|
|
let mnet: str = "over-cap tuple initialiser from a non-call source unwired (see #10/#22b)\n";
|
|
os.write(2, mnet.ptr, mnet.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
|
|
// Walk elements in declaration order, store each at off + i*esz
|
|
// using the right width for the element type. Trailing `...`
|
|
// after the last value (an nkind.N_FIELD with str=="...") fills the
|
|
// remaining slots up to the declared length with that value.
|
|
//
|
|
// str/slice element (24B = ptr+len+cap, post-#1) needs all 3
|
|
// words stored. cgstrlit / cgident leave it as (AX=ptr, BX=len,
|
|
// CX=cap) and a single MOVQ from AX would leave .len/.cap as
|
|
// whatever the stack held — silent miscompile. Worse,
|
|
// primsize("str") returns 0 so esz would fall back to 8, also
|
|
// collapsing the per-element stride (element i+1 would overwrite
|
|
// element i's would-be .len half). Detect the str/slice element
|
|
// case up front so both esz and the store path are right.
|
|
// (primsize's default-to-8-on-zero pattern is brittle for
|
|
// composites generally. The str/slice element now stores all 3
|
|
// words; [N]tagged element arrays still hit the gap, task #12.)
|
|
if (rhs.kind == syntax.nkind.N_ARRLIT) {
|
|
cgarrlitfillbp(c, n.lhs, rhs, off);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// Struct literal init: `let p: point = point{x=..., y=...};`.
|
|
// Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS
|
|
// autofill + per-field walk, with nested struct-typed structlit
|
|
// values recursing into the helper instead of landing only AX
|
|
// (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET
|
|
// structlit branch.
|
|
if (rhs.kind == syntax.nkind.N_STRUCTLIT) {
|
|
// #63: an alias-NAMED struct literal (`type rep2 = rep;
|
|
// let r = rep2{id=6}`) parses its type ref as N_IDENT/N_TNAME
|
|
// "rep2", but only the base `rep` is registered — bare
|
|
// structlookup(c, "rep2") returns nil, so the fill never
|
|
// fired: the slot zeroed + the lit DROPPED (≤8B silent) or
|
|
// fell to the :2920 LOUD (>8B). structlookupchain chases the
|
|
// alias chain to the base struct, the #92/W2 SSoT already
|
|
// adopted at cgenstmt:1974/:2687. cs chases via
|
|
// type_chase_named, runtime-correct.
|
|
let trefn: *syntax.node = rhs.lhs;
|
|
let si: *structinfo = structlookupchain(c, trefn);
|
|
if (si != nil) {
|
|
cgstructlitfillbp(c, si, rhs, off);
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
// sret receive (#23): plain TY_STRUCT > 24B from a call.
|
|
// The let's own slot IS the caller-prealloc dest; the
|
|
// nested cgexpr → cgcall path emits `LEAQ off(BP), DI`
|
|
// before the CALL and the callee writes through it. No
|
|
// AX/DX/CX shuffle; AX returns the dest pointer per SysV
|
|
// sret discipline (irrelevant here).
|
|
if (rhs.kind == syntax.nkind.N_CALL) {
|
|
let scs: i32 = callsretsize(c, rhs);
|
|
if (scs > 0) {
|
|
c.sretdestoff = off;
|
|
cgexpr(c, rhs);
|
|
c.sretdestoff = 0;
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
// Whole-struct receive for sizes <=24B (call-result rhs).
|
|
// Counterpart of #4's cgreturn ABI: cgexpr leaves
|
|
// AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23],
|
|
// zero-padded to 24B by the producer.
|
|
//
|
|
// ASYMMETRY (do NOT mirror the sender): producer emits three
|
|
// uniform MOVQs into a zero-padded 24B scratch slot; the
|
|
// receiver writes only `sz` bytes — MOVQ for full 8B chunks
|
|
// plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
|
|
// struct size. Otherwise a trailing 1..7-byte chunk would
|
|
// overrun into the next local slot.
|
|
//
|
|
// Tail chunks in {3,5,6,7} (unreachable under WW struct
|
|
// alignment rules — field aligns force size%align==0) fall
|
|
// through to the generic scalar store rather than emit a
|
|
// stomping MOVQ tail. Sizes >24B also fall through (sret
|
|
// deferred, same constraint as #4). Mirrors the cstage
|
|
// cgen.c N_LET receive branch.
|
|
// #171a: float-bearing struct RECEIVE (return twin of #165's
|
|
// param recv). cgexpr leaves each float eightbyte in its SSE
|
|
// return reg (X0,X1 = tupsse) and each INT eightbyte in its
|
|
// INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors
|
|
// per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read
|
|
// from the next XMM regardless of its positional eightbyte
|
|
// (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's
|
|
// abisize is maxalign-rounded to a multiple of 8 (an f64
|
|
// forces align 8), so every eightbyte is a full word — the
|
|
// #169 sized tail is unreachable here. structfloatclass gates
|
|
// to qualifying structs; all-int + f32 fall to the GP recv
|
|
// below (byte-id / #171b).
|
|
if (rhs.kind == syntax.nkind.N_CALL && tn != nil) {
|
|
let sfc: i32 = structfloatclass(c, tn);
|
|
if (sfc != 0) {
|
|
cgexpr(c, rhs);
|
|
let nb: i32 = sfc & 15;
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
let e: i32 = 0;
|
|
for (e < nb) {
|
|
let issse: bool = (sfc & (16 << e)) != 0;
|
|
if (issse) {
|
|
emitline("\tMOVSD\t");
|
|
emitline(tupsse(ssecur));
|
|
emitline(", ");
|
|
emitoff((off + e*8): i64);
|
|
emitline("(BP)\n");
|
|
ssecur += 1;
|
|
} else {
|
|
emitline("\tMOVQ\t");
|
|
emitline(tupreg(gpcur));
|
|
emitline(", ");
|
|
emitoff((off + e*8): i64);
|
|
emitline("(BP)\n");
|
|
gpcur += 1;
|
|
};
|
|
e += 1;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
if (rhs.kind == syntax.nkind.N_CALL) {
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (tn != nil) {
|
|
if (tn.kind == syntax.nkind.N_TNAME) {
|
|
sname = tn.str;
|
|
};
|
|
};
|
|
if (sname.len > 0) {
|
|
let lsi: *structinfo = structlookup(c, sname);
|
|
if (lsi != nil) {
|
|
// ≤24B register RECV: the value arrives packed
|
|
// in AX/DX/CX, so size by the maxalign-rounded
|
|
// ABI size (cstage lu->size), not the natural
|
|
// extent — see structabisize (#169).
|
|
let lsz: i32 = structabisize(lsi);
|
|
let tlm: i32 = lsz - (lsz / 8) * 8;
|
|
if (lsz <= 24) {
|
|
if (tlm == 0 || tlm == 1
|
|
|| tlm == 2 || tlm == 4) {
|
|
cgexpr(c, rhs);
|
|
let full: i32 = lsz / 8;
|
|
let i: i32 = 0;
|
|
for (i < full) {
|
|
let reg: str = "AX";
|
|
if (i == 1) { reg = "DX"; };
|
|
if (i == 2) { reg = "CX"; };
|
|
emitline("\tMOVQ\t");
|
|
emitline(reg);
|
|
emitline(", ");
|
|
emitoff((off + i * 8): i64);
|
|
emitline("(BP)\n");
|
|
i += 1;
|
|
};
|
|
if (tlm > 0) {
|
|
let top: str = "MOVB";
|
|
if (tlm == 4) { top = "MOVL"; };
|
|
if (tlm == 2) { top = "MOVW"; };
|
|
let treg: str = "AX";
|
|
if (full == 1) { treg = "DX"; };
|
|
if (full == 2) { treg = "CX"; };
|
|
emitline("\t");
|
|
emitline(top);
|
|
emitline("\t");
|
|
emitline(treg);
|
|
emitline(", ");
|
|
emitoff((off + full * 8): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// #267: array return-by-value RECV ≤24B — `let c = mk()`
|
|
// where mk returns an array. Arrays ride the struct reg-recv
|
|
// path (AX/DX/CX, sized tail). >24B sret rides the sret recv
|
|
// above (callsretsize keyed). Array natural size (tinfo.size
|
|
// = sub.size*len) mirrors cstage lu->size. No structfloatclass
|
|
// (pure-int element arrays).
|
|
if (rhs.kind == syntax.nkind.N_CALL && tn != nil
|
|
&& tn.kind == syntax.nkind.N_TARRAY) {
|
|
let ati: *syntax.tinfo = tn.type_: *syntax.tinfo;
|
|
ati = tichase(ati);
|
|
if (ati != nil) {
|
|
let lsz: i32 = ati.size: i32;
|
|
let tlm: i32 = lsz - (lsz / 8) * 8;
|
|
if (lsz <= 24) {
|
|
if (tlm == 0 || tlm == 1
|
|
|| tlm == 2 || tlm == 4) {
|
|
cgexpr(c, rhs);
|
|
let full: i32 = lsz / 8;
|
|
let i: i32 = 0;
|
|
for (i < full) {
|
|
let reg: str = "AX";
|
|
if (i == 1) { reg = "DX"; };
|
|
if (i == 2) { reg = "CX"; };
|
|
emitline("\tMOVQ\t");
|
|
emitline(reg);
|
|
emitline(", ");
|
|
emitoff((off + i * 8): i64);
|
|
emitline("(BP)\n");
|
|
i += 1;
|
|
};
|
|
if (tlm > 0) {
|
|
let top: str = "MOVB";
|
|
if (tlm == 4) { top = "MOVL"; };
|
|
if (tlm == 2) { top = "MOVW"; };
|
|
let treg: str = "AX";
|
|
if (full == 1) { treg = "DX"; };
|
|
if (full == 2) { treg = "CX"; };
|
|
emitline("\t");
|
|
emitline(top);
|
|
emitline("\t");
|
|
emitline(treg);
|
|
emitline(", ");
|
|
emitoff((off + full * 8): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// Struct ident copy: `let p2: T = p1;` where T is a struct
|
|
// >8B and rhs is a local ident. Per-qword MOVQ from src
|
|
// slot to dst slot, with a sized tail (MOVL/MOVB) for
|
|
// ABI sizes that aren't 8-aligned (e.g. `struct
|
|
// { i32, i32, i32 }`, maxalign 4 → ABI 12B). Pre-fix this path fell
|
|
// through to `cgexpr + MOVQ AX, off(BP)` which stored
|
|
// only the first qword (and a stale BX for sz==16 lets
|
|
// via the str-init tail) — silent partial copy. Mirrors
|
|
// cstage cgen.c N_LET struct-ident branch (Task #32).
|
|
if (rhs.kind == syntax.nkind.N_IDENT) {
|
|
let sname: str;
|
|
sname.ptr = nil; sname.len = 0;
|
|
if (tn != nil) {
|
|
if (tn.kind == syntax.nkind.N_TNAME) { sname = tn.str; };
|
|
};
|
|
if (sname.len > 0) {
|
|
let lsi: *structinfo = structlookup(c, sname);
|
|
if (lsi != nil) {
|
|
// memcpy run sizes on the maxalign-rounded ABI
|
|
// size — cstage N_LET sets sz = lu->size
|
|
// (cgen.c:7533, struct-IDENT branch :7869),
|
|
// check.c:760 SSoT. structnaturalsize would
|
|
// short struct{i64,i32} (natural 12, ABI 16)
|
|
// to MOVQ+MOVL where cstage writes MOVQ+MOVQ.
|
|
let lsz: i32 = structabisize(lsi);
|
|
if (lsz > 8) {
|
|
let lc: *local = localfindnode(c, rhs.str);
|
|
if (lc != nil) {
|
|
let soff: i32 = lc.off;
|
|
let ki: i32 = 0;
|
|
for (ki + 8 <= lsz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((soff + ki): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + ki): i64);
|
|
emitline("(BP)\n");
|
|
ki += 8;
|
|
};
|
|
if (ki < lsz) {
|
|
let tail: i32 = lsz - ki;
|
|
let lop: str = "MOVQ";
|
|
if (tail == 4) { lop = "MOVL"; }
|
|
else { if (tail == 1) { lop = "MOVB"; }; };
|
|
emitline("\t");
|
|
emitline(lop);
|
|
emitline("\t");
|
|
emitoff((soff + ki): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\t");
|
|
emitline(lop);
|
|
emitline("\tAX, ");
|
|
emitoff((off + ki): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// #265 fold-1/1b (#268): aggregate let-init copy from an
|
|
// ADDRESSABLE rhs — `*p` (deref), an array ident `= s`
|
|
// (struct-ident is the arm above), an N_DOT field `= o.i`, an
|
|
// N_INDEX element `= a[i]`, T a struct/array >8B. ONE memcpy
|
|
// loop fed by a per-rhs source-address setup landing the SOURCE
|
|
// ADDRESS in SI; copy N bytes (the #254 non-slot-padded ABI
|
|
// extent: structabisize for a struct, tinfo.size for an array)
|
|
// slot→slot — a MOVQ run plus a sized MOVL/MOVW/MOVB tail. Pre-
|
|
// fix array-ident/N_DOT truncated to the 8B scalar tail below
|
|
// and N_INDEX scalar-loaded the element address (segfault).
|
|
// Mirror of cstage cgen.c N_LET arm (rule-10); the by-value
|
|
// RETURN ABI is fold-2 (#267). Source-addr setups reuse closed
|
|
// machinery: LEAQ-slot (ident), the deref operand (cgexpr),
|
|
// dotchainaddr (#253, N_DOT), the &base[i] spine (#252,
|
|
// N_INDEX).
|
|
let aggn: i32 = 0;
|
|
let aggsi: *structinfo = structlookupchain(c, tn);
|
|
if (aggsi != nil) {
|
|
aggn = structabisize(aggsi);
|
|
} else {
|
|
let aggti: *syntax.tinfo = nil;
|
|
if (tn != nil) { aggti = tn.type_: *syntax.tinfo; };
|
|
aggti = tichase(aggti);
|
|
if (aggti != nil) {
|
|
if (aggti.kind == syntax.tykind.TY_ARRAY) {
|
|
aggn = aggti.size: i32;
|
|
};
|
|
};
|
|
};
|
|
if (aggn > 8) {
|
|
let havesrc: bool = false;
|
|
if (rhs.kind == syntax.nkind.N_UN) {
|
|
if (rhs.op == syntax.tkind.TK_STAR) {
|
|
cgexpr(c, rhs.lhs);
|
|
emitline("\tMOVQ\tAX, SI\n");
|
|
havesrc = true;
|
|
};
|
|
};
|
|
if (!havesrc) { if (rhs.kind == syntax.nkind.N_IDENT) {
|
|
let lc: *local = localfindnode(c, rhs.str);
|
|
if (lc != nil) {
|
|
emitline("\tLEAQ\t");
|
|
emitoff(lc.off: i64);
|
|
emitline("(BP), SI\n");
|
|
havesrc = true;
|
|
} else {
|
|
// rule-10: the addressable-def set must
|
|
// equal cstage's let_islet ||
|
|
// def_isarraydef || def_isstructdef —
|
|
// the laid-out-aggregate globals (#129
|
|
// A.2/A.3). Bare deflookup (any def)
|
|
// over-copies struct-defs on wwstage
|
|
// only; mirror the defisaddressable
|
|
// pairing instead.
|
|
let aggdtn: *syntax.node = defvartnode(c, rhs.str);
|
|
let aggisdef: bool = defvarstructinfo(c, rhs.str) != nil;
|
|
if (aggdtn != nil) {
|
|
if (aggdtn.kind == syntax.nkind.N_TARRAY) { aggisdef = true; };
|
|
};
|
|
if (isletvar(c, rhs.str) || aggisdef) {
|
|
emitline("\tLEAQ\t");
|
|
emitsymname(c, rhs.str);
|
|
emitline("(SB), SI\n");
|
|
havesrc = true;
|
|
};
|
|
};
|
|
}; };
|
|
if (!havesrc) { if (rhs.kind == syntax.nkind.N_DOT) {
|
|
if (dotchainaddr(c, rhs, "SI")) {
|
|
havesrc = true;
|
|
};
|
|
}; };
|
|
if (!havesrc) { if (rhs.kind == syntax.nkind.N_INDEX) {
|
|
let base: *syntax.node = rhs.lhs;
|
|
let idx: *syntax.node = rhs.rhs;
|
|
let bu: *syntax.tinfo = nil;
|
|
if (base != nil) { bu = base.type_: *syntax.tinfo; };
|
|
bu = tichase(bu);
|
|
if (base != nil && base.kind == syntax.nkind.N_IDENT
|
|
&& bu != nil && bu.kind == syntax.tykind.TY_ARRAY) {
|
|
let esz: i32 = 1;
|
|
if (bu.sub != nil) {
|
|
esz = bu.sub.size: i32;
|
|
};
|
|
cgexpr(c, idx);
|
|
if (esz > 1) {
|
|
emitline("\tMOVQ\t$");
|
|
emitint(esz: i64);
|
|
emitline(", CX\n");
|
|
emitline("\tIMULQ\tCX, AX\n");
|
|
};
|
|
let bl: *local = localfindnode(c,
|
|
base.str);
|
|
if (bl != nil) {
|
|
emitline("\tLEAQ\t");
|
|
emitoff(bl.off: i64);
|
|
emitline("(BP), BX\n");
|
|
} else {
|
|
emitline("\tLEAQ\t");
|
|
emitsymname(c, base.str);
|
|
emitline("(SB), BX\n");
|
|
};
|
|
emitline("\tADDQ\tBX, AX\n");
|
|
emitline("\tMOVQ\tAX, SI\n");
|
|
havesrc = true;
|
|
};
|
|
// #270-3a: the index BASE is an N_DOT
|
|
// array-field (`x.arr[i]`) or a nested N_INDEX
|
|
// (`a[i][j]`); the N_IDENT-base arm above missed
|
|
// both, so the copy fell to the 8B truncation
|
|
// below. Compute &base[idx]: scaled idx on the
|
|
// stack, then &base via dotbaseaddr (N_DOT field
|
|
// address) or the &abase[bidx] spine (nested
|
|
// N_IDENT-array base), then add.
|
|
if (!havesrc && base != nil
|
|
&& (base.kind == syntax.nkind.N_DOT
|
|
|| base.kind == syntax.nkind.N_INDEX)) {
|
|
let esz2: i32 = 1;
|
|
if (bu != nil && bu.sub != nil) {
|
|
esz2 = bu.sub.size: i32;
|
|
};
|
|
cgexpr(c, idx);
|
|
if (esz2 > 1) {
|
|
emitline("\tMOVQ\t$");
|
|
emitint(esz2: i64);
|
|
emitline(", CX\n");
|
|
emitline("\tIMULQ\tCX, AX\n");
|
|
};
|
|
emitline("\tPUSHQ\tAX\n");
|
|
let baseok: bool = false;
|
|
if (base.kind == syntax.nkind.N_DOT) {
|
|
if (dotbaseaddr(c, base, "AX")) {
|
|
baseok = true;
|
|
};
|
|
} else {
|
|
let ab: *syntax.node = base.lhs;
|
|
let bidx: *syntax.node = base.rhs;
|
|
let abu: *syntax.tinfo = nil;
|
|
if (ab != nil) { abu = ab.type_: *syntax.tinfo; };
|
|
abu = tichase(abu);
|
|
if (ab != nil && ab.kind == syntax.nkind.N_IDENT
|
|
&& abu != nil && abu.kind == syntax.tykind.TY_ARRAY) {
|
|
let aesz: i32 = 1;
|
|
if (abu.sub != nil) {
|
|
aesz = abu.sub.size: i32;
|
|
};
|
|
cgexpr(c, bidx);
|
|
if (aesz > 1) {
|
|
emitline("\tMOVQ\t$");
|
|
emitint(aesz: i64);
|
|
emitline(", CX\n");
|
|
emitline("\tIMULQ\tCX, AX\n");
|
|
};
|
|
let abl: *local = localfindnode(c, ab.str);
|
|
if (abl != nil) {
|
|
emitline("\tLEAQ\t");
|
|
emitoff(abl.off: i64);
|
|
emitline("(BP), BX\n");
|
|
} else {
|
|
emitline("\tLEAQ\t");
|
|
emitsymname(c, ab.str);
|
|
emitline("(SB), BX\n");
|
|
};
|
|
emitline("\tADDQ\tBX, AX\n");
|
|
baseok = true;
|
|
};
|
|
};
|
|
emitline("\tPOPQ\tBX\n");
|
|
if (baseok) {
|
|
emitline("\tADDQ\tBX, AX\n");
|
|
emitline("\tMOVQ\tAX, SI\n");
|
|
havesrc = true;
|
|
};
|
|
};
|
|
}; };
|
|
// C4 (F5, task #7): the remaining ADDRESSABLE rhs
|
|
// shapes — a slice-base element (`= xs[0]`; the arms
|
|
// above have TY_ARRAY/N_DOT/N_INDEX bases but no
|
|
// TY_SLICE base) and deref-spine leaves
|
|
// (`= (*ts)[i].cap`) — resolve through cgplaceaddr
|
|
// (the C1 resolver; enumerated arms dispatch first so
|
|
// their asm is untouched). Pre-C4 these fell through
|
|
// to the scalar default's 8B truncation while cstage
|
|
// emitted NOTHING — gate-blind cs≠ww.
|
|
if (!havesrc) {
|
|
if (cgplaceaddr(c, rhs, "SI")) { havesrc = true; };
|
|
};
|
|
if (havesrc) {
|
|
let k: i32 = 0;
|
|
for (k + 8 <= aggn) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
if (k + 4 <= aggn) {
|
|
emitline("\tMOVL\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
if (k + 2 <= aggn) {
|
|
emitline("\tMOVW\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVW\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 2;
|
|
};
|
|
if (k + 1 <= aggn) {
|
|
emitline("\tMOVB\t");
|
|
emitoff(k: i64);
|
|
emitline("(SI), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
// #38b (rule 7): `?`/`!` over an sret-class call into
|
|
// an aggregate let — keep the established #38b/#40
|
|
// loud-stop marker (mirror of cstage's pre-arm fatal,
|
|
// cgen.c N_LET; pre-C4 this shape fell through to the
|
|
// cgtryunw/cgtryprop gates, which the C4 tail below
|
|
// now pre-empts in let position).
|
|
if (rhs.kind == syntax.nkind.N_TRYUNW
|
|
|| rhs.kind == syntax.nkind.N_TRYPROP) {
|
|
if (rhs.lhs != nil) {
|
|
if (rhs.lhs.kind == syntax.nkind.N_CALL) {
|
|
if (callsretsize(c, rhs.lhs) > 0) {
|
|
let m38f: str = "#38b: `?`/`!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
|
|
os.write(2, m38f.ptr, m38f.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// C4: nothing below this arm can initialise a >8B
|
|
// struct/array slot — the scalar default's 8B store
|
|
// was a silent truncation (rule 7).
|
|
let mf5: str = "let: aggregate init from unhandled rhs shape (task #7/rule-7)\n";
|
|
os.write(2, mf5.ptr, mf5.len: u64);
|
|
os.exit(1);
|
|
};
|
|
cgexpr(c, rhs);
|
|
// Float local: cgexpr leaves the value in X0. Spill via
|
|
// MOVSS (f32, 4B) or MOVSD (f64, 8B).
|
|
if (isfloattype(c, n.lhs)) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, n.lhs)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\tX0, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
// str IS []u8: cgexpr leaves (ptr,len,cap) in AX/BX/CX; store
|
|
// all three, same as the slice arm below (#1/Phase 3).
|
|
// #60: gate by kind too — under #1's str=24 bump, sizeof(str)
|
|
// and sizeof(slice) collide, so a bare `sz ==` check fires
|
|
// both branches for one let. Mirrors cstage cgen.c's
|
|
// `type_isstr(lt) && sz == ty_str->size` shape.
|
|
if (isstrtype(c, tn) && sz == primtypesize("str"): i32) {
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
// slice init: ptr/len/cap in AX/BX/CX. Same kind+size gate as
|
|
// the str arm — without the kind check this fires on a str let
|
|
// once sz==24 (#60).
|
|
if (isslicetype(c, tn) && sz == tyslicesize(): i32) {
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tCX, ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
} else {
|
|
// Bare `let x: T;` with no initializer. C cgen
|
|
// (cmd/w6c/cgen.c N_LET no-rhs branch) zero-inits in two
|
|
// shapes:
|
|
// - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.):
|
|
// single `MOVQ $0, off(BP)`.
|
|
// - multi-word composites (str/slice/tuple/struct/tagged):
|
|
// `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot
|
|
// so reads after the bare let see {0...} rather than
|
|
// stack garbage.
|
|
// #84 (user ruling, Go-zero): `[N]T` arrays zero-fill like
|
|
// every other composite. They were excluded here, so a
|
|
// dirtied-stack `let a: [3]int;` read garbage — BOTH stages,
|
|
// both-wrong-IDENTICAL, gate-blind (#263). Dropping the
|
|
// exclusion (cstage dropped `!TY_ARRAY` symmetrically) routes
|
|
// arrays into the zsz>8 / zsz==8 arms below; an 8B array is
|
|
// already caught by typeis8byteprimitive (TY_ARRAY size==8) →
|
|
// single MOVQ $0, matching cstage's sz==8 store.
|
|
// Zero-fill extent. cstage sizes the run on `lu->size`
|
|
// (the natural ABI size from the type table, cgen.c:8397);
|
|
// wwstage's `sz` from letslotsize is slot-padded (round-to-8),
|
|
// so a struct with maxalign<8 and a sub-8 tail would over-zero
|
|
// MOVQ where cstage emits nothing. Source the extent from the
|
|
// type table's tinfo.size for a struct-typed let to converge;
|
|
// slot allocation stays on `sz` (frame uses slot-padded slots).
|
|
// #254: structabisize is NOT a sound ABI-size source here — it
|
|
// sums fieldsize(), which slot-pads a nested value-struct field
|
|
// to 8, so a sub-8 outer struct (e.g. `struct{struct{[4]u8}}`,
|
|
// ABI 4) read 8 and emitted a stray MOVQ $0 cstage doesn't.
|
|
// fieldsize / registerstruct / frame slot-padding stay
|
|
// UNTOUCHED — moving the fix there would shift field offsets.
|
|
let zsz: i32 = sz;
|
|
if (n.lhs != nil) {
|
|
// #84: an array's zero-fill extent is its chased ABI
|
|
// size (cstage `lu->size`), NOT the slot-padded sz from
|
|
// letslotsize — a non-8-multiple array (e.g. [20]u8 = 20)
|
|
// would over-zero MOVQ-rounded to 24 and diverge from
|
|
// cstage's exact 20-byte run. Handles direct N_TARRAY and
|
|
// alias-to-array (N_TNAME chasing through TY_NAMED) alike.
|
|
let zti: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
|
|
zti = tichase(zti);
|
|
if (zti != nil && zti.kind == syntax.tykind.TY_ARRAY) {
|
|
zsz = zti.size: i32;
|
|
} else { if (n.lhs.kind == syntax.nkind.N_TNAME) {
|
|
let szi: *structinfo = structlookupchain(c, n.lhs);
|
|
if (szi != nil) {
|
|
let ti: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
|
|
ti = tichase(ti);
|
|
if (ti != nil) { zsz = ti.size: i32; };
|
|
};
|
|
}; };
|
|
};
|
|
if (typeis8byteprimitive(c, n.lhs)) {
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
} else { if (zsz == 8) {
|
|
// #213: an 8B composite (single-field struct / tagged) is
|
|
// neither an 8B primitive nor zsz>8, so it fell through
|
|
// un-zeroed while cstage emits MOVQ $0 (cgen.c N_LET
|
|
// `else if (sz == 8)`); a read-before-init then saw stack
|
|
// garbage (cs!=ww byte-id + a latent garbage-read). Match
|
|
// cstage's immediate MOVQ $0, checked BEFORE the run arm
|
|
// below so an 8B slot stays one immediate store, not
|
|
// XORQ+MOVQ (rule-10 byte-id).
|
|
emitline("\tMOVQ\t$0, ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
} else { if (zsz > 0) {
|
|
// #16: the run arm was gated `zsz > 8`, so a SUB-8
|
|
// aggregate (`let c: [3]u8;` = 3, a 3-byte struct, etc.)
|
|
// matched no arm and fell through un-zeroed — the exact
|
|
// stack-garbage read ken's bytes verdict pinpointed
|
|
// (ltrim_cases' `let c: [3]u8;`), BOTH stages, gate-blind
|
|
// (#263). cstage widened its `!n->rhs && sz > 8` gate to
|
|
// `sz > 0` symmetrically; the MOVL/MOVB tail already sizes
|
|
// the run to any 1..7-byte extent. (`[0]T`, zsz == 0, needs
|
|
// no stores — the lone XORQ is skipped, matching cstage.)
|
|
emitline("\tXORQ\tAX, AX\n");
|
|
let zi: i32 = 0;
|
|
for (zi + 8 <= zsz) {
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 8;
|
|
};
|
|
for (zi + 4 <= zsz) {
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 4;
|
|
};
|
|
for (zi < zsz) {
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((off + zi): i64);
|
|
emitline("(BP)\n");
|
|
zi += 1;
|
|
};
|
|
}; }; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
fn cgif(c: *cgen, n: *syntax.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: *syntax.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");
|
|
};
|
|
|
|
// #42: bound the push. The buffers are sized exactly LOOP_MAX, so an
|
|
// unguarded push at nesting depth LOOP_MAX+1 is an OOB heap write;
|
|
// fail loud at the cap, both stages (cgen.c twin fatals too).
|
|
if (c.looptop >= LOOP_MAX) {
|
|
let msg: str = "cgen: loop nesting too deep\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
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: *syntax.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: *syntax.node = rettupleof(c, n.rhs);
|
|
|
|
// #10 Fold B: over-cap tuple destructure REASSIGN. Same sret copy-out
|
|
// as cgmlet but the slots already exist (localfind); a `_` / missing
|
|
// binding (off == 0) SKIPS its store yet still ADVANCES foff so the
|
|
// next element stays aligned (harec `_`). Byte-identical to the
|
|
// cstage N_MASSIGN over-cap arm.
|
|
let sretrecv: i32 = 0;
|
|
if (n.rhs != nil) {
|
|
if (n.rhs.kind == syntax.nkind.N_CALL) {
|
|
sretrecv = callsretsize(c, n.rhs);
|
|
};
|
|
};
|
|
|
|
// #64: a tuple-LITERAL rhs carries a DECLARED tuple type (built from
|
|
// the lvalue binding types) into the cursor fill, so a declared-tagged
|
|
// element's concrete rvalue widens into the box instead of riding the
|
|
// decl-less stamped-keyed route — the #57 decl wire extended past
|
|
// cgmlet/cgreturn to destructure-reassign. A `_` lvalue has no local
|
|
// (no declared type node); its decl element stays nil and the
|
|
// fill/receive fall back to the rhs literal element's own stamped type
|
|
// for the cursor stride (harec `_` advance; pinned by the R3 control).
|
|
let litrhs: bool = false;
|
|
if (n.rhs != nil) { if (n.rhs.kind == syntax.nkind.N_TUPLE) { litrhs = true; }; };
|
|
let synthdecl: *syntax.node = nil;
|
|
if (litrhs) {
|
|
synthdecl = syntax.newnode(syntax.nkind.N_TTUPLE, n.rhs.file, n.rhs.line, n.rhs.col);
|
|
let dtail: *syntax.node = nil;
|
|
let lb0: *syntax.node = n.list;
|
|
for (lb0 != nil) {
|
|
let w: *syntax.node = syntax.newnode(syntax.nkind.N_TUPLE, n.rhs.file, n.rhs.line, n.rhs.col);
|
|
w.lhs = nil;
|
|
if (lb0.kind == syntax.nkind.N_IDENT) {
|
|
let lc0: *local = localfindnode(c, lb0.str);
|
|
if (lc0 != nil) { w.lhs = lc0.tnode; };
|
|
};
|
|
w.next = nil;
|
|
if (dtail == nil) { synthdecl.list = w; } else { dtail.next = w; };
|
|
dtail = w;
|
|
lb0 = lb0.next;
|
|
};
|
|
cgtuplelittocursor(c, n.rhs, synthdecl);
|
|
} else {
|
|
if (n.rhs != nil) { cgexpr(c, n.rhs); };
|
|
};
|
|
|
|
if (sretrecv > 0) {
|
|
let scr: i32 = localfind(c, "@sretscr");
|
|
let pt2: *syntax.node = nil;
|
|
if (rettuple != nil) { pt2 = rettuple.list; };
|
|
let foff: i32 = 0;
|
|
let lb: *syntax.node = n.list;
|
|
for (lb != nil) {
|
|
let tn: *syntax.node = nil;
|
|
if (pt2 != nil) { tn = pt2.lhs; };
|
|
let isflt: bool = isfloattype(c, tn);
|
|
// #22b: the >8B copy-out keys on the ACCESSOR's slot
|
|
// (str/slice header AND tagged box), not a str/slice
|
|
// kind test — the tagged element took the scalar arm
|
|
// (8B silent truncation; unreachable while the SEND
|
|
// louded, live once #22b unwires it). Byte-id for
|
|
// str/slice (esz == eslot == 24). Mirrors the cstage
|
|
// N_MASSIGN sret arm + the R-1 all-three-routings lesson.
|
|
let eslot: i32 = tupeslotn(tn);
|
|
let esz: i32 = 8;
|
|
if (pt2 != nil) {
|
|
let eti: *syntax.tinfo = pt2.lhs.type_: *syntax.tinfo;
|
|
if (eti != nil) { esz = eti.size: i32; };
|
|
};
|
|
let off: i32 = 0;
|
|
if (lb.kind == syntax.nkind.N_IDENT) { off = localfind(c, lb.str); };
|
|
if (off != 0) {
|
|
if (isflt) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, tn)) { mov = "MOVSS"; };
|
|
emitline("\t"); emitline(mov); emitline("\t");
|
|
emitoff((scr + foff): i64);
|
|
emitline("(BP), X0\n");
|
|
emitline("\t"); emitline(mov);
|
|
emitline("\tX0, ");
|
|
emitoff(off: i64); emitline("(BP)\n");
|
|
} else {
|
|
if (eslot > 8) {
|
|
let k: i32 = 0;
|
|
for (k < eslot) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scr + foff + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
} else {
|
|
let lop: str = tnodeloadop(c, tn, esz);
|
|
let sop: str = tnodestoreop(c, tn, esz);
|
|
emitline("\t"); emitline(lop);
|
|
emitline("\t");
|
|
emitoff((scr + foff): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\t"); emitline(sop);
|
|
emitline("\tAX, ");
|
|
emitoff(off: i64); emitline("(BP)\n");
|
|
};
|
|
};
|
|
};
|
|
// C-t0: slot stride — must mirror the N_RETURN
|
|
// over-cap SEND's buffer layout (cstage N_MASSIGN
|
|
// twin strides tuple_eslot).
|
|
foff += tupeslotn(tn);
|
|
lb = lb.next;
|
|
if (pt2 != nil) { pt2 = pt2.next; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
|
|
let gptotal: i32 = 0;
|
|
let ssetotal: i32 = 0;
|
|
let l: *syntax.node = n.list;
|
|
let pt: *syntax.node = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
// #64: a tuple-LITERAL rhs keys element WIDTH on the DECLARED lvalue
|
|
// type (synthdecl), not the rettuple (nil for a literal); a `_` slot
|
|
// (declared type nil) falls back to the rhs literal element's own
|
|
// stamped type for the cursor stride.
|
|
let dp: *syntax.node = nil;
|
|
let re: *syntax.node = nil;
|
|
if (litrhs) { dp = synthdecl.list; re = n.rhs.list; };
|
|
for (l != nil) {
|
|
let tn: *syntax.node = nil;
|
|
if (litrhs) {
|
|
if (dp != nil && dp.lhs != nil) { tn = dp.lhs; } else { tn = re; };
|
|
} else {
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
};
|
|
if (isfloattype(c, tn)) {
|
|
ssetotal = ssetotal + 1;
|
|
} else {
|
|
gptotal = gptotal + tupeslotn(tn) / 8;
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
if (dp != nil) { dp = dp.next; };
|
|
if (re != nil) { re = re.next; };
|
|
};
|
|
if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity
|
|
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
|
|
// fatal(), err.c) — surface, don't corrupt.
|
|
let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
if (ssetotal > ssecap) {
|
|
let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
l = n.list;
|
|
pt = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
dp = nil;
|
|
re = nil;
|
|
if (litrhs) { dp = synthdecl.list; re = n.rhs.list; };
|
|
for (l != nil) {
|
|
let tn: *syntax.node = nil;
|
|
if (litrhs) {
|
|
if (dp != nil && dp.lhs != nil) { tn = dp.lhs; } else { tn = re; };
|
|
} else {
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
};
|
|
let isflt: bool = isfloattype(c, tn);
|
|
let eslot: i32 = tupeslotn(tn);
|
|
let off: i32 = 0;
|
|
if (l.kind == syntax.nkind.N_IDENT) { off = localfind(c, l.str); };
|
|
// harec `_` (off==0): skip the store but CONSUME the cursor
|
|
// slot so the next element stays aligned.
|
|
if (off != 0) {
|
|
tupstore(c, gpcur, ssecur, off, eslot, tn);
|
|
};
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + eslot / 8;
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
if (dp != nil) { dp = dp.next; };
|
|
if (re != nil) { re = re.next; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// Multi-let from a tuple-returning call: `let n, s = call();` or
|
|
// `let (n, s) = call();`. wwstage has no checker, so each binding's
|
|
// type is taken from its explicit annotation (l.lhs) when present
|
|
// or inferred from the called fn's return-type tuple element.
|
|
//
|
|
// Per the AX:DX:CX:R8 return convention (mirrors C cgen nkind.N_MLET):
|
|
// (scalar, scalar) — AX → l0, DX → l1.
|
|
// (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot
|
|
// as (.ptr, .len, .cap). Position-agnostic — the
|
|
// regs are routed by element type, not by AX/DX.
|
|
// str IS []u8 (24B): cap rides R8 (#1/Phase 3, task #5).
|
|
fn cgmlet(c: *cgen, n: *syntax.node) void = {
|
|
let rhs: *syntax.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: *syntax.node = rettupleof(c, rhs);
|
|
|
|
// #10 Fold B: over-cap tuple destructure RECEIVE. The callee sret'd
|
|
// the whole tuple into the @sretscr discard slot (cgcall sees
|
|
// callsretsize > 0, no lvalue dest wired). Copy each element out to
|
|
// its binding slot at the SAME packed offset the SEND wrote (foff +=
|
|
// element size — the t.0/t.1 layout), each at its NATURAL width
|
|
// (#169). Byte-identical to the cstage N_MLET over-cap arm.
|
|
let sretrecv: i32 = 0;
|
|
if (rhs.kind == syntax.nkind.N_CALL) { sretrecv = callsretsize(c, rhs); };
|
|
|
|
// #242: rhs is a tuple already materialised in a local slot (a match-
|
|
// bound union payload, `let (a,b)=t`), NOT a register-returning call.
|
|
// cgexpr(tuple ident) loads only word0->AX, so the register cursor
|
|
// path below reads DX/CX stale. Copy each element from the ident's
|
|
// slot at the register-ABI 8B stride (24B for a slice/str header) —
|
|
// the SAME layout the tagged construct + match payload-bind write.
|
|
// Mirror of cstage cgen.c N_MLET tuple-ident arm. The binding element
|
|
// types ride l.lhs (stamped by the checker's stamptuplebinds).
|
|
if (rhs.kind == syntax.nkind.N_IDENT) {
|
|
let rl: *local = localfindnode(c, rhs.str);
|
|
if (rl != nil) {
|
|
let rti: *syntax.tinfo = rl.tnode.type_: *syntax.tinfo;
|
|
rti = tichase(rti);
|
|
if (rti != nil) { if (rti.kind == syntax.tykind.TY_TUPLE) {
|
|
let srcoff: i32 = rl.off;
|
|
let foff: i32 = 0;
|
|
let lb: *syntax.node = n.list;
|
|
for (lb != nil) {
|
|
let tn: *syntax.node = lb.lhs;
|
|
let isflt: bool = isfloattype(c, tn);
|
|
let eslot: i32 = tupeslotn(tn);
|
|
let esz: i32 = 8;
|
|
let eti: *syntax.tinfo = nil;
|
|
if (tn != nil) { eti = tn.type_: *syntax.tinfo; };
|
|
if (eti != nil) { esz = eti.size: i32; };
|
|
let bsz: i32 = 8;
|
|
if (eslot > 8) { bsz = eslot; };
|
|
let off: i32 = localadd(c, lb.str, bsz, tn);
|
|
if (isflt) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, tn)) { mov = "MOVSS"; };
|
|
emitline("\t"); emitline(mov); emitline("\t");
|
|
emitoff((srcoff + foff): i64);
|
|
emitline("(BP), X0\n");
|
|
emitline("\t"); emitline(mov); emitline("\tX0, ");
|
|
emitoff(off: i64); emitline("(BP)\n");
|
|
} else { if (eslot > 8) {
|
|
let k: i32 = 0;
|
|
for (k < eslot) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((srcoff + foff + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
} else {
|
|
let lop: str = tnodeloadop(c, tn, esz);
|
|
let sop: str = tnodestoreop(c, tn, esz);
|
|
emitline("\t"); emitline(lop); emitline("\t");
|
|
emitoff((srcoff + foff): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\t"); emitline(sop); emitline("\tAX, ");
|
|
emitoff(off: i64); emitline("(BP)\n");
|
|
}; };
|
|
foff += eslot;
|
|
lb = lb.next;
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
}; };
|
|
};
|
|
};
|
|
|
|
cgexpr(c, rhs);
|
|
|
|
if (sretrecv > 0) {
|
|
let scr: i32 = localfind(c, "@sretscr");
|
|
let pt2: *syntax.node = nil;
|
|
if (rettuple != nil) { pt2 = rettuple.list; };
|
|
let foff: i32 = 0;
|
|
let lb: *syntax.node = n.list;
|
|
for (lb != nil) {
|
|
let tn: *syntax.node = nil;
|
|
if (pt2 != nil) { tn = pt2.lhs; };
|
|
let isflt: bool = isfloattype(c, tn);
|
|
let eslot: i32 = tupeslotn(tn);
|
|
let esz: i32 = 8;
|
|
if (pt2 != nil) {
|
|
let eti: *syntax.tinfo = pt2.lhs.type_: *syntax.tinfo;
|
|
if (eti != nil) { esz = eti.size: i32; };
|
|
};
|
|
let bsz: i32 = 8;
|
|
if (eslot > 8) { bsz = eslot; };
|
|
let off: i32 = localadd(c, lb.str, bsz, tn);
|
|
if (isflt) {
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, tn)) { mov = "MOVSS"; };
|
|
emitline("\t"); emitline(mov); emitline("\t");
|
|
emitoff((scr + foff): i64);
|
|
emitline("(BP), X0\n");
|
|
emitline("\t"); emitline(mov); emitline("\tX0, ");
|
|
emitoff(off: i64); emitline("(BP)\n");
|
|
} else {
|
|
if (eslot > 8) {
|
|
let k: i32 = 0;
|
|
for (k < eslot) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((scr + foff + k): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
} else {
|
|
let lop: str = tnodeloadop(c, tn, esz);
|
|
let sop: str = tnodestoreop(c, tn, esz);
|
|
emitline("\t"); emitline(lop); emitline("\t");
|
|
emitoff((scr + foff): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\t"); emitline(sop);
|
|
emitline("\tAX, ");
|
|
emitoff(off: i64); emitline("(BP)\n");
|
|
};
|
|
};
|
|
foff += eslot;
|
|
lb = lb.next;
|
|
if (pt2 != nil) { pt2 = pt2.next; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
|
|
let gptotal: i32 = 0;
|
|
let ssetotal: i32 = 0;
|
|
let l: *syntax.node = n.list;
|
|
let pt: *syntax.node = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
for (l != nil) {
|
|
let tn: *syntax.node = l.lhs;
|
|
if (tn == nil) {
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
};
|
|
if (isfloattype(c, tn)) {
|
|
ssetotal = ssetotal + 1;
|
|
} else {
|
|
gptotal = gptotal + tupeslotn(tn) / 8;
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity
|
|
// pinned loud-stop, inline like cgen.ww:604 (cstage uses
|
|
// fatal(), err.c) — surface, don't corrupt.
|
|
let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
if (ssetotal > ssecap) {
|
|
let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
|
|
let gpcur: i32 = 0;
|
|
let ssecur: i32 = 0;
|
|
l = n.list;
|
|
pt = nil;
|
|
if (rettuple != nil) { pt = rettuple.list; };
|
|
for (l != nil) {
|
|
let tn: *syntax.node = l.lhs;
|
|
if (tn == nil) {
|
|
if (pt != nil) { tn = pt.lhs; };
|
|
};
|
|
let isflt: bool = isfloattype(c, tn);
|
|
let eslot: i32 = tupeslotn(tn);
|
|
let sz: i32 = 8;
|
|
if (eslot > 8) { sz = eslot; };
|
|
let off: i32 = localadd(c, l.str, sz, tn);
|
|
tupstore(c, gpcur, ssecur, off, eslot, tn);
|
|
if (isflt) {
|
|
ssecur = ssecur + 1;
|
|
} else {
|
|
gpcur = gpcur + eslot / 8;
|
|
};
|
|
l = l.next;
|
|
if (pt != nil) { pt = pt.next; };
|
|
};
|
|
c.lastwasreturn = 0;
|
|
return;
|
|
};
|
|
|
|
// paramfieldsize — raw byte size of a tuple-field type. Mirrors the
|
|
// `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for
|
|
// i32/u32, 8 for i64/u64/*T/fn, 24 for str/slice (str IS []u8, the slice
|
|
// header SSoT), tuple → sum of its 8B-floored element slots, default 8.
|
|
fn paramfieldsize(t: *syntax.node) i32 = {
|
|
if (t == nil) { return 8; };
|
|
let k: syntax.nkind = t.kind;
|
|
if (k == syntax.nkind.N_TPTR) { return 8; };
|
|
if (k == syntax.nkind.N_TFN) { return 8; };
|
|
if (k == syntax.nkind.N_TCHAN) { return 8; };
|
|
// #43 (F7-c4): a slice tuple-field carries the 24B header (ptr+len+
|
|
// cap), not the 8B scalar default. Without this arm the for-range
|
|
// destructure over `[N]([]T, U)` strode the tuple at 8 not 24 and
|
|
// read field-2 at the wrong offset (cs=42/ww=8, the cat-A repro).
|
|
// tyslicesize() is the slice-header SSoT (rule-13); cstage reads the
|
|
// same width via tp->type->size (cmd/w6c/cgen.c N_FORRANGE).
|
|
if (k == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; };
|
|
// #53: a tagged-union tuple-field carries its full box (tag + widest
|
|
// payload, slot-padded), NOT the 8B scalar default — a for-range
|
|
// destructure binding sized 8 loaded only the tag word (cs=56/ww=9, the
|
|
// cat-A repro). The box width is variant-dependent, so read it from the
|
|
// checker-stamped tinfo (rule-13): cstage's tp->type->size reads the same
|
|
// resolved width. The stamp already collapsed any alias, so this also
|
|
// covers an N_TNAME field naming a tagged union (paramfieldsize's no-`c`
|
|
// structural contract can't aliaslookup-chase the node). Mirrors the
|
|
// N_TSLICE arm's type-table SSoT.
|
|
let tgi: *syntax.tinfo = t.type_: *syntax.tinfo;
|
|
if (tgi != nil) {
|
|
tgi = tichase(tgi);
|
|
if (tgi != nil && tgi.kind == syntax.tykind.TY_TAGGED) {
|
|
return tgi.size: i32;
|
|
};
|
|
};
|
|
// #43 (F7-c4): a nested tuple field sizes as the sum of its element
|
|
// SLOTS — each element floored UP to one 8B eightbyte (str/slice keep
|
|
// their 24B header), per the tuple-slot ruling and tupeslot's
|
|
// roundup8. Recurse structurally so a tuple-of-tuple lands the same
|
|
// stride cstage's tp->type->size computes.
|
|
if (k == syntax.nkind.N_TTUPLE) {
|
|
let total: i32 = 0;
|
|
let dp: *syntax.node = t.list;
|
|
for (dp != nil) {
|
|
let esz: i32 = paramfieldsize(dp.lhs);
|
|
total = total + (esz + 7) / 8 * 8;
|
|
dp = dp.next;
|
|
};
|
|
return total;
|
|
};
|
|
if (k == syntax.nkind.N_TNAME) {
|
|
let nm: str = t.str;
|
|
if (syntax.streq(nm, "str")) { return primtypesize("str"): i32; };
|
|
// primsize-ok (#101/#109): paramfieldsize is a STRUCTURAL
|
|
// (no-`c`, no-chase) sizer by design — it takes a *node, not a
|
|
// *cgen, so it cannot run aliasprimsize's aliaslookup chase
|
|
// (threading c is the dormant #110). A bare primsize is correct
|
|
// here, not the #101 narrow-alias bug shape.
|
|
let ps: i32 = primsize(nm);
|
|
if (ps > 0) { return ps; };
|
|
};
|
|
// rule-7: N_TARRAY (an array-typed tuple field) is intentionally not
|
|
// sized here — and PROVABLY unreachable, not merely latent (#39).
|
|
// paramfieldsize's only callers are tuple-field contexts (the N_TTUPLE
|
|
// recursion above + the cgforrange destructure sizers below), and the
|
|
// checker loud-REJECTS an array/struct/nested-tuple tuple element at
|
|
// N_TTUPLE resolution (check.ww:2150, "composite element deferred to
|
|
// task #60"; pinned both-stage by test 832_tuple_elem_overlong). So no
|
|
// tuple field can carry an N_TARRAY type — this arm cannot be reached
|
|
// until #60's inline-composite layout lands and lifts that gate. The 8B
|
|
// fall-through is correct-by-vacuity; reopen WITH #60, adding the arm
|
|
// (read t.type_.size, twin of the #53 tagged arm above).
|
|
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: *syntax.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: *syntax.node) void = {
|
|
let slc: *syntax.node = n.lhs;
|
|
let slclocal: *local = nil;
|
|
let slctn: *syntax.node = nil;
|
|
if (slc != nil) {
|
|
if (slc.kind == syntax.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: *syntax.node = nil;
|
|
if (slctn != nil) {
|
|
let sk: syntax.nkind = slctn.kind;
|
|
if (sk == syntax.nkind.N_TSLICE) { elemt = slctn.lhs; };
|
|
if (sk == syntax.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 == syntax.nkind.N_TNAME) {
|
|
if (syntax.streq(slctn.str, "str")) {
|
|
let sti: *syntax.tinfo = slctn.type_: *syntax.tinfo;
|
|
if (sti != nil) {
|
|
if (sti.sub != nil) {
|
|
let u8n: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, slctn.file, slctn.line, slctn.col);
|
|
u8n.str = "u8";
|
|
u8n.type_ = sti.sub: *void;
|
|
elemt = u8n;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
// #60 (alias arc #5): alias-NAMED scrutinee (`let a: arr`, arr =
|
|
// [4]int) — the tnode peek above sees only the N_TNAME leaf:
|
|
// elemt nil, esz 1-sentinel, neither isarr nor isslicestr, so the
|
|
// per-iteration base walked the array words as a POINTER (SEGV).
|
|
// Chase the stamped tinfo (cstage N_FORRANGE u = type_chase_named
|
|
// (slc->type) feeds esz/alen/base classify uniformly) and
|
|
// synthesise the element node off .sub — the FC0 non-ident
|
|
// precedent below.
|
|
let rti60: *syntax.tinfo = nil;
|
|
if (slctn != nil) {
|
|
if (slctn.kind == syntax.nkind.N_TNAME) {
|
|
let st60: *syntax.tinfo = slctn.type_: *syntax.tinfo;
|
|
if (st60 != nil) {
|
|
if (st60.kind == syntax.tykind.TY_NAMED) { rti60 = tichase(st60); };
|
|
};
|
|
};
|
|
};
|
|
if (rti60 != nil && elemt == nil) {
|
|
if (rti60.sub != nil) {
|
|
let en60: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, slctn.file, slctn.line, slctn.col);
|
|
en60.str = rti60.sub.name;
|
|
en60.type_ = rti60.sub: *void;
|
|
elemt = en60;
|
|
};
|
|
};
|
|
// esz: raw elem byte size. For tuple-element slices `[](T0, T1)`,
|
|
// C cgen reads the resolved tuple's size (sum of raw param sizes,
|
|
// no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8.
|
|
// elemsizeof returns 8 for non-primitive elem, which would be
|
|
// wrong here — compute from the tuple param walk instead.
|
|
// C4 (task #7): elemsizeofc, not elemsizeof — a struct element
|
|
// (`[]thread`, 16B) hit elemsizeof's 8-sentinel while cstage reads
|
|
// the stamped slc->type sub size (IMULQ $8 vs $16, gate-blind
|
|
// cs≠ww). elemsizeofc recovers the width from the stamped tinfo
|
|
// (the #8 named-narrow precedent).
|
|
let esz: i32 = elemsizeofc(c, slctn);
|
|
// #60: alias-NAMED scrutinee — stride off the chased stamped
|
|
// element (cstage esz = u->sub->size).
|
|
if (rti60 != nil) {
|
|
let es60: *syntax.tinfo = tichase(rti60.sub);
|
|
if (es60 != nil) { esz = es60.size: i32; };
|
|
};
|
|
if (elemt != nil) {
|
|
if (elemt.kind == syntax.nkind.N_TTUPLE) {
|
|
let total: i32 = 0;
|
|
let p: *syntax.node = elemt.list;
|
|
for (p != nil) {
|
|
total += paramfieldsize(p.lhs);
|
|
p = p.next;
|
|
};
|
|
esz = total;
|
|
};
|
|
};
|
|
// C4 (FC0, task #7): a non-ident scrutinee (`re.charsets`) has no
|
|
// local tnode — slctn is nil, so esz fell to 1 and the binding
|
|
// registered typeless (cstage reads the stamped slc->type: esz 24,
|
|
// slice-header readbacks → cs≠ww). Derive both from the checker-
|
|
// stamped slc.type_ (tinfo SSoT, the #209/#211 discipline); the
|
|
// synthesised N_TNAME carries the element tinfo so cgident's
|
|
// str/slice/float keys read it like a declared local (the str→u8
|
|
// synthesis precedent above).
|
|
if (slctn == nil && slc != nil) {
|
|
let sti2: *syntax.tinfo = slc.type_: *syntax.tinfo;
|
|
sti2 = tichase(sti2);
|
|
if (sti2 != nil) {
|
|
if (sti2.kind == syntax.tykind.TY_SLICE
|
|
|| sti2.kind == syntax.tykind.TY_STR
|
|
|| sti2.kind == syntax.tykind.TY_ARRAY) {
|
|
if (sti2.sub != nil) {
|
|
esz = sti2.sub.size: i32;
|
|
let en: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, slc.file, slc.line, slc.col);
|
|
en.str = sti2.sub.name;
|
|
en.type_ = sti2.sub: *void;
|
|
elemt = en;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
let destruct: bool = (n.list != nil);
|
|
|
|
// .rgi (counter) + .rgl (length) scratch slots. #70: a NON-IDENT
|
|
// slice/str base (field chain, indexed element, call) also needs
|
|
// a .rgb base spill — pre-#70 the init stored cgexpr's AX (the
|
|
// DATA POINTER — a slice-valued cgexpr leaves AX=ptr, BX=len,
|
|
// CX=cap) into .rgl, and the per-iteration code had no non-ident
|
|
// base arm, so the bound-reload BX doubled as the base: i was
|
|
// compared against the POINTER and walked off the end
|
|
// (regex.finish, SEGV on the first non-empty charsets; empty
|
|
// slices coincidentally exited on ptr==0 — latent since fold 1,
|
|
// byte-id both stages). A non-ident ARRAY base is loud (rule 7):
|
|
// its cgexpr shape is not the slice header.
|
|
let iname: str = mkscratchname(c, "rgi");
|
|
let lname: str = mkscratchname(c, "rgl");
|
|
let ioff: i32 = localalloc(c, iname, 8, nil);
|
|
let loff: i32 = localalloc(c, lname, 8, nil);
|
|
let baseoff: i32 = 0;
|
|
if (slc != nil) {
|
|
if (slc.kind != syntax.nkind.N_IDENT) {
|
|
let stu70: *syntax.tinfo = slc.type_: *syntax.tinfo;
|
|
stu70 = tichase(stu70);
|
|
let arr70: bool = false;
|
|
if (stu70 != nil) {
|
|
if (stu70.kind == syntax.tykind.TY_ARRAY) {
|
|
arr70 = true;
|
|
};
|
|
};
|
|
if (arr70) {
|
|
let m70: str = "for-range over a non-ident array base unwired (#70)\n";
|
|
os.write(2, m70.ptr, m70.len: u64);
|
|
os.exit(1);
|
|
};
|
|
// #11: cgexpr on a slice DEREF (*p) does not deliver
|
|
// the AX/BX/CX header convention the spill assumes
|
|
// (the deref-spine load family) — keep it LOUD until
|
|
// #11 wires the deref load.
|
|
if (slc.kind == syntax.nkind.N_UN) {
|
|
if (slc.op == syntax.tkind.TK_STAR) {
|
|
let m11: str = "for-range over a deref base unwired (#11)\n";
|
|
os.write(2, m11.ptr, m11.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
let bname: str = mkscratchname(c, "rgb");
|
|
baseoff = localalloc(c, bname, 8, nil);
|
|
};
|
|
};
|
|
// #121 leg (c): for-range over a module-GLOBAL slice/str/array base
|
|
// SEGV's today — the init + per-iteration base resolution below
|
|
// assume a frame-local slot (localfindnode), so a global let/def base
|
|
// reads saved-BP as the .ptr/.len. LOUD-STOP symmetric with cstage
|
|
// cgen.c (byte-id-neutral; segfault→compile-error is pure
|
|
// improvement). The fix (the N_INDEX isglobal base resolution ported
|
|
// into the for-range spine) is a DISTINCT mechanism — filed as a #121
|
|
// sibling, off fold-6's path.
|
|
if (slc != nil) {
|
|
if (slc.kind == syntax.nkind.N_IDENT) {
|
|
if (localfindnode(c, slc.str) == nil) {
|
|
let isglob: bool = isletvar(c, slc.str);
|
|
if (!isglob) {
|
|
let gdtn: *syntax.node = defvartnode(c, slc.str);
|
|
if (gdtn != nil) {
|
|
if (gdtn.kind == syntax.nkind.N_TARRAY) { isglob = true; };
|
|
};
|
|
};
|
|
if (isglob) {
|
|
let mc: str = "#121: for-range over a module-global slice/array base unwired (global-base resolution gap)\n";
|
|
os.write(2, mc.ptr, mc.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
// Per-binding (up to 8 — matches the C array). Parallel arrays so
|
|
// we don't depend on local-struct cgen.
|
|
let bind_off: [8]i32;
|
|
let bind_sz: [8]i32;
|
|
let bind_foff: [8]i32;
|
|
let bind_signed: [8]bool;
|
|
let nbinds: i32 = 0;
|
|
|
|
if (destruct) {
|
|
let tp: *syntax.node = nil;
|
|
if (elemt != nil) {
|
|
if (elemt.kind == syntax.nkind.N_TTUPLE) { tp = elemt.list; };
|
|
};
|
|
let field_off: i32 = 0;
|
|
let m: *syntax.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: *syntax.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: syntax.nkind = slctn.kind;
|
|
if (tk == syntax.nkind.N_TSLICE) { isslicestr = true; };
|
|
if (tk == syntax.nkind.N_TARRAY) { isarr = true; };
|
|
if (tk == syntax.nkind.N_TNAME) {
|
|
if (syntax.streq(slctn.str, "str")) { isslicestr = true; };
|
|
};
|
|
};
|
|
// #60: alias-NAMED scrutinee — classify off the chased stamped
|
|
// kind (cstage gates on u->kind TY_SLICE/TY_STR vs TY_ARRAY).
|
|
if (rti60 != nil) {
|
|
if (rti60.kind == syntax.tykind.TY_ARRAY) { isarr = true; };
|
|
if (rti60.kind == syntax.tykind.TY_SLICE) { isslicestr = true; };
|
|
if (rti60.kind == syntax.tykind.TY_STR) { isslicestr = true; };
|
|
};
|
|
if (isslicestr) {
|
|
if (slc.kind == syntax.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 == syntax.nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; };
|
|
};
|
|
// #60: alias-NAMED scrutinee has no length tnode — bound off
|
|
// the chased tinfo (cstage aimm(u->alen)).
|
|
if (rti60 != nil) { alen = rti60.alen: i64; };
|
|
emitline("\tMOVQ\t$");
|
|
emitint(alen);
|
|
emitline(", ");
|
|
emitoff(loff: i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
cgexpr(c, slc);
|
|
if (baseoff != 0) {
|
|
// #70: slice/str header from cgexpr is AX=ptr,
|
|
// BX=len, CX=cap — bound is LEN; spill the base ptr
|
|
// for the per-iteration element address.
|
|
emitline("\tMOVQ\tBX, ");
|
|
emitoff(loff: i64);
|
|
emitline("(BP)\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(baseoff: i64);
|
|
emitline("(BP)\n");
|
|
} else {
|
|
// ident with unresolved type — legacy path, unchanged.
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff(loff: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
};};
|
|
|
|
let loopl: str = mklabel(c, "rloop");
|
|
let endl: str = mklabel(c, "rend");
|
|
let naturall: str = endl;
|
|
if (n.els != nil) { naturall = mklabel(c, "relseloop"); };
|
|
// #138 (range form): `continue` must run the implicit `i+=1`
|
|
// post-step before re-testing the bound. Pre-fix cont = loopl
|
|
// (top), skipping the ADDQ $1, ioff below — infinite loop on
|
|
// the value that triggered continue. Dedicated `rpost` label.
|
|
let rpost: str = mklabel(c, "rpost");
|
|
|
|
// #42: bound the push. The buffers are sized exactly LOOP_MAX, so an
|
|
// unguarded push at nesting depth LOOP_MAX+1 is an OOB heap write;
|
|
// fail loud at the cap, both stages (cgen.c twin fatals too).
|
|
if (c.looptop >= LOOP_MAX) {
|
|
let msg: str = "cgen: loop nesting too deep\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
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 == syntax.nkind.N_IDENT) {
|
|
if (slclocal != nil) {
|
|
if (isarr) {
|
|
emitline("\tLEAQ\t");
|
|
emitoff(slclocal.off: i64);
|
|
emitline("(BP), BX\n");
|
|
} else {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(slclocal.off: i64);
|
|
emitline("(BP), BX\n");
|
|
};
|
|
};
|
|
} else {
|
|
// #70: non-ident slice/str base — reload the spilled data
|
|
// pointer (pre-#70 BX held the bound reload).
|
|
emitline("\tMOVQ\t");
|
|
emitoff(baseoff: i64);
|
|
emitline("(BP), BX\n");
|
|
};
|
|
emitline("\tADDQ\tAX, BX\n");
|
|
|
|
// Per-binding load from BX+foff. Signedness comes from bind_signed
|
|
// (set via paramissigned → fieldissignedc), so enum-aliased narrows
|
|
// pick the right MOVS*Q without a literal-name gate.
|
|
// C4 (F5/FC0, task #7): a by-value AGGREGATE element (struct /
|
|
// tuple / str/slice header, esz > 8) copies its FULL extent — the
|
|
// single load word truncated it to 8B, so every field past word 0
|
|
// (str/slice .len/.cap included) read stale slot bytes
|
|
// (regex.finish's 24B charset binding, gate-blind cs≠ww). Same
|
|
// word-run + sized-tail idiom as the cglet aggregate copy.
|
|
if (!destruct && esz > 8) {
|
|
let k: i32 = 0;
|
|
for (k + 8 <= esz) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff(k: i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((bind_off[0] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
if (k + 4 <= esz) {
|
|
emitline("\tMOVL\t");
|
|
emitoff(k: i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((bind_off[0] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
if (k + 2 <= esz) {
|
|
emitline("\tMOVW\t");
|
|
emitoff(k: i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVW\tAX, ");
|
|
emitoff((bind_off[0] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 2;
|
|
};
|
|
if (k + 1 <= esz) {
|
|
emitline("\tMOVB\t");
|
|
emitoff(k: i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((bind_off[0] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
} else {
|
|
let b: i32 = 0;
|
|
for (b < nbinds) {
|
|
// #40 (#263): a str/slice/struct destructure binding
|
|
// (24B header / aggregate, sz>8) copies its FULL extent
|
|
// — the single load word truncated a slice binding to
|
|
// its .ptr, dropping .len/.cap (both stages identically,
|
|
// byte-id-WRONG; F7-c4 fixed only the STRIDE). Same
|
|
// word-run + sized-tail idiom as the non-destructure
|
|
// aggregate copy above.
|
|
if (bind_sz[b] > 8) {
|
|
let k: i32 = 0;
|
|
for (k + 8 <= bind_sz[b]) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((bind_foff[b] + k): i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((bind_off[b] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 8;
|
|
};
|
|
if (k + 4 <= bind_sz[b]) {
|
|
emitline("\tMOVL\t");
|
|
emitoff((bind_foff[b] + k): i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVL\tAX, ");
|
|
emitoff((bind_off[b] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 4;
|
|
};
|
|
if (k + 2 <= bind_sz[b]) {
|
|
emitline("\tMOVW\t");
|
|
emitoff((bind_foff[b] + k): i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVW\tAX, ");
|
|
emitoff((bind_off[b] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 2;
|
|
};
|
|
if (k + 1 <= bind_sz[b]) {
|
|
emitline("\tMOVB\t");
|
|
emitoff((bind_foff[b] + k): i64);
|
|
emitline("(BX), AX\n");
|
|
emitline("\tMOVB\tAX, ");
|
|
emitoff((bind_off[b] + k): i64);
|
|
emitline("(BP)\n");
|
|
k += 1;
|
|
};
|
|
b += 1;
|
|
continue;
|
|
};
|
|
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: *syntax.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: *syntax.node = nil;
|
|
|
|
let cs: *syntax.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: *syntax.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: *syntax.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: *syntax.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;
|
|
};
|
|
|
|
|