Re: [External] : Re: Remainder in pattern matching

2022-04-07 Thread Alan Malloy
Yes, this clears up my concerns.

On Fri, Apr 1, 2022 at 6:56 AM Brian Goetz  wrote:

>
> 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.
>
>
> Not exactly.  The behavior of *switch* is to throw when they meet
> something in the remainder of *all their patterns*.  For example:
>
> Box> bbs = new Box(null);
> switch (bbs) {
> case Box(Box(String s)): ...
> case null, Box b: ...
> }
>
> has no remainder and will not throw.  Box(null) doesn't match the first
> pattern, because when we unroll to what amounts to
>
> if (x instanceof Box alpha && alpha != null && alpha.value()
> instanceof Box beta && beta != null) {
> s = beta.value(); ...
> }
> else if (x == null || x instanceof Box) { ... }
>
> we never dereference something we don't know to be non-null.  So Box(null)
> doesn't match the first case, but the second case gets a shot at it.  Only
> if no case matches does switch throw; *pattern matching* should never
> throw.  (Same story with let, except its like a switch with one
> putatively-exhaustive case.)
>
> 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.
>
>
> Again, primitive patterns won't throw, they just won't match.  Under the
> rules I outlined last time, if I have:
>
> Box b = new Box(null);
> switch (b) {
> case Box(int x): ...
> ...
> }
>
> when we try to match Box(int x) to Box(null), it will not NPE, it will
> just not match, and we'll go on to the next case.  If all cases don't
> match, then the switch will throw ME, which is a failure of
> *exhaustiveness*, not a failure in *pattern matching*.
>
> Does this change your first statement?
>
>
> On Wed, Mar 30, 2022 at 7:40 AM Brian Goetz 
> 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 

Re: [External] : Re: Remainder in pattern matching

2022-04-01 Thread Brian Goetz


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.


Not exactly.  The behavior of *switch* is to throw when they meet 
something in the remainder of *all their patterns*.  For example:


    Box> bbs = new Box(null);
    switch (bbs) {
    case Box(Box(String s)): ...
    case null, Box b: ...
    }

has no remainder and will not throw.  Box(null) doesn't match the first 
pattern, because when we unroll to what amounts to


    if (x instanceof Box alpha && alpha != null && alpha.value() 
instanceof Box beta && beta != null) {

    s = beta.value(); ...
    }
    else if (x == null || x instanceof Box) { ... }

we never dereference something we don't know to be non-null.  So 
Box(null) doesn't match the first case, but the second case gets a shot 
at it.  Only if no case matches does switch throw; *pattern matching* 
should never throw.  (Same story with let, except its like a switch with 
one putatively-exhaustive case.)


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.


Again, primitive patterns won't throw, they just won't match.  Under the 
rules I outlined last time, if I have:


    Box b = new Box(null);
    switch (b) {
    case Box(int x): ...
    ...
    }

when we try to match Box(int x) to Box(null), it will not NPE, it will 
just not match, and we'll go on to the next case.  If all cases don't 
match, then the switch will throw ME, which is a failure of 
*exhaustiveness*, not a failure in *pattern matching*.


Does this change your first statement?



On Wed, Mar 30, 2022 at 7:40 AM Brian Goetz  
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 

Re: [External] : Re: Remainder in pattern matching

2022-03-31 Thread Brian Goetz

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  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 

Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Remi Forax
- Original Message -
> From: "Dan Heidinga" 
> To: "Brian Goetz" 
> Cc: "amber-spec-experts" 
> Sent: Wednesday, March 30, 2022 8:43:42 PM
> Subject: Re: [External] : Re: Remainder in pattern matching

> On Wed, Mar 30, 2022 at 2:38 PM Brian Goetz  wrote:
>>

[...]

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

Wrapping exceptions into a MatchException seems a very bad idea to me.
When you compute something on an AST, the pattern matching is recursive, so if 
an exception occurs, instead of having one exception with a long stacktrace, we 
will get a linked list of MatchException with each of them having a long 
stacktraces.

> 
> --Dan

Rémi

> 
>> 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
>>


Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Brian Goetz
Yes, and this is a special case of a more general thing -- that while 
pattern declarations may have a lot in common with methods, they are not 
"just methods with multiple return" (e.g., they have a different set of 
characteristics at the declaration, they are intrinsically conditional, 
they are "invoked" differently.)  While their bodies may look 
method-like, and ultimately they boil down to methods, thinking "they 
are just methods" is likely to drag you to the wrong place.  Of course, 
its a balance between how similar and HOW DIFFERENT they are, and that's 
what we're looking for.



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.


Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread forax
> From: "Brian Goetz" 
> To: "Remi Forax" 
> Cc: "amber-spec-experts" 
> Sent: Wednesday, March 30, 2022 8:35:17 PM
> Subject: Re: [External] : Re: Remainder in pattern matching

>> It seems that what you are saying is that you think an Exception is better 
>> than
>> an Error.

> Not exactly; what I'm saying is that the attempt to separate stray nulls from
> separate compilation issues here seems like a heroic effort for low value, and
> I'd rather have one channel for "exhaustiveness failure" and let
> implementations decide how heroic they want to get in sorting out the possible
> causes.
NPE is a developer issue, separate compilation failure/ICCE is a deployment 
issue, there is no point to have one channel. 

Rémi 


Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Dan Heidinga
On Wed, Mar 30, 2022 at 2:38 PM Brian Goetz  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
>
>



Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Remi Forax
- Original Message -
> From: "Brian Goetz" 
> To: "Dan Heidinga" 
> Cc: "amber-spec-experts" 
> Sent: Wednesday, March 30, 2022 8:26:53 PM
> Subject: Re: [External] : Re: Remainder in pattern matching

[...]

> 
> One thing wrapping gains is that it gives us a place to centralize
> "something failed in pattern matching", which includes exhaustiveness
> failures as well as failures of invariants which PM assumes (e.g., dtors
> don't throw.) 

but such centralization is a bad practice, that the reason why catch(Exception) 
is considered as a bad practice.

BTW, i hope that with loom people will use virtual threads (they are cheap) to 
manage scenarios where you want to discard a computation if something fails, 
like in Erlang.

Rémi


Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Brian Goetz

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.


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



Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Brian Goetz




It seems that what you are saying is that you think an Exception is 
better than an Error.


Not exactly; what I'm saying is that the attempt to separate stray nulls 
from separate compilation issues here seems like a heroic effort for low 
value, and I'd rather have one channel for "exhaustiveness failure" and 
let implementations decide how heroic they want to get in sorting out 
the possible causes.

Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread forax
> From: "Brian Goetz" 
> To: "Remi Forax" 
> Cc: "amber-spec-experts" 
> Sent: Wednesday, March 30, 2022 6:32:15 PM
> Subject: Re: [External] : Re: Remainder in pattern matching

>> For when the static world and the dynamic world disagree, i think your 
>> analysis
>> has miss an important question, switching on an enum throw an ICCE very late
>> when we discover an unknown value, but in the case of a sealed type,

> Actually, I thought about that quite a bit before proposing this. And my
> conclusion is: using ICCE was mostly a (well intentioned) mistake here, and
> "doubling down" on that path is more trouble than it is worth. So we are
> minimally consistent with the ICCE choice in the cases that were compilable in
> 12, but for anything else, we follow the general rule.

> The thought experiment that I did was: what if we had not done switch
> expressions in 12. Then the only precedent we have to deal with is the null
> case, which has a pretty obvious answer. So what would we do? Would we
> introduce 10s of catch-all cases solely for the purpose of diagnosing the
> source of remainder, or would we introduce a throwing default that throws
> MatchException on everything but null? I concluded we would do the latter, so
> what is proposed here is basically that, but carving out the 12-compatibility
> case.
We are discussing about what to do if a sealed types has more permitted 
subtypes at runtime than the one seen when the code was compiled. 
It's a separate compilation issue, hence the ICCE. 

It seems that what you are saying is that you think an Exception is better than 
an Error. 
If we follow that path, it means that it may make sense to recover from a 
MatchException but i fail to see how, we can not ask a developer of a code to 
change it while that code is executed, separate compilation errors are not 
recoverable. 

>> Remainders are dangling else in a cascade of if ... else, so yes, we have to
>> care of them.

> Yes, but we can care for all of them in one swoop with a synthetic default.

>> So yes, it may a lot of bytecodes if we choose to add all branches but the
>> benefit is not questionable, it's far better than the alternative which is
>> GoodLuckFigureByYourselfException.

> Yes, when you get a dynamic error here in a complex switch, the range of what
> could have gone wrong is large. (The same will be true outside of switches 
> when
> we have more kinds of patterns (list patterns, map patterns, etc) and more 
> ways
> to compose patterns into bigger patterns; if we have a big complex pattern 
> that
> matches the JSON document with the keys we want, if it doesn't match because
> (say) some integer nested nine levels deep overflowed 32 bits, this is also
> going to be hard to diagnose.) But you are proposing a new and significant
> language requirement -- that the language should mandate an arbitrarily 
> complex
> explanation of why something didn't match. I won't dispute that this has
> benefit -- but I am not convinced this is necessarily the place for this, or
> whether the cost is justified by the benefit.
The explanation is not complex, there is a sealed type has more subtypes now 
than at a time the code was compiled. 

Rémi 


Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Brian Goetz
It's a little like calling a method, but a little not like it too. For 
example, when you match on a record pattern:


    case Point(var x, var y): ...

what may happen is *either* you will invoke a user-written deconstructor 
pattern, *or* we will test if you are a Point with `instanceof`, and 
then invoke the accessor methods (which might be user-written or 
implicit.)  Similarly, if you match:


    case Point(P, Q):
    case Point(R, S):

we may invoke the Point deconstructor once, or twice.  And there's no 
way to _directly_ invoke a pattern, only through switch, instanceof, and 
other contexts.


All of this means that invocations of pattern methods is more indirect, 
and mediated by the language, than invoking a method. When you invoke a 
method, you are assenting to its contract about what it returns, what it 
throws, etc.  When you match a pattern, it feels more likely are 
assenting to the contract of _pattern matching_, which in turn hides 
implementation details of what pattern methods are invoked, when they 
are invoked, how often, etc.


Dtors and record accessors cannot throw checked exceptions at all, and 
will be discouraged from throwing exceptions at all.


One thing wrapping gains is that it gives us a place to centralize 
"something failed in pattern matching", which includes exhaustiveness 
failures as well as failures of invariants which PM assumes (e.g., dtors 
don't throw.)   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.


Agree we do not expect users to explicitly handle ME, any more so than NPE.


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





Re: [External] : Re: Remainder in pattern matching

2022-03-30 Thread Brian Goetz




For when the static world and the dynamic world disagree, i think your 
analysis has miss an important question, switching on an enum throw an 
ICCE very late when we discover an unknown value, but in the case of a 
sealed type,


Actually, I thought about that quite a bit before proposing this. And my 
conclusion is: using ICCE was mostly a (well intentioned) mistake here, 
and "doubling down" on that path is more trouble than it is worth.  So 
we are minimally consistent with the ICCE choice in the cases that were 
compilable in 12, but for anything else, we follow the general rule.


The thought experiment that I did was: what if we had not done switch 
expressions in 12.  Then the only precedent we have to deal with is the 
null case, which has a pretty obvious answer.  So what would we do?  
Would we introduce 10s of catch-all cases solely for the purpose of 
diagnosing the source of remainder, or would we introduce a throwing 
default that throws MatchException on everything but null?  I concluded 
we would do the latter, so what is proposed here is basically that, but 
carving out the 12-compatibility case.


Remainders are dangling else in a cascade of if ... else, so yes, we 
have to care of them.


Yes, but we can care for all of them in one swoop with a synthetic default.

So yes, it may a lot of bytecodes if we choose to add all branches but 
the benefit is not questionable, it's far better than the alternative 
which is GoodLuckFigureByYourselfException.


Yes, when you get a dynamic error here in a complex switch, the range of 
what could have gone wrong is large.  (The same will be true outside of 
switches when we have more kinds of patterns (list patterns, map 
patterns, etc) and more ways to compose patterns into bigger patterns; 
if we have a big complex pattern that matches the JSON document with the 
keys we want, if it doesn't match because (say) some integer nested nine 
levels deep overflowed 32 bits, this is also going to be hard to 
diagnose.)  But you are proposing a new and significant language 
requirement -- that the language should mandate an arbitrarily complex 
explanation of why something didn't match.  I won't dispute that this 
has benefit -- but I am not convinced this is necessarily the place for 
this, or whether the cost is justified by the benefit.


Also, note that the two are not inconsistent.  If the switch is required 
to throw MatchException on remainder, the compiler is *allowed* to try 
and diagnose the root cause (the ME can wrap something more specific), 
but not required to.   Pattern failure diagnosis then becomes a quality 
of implementation choice, rather than having complex, brittle rules 
mandated by the spec.  There's nothing to stop us from doing the 
equivalent of the "helpful NPE" JEP in the future.