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

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