From 73c0cf4c785d80bc593414479d686f49449bf7f0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 18:40:41 +0900 Subject: [PATCH] w6c+selfhost: match-arm scope/spill + alloc(structlit) sugar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps in the wwstage cgen relative to C w6c, plus a matching C-side bug surfaced along the way. cgmatch (selfhost) handles non-ident scrutinees: `match (foo())` now spills the AX:DX:CX return triple into a 24B `@match_spill` slot rather than reading garbage off BP+0. scanlocals counts the slot so the prologue SUBQ stays in sync. For N_CALL we recover the return type via fnretlookup so nullable dispatch picks the pointer-vs-null discriminator. Mirrors @match_spill in cmd/w6c/cgen.c N_MATCH. cgcall (selfhost) special-cases `alloc(structlit{...})`: lower to rt_alloc(totsize) + per-field MOV* at the struct's field offsets, mirroring cmd/w6c/cgen.c's existing path. Previously the structlit fell into pushargsrev and produced wrong code. check.ww's N_MCASE branch now pushes a fresh scope around each arm body. Without this, `case let e: str` inside a fn with an outer `let e: *T` collided with scopedefine's same-scope dedup, the inner binding silently dropped, and references to `e` inside the arm resolved through the outer type. Both cgens save/restore the locals head around case bodies so arm binds (and nested arm-body lets) don't leak past the arm — code after the match resolves names back through the outer scope. w6c gains `local_alloc`: same as `localoff` minus the dedup. N_MATCH case-bind allocation switches to it. Previously `let e: *T` (8B) shadowed by `case let e: str` (16B) reused the outer 8B slot and the inner str.len store overflowed into the saved BP, segfaulting on return. Tests 26/26. --- cmd/w6c/cgen.c | 40 +++++++- selfhost/cmd/w6c/main.combined.ww | 143 ++++++++++++++++++++++++++- selfhost/cmd/wcc/cgendecl.ww | 10 ++ selfhost/cmd/wcc/cgenexpr.ww | 124 +++++++++++++++++++++++ selfhost/cmd/wcc/check.ww | 9 +- selfhost/cmd/wwdump/main.combined.ww | 143 ++++++++++++++++++++++++++- 6 files changed, 463 insertions(+), 6 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 64224885..b928acc2 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -720,6 +720,26 @@ localoff(Cg *c, Local **head, const char *name, int size, int *frame) return off; } +/* local_alloc — always push a fresh slot, never dedup by name. Used for + * match-arm bindings, where `case let e: T` must shadow any outer `e` + * with a slot sized to T — localoff's dedup would reuse the outer's + * (possibly smaller) slot and let multi-word writes overflow into the + * saved BP / return address. localfind walks from the head, so the + * fresh entry still wins inside the arm body. */ +static int +local_alloc(Cg *c, Local **head, const char *name, int size, int *frame) +{ + int al = 8; + *frame = (*frame + size + al - 1) & ~(al - 1); + int off = -*frame; + Local *l = amalloc(c->a, sizeof *l); + l->name = name; + l->off = off; + l->next = *head; + *head = l; + return off; +} + static int localfind(Local *head, const char *name) { @@ -2232,6 +2252,13 @@ cgexpr(Cg *c, Node *n, Local *locals) } for (Node *cs = n->list; cs; cs = cs->next) { char *next = mklabel(c, "match_next"); + /* Per-arm scope: save the locals head, restore it + * after the body runs. Mirrors check.c's saved/restore + * around cstmt — the case bind (and any lets inside + * the arm) shouldn't leak past the arm, where a + * matching outer name would otherwise resolve to the + * shadow instead of the original. */ + Local *arm_locals_saved = locals; if (cs->type != NULL) { int tag = cg_tag_for_variant(su, cs->type); ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); @@ -2281,9 +2308,11 @@ cgexpr(Cg *c, Node *n, Local *locals) * value IS the slot's pointer word; no * payload to copy. void binding is * unusable (size 0), so only emit for - * the *T variant. */ + * the *T variant. local_alloc (not + * localoff): the bind must NEVER reuse + * an outer same-named slot. */ if (bu && bu->kind == TY_PTR) { - int voff = localoff(c, &locals, + int voff = local_alloc(c, &locals, cs->str, 8, cg_frame); ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), @@ -2294,7 +2323,11 @@ cgexpr(Cg *c, Node *n, Local *locals) } else { int bsz = (bu && bu->kind == TY_STR) ? 16 : 8; - int voff = localoff(c, &locals, cs->str, + /* local_alloc to dodge name-collision + * dedup — a 16B str bind shadowing an + * 8B outer would otherwise overflow + * into the saved BP. */ + int voff = local_alloc(c, &locals, cs->str, bsz, cg_frame); int nwords = (bsz + 7) / 8; for (int w = 0; w < nwords; w++) { @@ -2307,6 +2340,7 @@ cgexpr(Cg *c, Node *n, Local *locals) } } cgstmt(c, cs->body, &locals, cg_frame); + locals = arm_locals_saved; ins1(c, A_JMP, abranch(end)); label(c, next); } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a7588e18..2c553c58 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -4215,14 +4215,21 @@ fn resolvewalk(c: *checker, n: *node) void = { }; // `match (e) { case let v: T => stmt; ... }` — the binding `v` - // is declared by the case arm and visible inside its body. + // is declared by the case arm and visible inside its body. Push a + // fresh scope so `case let e: str` doesn't collide with an outer + // `let e: *T` (scopedefine drops same-scope dupes silently and + // would leave references to `e` resolving to the outer type). + // Mirrors cmd/wcc/check.c's newscope/saved-restore around cstmt. if (k == nkind.N_MCASE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + let outer: *scope = c.cur; + c.cur = newscope(c.a, outer); let nm: str = n.str; if (nm.len > 0) { scopedefine(c.cur, nm, skind.SK_VAR, nil, n); }; if (n.body != nil) { resolvewalk(c, n.body); }; + c.cur = outer; return; }; @@ -6852,6 +6859,42 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutoff = lc.off; scrutt = resolvetype(c, lc.tnode); }; + } else { + // Non-ident scrutinee (call result, ?, etc.). Spill into a + // 24B `@match_spill` scratch slot and dispatch off it. + // Tagged returns follow the AX:DX:CX convention, so store + // all three words at +0/+8/+16; nullable returns are + // single-word (AX = ptr) and only read +0, so the extra + // stores are harmless. For N_CALL we recover the return + // type via fnretlookup so nullable dispatch can pick the + // pointer-vs-null discriminator. Mirrors C cgen's + // @match_spill path in cmd/w6c/cgen.c N_MATCH. + scrutoff = localalloc(c, "@match_spill", 24, nil); + if (scrut.kind == nkind.N_CALL) { + let callee: *node = scrut.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) { scrutt = resolvetype(c, rt); }; + }; + }; + }; + cgexpr(c, scrut); + emitline("\tMOVQ\tAX, "); + emitoff(scrutoff: i64); + emitline("(BP)\n"); + if (!isnullabletype(scrutt)) { + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; }; }; let endl: str = mklabel(c, "match_end"); @@ -6865,6 +6908,14 @@ fn cgmatch(c: *cgen, n: *node) void = { let nxt: str = mklabel(c, "match_next"); let pat: *node = cs.lhs; let nullable: bool = isnullabletype(scrutt); + // Per-arm scope: save c.locals before allocating the bind + // and restore after the body runs, so the arm's bind (and + // any nested lets) don't leak past the arm. Matches the + // checker's newscope/restore around N_MCASE. Without this, + // `let e: *T = ...; match (r) { case let e: str => ... }; + // use e` would resolve `e` after the match to the inner + // str slot instead of the outer ptr. + let arm_locals_saved: *local = c.locals; // Compute the variant tag for this arm. Default arm // (no pattern) skips the tag check. if (pat != nil) { @@ -6968,6 +7019,10 @@ fn cgmatch(c: *cgen, n: *node) void = { }; // Body. Match arms are statements; we cgstmt them. if (cs.body != nil) { cgstmt(c, cs.body); }; + // Restore the locals head — pop everything the arm pushed + // so post-match code resolves names to their original (outer) + // bindings. + c.locals = arm_locals_saved; emitline("\tJMP\t"); emitline(endl); emitline("\n"); @@ -7562,6 +7617,72 @@ fn cgbin(c: *cgen, n: *node) void = { return; }; +// cgalloc — `alloc(value)` builtin lowering. Allocate sizeof(value) +// bytes via rt_alloc, then write the value's bytes into the new +// region. For an N_STRUCTLIT arg, allocate the struct's totsize and +// emit per-field stores at each field's offset. For a scalar/ptr, +// allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's +// alloc-special branch in N_CALL. Returns the heap ptr in AX. +fn cgalloc(c: *cgen, n: *node) void = { + let v: *node = n.list; + let sz: i32 = 8; + let si: *structinfo = nil; + if (v.kind == nkind.N_STRUCTLIT) { + let trefn: *node = v.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (trefn != nil) { + if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; } + else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; }; + }; + si = structlookup(c, sname); + if (si != nil) { sz = si.totsize; }; + }; + emitline("\tMOVQ\t$"); + emitint(sz: i64); + emitline(", DI\n"); + emitline("\tCALL\trt_alloc(SB)\n"); + emitline("\tPUSHQ\tAX\n"); + if (v.kind == nkind.N_STRUCTLIT) { + if (si != nil) { + let f: *node = v.list; + for (f != nil) { + if (f.kind == nkind.N_FIELD) { + let fname: str = f.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + cgexpr(c, f.lhs); + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + f = f.next; + }; + }; + } else { + cgexpr(c, v); + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = "MOVQ"; + if (sz == 1) { sop = "MOVB"; } + else { if (sz == 4) { sop = "MOVL"; }; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + }; + emitline("\tPOPQ\tAX\n"); +}; + // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: @@ -7705,6 +7826,16 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // `alloc(value)` builtin: heap-init a fresh *T with the + // value's bytes. For struct literals, lower to rt_alloc + // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL + // alloc path. + if (streq(callee.str, "alloc")) { + if (n.list != nil) { + cgalloc(c, n); + return; + }; + }; }; }; @@ -9698,6 +9829,16 @@ fn scanlocals(c: *cgen, n: *node) i32 = { };}; }; }; + // `match (non-ident)` needs a 24B `@match_spill` scratch slot for + // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's + // localoff("@match_spill", ...). N_IDENT scrutinees read the slot + // directly off the local — no spill needed. + if (n.kind == nkind.N_MATCH) { + let sc: *node = n.lhs; + if (sc != nil) { + if (sc.kind != nkind.N_IDENT) { total += 24; }; + }; + }; // 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, diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 7860fb7b..2c03d48b 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -119,6 +119,16 @@ fn scanlocals(c: *cgen, n: *node) i32 = { };}; }; }; + // `match (non-ident)` needs a 24B `@match_spill` scratch slot for + // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's + // localoff("@match_spill", ...). N_IDENT scrutinees read the slot + // directly off the local — no spill needed. + if (n.kind == nkind.N_MATCH) { + let sc: *node = n.lhs; + if (sc != nil) { + if (sc.kind != nkind.N_IDENT) { total += 24; }; + }; + }; // 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, diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 9b2622fb..7eb74bbf 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -691,6 +691,42 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutoff = lc.off; scrutt = resolvetype(c, lc.tnode); }; + } else { + // Non-ident scrutinee (call result, ?, etc.). Spill into a + // 24B `@match_spill` scratch slot and dispatch off it. + // Tagged returns follow the AX:DX:CX convention, so store + // all three words at +0/+8/+16; nullable returns are + // single-word (AX = ptr) and only read +0, so the extra + // stores are harmless. For N_CALL we recover the return + // type via fnretlookup so nullable dispatch can pick the + // pointer-vs-null discriminator. Mirrors C cgen's + // @match_spill path in cmd/w6c/cgen.c N_MATCH. + scrutoff = localalloc(c, "@match_spill", 24, nil); + if (scrut.kind == nkind.N_CALL) { + let callee: *node = scrut.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) { scrutt = resolvetype(c, rt); }; + }; + }; + }; + cgexpr(c, scrut); + emitline("\tMOVQ\tAX, "); + emitoff(scrutoff: i64); + emitline("(BP)\n"); + if (!isnullabletype(scrutt)) { + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; }; }; let endl: str = mklabel(c, "match_end"); @@ -704,6 +740,14 @@ fn cgmatch(c: *cgen, n: *node) void = { let nxt: str = mklabel(c, "match_next"); let pat: *node = cs.lhs; let nullable: bool = isnullabletype(scrutt); + // Per-arm scope: save c.locals before allocating the bind + // and restore after the body runs, so the arm's bind (and + // any nested lets) don't leak past the arm. Matches the + // checker's newscope/restore around N_MCASE. Without this, + // `let e: *T = ...; match (r) { case let e: str => ... }; + // use e` would resolve `e` after the match to the inner + // str slot instead of the outer ptr. + let arm_locals_saved: *local = c.locals; // Compute the variant tag for this arm. Default arm // (no pattern) skips the tag check. if (pat != nil) { @@ -807,6 +851,10 @@ fn cgmatch(c: *cgen, n: *node) void = { }; // Body. Match arms are statements; we cgstmt them. if (cs.body != nil) { cgstmt(c, cs.body); }; + // Restore the locals head — pop everything the arm pushed + // so post-match code resolves names to their original (outer) + // bindings. + c.locals = arm_locals_saved; emitline("\tJMP\t"); emitline(endl); emitline("\n"); @@ -1401,6 +1449,72 @@ fn cgbin(c: *cgen, n: *node) void = { return; }; +// cgalloc — `alloc(value)` builtin lowering. Allocate sizeof(value) +// bytes via rt_alloc, then write the value's bytes into the new +// region. For an N_STRUCTLIT arg, allocate the struct's totsize and +// emit per-field stores at each field's offset. For a scalar/ptr, +// allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's +// alloc-special branch in N_CALL. Returns the heap ptr in AX. +fn cgalloc(c: *cgen, n: *node) void = { + let v: *node = n.list; + let sz: i32 = 8; + let si: *structinfo = nil; + if (v.kind == nkind.N_STRUCTLIT) { + let trefn: *node = v.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (trefn != nil) { + if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; } + else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; }; + }; + si = structlookup(c, sname); + if (si != nil) { sz = si.totsize; }; + }; + emitline("\tMOVQ\t$"); + emitint(sz: i64); + emitline(", DI\n"); + emitline("\tCALL\trt_alloc(SB)\n"); + emitline("\tPUSHQ\tAX\n"); + if (v.kind == nkind.N_STRUCTLIT) { + if (si != nil) { + let f: *node = v.list; + for (f != nil) { + if (f.kind == nkind.N_FIELD) { + let fname: str = f.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + cgexpr(c, f.lhs); + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + f = f.next; + }; + }; + } else { + cgexpr(c, v); + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = "MOVQ"; + if (sz == 1) { sop = "MOVB"; } + else { if (sz == 4) { sop = "MOVL"; }; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + }; + emitline("\tPOPQ\tAX\n"); +}; + // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: @@ -1544,6 +1658,16 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // `alloc(value)` builtin: heap-init a fresh *T with the + // value's bytes. For struct literals, lower to rt_alloc + // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL + // alloc path. + if (streq(callee.str, "alloc")) { + if (n.list != nil) { + cgalloc(c, n); + return; + }; + }; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 370865a1..78ece2cc 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -181,14 +181,21 @@ fn resolvewalk(c: *checker, n: *node) void = { }; // `match (e) { case let v: T => stmt; ... }` — the binding `v` - // is declared by the case arm and visible inside its body. + // is declared by the case arm and visible inside its body. Push a + // fresh scope so `case let e: str` doesn't collide with an outer + // `let e: *T` (scopedefine drops same-scope dupes silently and + // would leave references to `e` resolving to the outer type). + // Mirrors cmd/wcc/check.c's newscope/saved-restore around cstmt. if (k == nkind.N_MCASE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + let outer: *scope = c.cur; + c.cur = newscope(c.a, outer); let nm: str = n.str; if (nm.len > 0) { scopedefine(c.cur, nm, skind.SK_VAR, nil, n); }; if (n.body != nil) { resolvewalk(c, n.body); }; + c.cur = outer; return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index efc7ec43..391bf1b1 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -4215,14 +4215,21 @@ fn resolvewalk(c: *checker, n: *node) void = { }; // `match (e) { case let v: T => stmt; ... }` — the binding `v` - // is declared by the case arm and visible inside its body. + // is declared by the case arm and visible inside its body. Push a + // fresh scope so `case let e: str` doesn't collide with an outer + // `let e: *T` (scopedefine drops same-scope dupes silently and + // would leave references to `e` resolving to the outer type). + // Mirrors cmd/wcc/check.c's newscope/saved-restore around cstmt. if (k == nkind.N_MCASE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + let outer: *scope = c.cur; + c.cur = newscope(c.a, outer); let nm: str = n.str; if (nm.len > 0) { scopedefine(c.cur, nm, skind.SK_VAR, nil, n); }; if (n.body != nil) { resolvewalk(c, n.body); }; + c.cur = outer; return; }; @@ -6852,6 +6859,42 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutoff = lc.off; scrutt = resolvetype(c, lc.tnode); }; + } else { + // Non-ident scrutinee (call result, ?, etc.). Spill into a + // 24B `@match_spill` scratch slot and dispatch off it. + // Tagged returns follow the AX:DX:CX convention, so store + // all three words at +0/+8/+16; nullable returns are + // single-word (AX = ptr) and only read +0, so the extra + // stores are harmless. For N_CALL we recover the return + // type via fnretlookup so nullable dispatch can pick the + // pointer-vs-null discriminator. Mirrors C cgen's + // @match_spill path in cmd/w6c/cgen.c N_MATCH. + scrutoff = localalloc(c, "@match_spill", 24, nil); + if (scrut.kind == nkind.N_CALL) { + let callee: *node = scrut.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) { scrutt = resolvetype(c, rt); }; + }; + }; + }; + cgexpr(c, scrut); + emitline("\tMOVQ\tAX, "); + emitoff(scrutoff: i64); + emitline("(BP)\n"); + if (!isnullabletype(scrutt)) { + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; }; }; let endl: str = mklabel(c, "match_end"); @@ -6865,6 +6908,14 @@ fn cgmatch(c: *cgen, n: *node) void = { let nxt: str = mklabel(c, "match_next"); let pat: *node = cs.lhs; let nullable: bool = isnullabletype(scrutt); + // Per-arm scope: save c.locals before allocating the bind + // and restore after the body runs, so the arm's bind (and + // any nested lets) don't leak past the arm. Matches the + // checker's newscope/restore around N_MCASE. Without this, + // `let e: *T = ...; match (r) { case let e: str => ... }; + // use e` would resolve `e` after the match to the inner + // str slot instead of the outer ptr. + let arm_locals_saved: *local = c.locals; // Compute the variant tag for this arm. Default arm // (no pattern) skips the tag check. if (pat != nil) { @@ -6968,6 +7019,10 @@ fn cgmatch(c: *cgen, n: *node) void = { }; // Body. Match arms are statements; we cgstmt them. if (cs.body != nil) { cgstmt(c, cs.body); }; + // Restore the locals head — pop everything the arm pushed + // so post-match code resolves names to their original (outer) + // bindings. + c.locals = arm_locals_saved; emitline("\tJMP\t"); emitline(endl); emitline("\n"); @@ -7562,6 +7617,72 @@ fn cgbin(c: *cgen, n: *node) void = { return; }; +// cgalloc — `alloc(value)` builtin lowering. Allocate sizeof(value) +// bytes via rt_alloc, then write the value's bytes into the new +// region. For an N_STRUCTLIT arg, allocate the struct's totsize and +// emit per-field stores at each field's offset. For a scalar/ptr, +// allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's +// alloc-special branch in N_CALL. Returns the heap ptr in AX. +fn cgalloc(c: *cgen, n: *node) void = { + let v: *node = n.list; + let sz: i32 = 8; + let si: *structinfo = nil; + if (v.kind == nkind.N_STRUCTLIT) { + let trefn: *node = v.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (trefn != nil) { + if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; } + else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; }; + }; + si = structlookup(c, sname); + if (si != nil) { sz = si.totsize; }; + }; + emitline("\tMOVQ\t$"); + emitint(sz: i64); + emitline(", DI\n"); + emitline("\tCALL\trt_alloc(SB)\n"); + emitline("\tPUSHQ\tAX\n"); + if (v.kind == nkind.N_STRUCTLIT) { + if (si != nil) { + let f: *node = v.list; + for (f != nil) { + if (f.kind == nkind.N_FIELD) { + let fname: str = f.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + cgexpr(c, f.lhs); + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + f = f.next; + }; + }; + } else { + cgexpr(c, v); + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = "MOVQ"; + if (sz == 1) { sop = "MOVB"; } + else { if (sz == 4) { sop = "MOVL"; }; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + }; + emitline("\tPOPQ\tAX\n"); +}; + // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: @@ -7705,6 +7826,16 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // `alloc(value)` builtin: heap-init a fresh *T with the + // value's bytes. For struct literals, lower to rt_alloc + // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL + // alloc path. + if (streq(callee.str, "alloc")) { + if (n.list != nil) { + cgalloc(c, n); + return; + }; + }; }; }; @@ -9698,6 +9829,16 @@ fn scanlocals(c: *cgen, n: *node) i32 = { };}; }; }; + // `match (non-ident)` needs a 24B `@match_spill` scratch slot for + // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's + // localoff("@match_spill", ...). N_IDENT scrutinees read the slot + // directly off the local — no spill needed. + if (n.kind == nkind.N_MATCH) { + let sc: *node = n.lhs; + if (sc != nil) { + if (sc.kind != nkind.N_IDENT) { total += 24; }; + }; + }; // 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,