jspecify icon indicating copy to clipboard operation
jspecify copied to clipboard

Issues relating to Valhalla / primitive classes

Open kevinb9n opened this issue 3 years ago • 11 comments

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).

kevinb9n avatar Aug 04 '21 21:08 kevinb9n

(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?

cpovirk avatar Aug 05 '21 16:08 cpovirk

That indeed is my impression. So that puts them on much of the same slope we're on.

kevinb9n avatar Aug 05 '21 17:08 kevinb9n

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".

kevinb9n avatar Aug 05 '21 17:08 kevinb9n

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?

kevin1e100 avatar Aug 05 '21 20:08 kevin1e100

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.)

kevinb9n avatar Aug 05 '21 20:08 kevinb9n

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

kevin1e100 avatar Aug 05 '21 20:08 kevin1e100

(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?

kevinb9n avatar Aug 05 '21 21:08 kevinb9n

(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 .

kevin1e100 avatar Aug 05 '21 22:08 kevin1e100

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

kevinb9n avatar Aug 05 '21 23:08 kevinb9n

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.

kevinb9n avatar Nov 24 '21 17:11 kevinb9n

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.

kevinb9n avatar Aug 12 '22 00:08 kevinb9n