Switch on Int with exhaustive cases still needs default?

If the cases of my switch on an Int cover every number from Int.min to Int.max, why does it still make me put a default?

For example:

let number = 5

switch number {
case Int.min ... 0:
    print("Too small")
case 1...100:
    print("Reasonable")
case 101 ... Int.max:
    print("Too big")
}

Even though there isn’t a value number could be that is not covered by these three cases, I still get “Switch must be exhaustive” (in a Swift Playground).

I know I could change the final case to default: or case _: to satisfy the compiler, but at the cost of expressiveness. Why doesn’t the compiler recognize that this switch is exhaustive?

1 Like

AFAIK, currently, there isn't exhaustive checking in place for types beyond enums & tuples (don't quote me on that).

6 Likes

That makes sense and is reasonable.

See [SR-766] Switch doesn't need to cover negative Int cases · Issue #43378 · apple/swift · GitHub

Unfortunately, integer operations like '...' and '<' are just plain functions to Swift, so it'd be difficult to do this kind of analysis. Even with special case understanding of integer intervals, I think there are still cases in the full generality of pattern matching for which exhaustiveness matching would be undecidable. We may eventually be able to handle some cases, but there will always be special cases involved in doing so.

and Bool :)

5 Likes

Huh. I’d never looked up the Bool definition. I’d always assumed it was an enum. Apparently it is a struct.

3 Likes

Bool used to be an enum in Swift’s early days. It was changed to a struct in late 2013.

6 Likes

Was hit with this gotcha just now. Nothing major, but feels like a bug.

There are always going to be patterns that the compiler cannot prove are exhaustive. I don't think our current limitations are unreasonable.

1 Like

I wonder if the compiler has improved to the point that Bool could be defined as an enum, ABI notwithstanding.

Very similar case:

func foo() {
    for i in 0 ... 1 {
        switch i { // Error: Switch must be exhaustive
        case 0: break
        case 1: break
        }
    }
}
2 Likes

There are always going to be patterns that the compiler cannot catch, but some patterns are trivial and expected to be caught.

I guess the question is where to draw the line between "trivial and should be automatic" and "compiler magic" (and for someone to implement it).

Then wouldn't it be better for the compiler error to ask for a default case? That would save some time for newbies, who will most likely spend time trying to be exhaustive before adding default case.

1 Like

Or the compiler could just do the "right thing" and put in a default case with an assertion. If the existing cases are truly exhaustive - no harm done; otherwise, for newbies, they get an assertion for a missing case.

enums I can understand. Can you give me an example in tuples?

Btw, I tried this...

//checking if switch is exhaustive
let x = 5
switch x {
case Int.min...Int.max:
    print("x=\(x)") 
}

...and it didn't compile.

func foobar(v: (Bool, Bool)) {
    switch v {
    case (false, false): print("false false")
    case (false, true): print("false true")
    case (true, false): print("true false")
    case (true, true): print("true true")
    // default clause is not needed, all possible cases are enumerated
    }
}

As mentioned above this is a known limitation. Also, merely putting things in a struct breaks exhaustiveness checks:

struct S: Equatable {
    let bool: Bool
}

func foobar(v: S) {
    switch v { // 🛑 Switch must be exhaustive
    case S(bool: false): print("false")
    case S(bool: true): print("true")
    }
}
1 Like

Bool exhaustiveness checks are not perfect either:

func foo(_ v: Bool) {
    let myfalse = false
    switch v {
        case false: print("false");
        case true: print("true")
        case myfalse: print("myfalse") // ✅ expected warning: "Case will never be executed"
    }
    switch v {
        case myfalse: print("myfalse")
        case false: print("false") // 👎 no warning here
        case true: print("true")
    }
    switch v {
        case false: print("false")
        case myfalse: print("myfalse") // 👎 no warning here
        case true: print("true")
    }
    switch v { // 👎 🛑 unexpected error: Switch must be exhaustive
        case myfalse: print("myfalse")
        case true: print("true")
    }
    switch v { // 🛑 unexpected error: Switch must be exhaustive
        case false: print("myfalse")
        case !false: print("true")
    }
}
1 Like