From 771cca9c23968ab6a5afb96158d1dc3b99e15775 Mon Sep 17 00:00:00 2001 From: Denis Zharkov Date: Wed, 8 Jun 2016 13:37:55 +0300 Subject: [PATCH] Fix some try-catch block related problems in coroutine transformer Split all try-catch blocks when they intersect suspension points --- .../CoroutineTransformationClassBuilder.kt | 119 +++++++++--- .../codegen/optimization/common/Util.kt | 14 +- .../box/coroutines/inlinedTryCatchFinally.kt | 155 ++++++++++++++++ .../codegen/box/coroutines/nestedTryCatch.kt | 109 +++++++++++ .../tryCatchFinallyWithHandleResult.kt | 172 ++++++++++++++++++ .../coroutines/tryCatchWithHandleResult.kt | 146 +++++++++++++++ .../coroutines/tryFinallyWithHandleResult.kt | 93 ++++++++++ .../codegen/BlackBoxCodegenTestGenerated.java | 30 +++ 8 files changed, 812 insertions(+), 26 deletions(-) create mode 100644 compiler/testData/codegen/box/coroutines/inlinedTryCatchFinally.kt create mode 100644 compiler/testData/codegen/box/coroutines/nestedTryCatch.kt create mode 100644 compiler/testData/codegen/box/coroutines/tryCatchFinallyWithHandleResult.kt create mode 100644 compiler/testData/codegen/box/coroutines/tryCatchWithHandleResult.kt create mode 100644 compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt index a74bac12dc6..47dec903988 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt @@ -17,10 +17,12 @@ package org.jetbrains.kotlin.codegen.coroutines import org.jetbrains.kotlin.codegen.* +import org.jetbrains.kotlin.codegen.optimization.DeadCodeEliminationMethodTransformer import org.jetbrains.kotlin.codegen.optimization.MandatoryMethodTransformer 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.common.removeEmptyCatchBlocks import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer import org.jetbrains.kotlin.resolve.jvm.AsmTypes import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin @@ -64,18 +66,27 @@ 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) - val suspensionPoints = collectSuspensionPoints(methodNode) if (suspensionPoints.isEmpty()) return + for (suspensionPoint in suspensionPoints) { + splitTryCatchBlocksContainingSuspensionPoint(methodNode, suspensionPoint) + } + + // Spill stack to variables before suspension points, try/catch blocks + MandatoryMethodTransformer().transform("fake", methodNode) + + // 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("fake", methodNode) + suspensionPoints.removeAll { it.suspensionCall.next == null && it.suspensionCall.previous == null } + + processUninitializedStores(methodNode) + spillVariables(suspensionPoints, methodNode) - val suspensionPointLabels = suspensionPoints.map { - transformCallAndReturnContinuationLabel(it, methodNode) + val suspensionPointLabels = suspensionPoints.withIndex().map { + transformCallAndReturnContinuationLabel(it.index + 1, it.value, methodNode) } methodNode.instructions.apply { @@ -88,7 +99,7 @@ class CoroutineTransformerMethodVisitor( FieldInsnNode( Opcodes.GETFIELD, classBuilder.thisName, COROUTINE_LABEL_FIELD_NAME, Type.INT_TYPE.descriptor), TableSwitchInsnNode(0, - suspensionPoints.map { it.id }.max()!!, + suspensionPoints.size, defaultLabel, *(arrayOf(startLabel) + suspensionPointLabels)), startLabel)) @@ -101,9 +112,11 @@ class CoroutineTransformerMethodVisitor( }) } + methodNode.removeEmptyCatchBlocks() + } - private fun collectSuspensionPoints(methodNode: MethodNode): List { + private fun collectSuspensionPoints(methodNode: MethodNode): MutableList { val suspensionPoints = mutableListOf() for (methodInsn in methodNode.instructions.asSequence().filterIsInstance()) { @@ -115,7 +128,7 @@ class CoroutineTransformerMethodVisitor( "Expected method call instruction after suspension point, but ${methodInsn.next} found" } - suspensionPoints.add(SuspensionPoint(suspensionPoints.size + 1, methodInsn.next as MethodInsnNode)) + suspensionPoints.add(SuspensionPoint(methodInsn.next as MethodInsnNode)) } else -> error("Unexpected suspension point marker kind '${methodInsn.name}'") @@ -170,7 +183,7 @@ class CoroutineTransformerMethodVisitor( insertBefore(call, FieldInsnNode(Opcodes.PUTFIELD, classBuilder.thisName, fieldName, normalizedType.descriptor)) // restore variable after suspension call - val nextInsnAfterCall = call.next + val nextInsnAfterCall = call.tryCatchBlockEndLabelAfterSuspensionCall.next insertBefore(nextInsnAfterCall, VarInsnNode(Opcodes.ALOAD, 0)) insertBefore(nextInsnAfterCall, FieldInsnNode(Opcodes.GETFIELD, classBuilder.thisName, fieldName, normalizedType.descriptor)) @@ -197,7 +210,16 @@ class CoroutineTransformerMethodVisitor( } } - private fun transformCallAndReturnContinuationLabel(suspension: SuspensionPoint, methodNode: MethodNode): LabelNode { + private val MethodInsnNode.tryCatchBlockEndLabelAfterSuspensionCall: LabelNode + get() { + assert(next is LabelNode) { + "Next instruction after ${this} should be a label, but ${next.javaClass}/${next.opcode} was found" + } + + return next as LabelNode + } + + private fun transformCallAndReturnContinuationLabel(id: Int, suspension: SuspensionPoint, methodNode: MethodNode): LabelNode { val call = suspension.suspensionCall val method = Method(call.name, call.desc) call.desc = Method(method.name, Type.VOID_TYPE, method.argumentTypes).descriptor @@ -209,11 +231,11 @@ class CoroutineTransformerMethodVisitor( insertBefore(call, insnListOf( VarInsnNode(Opcodes.ALOAD, 0), - *withInstructionAdapter { iconst(suspension.id) }.toArray(), + *withInstructionAdapter { iconst(id) }.toArray(), FieldInsnNode( Opcodes.PUTFIELD, classBuilder.thisName, COROUTINE_LABEL_FIELD_NAME, Type.INT_TYPE.descriptor))) - val nextInsnAfterCall = call.next + val nextInsnAfterCall = call.tryCatchBlockEndLabelAfterSuspensionCall.next // Exit insertBefore(nextInsnAfterCall, InsnNode(Opcodes.RETURN)) @@ -221,8 +243,20 @@ class CoroutineTransformerMethodVisitor( // Mark place for continuation insertBefore(nextInsnAfterCall, continuationLabel) - // Check if resumeWithException has been called - insertBefore(nextInsnAfterCall, withInstructionAdapter { + // After suspension point there is always three nodes: L1, NOP, L2 + // And if there are relevant exception handlers, they always start at L2 + // See 'splitTryCatchBlocksContainingSuspensionPoint' + val possibleTryCatchBlockStart = suspension.tryCatchBlocksContinuationLabel + + // Remove NOP as it's unnecessary anymore + assert(possibleTryCatchBlockStart.previous.opcode == Opcodes.NOP) { + "NOP expected but ${possibleTryCatchBlockStart.previous.opcode} was found" + } + remove(possibleTryCatchBlockStart.previous) + + + insert(possibleTryCatchBlockStart, withInstructionAdapter { + // Check if resumeWithException has been called load(2, AsmTypes.OBJECT_TYPE) dup() val noExceptionLabel = Label() @@ -231,15 +265,56 @@ class CoroutineTransformerMethodVisitor( mark(noExceptionLabel) pop() - }) - // Load continuation argument just like suspending function returns it - insertBefore(nextInsnAfterCall, VarInsnNode(Opcodes.ALOAD, 1)) - insertBefore(nextInsnAfterCall, coercionInsns(AsmTypes.OBJECT_TYPE, method.returnType)) + // Load continuation argument just like suspending function returns it + load(1, AsmTypes.OBJECT_TYPE) + StackValue.coerce(AsmTypes.OBJECT_TYPE, method.returnType, this) + }) } return continuationLabel } + + // It's necessary to preserve some sensible invariants like there should be no jump in the middle of try-catch-block + // Also it's important that spilled variables are being restored outside of TCB, + // otherwise they would be treated as uninitialized within catch-block while they can be used there + // How suspension point area will look like after all transformations: + // + // INVOKEVIRTUAL suspensionMethod() + // L1: -- end of all TCB's that are containing the suspension point (inserted by this method) + // RETURN + // L2: -- continuation label (used for the TABLESWITCH) + // (no try-catch blocks here) + // L3: begin/continuation of all TCB's that are containing the suspension point (inserted by this method) + // ... + private fun splitTryCatchBlocksContainingSuspensionPoint(methodNode: MethodNode, suspensionPoint: SuspensionPoint) { + val instructions = methodNode.instructions + val indexOfSuspension = instructions.indexOf(suspensionPoint.suspensionCall) + + val firstLabel = LabelNode() + val secondLabel = LabelNode() + instructions.insert(suspensionPoint.suspensionCall, firstLabel) + // NOP is needed to preventing these label merge + // Here between these labels additional instructions are supposed to be inserted (variables spilling, etc.) + instructions.insert(firstLabel, InsnNode(Opcodes.NOP)) + instructions.insert(firstLabel.next, secondLabel) + + methodNode.tryCatchBlocks = + methodNode.tryCatchBlocks.flatMap { + val isContainingSuspensionPoint = + instructions.indexOf(it.start) < indexOfSuspension && indexOfSuspension < instructions.indexOf(it.end) + + if (isContainingSuspensionPoint) + listOf(TryCatchBlockNode(it.start, firstLabel, it.handler, it.type), + TryCatchBlockNode(secondLabel, it.end, it.handler, it.type)) + else + listOf(it) + } + + suspensionPoint.tryCatchBlocksContinuationLabel = secondLabel + + return + } } private fun Type.fieldNameForVar(index: Int) = descriptor.first() + "$" + index @@ -260,4 +335,6 @@ private fun Type.normalize() = else -> this } -private class SuspensionPoint(val id: Int, val suspensionCall: MethodInsnNode) +private class SuspensionPoint(val suspensionCall: MethodInsnNode) { + lateinit var tryCatchBlocksContinuationLabel: LabelNode +} diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/Util.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/Util.kt index e88552c117a..89d268b9644 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/Util.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/Util.kt @@ -45,11 +45,7 @@ class InsnSequence(val from: AbstractInsnNode, val to: AbstractInsnNode?) : Sequ fun InsnList.asSequence() = InsnSequence(this) fun MethodNode.prepareForEmitting() { - tryCatchBlocks = tryCatchBlocks.filter { tcb -> - InsnSequence(tcb.start, tcb.end).any { insn -> - insn.isMeaningful - } - } + removeEmptyCatchBlocks() // local variables with live ranges starting after last meaningful instruction lead to VerifyError localVariables = localVariables.filter { lv -> @@ -72,6 +68,14 @@ fun MethodNode.prepareForEmitting() { } } +fun MethodNode.removeEmptyCatchBlocks() { + tryCatchBlocks = tryCatchBlocks.filter { tcb -> + InsnSequence(tcb.start, tcb.end).any { insn -> + insn.isMeaningful + } + } +} + abstract class BasicValueWrapper(val wrappedValue: BasicValue?) : BasicValue(wrappedValue?.type) { val basicValue: BasicValue? get() = (wrappedValue as? BasicValueWrapper)?.basicValue ?: wrappedValue diff --git a/compiler/testData/codegen/box/coroutines/inlinedTryCatchFinally.kt b/compiler/testData/codegen/box/coroutines/inlinedTryCatchFinally.kt new file mode 100644 index 00000000000..34234c06041 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/inlinedTryCatchFinally.kt @@ -0,0 +1,155 @@ +// WITH_RUNTIME +var globalResult = "" +var wasCalled = false +class Controller { + val postponedActions = java.util.ArrayList<() -> Unit>() + + suspend fun suspendWithValue(v: String, x: Continuation) { + postponedActions.add { + x.resume(v) + } + } + + suspend fun suspendWithException(e: Exception, x: Continuation) { + postponedActions.add { + x.resumeWithException(e) + } + } + + operator fun handleResult(x: String, c: Continuation) { + globalResult = x + } + + fun run(c: Controller.() -> Continuation) { + c(this).resume(Unit) + while (postponedActions.isNotEmpty()) { + postponedActions[0]() + postponedActions.removeAt(0) + } + } +} + +fun builder(expectException: Boolean = false, coroutine c: Controller.() -> Continuation) { + val controller = Controller() + + globalResult = "#" + wasCalled = false + if (!expectException) { + controller.run(c) + } + else { + try { + controller.run(c) + globalResult = "fail: exception was not thrown" + } catch (e: Exception) { + globalResult = e.message!! + } + } + + if (!wasCalled) { + throw RuntimeException("fail wasCalled") + } + + if (globalResult != "OK") { + throw RuntimeException("fail $globalResult") + } +} + +fun commonThrow(t: Throwable) { + throw t +} + +inline fun tryCatch(t: () -> String, onException: (Exception) -> String) = + try { + t() + } catch (e: java.lang.RuntimeException) { + onException(e) + } + +inline fun tryCatchFinally(t: () -> String, onException: (Exception) -> String, f: () -> Unit) = + try { + t() + } catch (e: java.lang.RuntimeException) { + onException(e) + } finally { + f() + } + +fun box(): String { + builder { + tryCatch( + { + suspendWithValue("") + wasCalled = true + suspendWithValue("OK") + }, + { e -> + suspendWithValue("fail 1") + } + ) + } + + builder { + tryCatch( + { + suspendWithException(RuntimeException("M")) + }, + { e -> + if (e.message != "M") throw RuntimeException("fail 2") + wasCalled = true + suspendWithValue("OK") + } + ) + } + + builder { + tryCatchFinally( + { + suspendWithValue("") + wasCalled = true + suspendWithValue("OK") + }, + { + suspendWithValue("fail 1") + }, + { + suspendWithValue("ignored 1") + wasCalled = true + } + ) + } + + builder { + tryCatchFinally( + { + suspendWithException(RuntimeException("M")) + }, + { e -> + if (e.message != "M") throw RuntimeException("fail 2") + suspendWithValue("OK") + }, + { + suspendWithValue("ignored 2") + wasCalled = true + } + ) + } + + builder { + tryCatchFinally( + { + if (suspendWithValue("56") == "56") return@tryCatchFinally "OK" + suspendWithValue("fail 4") + }, + { + suspendWithValue("fail 5") + }, + { + suspendWithValue("ignored 3") + wasCalled = true + } + ) + } + + return globalResult +} diff --git a/compiler/testData/codegen/box/coroutines/nestedTryCatch.kt b/compiler/testData/codegen/box/coroutines/nestedTryCatch.kt new file mode 100644 index 00000000000..f2c991e8ec9 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/nestedTryCatch.kt @@ -0,0 +1,109 @@ +// WITH_RUNTIME +var globalResult = "" +var wasCalled = false +class Controller { + val postponedActions = java.util.ArrayList<() -> Unit>() + + suspend fun suspendWithValue(v: String, x: Continuation) { + postponedActions.add { + x.resume(v) + } + } + + suspend fun suspendWithException(e: Exception, x: Continuation) { + postponedActions.add { + x.resumeWithException(e) + } + } + + operator fun handleResult(x: String, c: Continuation) { + globalResult = x + } + + fun run(c: Controller.() -> Continuation) { + c(this).resume(Unit) + while (postponedActions.isNotEmpty()) { + postponedActions[0]() + postponedActions.removeAt(0) + } + } +} + +fun builder(expectException: Boolean = false, coroutine c: Controller.() -> Continuation) { + val controller = Controller() + + globalResult = "#" + wasCalled = false + if (!expectException) { + controller.run(c) + } + else { + try { + controller.run(c) + globalResult = "fail: exception was not thrown" + } catch (e: Exception) { + globalResult = e.message!! + } + } + + if (!wasCalled) { + throw RuntimeException("fail wasCalled") + } + + if (globalResult != "OK") { + throw RuntimeException("fail $globalResult") + } +} + +fun commonThrow(t: Throwable) { + throw t +} + +fun box(): String { + builder { + try { + try { + suspendWithValue("") + suspendWithValue("OK") + } + catch (e: RuntimeException) { + suspendWithValue("fail 1") + } + } finally { + wasCalled = true + } + } + + builder { + try { + try { + suspendWithException(RuntimeException("M1")) + } + catch (e: RuntimeException) { + if (e.message != "M1") throw RuntimeException("fail 2") + wasCalled = true + suspendWithValue("OK") + } + } catch (e: Exception) { + suspendWithValue("fail 3") + } + } + + builder { + try { + try { + suspendWithException(Exception("M2")) + } + catch (e: RuntimeException) { + suspendWithValue("fail 4") + } finally { + wasCalled = true + } + } catch (e: Exception) { + if (e.message != "M2") throw RuntimeException("fail 5") + suspendWithValue("OK") + } + } + + return globalResult +} diff --git a/compiler/testData/codegen/box/coroutines/tryCatchFinallyWithHandleResult.kt b/compiler/testData/codegen/box/coroutines/tryCatchFinallyWithHandleResult.kt new file mode 100644 index 00000000000..1bd9e4958fa --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/tryCatchFinallyWithHandleResult.kt @@ -0,0 +1,172 @@ +// WITH_RUNTIME +var globalResult = "" +var wasCalled = false +class Controller { + val postponedActions = java.util.ArrayList<() -> Unit>() + + suspend fun suspendWithValue(v: String, x: Continuation) { + postponedActions.add { + x.resume(v) + } + } + + suspend fun suspendWithException(e: Exception, x: Continuation) { + postponedActions.add { + x.resumeWithException(e) + } + } + + operator fun handleResult(x: String, c: Continuation) { + globalResult = x + } + + fun run(c: Controller.() -> Continuation) { + c(this).resume(Unit) + while (postponedActions.isNotEmpty()) { + postponedActions[0]() + postponedActions.removeAt(0) + } + } +} + +fun builder(expectException: Boolean = false, coroutine c: Controller.() -> Continuation) { + val controller = Controller() + + globalResult = "#" + wasCalled = false + if (!expectException) { + controller.run(c) + } + else { + try { + controller.run(c) + globalResult = "fail: exception was not thrown" + } catch (e: Exception) { + globalResult = e.message!! + } + } + + if (!wasCalled) { + throw RuntimeException("fail wasCalled") + } + + if (globalResult != "OK") { + throw RuntimeException("fail $globalResult") + } +} + +fun commonThrow(t: Throwable) { + throw t +} + +fun box(): String { + builder { + try { + suspendWithValue("") + suspendWithValue("OK") + } catch (e: RuntimeException) { + suspendWithValue("fail 1") + } finally { + suspendWithValue("ignored 1") + wasCalled = true + } + } + + builder { + try { + suspendWithException(RuntimeException("M")) + } catch (e: RuntimeException) { + if (e.message != "M") throw RuntimeException("fail 2") + suspendWithValue("OK") + } finally { + suspendWithValue("ignored 2") + wasCalled = true + } + } + + builder(expectException = true) { + try { + suspendWithException(Exception("OK")) + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 3") + } finally { + suspendWithValue("ignored 3") + wasCalled = true + } + } + + builder(expectException = true) { + try { + suspendWithException(Exception("OK")) + } catch (e: RuntimeException) { + suspendWithValue("fail") + return@builder "xyz" + } finally { + suspendWithValue("ignored 4") + wasCalled = true + } + } + + builder { + try { + suspendWithException(Exception("M2")) + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 4") + } catch (e: Exception) { + if (e.message != "M2") throw Exception("fail 5: ${e.message}") + suspendWithValue("OK") + } finally { + suspendWithValue("ignored 4") + wasCalled = true + } + } + + builder { + try { + suspendWithValue("123") + commonThrow(RuntimeException("M3")) + suspendWithValue("456") + } catch (e: RuntimeException) { + if (e.message != "M3") throw Exception("fail 6: ${e.message}") + suspendWithValue("OK") + } finally { + suspendWithValue("ignored 5") + wasCalled = true + } + } + + builder(expectException = true) { + try { + suspendWithValue("123") + commonThrow(Exception("OK")) + suspendWithValue("456") + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 7") + } finally { + suspendWithValue("ignored 6") + wasCalled = true + } + } + + builder { + try { + suspendWithValue("123") + commonThrow(Exception("M3")) + suspendWithValue("456") + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 8") + } catch (e: Exception) { + if (e.message != "M3") throw Exception("fail 9: ${e.message}") + suspendWithValue("OK") + } finally { + suspendWithValue("ignored 7") + wasCalled = true + } + } + + return globalResult +} diff --git a/compiler/testData/codegen/box/coroutines/tryCatchWithHandleResult.kt b/compiler/testData/codegen/box/coroutines/tryCatchWithHandleResult.kt new file mode 100644 index 00000000000..55c898531b3 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/tryCatchWithHandleResult.kt @@ -0,0 +1,146 @@ +// WITH_RUNTIME +var globalResult = "" +var wasCalled = false +class Controller { + val postponedActions = java.util.ArrayList<() -> Unit>() + + suspend fun suspendWithValue(v: String, x: Continuation) { + postponedActions.add { + x.resume(v) + } + } + + suspend fun suspendWithException(e: Exception, x: Continuation) { + postponedActions.add { + x.resumeWithException(e) + } + } + + operator fun handleResult(x: String, c: Continuation) { + globalResult = x + } + + fun run(c: Controller.() -> Continuation) { + c(this).resume(Unit) + while (postponedActions.isNotEmpty()) { + postponedActions[0]() + postponedActions.removeAt(0) + } + } +} + +fun builder(expectException: Boolean = false, coroutine c: Controller.() -> Continuation) { + val controller = Controller() + + globalResult = "#" + wasCalled = false + if (!expectException) { + controller.run(c) + } + else { + try { + controller.run(c) + globalResult = "fail: exception was not thrown" + } catch (e: Exception) { + globalResult = e.message!! + } + } + + if (!wasCalled) { + throw RuntimeException("fail wasCalled") + } + + if (globalResult != "OK") { + throw RuntimeException("fail $globalResult") + } +} + +fun commonThrow(t: Throwable) { + throw t +} + +fun box(): String { + builder { + try { + suspendWithValue("") + wasCalled = true + suspendWithValue("OK") + } catch (e: RuntimeException) { + suspendWithValue("fail 1") + } + } + + builder { + try { + suspendWithException(RuntimeException("M")) + } catch (e: RuntimeException) { + if (e.message != "M") throw RuntimeException("fail 2") + wasCalled = true + suspendWithValue("OK") + } + } + + builder(expectException = true) { + try { + wasCalled = true + suspendWithException(Exception("OK")) + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 3") + } + } + + builder { + try { + suspendWithException(Exception("M2")) + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 4") + } catch (e: Exception) { + if (e.message != "M2") throw Exception("fail 5: ${e.message}") + wasCalled = true + suspendWithValue("OK") + } + } + + builder { + try { + suspendWithValue("123") + commonThrow(RuntimeException("M3")) + suspendWithValue("456") + } catch (e: RuntimeException) { + if (e.message != "M3") throw Exception("fail 6: ${e.message}") + wasCalled = true + suspendWithValue("OK") + } + } + + builder(expectException = true) { + try { + suspendWithValue("123") + wasCalled = true + commonThrow(Exception("OK")) + suspendWithValue("456") + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 7") + } + } + + builder { + try { + suspendWithValue("123") + commonThrow(Exception("M3")) + suspendWithValue("456") + } catch (e: RuntimeException) { + suspendWithValue("fail") + throw RuntimeException("fail 8") + } catch (e: Exception) { + if (e.message != "M3") throw Exception("fail 9: ${e.message}") + wasCalled = true + suspendWithValue("OK") + } + } + + return globalResult +} diff --git a/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt b/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt new file mode 100644 index 00000000000..e134bf24687 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt @@ -0,0 +1,93 @@ +// WITH_RUNTIME +var globalResult = "" +var wasCalled = false +class Controller { + val postponedActions = java.util.ArrayList<() -> Unit>() + + suspend fun suspendWithValue(v: String, x: Continuation) { + postponedActions.add { + x.resume(v) + } + } + + suspend fun suspendWithException(e: Exception, x: Continuation) { + postponedActions.add { + x.resumeWithException(e) + } + } + + operator fun handleResult(x: String, c: Continuation) { + globalResult = x + } + + fun run(c: Controller.() -> Continuation) { + c(this).resume(Unit) + while (postponedActions.isNotEmpty()) { + postponedActions[0]() + postponedActions.removeAt(0) + } + } +} + +fun builder(expectException: Boolean = false, coroutine c: Controller.() -> Continuation) { + val controller = Controller() + + globalResult = "#" + wasCalled = false + if (!expectException) { + controller.run(c) + } + else { + try { + controller.run(c) + globalResult = "fail: exception was not thrown" + } catch (e: Exception) { + globalResult = e.message!! + } + } + + if (!wasCalled) { + throw RuntimeException("fail wasCalled") + } + + if (globalResult != "OK") { + throw RuntimeException("fail $globalResult") + } +} + +fun commonThrow() { + throw RuntimeException("OK") +} + +fun box(): String { + builder { + try { + suspendWithValue("OK") + } finally { + if (suspendWithValue("G") != "G") throw RuntimeException("fail 1") + wasCalled = true + } + } + + builder(expectException = true) { + try { + suspendWithException(java.lang.RuntimeException("OK")) + } finally { + if (suspendWithValue("G") != "G") throw RuntimeException("fail 2") + wasCalled = true + } + } + + builder(expectException = true) { + try { + suspendWithValue("OK") + commonThrow() + suspendWithValue("OK") + } finally { + if (suspendWithValue("G") != "G") throw RuntimeException("fail 3") + wasCalled = true + } + } + + return globalResult +} diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index 069878f330d..d32a9e8bd34 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -4129,6 +4129,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { doTest(fileName); } + @TestMetadata("inlinedTryCatchFinally.kt") + public void testInlinedTryCatchFinally() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/inlinedTryCatchFinally.kt"); + doTest(fileName); + } + @TestMetadata("innerSuspensionCalls.kt") public void testInnerSuspensionCalls() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/innerSuspensionCalls.kt"); @@ -4141,6 +4147,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { doTest(fileName); } + @TestMetadata("nestedTryCatch.kt") + public void testNestedTryCatch() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/nestedTryCatch.kt"); + doTest(fileName); + } + @TestMetadata("nonLocalReturnFromInlineLambda.kt") public void testNonLocalReturnFromInlineLambda() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/nonLocalReturnFromInlineLambda.kt"); @@ -4207,11 +4219,29 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { doTest(fileName); } + @TestMetadata("tryCatchFinallyWithHandleResult.kt") + public void testTryCatchFinallyWithHandleResult() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tryCatchFinallyWithHandleResult.kt"); + doTest(fileName); + } + + @TestMetadata("tryCatchWithHandleResult.kt") + public void testTryCatchWithHandleResult() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tryCatchWithHandleResult.kt"); + doTest(fileName); + } + @TestMetadata("tryFinallyInsideInlineLambda.kt") public void testTryFinallyInsideInlineLambda() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tryFinallyInsideInlineLambda.kt"); doTest(fileName); } + + @TestMetadata("tryFinallyWithHandleResult.kt") + public void testTryFinallyWithHandleResult() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt"); + doTest(fileName); + } } @TestMetadata("compiler/testData/codegen/box/dataClasses")