[sealed] Module & package constraints

Dan Smith daniel.smith at oracle.com
Thu Apr 23 18:27:30 UTC 2020

I'm scrutinizing this rule from the sealed types language spec (8.1.6):

"It is a compile-time error if any class named in a permits clause of a sealed class declaration C is not a member of the same module as C. If the sealed class C is a member of the unnamed module, then it is a compile-time error if any class named in the permits clause of the declaration of C is not in the same package as C."

I'm wondering what run-time checks should correspond to this rule (prompted by the "runtime checking of PermittedSubtypes" thread), and whether we want to say something slightly different.

Here are some knobs. Where do we want to turn them?

1) Is it legal to have a permitted subclass/subinterface that does not actually extend the permitting class or interface?

In past discussions about compilation, I feel like we've leaned towards "no", but I don't see a corresponding rule. We're definitely not interested in scenarios involving separate compilation; so if the child and parent disagree, we're compiling an inconsistent set of classes. Seems reasonable to ask the programmer to fix it. If we don't, we end up with downstream design issues like whether to trust the 'permits' clause when defining exhaustiveness.

At run time, it's convenient for the JVM if the answer is "yes". It's expensive to try to validate a PermittedSubtypes attribute all at once (could require O(nm) class loads, n=sealed hierarchy height, m=branching factor; although both are typically small). Instead, the best time to validate is when another class/interface attempts to extend the sealed class/interface.

2) What are the constraints on module membership?

At compilation time, the only way to extend across a module boundary is if the parent permits the child, but the child doesn't reciprocate. (If the child attempts to reciprocate, it won't be able to access the parent class, or there's an illegal module circularity.) So depending on the choice for (1), a restriction on modules may be redundant.

At run time, when we validate an 'extends'/'implements' clause, the mutual references *almost* imply membership in the same run-time module, with one exception: unnamed modules can have circular references. For example: class Foo extends sealed class Bar, Foo belongs to the unnamed module of loader L1, Bar belongs to the unnamed module of loader L2. The loaders can "see" each other, and all class references resolve safely. I'm not sure how likely this scenario is, but it could be a major problem for a program if the JVM starts blowing up after someone makes Bar sealed. (On the other hand, it's really helpful for the JVM if it can require the classes to have to same loader.)

3) What are the constraints on package membership?

The use case here is a class/interface extending a sealed class/interface, both in the same unnamed module.

At compile time, if the child and parent must successfully refer to each other, then we've already guaranteed that they're compiled at the same time. Is there something more to be gained from forcing them into the same package?

(java.lang.reflect.Type is an example of a potential sealed interface that couldn't be declared under this rule if it didn't belong to a named module, because java.lang.Class is in a different package. I imagine lots of real code bases will have relationships like this. Then again, it's not totally unreasonable to tell these code bases they need to declare a module if they want cross-package sealed types.)

At run time, it's fairly straightforward to check for the same package name when we validate a subclass. But it's also doesn't benefit the JVM in any way, so maybe this is more of a language-specific restriction that should be ignored by the JVM. (E.g., maybe Kotlin doesn't mind compiling sealed hierarchies across different packages in the unnamed module, even if Java won't do it.)

More information about the amber-spec-experts mailing list