// 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. package wcc; import os; import mem; import ast; import tok; import typ; import sym; import strconv; // tagscrbump — record that the body needs an @tagscr scratch slot of at // least `need` bytes and return how many additional frame bytes that // imposes. Each tagged-scratch reservation site calls this; the first // raises c.tagscrsz from 0, later sites only grow it when they need // more. Closes STATUS latent #1: pre-fix every site reserved a flat 24B // and the slot under-allocated for any tagged-union with a 24B+ payload // (e.g. `(void | err)` where `err` is 24B → slot_sz 32). Cgen-side // emit (cgreturn / pushargsrev / cgindex / cgwidentaggedstore) reads // c.tagscrsz to allocate the actual slot — scan + emit see the same // number, so rob's "lockstep" invariant holds. The @tagscr lifetime is // short-lived per use (zero, fill, copy out), and uses are sequential // within a fn body, so sharing the max is safe. fn tagscrbump(c: *cgen, need: i32) i32 = { let n: i32 = need; if (n < 8) { n = 8; }; if ((n & 7) != 0) { n = (n + 7) & ~7; }; if (n <= c.tagscrsz) { return 0; }; let delta: i32 = n - c.tagscrsz; c.tagscrsz = n; return delta; }; // sretscrbump — sister of tagscrbump for the sret discard slot // (#23). Tracks max sret return type used as a CALL discard / nested // receiver. Returns frame-byte delta vs the previous high water mark // (rounded up to 8B). fn sretscrbump(c: *cgen, need: i32) i32 = { let n: i32 = need; if (n < 8) { n = 8; }; if ((n & 7) != 0) { n = (n + 7) & ~7; }; if (n <= c.sretscrsz) { return 0; }; let delta: i32 = n - c.sretscrsz; c.sretscrsz = n; return delta; }; // // 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_" 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_` // (counter) and `.rgl_` (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 an `@match_spill` scratch slot sized // to the scrutinee's tagged-union slot (16/24/32 for 1/2/3-word // payload). Mirrors cstage's `slot_size = su->size` default 16 // in cmd/w6c/cgen.c cgmatch (task #9 align-down to cstage). // matchspillsz must agree with cgmatch's emit-time computation // for scan+emit lockstep. 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 += matchspillsz(c, matchscrutt(c, sc)); }; }; }; // 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 an @tagscr scratch slot for // cgwidentaggedstore to materialise the source in before copying // to the element address. Slot is shared per function via // c.tagscrsz (raised to the largest element slot_sz seen). // N_DOT-base graduated (task #30) so `obj.arr[i] = v` for an // [N]Tagged struct field pre-reserves the slot — parallels the // cgassign N_DOT-base elemtn fix in cgenexpr.ww. Without this // arm the runtime localadd allocates past the frame boundary // and clobbers live locals; cstage handles the shape naturally // via first-use+fail-loud (rule-10 convergence filed as #15). 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)) { total += tagscrbump(c, slotsize(c, etn)); }; }; }; }; if (abase.kind == nkind.N_DOT) { let ft: *node = dotfieldtnode(c, abase); if (ft != nil) { let bk: nkind = ft.kind; let etn: *node = nil; if (bk == nkind.N_TARRAY) { etn = ft.lhs; }; if (bk == nkind.N_TSLICE) { etn = ft.lhs; }; if (bk == nkind.N_TPTR) { etn = ft.lhs; }; if (etn != nil) { if (istaggedtype(c, etn)) { total += tagscrbump(c, slotsize(c, etn)); }; }; }; }; }; }; }; }; // 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, fixed) and @tagscr (sized to the // field's tagged slot). The @tagscr size feeds c.tagscrsz. 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; }; total += tagscrbump(c, slotsize(c, fi.tnode)); }; }; 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/R8. 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. Slot is // sized to the return type's slot_sz (was hardcoded 24, which // truncated 32B slots — `(void | err24)` clobbered its own // payload local; task #38). 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) { total += tagscrbump(c, slotsize(c, c.fnret)); }; }; }; }; }; // 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; }; }; }; }; }; }; }; // sret CALL (#23): callee returns plain TY_STRUCT > 24B. The // receive site (cglet / cgassign ident) overrides at emit time // with the dest local's own slot; discards / nested calls fall // back to @sretscr. Single-slot per fn sized to the max sret // return — sretscrbump tracks the high-water mark so a later // larger call grows the frame without re-counting the prior // reservation. Mirrors @tagscr's cumulative tagscrbump. if (n.kind == nkind.N_CALL) { let scs: i32 = callsretsize(c, n); if (scs > 0) { total += sretscrbump(c, scs); }; }; // 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; total += tagscrbump(c, slotsize(c, pt)); }; }; }; }; if (a != nil) { a = a.next; ps = ps.next; }; }; }; }; // Hare-style variadic call: reserve @vararg_d_ for the // element data and @vararg_sl_ 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; // 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 (c.sretargoff != 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; 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 { 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 { 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_.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. Decision // made here so the frame pre-scan and cgfnparams see the same // view of the int-arg cursor. let sret_callee: bool = sretretsize(c, c.fnret) > 0; // 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_.nmod); 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 ` 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; // Reserve @sretarg (8B) BEFORE the param-induced frame, and // start argi at 1 so the param walker sees RDI as consumed. if (sret_callee) { frame += 8; argi = 1; }; 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; let isstruct: bool = false; let structsz: i32 = 0; 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); }; if (!issl && !isst) { structsz = structparamsize(c, scanp.lhs); isstruct = structsz > 0; }; }; }; 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 (isstruct) { sz = structsz; eb = 1; if (structsz > 8) { eb = 2; }; } 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 || isstruct || 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. }; }; // Record the param's tnode on the stub so scanlocals's // `h.f = v` / `&h[i]` / etc. detection paths can // resolve a *struct / *[]T / *T param through // localfindnode rather than seeing tnode=nil and // skipping the reservation. Latent pre-#38: the // pointer-rooted struct-field tagged write // (cgendecl.ww:264) never fired for `fn fill(h: *holder) // { h.e = v; }` because the param stub had no type // info, so @tagscr / @tagbase weren't counted in the // frame. Emit-time localadd happened to fit pre-#38 // because the 24B hardcoded slot didn't collide with // the 8B @tagbase neighbour, but a correctly-sized // slot revealed the under-reservation. if (!scanseenmark(c, scanp.str)) { c.locals.tnode = scanp.lhs; }; }; 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"); 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 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); };