Here's some candidate spec text for MatchException:
Prototype spec for MatchException ( a preview API class ).
Thrown to indicate an unexpected failure in pattern matching.
MatchException may be thrown when an exhaustive pattern matching
language construct (such as a switch expression) encounters a match
target that does not match any of the provided patterns at runtime.
This can arise from a number of cases:
- Separate compilation anomalies, where a sealed interface has a
different set of permitted subtypes at runtime than it had at
compilation time, an enum has a different set of constants at runtime
than it had at compilation time, or the type hierarchy has changed in
incompatible ways between compile time and run time.
- Null targets and sealed types. If an interface or abstract class
`C` is sealed to permit `A` and `B`, then the set of record patterns
`R(A a)` and `R(B b)` are exhaustive on a record `R` whose sole
component is of type `C`, but neither of these patterns will match `new
R(null)`.
- Null targets and nested record patterns. Given a record type `R`
whose sole component is `S`, which in turn is a record whose sole
component is `String`, then the nested record pattern `R(S(String s))`
will not match `new R(null)`.
Match failures arising from unexpected inputs will generally throw
`MatchException` only after all patterns have been tried; even if
`R(S(String s))` does not match `new R(null)`, a later pattern (such as
`R r`) may still match the target.
MatchException may also be thrown when operations performed as part of
pattern matching throw an unexpected exception. For example, pattern
matching may cause methods such as record component accessors to be
implicitly invoked in order to extract pattern bindings. If these
methods throw an exception, execution of the pattern matching construct
may fail with `MatchException`.
On 3/30/2022 2:43 PM, Dan Heidinga wrote:
On Wed, Mar 30, 2022 at 2:38 PM Brian Goetz<brian.go...@oracle.com> wrote:
Another way to think about this is:
- If any of the code that the user actually wrote (the RHS of case clauses,
or guards on case labels) throws, then the switch throws that
- If any of the machinery of the switch dispatch throws, it throws
MatchException.
That's a reasonable way to factor this and makes the difference
between the machinery and the direct user code clear, even when
looking at stacktraces.
And from your other response:
Another thing it gains is that it discourages people
from thinking they can use exceptions in dtors; having these laundered
through MatchException discourages using this as a side channel, though
that's a more minor thing.
This is a stronger argument than you give it credit for being.
Wrapping the exception adds a bit of friction to doing the wrong thing
which will pay off in helping guide users to the intended behaviour.
--Dan
On 3/30/2022 2:12 PM, Dan Heidinga wrote:
The rules regarding NPE, ICCE and MatchException look reasonable to me.
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.)
My intuition (and maybe I have the wrong mental model?) is that the
pattern matching calling a user written dtor / record accessor is akin
to calling a method. We don't wrap the exceptions thrown by methods
apart from some very narrow cases (ie: reflection), and I thought part
of reflection's behaviour was related to needing to ensure exceptions
(particularly checked ones) were converted to something explicitly
handled by the caller.
If the dtor / record accessor can declare they throw checked
exceptions, then I can kind of see the rationale for wrapping them.
Otherwise, it seems clearer to me to let them be thrown without
wrapping.
I don't think we expect users to explicitly handle MatchException when
using pattern matching so what does wrapping gain us here?
--Dan