From 607cc8d3303f0a354f99ff19b477d4c5f76e24b0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 11:19:42 +0900 Subject: [PATCH] cgen: name-based slot reuse, fn-addr ffi_resolve, indirect call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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. --- selfhost/cmd/wwc/cgen.ww | 130 +++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 18 deletions(-) diff --git a/selfhost/cmd/wwc/cgen.ww b/selfhost/cmd/wwc/cgen.ww index 76239d4c..6519e287 100644 --- a/selfhost/cmd/wwc/cgen.ww +++ b/selfhost/cmd/wwc/cgen.ww @@ -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 ` 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; };