Exploring inference for sealed types

John Rose john.r.rose at oracle.com
Wed Oct 2 00:05:03 UTC 2019


On Sep 23, 2019, at 9:04 AM, Gavin Bierman <gavin.bierman at oracle.com> wrote:
> 
> ...
> But there are wrinkles with this design. 
> 
> 1. Obviously we will rule out the explicit declaration of an empty `permits`
> clause, but what if we _infer_ one? To be concrete where we have the 
> compilation unit:
> 
> ```
> sealed class Foo { } 
> //EOF
> ```
> 
> What's the right thing here? Is this an compile-time error? A `sealed` class
> with an empty `permits` clause is morally `final`, so should we infer the
> class to be `final`? Or do we need to keep the two - `final` and
> `sealed`-with-empty-`permits`) and make sure we treat them identically?

From a VM point of view I’d rather not have two configurations that
both amount of “final” (classic final + sealed-with-empty-permits).
Adding unused degrees of freedom adds places for bugs to hide.
From a source point of view, when the sealed class has no subs,
it seems just fine for javac to report an error requiring a bit of
ceremony to change “sealed” to “final”.  The end product is more
readable (less ambiguous), so the adjustment is a legitimate thing
to require.

> 2. Consider the following:
> 
> ```
> class Outer {
>    sealed class SuperSealed {}
> 
>    sealed interface SealedI {}
> 
>    class SubFinal1 extends SuperSealed {}
> 
>    class SubFinal2 implements SealedI {}
> 
>    non-sealed SubNonSealed extends SuperSealed implements SealedI {}

> 
>    class WhatAboutMe extends SubNonSealed implements SealedI {}

//maybe also
      non-sealed NonSealedI extends SealedI {}

      interface WhatAboutMeToo extends NonSealedI, SuperSealed {}

> }
> ```
> 
> The issue is around class WhatAboutMe. It redundantly implements SealedI. But
> with the cascading behavior this is significant. *With* the `implements` clause
> we infer that WhatAboutMe should be `final`. *Without* it we would not. 
> 
> As is common with other inference systems, our inference may not be invariant
> under semantically equivalent declarations. Is that going to be too
> confusing for users?

Yes, it will be confusing.  It is probably one of the reasons we are shying
away from this level of inferencing:  It’s got at least one surprise in it.

A standard answer to surprises is to outlaw them, on the grounds that if
they are surprising they might as well be declared illegal ambiguities.
So if the supers of an unmarked type are a mix of open and sealed,
the inferencer doesn’t pick a favorite; rather, it requires the user to
write an explicit (non-inferred) choice.

So WhatAboutMe would be open without the implements clause,
and *in error* with the implements clause, on the grounds that its
two supers can’t agree about their effect on inference.  Since only
local classes affect such inference, the error check can look
for a mix of supers (two implements, or a super-class and an
implement) which were (a) local and (b) differing in their open-ness.

In the case of WhatAboutMe, an extra feature is that it redundantly
implements SealedI.  I don’t think the error check needs to care about
that feature; it only needs to look a mix of sealed and non-sealed
supers *among the supers defined in the same compilation unit*.
The compiler will complain, and the user will grumble a bit and add
“sealed” (or ”final”) or “non-sealed” to the type with the funny mix
of supers.  As before, it’s worth it since it improves readability in a
place that would otherwise be doubtful.

> 3. This cascading inference puts strain on the compiler. It works by analyzing
> which types extend/implement the `sealed` type in question. Whilst such
> hierarchies are probably going to be small, one could imagine people
> exploiting this inference feature and building extensive and complicated
> sealed hierarchies within a single compilation unit. Do we want to commit
> compilers to navigating these graphs? Would we be encouraging people to defining
> auxiliary classes when we have been recommending *not* to do so?

Easy for me to say as a VM guy, but compile time is cheap.
Also, source files are usually pretty small.  If a user puts 1000s
of types in a source file and gets a slow compilation, it’s obvious
what to do.

> Thoughts welcome!
> Gavin
> 
> SPEC DETAILS
> ------------
> 
> To deal with modifiers; first for classes:
> 
> ---
> 
> If a class _C_ extends a `sealed` class ([8.1.4]) that is declared in the same
> compilation unit ([7.3]), or implements a `sealed` interface ([9.1.1.3]) that
> is declared in the same compilation unit, one of the following applies:
> 
> - The class is explicitly declared `final` or `sealed`.
> 
> - The class is explicitly declared `non-sealed`, meaning that there are no
> restrictions on the subclasses of _C_.
> 
> - The class is not declared `final`, `sealed`, nor `non-sealed`. In this case,
> if _C_ is the direct superclass ([8.1.4]) of another class declared in the
> same compilation unit, then class _C_ is implicitly declared `sealed`;
> otherwise class _C_ is implicitly declared `final`.

Add an exception:

- Nevertheless, if such an implicitly modified _C_extends a class
or  implements an interface defined in the same compilation unit,
and that class or interface is implicitly or explicitly marked non-sealed,
then a compile-time error occurs.

> 
> Otherwise, if a class _C_ extends a `sealed` class or implements a `sealed`
> interface, one of the following applies:
> 
> - The class is explicitly declared `final` or `sealed`.
> 
> - The class is explicitly declared `non-sealed`, meaning that there are no
> restrictions on the subclasses of _C_.
> 
> - The class is not declared `final`, `sealed`, nor `non-sealed`, and a
> compile-time error occurs.
> 
> ---
> 
> And for interfaces:
> 
> ---
> 
> If an interface _I_ extends a `sealed` interface ([9.1.3]) that is declared in
> the same compilation unit ([7.3]), one of the following applies:
> 
> - The interface _I_ is explicitly declared `sealed`.
> 
> - The interface _I_ is explicitly declared `non-sealed`, meaning that there are
> no restrictions on the subtypes of _I_.
> 
> - The interface _I_ is not declared `sealed` or `non-sealed`. In this case, if
> _I_ is the superinterface ([8.1.5], [9.1.3]) of another class or interface 
> declared in the same compilation unit, then interface _I_ is implicitly
> declared `sealed`; otherwise a compile-time error occurs.

Add an exception:

- Nevertheless, if such an implicitly modified _I_extends an interface
defined in the same compilation unit, and that interface is implicitly or
explicitly marked non-sealed, then a compile-time error occurs.

> ---
> 
> To deal with `permits` clauses; first for classes:
> 
> ---
> 
> A `sealed` class _C_ without an explicitly declared `permits`
> clause, has an implicitly declared `permits` clause that lists as permitted
> subclasses all the classes in the same compilation unit ([7.3]) as _C_ that
> declare _C_ as their direct superclass.

Add:

There must be at least one such class, or a compile-time error occurs.

> 
> ---
> 
> and for interfaces:
> 
> ---
> 
> If a `sealed` interface _I_ does not have an explicit `permits` clause, then
> it has an implicitly declared `permits` clause that lists as permitted
> subtypes all the classes and interfaces in the same compilation unit ([7.3])
> as _I_ that declare _I_ as their direct superinterface.
> 

Add:

There must be at least one such class or interface, or a compile-time error occurs.

> ---



More information about the amber-spec-experts mailing list