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:
@@ -145,6 +145,10 @@ static const struct row rows[] = {
|
||||
"fn parse() (invalid | i64 | overflow) = { return 0i64; }; "
|
||||
"fn caller() i32 = { let v: i64 = parse()?; return v: i32; };",
|
||||
"enclosing function must return a tagged union" },
|
||||
|
||||
/* nullable pointer folding accepts `(*T | void)` */
|
||||
{ "fn lookup(p: *i32) (*i32 | void) = { return p; };", "ok" },
|
||||
{ "fn lookup() (*i32 | void) = { return; };", "ok" }, /* bare return → null */
|
||||
};
|
||||
|
||||
int
|
||||
|
||||
@@ -538,6 +538,44 @@ static const struct row rows[] = {
|
||||
" };\n"
|
||||
" return 0;\n"
|
||||
"};", 3 },
|
||||
/* Nullable pointer folding: `(*T | void)` is one 8-byte word
|
||||
* where null = void variant. match/is/as/?/! all key off the
|
||||
* pointer-vs-null discriminator instead of a separate tag. */
|
||||
{ "fn lookup(p: *i32, b: bool) (*i32 | void) = {\n"
|
||||
" if (b) { return p; };\n"
|
||||
" return;\n"
|
||||
"};\n"
|
||||
"fn use_arg(r: (*i32 | void)) i32 = {\n"
|
||||
" match (r) {\n"
|
||||
" case let q: *i32 => return *q;\n"
|
||||
" case void => return 99;\n"
|
||||
" };\n"
|
||||
" return 0;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let x: i32 = 42;\n"
|
||||
" let ok: i32 = use_arg(lookup(&x, true));\n"
|
||||
" let no: i32 = use_arg(lookup(&x, false));\n"
|
||||
" if (ok != 42) { return 1; };\n"
|
||||
" if (no != 99) { return 2; };\n"
|
||||
" return 7;\n"
|
||||
"};", 7 },
|
||||
/* Nullable with is/as: discriminator is ptr-vs-null. */
|
||||
{ "fn lookup(p: *i32, b: bool) (*i32 | void) = {\n"
|
||||
" if (b) { return p; };\n"
|
||||
" return;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let x: i32 = 42;\n"
|
||||
" let r1: (*i32 | void) = lookup(&x, true);\n"
|
||||
" let r2: (*i32 | void) = lookup(&x, false);\n"
|
||||
" let acc: i32 = 0;\n"
|
||||
" if (r1 is *i32) { acc += 1; };\n"
|
||||
" if (r2 is void) { acc += 2; };\n"
|
||||
" let p: *i32 = r1 as *i32;\n"
|
||||
" if (*p == 42) { acc += 4; };\n"
|
||||
" return acc;\n"
|
||||
"};", 7 },
|
||||
/* `!`-flagged error variants: success picked by absence of `!`,
|
||||
* errors picked by presence. Tag remap still works across
|
||||
* different variant orders between operand and enclosing fn. */
|
||||
|
||||
Reference in New Issue
Block a user