From fa070b6d07194870a474b0b34369da76805fe9c5 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 01:31:35 +0900 Subject: [PATCH] wcc: tagged-union foundations (never, void, flatten, exhaust) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `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. --- cmd/w6c/cgen.c | 19 +++++- cmd/wcc/check.c | 93 ++++++++++++++++++++++++++-- cmd/wcc/type.c | 4 ++ cmd/wcc/ww.h | 2 + lib/ww/typ.ww | 15 +++-- selfhost/cmd/w6c/main.combined.ww | 52 +++++++++++++--- selfhost/cmd/wcc/cgenstmt.ww | 18 +++++- selfhost/cmd/wcc/cgenutil.ww | 18 ++++++ selfhost/cmd/wcc/check.ww | 1 + selfhost/cmd/wwdump/main.combined.ww | 52 +++++++++++++--- test/wcc/300_check.c | 19 ++++++ 11 files changed, 260 insertions(+), 33 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 997c4267..e7883664 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index f098b85f..d9e6d41c 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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; } diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index 76402cd4..8c66ff5f 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -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, "", 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 ""; + 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"; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index d81ee5f3..394660c6 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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; diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index f5ba9f46..52003923 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -42,12 +42,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 ----------------------------------------- @@ -100,6 +101,7 @@ type tctx = struct { tyf64: *tinfo, tystr: *tinfo, tyerr: *tinfo, + tynever: *tinfo, tyuntypedint: *tinfo, tyuntypedfloat: *tinfo, tyuntypedstr: *tinfo, @@ -144,6 +146,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, "", 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); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index bc943174..43aac02a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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, "", 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). diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 1ed6ac25..2df61457 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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). diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 742f2648..d32217a5 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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. diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 4efb48a7..53beb22b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9e6fe859..9af650bf 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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, "", 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). diff --git a/test/wcc/300_check.c b/test/wcc/300_check.c index c3f09427..87d87c76 100644 --- a/test/wcc/300_check.c +++ b/test/wcc/300_check.c @@ -105,6 +105,25 @@ static const struct row rows[] = { { "fn f(a: i32) i32 = { return a; }; fn g() i32 = { return f(); };", "not enough arguments" }, { "fn f() void = { if (1) { }; };", "if condition" }, + + /* tagged-union normalization & exhaustiveness */ + { "fn f() (i32 | never) = { return 7; }; " + "fn g() i32 = { return f(); };", "ok" }, /* (T|never) → T */ + { "fn f() (i32 | i32) = { return 5; }; " + "fn g() i32 = { return f(); };", "ok" }, /* dedup → i32 */ + { "fn f() (i32 | void) = { return; };", "ok" }, /* bare return → void variant */ + { "fn f(b: bool) (i32 | void) = { if (b) { return 1; }; return; };", + "ok" }, + { "fn f() (i32 | str | bool) = { return 1; }; " + "fn g() void = { let v: (i32 | str | bool) = f(); " + "match (v) { case let x: i32 => { }; case let x: str => { }; }; };", + "variant bool not handled" }, + { "fn f() (i32 | str | bool) = { return 1; }; " + "fn g() void = { let v: (i32 | str | bool) = f(); " + "match (v) { case let x: i32 => { }; case => { }; }; };", + "ok" }, /* default arm absorbs missing variants */ + { "fn die() never = { for (true) { let _: i32 = 1; }; }; " + "fn f() i32 = { die(); };", "ok" }, /* never assignable to anything */ }; int