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

Reply via email to