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

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