wcc: case T => variant validity check (C + selfhost)

`match (u) { case T => ... }` where T isn't a variant of u was
silently accepted by both checkers. The cgen would emit a tag
comparison against an index that never appears, leaving the arm
unreachable — wasted code that's almost always a bug or typo.

C check.c now mirrors the existing is/as rule for match arms:
each `case T` and each alt of multi-pattern `case T1 | T2` is
checked against the scrutinee's variant list via variant_present.

selfhost check.ww gets the same shape with AST-level type_eq_ast
comparison. Both checks land in the same scope-aware pass that
already runs exhaustiveness and ? subset.

New test rows in 300_check (C side) and 950_selfcheck (selfhost
side) exercise both single-pattern and multi-pattern alt typos.
The 950 driver's err_present detector picks up the new
"case: not a variant" prefix.
This commit is contained in:
2026-05-12 03:37:21 +09:00
parent 68bd8197d5
commit 751271a6bd
6 changed files with 183 additions and 3 deletions

View File

@@ -149,6 +149,19 @@ static const struct row rows[] = {
/* nullable pointer folding accepts `(*T | void)` */
{ "fn lookup(p: *i32) (*i32 | void) = { return p; };", "ok" },
{ "fn lookup() (*i32 | void) = { return; };", "ok" }, /* bare return → null */
/* `case T` must name a variant of the scrutinee */
{ "fn pick() (i32 | str) = { return 1; }; "
"fn caller() void = { let v: (i32 | str) = pick(); "
"match (v) { case let n: i32 => { }; case let s: str => { }; "
"case let f: f64 => { }; }; };",
"case: f64 is not a variant" },
{ "fn pick() (i32 | str | bool) = { return 1; }; "
"fn caller() i32 = { let v: (i32 | str | bool) = pick(); "
"match (v) { case let n: i32 => return n; "
"case str | f64 => return 9; case let b: bool => return 1; }; "
"return 0; };",
"case: f64 is not a variant" },
};
int