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.


Reply via email to