func bestMatch(forInputBitDepth bitDepth: Int) -> Int {
switch bitDepth { // ❌ Switch must be exhaustive
case ...8: 8
case 9...10: 10
case 11...: 12
}
}
It's amazing how often I hit this. Maybe it's just me, but this rudimentary reasoning about ranges pops up frequently.
This has been notedbefore. What I couldn't find in prior discussions is a great workaround for this limitation. The best I've seen mentioned so far is default: fatalError("This should be impossible but obviously isn't. 😔"). @scanon promises that fatalError will not actually make it out into the compiled binary. But, I'm a suspicious bugger, so I want to ensure it doesn't. Is there some similar construct I can use that will actually emit a compiler error if the default clause somehow makes it all the way through the optimiser and is about to be emitted into the final binary?
P.S. The phrasing of the compiler error in these cases is a bit antagonistic. The switch obviously is exhaustive, but the compiler is mistaking its own limitations for user error. Changing it to e.g. "Unable to tell if this switch is exhaustive - please rephrase it or add a default clause" would at least not raise my hackles as much every time I see it.
In the general case, though, there’s not a good way to do this (to my knowledge) because the optimizations that run early enough to still produce diagnostics don’t include integer range reasoning.
You can poorly do this, however, by declaring a function that doesn’t exist in a C header, and then calling it in your default case; if it’s not optimized away, you’ll get a linker error. Just be sure to only use that trick in optimized builds; in a debug build you should stick to fatalError (or preconditionFailure) instead.
EDIT: This should in no way be taken as “Swift can’t ever add explicit support for Ranges to the switch exhaustivity checker”; that’s just been discussed plenty in the previous threads.
Interesting approach, but upon a closer inspection you've just changed one switch with the unreachable default statement with two nested switches with the unreachable (false, false) condition.
Wonderful workaround! Do you say "poorly" just because this would compile/link in debug and fail to link in release? Expanding:
@inline(never) func foo(_ v: Bool) -> Int {
// this switch requiters default statament
switch v {
case false: 0x1234
case !false: 0x5678
default: unreachable()
}
}
//-> 0x100002468 <+0>: tst w0, #0x1
// 0x10000246c <+4>: mov w8, #0x1234
// 0x100002470 <+8>: mov w9, #0x5678
// 0x100002474 <+12>: csel x0, x9, x8, ne
// 0x100002478 <+16>: ret
func unreachable() -> Never {
#if DEBUG
preconditionFailure("unreachable code!")
#else
nonExistingFunction()
// void nonExistingFunction(void); in BridgingHeader
unreachable() // to make it truly unreachable
#endif
}
There could be pathological cases when DEBUG is defined with -O but other than those this looks perfect. Could we have it in the standard library?
It's reachable for floats! And it's a different sort of uncovered case, too, in that you can't reach it via typo. But yes, if you want the guarantee you'd have to do the same sort of trick (and also make the function inline-always).
No, I say "poorly" because you get a linker error, which, if you're lucky, tells you the filename of the uncovered switch, but not a line number or function name. (Okay, sometimes the "referenced from" line includes the mangled name of the function, which is slightly better.)
Also for RockPaperScissors kind of types and anything else that has a bogus Comparable conformance, but I was talking about the specific integer "func bestMatch(forInputBitDepth bitDepth: Int) -> Int" example.
Yep . I guess in that case you'd want to provide the missing symbol (with a trap in it), run the app and figure out where it traps but this is not good.
Indeed that's what I sometimes resort to. The downside is that it's less clear what the intent was, and that it opens the door to errors (in the face of future changes to the switch statement - not a big concern in a simple case like this, but as switch statements get larger the concern increases superlinearly).
Since Swift introduced me to exhaustivity checking, I've increasingly come to think of default clauses as bad. In my experience they often indicate laziness, lack of proper strong typing, or some other such code smell. They are of course unavoidable in some cases (e.g. open enums from 3rd party modules) - and genuinely warranted in some too - but I find it's wise to avoid them if possible. A conceptually trivial situation like this, just working with a number line, is not something that (in principle) should require default, IMO.
This is the sort of thing that makes me wish we had an assume and assumeUnchecked magic function. Or at the very least, unreachable and unreachableUnchecked like Rust does, so we can make such a function ourselves.