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; };