lang: Treat optional wildcards as exhaustive

94e18aa54a4e4122f349196e9e5331ff17a56a5599235b6b3bf134d3ed49acd2
Optional match coverage only marked an unguarded else arm as a
catch-all. A wildcard pattern was lowered as a catch-all but still
failed semantic exhaustiveness checks.

Use the computed catch-all status for both else and wildcard arms
when validating optional matches.

Assisted-by: Codex:gpt-5.6-sol
Alexis Sellier committed ago 1 parent 168352d2
lib/std/lang/resolver.rad +6 -9
4456 4456
4457 4457
    for prongNode in prongs {
4458 4458
        let case ast::NodeValue::MatchProng(prong) = prongNode.value
4459 4459
            else panic "resolveMatchOptional: expected match prong";
4460 4460
4461 -
        // For optionals, a binding prong only covers the value case, not `nil`.
4462 -
        // Only `else` without a guard is a true catch-all.
4463 -
        if prong.arm == ast::ProngArm::Else and prong.guard == nil {
4464 -
            if catchAll {
4465 -
                throw emitError(self, prongNode, ErrorKind::DuplicateCatchAll);
4466 -
            }
4467 -
            set catchAll = true;
4468 -
        }
4469 -
4470 4461
        let mut isCatchAll = false;
4471 4462
        if prong.guard == nil {
4472 4463
            match prong.arm {
4473 4464
                case ast::ProngArm::Else => set isCatchAll = true,
4474 4465
                case ast::ProngArm::Case(patterns) => set isCatchAll = hasWildcardPattern(patterns),
4475 4466
                case ast::ProngArm::Binding(_) => {
4476 4467
                    // For optionals, a binding does *not* always match.
4477 4468
                }
4478 4469
            }
4479 4470
        }
4471 +
        if isCatchAll {
4472 +
            if catchAll {
4473 +
                throw emitError(self, prongNode, ErrorKind::DuplicateCatchAll);
4474 +
            }
4475 +
            set catchAll = true;
4476 +
        }
4480 4477
        setProngCatchAll(self, prongNode, isCatchAll);
4481 4478
        set matchType = try visitMatchProng(self, prongNode, prong, subjectTy, matchType, matchBy);
4482 4479
4483 4480
        // Track coverage. Guarded prongs don't count as covering a case.
4484 4481
        if prong.guard == nil {
test/tests/match.optional.wildcard.rad added +15 -0
1 +
//! returns: 0
2 +
//! An unguarded wildcard covers both cases of an optional value.
3 +
4 +
fn classify(value: ?i32) -> i32 {
5 +
    match value {
6 +
        case _ => return 7,
7 +
    }
8 +
}
9 +
10 +
@default fn main() -> i32 {
11 +
    if classify(nil) + classify(1) == 14 {
12 +
        return 0;
13 +
    }
14 +
    return 1;
15 +
}