Record pattern, the runtime side

Remi Forax forax at
Sun Mar 13 14:59:47 UTC 2022

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

An example of switch

An example of assignment


More information about the amber-spec-experts mailing list