Files
ww/selfhost/cmd/wcc/cgendecl.ww
Hojun-Cho 3c812faa08 w6c+selfhost: codegen for top-level mutable let
Third step toward writable globals. The C cgen and its selfhost
mirror now:

  - emit DATAW <name>(SB),"<8 LE bytes>" for every top-level `let`
    whose type lands in the scalar set (i8..i64/u8..u64/bool/rune/
    int/uint/uintptr/ptr; floats and multi-word types deferred);
  - drop the "no writable .data" silent-drop guard at the N_IDENT
    store path, replacing it with a RIP-relative MOVQ for `=` and
    a load→combine→store sequence for the compound ops; and
  - route `&name` through LEAQ name(SB) instead of dropping it.

Type aliases resolve via aliaslookup so `type counter = i32; let c:
counter = 0;` still emits a DATAW slot. Non-literal initialisers
silently skip, which surfaces as a clean undefined-symbol error if
the binding is ever referenced.

The selfhost mirror lands in the same commit because test 990
diffs the C cgen against wwdump_ww -c on err.ww (which has
top-level `let nerrors: i32 = 0; ... nerrors += 1;`). Any drift
between the two cgens makes 990 fail. Bootstrap stays at a fixed
point: ww2 == ww3 == ww4 byte-identical.
2026-05-12 11:56:51 +09:00

304 lines
8.4 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;
};
};
// 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;
for (p != nil) {
if (p.kind == nkind.N_PARAM) {
let nm: str = p.str;
if (istaggedtype(p.lhs)) {
// tagged-union param: spill size/8 registers
// (tag + value words). Slot sized to match.
let slot: i32 = slotsize(c, p.lhs);
let off: i32 = localadd(c, nm, slot, p.lhs);
let nw: i32 = slot / 8;
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 (isslicetype(c, p.lhs)) {
// slice param: 3 regs (ptr, len, cap), 24-byte slot.
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 (isstrtype(c, p.lhs)) {
// str param: passed in two regs (ptr, len).
// Slot is 16 bytes; ptr at off+0, len at off+8.
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 {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 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(scanp.lhs)) { frame += 24; }
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;
};
emitdatasection(c);
emitdefconstants(c, file);
emitletdataw(c, file);
};