Fix generated code for suspending in the middle of object construction
This commit is contained in:
+5
-2
@@ -21,6 +21,7 @@ import org.jetbrains.kotlin.codegen.optimization.MandatoryMethodTransformer
|
||||
import org.jetbrains.kotlin.codegen.optimization.SKIP_MANDATORY_TRANSFORMATIONS_ANNOTATION_DESC
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.OptimizationBasicInterpreter
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.asSequence
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.insnListOf
|
||||
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
|
||||
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
|
||||
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin
|
||||
@@ -64,7 +65,11 @@ class CoroutineTransformerMethodVisitor(
|
||||
if (methodNode.visibleAnnotations?.none { it.desc == CONTINUATION_METHOD_ANNOTATION_DESC } != false) return
|
||||
methodNode.visibleAnnotations.removeAll { it.desc == CONTINUATION_METHOD_ANNOTATION_DESC }
|
||||
|
||||
// Spill stack to variables before suspension points
|
||||
MandatoryMethodTransformer().transform("fake", methodNode)
|
||||
|
||||
processUninitializedStores(methodNode)
|
||||
|
||||
methodNode.visibleAnnotations.add(AnnotationNode(SKIP_MANDATORY_TRANSFORMATIONS_ANNOTATION_DESC))
|
||||
|
||||
val suspensionPoints = collectSuspensionPoints(methodNode)
|
||||
@@ -259,5 +264,3 @@ private fun Type.normalize() =
|
||||
}
|
||||
|
||||
private class SuspensionPoint(val id: Int, val suspensionCall: MethodInsnNode)
|
||||
|
||||
private fun insnListOf(vararg insns: AbstractInsnNode) = InsnList().apply { insns.forEach { add(it) } }
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2010-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.codegen.coroutines
|
||||
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.CustomFramesMethodAnalyzer
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.OptimizationBasicInterpreter
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.insnListOf
|
||||
import org.jetbrains.org.objectweb.asm.Opcodes
|
||||
import org.jetbrains.org.objectweb.asm.Type
|
||||
import org.jetbrains.org.objectweb.asm.tree.*
|
||||
import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue
|
||||
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
|
||||
import org.jetbrains.org.objectweb.asm.tree.analysis.Interpreter
|
||||
|
||||
/**
|
||||
* In cases like:
|
||||
* NEW
|
||||
* DUP
|
||||
* LDC "First"
|
||||
* ASTORE 1
|
||||
* ASTORE 2
|
||||
* ASTORE 3
|
||||
* INVOKE suspensionPoint
|
||||
* ALOAD 3
|
||||
* ALOAD 2
|
||||
* ALOAD 1
|
||||
* LDC "Second"
|
||||
* INVOKESPECIAL <init>(String;String)
|
||||
|
||||
* Replace store/load instruction with moving NEW/DUP after suspension point:
|
||||
* LDC "First"
|
||||
* ASTORE 1
|
||||
* INVOKE suspensionPoint
|
||||
* ALOAD 1
|
||||
* LDC "Second"
|
||||
* ASTORE 5
|
||||
* ASTORE 4
|
||||
* NEW
|
||||
* DUP
|
||||
* ALOAD 4
|
||||
* ASTORE 5
|
||||
* INVOKESPECIAL <init>(String)
|
||||
*
|
||||
* This is needed because later we spill this variables containing uninitialized object into fields -> leads to VerifyError
|
||||
* Note that this transformation changes semantics a bit (class <clinit> may be invoked by NEW instruction)
|
||||
* TODO: current implementation affects all store/loads of uninitialized objects, even valid ones:
|
||||
* MyClass(try { 1 } catch (e: Exception) { 0 }) // here uninitialized MyClass-object is being spilled before try-catch and then loaded
|
||||
*
|
||||
* How this works:
|
||||
* 1. For each invokespecial <init> determine if NEW uninitialized value was saved to local at least once
|
||||
* 2. If it wasn't then do nothing
|
||||
* 3. If it was then:
|
||||
* - remove all relevant NEW/DUP/LOAD/STORE instructions
|
||||
* - spill rest of constructor arguments to new local vars
|
||||
* - generate NEW/DUP
|
||||
* - restore constructor arguments
|
||||
*/
|
||||
internal fun processUninitializedStores(methodNode: MethodNode) {
|
||||
val interpreter = UninitializedNewValueMarkerInterpreter()
|
||||
val frames = CustomFramesMethodAnalyzer("fake", methodNode, interpreter, ::UninitializedNewValueFrame).analyze()
|
||||
|
||||
for ((index, insn) in methodNode.instructions.toArray().withIndex()) {
|
||||
val frame = frames[index] ?: continue
|
||||
val uninitializedValue = frame.getUninitializedValueForConstructorCall(insn) ?: continue
|
||||
|
||||
val copyUsages: Set<AbstractInsnNode> = interpreter.uninitializedValuesToCopyUsages[uninitializedValue.newInsn]!!
|
||||
assert(copyUsages.size > 0) { "At least DUP copy operation expected" }
|
||||
|
||||
// Value generated by NEW wasn't store to local/field (only DUPed)
|
||||
if (copyUsages.size == 1) continue
|
||||
|
||||
(copyUsages + uninitializedValue.newInsn).forEach {
|
||||
methodNode.instructions.remove(it)
|
||||
}
|
||||
|
||||
val indexOfConstructorArgumentFromTopOfStack = Type.getArgumentTypes((insn as MethodInsnNode).desc).size
|
||||
val storedTypes = arrayListOf<Type>()
|
||||
var nextVarIndex = methodNode.maxLocals
|
||||
|
||||
for (i in 0 until indexOfConstructorArgumentFromTopOfStack) {
|
||||
val value = frame.getStack(frame.stackSize - 1 - i)
|
||||
val type = value.type
|
||||
methodNode.instructions.insertBefore(insn, VarInsnNode(type.getOpcode(Opcodes.ISTORE), nextVarIndex))
|
||||
nextVarIndex += type.size
|
||||
storedTypes.add(type)
|
||||
}
|
||||
methodNode.maxLocals = Math.max(methodNode.maxLocals, nextVarIndex)
|
||||
|
||||
methodNode.instructions.insertBefore(insn, insnListOf(
|
||||
TypeInsnNode(Opcodes.NEW, uninitializedValue.newInsn.desc),
|
||||
InsnNode(Opcodes.DUP)
|
||||
))
|
||||
|
||||
for (type in storedTypes.reversed()) {
|
||||
nextVarIndex -= type.size
|
||||
methodNode.instructions.insertBefore(insn, VarInsnNode(type.getOpcode(Opcodes.ILOAD), nextVarIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class UninitializedNewValue(
|
||||
val newInsn: TypeInsnNode, val internalName: String
|
||||
) : BasicValue(Type.getObjectType(internalName)) {
|
||||
override fun toString() = "UninitializedNewValue(internalName='$internalName')"
|
||||
}
|
||||
|
||||
private class UninitializedNewValueFrame(nLocals: Int, nStack: Int) : Frame<BasicValue>(nLocals, nStack) {
|
||||
override fun execute(insn: AbstractInsnNode, interpreter: Interpreter<BasicValue>?) {
|
||||
val replaceTopValueWithInitialized = getUninitializedValueForConstructorCall(insn) != null
|
||||
|
||||
super.execute(insn, interpreter)
|
||||
|
||||
if (replaceTopValueWithInitialized) {
|
||||
// Drop top value
|
||||
val value = pop() as UninitializedNewValue
|
||||
|
||||
// uninitialized value become initialized after <init> call
|
||||
push(BasicValue(value.type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return value generated by NEW that used as 0-th argument of constructor call or null if current instruction is not constructor call
|
||||
*/
|
||||
private fun Frame<BasicValue>.getUninitializedValueForConstructorCall(
|
||||
insn: AbstractInsnNode
|
||||
): UninitializedNewValue? {
|
||||
if (!insn.isConstructorCall()) return null
|
||||
|
||||
assert(insn.opcode == Opcodes.INVOKESPECIAL) { "Expected opcode Opcodes.INVOKESPECIAL for <init>, but ${insn.opcode} found" }
|
||||
val paramsCountIncludingReceiver = Type.getArgumentTypes((insn as MethodInsnNode).desc).size + 1
|
||||
val newValue = getStack(stackSize - (paramsCountIncludingReceiver + 1)) as? UninitializedNewValue ?: error("Expected value generated with NEW")
|
||||
|
||||
assert(getStack(stackSize - paramsCountIncludingReceiver) is UninitializedNewValue) {
|
||||
"Next value after NEW should be one generated by DUP"
|
||||
}
|
||||
|
||||
return newValue
|
||||
}
|
||||
|
||||
private fun AbstractInsnNode.isConstructorCall() = this is MethodInsnNode && this.name == "<init>"
|
||||
|
||||
private class UninitializedNewValueMarkerInterpreter : OptimizationBasicInterpreter() {
|
||||
val uninitializedValuesToCopyUsages = hashMapOf<AbstractInsnNode, MutableSet<AbstractInsnNode>>()
|
||||
override fun newOperation(insn: AbstractInsnNode): BasicValue? {
|
||||
if (insn.opcode == Opcodes.NEW) {
|
||||
uninitializedValuesToCopyUsages[insn] = mutableSetOf()
|
||||
return UninitializedNewValue(insn as TypeInsnNode, insn.desc)
|
||||
}
|
||||
return super.newOperation(insn)
|
||||
}
|
||||
|
||||
override fun copyOperation(insn: AbstractInsnNode, value: BasicValue?): BasicValue? {
|
||||
if (value is UninitializedNewValue) {
|
||||
uninitializedValuesToCopyUsages[value.newInsn]!!.add(insn)
|
||||
return value
|
||||
}
|
||||
return super.copyOperation(insn, value)
|
||||
}
|
||||
|
||||
override fun merge(v: BasicValue, w: BasicValue): BasicValue {
|
||||
if (v === w) return v
|
||||
if (v === BasicValue.UNINITIALIZED_VALUE || w === BasicValue.UNINITIALIZED_VALUE) {
|
||||
return BasicValue.UNINITIALIZED_VALUE
|
||||
}
|
||||
|
||||
if (v is UninitializedNewValue || w is UninitializedNewValue) {
|
||||
if ((v as? UninitializedNewValue)?.newInsn !== (w as? UninitializedNewValue)?.newInsn) {
|
||||
// Merge of two different ANEW result is possible, but such values should not be used further
|
||||
return BasicValue.UNINITIALIZED_VALUE
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
return super.merge(v, w)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2010-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.codegen.optimization.common
|
||||
|
||||
import org.jetbrains.org.objectweb.asm.tree.MethodNode
|
||||
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
|
||||
import org.jetbrains.org.objectweb.asm.tree.analysis.Interpreter
|
||||
import org.jetbrains.org.objectweb.asm.tree.analysis.Value
|
||||
|
||||
class CustomFramesMethodAnalyzer<V : Value>(
|
||||
owner: String, method: MethodNode, interpreter: Interpreter<V>,
|
||||
private val frameFactory: (Int, Int) -> Frame<V>
|
||||
) : MethodAnalyzer<V>(owner, method, interpreter) {
|
||||
override fun newFrame(nLocals: Int, nStack: Int) = frameFactory(nLocals, nStack)
|
||||
}
|
||||
@@ -119,3 +119,5 @@ val AbstractInsnNode.intConstant: Int? get() =
|
||||
LDC -> (this as LdcInsnNode).cst as? Int
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun insnListOf(vararg insns: AbstractInsnNode) = InsnList().apply { insns.forEach { add(it) } }
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
class Controller {
|
||||
suspend fun suspendHere(x: Continuation<String>) {
|
||||
x.resume("K")
|
||||
}
|
||||
|
||||
suspend fun suspendWithArgument(v: String, x: Continuation<String>) {
|
||||
x.resume(v)
|
||||
}
|
||||
|
||||
suspend fun suspendWithDouble(v: Double, x: Continuation<Double>) {
|
||||
x.resume(v)
|
||||
}
|
||||
}
|
||||
|
||||
fun builder(coroutine c: Controller.() -> Continuation<Unit>) {
|
||||
c(Controller()).resume(Unit)
|
||||
}
|
||||
|
||||
class A(val first: String, val second: String) {
|
||||
override fun toString() = "$first$second"
|
||||
}
|
||||
class B(val first: String, val second: String, val third: String) {
|
||||
override fun toString() = "$first$second$third"
|
||||
}
|
||||
|
||||
class C(val first: Long, val second: Double, val third: String) {
|
||||
override fun toString() = "$first#$second#$third"
|
||||
}
|
||||
|
||||
|
||||
fun box(): String {
|
||||
var result = "OK"
|
||||
|
||||
builder {
|
||||
var local: Any = A("O", suspendHere())
|
||||
|
||||
if (local.toString() != "OK") {
|
||||
result = "fail 1: $local"
|
||||
return@builder
|
||||
}
|
||||
|
||||
local = A(suspendWithArgument("O"), suspendHere())
|
||||
|
||||
if (local.toString() != "OK") {
|
||||
result = "fail 2: $local"
|
||||
return@builder
|
||||
}
|
||||
|
||||
local = B("#", suspendWithArgument("O"), suspendHere())
|
||||
|
||||
if (local.toString() != "#OK") {
|
||||
result = "fail 3: $local"
|
||||
return@builder
|
||||
}
|
||||
|
||||
local = B(suspendWithArgument("#"), "O", suspendHere())
|
||||
|
||||
if (local.toString() != "#OK") {
|
||||
result = "fail 4: $local"
|
||||
return@builder
|
||||
}
|
||||
|
||||
local = B("#", B("", "O", suspendWithArgument("")).toString(), suspendHere())
|
||||
|
||||
if (local.toString() != "#OK") {
|
||||
result = "fail 5: $local"
|
||||
return@builder
|
||||
}
|
||||
|
||||
val condition = local.toString() == "#OK"
|
||||
|
||||
local = B(
|
||||
if (!condition) "1" else suspendWithArgument("#"),
|
||||
if (condition) suspendWithArgument("O") else "2",
|
||||
if (condition) suspendHere() else suspendWithArgument("3"))
|
||||
|
||||
if (local.toString() != "#OK") {
|
||||
result = "fail 5: $local"
|
||||
return@builder
|
||||
}
|
||||
|
||||
local = C(1234567890123L, suspendWithDouble(3.14), suspendWithArgument("OK"))
|
||||
|
||||
if (local.toString() != "1234567890123#3.14#OK") {
|
||||
result = "fail 5: $local"
|
||||
return@builder
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -4176,6 +4176,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/suspendInCycle.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("suspendInTheMiddleOfObjectConstruction.kt")
|
||||
public void testSuspendInTheMiddleOfObjectConstruction() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/suspendInTheMiddleOfObjectConstruction.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("compiler/testData/codegen/box/dataClasses")
|
||||
|
||||
Reference in New Issue
Block a user