High-order tradeoffs:

 - Having R/VObject be classes helps from the pedagogical perspective (it paints an accurate map of the object model.)

 - There were some anomalies raised that were the result of rewriting an Object supertype to RefObject, and some concerns about "all our tables got one level deeper."  I don't really have a strong opinion on these.

 - Using interfaces is less intrusive, but less powerful.

 - None of the approaches give an obvious solution for the "make me a lock Object" problem.


I think the useful new observation in this line of discussion is this:

 - The premise of L-World is that legacy Object-consuming code can keep working with values.
 - We think that's a good thing.
 - But .... we also think there will be some cases where that's not a good thing, and that code will wish it had said `m(RefObject)` instead of `m(Object)`.  [ this is the new thing ]

Combining this with the migration stuff going on in a separate thread, I think what you're saying is you want to be able to take a method:

    m(Object o) { }

and _migrate_ it to be

    m(RefObject o) { }

with a forwarder

    @ForwardTo( m(RefObject) )
    m(Object o);

So that code could, eventually, be migrated to RefObject-consuming code, and all is good again.  And the JIT can see that o is a RefObject and credibly fall back to a legacy interpretation of ACMP and locking.





On 4/12/2019 11:16 AM, Daniel Heidinga wrote:
During the last EG call, I suggested there are benefits to having both 
RefObject and ValObject be classes rather than interfaces.

Old code should be able work with both values and references (that's the 
promise of L-World after all!). New code should be able to opt into whether it 
wants to handle only references or values as there are APIs that may only make 
sense for one or the other. A good example of this is 
java.lang.Reference-subtypes which can't reasonably deal with values. Having 
RefObject in their method signatures would ensure that they're never passed a 
ValObject. (ie: the ctor becomes WeakReference(RefObject o) {...})

For good or ill, interfaces are not checked by the verifier. They're passed as 
though they are object and the interface check is delayed until 
invokeinterface, etc. Using interfaces for Ref/Val Object doesn't provide 
verifier guarantees that the methods will never be passed the wrong type. Javac 
may not generate the code but the VM can't count on that being the case due to 
bytecode instrumentation, other compilers, etc.

Using classes does provide a strong guarantee to the VM which will help to 
alleviate any costs (acmp, array access) for methods that are declared in terms 
of RefObject and ensures that the user is getting exactly what they asked for 
when they declared their method to take RefObject.

It does leave some oddities as you mention:
* new Object() -> returns a new RefObject
* getSuperclass() for old code may return a new superclass (though this may be 
the case already when using instrumentation in the classfile load hook)
* others?

though adding interfaces does as well:
* getInterfaces() would return an interface not declared in the source
* Object would need to implement RefObject for the 'new Object()` case which 
would mean all values implemented RefObject (yuck!)

Letting users say what they mean and have it strongly enforced by the verifier 
is preferable in my view, especially as getSuperclass() issue will only apply 
to old code as newly compiled code will have the correct superclass in its 
classfile.

--Dan


-----"valhalla-spec-experts" <valhalla-spec-experts-boun...@openjdk.java.net> 
wrote: -----

To: valhalla-spec-experts <valhalla-spec-experts@openjdk.java.net>
From: Brian Goetz
Sent by: "valhalla-spec-experts"
Date: 04/08/2019 04:00PM
Subject: RefObject and ValObject

We never reached consensus on how to surface Ref/ValObject.

Here are some places we might want to use these type names:

- Parameter types / variables: we might want to restrict the domain
of a parameter or variable to only hold a reference, or a value:

void m(RefObject ro) { … }

- Type bounds: we might want to restrict the instantiation of a
generic class to only hold a reference (say, because we’re going to
lock on it):

class Foo<T extends RefObject> { … }

- Dynamic tests: if locking on a value is to throw, there must be a
reasonable idiom that users can use to detect lockability without
just trying to lock:

if (x instanceof RefObject) {
synchronized(x) { … }
}

- Ref- or Val-specific methods. This one is more vague, but its
conceivable we may want methods on ValObject that are members of all
values.


There’s been three ways proposed (so far) that we might reflect these
as top types:

- RefObject and ValObject are (somewhat special) classes. We spell
(at least in the class file) “value class” as “class X extends
ValObject”. We implicitly rewrite reference classes at runtime that
extend Object to extend RefObject instead. This has obvious
pedagogical value, but there are some (small) risks of anomalies.

- RefObject and ValObject are interfaces. We ensure that no class
can implement both. (Open question whether an interface could extend
one or the other, acting as an implicit constraint that it only be
implemented by value classes or reference classes.). Harder to do
things like put final implementations of wait/notify in ValObject,
though maybe this isn’t of as much value as it would have been if
we’d done this 25 years ago.

- Split the difference; ValObject is a class, RefObject is an
interface. Sounds weird at first, but acknowledges that we’re
grafting this on to refs after the fact, and eliminates most of the
obvious anomalies.

No matter which way we go, we end up with an odd anomaly: “new
Object()” should yield an instance of RefObject, but we don’t want
Object <: RefObject for obvious reasons. Its possible that “new
Object()” could result in an instance of a _species_ of Object that
implement RefObject… but our theory of species doesn’t quite go there
and it seems a little silly to add new requirements just for this.





Reply via email to