Updated patterns-in-switch doc

Brian Goetz brian.goetz at oracle.com
Tue Sep 22 17:22:11 UTC 2020

>> with the possible temporary exception of the three primitive types not currently permitted
> I believe, there are four of them: boolean, long, double, and float


> #2 Disallowing switch expressions inside guards
>> And worse if we allow switch expressions inside guards, which we shouldn't do.
> Hm... sounds like this may heavily complicate the grammar if we really
> want to prohibit switch expressions anywhere inside the guard. E.g.:
> switch (obj) {
> case Foo(int x)
>    __where process(switch(x) {case 1 -> 10;case 2 -> 20;default -> 0;}) -> ...
> }
> Should this be allowed? If yes, then there's no point to disallow
> top-level switch expressions inside guards, as nested ones are
> confusing to the same level.
> If yes, then the grammar should be updated to include tons of
> productions like ExpressionNoSwitch, MethodInvocationNoSwitch,
> ArgumentListNoSwitch, and so on.
> To me, adding any restrictions to expressions inside guards looks an
> arbitrary decision. If users really want to write confusing code, let
> them allow using expression switches in guards.

Let's separate out the grammar from the goal.

Embedding switch expressions has two problems here; one is the obvious 
syntactic confusion (what switch is that case a part of?), and the other 
is side-effects; switch expressions are the only expressions that can 
embed statements.  Both are a bad match for expressing guard 
conditions.  (Note that when we did switch expressions, we omitted the 
possibility that a switch expression could be a constant expression, to 
avoid their use in, say, case labels in a constant switch:

     case switch (x) { case 1 -> 3; case 2 -> 4; } -> 5;

I think that was the right call :)

As you pointed out yesterday on Twitter regarding old-style array 
declarations in record components, there are two ways to address it; 
constrain the grammar, or use a deliberately coarse grammar and then 
perform a post-parse check.  So if the answer is to constrain what you 
can put in guards, that doesn't mean we have to constrain the grammar.

The concern about side-effects (which I know we can't fully contain) 
comes from a bigger goal, one that may not have been explicitly stated: 
I want that the computation inherent in a pattern switch effectively be 
"constant", for a number of reasons.  Having side-effects in case labels 
or guards effectively undermines that. One benefit (but not the only) of 
doing this is that it is a necessary condition for encoding the entire 
switch logic in an `indy` bootstrap, so that we can dynamically 
construct a decision tree based on the characteristics of the case 
labels.  The more imperative a switch looks, the harder it is to do 
that.  (So, for example, guards should probably be restricted to 
capturing effectively-final locals.)

THe remaining constraining of side-effects will come from making only 
vague promises about when, and how often, to execute pattern bodies.  if 
we have a switch with:

     case Foo(var x) where x == 0:
     case Foo(var x) where x > 0:
     case Foo(var x):

we should be free to execute the deconstructor body early or 
just-in-time, once or three times (or more!)

> #3 Total patterns in instanceof
>> It is sensible because x instanceof <total pattern> is in some sense a silly question, in that it will always be true and there's a simpler way (local variable assignment) to express the same thing.
> It should be noted that local variable assignment requires the
> variable declaration which cannot be done inside the expression.

Yes, I realize that one can use pattern matching as a form of implicit 
assignment.  But if we think that is important, maybe it is better to 
try to get there more directly, rather than relying on a "trick".   The 
idiom you describe -- pulling a DU local into a broader scope -- works, 
but is less than ideal, because you don't get the perfect scoping.

> Well, we may split declaration and assignment and put the assignment
> inside the condition:
> Foo foo;
> if (x != null && (foo = x.doExpensiveCalculation()) != null &&
> foo.isValidResult()) {
>    use(foo);
> }
> But this has at least two drawbacks: necessity to specify explicit Foo
> type (var doesn't work anymore) and broadening the scope of `foo` more
> than necessary (it pollutes the namespace after `if`). In general, it
> looks asymmetrical to me that the condition can introduce variables
> only if we can express the condition on that variable with a pattern,
> but we cannot do this if we have a guard-like condition.

Here's how I would rather write that (if the status quo isn't good enough):

     if (x != null && (var foo = x.doExpensiveCalculation()) != null && 
foo.isValidResult()) { use(foo); }

That is, treat `var x = e` as a pattern match that always succeeds and 
generates one binding.

More information about the amber-spec-experts mailing list