It seems pretty hard to land anywhere other than where you've landed, for
most of this. I have the same sort of question as Dan: do we really want to
wrap exceptions thrown by other patterns? You say we want to discourage
patterns from throwing at all, and that's a lovely dream, but the behavior
of total patterns is to throw when they meet something in their remainder.
Since user-defined patterns will surely involve primitive patterns at some
point, there is the possibility that one of those primitive patterns
throws, which bubbles up as an exception thrown by a user-defined pattern.

On Wed, Mar 30, 2022 at 7:40 AM Brian Goetz <brian.go...@oracle.com> wrote:

> We should have wrapped this up a while ago, so I apologize for the late
> notice, but we really have to wrap up exceptions thrown from pattern
> contexts (today, switch) when an exhaustive context encounters a
> remainder.  I think there's really one one sane choice, and the only thing
> to discuss is the spelling, but let's go through it.
>
> In the beginning, nulls were special in switch.  The first thing is to
> evaluate the switch operand; if it is null, switch threw NPE.  (I don't
> think this was motivated by any overt null hostility, at least not at
> first; it came from unboxing, where we said "if its a box, unbox it", and
> the unboxing throws NPE, and the same treatment was later added to enums
> (though that came out in the same version) and strings.)
>
> We have since refined switch so that some switches accept null.  But for
> those that don't, I see no other move besides "if the operand is null and
> there is no null handling case, throw NPE."  Null will always be a special
> remainder value (when it appears in the remainder.)
>
> In Java 12, when we did switch expressions, we had to confront the issue
> of novel enum constants.  We considered a number of alternatives, and came
> up with throwing ICCE.  This was a reasonable choice, though as it turns
> out is not one that scales as well as we had hoped it would at the time.
> The choice here is based on "the view of classfiles at compile time and run
> time has shifted in an incompatible way."  ICCE is, as Kevin pointed out, a
> reliable signal that your classpath is borked.
>
> We now have two precedents from which to extrapolate, but as it turns out,
> neither is really very good for the general remainder case.
>
> Recall that we have a definition of _exhaustiveness_, which is, at some
> level, deliberately not exhaustive.  We know that there are edge cases for
> which it is counterproductive to insist that the user explicitly cover,
> often for two reasons: one is that its annoying to the user (writing cases
> for things they believe should never happen), and the other that it
> undermines type checking (the most common way to do this is a default
> clause, which can sweep other errors under the rug.)
>
> If we have an exhaustive set of patterns on a type, the set of possible
> values for that type that are not covered by some pattern in the set is
> called the _remainder_.  Computing the remainder exactly is hard, but
> computing an upper bound on the remainder is pretty easy.  I'll say "x may
> be in the remainder of P* on T" to indicate that we're defining the upper
> bound.
>
>  - If P* contains a deconstruction pattern P(Q*), null may be in the
> remainder of P*.
>  - If T is sealed, instances of a novel subtype of T may be in the
> remainder of P*.
>  - If T is an enum, novel enum constants of T may be in the remainder of
> P*.
>  - If R(X x, Y y) is a record, and x is in the remainder of Q* on X, then
> `R(x, any)` may be in the remainder of { R(q) : q in Q*} on R.
>
> Examples:
>
>     sealed interface X permits X1, X2 { }
>     record X1(String s) implements X { }
>     record X2(String s) implements X { }
>
>     record R(X x1, X x2) { }
>
>     switch (r) {
>          case R(X1(String s), any):
>          case R(X2(String s), X1(String s)):
>          case R(X2(String s), X2(String s)):
>     }
>
> This switch is exhaustive.  Let N be a novel subtype of X.  So the
> remainder includes:
>
>     null, R(N, _), R(_, N), R(null, _), R(X2, null)
>
> It might be tempting to argue (in fact, someone has) that we should try to
> pick a "root cause" (null or novel) and throw that.  But I think this is
> both excessive and unworkable.
>
> Excessive: This means that the compiler would have to enumerate the
> remainder set (its a set of patterns, so this is doable) and insert an
> extra synthetic clause for each.  This is a lot of code footprint and
> complexity for a questionable benefit, and the sort of place where bugs
> hide.
>
> Unworkable: Ultimately such code will have to make an arbitrary choice,
> because R(N, null) and R(null, N) are in the remainder set.  So which is
> the root cause?  Null or novel?  We'd have to make an arbitrary choice.
>
>
> So what I propose is the following simple answer instead:
>
>  - If the switch target is null and no case handles null, throw NPE.  (We
> know statically whether any case handles null, so this is easy and similar
> to what we do today.)
>  - If the switch is an exhaustive enum switch, and no case handles the
> target, throw ICCE.  (Again, we know statically whether the switch is over
> an enum type.)
>  - In any other case of an exhaustive switch for which no case handles the
> target, we throw a new exception type, java.lang.MatchException, with an
> error message indicating remainder.
>
> The first two rules are basically dictated by compatibility.  In
> hindsight, we might have not chosen ICCE in 12, and gone with the general
> (third) rule instead, but that's water under the bridge.
>
> We need to wrap this up in the next few days, so if you've concerns here,
> please get them on the record ASAP.
>
>
> As a separate but not-separate exception problem, we have to deal with at
> least two additional sources of exceptions:
>
>  - A dtor / record acessor may throw an arbitrary exception in the course
> of evaluating whether a case matches.
>
>  - User code in the switch may throw an arbitrary exception.
>
> For the latter, this has always been handled by having the switch
> terminate abruptly with the same exception, and we should continue to do
> this.
>
> For the former, we surely do not want to swallow this exception (such an
> exception indicates a bug).  The choices here are to treat this the same
> way we do with user code, throwing it out of the switch, or to wrap with
> MatchException.
>
> I prefer the latter -- wrapping with MatchException -- because the
> exception is thrown from synthetic code between the user code and the
> ultimate thrower, which means the pattern matching feature is mediating
> access to the thrower.  I think we should handle this as "if a pattern
> invoked from pattern matching completes abruptly by throwing X, pattern
> matching completes abruptly with MatchException", because the specific X is
> not a detail we want the user to bind to.  (We don't want them to bind to
> anything, but if they do, we want them to bind to the logical action, not
> the implementation details.)
>
>
>

Reply via email to