Record patterns (and beyond): exceptions

Brian Goetz brian.goetz at oracle.com
Wed Feb 16 18:34:17 UTC 2022


As we move towards the next deliverable -- record patterns -- we have 
two new questions regarding exceptions to answer.

#### Questions

1.  When a dtor throws an exception.  (You might think we can kick this 
down the road, since records automatically acquire a synthetic dtor, and 
users can't write dtors yet, but since that synthetic dtor will invoke 
record component accessors, and users can override record component 
accessors and therefore they can throw, we actually need to deal with 
this now.)

This has two sub-questions:

1a.  Do we want to do any extra type checking on the bodies of dtors / 
record accessors to discourage explicitly throwing exceptions?  
Obviously we cannot prevent exceptions like NPEs arising out of 
dereference, but we could warn on an explicit throw statement in a 
record accessor / dtor declaration, to remind users that throwing from 
dtors is not the droid they are looking for.

1b.  When the dtor for Foo in the switch statement below throws E:

     switch (x) {
         case Box(Foo(var a)): ...
         case Box(Bar(var b)): ...
     }

what should happen?  Candidates include:

  - allow the switch to complete abruptly by throwing E?
  - same, but wrap E in some sort of ExceptionInMatcherException?
  - ignore the exception and treat the match as having failed, and move 
on to the next case?

2.  Exceptions for remainder.  We've established that there is a 
difference between an _exhaustive_ set of patterns (one good enough to 
satisfy the compiler that the switch is complete enough) and a _total_ 
set of patterns (one that actually covers all input values.)  The 
difference is called the _remainder_.  For constructs that require 
totality, such as pattern switches and let/bind, we have invariants 
about what will have happened if the construct completes normally; for 
switches, this means exactly one of the cases was selected.  The 
compiler must make up the difference by inserting a throwing catch-all, 
as we already do with expression switches over enums, and all switches 
over sealed types, that lack total/default clauses.

So far, remainder is restricted to two kinds of values: null (about 
which switch already has a strong opinion) and "novel" enum constants / 
novel subtypes of sealed types. For the former, we throw NPE; for the 
latter, we throw ICCE.

As we look ahead to record patterns, there is a new kind of remainder: 
the "spine" of nested record patterns.  This includes things like 
Box(null), Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc.  
It should be clear that there is no clean extrapolation from what we 
currently do, to what we should do in these cases.  But that's OK; both 
of the existing remainder-rejection cases derive from "what does the 
context think" -- switch hates null (so, NPE), and enum switches are a 
thing (which makes ICCE on an enum switch reasonable.)  But in the 
general case, we'll want some sort of MatchRemainderException.

Note that throwing an exception from remainder is delayed until the last 
possible moment.  We could have:

     case Box(Bag(var x)): ...
     case Box(var x) when x == null: ...

and the reasonable treatment is to treat Box(Bag(var x)) as not matching 
Box(null), even though it is exhuastive on Box<Bag<?>>), and therefore 
fall into the second case on Box(null).  Only when we reach the end of 
the switch, and we haven't matched any cases, do we throw 
MatchRemainderException.

#### Discussion

For (1a), my inclination is to do nothing for record accessors, but when 
we get to explicit dtors, warning on explicit throw is not a bad idea.  
Unlike ctors, where exceptions are part of the standard toolbox, 
deconstructors are handed an already-constructed object, and are 
supposed to be total.  If you're inclined to write a partial dtor, 
you're probably doing it wrong.  As it is a new construct, the 
additional error checking to guide people to its proper use is probably 
reasonable (and cheap.)

For (1b), since an exception in a dtor is suppose to indicate an 
exceptional failure, I don't think swallowing it and trying to go on 
with the show is a good move.  My preference would be to wrap the 
exception, as we do with ExceptionInInitializerError, to make it clear 
that an exception from a dtor is a truly unexpected thing, and clearly 
name-and-shame the offending dtor.  So there's a bikeshed to paint for 
what we call this exception.

For (2), trying to repurpose either NPE or ICCE here is a losing move.  
Better to invent an exception type that means "uncovered remainder" 
(which is more akin to an IAE than anything else; someone passed a bad 
value to an exhaustive-enough switch.)  We would use the same exception 
in let/bind, so this shouldn't have "switch" in its name, but probably 
something more like MatchRemainderException.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220216/9f89bae8/attachment-0001.htm>


More information about the amber-spec-experts mailing list