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
RIght.
#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.