w6c+selfhost: match-arm scope/spill + alloc(structlit) sugar

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.
This commit is contained in:
2026-05-12 18:40:41 +09:00
parent 67eaa9796a
commit 73c0cf4c78
6 changed files with 463 additions and 6 deletions

View File

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

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

@@ -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,