KT-15017 Throwing exception in the end of inline suspend-functions lead to internal compiler error

Suspend function call with a reachable (alive) begin marker and unreachable (dead) end marker
is an exit point for the corresponding coroutine.
It isn't a suspension point, and doesn't introduce a new state in the coroutine FSM.
This commit is contained in:
Dmitry Petrov
2017-01-24 16:46:52 +03:00
parent 3be1174824
commit e05f2eaff6
9 changed files with 137 additions and 22 deletions
@@ -83,10 +83,7 @@ class CoroutineTransformerMethodVisitor(
// Remove unreachable suspension points
// If we don't do this, then relevant frames will not be analyzed, that is unexpected from point of view of next steps (e.g. variable spilling)
DeadCodeEliminationMethodTransformer().transform(classBuilder.thisName, methodNode)
suspensionPoints.removeAll {
it.suspensionCallBegin.next == null && it.suspensionCallBegin.previous == null
}
removeUnreachableSuspensionPointsAndExitPoints(methodNode, suspensionPoints)
processUninitializedStores(methodNode)
@@ -136,6 +133,22 @@ class CoroutineTransformerMethodVisitor(
}
private fun removeUnreachableSuspensionPointsAndExitPoints(methodNode: MethodNode, suspensionPoints: MutableList<SuspensionPoint>) {
val dceResult = DeadCodeEliminationMethodTransformer().transformWithResult(classBuilder.thisName, methodNode)
// If the suspension call begin is alive and suspension call end is dead
// (e.g., an inlined suspend function call ends with throwing a exception -- see KT-15017),
// this is an exit point for the corresponding coroutine.
// It doesn't introduce an additional state to the corresponding coroutine's FSM.
suspensionPoints.forEach {
if (dceResult.isAlive(it.suspensionCallBegin) && dceResult.isRemoved(it.suspensionCallEnd)) {
methodNode.instructions.remove(it.suspensionCallBegin)
}
}
suspensionPoints.removeAll { dceResult.isRemoved(it.suspensionCallBegin) || dceResult.isRemoved(it.suspensionCallEnd) }
}
private fun collectSuspensionPoints(methodNode: MethodNode): MutableList<SuspensionPoint> {
val suspensionPoints = mutableListOf<SuspensionPoint>()
val beforeSuspensionPointMarkerStack = Stack<MethodInsnNode>()
@@ -149,12 +162,7 @@ class CoroutineTransformerMethodVisitor(
}
AFTER_SUSPENSION_POINT_MARKER_NAME -> {
suspensionPoints.add(
SuspensionPoint(
suspensionCallBegin = beforeSuspensionPointMarkerStack.pop(),
suspensionCallEnd = methodInsn
)
)
suspensionPoints.add(SuspensionPoint(beforeSuspensionPointMarkerStack.pop(), methodInsn))
}
}
}
@@ -186,8 +194,8 @@ class CoroutineTransformerMethodVisitor(
for (suspension in suspensionPoints) {
val suspensionCallBegin = suspension.suspensionCallBegin
val suspensionCallEnd = suspension.suspensionCallEnd
assert(frames[suspensionCallEnd.next.index()]?.stackSize == 1) {
assert(frames[suspension.suspensionCallEnd.next.index()]?.stackSize == 1) {
"Stack should be spilled before suspension call"
}
@@ -430,9 +438,3 @@ private class SuspensionPoint(
) {
lateinit var tryCatchBlocksContinuationLabel: LabelNode
}
private val DEFAULT_VALUE_OPCODES =
setOf(Opcodes.ICONST_0, Opcodes.LCONST_0, Opcodes.FCONST_0, Opcodes.DCONST_0, Opcodes.ACONST_NULL,
// GETSTATIC Unit.Instance
Opcodes.GETSTATIC)
@@ -16,15 +16,22 @@
package org.jetbrains.kotlin.codegen.optimization
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
import org.jetbrains.kotlin.codegen.optimization.common.OptimizationBasicInterpreter
import org.jetbrains.kotlin.codegen.optimization.common.isMeaningful
import org.jetbrains.kotlin.codegen.optimization.common.removeEmptyCatchBlocks
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
class DeadCodeEliminationMethodTransformer : MethodTransformer() {
override fun transform(internalClassName: String, methodNode: MethodNode) {
val frames = MethodTransformer.analyze(internalClassName, methodNode, OptimizationBasicInterpreter())
transformWithResult(internalClassName, methodNode)
}
fun transformWithResult(internalClassName: String, methodNode: MethodNode): Result {
val removedNodes = HashSet<AbstractInsnNode>()
val frames = analyze(internalClassName, methodNode, OptimizationBasicInterpreter())
val insnList = methodNode.instructions
val insnsArray = insnList.toArray()
@@ -32,9 +39,19 @@ class DeadCodeEliminationMethodTransformer : MethodTransformer() {
// by try/catch blocks or local variables table.
insnsArray.zip(frames).filter {
it.second == null && it.first.isMeaningful
}.forEach { insnList.remove(it.first) }
}.forEach {
insnList.remove(it.first)
removedNodes.add(it.first)
}
// Remove empty try-catch blocks to make sure we don't break data flow analysis invariants by dead code elimination.
methodNode.removeEmptyCatchBlocks()
return Result(removedNodes)
}
class Result(val removedNodes: Set<AbstractInsnNode>) {
fun isRemoved(node: AbstractInsnNode) = removedNodes.contains(node)
fun isAlive(node: AbstractInsnNode) = !isRemoved(node)
}
}
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.codegen.optimization.common
import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Opcodes.*
import org.jetbrains.org.objectweb.asm.tree.*
@@ -120,3 +121,7 @@ fun insnListOf(vararg insns: AbstractInsnNode) = InsnList().apply { insns.forEac
fun AbstractInsnNode.isStoreOperation(): Boolean = getOpcode() in Opcodes.ISTORE..Opcodes.ASTORE
fun AbstractInsnNode.isLoadOperation(): Boolean = getOpcode() in Opcodes.ILOAD..Opcodes.ALOAD
val AbstractInsnNode?.insnText get() = InlineCodegenUtil.getInsnText(this)
val AbstractInsnNode?.debugText get() =
if (this == null) "<null>" else "${this.javaClass.simpleName}: $insnText"