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

@@ -2691,7 +2691,24 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
*
* Tagged-return ABI: AX=tag, DX=value0[, CX=value1]. CX is
* only meaningful when the union has a >8B variant (e.g.
* str, where ptr→DX and len→CX). */
* str, where ptr→DX and len→CX).
*
* Bare `return;` from a tagged-union-returning function: this
* is producing the void variant. Emit its tag; the payload is
* undefined (void has size 0). */
if (n->lhs == NULL && cg_ret_type) {
Type *rt = cg_ret_type;
if (rt->kind == TY_NAMED) rt = rt->under;
if (rt && rt->kind == TY_TAGGED) {
int tag = cg_tag_for_variant(rt, ty_void);
if (tag < 0) tag = 0;
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
}
if (n->lhs && cg_ret_type) {
Type *rt = cg_ret_type;
if (rt->kind == TY_NAMED) rt = rt->under;

View File

@@ -50,6 +50,7 @@ lookup_builtin(const char *name)
if (strcmp(name, "f32") == 0) return ty_f32;
if (strcmp(name, "f64") == 0) return ty_f64;
if (strcmp(name, "str") == 0) return ty_str;
if (strcmp(name, "never") == 0) return ty_never;
return NULL;
}
@@ -78,6 +79,26 @@ resolve_typename(Checker *c, Node *n)
return s->type;
}
/* Variant identity for tagged unions. Mirrors cg_variant_match in
* cgen: NAMED types are nominal (pointer-identical) and don't unify
* with their underlying; everything else is structural type_eq. */
static int
variant_match(Type *a, Type *b)
{
if (a == NULL || b == NULL) return 0;
if (a->kind == TY_NAMED && b->kind == TY_NAMED) return a == b;
if (a->kind == TY_NAMED || b->kind == TY_NAMED) return 0;
return type_eq(a, b);
}
static int
variant_present(Tparam *head, Type *vt)
{
for (Tparam *p = head; p; p = p->next)
if (variant_match(p->type, vt)) return 1;
return 0;
}
static Type *
resolve_type(Checker *c, Node *n)
{
@@ -123,19 +144,53 @@ resolve_type(Checker *c, Node *n)
return t;
}
case N_TTAGGED: {
/* (T1 | T2 | ...) — tag (8B) followed by the largest variant. */
/* (T1 | T2 | ...) — tag (8B) followed by the largest variant.
* Type-set normalization (Hare-style):
* - Flatten nested anonymous (A | B) | C → (A | B | C). Named
* aliases over tagged unions stay nominal — not flattened.
* - Drop `never`: bottom contributes no values.
* - Dedup variants. Equality follows cg_variant_match: NAMED
* types compare by pointer-identity, others structurally.
* - If exactly one variant remains, the tagged union collapses
* to that variant. (i32 | never) → i32.
* - If zero remain (all variants were `never`), the type is
* `never` itself. */
Type *t = newtype(c->a, TY_TAGGED);
Tparam *head = NULL, *tail = NULL;
u64 maxsz = 0, al = 8;
int nv = 0;
for (Node *e = n->list; e; e = e->next) {
Type *vt = resolve_type(c, e);
if (vt == ty_never) continue;
if (vt && vt->kind == TY_TAGGED) {
/* flatten anonymous nested tagged */
for (Tparam *src = vt->params; src; src = src->next) {
Type *st = src->type;
if (st == ty_never) continue;
if (variant_present(head, st)) continue;
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = st;
if (st && st->size > maxsz) maxsz = st->size;
if (st && st->align > al) al = st->align;
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
nv++;
}
continue;
}
if (variant_present(head, vt)) continue;
Tparam *tp = amalloc(c->a, sizeof *tp);
tp->type = resolve_type(c, e);
if (tp->type && tp->type->size > maxsz) maxsz = tp->type->size;
if (tp->type && tp->type->align > al) al = tp->type->align;
tp->type = vt;
if (vt && vt->size > maxsz) maxsz = vt->size;
if (vt && vt->align > al) al = vt->align;
if (head == NULL) head = tp;
else tail->next = tp;
tail = tp;
nv++;
}
if (nv == 0) return ty_never;
if (nv == 1 && head) return head->type;
t->params = head;
t->size = 8 + maxsz;
t->align = al;
@@ -728,6 +783,7 @@ cexpr(Checker *c, Node *n)
return n->type = err(c, n->pos,
"match on non-tagged-union %s", type_name(c->a, st));
}
int has_default = 0;
for (Node *cs = n->list; cs; cs = cs->next) {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
@@ -736,7 +792,9 @@ cexpr(Checker *c, Node *n)
* =>` get this — `case =>` (default) leaves cs->type NULL.
* For multi-pattern `case T1 | T2 =>` each alternative in
* cs->list also gets its type resolved in place. */
if (cs->lhs) {
if (cs->lhs == NULL) {
has_default = 1;
} else {
Type *vt = resolve_type(c, cs->lhs);
cs->type = vt;
for (Node *alt = cs->list; alt; alt = alt->next)
@@ -747,6 +805,31 @@ cexpr(Checker *c, Node *n)
cstmt(c, cs->body);
c->cur = saved;
}
/* Exhaustiveness: every variant must be handled. A default arm
* absorbs anything not otherwise covered. */
if (!has_default) {
for (Tparam *p = u->params; p; p = p->next) {
int covered = 0;
for (Node *cs = n->list; cs && !covered;
cs = cs->next) {
if (variant_match(cs->type, p->type)) {
covered = 1;
break;
}
for (Node *alt = cs->list; alt;
alt = alt->next)
if (variant_match(alt->type,
p->type)) {
covered = 1;
break;
}
}
if (!covered)
err(c, n->pos,
"match: variant %s not handled",
type_name(c->a, p->type));
}
}
n->type = ty_void;
return n->type;
}

View File

@@ -15,6 +15,7 @@ Type *ty_u8, *ty_u16, *ty_u32, *ty_u64;
Type *ty_int, *ty_uint, *ty_uintptr;
Type *ty_f32, *ty_f64, *ty_str;
Type *ty_err;
Type *ty_never;
Type *ty_untyped_int, *ty_untyped_float, *ty_untyped_str;
Type *ty_untyped_rune, *ty_untyped_bool, *ty_untyped_nil;
@@ -61,6 +62,7 @@ typesinit(Arena *a)
/* str is { *u8, len } — 16 bytes on amd64. ABI: pointer + u64. */
ty_str = prim(a, TY_STR, "str", 16, 8);
ty_err = prim(a, TY_ERR, "<err>", 0, 1);
ty_never = prim(a, TY_NEVER, "never", 0, 1);
ty_untyped_int = prim(a, TY_UNTYPED_INT, "untyped_int", 0, 1);
ty_untyped_float = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0, 1);
@@ -248,6 +250,7 @@ type_assignable(Type *dst, Type *src)
{
if (dst == NULL || src == NULL) return 0;
if (dst == ty_err || src == ty_err) return 1; /* swallow */
if (src == ty_never) return 1; /* bottom flows into anything */
if (type_eq(dst, src)) return 1;
/* Tagged-union variant inclusion: src is one of dst's variants.
@@ -327,6 +330,7 @@ type_name(Arena *a, Type *t)
case TY_F64: return "f64";
case TY_STR: return "str";
case TY_ERR: return "<err>";
case TY_NEVER: return "never";
case TY_UNTYPED_INT: return "untyped_int";
case TY_UNTYPED_FLOAT: return "untyped_float";
case TY_UNTYPED_STR: return "untyped_str";

View File

@@ -364,6 +364,7 @@ typedef enum {
TY_TUPLE,
TY_TAGGED, /* (T1 | T2 | ...) — Hare-style sum type */
TY_ERR,
TY_NEVER, /* bottom: assignable to anything; size 0 */
/* untyped constants (not surfaced to users; checker-internal) */
TY_UNTYPED_INT,
TY_UNTYPED_FLOAT,
@@ -408,6 +409,7 @@ extern Type *ty_u8, *ty_u16, *ty_u32, *ty_u64;
extern Type *ty_int, *ty_uint, *ty_uintptr;
extern Type *ty_f32, *ty_f64, *ty_str;
extern Type *ty_err;
extern Type *ty_never;
extern Type *ty_untyped_int, *ty_untyped_float, *ty_untyped_str;
extern Type *ty_untyped_rune, *ty_untyped_bool, *ty_untyped_nil;