[External] : Re: Consolidating the user model
Brian Goetz
brian.goetz at oracle.com
Thu Nov 4 18:35:33 UTC 2021
>
> An implication of universal generics is that there needs to be
> some common protocol that works on both vals and refs. In the
> val/ref model, that protocol is objects: both vals and refs are
> objects with members that can be accessed via '.'. In the
> value/object model, I'm not quite sure how you'd explain it. Maybe
> there's a third concept here, generalizing how values and objects
> behave.
>
>
> This is on point. I quite honestly forgot that "oh yeah, I don't fully
> understand universal generics yet", and I'll go work on that. It might
> be death to the model I'm clinging to, but in that case I'll become
> pretty good at explaining to people why that model fails, so cool.
>
Generics are often a clarifying lens through which to look at this
problem. We've caught ourselves multiple times trying to locally
optimize, only to find that is an impediment to "generify over all the
things." One of the arguments in favor of "everything is an object" (or
a class, or whatever), aside from its natural uniformity, is that then
generics have a more regular surface to quantify over; generifying over
all types is easier when the types have more in common.
For example, one of the reasons to allow the locution "String.ref" as an
alias for String, while useless, is that it strengthens the notion that
".ref" is a total operator, so "T.ref" makes sense simply by appealing
to substitution, rather than having to give it a more elaborate definition.
When considering universal (erased) generics, we had to totalize the
semantics of all operations, even when some operations are not allowed
under a strict-substitution interpretation. A quick tour (assume `t` is
of type `T`, an unbounded type variable, which is instantiated to `Point`.)
- Assignment to Object or interface (`Object o = t`). In the
language, this is considered a primitive widening (nee boxing)
conversion, but in the VM, this is mere subtyping (QFoo is-a LFoo). This
means that we can use the same `astore` or `putfield` operations to
simply move the value without conversion.
- Assignment to null (`T t = null`). Not all types under T are
nullable, but T is still erased to Object. In this case, we assign a
null and issue an unchecked warning; if that values bubbles out to
non-generic code, the cast to `Point` will catch the null, and treat
this as a form of heap pollution.
- Array covariance (`Object[] os = ts`). The JVM has been upgraded to
support array covariance for primitives, where `Point[] <: Point.ref[]`
(and transitivity gets us to `Object[]`.)
- Synchronization (`synchronized(t)`). Warnings at compile time, IMSE
at runtime.
- Equality (`o == t`). ACMP has been upgraded to understand
primitives, so we can translate as always.
I'm sure I missed a few, but what you see here is a bag of tricks for
creating totality. In some cases (equality, array covariance) we
engineered actual totality into the bytecodes; in some cases
(synchronization) we rely on compile time warnings and runtime errors;
in others, we rely on erasure and lean on existing detection of heap
pollution.
When moving forward to specialized generics, the constraints get
stiffer. We want a model where the _bytecode_ is invariant across
specializations, all specialization operates on the constant pool, and
specialization is strictly optional at runtime (meaning erasure is still
a valid runtime strategy.) This might mean that some total-seeming
operations (e.g., T.default) are either outlawed or require complex
translation through a reflective runtime.
All of this is to say, there may be some hidden indirect constraints
that derive from the desire for a uniform but still specializable
translation.
More information about the valhalla-spec-observers
mailing list