On 3/16/2022 4:34 PM, fo...@univ-mlv.fr wrote:
----- Original Message -----
From: "Brian Goetz" <brian.go...@oracle.com>
To: "Remi Forax" <fo...@univ-mlv.fr>, "amber-spec-experts" 
<amber-spec-experts@openjdk.java.net>
Sent: Wednesday, March 16, 2022 5:41:49 PM
Subject: Re: Record pattern, the runtime side
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.
We can "normalize" a complex pattern into a sequence of simpler
conditionals.  For example, matching the record pattern

     case Circle(Point(var x, var y), var r)

can be unrolled (and type inference applied) as

     x matches Circle(Point(var x, var y), var r)
     === x matches Circle(Point p, int r) && p matches Point(int x, int y)

Deconstruction patterns are known to have only an `instanceof`
precondition; the deconstructor body won't ever fail (unlike more
general static or instance patterns like Optional::of.)
If you define "matches" in term of instanceof this transformation does not work 
in the context of an assignment,
because you want
   Point(var x, var y) = null
to throw a NPE.

Yes, but this is not what matches means.  Matches is the three-place predicate that takes the static type of the target into account.  It differs from instanceof only at null, which is why I wrote "matches" rather than "instanceof".  You'll see in later rounds of lowering how this gets turned back into instanceof, taking into account the static type information.

But it's a very valid transformation if the pattern is not total and "matches" 
means instanceof in the context of a switch or instanceof and requireNonNull + cast in 
the context of an assignment.

Correct:

     P(Q) === P(var a) && a mathces Q  always, whereas
     P(Q) === P(var a) && a instanceof Q  **when Q is not unconditional on the target of P**

Updated terminology scorecard: a pattern P is *unconditional* on a type T if it matches all values of T; in other words, if it is not asking a question at all.  The only unconditional patterns are "any" patterns (_), "var" patterns, and total type patterns. Deconstruction patterns are never unconditional, because they don't match on nulls.

On the other hand, a pattern P is *exhaustive* on a type T if it is considered "good enough" for purposes of static type checking. Deconstruction patterns D(...) are exhaustive on types T <: D, even though they don't match null.  The difference is *remainder*.

Also from the runtime POV, a deconstructor and a pattern methods (static or 
instance) are identical, if we follow the idea of John to use null for not 
match. Obviously, it does not preclude us to differentiate between the two at 
the language level.

With one difference; the language makes deconstructors always total (they can't fail to match if the target is of the right type), whereas pattern methods can fail to match.  So in the translations where I write "c.deconstruct(...)" we are assuming that the deconstructor is always "true".

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).
With record patterns, we don't even need pattern descriptions, because
we can translate it all down to instanceof tests and invoking record
component accessors.  Of course, that ends when we have deconstruction
patterns, which correspond to imperative code; then having a Pattern
instantiation, and a way to get to its matching / binding-extraction
MHs, is needed.
Yes, record pattern is the last pattern we can implement in term of a cascade 
of if. I use that fact in the prototype, the runtime use the switch + type 
pattern because it does not use invokedynamic.

We can use a cascade of if either way, but record patterns are more "transparent" in that the compiler can lower the match criteria and extraction of bindings to primitives it already understands, whereas a method pattern is an opaque blob of code.

For the future, i'm not sure we will want to use invokedynamic for all 
patterns, indy is still quite slow until c2 kicks in.

It is a difficult tradeoff to decide what code to emit for narrowly-branching trees.  The indy approach means we can change plans later, but has a startup cost that may well not buy us very much; having a heuristic like "use if-else chains for fewer than five types" is brittle.


Reply via email to