wcc: nullable pointer folding for (*T | void)

A tagged union with exactly one `*T` variant and one `void` variant
collapses to a single 8-byte pointer slot, where the null bit
pattern is the void variant and any non-null is the *T variant.
Mirrors Hare's `(*T | null)` ABI optimisation.

Detected in resolve_type when the post-flatten variant list has
exactly two entries of the right shape; Type.nullable = 1 and
size = 8. Codegen branches every tagged-handling site on the flag:

- match: discriminator = pointer-vs-zero, not slot+0 tag word.
  Binding for the *T case copies the same word (the pointer itself)
  rather than slot+8.
- is/as: same ptr-vs-zero discriminator.
- ?: null = error (propagate AX=0 to caller's matching null
  encoding); non-null = success (AX is already the pointer).
- !: null aborts; non-null falls through with AX = pointer.
- let-init / return: spill or set just AX (no tag/value pair).
- call-arg push: push only AX, not the now-unused DX/CX.

Prologue spill already pulled size/8 = 1 arg register via the
existing tagged-arg loop, so no change needed there.

Two existing helpers in cgen.c get nullable-aware spelling:
type_isnullable() and nullable_ptr_tag() (which variant index is
the *T side; the void side is the other one).

The Hare-style `(*T | null)` spelling isn't supported — `null` is
not a type keyword in ww. Callers use `void` instead, which is
already a real type. The result is the same bit-level layout.
This commit is contained in:
2026-05-12 02:53:47 +09:00
parent dc8405429e
commit f4efaac144
5 changed files with 262 additions and 42 deletions

View File

@@ -247,6 +247,28 @@ resolve_type(Checker *c, Node *n)
if (nv == 0) return ty_never;
if (nv == 1 && head) return head->type;
t->params = head;
/* Nullable pointer folding: `(*T | void)` collapses to a
* single 8-byte pointer slot; null bit pattern is the void
* variant. Mirrors Hare's `(*T | null)`. Detected on exact
* two-variant shape with one TY_PTR and one TY_VOID. */
if (nv == 2) {
Tparam *a = head;
Tparam *b = head->next;
Type *au = (a->type && a->type->kind == TY_NAMED)
? a->type->under : a->type;
Type *bu = (b->type && b->type->kind == TY_NAMED)
? b->type->under : b->type;
int aptr = au && au->kind == TY_PTR;
int bptr = bu && bu->kind == TY_PTR;
int avoid = au && au->kind == TY_VOID;
int bvoid = bu && bu->kind == TY_VOID;
if ((aptr && bvoid) || (avoid && bptr)) {
t->nullable = 1;
t->size = 8;
t->align = 8;
return t;
}
}
/* Round value payload up to an 8-byte multiple so the slot
* layout (tag + N value words) stays word-aligned. The reg-
* passing ABI counts size/8 words; 12-byte unions like