I think, this part requires clarification. Some interesting cases:

switch(boolValue) {case true:case false:case _:break;} -- is case _ a
compilation error? What if case _ is changed to default?
switch(boxedBoolValue) {case true:case false:case null:case _:break;}
-- the same question

So, boolean is slightly easier than enum, because it's a lot less likely that new boolean values will appear via separate compilation than with enums.  Though, it sometimes happens:

    https://thedailywtf.com/articles/What_Is_Truth_0x3f_

So let's start with enums.  Today, users often have to code a `default` branch even when all enums are covered.  This is annoying, and we'd like to give users some relief from having to include silly code, but the motivation goes deeper than that.  If you have a switch expression over an enum without a default, the compiler will tell you when you've left out a case.  But if you have a default, the compiler will happily let you cover N-1 of the cases.  So not only is requiring a silly default annoying to the user, it takes away the compiler's ability to type check.  So clearly we want users to be able to enter exhaustive switches (over booleans, enums, sealed types, and maybe even byte) without default.

That said, we probably still want to allow a default even when the switch is exhaustive, because perhaps the user wants to customize the handling of such problems with a more scrutable error than simply getting an ICCE.  So, even though `default` / case _ might be dead at compile time, it might not be dead at runtime.  (For boolean and byte, OK, it's dead.  But that's a special case.)

That's a different case (heh) than:

    case Integer i -> 0;
    case integer j -> 1;

The second arm is going to be dead no matter what.  The examples of dominance that I gave were primarily type-based, so they fall in the latter category.  We should reject the second arm.

So, I think the dominance story holds up, but we do need to adjust it to handle cases that are exhaustive at compile time but are not guaranteed to be so at run time (which is primarily restricted to enums and sealed types.)

(Also note that exhaustiveness checking can easily become a lifetime activity.  The easy cases are easy, but there's a long tail of whack-a-mole that probably ends in the halting problem.)


assuming enum Direction {UP, DOWN} and Box(Direction)
switch(box) {case Box(UP):case Box(DOWN):case Box b:break;} -- I
assume that case Box b is allowed here as it can match Box(null). Even
if Box(Direction) constructor requires that Direction is not null,
compiler cannot know this. Or can?

Currently can not.  But even if it could, we're back in enum territory, and its possible that a new direction, LEFT, shows up at runtime.  So a `Box` or `Box(var x)` or `_` case are acceptable here.  But I think this derives from the fact that enums are inherently more fungible than booleans?

So, you're really asking two questions:
 - Under what conditions do we require a catch-all case?
 - Under what conditions do we allow a catch-all case, even if the switch seems exhaustive?

For the first, we require a catch-all if (a) the switch is an expression switch and (b) we cannot prove that the cases are exhaustive _relative to compile-time type knowledge_.  So if you switch on enums:

    case (trafficLightColor) {
        case RED:
        case YELLOW:
        case GREEN:
    }

you're good, even though a new color could come along later.  The compiler will insert a catch-all case here, throwing ICCE.  And there's no null possible, since none of the cases are nullable, so the switch will throw on null.

Raising it up a level:

    case (box) { // assume Box<TrafficLightColor>
        case Box(RED): ...
        case Box(YELLOW): ...
        case Box(GREEN): ...
    }

we can detect at compile time that we haven't handled Box(null), though it seems a bit mean to require a default here.  So we may want to treat null specially here -- let's think about that.

For the latter question, I think when we are reasoning about exhaustiveness in a brittle way (enums and sealed classes), we want to allow a catch-all case even if it seems dead at compile time. But if we're reasoning about exhaustiveness through types, then a `default` or `_` case might be truly dead, and we should reject it.


Also what about combinations of several fields? Assuming
TwoFlags(boolean, boolean):
switch(twoFlags) {case TwoFlags(true, _):case TwoFlags(_, true):case
TwoFlags(false, false):case TwoFlags t:break;} -- here all
combinations are covered by first three patterns, so TwoFlags t cannot
match anything. Will it be reported? What if we have ten enum fields?
Will compiler track all covered value combinations and issue an error
if it's statically known that some pattern is unreachable? I'm not
sure about computational complexity of such tracking in common case.

This is starting down that slippery slope of "lifetime activity" I was referring to :)

Reply via email to