wcc: tagged-union foundations (never, void, flatten, exhaust)

- `never` bottom type: TY_NEVER, assignable to anything; size 0.
- Type-set normalization for N_TTAGGED in resolve_type:
  - flatten nested anonymous (A|B)|C → (A|B|C); named aliases stay
    nominal (not flattened through)
  - dedup duplicates (NAMED pointer-id; others structural)
  - drop `never` variants
  - collapse single-element set: (T|never) → T, (T|T) → T
- Match exhaustiveness: error when a variant is unhandled and no
  default arm covers it. Multi-pattern `case T1 | T2 =>` counts
  each alt.
- (T | void) optionals: bare `return;` from a tagged-union-returning
  fn emits the void variant's tag (payload undefined; void size 0).

selfhost mirrored: TY_NEVER constant + tynever in tctx + seedprim
entry; voidvariantindex helper; cgreturn bare-return handling.
This commit is contained in:
2026-05-12 01:31:35 +09:00
parent 1ac1d985f6
commit fa070b6d07
11 changed files with 260 additions and 33 deletions

View File

@@ -3230,12 +3230,13 @@ def TY_NAMED: i32 = 24;
def TY_TUPLE: i32 = 25;
def TY_TAGGED: i32 = 26;
def TY_ERR: i32 = 27;
def TY_UNTYPED_INT: i32 = 28;
def TY_UNTYPED_FLOAT: i32 = 29;
def TY_UNTYPED_STR: i32 = 30;
def TY_UNTYPED_RUNE: i32 = 31;
def TY_UNTYPED_BOOL: i32 = 32;
def TY_UNTYPED_NIL: i32 = 33;
def TY_NEVER: i32 = 28;
def TY_UNTYPED_INT: i32 = 29;
def TY_UNTYPED_FLOAT: i32 = 30;
def TY_UNTYPED_STR: i32 = 31;
def TY_UNTYPED_RUNE: i32 = 32;
def TY_UNTYPED_BOOL: i32 = 33;
def TY_UNTYPED_NIL: i32 = 34;
// ---- tinfo / tfield / tparam -----------------------------------------
@@ -3288,6 +3289,7 @@ type tctx = struct {
tyf64: *tinfo,
tystr: *tinfo,
tyerr: *tinfo,
tynever: *tinfo,
tyuntypedint: *tinfo,
tyuntypedfloat: *tinfo,
tyuntypedstr: *tinfo,
@@ -3332,6 +3334,7 @@ export fn typesinit(c: *tctx, a: *arena) void = {
c.tyf64 = prim(a, TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(a, TY_STR, "str", 16u64, 8u64);
c.tyerr = prim(a, TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(a, TY_NEVER, "never", 0u64, 1u64);
c.tyuntypedint = prim(a, TY_UNTYPED_INT, "untyped_int", 0u64, 1u64);
c.tyuntypedfloat = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64);
@@ -3687,6 +3690,7 @@ fn seedprimitives(c: *checker) void = {
scopedefine(c.top, "f32", SK_TYPE, c.tc.tyf32, nil);
scopedefine(c.top, "f64", SK_TYPE, c.tc.tyf64, nil);
scopedefine(c.top, "str", SK_TYPE, c.tc.tystr, nil);
scopedefine(c.top, "never", SK_TYPE, c.tc.tynever, nil);
// `nil`, `true`, `false` are keywords — handled at the lex/parser
// level, no symbol needed.
// `len`, `alloc`, `free` are pseudo-builtins; scopedefine them so
@@ -4760,6 +4764,24 @@ fn istaggedtype(t: *node) bool = {
return false;
};
// 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.
fn voidvariantindex(tagged: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != N_TTAGGED) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, "void")) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// rhstargetname — for a returned value, what's its declared (or
// surface-inferred) type name? `expr: T` casts dictate T directly;
// bare strlit/intlit fall back to a primitive name.
@@ -6306,9 +6328,21 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
cgexpr(c, rhs);
} else {
// Bare `return;` in a void fn — zero AX so the caller
// sees a deterministic value (matches C cgen, which
// always falls through to `cgexpr_int(c, 0)`).
// Bare `return;` from a tagged-union-returning fn is
// 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");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
emitline("\tMOVQ\t$0, AX\n");
};
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).

View File

@@ -120,9 +120,21 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
cgexpr(c, rhs);
} else {
// Bare `return;` in a void fn — zero AX so the caller
// sees a deterministic value (matches C cgen, which
// always falls through to `cgexpr_int(c, 0)`).
// Bare `return;` from a tagged-union-returning fn is
// 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");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
emitline("\tMOVQ\t$0, AX\n");
};
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).

View File

@@ -876,6 +876,24 @@ fn istaggedtype(t: *node) bool = {
return false;
};
// 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.
fn voidvariantindex(tagged: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != N_TTAGGED) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, "void")) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// rhstargetname — for a returned value, what's its declared (or
// surface-inferred) type name? `expr: T` casts dictate T directly;
// bare strlit/intlit fall back to a primitive name.

View File

@@ -52,6 +52,7 @@ fn seedprimitives(c: *checker) void = {
scopedefine(c.top, "f32", SK_TYPE, c.tc.tyf32, nil);
scopedefine(c.top, "f64", SK_TYPE, c.tc.tyf64, nil);
scopedefine(c.top, "str", SK_TYPE, c.tc.tystr, nil);
scopedefine(c.top, "never", SK_TYPE, c.tc.tynever, nil);
// `nil`, `true`, `false` are keywords — handled at the lex/parser
// level, no symbol needed.
// `len`, `alloc`, `free` are pseudo-builtins; scopedefine them so

View File

@@ -3230,12 +3230,13 @@ def TY_NAMED: i32 = 24;
def TY_TUPLE: i32 = 25;
def TY_TAGGED: i32 = 26;
def TY_ERR: i32 = 27;
def TY_UNTYPED_INT: i32 = 28;
def TY_UNTYPED_FLOAT: i32 = 29;
def TY_UNTYPED_STR: i32 = 30;
def TY_UNTYPED_RUNE: i32 = 31;
def TY_UNTYPED_BOOL: i32 = 32;
def TY_UNTYPED_NIL: i32 = 33;
def TY_NEVER: i32 = 28;
def TY_UNTYPED_INT: i32 = 29;
def TY_UNTYPED_FLOAT: i32 = 30;
def TY_UNTYPED_STR: i32 = 31;
def TY_UNTYPED_RUNE: i32 = 32;
def TY_UNTYPED_BOOL: i32 = 33;
def TY_UNTYPED_NIL: i32 = 34;
// ---- tinfo / tfield / tparam -----------------------------------------
@@ -3288,6 +3289,7 @@ type tctx = struct {
tyf64: *tinfo,
tystr: *tinfo,
tyerr: *tinfo,
tynever: *tinfo,
tyuntypedint: *tinfo,
tyuntypedfloat: *tinfo,
tyuntypedstr: *tinfo,
@@ -3332,6 +3334,7 @@ export fn typesinit(c: *tctx, a: *arena) void = {
c.tyf64 = prim(a, TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(a, TY_STR, "str", 16u64, 8u64);
c.tyerr = prim(a, TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(a, TY_NEVER, "never", 0u64, 1u64);
c.tyuntypedint = prim(a, TY_UNTYPED_INT, "untyped_int", 0u64, 1u64);
c.tyuntypedfloat = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64);
@@ -3687,6 +3690,7 @@ fn seedprimitives(c: *checker) void = {
scopedefine(c.top, "f32", SK_TYPE, c.tc.tyf32, nil);
scopedefine(c.top, "f64", SK_TYPE, c.tc.tyf64, nil);
scopedefine(c.top, "str", SK_TYPE, c.tc.tystr, nil);
scopedefine(c.top, "never", SK_TYPE, c.tc.tynever, nil);
// `nil`, `true`, `false` are keywords — handled at the lex/parser
// level, no symbol needed.
// `len`, `alloc`, `free` are pseudo-builtins; scopedefine them so
@@ -4760,6 +4764,24 @@ fn istaggedtype(t: *node) bool = {
return false;
};
// 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.
fn voidvariantindex(tagged: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != N_TTAGGED) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, "void")) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// rhstargetname — for a returned value, what's its declared (or
// surface-inferred) type name? `expr: T` casts dictate T directly;
// bare strlit/intlit fall back to a primitive name.
@@ -6306,9 +6328,21 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
cgexpr(c, rhs);
} else {
// Bare `return;` in a void fn — zero AX so the caller
// sees a deterministic value (matches C cgen, which
// always falls through to `cgexpr_int(c, 0)`).
// Bare `return;` from a tagged-union-returning fn is
// 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");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
emitline("\tMOVQ\t$0, AX\n");
};
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).