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.


Reply via email to