selfhost: mirror nullable pointer folding for (*T | void)

C-cgen-side nullable folding landed in f4efaac. This commit catches
the selfhost cgen up so a wwstage-compiled binary produces the same
ABI for `(*T | void)`.

- cgenutil: isnullabletype(), nullableptrtag() helpers shaped to
  the selfhost cgen's AST-only world view (it doesn't carry a Type
  with a .nullable flag — it walks N_TTAGGED node lists). slotsize
  returns 8 for nullable.
- cgenexpr cgmatch: nullable arm uses pointer-vs-null discriminator
  and binds only the *T case (void has size 0).
- cgenstmt cglet: nullable target spills only AX (no tag word, no
  value-word DX/CX).
- cgenstmt cgreturn: nullable return passes AX through with no
  shuffle; bare `return;` emits AX = 0 (void encoding).

Verified end-to-end: a fn returning `(*i32 | void)` compiled by the
wwstage cgen produces the same exit code as the C-cgen build. The
995 fixed-point gate stays green — no selfhost source uses nullable
yet, so the existing tagged paths are still byte-identical.
This commit is contained in:
2026-05-12 02:57:47 +09:00
parent f4efaac144
commit 67e27589fd
5 changed files with 438 additions and 150 deletions

View File

@@ -4677,6 +4677,8 @@ fn slotsize(c: *cgen, typn: *node) i32 = {
return total;
};
if (k == N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
@@ -4864,6 +4866,38 @@ fn istaggedtype(t: *node) bool = {
return false;
};
// isnullabletype — N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == N_TPTR);
let bptr: bool = (b.kind == N_TPTR);
let avoid: bool = (a.kind == N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != N_TTAGGED) { return 0; };
let a: *node = t.list;
if (a != nil) { if (a.kind == N_TPTR) { return 0; }; };
return 1;
};
// voidvariantindex — find the 0-based index of the `void` variant in a
// tagged-union type expr, -1 if absent. Used by cgreturn to map bare
// `return;` in a tagged-union-returning fn to the void variant's tag.
@@ -5389,68 +5423,105 @@ fn cgmatch(c: *cgen, n: *node) void = {
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
let pat: *node = cs.lhs;
let nullable: bool = isnullabletype(scrutt);
// Compute the variant tag for this arm. Default arm
// (no pattern) skips the tag check.
if (pat != nil) {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
if (nullable) {
// Discriminator = pointer-vs-null.
// *T arm: skip if ptr == 0.
// void arm: skip if ptr != 0.
let ptr_tag: i32 = nullableptrtag(scrutt);
let cur_tag: i32 = 0;
if (pat.kind == N_TPTR) { cur_tag = ptr_tag; }
else { if (ptr_tag == 0) { cur_tag = 1; }; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$0, AX\n");
if (cur_tag == ptr_tag) {
emitline("\tJE\t");
} else {
emitline("\tJNE\t");
};
emitline(nxt);
emitline("\n");
} else {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
if (!found) { want = 0; };
};
if (!found) { want = 0; };
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
// Bind `let v: T` from the slot, if requested.
let bn: str = cs.str;
if (bn.len > 0) {
if (pat != nil) {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
if (nullable) {
// Bind the pointer (or skip for the
// void arm, which has zero-size). The
// value IS slot+0.
if (pat.kind == N_TPTR) {
let voff: i32 = localalloc(c, bn, 8, pat);
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
};
} else {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitline("(BP)\n");
};
};
};
};
@@ -6509,8 +6580,17 @@ fn cgreturn(c: *cgen, n: *node) void = {
// For str variant, cgexpr leaves (AX=ptr, BX=len), so we
// shuffle DX←AX (ptr) and CX←BX (len), then load tag.
// For other variants, cgexpr leaves AX, shuffle DX←AX.
// Nullable folded `(*T | void)`: just one word; AX is
// already the pointer (or 0). No shuffle, no tag.
if (istaggedtype(c.fnret)) {
cgexpr(c, rhs);
if (isnullabletype(c.fnret)) {
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
@@ -6534,11 +6614,16 @@ fn cgreturn(c: *cgen, n: *node) void = {
// the void variant: emit its tag. Payload is undefined
// (void has size 0). Otherwise zero AX for determinism.
if (istaggedtype(c.fnret)) {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
if (isnullabletype(c.fnret)) {
// null = void variant; AX = 0.
emitline("\tMOVQ\t$0, AX\n");
} else {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -6578,6 +6663,7 @@ fn cglet(c: *cgen, n: *node) void = {
// - Otherwise rhs is a bare variant value: pack tag +
// value(s).
if (istaggedtype(n.lhs)) {
let nullable: bool = isnullabletype(n.lhs);
let rhsreturnstagged: bool = false;
if (rhs.kind == N_CALL) {
let callee: *node = rhs.lhs;
@@ -6593,6 +6679,16 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
cgexpr(c, rhs);
if (nullable) {
// Slot is one 8B word; AX is the pointer
// (or 0 for null/void). Same path whether
// the rhs is a call or a bare variant.
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
if (rhsreturnstagged) {
// Spill size/8 registers (tag + value words).
// Slots smaller than 24 don't carry a CX word.

View File

@@ -442,68 +442,105 @@ fn cgmatch(c: *cgen, n: *node) void = {
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
let pat: *node = cs.lhs;
let nullable: bool = isnullabletype(scrutt);
// Compute the variant tag for this arm. Default arm
// (no pattern) skips the tag check.
if (pat != nil) {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
if (nullable) {
// Discriminator = pointer-vs-null.
// *T arm: skip if ptr == 0.
// void arm: skip if ptr != 0.
let ptr_tag: i32 = nullableptrtag(scrutt);
let cur_tag: i32 = 0;
if (pat.kind == N_TPTR) { cur_tag = ptr_tag; }
else { if (ptr_tag == 0) { cur_tag = 1; }; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$0, AX\n");
if (cur_tag == ptr_tag) {
emitline("\tJE\t");
} else {
emitline("\tJNE\t");
};
emitline(nxt);
emitline("\n");
} else {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
if (!found) { want = 0; };
};
if (!found) { want = 0; };
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
// Bind `let v: T` from the slot, if requested.
let bn: str = cs.str;
if (bn.len > 0) {
if (pat != nil) {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
if (nullable) {
// Bind the pointer (or skip for the
// void arm, which has zero-size). The
// value IS slot+0.
if (pat.kind == N_TPTR) {
let voff: i32 = localalloc(c, bn, 8, pat);
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
};
} else {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitline("(BP)\n");
};
};
};
};

View File

@@ -99,8 +99,17 @@ fn cgreturn(c: *cgen, n: *node) void = {
// For str variant, cgexpr leaves (AX=ptr, BX=len), so we
// shuffle DX←AX (ptr) and CX←BX (len), then load tag.
// For other variants, cgexpr leaves AX, shuffle DX←AX.
// Nullable folded `(*T | void)`: just one word; AX is
// already the pointer (or 0). No shuffle, no tag.
if (istaggedtype(c.fnret)) {
cgexpr(c, rhs);
if (isnullabletype(c.fnret)) {
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
@@ -124,11 +133,16 @@ fn cgreturn(c: *cgen, n: *node) void = {
// the void variant: emit its tag. Payload is undefined
// (void has size 0). Otherwise zero AX for determinism.
if (istaggedtype(c.fnret)) {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
if (isnullabletype(c.fnret)) {
// null = void variant; AX = 0.
emitline("\tMOVQ\t$0, AX\n");
} else {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -168,6 +182,7 @@ fn cglet(c: *cgen, n: *node) void = {
// - Otherwise rhs is a bare variant value: pack tag +
// value(s).
if (istaggedtype(n.lhs)) {
let nullable: bool = isnullabletype(n.lhs);
let rhsreturnstagged: bool = false;
if (rhs.kind == N_CALL) {
let callee: *node = rhs.lhs;
@@ -183,6 +198,16 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
cgexpr(c, rhs);
if (nullable) {
// Slot is one 8B word; AX is the pointer
// (or 0 for null/void). Same path whether
// the rhs is a call or a bare variant.
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
if (rhsreturnstagged) {
// Spill size/8 registers (tag + value words).
// Slots smaller than 24 don't carry a CX word.

View File

@@ -703,6 +703,8 @@ fn slotsize(c: *cgen, typn: *node) i32 = {
return total;
};
if (k == N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
@@ -890,6 +892,38 @@ fn istaggedtype(t: *node) bool = {
return false;
};
// isnullabletype — N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == N_TPTR);
let bptr: bool = (b.kind == N_TPTR);
let avoid: bool = (a.kind == N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != N_TTAGGED) { return 0; };
let a: *node = t.list;
if (a != nil) { if (a.kind == N_TPTR) { return 0; }; };
return 1;
};
// voidvariantindex — find the 0-based index of the `void` variant in a
// tagged-union type expr, -1 if absent. Used by cgreturn to map bare
// `return;` in a tagged-union-returning fn to the void variant's tag.

View File

@@ -4677,6 +4677,8 @@ fn slotsize(c: *cgen, typn: *node) i32 = {
return total;
};
if (k == N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
@@ -4864,6 +4866,38 @@ fn istaggedtype(t: *node) bool = {
return false;
};
// isnullabletype — N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == N_TPTR);
let bptr: bool = (b.kind == N_TPTR);
let avoid: bool = (a.kind == N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != N_TTAGGED) { return 0; };
let a: *node = t.list;
if (a != nil) { if (a.kind == N_TPTR) { return 0; }; };
return 1;
};
// voidvariantindex — find the 0-based index of the `void` variant in a
// tagged-union type expr, -1 if absent. Used by cgreturn to map bare
// `return;` in a tagged-union-returning fn to the void variant's tag.
@@ -5389,68 +5423,105 @@ fn cgmatch(c: *cgen, n: *node) void = {
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
let pat: *node = cs.lhs;
let nullable: bool = isnullabletype(scrutt);
// Compute the variant tag for this arm. Default arm
// (no pattern) skips the tag check.
if (pat != nil) {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
if (nullable) {
// Discriminator = pointer-vs-null.
// *T arm: skip if ptr == 0.
// void arm: skip if ptr != 0.
let ptr_tag: i32 = nullableptrtag(scrutt);
let cur_tag: i32 = 0;
if (pat.kind == N_TPTR) { cur_tag = ptr_tag; }
else { if (ptr_tag == 0) { cur_tag = 1; }; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$0, AX\n");
if (cur_tag == ptr_tag) {
emitline("\tJE\t");
} else {
emitline("\tJNE\t");
};
emitline(nxt);
emitline("\n");
} else {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
if (!found) { want = 0; };
};
if (!found) { want = 0; };
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
// Bind `let v: T` from the slot, if requested.
let bn: str = cs.str;
if (bn.len > 0) {
if (pat != nil) {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
if (nullable) {
// Bind the pointer (or skip for the
// void arm, which has zero-size). The
// value IS slot+0.
if (pat.kind == N_TPTR) {
let voff: i32 = localalloc(c, bn, 8, pat);
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
};
} else {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitline("(BP)\n");
};
};
};
};
@@ -6509,8 +6580,17 @@ fn cgreturn(c: *cgen, n: *node) void = {
// For str variant, cgexpr leaves (AX=ptr, BX=len), so we
// shuffle DX←AX (ptr) and CX←BX (len), then load tag.
// For other variants, cgexpr leaves AX, shuffle DX←AX.
// Nullable folded `(*T | void)`: just one word; AX is
// already the pointer (or 0). No shuffle, no tag.
if (istaggedtype(c.fnret)) {
cgexpr(c, rhs);
if (isnullabletype(c.fnret)) {
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
@@ -6534,11 +6614,16 @@ fn cgreturn(c: *cgen, n: *node) void = {
// the void variant: emit its tag. Payload is undefined
// (void has size 0). Otherwise zero AX for determinism.
if (istaggedtype(c.fnret)) {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
if (isnullabletype(c.fnret)) {
// null = void variant; AX = 0.
emitline("\tMOVQ\t$0, AX\n");
} else {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -6578,6 +6663,7 @@ fn cglet(c: *cgen, n: *node) void = {
// - Otherwise rhs is a bare variant value: pack tag +
// value(s).
if (istaggedtype(n.lhs)) {
let nullable: bool = isnullabletype(n.lhs);
let rhsreturnstagged: bool = false;
if (rhs.kind == N_CALL) {
let callee: *node = rhs.lhs;
@@ -6593,6 +6679,16 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
cgexpr(c, rhs);
if (nullable) {
// Slot is one 8B word; AX is the pointer
// (or 0 for null/void). Same path whether
// the rhs is a call or a bare variant.
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
if (rhsreturnstagged) {
// Spill size/8 registers (tag + value words).
// Slots smaller than 24 don't carry a CX word.