Files
ww/selfhost/cmd/wcc/cgendecl.ww
Hojun-Cho 6e7c9e0df4 selfhost: alias-aware istaggedtype for nested-union match
`type error = !(invalid | overflow)` miscompiled — istaggedtype
only matched N_TTAGGED directly, so an `e: error` param spilled
as 8B scalar and the match's slot+8 read trailed into saved BP.

Mirror isstrtype's alias+bang unwrap; add resolvetagged() for
is/as/match sites that need the inner N_TTAGGED. Frame scan
counts via slotsize so wwstage stays byte-identical to cstage.
Unblocks lib/strconv.strerror.
2026-05-13 04:23:31 +09:00

392 lines
11 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. 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;
};
};
// 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_<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
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 { total += 8; };
};
};
};
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;
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 {
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;
};
if (!isffi) {
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 <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;
for (scanp != nil) {
if (scanp.kind == nkind.N_PARAM) {
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;
};
if (fn_.body != nil) { frame += scanlocals(c, fn_.body); };
// 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);
};