slot = roundup8(size(elem)) — 8B is a FLOOR, not a ceiling (user- ratified 2026-06-04; the #237 fieldslotsize precedent; (str,str)=48B predates this — tagged was the one truncated >8B kind). Pre-fix the checkers truncated a tagged element to one 8B slot and every cgen transport walk strode wide=(STR||SLICE)-else-8: cstage read the NEIGHBOR slot, wwstage read ZEROS — both-wrong-differently, so the byte-id gates were blind (prober-9 PG1, /tmp/p9). One stride accessor per stage — tuple_eslot (cgen.c) / tupeslot (cgenstmt.ww) — now feeds every tuple walk; the per-site predicates are deleted as absorbed. Sizer: check.c N_TTUPLE + check.ww tupleelemslot gain the TY_TAGGED arm (astsize already rides the type table since commit 0). Routes flipped to the accessor, both stages symmetric: cursor producers (lit/slot-to-cursor; tagged ident elements load their box from the slot — cgexpr's tagged ident load is word0-only), let-receive (tuple_store/tupstore generalized to eslot/8 words), N_RETURN send, by-value param receive, arg restage/drain (tagged stays loud per C-t2), destructure (MLET/MASSIGN, ident + sret + in-cap), t.N read + len(t.N) + global-g.N offset walks (t.N gains the tagged box load: AX=tag, DX/CX/R8=payload — the is/as spill cursor), sret classifier, DATA emit. wwstage cgtagvariantidx gains the #67 stamped-carrier arm (flatvariantidxt on .type_) — its AST-only key silently clamped 't.0 is size' to tag 0; fused here because the tuple-element read this commit wires is its only exercisable consumer. Exit invariant: zero silent tagged-tuple paths — in-cap shapes (<=4 GP eightbytes) are correct end-to-end; everything else is LOUD: over-cap sret return (#22b, task #28), call-arg (C-t2 #32), non-local literal element sources (#22b/#23), tuple-in-union payload (#242/#22b), global static-init, element write (pre-existing). Closure proof-grep at HEAD: 'tuple_ebytes|tupebytes' -> 0 hits; 'wide.*=.*(TY_SLICE|TY_STR)' tuple-walk survivors are all behind tagged loud-guards (cgen.c:2535/2568/12013 widen-store + over-cap send; cgenutil.ww:3527 twin). Latent cross-checks closed by the accessor: wwstage MLET-sret strode esz (4 for i32) vs cstage 8; wwstage param-receive strode slotsize (composite slotsize) vs cstage 8; both now the accessor's floor-8. Tagged inits in pins use the CAST form (5: size) — the bare untyped-int widen-store mis-tag is pre-existing at master and filed (task #33). 941 gains 13 rows: t22_* size/align folds (+ void-elem 0-slot pin), the full runtime round-trip (read, is/as, both element orders, void variant, destructure, literal-let), two-tagged-elem in-cap and float+tagged in BOTH orders (ken k1/k2 regression-pinned as rows), t.N-as-call-arg, 5 loud pins (arg, over-cap return, call-source element, global init, element write), and the sret_narrow_mix_* triple below. Runtime rows exit-checked under both drivers + byte-id. 129/129; unit tests green; sizelint clean. ken R1 (re-validation fix): an OVER-CAP tuple init whose rhs is not a CALL fell past every cstage N_LET store arm to NOTHING — silent uninitialized-frame reads — while wwstage loud-rejected the same shape. Pre-existing for (str,str) literals; the #22a tagged slots routed >16B-box tagged shapes into it (k5b/k5c/k5d, base-correct because base truncated them in-cap). cstage now routes the rhs through cgexpr (the cursor materialisers carry the exact wwstage loud texts) with a trailing fatal as the net; 941 pins both the tagged and the (str,str) spellings of the hole. reviewer-22 (review fixes, folded): the MLET-receive slot flip above landed ALONE on the wwstage over-cap sret family — the ww RETURN-send and MASSIGN-receive still strode packed esz (4 for u32), so `let (a,b,c) = f()` over (str,u32,str) read c at slot offset 32 while ww's send wrote it packed at 28: a ww runtime REGRESSION vs base (base was packed-consistent on both sides — ran right, byte-diff). Both walks now stride the slot (send mirrors cstage's `wide ? esz : 8`; MASSIGN strides tupeslotn) — closing, in the same stroke, the pre-existing base skews: ww `let t = f()` over-cap narrow-mix read (reader slot-laid vs send packed, runtime-wrong at base) and the cs≠ww asm on all three routings. Second find: t.N tagged element as a CALL ARG — cstage rides its generic node_istaggedarg cursor push, but wwstage's kind-gated aistagged missed N_DOT and mis-routed the box into the widening branch (taggedvariantindex -1 clamped to 0: callee read variant 0, silent, ww-only). cgenutil.ww gains the N_DOT arm (aistagged + pushargsrev), the #67 stamped-carrier twin of the N_INDEX (#12) arm. The N_DOT arm was a LIVE wwstage stdlib miscompile: hextest's import graph carried two t.N tagged-arg sites (base .s diff = exactly the clamped-tag PUSHQ pair) — the 989 lib-byteid ratchet caught the convergence and #59.4 graduates to M_ID (runtime-correct per 979_hex_run). Probes: /tmp/r22. Residual filed: tagged arg from deref/cast/unwrap sources is a word0-only read on BOTH stages (pre-existing, base-confirmed) — generalizing wwstage's kind gate to cstage's type-keyed check rides that task.
637 lines
19 KiB
Plaintext
637 lines
19 KiB
Plaintext
// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
|
|
//
|
|
// Houses the top-level emission glue:
|
|
// - cgfnparams: parameter spilling per SysV
|
|
// - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue
|
|
// deferred via cgen.ww's cgoutstate so the frame size
|
|
// reflects every emit-time localadd (#15/#26c)
|
|
// - cgfile: file-level entry (the exported driver)
|
|
//
|
|
// Bundler pulls this in transitively via cgen.ww; consumers don't
|
|
// need to `use cgendecl;` directly.
|
|
|
|
package wcc;
|
|
|
|
import os;
|
|
import ast;
|
|
import tok;
|
|
import typ;
|
|
import sym;
|
|
import strconv;
|
|
|
|
|
|
// ---- function-level cgen ---------------------------------------------
|
|
|
|
fn cgfnparams(c: *cgen, params: *node) void = {
|
|
let p: *node = params;
|
|
// sret (#23): RDI is consumed by the hidden dest pointer
|
|
// (already spilled to @sretarg by cgfn); the first user param
|
|
// lands in SI.
|
|
let idx: i32 = 0;
|
|
if (localfind(c, "@sretarg") != 0) { idx = 1; };
|
|
let fidx: i32 = 0;
|
|
// Cursor for args that overflow the SysV reg windows. Each
|
|
// stack-passed arg lives at 16+8*k(BP) — no spill, the local
|
|
// is registered with a *positive* offset pointing into the
|
|
// caller's frame. Mirrors C cgen's cg_stack_arg_cursor.
|
|
let stkcursor: i32 = 0;
|
|
// #38b: words consumed by MEMORY-class (>48B tagged) params —
|
|
// post-walk consistency check against stkcursor.
|
|
let memwords: i32 = 0;
|
|
for (p != nil) {
|
|
if (p.kind == nkind.N_PARAM) {
|
|
let nm: str = p.str;
|
|
// Hare-style variadic `T...`: callee receives a []T
|
|
// slice (3 register words / 24B). p.lhs is already
|
|
// the []T wrap installed by check.ww installparams
|
|
// (mirrors cstage check.c:455 tp->type promotion), so
|
|
// we consume it directly — re-wrapping via slicewrap
|
|
// would yield [][]T.
|
|
if (p.op == tkind.TK_ELLIPSIS) {
|
|
let tn: *node = p.lhs;
|
|
if (idx + 3 <= 6) {
|
|
let off: i32 = localadd(c, nm, tyslicesize(): i32, tn);
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
} else { if (idx < 6) {
|
|
// Partial-fit stitch — variadic `T...` is a slice
|
|
// at the ABI boundary (the call site synthesises a
|
|
// 24B descriptor and pushes ptr/len/cap), so this
|
|
// mirrors the slice branch at cgendecl.ww:518.
|
|
let off: i32 = localadd(c, nm, tyslicesize(): i32, tn);
|
|
let regs_left: i32 = 6 - idx;
|
|
let w: i32 = 0;
|
|
for (w < regs_left) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
for (w < 3) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((16 + stkcursor*8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
stkcursor += 1;
|
|
w += 1;
|
|
};
|
|
} else {
|
|
localaddstack(c, nm, tn, 16 + stkcursor*8);
|
|
stkcursor += 3;
|
|
};};
|
|
p = p.next;
|
|
continue;
|
|
};
|
|
if (p.lhs != nil) { if (p.lhs.kind == nkind.N_TTUPLE) {
|
|
// #163: tuple PARAM receive (param twin of #164's
|
|
// return). Walk the tuple's elements over the SysV
|
|
// arg cursor — a float reads its XMM (X0..X7),
|
|
// everything else an INTEGER arg reg (DI/SI/..); a
|
|
// slice/str its 3-word {ptr,len,cap} — storing each
|
|
// into the param slot positionally (eoff steps by
|
|
// slotsize, matching the t.0/t.1 field-access walk +
|
|
// the SEND). Reg overflow loud-stops (rule 7); the
|
|
// partial-spill stitch is out of scope (twin of #164).
|
|
let off: i32 = localadd(c, nm, slotsize(c, p.lhs), p.lhs);
|
|
let eoff: i32 = 0;
|
|
let te: *node = p.lhs.list;
|
|
for (te != nil) {
|
|
let et: *node = te.lhs;
|
|
if (isfloattype(c, et)) {
|
|
if (fidx >= 8) {
|
|
let msg: str = "tuple param float element overflows SSE arg regs (X0..X7); stitch out of scope, see #163\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let mov: str = "MOVSD";
|
|
if (isf32type(c, et)) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\t");
|
|
emitline(fargregname(fidx));
|
|
emitline(", ");
|
|
emitoff((off + eoff): i64);
|
|
emitline("(BP)\n");
|
|
fidx += 1;
|
|
} else {
|
|
let eb: i32 = tupeslotn(et) / 8;
|
|
if (idx + eb > 6) {
|
|
let msg: str = "tuple param element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
let k: i32 = 0;
|
|
for (k < eb) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + eoff + k*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
k += 1;
|
|
};
|
|
};
|
|
eoff += tupeslotn(et);
|
|
te = te.next;
|
|
};
|
|
p = p.next;
|
|
continue;
|
|
}; };
|
|
if (isfloattype(c, p.lhs)) {
|
|
// Float param: SysV uses the XMM stream
|
|
// (X0..X7). 8B (f64) or 4B (f32) slot.
|
|
let fsz: i32 = 8;
|
|
if (isf32type(c, p.lhs)) { fsz = 4; };
|
|
if (fidx < 8) {
|
|
let off: i32 = localadd(c, nm, fsz, p.lhs);
|
|
let mov: str = "MOVSD";
|
|
if (fsz == 4) { mov = "MOVSS"; };
|
|
emitline("\t");
|
|
emitline(mov);
|
|
emitline("\t");
|
|
emitline(fargregname(fidx));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
fidx += 1;
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += 1;
|
|
};
|
|
p = p.next;
|
|
continue;
|
|
};
|
|
let sfc: i32 = structfloatclass(c, p.lhs);
|
|
if (sfc != 0) {
|
|
// #165: float-bearing struct PARAM receive (param
|
|
// twin of #163's tuple). Classify each SysV
|
|
// eightbyte; a lone-f64 eightbyte reads its XMM
|
|
// (X0..X7), a pure-INT eightbyte its INTEGER arg reg
|
|
// (DI/SI/..), stored into the param slot at the
|
|
// 8-byte eightbyte stride. Gated to qualifying
|
|
// structs by structfloatclass — all-int + f32-packed
|
|
// fall through to the GP struct arm below (byte-id /
|
|
// #165b). Reg overflow loud-stops (rule 7).
|
|
let off: i32 = localadd(c, nm, structparamsize(c, p.lhs), p.lhs);
|
|
let nb: i32 = sfc & 15;
|
|
let e: i32 = 0;
|
|
for (e < nb) {
|
|
let issse: bool = (sfc & (16 << e)) != 0;
|
|
if (issse) {
|
|
if (fidx >= 8) {
|
|
let msg: str = "float struct param eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
emitline("\tMOVSD\t");
|
|
emitline(fargregname(fidx));
|
|
emitline(", ");
|
|
emitoff((off + e*8): i64);
|
|
emitline("(BP)\n");
|
|
fidx += 1;
|
|
} else {
|
|
if (idx >= 6) {
|
|
let msg: str = "float struct param eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + e*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
};
|
|
e += 1;
|
|
};
|
|
p = p.next;
|
|
continue;
|
|
};
|
|
if (istaggedtype(c, p.lhs)) {
|
|
let slot: i32 = slotsize(c, p.lhs);
|
|
let nw: i32 = slot / 8;
|
|
// #38b: MEMORY-class (>48B tagged) param — the
|
|
// caller staged the whole slot below the return
|
|
// address; read it in place at positive BP
|
|
// offsets. No spill, no frame growth, zero
|
|
// prologue bytes. Pre-fix this fell into the
|
|
// greedy stitch arm below while cstage received
|
|
// one scalar word (cs≠ww, silent).
|
|
// ref/qbe/amd64/sysv.c:80-85 / :411-426.
|
|
if (taggedmemargsize(p.lhs.type_: *tinfo) > 0) {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += nw;
|
|
memwords += nw;
|
|
} else { if (idx + nw <= 6) {
|
|
let off: i32 = localadd(c, nm, slot, p.lhs);
|
|
let w: i32 = 0;
|
|
for (w < nw) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
} else { if (idx < 6 && nw > 1) {
|
|
// Partial fit: fill remaining regs, then read
|
|
// the tail from positive BP offsets. Mirrors
|
|
// the caller's greedy reg fill in pushargsrev.
|
|
let off: i32 = localadd(c, nm, slot, p.lhs);
|
|
let regs_left: i32 = 6 - idx;
|
|
let w: i32 = 0;
|
|
for (w < regs_left) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
for (w < nw) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((16 + stkcursor*8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
stkcursor += 1;
|
|
w += 1;
|
|
};
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += nw;
|
|
};};};
|
|
} else { if (isslicetype(c, p.lhs)) {
|
|
if (idx + 3 <= 6) {
|
|
let off: i32 = localadd(c, nm, tyslicesize(): i32, p.lhs);
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
} else { if (idx < 6) {
|
|
// Partial-fit stitch — mirrors tagged at lines
|
|
// 440-469. Caller's pushargsrev greedy-fills the
|
|
// remaining argregs (ptr,len,cap order), the tail
|
|
// spills to +16+stkcursor*8(BP).
|
|
let off: i32 = localadd(c, nm, tyslicesize(): i32, p.lhs);
|
|
let regs_left: i32 = 6 - idx;
|
|
let w: i32 = 0;
|
|
for (w < regs_left) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
for (w < 3) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((16 + stkcursor*8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
stkcursor += 1;
|
|
w += 1;
|
|
};
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += 3;
|
|
};};
|
|
} else { if (isstrtype(c, p.lhs)) {
|
|
if (idx + 3 <= 6) {
|
|
// str IS []u8: 3-word param (ptr,len,cap), same as
|
|
// the slice arm above (#1/Phase 3). #60: route slot
|
|
// width through the primtypesize SSoT so #1's ty_str
|
|
// bump propagates here.
|
|
let off: i32 = localadd(c, nm, primtypesize("str"): i32, p.lhs);
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + 8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + 16): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
} else { if (idx < 6) {
|
|
// Partial-fit stitch — mirrors the slice arm above.
|
|
// #60: same SSoT routing as the regs-fit arm above.
|
|
let off: i32 = localadd(c, nm, primtypesize("str"): i32, p.lhs);
|
|
let regs_left: i32 = 6 - idx;
|
|
let w: i32 = 0;
|
|
for (w < regs_left) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
for (w < 3) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((16 + stkcursor*8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
stkcursor += 1;
|
|
w += 1;
|
|
};
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += 3;
|
|
};};
|
|
} else { let stsz: i32 = structparamsize(c, p.lhs);
|
|
if (stsz > 0) {
|
|
// User-defined by-value struct ≤ 16B: 1 or 2
|
|
// integer eightbytes. Mirrors cstage's
|
|
// `struct_eb = (pu->size > 8) ? 2 : 1` and the
|
|
// matching reg/stack/stitch arms in cgen.c cgfn.
|
|
let nw: i32 = 1;
|
|
if (stsz > 8) { nw = 2; };
|
|
if (idx + nw <= 6) {
|
|
let off: i32 = localadd(c, nm, stsz, p.lhs);
|
|
let w: i32 = 0;
|
|
for (w < nw) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
} else { if (idx < 6 && nw > 1) {
|
|
let off: i32 = localadd(c, nm, stsz, p.lhs);
|
|
let regs_left: i32 = 6 - idx;
|
|
let w: i32 = 0;
|
|
for (w < regs_left) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
for (w < nw) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((16 + stkcursor*8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
stkcursor += 1;
|
|
w += 1;
|
|
};
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += nw;
|
|
};};
|
|
} else { let aggsz2: i32 = aggargsizetn(p.lhs.type_: *tinfo);
|
|
if (aggsz2 > 0) {
|
|
// #271: array / >16B-struct by-value param —
|
|
// received as ceil(sz/8) GP eightbytes, the
|
|
// callee twin of the generalised aggregate-arg
|
|
// push. Mirror of the cstage is_bigagg arm
|
|
// (regs-fit / partial-stitch / stack-spill).
|
|
let nw2: i32 = (aggsz2 + 7) / 8;
|
|
if (idx + nw2 <= 6) {
|
|
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
|
|
let w: i32 = 0;
|
|
for (w < nw2) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
} else { if (idx < 6) {
|
|
let off: i32 = localadd(c, nm, aggsz2, p.lhs);
|
|
let regs_left: i32 = 6 - idx;
|
|
let w: i32 = 0;
|
|
for (w < regs_left) {
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
w += 1;
|
|
};
|
|
for (w < nw2) {
|
|
emitline("\tMOVQ\t");
|
|
emitoff((16 + stkcursor*8): i64);
|
|
emitline("(BP), AX\n");
|
|
emitline("\tMOVQ\tAX, ");
|
|
emitoff((off + w*8): i64);
|
|
emitline("(BP)\n");
|
|
stkcursor += 1;
|
|
w += 1;
|
|
};
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += nw2;
|
|
};};
|
|
} else {
|
|
if (idx < 6) {
|
|
let off: i32 = localadd(c, nm, 8, p.lhs);
|
|
emitline("\tMOVQ\t");
|
|
emitline(argregname(idx));
|
|
emitline(", ");
|
|
emitoff(off: i64);
|
|
emitline("(BP)\n");
|
|
idx += 1;
|
|
} else {
|
|
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
|
|
stkcursor += 1;
|
|
};
|
|
};
|
|
};
|
|
};};};
|
|
};
|
|
p = p.next;
|
|
};
|
|
// #38b: a MEMORY-class tagged param cannot coexist with stack-
|
|
// spilled register-class params — both walk the same positive-BP
|
|
// cursor in declaration order while the caller's residual region
|
|
// puts spill words below every mem copy. Any non-mem cursor use
|
|
// leaves stkcursor past the mem words. Mirror of the cgcall
|
|
// caller-side check; loud-stop (rule 7).
|
|
if (memwords > 0 && stkcursor != memwords) {
|
|
let mp: str = "#38b: >48B tagged param mixed with stack-spilled params unwired\n";
|
|
os.write(2, mp.ptr, mp.len: u64);
|
|
os.exit(1);
|
|
};
|
|
};
|
|
|
|
fn cgfn(c: *cgen, fn_: *node) void = {
|
|
cgeninit(c);
|
|
c.fnname = fn_.str;
|
|
c.curmod = fn_.nmod;
|
|
c.fnret = fn_.lhs;
|
|
|
|
// sret callee (#23): return type is plain TY_STRUCT > 24B.
|
|
// Reserve 8B for @sretarg (holds the saved hidden RDI dest
|
|
// pointer); cgfnparams skips DI for user args, cgreturn writes
|
|
// through *(@sretarg) and returns @sretarg in RAX.
|
|
let sret_callee: bool = sretretsize(c, c.fnret) > 0;
|
|
|
|
// Capture the body into cgoutstate while c.frame grows under
|
|
// emit-time localadd calls (#15/#26c — wwstage dropped its
|
|
// scanlocals pre-pass to align DOWN with cstage's first-use
|
|
// pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits
|
|
// after the body finishes so the frame size reflects every
|
|
// localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds
|
|
// `subsp`/`text` Progs up front and patches their `from.offset`
|
|
// at the end via txt_emit.
|
|
cgout_enable();
|
|
|
|
if (sret_callee) {
|
|
let saoff: i32 = localadd(c, "@sretarg", 8, nil);
|
|
emitline("\tMOVQ\tDI, ");
|
|
emitoff(saoff: i64);
|
|
emitline("(BP)\n");
|
|
};
|
|
|
|
cgfnparams(c, fn_.list);
|
|
c.lastwasreturn = 0;
|
|
// Iterate the fn body's statements directly rather than dispatching
|
|
// the outermost N_BLOCK through cgstmt — cgblock now save/restores
|
|
// c.locals to scope inner shadows (post-#27), but the function body
|
|
// is not "an inner block": defers (queued during the body) and the
|
|
// implicit-return epilogue both call cgexpr after this loop and
|
|
// resolve identifiers via localfind, so the body's locals must
|
|
// still be in c.locals when we get there.
|
|
if (fn_.body != nil) {
|
|
if (fn_.body.kind == nkind.N_BLOCK) {
|
|
let s: *node = fn_.body.list;
|
|
for (s != nil) {
|
|
cgstmt(c, s);
|
|
s = s.next;
|
|
};
|
|
} else {
|
|
cgstmt(c, fn_.body);
|
|
};
|
|
};
|
|
|
|
if (c.lastwasreturn == 0) {
|
|
// Run any registered defers in LIFO order before the
|
|
// implicit return.
|
|
rundefers(c);
|
|
// Zero AX before the fall-through return — matches cstage,
|
|
// which always emits this so void-returning fns don't leak
|
|
// a stale callee value to their caller.
|
|
emitline("\tMOVQ\t$0, AX\n");
|
|
emitline("\tMOVQ\tBP, SP\n");
|
|
emitline("\tPOPQ\tBP\n");
|
|
emitline("\tRET\n");
|
|
};
|
|
|
|
cgout_disable();
|
|
|
|
let frame: i32 = c.frame;
|
|
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
|
|
|
// Emit the TEXT label via emitfnname so the def site picks up the
|
|
// same skip rule (FFI / `main` / empty-module) and the same module
|
|
// hint (this fn's own module) that the call sites use.
|
|
emitline("TEXT ");
|
|
emitfnname(c, fn_.str, fn_.nmod);
|
|
emitline(",$");
|
|
emitint(frame: i64);
|
|
emitline("\n");
|
|
|
|
emitline("\tPUSHQ\tBP\n");
|
|
emitline("\tMOVQ\tSP, BP\n");
|
|
emitline("\tSUBQ\t$");
|
|
emitint(frame: i64);
|
|
emitline(", SP\n");
|
|
|
|
cgout_flush();
|
|
};
|
|
|
|
// ---- file-level entry ------------------------------------------------
|
|
|
|
export fn cgfile(c: *cgen, file: *node) void = {
|
|
if (file == nil) { return; };
|
|
c.strlits = nil;
|
|
c.strlitseq = 0;
|
|
collectaliases(c, file);
|
|
// Enums must register before structs — fieldsize on a tkind-typed
|
|
// field needs the enum's storage size, otherwise it falls back to
|
|
// 8 (wrong load width).
|
|
collectenums(c, file);
|
|
collectstructs(c, file);
|
|
collectdefs(c, file);
|
|
collectfnrets(c, file);
|
|
fficollect(c, file);
|
|
collectmods(c, file);
|
|
collectlets(c, file);
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
if (d.body != nil) {
|
|
cgfn(c, d);
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
letpreintern(c, file);
|
|
emitdatasection(c);
|
|
emitdefconstants(c, file);
|
|
emitletdataw(c, file);
|
|
};
|