[External] : Re: Record patterns (and beyond): exceptions

Brian Goetz brian.goetz at oracle.com
Fri Feb 18 15:20:33 UTC 2022


We're lost in the weeds; I really can't follow what you're on about 
here, and more replies doesn't seem to be improving it. Since we're 
rapidly heading towards the danger zones I warned about in:

https://mail.openjdk.java.net/pipermail/amber-spec-observers/2020-August/002458.html

I think we should prune this sub-thread and give other folks a chance to 
reply to the main points.


On 2/18/2022 10:07 AM, forax at univ-mlv.fr wrote:
>
>
> ------------------------------------------------------------------------
>
>     *From: *"Brian Goetz" <brian.goetz at oracle.com>
>     *To: *"Remi Forax" <forax at univ-mlv.fr>
>     *Cc: *"amber-spec-experts" <amber-spec-experts at openjdk.java.net>
>     *Sent: *Friday, February 18, 2022 3:34:45 PM
>     *Subject: *Re: [External] : Re: Record patterns (and beyond):
>     exceptions
>
>
>             But this clearly does not fall into ICCE.  ICCE means,
>             basically, "your classpath is borked"; that things that
>             were known to be true at compile time are not true at
>             runtime.  (Inconsistent separate compilation is the most
>             common cause.)  But Box(Bag(null)) is not an artifact of
>             inconsistent separate compilation. 
>
>
>         I think i've not understood the problem correctly, i was
>         thinking the error was due to the erasure, Box<Bag> being
>         erased to Box, the problem with erasure is that you see the
>         problem late, in case of the switch after the phase that does
>         instanceofs, so we end up with ClassCastException instead of ICCE.
>
>
>     CCE is not the right thing either.  Let's step back and go over
>     the concepts.
>
>     We want for the compiler to be able to do type checking that a
>     switch is "total enough" to not require a default clause. We want
>     this not just because writing a default clause when you think
>     you've got things covered is annoying, but also, because once you
>     have a default clause, you've given up on getting any better type
>     checking for totality.  In a switch over enum X {A, B}, having
>     only cases for A and B means that, when someone adds C later,
>     you'll find out about it, rather than sweeping it under the rug. 
>     Sealed class hierarchies have the same issues as enums; the
>     possibility of novel values due to separate compilation.  So far,
>     all of these could be described by ICCE (and they are, currently.)
>
>     We've already talked for several lifetimes about null; switches
>     that reject null do so with NPE.  That also makes sense.  We had
>     hoped that this covered the weird values that might leak out of
>     otherwise-exhaustive switches, but that was wishful thinking.
>
>     Having nested deconstruction patterns introduces an additional
>     layer of weirdness.  Suppose we have
>
>         sealed interface A permits X, Y { }
>         Box<A> box;
>
>         switch (box) {
>             case Box(X x):
>             case Box(Y y):
>         }
>
>     This should be exhaustive, but we have to deal with two additional
>     bad values: Box(null), which is neither a Box(A) or a Box(B), and
>     Box(C), for a novel subtype C.  We don't want to disturb the user
>     to deal with these by making them have a default clause.
>
>     So we define exhaustiveness separately from totality, and
>     remainder is the difference.  (We can constructively characterize
>     the upper bound on remainder.)  And we can introduce a throwing
>     default, as we did with expression switches over enums.  But what
>     should it throw?
>
>     The obvious but naive answer is "well, Box(null) should throw NPE,
>     and Box(C) should throw ICCE."  But only a few minutes thinking
>     shows this to be misleading, expensive, and arbitrary.  When we
>     encountered Box(null), it was not because anyone tried to
>     dereference a null, so throwing NPE is misleading. 
>
>
> A NPE is not a problem if (the big if) the error message is "null 
> neither match Box(X) nor Box(Y)"
>
>     If the shape of the remainder is complicated, this means
>     generating tons of low-value, compiler-generated boilerplate to
>     differentiate Box(Bag(null)) from Box(Container(<novel>)).  That's
>     expensive.  And, what about Foo(null, C)?  Then we have to
>     arbitrarily pick one. It's a silly scheme. 
>
>
> We already talked about that, the shape of the remainder is complex if 
> you want to generate all branches at compile time, it's not an issue 
> if you generate the branches at runtime, because you can generate them 
> lazily.
> For some checks, they can only be done at runtime anyway, like does 
> this pattern is still total ?
>
> About Foo(null, C), i suppose you mean a case where you have both a 
> null that need to be deconstructed and a new subtype, the solution is 
> to go left to right, like usual in Java.
>
>
>
>     So the logical thing to do is to say that these things fall into a
>     different category from NPE and ICCE, which is that they are
>     remainder, which gets its own label. 
>
>
> Nope, as a user i want a real error message, not something saying 
> nope, sorry too complex, i bailout.
>
> [...]
>
>
>
>
>
>             Some patterns are considered exhaustive, but not total.  A
>             deconstruction pattern D(E(total)) is one such example; it
>             is exhaustive on D, but does not match D(null), because
>             matching the nested E(total) requires invoking a
>             deconstructor in E, and you can't invoke an instance
>             member on a null receiver. Still, we consider D(E(total))
>             exhaustive on D<E>, which means it is enough to satisfy
>             the type checker that you've covered everything. Remainder
>             is just the gap between exhaustiveness and totality. 
>
>
>         The gap is due to E(...) not matching null, for me it's a NPE
>         with an error message saying exactly that.
>
>
>     See above -- this is (a) NOT about dereferencing a null; it's
>     about a value outside the set of match values, (b) the scheme
>     involving NPE does not scale, and (c) will eventually force us to
>     silly arbitrary choices. 
>
>
> It scales if you don't try to generates all the branches at compile 
> time but only generate the one you need at runtime.
> Like JEP 358 (Helpful NPE) at the point you detect an error, you can 
> take a look at all the patterns that may match and generate a helpful 
> error message.
>
>
>
>         What you are saying is that at runtime you need to know if a
>         pattern is total or not, exactly you need to know if was
>         decided to be total at compile, so at runtime you can decide
>         to throw a NPE or not.
>         Furthermore, if at runtime you detect that the total pattern
>         is not total anymore, a ICCE should be raised.
>
>
>     No, what I'm saying is that totality and exhaustiveness are
>     related, but separate, concepts, and these do not stem from NPE or
>     ICCE, that this is a fundamental thing about switch exhaustiveness
>     (and later, same for let/bind) that needs to be captured in the
>     language. 
>
>
> I agree that totality and exhaustiveness are separate concept but at 
> runtime if you detect that either exhaustiveness or totality is not 
> true anymore, you can generate the appropriate exception with an 
> helpful error message.
>
> Rémi
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220218/9bf25c5c/attachment-0001.htm>


More information about the amber-spec-experts mailing list