// 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. Same-name re-declarations share the first // slot (see scanseenmark / localadd). if (!scanseenmark(c, n.str)) { let sz: i32 = letslotsize(c, n); if (sz < 8) { sz = 8; }; if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; total += sz; }; // Carry the let's tnode into the stub so scanlocals can // dispatch on type later in the walk — e.g. detecting // `arr[i] = ...` where arr is a tagged-element array, // which needs an @tagscr scratch slot reservation. let stub: *local = localfindnode(c, n.str); if (stub != nil) { if (stub.tnode == nil) { if (n.lhs != nil) { stub.tnode = n.lhs; }; }; }; }; // 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) { if (!scanseenmark(c, l.str)) { 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; }; 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 if (n.list != nil) { let m: *node = n.list; for (m != nil) { let bnm: str = m.str; if (bnm.len == 0) { total += 8; // discard binding still gets a slot } else { if (!scanseenmark(c, bnm)) { total += 8; };}; m = m.next; }; } else { let bnm: str = n.str; if (bnm.len == 0) { total += 8; } else { if (!scanseenmark(c, bnm)) { total += 8; };}; }; }; // `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) { if (isstrtype(c, pat)) { total += 16; } else { if (isslicetype(c, pat)) { total += 24; } else { total += 8; }; }; }; }; // 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 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; }; }; }; }; }; }; // 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_ 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; 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 { 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 { 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 { 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.fnret = fn_.lhs; emitline("TEXT "); if (fn_.exported == 0) { if (fn_.module.len > 0) { let isffi: bool = false; let a: *node = fn_.attr; for (a != nil) { if (a.kind == nkind.N_ATTR) { let an: str = a.str; if (streq(an, "symbol")) { isffi = true; }; }; a = a.next; }; // `main` is the linker entry-point convention; even // when not marked `export`, it must keep its bare // name so w6l's _start can resolve `CALL main(SB)`. // Mirror of cmd/w6c/cgen.c collectmods exemption. let isentry: bool = streq(fn_.str, "main"); if (!isffi && !isentry) { os.write(1, fn_.module.ptr, fn_.module.len: u64); os.write(1, ".".ptr, 1u64); }; }; }; let nm: str = fn_.str; os.write(1, nm.ptr, nm.len: u64); emitline(",$"); // Pre-scan total frame: 24 bytes per slice param, 16 per str // param, 8 per other param, plus per-let from scanlocals. // 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; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { // Hare-style variadic `T...`: param is []T inside // the callee, so it occupies a 24B slice slot. if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } else { frame += 8; }; }; }; }; 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; if (fn_.body != nil) { 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); };