Semantics of multiple patterns with the same prefix in a switch
Brian Goetz
brian.goetz at oracle.com
Sun Jan 31 22:57:21 UTC 2021
TO match Box(Foo f):
First, we statically check for applicability.
- First, we do an overload selection on Box deconstructors; assume we
find Box(Shape s).
- The match operand is Object, and the target type of the pattern
Box(...) is Box. So we first check to see if it is even possible to
cast Object to Box, by the rules of cast conversion, and whether that
cast would be unchecked. (This would prevent us from saying `anInteger
instanceof String s`, because casting Integer to String is known to fail
at compile time.)
- Assuming we pass the above, we then move on to the nested pattern.
THe Box deconstructor yields, presumably, a Shape. So we do the same
applicability game for `Shape` and `Foo f`.
- Assuming we pass the above, the match compiles.
As for runtime...
- SInce the match operand is not already a Box, we first do
`instanceof Box`.
- Since the Box deconstructor is total on boxes, if that test passes,
we run the Box deconstructor. It yields Shape s.
- Since the inner pattern is not "trivial", we take the Shape s, and
match it to the inner pattern `Foo f`. That's a non-total type pattern,
so we test if `s instanceof Foo`.
I believe I specified a translation model in a recent mail that should
spit out the translation, using LetExpr blocks. This is the semantics
of matching a _single_ pattern.
Your question, I believe, is whether in such a switch, we may, may not,
or must, run the deconstructor twice when we get to the second Box(...)
case. We haven't yet written down the rules for this, though of course
we've thought about it. As you point out, if the deconstructor has a
side-effect, the number of invocations would be visible.
I think there are two realistic options:
- The compiler and runtime _cannot_ try to optimize away the redundant
invocation.
- The compiler and runtime _may_ try to optimize away redundant
invocations, but there is no guarantee it does so.
The third option -- that we _must_ do so -- can be quickly seen to
becomes unrealistic as patterns get more complicated and harder to
analyze. So the first question is do we just give up and guarantee the
obvious execution, or do we give ourselves license to be clever, but
less deterministic.
Note that nested translation you wrote down is _not_ the obvious one; it
is already a "clever" one. THe obvious if-else chain is:
if (object instanaceof Box(Circle circle)) { ... }
else if (object instanceof Box(Square square)) { ... }
which can be further unrolled to:
if (object instanaceof Box(Shape s) && s instanceof Circle circle)
{ ... }
else if (object instanceof Box(Shape s) && s instanceof Square
square) { ... }
The one that you suggest is already an optimization of the above. And
while I can see my way to _allowing_ compilers to perform that
transformation, I am having a harder time _specifying_ a particular
optimization that would guarantee a deterministic number of executions
that properly normalizes over all pattern trees. (For the simple cases,
of course it is obvious.)
Here's an only slightly less trivial example. Imagine the Foo
deconstructor is Foo(Object).
case Foo(Frog f, Baz(Integer i)): ...
case Foo(Tadpole t, Baz(Float f)): ...
The two patterns have a lot in common, not just the outer Foo test, but
the right Baz test. There is a translation that minimizes the number of
both Foo and Baz executions, but it's not necessarily the obvious
transformation.
But, we're already in the weeds of translation details and
optimization. A much simpler question that has to be answered first is:
are we even willing to say "side effects in patterns can't be relied on
to happen with the obvious arity and order, and if you have them, good
luck to you"? Similarly, if we have a nested pattern P(Q, R), do we
promise that the Q and R tests happen left to right, or let the compiler
do what it likes?
On 1/31/2021 4:59 PM, Remi Forax wrote:
> Just to be sure, when we have a switch like
>
> Object object = ...
> switch(object) {
> case Box(Circle circle): ...
> case Box(Square square): ...
> }
>
> We have agreed that case Box(Foo ..) is equivalent to an instanceof + a call to the deconstructor,
> but i don't think we have agree what the equivalent code should be,
> either
>
> if (object instanceof Box(var value)) {
> if (value instanceof Circle circle) { ... }
> else if (value instanceof Square square) { ... }
> }
>
> or
>
> if ((object instanceof Box(var value)) && (value instanceof Circle circle)) { ... }
> else if ((object instanceof Box(var value)) && (value instanceof Square square)) { ... }
>
>
> It's important because if Box is a class with a deconstructor, i can insert a side effect in it and see how many times the deconstructor of Box is called.
>
> Given that i don't see the point of calling the deconstructor twice, in my mind, the switch is equivalent to the code with nested ifs and not the one with plain ifs.
> But re-reading the last document sent by Brian, i'm not sure anymore.
>
> regards,
> Rémi
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20210131/decae3ef/attachment.htm>
More information about the amber-spec-experts
mailing list