Following the discussions we had, i've implemented a prototype of what can be 
the runtime part of the pattern matching that supports record pattern.

It works in 3 steps:
Step 1, at compile time, the compiler takes all the patterns and creates a tree 
of pattern from the list of patterns,
pattern that starts with the same prefix are merged together.
In the end, the tree of patterns is encoded in the bytecode as a tree of 
constant dynamic (each Pattern is created only from constant and patterns).

  sealed interface Pattern {}
  record NullPattern() implements Pattern {}
  record ConstantPattern(Object constant) implements Pattern {}
  record TypePattern(Class<?> type) implements Pattern {}
  record RecordPattern(Class<?> recordClass, Pattern... patterns) implements 
Pattern {}
  
  record OrPattern(Pattern pattern1, Pattern pattern2) implements Pattern {}
  record ResultPattern(int index, Pattern pattern) implements Pattern {} 

The last two patterns are less obvious, the OrPattern allows to create the tree 
by saying that a pattern should be executed before another one.
Because the patterns are organized as a tree and not a list, we need a way to 
associate which branch of the tree correspond to which pattern from the use 
POV, the ResultPattern encodes in the carrier the index (the first pattern is 
0, the second one is 1, etc) inside the first component of the carrier.

I've chosen to not represent the bindings (and their corresponding the 
component index in the carrier) and to use the fact that binding slot are 
numbered in the order of the tree traversal.
This is maybe too brittle because the compiler and the runtime has to agree on 
the order of the traversal. But this avoids to encode too many information in 
the bytecode.

Step 2, 
at runtime, the first call to invokedynamic with the pattern tree as arguments, 
creates a tree of method handles that will match the pattern.
During that phase, the runtime environment can be checked to see if the pattern 
tree is invalid with the runtime classes, in that case a linkage error is 
thrown.
In the prototype the method is called toMatcher and takes a Lookup (to find the 
accessors of the record of the RecordPattern), a receiverClass the type of the 
variable switch upon,
the carrier type (a method type as a tuple of the type of the binding in the 
tree traversal order), the index of the first binding (in case of a switch the 
first component in the matching index so the binding slots starts at 1) and 
method handle (the null matcher) to call if a null is found (the two possible 
semantics are doNotMatch/return null or throw a NPE).

  pattern.toMatcher(lookup, receiverClass, carrierType, firstBinding (0 or 1), 
nullMatcher);

Step 3,
during the execution,
- if it's an instanceof a carrier which is null means no match otherwise the 
carrier contains the value of the bindings,
- if it's a switch, a carrier which is null means "default" otherwise the 
component 0 or the carrier contains the index of the pattern matched and the 
binding values
- if it's an assignment, the carrier can not be null because the nullMatcher 
throws a NPE earlier, so the carrier contains the binding values

An example of instanceof
https://github.com/forax/switch-carrier/blob/master/src/main/java/com/github/forax/carrier/java/lang/runtime/InstanceOfExamples.java#L15

An example of switch
https://github.com/forax/switch-carrier/blob/master/src/main/java/com/github/forax/carrier/java/lang/runtime/SwitchExamples.java#L17

An example of assignment
https://github.com/forax/switch-carrier/blob/master/src/main/java/com/github/forax/carrier/java/lang/runtime/AssignmentExamples.java#L14

regards,
RĂ©mi

Reply via email to