Files
ww/selfhost/cmd/wcc/cgendecl.ww
Hojun-Cho 8e93b31088 cmd/w6c+selfhost/wcc+lib: route sizeof(str)/sizeof(slice) through SSoT
Audit §1.1/§1.2 cataloged 17 wwstage sites hardcoding 16 for sizeof(str)
and ~10 hardcoding 24 for sizeof(slice), plus 4 cstage str-size sites
and the cstage let_emit_size str/slice arms.  Each new size constant
required ~30 edits in both stages to bump cleanly — task #1 (str → 24B
{ptr,len,cap}) can't land until the literal sweep is done.

Track A — wwstage codegen (selfhost/cmd/wcc/*):

  - check.ww introduces two stateless helpers next to astsize:
    primtypesize(nm)  — primitive-name → byte size (i64; -1 unknown)
    tyslicesize()     — slice-header bytes (i64; 24 today)
    astsize now reads both for its N_TNAME-primitive and N_TSLICE arms,
    so the size(T) fold gets the SSoT for free.
  - cgen.ww, cgenutil.ww, cgenstmt.ww, cgendecl.ww: every `return 16`
    / `esz = 16` / `sz0 = 16` for str, every `return 24` /
    `localadd(c, _, 24, _)` for slice, plus the matching `sz == 16` /
    `sz == 24` / `for (i < 16/24)` gates in the global-let DATAW emit,
    route through primtypesize / tyslicesize.
  - Direct delegation slotsize→astsize would require restructuring
    astsize to drop its *checker dep (resolvealias) — the leaf
    primitive/slice cases factor out cleanly, the alias-chain leaves
    diverge because cgen's aliaslookup/structlookup tables and check's
    scope chain aren't unified yet (§1.8, task #50 follow-up).  Sharing
    the leaf table satisfies the SSoT promise without that refactor.

Track B — cstage (cmd/w6c/cgen.c):

  - let_emit_size's TY_STR/TY_SLICE arms drop the hardcoded 16/24 and
    fall to `(int)u->size` like the existing TY_STRUCT/TUPLE/TAGGED arms.
  - N_LET cgstmt's per-kind `sz` cascade collapses to a single
    `if (lu->kind ∈ {ARRAY,SLICE,STR,STRUCT,TUPLE,TAGGED}) sz = lu->size`.
  - N_LET cgexpr's match-bind primitive sizing: `bsz = (int)bu->size`
    drops the TY_STR/TY_SLICE special-cases (same outcome — ty_str/
    ty_slice already have ->size set by type.c).
  - Three `sz == 16` / `let_emit_size(d->type) != 16` gates against the
    str slot width route through ty_str->size.

  Cap-offset sites (cgen.c:2440/1994/3206/5517 `delta = 16` for
  slice's .cap field-write) intentionally NOT touched: 16 there is the
  *offset of .cap inside a slice header*, structurally always 16
  regardless of str.size.  #1 doesn't move the slice layout.

Track C — lib/ user code:

  - lib/strings.freeall + appendstr, lib/shlex.freepartial + appendstr:
    the four `16u64` literals (per-str-element stride for rt_ensure and
    os.free) become `size(str): u64`.  Check-time fold via #42's
    intercept resolves to 16 today; #1 reroutes via the bumped tinfo.

After this commit, bumping ty_str to 24B for task #1 requires editing
exactly two places (cmd/wcc/type.c:64 ty_str.size, plus check.ww
primtypesize's "str" arm) for the SSoT to propagate.

Verification:
  - 131/131 tests pass.  994_w6c_ww + 995_self_rebuild byte-identity
    holds — each replacement evaluates to the same constant the
    literal had today, so cgen output is unchanged.
  - selfhost source's `size(str): u64` folds at check time (cstage
    cmd/wcc/check.c:907-960 for the C-bootstrap of selfhost; wwstage
    check.ww:898-942 for the rebuild path), no runtime call introduced.
2026-05-20 08:50:40 +09:00

453 lines
13 KiB
Plaintext

// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
//
// Houses the top-level emission glue:
// - cgfnparams: parameter spilling per SysV
// - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue
// deferred via cgen.ww's cgoutbuf so the frame size
// reflects every emit-time localadd (#15/#26c)
// - 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;
// ---- 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 (localfind(c, "@sretarg") != 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, tyslicesize(): i32, 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, tyslicesize(): i32, 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, tyslicesize(): i32, 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, tyslicesize(): i32, 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.
let sret_callee: bool = sretretsize(c, c.fnret) > 0;
// Capture the body into cgoutbuf while c.frame grows under
// emit-time localadd calls (#15/#26c — wwstage dropped its
// scanlocals pre-pass to align DOWN with cstage's first-use
// pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits
// after the body finishes so the frame size reflects every
// localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds
// `subsp`/`text` Progs up front and patches their `from.offset`
// at the end via txt_emit.
cgout_enable(c.a);
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 cstage,
// 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");
};
cgout_disable();
let frame: i32 = c.frame;
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
// 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.
emitline("TEXT ");
emitfnname(c, fn_.str, fn_.nmod);
emitline(",$");
emitint(frame: i64);
emitline("\n");
emitline("\tPUSHQ\tBP\n");
emitline("\tMOVQ\tSP, BP\n");
emitline("\tSUBQ\t$");
emitint(frame: i64);
emitline(", SP\n");
cgout_flush();
};
// ---- 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);
};