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

@@ -4079,6 +4079,31 @@ fn err_match_variant(c: *checker, n: *node, vname: *node) void = {
c.errs += 1;
};
// case_variant_in — true iff `pat` (a `case T` pattern, including
// each alt of a multi-pattern) names a variant of the tagged
// union `tagged`.
fn case_variant_in(tagged: *node, pat: *node) bool = {
let v: *node = tagged.list;
for (v != nil) {
if (type_eq_ast(v, pat)) { return true; };
v = v.next;
};
return false;
};
fn err_bad_case_variant(c: *checker, pat: *node) void = {
os.write(2, "case: not a variant of scrutinee".ptr, 32u64);
if (pat != nil) {
if (pat.kind == N_TNAME) {
os.write(2, " (".ptr, 2u64);
os.write(2, pat.str.ptr, pat.str.len: u64);
os.write(2, ")".ptr, 1u64);
};
};
os.write(2, "\n".ptr, 1u64);
c.errs += 1;
};
fn check_match_exhaustive(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
@@ -4086,7 +4111,26 @@ fn check_match_exhaustive(c: *checker, n: *node) void = {
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
if (u.kind != N_TTAGGED) { return; };
// Default arm absorbs anything; skip.
// Validity: every `case T` pattern (and multi-pattern alts)
// must name a variant of u. Catches typos and dead arms that
// the dispatch would never reach.
let cs0: *node = n.list;
for (cs0 != nil) {
if (cs0.lhs != nil) {
if (!case_variant_in(u, cs0.lhs)) {
err_bad_case_variant(c, cs0.lhs);
};
let alt: *node = cs0.list;
for (alt != nil) {
if (!case_variant_in(u, alt)) {
err_bad_case_variant(c, alt);
};
alt = alt.next;
};
};
cs0 = cs0.next;
};
// Default arm absorbs anything; skip exhaustiveness.
let cs: *node = n.list;
for (cs != nil) {
if (cs.lhs == nil) { return; }; // default