Files
ww/selfhost/cmd/wcc/cgendecl.ww
Hojun-Cho 1292f98c91 cstage+selfhost+test: scope-correct localoff via block save/restore (#27)
localoff (cstage) / localadd (wwstage) deduped stack slots by name
alone, ignoring scope. Outer `let a: [128]u8` and an inner-block
`let a: *u8` shared one 8B slot; prologue truncated to inner size
and outer-scope writes past saved RIP corrupted the frame. Worker-19
hit it during #19 (selfhost/cmd/w6a/main.ww carries a defensive
asm→s rename pointing at this task).

Drop the name-dedup. Each let allocates fresh. Then preserve
outer-scope visibility across inner blocks: cgstmt's N_BLOCK case
saves `*locals` head, walks body, restores. cgfn iterates fn->body
->list directly (bypassing the outermost N_BLOCK) so defers and the
implicit-return epilogue still see fn-body locals after the loop.

Wwstage symmetric: localadd keeps dedup only for `@`-prefixed
synthetic scratches (`@tagscr` / `@retscr` / `@tagbase`) which need
single-slot semantics; user names get fresh stubs. scanlocals always
counts + always appends a fresh stub for N_LET / N_MLET / N_FORRANGE
so prologue SUBQ stays in sync with emit-time offsets. cgblock and
cgfn mirror cstage.

ww2 == ww3 == ww4 byte-identical post-fix.

Test 709 (localoff_scope): 8 rows × 2 drivers = 16 fixtures —
inner_first_outer_bigger, outer_first_inner_writes, nested_3_deep,
same_name_diff_type, same_block_redecl_pin, defer_shadow,
forrange_body_shadow, if_body_shadow. defer_shadow pins the cgfn
body-bypass; if_body_shadow pins the save/restore independently.
Asm byte-id not diffed in 709 — 995_self_rebuild covers cross-stage
drift more broadly.

Follow-ups (filed): #32 (check: refuse same-block let-redecl), w6a
`s`→`asm` revert sibling commit.
2026-05-16 09:38:30 +09:00

888 lines
26 KiB
Plaintext

// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
//
// Houses the top-level emission glue:
// - scanlocals: frame pre-scan that counts each local `let`
// - cgfnparams: parameter spilling per SysV
// - cgfn: fn prologue + body + epilogue
// - cgfile: file-level entry (the exported driver)
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgendecl;` directly.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
//
// Recursively walks the body to count every local `let`. Each gets a
// slot sized by slotsize(typ); 8-byte default. Match-bindings + for-
// init lets count too. Params are added by the cgfn driver.
fn scanlocals(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let total: i32 = 0;
if (n.kind == nkind.N_LET) {
// Match localadd's rounding: < 8 bumps to 8, then 8-align.
// scanlocals must agree with localadd or the prologue
// SUBQ undersizes the frame and lets overflow into the
// caller's stack — corrupting whatever's at -frameSize..-1
// of the caller. Post-#27 every let allocates fresh (no
// name dedup), so we always count + always append a stub.
// The stub carries n.lhs as tnode so later scanlocals
// nodes can dispatch on type — e.g. detecting `arr[i] = ...`
// where arr is a tagged-element array (needs @tagscr).
// localfindnode walks head-first, so the freshest stub
// (innermost binding) wins lookup.
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = n.str;
stub.off = 0;
stub.tnode = n.lhs;
stub.lnext = c.locals;
c.locals = stub;
};
// Multi-let from a tuple-returning call: each binding's size
// comes from its annotated type (l.lhs) when present, else from
// the rhs call's return-tuple element type. Marking via
// scanseenmark also dedupes the recursive descent into n.list
// so each child isn't counted again at the default 8B.
if (n.kind == nkind.N_MLET) {
let p0t: *node = nil;
let p1t: *node = nil;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == nkind.N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
};
let l: *node = n.list;
let pt: *node = p0t;
let bidx: i32 = 0;
for (l != nil) {
let t: *node = l.lhs;
if (t == nil) {
if (bidx == 0) { t = p0t; };
if (bidx == 1) { t = p1t; };
};
let sz: i32 = 8;
if (t != nil) { sz = slotsize(c, t); };
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
// Always-fresh stub (post-#27); tnode carries the
// binding's type so later array-index dispatch can
// resolve the let through localfindnode.
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = l.str;
stub.off = 0;
stub.tnode = t;
stub.lnext = c.locals;
c.locals = stub;
l = l.next;
bidx += 1;
};
};
// `switch` allocates an 8B scratch slot for the scrutinee so case
// bodies can spill through SP without losing it. The slot is named
// ".sw_<labelseq>" at cgen time — unique per switch — so it must
// not dedup. Count it here so the frame SUBQ matches.
if (n.kind == nkind.N_SWITCH) { total += 8; };
// `for (let x .. s)` allocates two 8B scratch slots — `.rgi_<seq>`
// (counter) and `.rgl_<seq>` (length) — plus one slot per binding.
// Per-binding sz defaults to 8 (covers scalar primitives + ptrs).
// `str` tuple-fields would need 16 — selfhost doesn't yet emit
// those, so the simple count tracks C cgen for current fixtures.
if (n.kind == nkind.N_FORRANGE) {
total += 16; // .rgi + .rgl scratch
// Each forrange binding gets a fresh 8B slot (post-#27).
// Stub is also appended so the body's references resolve
// to this binding via head-first localfindnode lookup.
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
let bnm: str = m.str;
total += 8;
if (bnm.len > 0) {
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = bnm;
stub.off = 0;
stub.tnode = m.lhs;
stub.lnext = c.locals;
c.locals = stub;
};
m = m.next;
};
} else {
let bnm: str = n.str;
total += 8;
if (bnm.len > 0) {
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = bnm;
stub.off = 0;
stub.tnode = nil;
stub.lnext = c.locals;
c.locals = stub;
};
};
};
// `match (non-ident)` needs a 24B `@match_spill` scratch slot for
// cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's
// localoff("@match_spill", ...). N_IDENT scrutinees read the slot
// directly off the local — no spill needed.
if (n.kind == nkind.N_MATCH) {
let sc: *node = n.lhs;
if (sc != nil) {
if (sc.kind != nkind.N_IDENT) { total += 24; };
};
};
// Match-arm binding (`case let v: T => ...`) gets a slot too.
// Crucially we do NOT dedup these against c.locals: C cgen
// handles a match as an expression with a by-value locals copy,
// so two separate matches in the same function each allocate
// their `v`/`e` slots fresh. Treating these as deduped would
// shrink the frame below what localadd then bumps it to.
if (n.kind == nkind.N_MCASE) {
let bn: str = n.str;
if (bn.len > 0) {
let pat: *node = n.lhs;
if (pat != nil) {
// Must mirror cgmatch's bind-slot sizing in
// cgenexpr.ww (`bsz = slotsize(c, pat)`):
// hardcoding str/slice/8 here underbooked the
// frame for TY_STRUCT variants — the emit-time
// localalloc(bsz=24) then wrote past the SUBQ'd
// SP, smashing whatever the OS put under it
// (project #31).
let psz: i32 = slotsize(c, pat);
if (psz <= 0) { psz = 8; };
if ((psz & 7) != 0) { psz = (psz + 7) & ~7; };
total += psz;
};
};
// Match arms get a fresh local scope at emission time
// (cgmatch saves c.locals before each arm and restores
// after). scanlocals must mirror that: walk the arm
// body with a saved/restored seenmark set so two arms
// declaring the same name each get their own slot,
// matching the per-arm frame growth the emit phase
// produces.
if (n.body != nil) {
let saved: *local = c.locals;
total += scanlocals(c, n.body);
c.locals = saved;
};
return total;
};
// Tagged-arr/slice index store needs a 24B scratch slot
// (`@tagscr`) for cgwidentaggedstore to materialise the source
// in before copying to the element address. Reserved once per
// function (dedup'd via scanseenmark) regardless of how many
// tagged-arr stores the body contains.
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
if (alhs.kind == nkind.N_INDEX) {
let abase: *node = alhs.lhs;
if (abase != nil) {
if (abase.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, abase.str);
let btn: *node = nil;
if (lc != nil) { btn = lc.tnode; }
else { btn = letvartnode(c, abase.str); };
if (btn != nil) {
let bk: nkind = btn.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = btn.lhs; };
if (bk == nkind.N_TSLICE) { etn = btn.lhs; };
if (bk == nkind.N_TPTR) { etn = btn.lhs; };
if (etn != nil) {
if (istaggedtype(c, etn)) {
if (!scanseenmark(c, "@tagscr")) {
total += 24;
};
};
};
};
};
};
};
};
};
// Tagged-union struct-field write: `s.f = v` or `(*p).f = v`
// where f is a tagged-union field. cgassign delegates to
// cgwidentaggedstore; for pointer-rooted dst the wrapper
// allocates @tagbase (8B) and @tagscr (slot_sz). Both names
// dedup with other tagged scratch users in the same function.
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
if (alhs.kind == nkind.N_DOT) {
let abase: *node = alhs.lhs;
if (abase != nil) {
if (abase.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, abase.str);
let btn: *node = nil;
if (lc != nil) { btn = lc.tnode; }
else { btn = letvartnode(c, abase.str); };
let isptr: bool = false;
let stype: *node = nil;
if (btn != nil) {
if (btn.kind == nkind.N_TPTR) {
isptr = true;
stype = btn.lhs;
};
if (btn.kind == nkind.N_TNAME) { stype = btn; };
};
if (stype != nil) {
if (stype.kind == nkind.N_TNAME) {
let si: *structinfo = structlookup(c, stype.str);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, alhs.str)) {
if (istaggedtype(c, fi.tnode)) {
if (isptr) {
if (!scanseenmark(c, "@tagbase")) {
total += 8;
};
if (!scanseenmark(c, "@tagscr")) {
total += 24;
};
};
};
fi = nil;
} else {
fi = fi.finext;
};
};
};
};
};
};
};
};
};
};
// Tagged-union return with struct payload or tagged-subset
// source — cgreturn materialises in @tagscr then loads
// AX/DX/CX. Detect via the same rhsstructpayload predicate
// the cgen uses, so we only reserve when the cgen will
// actually emit a scratch-using path. `!void` / `!i32`
// aliases share N_STRUCTLIT shape but resolve to
// non-struct types — they fall through to scalar/str and
// don't need scratch.
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (istaggedtype(c, c.fnret)) {
if (!isnullabletype(c.fnret)) {
let rhs: *node = n.lhs;
let needs: bool = false;
if (rhs != nil) {
let sn: str = rhsstructpayload(c, rhs);
if (sn.len > 0) { needs = true; };
if (rhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
needs = true;
};
};
};
};
if (needs) {
if (!scanseenmark(c, "@tagscr")) {
total += 24;
};
};
};
};
};
};
// Whole-struct return for sizes <= 24B uses @retscr — when
// the function's return type is a registered TY_STRUCT of
// size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT,
// cgreturn materialises in @retscr then loads AX/DX/CX.
// Mirrors cstage cgen.c which allocates the scratch slot
// inline; here we must pre-reserve so the prologue SUBQ
// reserves enough frame.
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
let rname: str = c.fnret.str;
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
if (rsi.totsize <= 24) {
let rhs: *node = n.lhs;
let okrhs: bool = false;
if (rhs != nil) {
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
};
if (okrhs) {
if (!scanseenmark(c, "@retscr")) {
total += 24;
};
};
};
};
};
};
};
// Call-site struct-payload widening uses @tagscr — when the
// arg is a struct literal/ident and the callee's param is
// tagged, pushargsrev materialises in scratch and pushes.
// Scalar / str args take the direct-push fast path (no
// scratch). Tagged-typed ident args also skip widening (the
// slot is already laid out, so pushargsrev pushes slot words
// directly). Both fast paths agree with C cgen bytewise, so
// only struct-payload sites get a scratch reservation.
if (n.kind == nkind.N_CALL) {
let callee: *node = n.lhs;
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
};
if (cnm.len > 0) {
let ps: *node = fnparamslookup(c, cnm);
let a: *node = n.list;
for (a != nil) {
if (ps == nil) { a = nil; }
else {
if (ps.kind == nkind.N_PARAM) {
let pt: *node = ps.lhs;
if (istaggedtype(c, pt)) {
if (!isnullabletype(pt)) {
let sn: str = rhsstructpayload(c, a);
if (sn.len > 0) {
let isidentstruct: bool = false;
if (a.kind == nkind.N_IDENT) {
// Struct ident as
// tagged arg — pushargsrev
// still routes through the
// scratch path.
isidentstruct = true;
};
let _u: bool = isidentstruct;
if (!scanseenmark(c, "@tagscr")) {
total += 24;
};
};
};
};
};
if (a != nil) {
a = a.next;
ps = ps.next;
};
};
};
};
// Hare-style variadic call: reserve @vararg_d_<seq> for the
// element data and @vararg_sl_<seq> for the 24B slice
// descriptor. The seq is recorded on the N_CALL node so
// cgcall picks the same names regardless of walk order
// (scanlocals descends LTR; pushargsrev evaluates RTL).
let nfixed: i32 = 0;
let varp: *node = callee_variadic_param(c, n.lhs, &nfixed);
if (varp != nil) {
let nargs: i32 = 0;
let aw: *node = n.list;
for (aw != nil) { nargs += 1; aw = aw.next; };
let nvar: i32 = nargs - nfixed;
if (nvar < 0) { nvar = 0; };
let forwarding: bool = false;
if (nvar == 1) {
let aa: *node = n.list;
let k0: i32 = 0;
for (k0 < nfixed) { aa = aa.next; k0 += 1; };
if (aa != nil) {
if (aa.kind == nkind.N_SPREAD) {
forwarding = true;
};
};
};
if (!forwarding) {
let seq: i32 = c.varargseq;
n.uval = seq: u64;
c.varargseq += 1;
let esz: i32 = slotsize(c, varp.lhs);
if (esz < 1) { esz = 1; };
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
if (nvar > 0) {
if (!scanseenmark(c, dname)) {
let dsz: i32 = nvar * esz;
if ((dsz & 7) != 0) {
dsz = (dsz + 7) & ~7;
};
total += dsz;
};
};
if (!scanseenmark(c, sname)) { total += 24; };
};
};
};
if (n.lhs != nil) { total += scanlocals(c, n.lhs); };
if (n.rhs != nil) { total += scanlocals(c, n.rhs); };
if (n.cond != nil) { total += scanlocals(c, n.cond); };
if (n.body != nil) { total += scanlocals(c, n.body); };
if (n.els != nil) { total += scanlocals(c, n.els); };
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
total += scanlocals(c, m);
m = m.next;
};
};
return total;
};
// ---- function-level cgen ---------------------------------------------
fn cgfnparams(c: *cgen, params: *node) void = {
let p: *node = params;
let idx: i32 = 0;
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;
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). Mirror the slice-
// param spill below but use a synthesised TSLICE
// tnode so body references see the slot as a slice.
if (p.op == tkind.TK_ELLIPSIS) {
let tn: *node = slicewrap(c, p.lhs);
if (idx + 3 <= 6) {
let off: i32 = localadd(c, nm, 24, 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, 24, 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 (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;
};
if (istaggedtype(c, p.lhs)) {
let slot: i32 = slotsize(c, p.lhs);
let nw: i32 = slot / 8;
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, 24, 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, 24, 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 + 2 <= 6) {
let off: i32 = localadd(c, nm, 16, 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;
} else { if (idx < 6) {
// Partial-fit stitch — mirrors tagged at lines
// 440-469. Only idx=5 hits this (nw=2,
// regs_left=1): ptr lands in R9, len at
// +16+stkcursor*8(BP).
let off: i32 = localadd(c, nm, 16, 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 < 2) {
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 += 2;
};};
} 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;
};
};
fn cgfn(c: *cgen, fn_: *node) void = {
cgeninit(c, c.a);
c.fnname = fn_.str;
c.curmod = fn_.module;
c.fnret = fn_.lhs;
// 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. Drops the
// `exported == 0` skip in the legacy inline form — exported fns
// now mangle too, so cross-module same-leaf exports coexist.
emitline("TEXT ");
emitfnname(c, fn_.str, fn_.module);
emitline(",$");
// Pre-scan total frame: only count params that land in a local
// slot. SysV-class accounting; mirrors runtime walk in cstage
// cgen.c §5130-5223 and cgfnparams below. A stack-spilled param
// is addressed at a positive BP offset by cgfnparams (via
// localaddstack) and consumes no frame, so adding its size here
// would over-allocate. Seed c.locals with param-name stubs so
// scanlocals dedups a re-declared `let <name>` in the body
// against the param's slot (matches C cgen). Stubs get cleared
// before emission.
let scanp: *node = fn_.list;
let frame: i32 = 0;
let argi: i32 = 0;
let fargi: i32 = 0;
for (scanp != nil) {
if (scanp.kind == nkind.N_PARAM) {
let isvar: bool = scanp.op == tkind.TK_ELLIPSIS;
let isf: bool = false;
let istg: bool = false;
let issl: bool = false;
let isst: bool = false;
if (!isvar) {
isf = isfloattype(c, scanp.lhs);
istg = istaggedtype(c, scanp.lhs);
if (!isf && !istg) {
issl = isslicetype(c, scanp.lhs);
if (!issl) { isst = isstrtype(c, scanp.lhs); };
};
};
let eb: i32 = 1;
let sz: i32 = 8;
if (isvar) { eb = 3; sz = 24; }
else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; }
else { if (issl) { eb = 3; sz = 24; }
else { if (isst) { eb = 2; sz = 16; }
else { if (isf) {
eb = 1;
sz = 8;
if (isf32type(c, scanp.lhs)) { sz = 4; };
}; }; }; }; };
let regs_left: i32 = 6 - argi;
if (isf) { regs_left = 8 - fargi; };
if (regs_left >= eb) {
frame += sz;
if (isf) { fargi += 1; }
else { argi += eb; };
} else { if (eb > 1 && regs_left > 0 && (istg || issl || isst || isvar)) {
// Multi-word param straddles the reg/stack boundary;
// cgfnparams stitches the tail from positive BP
// offsets into a single local slot, so we still
// reserve the full size. Symmetric across tagged,
// slice, str and variadic `T...`
// (cgendecl.ww:467/518/564/394).
frame += sz;
argi = 6;
} else {
// Pure stack: lives at +BP(16+stkcursor*8); no
// local slot consumed. The reg cursor stays put.
}; };
scanseenmark(c, scanp.str);
};
scanp = scanp.next;
};
c.varargseq = 0;
if (fn_.body != nil) { frame += scanlocals(c, fn_.body); };
c.varargseq = 0;
// Drop the stubs so emission rebuilds c.locals with real offsets.
c.locals = nil;
if ((frame & 15) != 0) {
frame = (frame + 15) & ~15;
};
emitint(frame: i64);
emitline("\n");
emitline("\tPUSHQ\tBP\n");
emitline("\tMOVQ\tSP, BP\n");
emitline("\tSUBQ\t$");
emitint(frame: i64);
emitline(", SP\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 C cgen,
// 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");
};
};
// ---- 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);
};