Thanks Tagir for digging into this.  This is good analysis.

The decision to require completeness of enhanced switches was taken relatively late, so its quite possible we have left something out.  And in the next round, when nested patterns come along, we need to formalize totality and remainder, which also intersects with this problem.  So this is timely.



On 2/3/2022 12:41 AM, Tagir Valeev wrote:
Hello!

JLS requires (14.11.2) that the new "enhanced" switch statement (not
expressions) must be exhaustive. In particular, for the switch over
sealed abstract type, it's required to list all the permitted subtypes
or provide a total/default branch. This is good. However, this means
that from CFG point of view exactly one switch branch must always be
visited. This is indeed so if we take a look at the bytecode: the
synthetic default branch throws IncompatibleClassChangeError(). But
compiler ignores this fact and refuses to compile the code like this
(tried 18-ea+33-2077):

sealed interface Parent {}
record A() implements Parent {}
record B() implements Parent {}

void test(Parent p) {
     int x;
     switch (p) {
         case A a -> x = 1;
         case B b -> x = 2;
     }
     System.out.println(x); // error: variable x might not have been initialized
}

I think this is a mistake that should be corrected: in this code, `x`
should be considered as definitely assigned.

Agreed.  This is exactly why we have "remainder"; the set { A, B } is total on Parent, with remainder; the remainder includes all novel subclasses of Parent that were born after test(Parent) was compiled.  Our goal is that for a switch (or other pattern match construct) where the pattern(s) are total on the target, is that you cannot arrive at the statement after the switch without one of the branches being taken.  In the case of the remainder (e.g., a late-breaking C class), the switch will complete abruptly, so the correct characterization of your "exactly one" is that "if the switch completes normally, exactly one branch was visited."  Then we arrange for the switch to complete abruptly in case of remainder. (Remainder also includes things like Box(null) in a case Box(Bag(String s)); we had a long thread about the interesting shape of remainder sets last summer, and this topic will soon return, hopefully with the aid of more mathematical formalism.)

I understand that the same reasoning does not apply for switch over
enums, as for compatibility reasons, default behavior is to do
nothing. However, for patterns, uninitialized `x` cannot appear after
the switch, even if we recompile the sealed interface separately
adding one more inheritor.

Slight correction: *statement* switches over enums.  We've carved out a place where the only switches that are not total, are *statement* switches over the legacy types with the legacy switch labels.  In that case, DU analysis picks up some of the slack.

(It is still on our "to be considered" list whether it is worth it to allow statement switches to be explicitly marked as total to engage greater typechecking, or whether we want to embark on the decade-long path of warning increasingly loudly on non-total switches until we eventually make them illegal.)

I try to understand what's written in 16.2.9 regarding this [1].
Unfortunately, its current state looks confusing to me. Sorry if I'm
missing something, as I'm not very experienced in reading  chapter 16
of JLS. Nevertheless, it says:

V is [un]assigned after a switch statement (14.11) iff all of the
following are true:
...
Original spec:
- If there is no default label in the switch block, or if the switch
block ends with a switch label followed by the } separator, then V is
[un]assigned after the selector expression.
Preview spec:
- If the switch block covers the type of the selector expression, or
if the switch block ends with a switch label followed by the }
separator, then V is [un]assigned after the selector expression.

It looks strange that "no default label" (~= non-exhaustive) was
replaced with "covers the type" (= exhaustive). Was negation lost
somewhere? In current state it looks like, all exhaustive switches
(which is almost all switches), including ones with `default` branch
cannot definitely assign a variable, which contradicts the previous
state.

If negation is actually lost, then the sample above should compile, as
it's exhaustive.

I'll take a read-through of the spec (or more likely, Gavin will beat me to it) and respond more completely at that point.

Thanks,
-Brian

Reply via email to