From d5a808dff680a8f10239c2eb7f895646173e083e Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 1 Nov 2016 19:22:17 +0300 Subject: [PATCH] JS: implementing try/finally in coroutines --- .../coroutines/tryFinallyWithHandleResult.kt | 4 +- .../js/coroutine/CoroutineBodyTransformer.kt | 274 ++++++++++++++---- .../coroutine/CoroutineFunctionTransformer.kt | 33 ++- 3 files changed, 250 insertions(+), 61 deletions(-) diff --git a/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt b/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt index e134bf24687..ecb47dbd848 100644 --- a/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt +++ b/compiler/testData/codegen/box/coroutines/tryFinallyWithHandleResult.kt @@ -2,7 +2,7 @@ var globalResult = "" var wasCalled = false class Controller { - val postponedActions = java.util.ArrayList<() -> Unit>() + val postponedActions = mutableListOf<() -> Unit>() suspend fun suspendWithValue(v: String, x: Continuation) { postponedActions.add { @@ -71,7 +71,7 @@ fun box(): String { builder(expectException = true) { try { - suspendWithException(java.lang.RuntimeException("OK")) + suspendWithException(RuntimeException("OK")) } finally { if (suspendWithValue("G") != "G") throw RuntimeException("fail 2") wasCalled = true diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineBodyTransformer.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineBodyTransformer.kt index 74b6d9aca4a..08b0daae7e3 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineBodyTransformer.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineBodyTransformer.kt @@ -30,23 +30,32 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t private val currentStatements: MutableList get() = currentBlock.statements val resultFieldName by lazy { scope.declareFreshName("\$result") } + val exceptionFieldName by lazy { scope.declareFreshName("\$exception") } val stateFieldName by lazy { scope.declareFreshName("\$state") } val controllerFieldName by lazy { scope.declareFreshName("\$controller") } val exceptionStateName by lazy { scope.declareFreshName("\$exceptionState") } - private val breakTargets = mutableMapOf() - private val continueTargets = mutableMapOf() - private var defaultBreakTarget: JsStatement = JsEmpty - private var defaultContinueTarget: JsStatement = JsEmpty + val finallyPathFieldName by lazy { scope.declareFreshName("\$finallyPath") } + private val breakTargets = mutableMapOf() + private val continueTargets = mutableMapOf() + private var defaultBreakTarget: JumpTarget = JumpTarget(JsEmpty, 0) + private var defaultContinueTarget: JumpTarget = JumpTarget(JsEmpty, 0) private val referencedBlocks = mutableSetOf() private val breakBlocks = mutableMapOf() private val continueBlocks = mutableMapOf() private lateinit var nodesToSplit: Set private var currentCatchBlock = globalCatchBlock + private val tryStack = mutableListOf(TryBlock(globalCatchBlock, null)) + + var hasFinallyBlocks = false + get + private set + + private val currentTryDepth = tryStack.lastIndex fun preProcess(node: JsNode) { val nodes = mutableSetOf() - node.accept(object : RecursiveJsVisitor() { + val visitor = object : RecursiveJsVisitor() { var childrenInSet = false override fun visitInvocation(invocation: JsInvocation) { @@ -57,10 +66,33 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t } } + override fun visitReturn(x: JsReturn) { + super.visitReturn(x) + nodes += x + childrenInSet = true + } + + override fun visitBreak(x: JsBreak) { + super.visitBreak(x) + + // It's a simplification + // TODO: don't split break and continue statements when possible + nodes += x + childrenInSet = true + } + + override fun visitContinue(x: JsContinue) { + super.visitContinue(x) + nodes += x + childrenInSet = true + } + override fun visitElement(node: JsNode) { val oldChildrenInSet = childrenInSet childrenInSet = false + node.acceptChildren(this) + if (childrenInSet) { nodes += node } @@ -68,16 +100,20 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t childrenInSet = oldChildrenInSet } } - }) + } + + visitor.accept(node) + visitor.accept(node) nodesToSplit = nodes + + currentStatements += exceptionState(currentCatchBlock) } fun postProcess(): List { - if (entryBlock == currentBlock) return listOf(entryBlock) - currentBlock.statements += JsReturn() - val orderedBlocks = DFS.topologicalOrder(listOf(entryBlock)) { it.collectTargetBlocks() } + val graph = buildBlockGraph() + val orderedBlocks = DFS.topologicalOrder(listOf(entryBlock)) { graph[it].orEmpty() } val blockIndexes = orderedBlocks.withIndex().associate { (index, block) -> Pair(block, index) } val blockReplacementVisitor = object : JsVisitorWithContextImpl() { @@ -95,6 +131,18 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t val rhs = program.getNumberLiteral(blockIndexes[exceptionTarget]!!) ctx.replaceMe(JsAstUtils.assignment(lhs, rhs).makeStmt()) } + + val finallyPath = x.finallyPath + if (finallyPath != null) { + if (finallyPath.isNotEmpty()) { + val lhs = JsNameRef(finallyPathFieldName, JsLiteral.THIS) + val rhs = JsArrayLiteral(finallyPath.map { program.getNumberLiteral(blockIndexes[it]!!) }) + ctx.replaceMe(JsAstUtils.assignment(lhs, rhs).makeStmt()) + } + else { + ctx.removeMe() + } + } } } for (block in orderedBlocks) { @@ -104,6 +152,35 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t return orderedBlocks } + private fun buildBlockGraph(): Map> { + // That's a little more than DFS due to need of tracking finally paths + + val visitedBlocks = mutableSetOf() + val graph = mutableMapOf>() + + fun visitBlock(block: CoroutineBlock) { + if (block in visitedBlocks) return + + for (finallyPath in block.collectFinallyPaths()) { + for ((finallySource, finallyTarget) in (listOf(block) + finallyPath).zip(finallyPath)) { + if (graph.getOrPut(finallySource) { mutableSetOf() }.add(finallyTarget)) { + visitedBlocks -= finallySource + } + } + } + + visitedBlocks += block + + val successors = graph.getOrPut(block) { mutableSetOf() } + successors += block.collectTargetBlocks() + successors.forEach(::visitBlock) + } + + visitBlock(entryBlock) + + return graph + } + override fun visitBlock(x: JsBlock) = splitIfNecessary(x) { for (statement in x.statements) { statement.accept(this) @@ -146,7 +223,9 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t else -> splitIfNecessary(x) { val successor = CoroutineBlock() - accept(inner) + withBreakAndContinue(x.name, x.statement, successor, null) { + accept(inner) + } if (successor in referencedBlocks) { currentBlock.statements += stateAndJump(successor) currentBlock = successor @@ -237,68 +316,121 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t override fun visitBreak(x: JsBreak) { val targetLabel = x.label?.name - val targetStatement = if (targetLabel == null) { + val (targetStatement, targetTryDepth) = if (targetLabel == null) { defaultBreakTarget } else { breakTargets[targetLabel]!! } - if (targetStatement in nodesToSplit) { - val targetBlock = breakBlocks[targetStatement]!! - referencedBlocks += targetBlock - currentStatements += stateAndJump(targetBlock) - } - else { - currentStatements += x - } + val targetBlock = breakBlocks[targetStatement]!! + referencedBlocks += targetBlock + jumpWithFinally(targetTryDepth, targetBlock) + currentStatements += jump() } override fun visitContinue(x: JsContinue) { val targetLabel = x.label?.name - val targetStatement = if (targetLabel == null) { + val (targetStatement, targetTryDepth) = if (targetLabel == null) { defaultContinueTarget } else { continueTargets[targetLabel]!! } - if (targetStatement in nodesToSplit) { - val targetBlock = continueBlocks[targetStatement]!! - referencedBlocks += targetBlock - currentStatements += stateAndJump(targetBlock) - } - else { - currentStatements += x + val targetBlock = continueBlocks[targetStatement]!! + referencedBlocks += targetBlock + + jumpWithFinally(targetTryDepth, targetBlock) + currentStatements += jump() + } + + private fun jumpWithFinally(targetTryDepth: Int, successor: CoroutineBlock) { + if (targetTryDepth == tryStack.size) return + + val tryBlock = tryStack[targetTryDepth] + currentStatements += exceptionState(tryBlock.catchBlock) + + val relativeFinallyPath = relativeFinallyPath(targetTryDepth) + val fullPath = relativeFinallyPath + successor + if (fullPath.size > 1) { + currentStatements += updateFinallyPath(fullPath.drop(1)) } + currentStatements += state(fullPath[0]) } override fun visitTry(x: JsTry) = splitIfNecessary(x) { - val catch = x.catches.firstOrNull() + val catchNode = x.catches.firstOrNull() + val finallyNode = x.finallyBlock val successor = CoroutineBlock() val catchBlock = CoroutineBlock() + val finallyBlock = CoroutineBlock() + + val tryBlock = TryBlock(catchBlock, if (finallyNode != null) finallyBlock else null) + tryStack += tryBlock + val oldCatchBlock = currentCatchBlock - if (catch != null) { - currentCatchBlock = catchBlock - } + currentCatchBlock = catchBlock + currentStatements += exceptionState(catchBlock) x.tryBlock.statements.forEach { it.accept(this) } - currentBlock.statements += stateAndJump(successor) + currentStatements += exceptionState(oldCatchBlock) + if (finallyNode != null) { + currentStatements += updateFinallyPath(listOf(successor)) + currentStatements += stateAndJump(finallyBlock) + } + else { + currentStatements += stateAndJump(successor) + } currentCatchBlock = oldCatchBlock - if (catch != null) { - currentBlock = catchBlock - currentBlock.statements += JsAstUtils.newVar(catch.parameter.name, JsNameRef(resultFieldName, JsLiteral.THIS)) + // Handle catch node + currentBlock = catchBlock - catch.body.statements.forEach { it.accept(this) } - currentBlock.statements += stateAndJump(successor) + if (finallyNode != null) { + currentStatements += updateFinallyPath(listOf(oldCatchBlock)) + currentStatements += exceptionState(finallyBlock) + } + else { + currentStatements += exceptionState(oldCatchBlock) } + if (catchNode != null) { + currentStatements += JsAstUtils.newVar(catchNode.parameter.name, JsNameRef(exceptionFieldName, JsLiteral.THIS)) + catchNode.body.statements.forEach { it.accept(this) } + + if (finallyNode == null) { + currentStatements += stateAndJump(successor) + } + else { + currentStatements += updateFinallyPath(listOf(successor)) + currentStatements += stateAndJump(finallyBlock) + } + } + + // Handle finally node + if (finallyNode != null) { + currentBlock = finallyBlock + finallyNode.statements.forEach { it.accept(this) } + generateFinallyExit() + hasFinallyBlocks = true + } + + tryStack.removeAt(tryStack.lastIndex) + currentBlock = successor } + private fun generateFinallyExit() { + val finallyPathRef = JsNameRef(finallyPathFieldName, JsLiteral.THIS) + val stateRef = JsNameRef(stateFieldName, JsLiteral.THIS) + val nextState = JsInvocation(JsNameRef("shift", finallyPathRef)) + currentStatements += JsAstUtils.assignment(stateRef, nextState).makeStmt() + currentStatements += jump() + } + override fun visitExpressionStatement(x: JsExpressionStatement) { val expression = x.expression if (expression is JsInvocation && expression.isSuspend) { @@ -323,11 +455,26 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t } override fun visitReturn(x: JsReturn) { + val returnBlock = CoroutineBlock() + val isInFinally = hasEnclosingFinallyBlock() + if (isInFinally) { + jumpWithFinally(0, returnBlock) + } + val returnExpression = x.expression if (returnExpression != null) { x.expression = handleExpression(returnExpression) } - currentStatements += x + + if (isInFinally) { + currentStatements += x.expression?.makeStmt().singletonOrEmptyList() + currentStatements += jump() + currentBlock = returnBlock + currentStatements += JsReturn() + } + else { + currentStatements += x + } } override fun visitThrow(x: JsThrow) { @@ -373,13 +520,10 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t } private fun state(target: CoroutineBlock): List { - val nextPlaceholder = JsDebugger() - nextPlaceholder.targetBlock = target + val placeholder = JsDebugger() + placeholder.targetBlock = target - val exceptionPlaceholder = JsDebugger() - exceptionPlaceholder.targetExceptionBlock = currentCatchBlock - - return listOf(nextPlaceholder, exceptionPlaceholder) + return listOf(placeholder) } private fun jump() = JsContinue() @@ -388,6 +532,19 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t return state(target) + jump() } + private fun exceptionState(target: CoroutineBlock): List { + val placeholder = JsDebugger() + placeholder.targetExceptionBlock = target + + return listOf(placeholder) + } + + private fun updateFinallyPath(path: List): List { + val placeholder = JsDebugger() + placeholder.finallyPath = path + return listOf(placeholder) + } + private inline fun splitIfNecessary(statement: JsStatement, action: () -> Unit) { if (statement in nodesToSplit) { action() @@ -418,15 +575,15 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t Pair(null, null) } - defaultBreakTarget = statement + defaultBreakTarget = JumpTarget(statement, currentTryDepth) if (label != null) { - breakTargets[label] = statement + breakTargets[label] = JumpTarget(statement, currentTryDepth) if (continueBlock != null) { - continueTargets[label] = statement + continueTargets[label] = JumpTarget(statement, currentTryDepth) } } if (continueBlock != null) { - defaultContinueTarget = statement + defaultContinueTarget = JumpTarget(statement, currentTryDepth) } action() @@ -448,8 +605,16 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t } } } + + private fun relativeFinallyPath(targetTryDepth: Int) = tryStack.subList(targetTryDepth, tryStack.size).mapNotNull { it.finallyBlock } + + private fun hasEnclosingFinallyBlock() = tryStack.any { it.finallyBlock != null } } +private data class JumpTarget(val statement: JsStatement, val tryDepth: Int) + +private class TryBlock(val catchBlock: CoroutineBlock, val finallyBlock: CoroutineBlock?) + private fun CoroutineBlock.collectTargetBlocks(): Set { val targetBlocks = mutableSetOf() jsBlock.accept(object : RecursiveJsVisitor() { @@ -460,5 +625,16 @@ private fun CoroutineBlock.collectTargetBlocks(): Set { return targetBlocks } +private fun CoroutineBlock.collectFinallyPaths(): List> { + val finallyPaths = mutableListOf>() + jsBlock.accept(object : RecursiveJsVisitor() { + override fun visitDebugger(x: JsDebugger) { + x.finallyPath?.let { finallyPaths += it } + } + }) + return finallyPaths +} + private var JsDebugger.targetBlock: CoroutineBlock? by MetadataProperty(default = null) -private var JsDebugger.targetExceptionBlock: CoroutineBlock? by MetadataProperty(default = null) \ No newline at end of file +private var JsDebugger.targetExceptionBlock: CoroutineBlock? by MetadataProperty(default = null) +private var JsDebugger.finallyPath: List? by MetadataProperty(default = null) \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineFunctionTransformer.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineFunctionTransformer.kt index 59964f83fd1..ff55414ee25 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineFunctionTransformer.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/coroutine/CoroutineFunctionTransformer.kt @@ -98,6 +98,10 @@ class CoroutineFunctionTransformer( constructor.body.statements.run { assign(bodyTransformer.stateFieldName, program.getNumberLiteral(0)) + assign(bodyTransformer.exceptionStateName, program.getNumberLiteral(0)) + if (bodyTransformer.hasFinallyBlocks) { + assign(bodyTransformer.finallyPathFieldName, JsLiteral.NULL) + } assign(bodyTransformer.controllerFieldName, controllerName.makeRef()) for (localVariable in localVariables) { val value = if (localVariable !in parameterNames) JsLiteral.NULL else localVariable.makeRef() @@ -185,13 +189,24 @@ class CoroutineFunctionTransformer( throwName: JsName?, exceptionName: JsName ): List { + val indexOfGlobalCatch = blocks.indexOf(transformer.globalCatchBlock) + val stateRef = JsNameRef(transformer.stateFieldName, JsLiteral.THIS) + + val isFromGlobalCatch = JsAstUtils.equality(stateRef, program.getNumberLiteral(indexOfGlobalCatch)) val catch = JsCatch(functionWithBody.scope, "e") + val continueWithException = JsBlock( + JsAstUtils.assignment(stateRef.deepCopy(), JsNameRef(transformer.exceptionStateName, JsLiteral.THIS)).makeStmt(), + JsAstUtils.assignment(JsNameRef(transformer.exceptionFieldName, JsLiteral.THIS), catch.parameter.name.makeRef()).makeStmt() + ) + catch.body = JsBlock(JsIf(isFromGlobalCatch, JsThrow(catch.parameter.name.makeRef()), continueWithException)) + + val throwResultRef = JsNameRef(transformer.exceptionFieldName, JsLiteral.THIS) if (throwName != null) { val throwMethodRef = JsNameRef(throwName, JsNameRef(transformer.controllerFieldName, JsLiteral.THIS)) - catch.body = JsBlock(JsReturn(JsInvocation(throwMethodRef, catch.parameter.name.makeRef()))) - - val resultRef = JsNameRef(transformer.resultFieldName, JsLiteral.THIS) - transformer.globalCatchBlock.statements += JsReturn(JsInvocation(throwMethodRef.deepCopy(), resultRef)) + transformer.globalCatchBlock.statements += JsReturn(JsInvocation(throwMethodRef.deepCopy(), throwResultRef)) + } + else { + transformer.globalCatchBlock.statements += JsThrow(throwResultRef) } val cases = blocks.withIndex().map { (index, block) -> @@ -200,20 +215,18 @@ class CoroutineFunctionTransformer( statements += block.statements } } - val stateRef = JsNameRef(transformer.stateFieldName, JsLiteral.THIS) - val switchStatement = JsSwitch(stateRef, cases) - val loop = JsDoWhile(JsLiteral.TRUE, switchStatement) + val switchStatement = JsSwitch(stateRef.deepCopy(), cases) + val loop = JsDoWhile(JsLiteral.TRUE, JsTry(JsBlock(switchStatement), catch, null)) val testExceptionPassed = JsAstUtils.notOptimized( JsAstUtils.typeOfIs(exceptionName.makeRef(), program.getStringLiteral("undefined"))) val stateToException = JsAstUtils.assignment( JsNameRef(transformer.stateFieldName, JsLiteral.THIS), JsNameRef(transformer.exceptionStateName, JsLiteral.THIS)) - val exceptionToResult = JsAstUtils.assignment(JsNameRef(transformer.resultFieldName, JsLiteral.THIS), exceptionName.makeRef()) + val exceptionToResult = JsAstUtils.assignment(JsNameRef(transformer.exceptionFieldName, JsLiteral.THIS), exceptionName.makeRef()) val throwExceptionIfNeeded = JsIf(testExceptionPassed, JsBlock(stateToException.makeStmt(), exceptionToResult.makeStmt())) - val resultBlock = JsBlock(throwExceptionIfNeeded, loop) - return if (throwName != null) listOf(JsTry(resultBlock, catch, null)) else resultBlock.statements + return listOf(throwExceptionIfNeeded, loop) } private fun JsBlock.replaceHandleResult(transformer: CoroutineBodyTransformer) {