[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