Closed exFalso closed 7 years ago
Thanks, this indeed reproduces the issue.
This seems to be a tough one, as Kotlin produces some very weird code:
protected void run();
descriptor: ()V
flags: ACC_PROTECTED
Code:
stack=3, locals=6, args_size=1
0: nop
1: new #19 // class SomeClass
4: dup
5: astore 4
7: astore_3
8: aload_0
9: getfield #23 // Field ac:LAbstractClass;
12: invokevirtual #28 // Method AbstractClass.call:()V
15: getstatic #16 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
18: astore 5
20: aload_3
21: aload 4
23: aload 5
25: invokespecial #32 // Method SomeClass."<init>":(Lkotlin/Unit;)V
28: pop
29: return
LocalVariableTable:
Start Length Slot Name Signature
8 7 1 $i$a$1$function I
1 28 2 $i$f$function I
0 30 0 this LMyFiber;
The SomeClass
instance is allocated in bci 1, but the constructor is only called in bci 25, after the suspendable call. When we want to capture the stack before the call, there is an uninitialized object there, that is basically inaccessible to any Java operation. I am not quite sure how to resolve this.
This code looks so weird that it may be a Kotlin bug.
OTOH, this code:
inline fun function(block: () -> Unit) {
val x = block()
SomeClass(x)
}
is compiled to the more reasonable bytecode:
protected void run();
descriptor: ()V
flags: ACC_PROTECTED
Code:
stack=3, locals=4, args_size=1
0: nop
1: nop
2: aload_0
3: getfield #21 // Field ac:LAbstractClass;
6: invokevirtual #26 // Method AbstractClass.call:()V
9: getstatic #16 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
12: astore_2
13: new #28 // class SomeClass
16: dup
17: aload_2
18: invokespecial #32 // Method SomeClass."<init>":(Lkotlin/Unit;)V
21: pop
22: return
LocalVariableTable:
Start Length Slot Name Signature
2 7 1 $i$a$1$function I
13 9 2 x$iv Lkotlin/Unit;
1 21 3 $i$f$function I
0 23 0 this LMyFiber;
Which works fine with Quasar's instrumentation.
Interesting!
Am I understanding it correctly that inlined lambda calls in argument lists are somehow expanded between the allocation and the initialisation of the outer object? This does smell like a Kotlin bug...
Could a "workaround" be to move all new
s that don't have a corresponding init
before the suspendable call to somewhere after the call (e.g. just before the init)? Although this would require some non-trivial analysis I guess. I'll raise an issue with JB
I guess -- that's certainly what's happening in this case -- but I haven't taken a close look at other cases of Kotlin compilation.
In general, calls to new
and to invokespecial <init>
should be close together. That they're not is technically valid (in terms of the JVM specification), but weird and probably unnecessary. As the new
operation triggers no user code, there is no reason to issue it so early. BTW, this is precisely why Quasar does not support blocking inside constructors, because the object is in an uninitialized state, and the JVM does not allow saving any reference to it, aside from temporary references on the operand stack -- an uninitialized object has this temporary type, uninitialized
, that, as the error state, cannot be considered an Object
.
For reference: https://youtrack.jetbrains.com/issue/KT-19251
I think the way this is going, is that it'll actually be considered a Quasar bug.
What Kotlin is doing is - as you observe - technically valid. They have a change to the compiler that can push new closer to invokespecial, but it changes evaluation ordering and is thus in their view not backwards compatible. The issue being that new
does run user code; the <clinit>
. So changing the emission ordering changes when the class c'tor runs.
My guess is that for Quasar to be fully JVMS compliant in this edge case will require more assistance from the JVM side to be able to process stacks containing uninitialised classes. Kotlin may change its behaviour in future, and I guess we can opt-in to the required behaviour now, but it'd also be required for our users to set this flag and that ... would be annoying.
At the very least, can Quasar perhaps detect this scenario early and bail out with an error message explaining to the user about the Kotlin compiler flag?
This is as far as I got reducing the test case:
Running the above main produces: