jspecify
jspecify copied to clipboard
Issues relating to Valhalla / primitive classes
Linguistically non-nullable types
The 8 predefined primitive types can never include null, and our specs may have things to say about this (perhaps that @Nullable
is illegal for them, perhaps that they are unaffected by being in @NullMarked
scope, etc.).
In the Valhalla future, any user-defined type might be "linguistically non-nullable" as well, so possibly we would want to address these in whatever same manner as int
. The difference is that the class declaration must be consulted to find out. [EDIT: scratch the last part: non-primitive value type usages will look like MyClass.val
.]
Universal type parameters
Value classes will automatically define both a value type and a reference type. The first is monomorphic -- does not participate in subtyping relationships at all -- and the latter is like a lightweight wrapper that does have all the appropriate supertypes.
So far there would be no way to use a value type as a type argument (it doesn't extend the type parameter's bound, since it doesn't extend anything). Fortunately the reference type can always be used, and is a fairly normal reference type (just lacking identity). This is just like how we can already use List<Integer>
today.
However this precludes many useful optimizations, so they want to move to "universal type parameters." For purposes of type argument bounds checking / wildcard containment (and array covariance), they would treat a value type as though it were a subtype of its corresponding reference type. Of course, to make that work, it will convert between the two as necessary, but it can optimize some of that need away.
The interesting bit is that this means that even at a basic language level, type variables will have parametric nullness, of (what seems like) very much the same kind as we have been talking about. I think (?) we will harmonize with that just fine... certain errors might just get reported sooner (by javac).
(Kevin did finish(?) his post, so if anyone reading this saw only the truncated email, do come back to read the full thing.)
Thanks for continuing to track Valhalla for JSpecify (and for other reasons, but especially for JSpecify :)).
even at a basic language level, type variables will have parametric nullness
Neat! So, to be clear, you're saying that, if I try to change Map
to...
interface Map<universal K, universal V> { ... }
...then I'll get a javac error in method implementations like compute
that contain return null
, since null
is not part of the set of values that make up V
, as V
is potentially a primitive value type?
That indeed is my impression. So that puts them on much of the same slope we're on.
btw, this bug can be a broad "umbrella" for now; anyone can append other issues/questions re:Valhalla; the value in splitting them out properly probably comes later.
But, I do think, at the high level (if not the detailed level), the more we can make JSpecify 1.0 treat primitive classes as already real, the better. It's a bit of a moving target, but I doubt it will move much more at that "high level".
Question: last I heard, there was going to be some kind of (auto?) boxing/unboxing for "primitive classes", which effectively would be a nullable P for any primitive class P IIUC. Is that still on the table, is there syntax for it, and how does it interact with JSpecify?
Yes indeed. This is the same as this part:
Value classes will automatically define both a value type and a reference type.
The latter is always (linguistically) nullable. It's also the thing that has the supertypes you asked for in your extends/implements. When the value type needs a type conversion to any of these supertype reference types, including the automatically defined <MyType>.ref
(or just <MyType>
) itself, that's the "boxing" (thought it is lighter weight than int-to-Integer has ever been).
The "unboxing" is, I think, an automatic type conversion of <MyType>
to <MyType>.val
and just like today's unboxing it constitutes a dereference. (If starting from some supertype, then cast, and maybe get CCE.)
[EDIT: this has been dropped, yay] (Just to not be misleading: the current notion is that a value class MyType
always creates both types MyType.val
and MyType.ref
, but the class signature can specify which one gets the MyType
alias.)
the automatically provided <MyType>.ref
Ok so that could become confusing. People would likely expect these types
to be implicitly nullable (similar to what's been discussed about Void).
OTOH maybe there could be situations where one might want method parameters
or returns of one of these types that aren't meant to be @Nullable? If so
then we'd still want people to use the annotation where needed. While
that's also nice for consistency, requiring explicit @Nullable <MyType>.ref
would
not only be verbose but also I'm expecting annoying for users.
Edited: @Nullable
was previously obfuscated
(hmm did github obfuscate something it thought was an email address?)
To be clear, <MyType>.ref
is a bona fide nullable type just like Integer
is now (and Integer
itself will be one of these things). It's only if the instance came from boxing something that we know it's not null. Does that clear anything up, or was I missing something?
(fixed, hilarious)
What I'm trying to get at is that with Integer
, we (currently at least) ask users to write @Nullable Integer foo()
for a method that can return null
or a boxed integer. Would we (want to) do the same thing with @Nullable <MyType>.ref foo()
? To restate the tradeoffs I'm seeing:
- requiring
@Nullable
would be consistent with what we currently do, and will be necessary if methods might reasonably return a non-null boxed primitive class. But I worry that requiring the annotation may confuse users even if there's a good reason. - not requiring
@Nullable
would mean that we'd need to account for implicitly nullable types in the spec, something that we're not doing ATM. This could also confuse users who might expect needing the annotation for consistency.
For reference we discussed something like this for Void
in #51 .
Ah, okay, the idea is that one would be unlikely to return literal <MyType>.ref
unless one was really intentionally trying to include null, so having to add the @Nullable
would feel redundant?
If I got that right, I think it should be filed separately and discussed in the context of Integer
first and <MyType>.ref
second.
EDIT: milestone changes below were just accidental
It is to some degree "on the table", at this point, to have MyValueType?
be the syntax for getting the reference-type form of MyValueType
. But I think it's understood that this makes waaay more sense if it is a stepping-stone to a broader use-site nullness language feature than a one-off thing.
It seems to me that the best way a user could prepare their generic class for becoming a universal-generic class in the future would be to use JSpecify-compliant nullness analysis, and declare <T extends @Nullable Object>
. Wherever they use @Nullable T
will one day become @Nullable T.ref
and just keep working. Possibly (I don't want to debate this now), JSpecify could document in the future that @Nullable
is automatically inferred for T.ref
.
The class would then be able to handle all three of: nullable reference types, non-nullable reference types, and value types.
If we ever add nullable types to the Java language, then these become <T extends Object?>
and T.ref?
, but it seems to me that Java ought to accept T?
as obviously implying ref coercion. I've been concerned that .ref
could become entirely vestigial -- but maybe T.ref
without the ?
would continue to have certain uses. For example, maybe instead of sorting a large T[]
you want to copy to a T.ref[]
, sort, then copy back? That has no desire to introduce null into the picture. Maybe it doesn't make sense though.