I have a tiny reservation about the co-accessibility of both projections,
although it’s a good principle overall. There might be cases (migration
and maybe new code) where the nullable type has wider access than
the inline type, where the type’s contract somehow embraces nullability
to the extent that the .val projection is invisible. But we can cross that
bridge when and if we come to it; I can’t think of compelling examples.
I was worried about this too, wanting to support patterns like "public
interface, private implementation." But then I realized I was thinking
like a compiler rather than a programmer! If we want that, we can just
write:
public interface Foo { ... }
private inline class FooImpl implements Foo { ... }
just like we always did.
(Nitpick: The JVM *fully* checks synchronization of such things dynamically;
it cannot fully check at load time. Given that, it is not a good idea to
partially
check for evidence of synchronization; that just creates the semblance of an
invariant where one does not exist. The JVM tries hard to make static checks
that actually prove things, rather than just “catch user errors”. So, please,
no JVM load-time checks for synchronized methods, except *maybe* within
the inline classes themselves.)
Sure, that's your call. The static compiler can do its own thing.
#### Translation -- classfiles
A val-default inline class `C` is translated to two classfiles, `C` (val
projection) and `C$ref` (ref projection). A ref-default inline class `D` is
translated to two classfiles, `D` (ref projection) and `D$val` (val
projection), as follows:
- The two classfiles are members of the same nest.
- The ref projection is a sealed abstract class that permits only the val
projection.
- Instance fields are lifted onto the val projection.
- Supertypes, methods (including static methods, and including the static
"constructor" factory), and static fields are lifted onto the ref projection.
Method bodies may internally require downcasting to `C.val` to access fields.
This is a little like MVT, in that inline classes end up containing very little
other than fields. This is the right move, IMO, for migrated classes.
Hollowing out *all* inline classes strikes me as over-rotation for the sake
of migration. I see how it allows both cases to have the same translation
strategy, *except for the name*. That’s a pleasing property on paper.
It's more than a pleasing property (though it is that.) Having
translation strategies with big switches in them is where surprising
migration compatibility constraints come from; would we want (say)
changing a method from private to public to be a binary-incompatible
change? These are the kinds of things that happen when the translation
strategy gets too squirrely and ad-hoc.
In the case of reflection, I think we can afford to show a consistent view
for both kinds of inlines, by making all fields and methods appear on
both projections.
The language does this; while reflection is usually classfile-based,
we'd like to treat these two classfiles as being mostly one artifact, so
this seems reasonable to consider.
This to say “the class holding the method” instead of “the C.ref”, we preserve
the immediate goal of supporting migration of Optional etc., but we incur
some migration debt, because it’s harder to move from val-default to
ref-default.
This, I think, is best fixed by adding auto-bridging of some sort later, rather
than over-rotating towards the migration case right now.
(Did I miss some other reason for putting everything on C.ref?)
Putting stuff on C.ref means we can lean harder on inheritance/subtyping
in the translation scheme; putting stuff on C.val means we're always
casting. That might be OK, since there are probably more derefs of the
val than of the ref.