RFR: 8291769: Translation of switch with record patterns could be improved

Jan Lahoda jan.lahoda at oracle.com
Thu Aug 4 16:35:18 UTC 2022


On 04. 08. 22 17:43, Remi Forax wrote:

>
> ----- Original Message -----
>> From: "Jan Lahoda" <jlahoda at openjdk.org>
>> To: compiler-dev at openjdk.org
>> Sent: Thursday, August 4, 2022 5:04:52 PM
>> Subject: RFR: 8291769: Translation of switch with record patterns could be improved
>> This is an attempt to improve the performance and scalability of switches with
>> record patterns.
>>
>> There are two main parts of this patch:
>> 1. for cases of consecutive runs of cases with the same record pattern, these
>> are replaced with a single case and a nested switch. E.g.:
>>
>> switch (obj) {
>>      case Box(String s) -> {}
>>      case Box(Integer i) -> {}
>>      case Box(Number n) -> {}
>>      ...
>> }
>> =>
>> switch (obj) {
>>      case Box b ->
>>           switch (b.o()) {
>>               case String s -> {}
>>               case Integer i -> {}
>>               case Number n -> {}
>>               default -> continue-with-outer-switch;
>>           };
>>     ...
>> }
>>
>>
>> This is done by first unrolling the record patterns into simple binding patterns
>> a guards, and then by finding cases with the common binding pattern as a label
>> and a viable guard. Binding patterns are reused as much as possibly,
>> eliminating specialized handling of record patterns as much as possible.
> How it works when you have a record pattern followed by a type pattern with the same type ?
> Something like:
>
>    switch (obj) {
>      case Box(String s) -> {}
>      case Box(Integer i) -> {}
>      
>      case Box b -> {}
>    }
>
> because you now have twice the same type pattern on the outer switch but the first one should not NPE.


Currently the `case Box b -> {}` is kept separate. It would be possible 
to merge (it would simply be a default in the nested switch), but it is 
an additional complexity in a fairly complex code already.


>
>
>> 2. When a record accessor method fails with an exception, we need to wrap this
>> exception with a `MatchException`. Currently this is being done by introducing
>> a special proxy method which catches the exception and re-throws. The proposed
>> patch here eliminates the need for the accessor methods by producing an
>> appropriate `ExceptionTable` in the classfile, and a separate catch handler.
>> This handler is attached to the innermost usable block, which is either the
>> method block, lambda body, (static or non-static) initializer or a try block.
>> This should ensure correct semantics, while not producing too many unnecessary
>> catch handlers.
> Yes !
>
> As said above, the semantics for null is slightly changed, because now you may throw a NPE instead of a MatchException.


The idea here is that the semantics should not be changed, and changes 
to semantics are bugs. But thanks to your comment, I realized null 
record components are not handled properly, so I've attempted to fix that:

https://github.com/openjdk/jdk/pull/9746/commits/7ddeafab1d1ea23285e60765ec0e61d7a509e442


> You can add explicit nullchecks to throw a MatchException instead but i think we can do better for our users.
>
> I believe that with this transformation, we can now make the difference between the 3 kinds of errors,
> - the call to the accessor / destructor fails throwing an error similar to ExceptionInTheInitializerError (ExceptionInTheDestructorError ?)
> - the tested value is null throwing a NPE because most of them are implicit when b.o() is called. This means that the compiler has to insert a requireNonNull if the nullcheck is not implicit.
> - when there are more permitted subtypes at runtime throwing an IncompatibleClassChangeError (like with an switch expression on an enum)


(FWIW, I believe the spec is saying what should happen in specific 
cases, so I don't think this is something javac should decide on its 
own. javac can decide on how the behavior is implemented, but not the 
behavior.)


>
> for the last one, the idea is that if there is a sealed type, instead of generating an if instanceof for all cases, the last one should not be an instanceof but just a cast with a cacth handler that catch the ClassCastException and throw an IncompatibleClassChangeError wrapping the CCE instead.


Note that when switching over a type we don't generate instanceof for 
the subtypes of that type. The typeSwitch bootstrap/indy does the 
checks, and the bytecode produced by javac only produces checkcasts. (It 
may produce instanceofs for the nested patterns, though.) It would be 
possible to use a default instead of the last case, and add catch 
handler for the checkcast, but generating cases for all the checked 
types, and adding a default which throws (which I believe is the current 
implementation) feels much easier and cleaner to implement, and I don't 
really see a significant disadvantage in that.


Thanks,

     Jan


>
> Rémi


More information about the compiler-dev mailing list