Here’s a few thoughts about the null checks vs class resolution issue (many thanks to Brian for his review and his improvements to this document).
Checkcast: is it a null issue or a type issue? There has been some discussion recently on how casts should be translated. While the static compiler has considerable latitude on how to translate language constructs to bytecode, I’d like to make sure that we first have a clean story at the bytecode level, and then take up the translation story (if we still need to.) History, and historical inconveniences Before Valhalla, classfiles had two ways to denote a reference type: the plain name used in CONSTANT_Class_info entries, and the name within an envelope in the field and method descriptors used in CONSTANT_Fieldref_info, CONSTANT_Methodref_info and CONSTANT_InterfaceMethodref_info entries. Having two syntaxes was already a sign that something was weird, but we mostly wrote that off as a historical accident. (Worse, it is not even applied uniformly: arrays are always denoted with their envelope, even in CONSTANT_Class_info entries.) Aesthetics aside, it worked because there was a single unambiguous translation from a class name to a class name with envelope. In the bytecode sequence: aload_1 checkcast #10 // class Foo invokestatic #19 // Method Bar:(LFoo;)V the real meaning of the checkcast was: “I guarantee that the top of stack is a reference to an instance of class Foo (a.k.a. LFoo;), otherwise I’ll throw an exception”. Because null is valid value of all reference types, the JVM does not load the class Foo if the value on the top of the stack is a null, and the verifier is still satisfied that the arguments on the stack match the signature of the method begin invoked. Valhalla turns up the pressure The Valhalla project introduces a new kind of envelope: Q*;. The spelling has remained the same, but it’s meaning has evolved with each prototype: With the v* bytecodes, it was a marker of a new kind of type; In L-world, it became a marker of null-hostility; In the current user model, it has become part of the type. The last two points require some explanation. In L-world, the L and Q flavors of an inline class were projected from a single set of class metadata. In this world, there were really three names — the L projection of C, the Q projection of C, and the class C itself — all of which could be given meaning. So it still could make sense to denote a class just by name — but it’s not clear this was a very good idea. For instance, the devaultvalue bytecode used a CONSTANT_Class_info entry referring to the value class by its plain name. This was unambiguous, because of course the defaultvalue bytecode was referring to the Q-version of the type. (Until some future when we want to apply defaultvalue to reference types, and get null out.) The information was missing from the constant pool entry but deduced from the context because of the implicit assumption that defaultvalue only applies to Q-types. But there were other cases where even such implicit assumptions was not sufficient to deduce which variant of a value type should be used. The checkcast bytecode was one of this cases; it then becoame necessary to denote the class argument with the full envelope in order to express the expected behavior. With the new model of inline types, a class can only have one envelope: either Q if it is an inline type, or L otherwise. Which means that LFoo; and QFoo; are not two variants of a same type, but are in fact two different types. As much as we’d like to ignore it, if Foo is an inline type, it is still possible to forge a reference with type LFoo; — we can create a class that declares a field of type LFoo;, instantiate an instance, and read the field. This LFoo; is a pretty silly type; it cannot interact with any other type, and it can only hold null. But the JVM has to deal with such silly types all the time, such as LBar; when Bar is a nonexistent class. But the reality is that LFoo; and QFoo; are two different types (with completely disjoint value sets!), and we should be honest about it. In the current inline type model, the envelope is an essential part of the identification of a type. Checkcast The legacy behavior of checkcast is on a collision course with the new type system. If the following bytecode sequence: aload_1 checkcast #10 // class Foo still means the same as before — checking that the reference on the top of the stack is of type LFoo; — we have a problem if Foo is an inline class, because if the top of stack holds the null, the checkcast will succeed (because null is indeed a valid value of the otherwise-useless type LFoo;), but this is not really what we had in mind when we asked whether the top of the stack held a Foo. It is easy to assume that this is just yet another bad nullity behavior, and forgivable to make this assumption because null has been the source of so much bad behavior in the past. But this would be putting the blame in the wrong place. In this example, the checkcast operation is simply operating on the wrong type, assuming LFoo; where it has no right to do so — LFoo; and QFoo; are completely distinct types. Quick, plug the hole! There was a lot of discussions on the EG mailing list, and many proposals for ways to restore peace and tranquility. Unfortunately, they all seem to be “quick fixes”, are each likely to generate new problems of their own. Without recapitulating the details of each of them, here’s a summary of their shortcomings: Generate a different sequence of bytecodes when casting to an inline type. This is a workaround for the current checkcast behavior, but is likely to cause trouble for generic code in the future that is specializable over both identity and inline types, because the goal is to share the bytecode across instantiations, and only patch the constant pool or type descriptors. Use Class::cast. Class::cast is a generic method returning T, which is erased to Object, which will hide the type information the verifier needs to guarantee correctness of method arguments types. Use invokedynamic to call custom behavior. This has serious risk of bootstrapping issues. Invent a checknull bytecode. This, and nother solutions focusing of the handling of null, address the symptom, not the problem. The problem is not the handling of null, it is checking that a particular value is within the value set of this particular type. The handling of the null reference should not be handled separately, and should just fall out of addressing the general question of whether a given value is in the value set of a given type. All of these solutions feel like quick fixes that are likely to bite us back in the fiture. Let’s solve the real problem instead. Concrete proposal Let’s fix this by fixing the underlying problem — being explicit about what type we are dealing with. Specifically, from Valhalla and beyond, the way to denote a class type in a classfile is always a class name with an envelope. The two possible envelopes (currently) are the L-envelope for types with a value set containing null, and the Q-envelope for types with a value set not containing null. This has several pleasant consequences: All representations within the class file itself are unified: CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info and CONSTANT_InterfaceMethodref_info will all use the same syntax, with no more translation required between names and type descriptors. Class denotation will be aligned with array denotation, which already uses type descriptors in CONSTANT_Class_info entries. All bytecodes referencing a CONSTANT_Class_info entry will have access to the full denotation, envelope + name, even when the class has not been loaded yet. The verifier will no longer have to translate between names and type descriptors. For the checkcast bytecode, the semantics has to be rephrased: checkcast must ensure that the reference on the top of the stack is within the value set of the type specified in argument, or throw an exception. For L types, this is the same behavior as before, but for Q types, the behavior reflects the value set of the type specified in the classfile. If we have: aload_1 checkcast #10 // class LFoo; then checkcast is being used with a type using a L-envelope, so we still know null is within the value set of Foo without having to load Foo. If the top of stack is not the null reference, then Foo must be loaded to check if this value is part of the remaining of Foo‘s value set, as before. On the other hand, if we have: aload_1 checkcast #11 // class QBar; then checkcast is used with a type using a Q-envelope, which means null cannot be part of the value set of Bar. So if the top of stack contains the null reference, an exception can be thrown (again, without loading Bar if we so desire). If the top of stack is not the null reference, then Bar must be loaded to check if this value is part of Bar‘s value set, as before. The bytecode sequence is the same for both inline types and not-inline-types, with the behavior being controlled by a constant pool entry, making it suitable for our specialization model, and the semantics being derived from the type on which checkcast operates. The benefits of always using a name+envelope will be less significant for other bytecodes, but they still do exist. (For example, using new on an inline type, could be caught at verification time instead of runtime.) Let’s take this opportunity to address the real problem — correct denotation of types — rather than pinning the blame on null (however many sins it committed in the past.) The current loose treatment of non-enveloped names has already caused trouble, and will be a huge source of technical debt going forward. Let’s just pay it off. Backward compatibility Pre-Valhalla class files only know about the L-envelope, so the JVM can continue to deal with them applying the old default translation from names to L*; descriptors. The implementation of checkcast won’t have to check the class file version, as the behavior can be deduced directly from the content of the CONSTANT_Class_info (plain name -> old syntax, name with envelope -> new syntax). New classfiles will reject the old syntax.
