JS: coroutines: fixes after code review

This commit is contained in:
Alexey Andreev
2016-11-16 12:13:30 +03:00
parent 43525abda3
commit e2dc7ba37e
10 changed files with 156 additions and 85 deletions
@@ -38,13 +38,14 @@ fun box(): String {
}
result += "ignore"
}
result += "*"
}
finally {
result += "finally"
}
result += "."
}
if (value != "AC!ED!@finally.") return "fail: $value"
if (value != "AC!ED!@*finally.") return "fail: $value"
return "OK"
}
@@ -0,0 +1,28 @@
// WITH_RUNTIME
class Controller {
var result = ""
suspend fun <T> suspendWithResult(value: T, c: Continuation<T>) {
c.resume(value)
}
}
fun builder(coroutine c: Controller.() -> Continuation<Unit>): String {
val controller = Controller()
c(controller).resume(Unit)
return controller.result
}
fun box(): String {
val value = builder {
for (x in listOf("O", "$", "K")) {
if (x == "$") continue
result += suspendWithResult(x)
}
result += "."
}
if (value != "OK.") return "fail: suspend in for body: $value"
return "OK"
}
@@ -0,0 +1,34 @@
// WITH_RUNTIME
// TODO: fix bug in JVM backend and remove this directive
// TARGET_BACKEND: JS
class Controller {
var result = ""
suspend fun <T> suspendWithResult(value: T, c: Continuation<T>) {
result += "["
c.resume(value)
}
}
fun builder(coroutine c: Controller.() -> Continuation<Unit>): String {
val controller = Controller()
c(controller).resume(Unit)
return controller.result
}
fun box(): String {
var value = builder {
for (v in listOf("A", "B", "C")) {
when (v) {
"A" -> result += "A;"
"B" -> result += suspendWithResult(v) + "]"
else -> result += suspendWithResult(v) + "]!"
}
}
}
if (value != "A;[B][C]!") return "fail: suspend as if condition: $value"
return "OK"
}
@@ -4744,6 +4744,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
doTest(fileName);
}
@TestMetadata("forContinue.kt")
public void testForContinue() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forContinue.kt");
doTest(fileName);
}
@TestMetadata("forStatement.kt")
public void testForStatement() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt");
@@ -4744,6 +4744,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
doTest(fileName);
}
@TestMetadata("forContinue.kt")
public void testForContinue() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forContinue.kt");
doTest(fileName);
}
@TestMetadata("forStatement.kt")
public void testForStatement() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt");
@@ -112,29 +112,28 @@ class CoroutineBodyTransformer(
}
override fun visitWhile(x: JsWhile) = splitIfNecessary(x) {
val predecessor = currentBlock
val successor = CoroutineBlock()
val bodyEntryBlock = CoroutineBlock()
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = bodyEntryBlock
if (x.condition != JsLiteral.TRUE) {
currentStatements += JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source }
}
withBreakAndContinue(x, successor, bodyEntryBlock) {
x.body.accept(this)
}
if (x.condition != JsLiteral.TRUE) {
val jsIf = JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source }
bodyEntryBlock.statements.add(0, jsIf)
}
currentBlock.statements += stateAndJump(bodyEntryBlock)
predecessor.statements += stateAndJump(bodyEntryBlock)
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = successor
}
override fun visitDoWhile(x: JsDoWhile) = splitIfNecessary(x) {
val predecessor = currentBlock
val successor = CoroutineBlock()
val bodyEntryBlock = CoroutineBlock()
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = bodyEntryBlock
withBreakAndContinue(x, successor, bodyEntryBlock) {
x.body.accept(this)
@@ -142,10 +141,10 @@ class CoroutineBodyTransformer(
if (x.condition != JsLiteral.TRUE) {
val jsIf = JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source }
currentBlock.statements.add(jsIf)
currentStatements.add(jsIf)
}
currentBlock.statements += stateAndJump(bodyEntryBlock)
predecessor.statements += stateAndJump(bodyEntryBlock)
currentBlock = successor
}
@@ -159,29 +158,26 @@ class CoroutineBodyTransformer(
}
}
val predecessor = currentBlock
val increment = CoroutineBlock()
val successor = CoroutineBlock()
val bodyEntryBlock = CoroutineBlock()
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = bodyEntryBlock
withBreakAndContinue(x, successor, predecessor) {
if (x.condition != null && x.condition != JsLiteral.TRUE) {
currentStatements += JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source }
}
withBreakAndContinue(x, successor, increment) {
x.body.accept(this)
}
val bodyExitBlock = currentBlock
if (x.condition != null && x.condition != JsLiteral.TRUE) {
val jsIf = JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source }
bodyEntryBlock.statements.add(0, jsIf)
}
bodyExitBlock.statements += stateAndJump(increment)
currentStatements += stateAndJump(increment)
currentBlock = increment
x.incrementExpression?.let { JsExpressionStatement(it).accept(this) }
currentStatements += stateAndJump(bodyEntryBlock)
predecessor.statements += stateAndJump(bodyEntryBlock)
currentBlock = successor
}
@@ -201,6 +197,10 @@ class CoroutineBodyTransformer(
currentStatements += jump()
}
/**
* When we perform break, continue or return, we can leave try blocks, so we should update $exceptionHandler correspondingly.
* Also, these try blocks can contain finally clauses, therefore we need to update $finallyPath as well.
*/
private fun jumpWithFinally(targetTryDepth: Int, successor: CoroutineBlock) {
if (targetTryDepth < tryStack.size) {
val tryBlock = tryStack[targetTryDepth]
@@ -223,15 +223,17 @@ class CoroutineBodyTransformer(
val catchBlock = CoroutineBlock()
val finallyBlock = CoroutineBlock()
val tryBlock = TryBlock(catchBlock, if (finallyNode != null) finallyBlock else null)
tryStack += tryBlock
tryStack += TryBlock(catchBlock, if (finallyNode != null) finallyBlock else null)
val oldCatchBlock = currentCatchBlock
currentCatchBlock = catchBlock
currentStatements += exceptionState(catchBlock)
x.tryBlock.statements.forEach { it.accept(this) }
currentStatements += exceptionState(oldCatchBlock)
currentCatchBlock = oldCatchBlock
if (finallyNode != null) {
currentStatements += updateFinallyPath(listOf(successor))
currentStatements += stateAndJump(finallyBlock)
@@ -240,8 +242,6 @@ class CoroutineBodyTransformer(
currentStatements += stateAndJump(successor)
}
currentCatchBlock = oldCatchBlock
// Handle catch node
currentBlock = catchBlock
@@ -279,6 +279,9 @@ class CoroutineBodyTransformer(
currentBlock = successor
}
// There's no implementation for JsSwitch, since we don't generate it. However, when we implement optimization
// for simple `when` statement, we will need to support JsSwitch here
private fun generateFinallyExit() {
val finallyPathRef = JsNameRef(context.finallyPathFieldName, JsLiteral.THIS)
val stateRef = JsNameRef(context.stateFieldName, JsLiteral.THIS)
@@ -320,6 +323,7 @@ class CoroutineBodyTransformer(
override fun visitThrow(x: JsThrow) {
if (throwFunctionName != null) {
// TODO: what if we catch exception in coroutine?
val methodRef = JsNameRef(throwFunctionName, JsNameRef(context.controllerFieldName, JsLiteral.THIS))
val invocation = JsInvocation(methodRef, x.expression).apply {
source = x.source
@@ -46,6 +46,10 @@ fun JsNode.collectNodesToSplit(breakContinueTargets: Map<JsContinue, JsStatement
}
}
// We don't handle JsThrow case here the same way as we do for JsReturn.
// Exception will be caught by the surrounding catch and then dispatched to a corresponding $exceptionState.
// Even if there's no `catch` clause, we generate a fake one that dispatches to a finally block.
override fun visitBreak(x: JsBreak) {
super.visitBreak(x)
@@ -218,43 +222,24 @@ fun JsBlock.replaceLocalVariables(scope: JsScope, context: CoroutineTransformati
}
override fun endVisit(x: JsVars, ctx: JsContext<in JsStatement>) {
val declaredNames = x.vars.map { it.name }
val totalCount = declaredNames.size
val localVarCount = declaredNames.count()
when {
totalCount == localVarCount -> {
val assignments = x.vars.mapNotNull {
val fieldName = scope.getFieldName(it.name)
val initExpression = it.initExpression
if (initExpression != null) {
JsAstUtils.assignment(JsNameRef(fieldName, JsLiteral.THIS), it.initExpression)
}
else {
null
}
}
if (assignments.isNotEmpty()) {
ctx.replaceMe(JsExpressionStatement(JsAstUtils.newSequence(assignments)))
}
else {
ctx.removeMe()
}
val assignments = x.vars.mapNotNull {
val fieldName = scope.getFieldName(it.name)
val initExpression = it.initExpression
if (initExpression != null) {
JsAstUtils.assignment(JsNameRef(fieldName, JsLiteral.THIS), it.initExpression)
}
localVarCount > 0 -> {
for (declaration in x.vars) {
if (declaration.name in localVariables) {
val fieldName = scope.getFieldName(declaration.name)
val assignment = JsAstUtils.assignment(JsNameRef(fieldName, JsLiteral.THIS), declaration.initExpression)
ctx.addPrevious(assignment.makeStmt())
}
else {
ctx.addPrevious(JsVars(declaration))
}
}
ctx.removeMe()
else {
null
}
}
if (assignments.isNotEmpty()) {
ctx.replaceMe(JsExpressionStatement(JsAstUtils.newSequence(assignments)))
}
else {
ctx.removeMe()
}
super.endVisit(x, ctx)
}
}
@@ -191,9 +191,9 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
accept(object : RecursiveJsVisitor() {
var defaultBreakTarget: JsStatement? = null
var breakTargets = mutableMapOf<JsName, JsStatement>()
var breakTargets = mutableMapOf<JsName, JsStatement?>()
var defaultContinueTarget: JsStatement? = null
var continueTargets = mutableMapOf<JsName, JsStatement>()
var continueTargets = mutableMapOf<JsName, JsStatement?>()
override fun visitLabel(x: JsLabel) {
val inner = x.statement
@@ -204,6 +204,8 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
is JsFor -> handleLoop(inner, inner.body, x.name)
is JsSwitch -> handleSwitch(inner, x.name)
else -> {
withBreakAndContinue(x.name, x.statement, null) {
accept(inner)
@@ -218,6 +220,14 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
override fun visitFor(x: JsFor) = handleLoop(x, x.body, null)
override fun visit(x: JsSwitch) = handleSwitch(x, null)
private fun handleSwitch(statement: JsSwitch, label: JsName?) {
withBreakAndContinue(label, statement) {
statement.cases.forEach { accept(it) }
}
}
private fun handleLoop(loop: JsStatement, body: JsStatement, label: JsName?) {
withBreakAndContinue(label, loop, loop) {
body.accept(this)
@@ -262,9 +272,7 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
defaultBreakTarget = breakTargetStatement
if (label != null) {
breakTargets[label] = breakTargetStatement
if (continueTargetStatement != null) {
continueTargets[label] = continueTargetStatement
}
continueTargets[label] = continueTargetStatement
}
if (continueTargetStatement != null) {
defaultContinueTarget = continueTargetStatement
@@ -275,18 +283,8 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
defaultBreakTarget = oldDefaultBreakTarget
defaultContinueTarget = oldDefaultContinueTarget
if (label != null) {
if (oldBreakTarget == null) {
breakTargets.keys -= label
}
else {
breakTargets[label] = oldBreakTarget
}
if (oldContinueTarget == null) {
continueTargets.keys -= label
}
else {
continueTargets[label] = oldContinueTarget
}
breakTargets[label] = oldBreakTarget
continueTargets[label] = oldContinueTarget
}
}
})
@@ -5920,6 +5920,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
doTest(fileName);
}
@TestMetadata("forContinue.kt")
public void testForContinue() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forContinue.kt");
doTest(fileName);
}
@TestMetadata("forStatement.kt")
public void testForStatement() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt");
@@ -5938,6 +5944,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
doTest(fileName);
}
@TestMetadata("switchLikeWhen.kt")
public void testSwitchLikeWhen() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/switchLikeWhen.kt");
doTest(fileName);
}
@TestMetadata("throwFromCatch.kt")
public void testThrowFromCatch() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/throwFromCatch.kt");
@@ -49,13 +49,10 @@ class UsageTracker(
// local named function
if (descriptor is FunctionDescriptor && descriptor.visibility == Visibilities.LOCAL) {
if (descriptor.isCoroutineLambda) {
captureIfNeed(descriptor)
}
else {
assert(!descriptor.getName().isSpecial) { "Function with special name can not be captured, descriptor: $descriptor" }
captureIfNeed(descriptor)
assert(descriptor.isCoroutineLambda || !descriptor.getName().isSpecial) {
"Function with special name can not be captured, descriptor: $descriptor"
}
captureIfNeed(descriptor)
}
// local variable
else if (descriptor is VariableDescriptor && descriptor !is PropertyDescriptor) {