[External] : Re: Record pattern, the runtime side
Brian Goetz
brian.goetz at oracle.com
Thu Mar 17 13:59:40 UTC 2022
On 3/16/2022 4:34 PM, forax at univ-mlv.fr wrote:
> ----- Original Message -----
>> From: "Brian Goetz" <brian.goetz at oracle.com>
>> To: "Remi Forax" <forax at univ-mlv.fr>, "amber-spec-experts" <amber-spec-experts at 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.
More information about the amber-spec-experts
mailing list