cgen: name-based slot reuse, fn-addr ffi_resolve, indirect call

Closes the byte-identity gap between selfhost/cmd/wwc/cgen.ww and
the C cgen, so a ww_ww-built binary matches the cstage-built binary
on the same input. The bootstrap fixed point was already green;
these are the bytes inside that fixed point that diverged from
what cmd/6c emits.

Slot allocation:

- local_add dedups by name (mirror cmd/6c/cgen.c:localoff). Two
  `let cp: pos;` in disjoint if-branches share one slot. The
  stored tnode is refreshed on each hit so a later `let m: *node`
  shadowing an earlier `let m: i32` sees its own type when
  emitting `m.next` — without this the N_DOT cgen fell into the
  SB-symbol fallback and the linker complained about undefined
  `next`.
- scan_locals dedups at frame-size time to keep the prologue SUBQ
  in sync. cgfn seeds c.locals with param-name stubs before the
  scan so a body's `let <param-name>` reuses the param slot, then
  resets c.locals before emission so real offsets get installed.
- local_alloc (no dedup) for N_MCASE bindings: C cgen handles a
  match as an expression with by-value `locals`, so two separate
  matches each get fresh slots for `v`/`e`.

Per-instruction matching:

- `return;` in a void fn zeros AX (C cgen falls through to
  cgexpr_int(c, 0)).
- N_INTLIT prints i64 (signed), not u64. FNV-1a's offset basis
  now prints as `$-3750763034362895579`, matching `$%lld`.
- *p = strexpr push order swapped to PUSH AX / PUSH BX → POP CX
  / POP AX (cgen.c:1033-1041).

Feature port from 635818e (the half that the wwdump corpus
actually exercises):

- N_IDENT used as a value with fn type now LEAQs through
  ffi_resolve, so `let fp = some_ffi_fn;` emits the C symbol.
- N_CALL on a bare ident checks local_find_node first; a local
  fn-pointer dispatches as `cgexpr(callee); CALL AX` instead of
  `CALL ident(SB)`.

Tests 990 probe 6 and 994 are byte-identical on mem/err/tok/smoke
+ the four selfhost main.combined.ww files. Bootstrap still
ww2 == ww3.
This commit is contained in:
2026-05-11 11:19:42 +09:00
parent 78ddc360aa
commit 607cc8d330

View File

@@ -173,10 +173,12 @@ fn cgen_init(c: *cgen, a: *arena) void = {
c.loop_cont_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
};
fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
// Round slot size up to 8-byte multiple so all locals stay
// 8-aligned (C cgen does the same — buf[4]u8 still occupies an
// 8-byte slot).
// local_alloc — append a slot for `name` without dedup. Used for
// match-arm bindings, which C cgen allocates via cgexpr's by-value
// `locals` list — so two separate matches each get fresh slots even
// when their bind names collide. scan_locals follows the same rule
// for N_MCASE.
fn local_alloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
let asz: i32 = sz;
if (asz < 8) { asz = 8; };
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
@@ -191,6 +193,50 @@ fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
return off;
};
fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
// Name-based slot reuse for N_LETs and params: if `name` is
// already declared in this function, return its existing
// offset. Mirrors C cgen (cmd/6c/cgen.c:localoff). Two
// disjoint scopes that declare the same name share one slot —
// so `escape` in wwdump (three `let cp: pos;` across separate
// branches) reserves one slot, not three. scan_locals does
// the matching dedup at prologue time so the SUBQ stays in
// sync.
//
// On a dedup hit we also overwrite the stored tnode to match
// the new declaration's type. C reads `n->lhs->type` (filled
// by the checker) at every N_DOT/N_CAST site; we read
// `lc.tnode`, so it must follow source order. Without this,
// a later `let m: *node` inside a branch keeps an earlier
// `let m: i32`'s tnode and `m.next` falls into the SB fallback.
let cur: *local = c.locals;
for (cur != nil) {
let cn: str = cur.name;
if (streq(cn, name)) {
cur.tnode = tnode;
return cur.off;
};
cur = cur.lnext;
};
return local_alloc(c, name, sz, tnode);
};
// scan_seen_mark — called by scan_locals on every let / match-bind
// site. Returns true if `name` is already tracked in c.locals (so
// the slot will be shared at emission time — no new frame bump).
// Otherwise appends a name-only stub and returns false. Stubs are
// thrown away when cgfn resets c.locals before emission.
fn scan_seen_mark(c: *cgen, name: str) bool = {
if (local_find_node(c, name) != nil) { return true; };
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = 0;
l.tnode = nil;
l.lnext = c.locals;
c.locals = l;
return false;
};
fn local_find_node(c: *cgen, name: str) *local = {
let l: *local = c.locals;
for (l != nil) {
@@ -1075,14 +1121,18 @@ fn cgexpr(c: *cgen, n: *node) void = {
let k: i32 = n.kind;
if (k == N_INTLIT) {
// Print signed (i64), not unsigned (u64). C cgen uses
// `$%lld` so 64-bit constants with bit 63 set show up as
// negative — e.g. FNV-1a's offset basis prints as
// $-3750763034362895579, not $14695981039346656037.
emit_line("\tMOVQ\t$");
emit_uint(n.uval);
emit_int(n.uval: i64);
emit_line(", AX\n");
return;
};
if (k == N_RUNELIT) {
emit_line("\tMOVQ\t$");
emit_uint(n.uval);
emit_int(n.uval: i64);
emit_line(", AX\n");
return;
};
@@ -1145,11 +1195,15 @@ fn cgexpr(c: *cgen, n: *node) void = {
return;
};
// Fn-name used as a value (e.g. `let f = some_fn;` or
// `... = some_fn;`). LEAQ the symbol address into AX.
// `... = some_fn;`). LEAQ the symbol address into AX
// resolved through ffi_resolve so a body-less FFI binding
// emits the C symbol it was declared with via @symbol(),
// not the ww-side ident.
let rt: *node = fnret_lookup(c, nm);
if (rt != nil) {
let resolved: str = ffi_resolve(c, nm);
emit_line("\tLEAQ\t");
os.write(1, nm.ptr, nm.len: u64);
os.write(1, resolved.ptr, resolved.len: u64);
emit_line("(SB), AX\n");
return;
};
@@ -1289,7 +1343,11 @@ fn cgexpr(c: *cgen, n: *node) void = {
if (pat != nil) {
let bsz: i32 = 8;
if (is_str_type(c, pat)) { bsz = 16; };
let voff: i32 = local_add(c, bn, bsz, pat);
// local_alloc (not local_add): match-arm
// binds don't dedup with same-named binds
// in *other* matches, since C's cgexpr
// allocates a fresh slot per match expr.
let voff: i32 = local_alloc(c, bn, bsz, pat);
emit_line("\tMOVQ\t");
emit_off((scrut_off + 8): i64);
emit_line("(BP), AX\n");
@@ -1664,9 +1722,19 @@ fn cgexpr(c: *cgen, n: *node) void = {
callee_name.ptr = nil; callee_name.len = 0;
// Detect fn-pointer field call: `w.emit(args)` where `w` is
// a struct local and `emit` is an N_TFN field. Load the
// field value into AX and CALL through it.
// field value into AX and CALL through it. Also detect a
// bare `fp(args)` where `fp` is a local holding a function
// pointer — mirror C cgen's localfind dispatch (commit
// 635818e). Without this the call emits `CALL fp(SB)` and
// the linker rightly fails.
let is_fnptr_call: bool = false;
if (callee != nil) {
if (callee.kind == N_IDENT) {
let cn: str = callee.str;
if (local_find_node(c, cn) != nil) {
is_fnptr_call = true;
};
};
if (callee.kind == N_DOT) {
let base: *node = callee.lhs;
let fld: str = callee.str;
@@ -1784,17 +1852,24 @@ fn cgexpr(c: *cgen, n: *node) void = {
};
};
cgexpr(c, n.rhs);
if (elem_str) { emit_line("\tPUSHQ\tBX\n"); };
// Push order matches C cgen
// (cmd/6c/cgen.c:1033-1041): PUSHQ AX
// (ptr) first, then PUSHQ BX (len) if
// str, so the pop sequence is POP CX
// (len) → POP AX (ptr) → MOVQ AX,
// (BX) → MOVQ CX, 8(BX).
emit_line("\tPUSHQ\tAX\n");
if (elem_str) { emit_line("\tPUSHQ\tBX\n"); };
cgexpr(c, inner);
emit_line("\tMOVQ\tAX, BX\n");
emit_line("\tPOPQ\tAX\n");
if (elem_str) {
emit_line("\tMOVQ\tAX, (BX)\n");
emit_line("\tPOPQ\tCX\n");
emit_line("\tPOPQ\tAX\n");
emit_line("\tMOVQ\tAX, (BX)\n");
emit_line("\tMOVQ\tCX, 8(BX)\n");
return;
};
emit_line("\tPOPQ\tAX\n");
emit_line("\t");
emit_line(store_op);
emit_line("\tAX, (BX)\n");
@@ -2297,13 +2372,21 @@ fn scan_locals(c: *cgen, n: *node) i32 = {
// scan_locals must agree with local_add or the prologue
// SUBQ undersizes the frame and lets overflow into the
// caller's stack — corrupting whatever's at -frameSize..-1
// of the caller.
let sz: i32 = slot_size(c, n.lhs);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
// of the caller. Same-name re-declarations share the first
// slot (see scan_seen_mark / local_add).
if (!scan_seen_mark(c, n.str)) {
let sz: i32 = slot_size(c, n.lhs);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
};
};
// 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 local_add then bumps it to.
if (n.kind == N_MCASE) {
let bn: str = n.str;
if (bn.len > 0) {
@@ -2393,6 +2476,11 @@ fn cgstmt(c: *cgen, n: *node) void = {
return;
};
cgexpr(c, rhs);
} else {
// Bare `return;` in a void fn — zero AX so the caller
// sees a deterministic value (matches C cgen, which
// always falls through to `cgexpr_int(c, 0)`).
emit_line("\tMOVQ\t$0, AX\n");
};
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).
// cgexpr leaves str in (AX, BX); shuffle BX→DX.
@@ -2804,6 +2892,9 @@ fn cgfn(c: *cgen, fn_: *node) void = {
// Pre-scan total frame: 24 bytes per slice param, 16 per str
// param, 8 per other param, plus per-let from scan_locals.
// Seed c.locals with param-name stubs so scan_locals dedups a
// re-declared `let <name>` in the body against the param's
// slot (matches C cgen). Stubs get cleared before emission.
let scan_p: *node = fn_.list;
let frame: i32 = 0;
for (scan_p != nil) {
@@ -2812,10 +2903,13 @@ fn cgfn(c: *cgen, fn_: *node) void = {
else { if (is_slice_type(c, scan_p.lhs)) { frame += 24; }
else { if (is_str_type(c, scan_p.lhs)) { frame += 16; }
else { frame += 8; }; }; };
scan_seen_mark(c, scan_p.str);
};
scan_p = scan_p.next;
};
if (fn_.body != nil) { frame += scan_locals(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;
};