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 += "ignore"
} }
result += "*"
} }
finally { finally {
result += "finally" result += "finally"
} }
result += "." result += "."
} }
if (value != "AC!ED!@finally.") return "fail: $value" if (value != "AC!ED!@*finally.") return "fail: $value"
return "OK" 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); 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") @TestMetadata("forStatement.kt")
public void testForStatement() throws Exception { public void testForStatement() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt"); String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt");
@@ -4744,6 +4744,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
doTest(fileName); 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") @TestMetadata("forStatement.kt")
public void testForStatement() throws Exception { public void testForStatement() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt"); 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) { override fun visitWhile(x: JsWhile) = splitIfNecessary(x) {
val predecessor = currentBlock
val successor = CoroutineBlock() val successor = CoroutineBlock()
val bodyEntryBlock = CoroutineBlock() val bodyEntryBlock = CoroutineBlock()
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = 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) { withBreakAndContinue(x, successor, bodyEntryBlock) {
x.body.accept(this) x.body.accept(this)
} }
if (x.condition != JsLiteral.TRUE) { currentStatements += stateAndJump(bodyEntryBlock)
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)
currentBlock = successor currentBlock = successor
} }
override fun visitDoWhile(x: JsDoWhile) = splitIfNecessary(x) { override fun visitDoWhile(x: JsDoWhile) = splitIfNecessary(x) {
val predecessor = currentBlock
val successor = CoroutineBlock() val successor = CoroutineBlock()
val bodyEntryBlock = CoroutineBlock() val bodyEntryBlock = CoroutineBlock()
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = bodyEntryBlock currentBlock = bodyEntryBlock
withBreakAndContinue(x, successor, bodyEntryBlock) { withBreakAndContinue(x, successor, bodyEntryBlock) {
x.body.accept(this) x.body.accept(this)
@@ -142,10 +141,10 @@ class CoroutineBodyTransformer(
if (x.condition != JsLiteral.TRUE) { if (x.condition != JsLiteral.TRUE) {
val jsIf = JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source } 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) currentBlock.statements += stateAndJump(bodyEntryBlock)
predecessor.statements += stateAndJump(bodyEntryBlock)
currentBlock = successor currentBlock = successor
} }
@@ -159,29 +158,26 @@ class CoroutineBodyTransformer(
} }
} }
val predecessor = currentBlock
val increment = CoroutineBlock() val increment = CoroutineBlock()
val successor = CoroutineBlock() val successor = CoroutineBlock()
val bodyEntryBlock = CoroutineBlock() val bodyEntryBlock = CoroutineBlock()
currentStatements += stateAndJump(bodyEntryBlock)
currentBlock = 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) x.body.accept(this)
} }
val bodyExitBlock = currentBlock
if (x.condition != null && x.condition != JsLiteral.TRUE) { currentStatements += stateAndJump(increment)
val jsIf = JsIf(JsAstUtils.notOptimized(x.condition), JsBlock(stateAndJump(successor))).apply { source = x.source }
bodyEntryBlock.statements.add(0, jsIf)
}
bodyExitBlock.statements += stateAndJump(increment)
currentBlock = increment currentBlock = increment
x.incrementExpression?.let { JsExpressionStatement(it).accept(this) } x.incrementExpression?.let { JsExpressionStatement(it).accept(this) }
currentStatements += stateAndJump(bodyEntryBlock) currentStatements += stateAndJump(bodyEntryBlock)
predecessor.statements += stateAndJump(bodyEntryBlock)
currentBlock = successor currentBlock = successor
} }
@@ -201,6 +197,10 @@ class CoroutineBodyTransformer(
currentStatements += jump() 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) { private fun jumpWithFinally(targetTryDepth: Int, successor: CoroutineBlock) {
if (targetTryDepth < tryStack.size) { if (targetTryDepth < tryStack.size) {
val tryBlock = tryStack[targetTryDepth] val tryBlock = tryStack[targetTryDepth]
@@ -223,15 +223,17 @@ class CoroutineBodyTransformer(
val catchBlock = CoroutineBlock() val catchBlock = CoroutineBlock()
val finallyBlock = CoroutineBlock() val finallyBlock = CoroutineBlock()
val tryBlock = TryBlock(catchBlock, if (finallyNode != null) finallyBlock else null) tryStack += TryBlock(catchBlock, if (finallyNode != null) finallyBlock else null)
tryStack += tryBlock
val oldCatchBlock = currentCatchBlock val oldCatchBlock = currentCatchBlock
currentCatchBlock = catchBlock currentCatchBlock = catchBlock
currentStatements += exceptionState(catchBlock) currentStatements += exceptionState(catchBlock)
x.tryBlock.statements.forEach { it.accept(this) } x.tryBlock.statements.forEach { it.accept(this) }
currentStatements += exceptionState(oldCatchBlock) currentStatements += exceptionState(oldCatchBlock)
currentCatchBlock = oldCatchBlock
if (finallyNode != null) { if (finallyNode != null) {
currentStatements += updateFinallyPath(listOf(successor)) currentStatements += updateFinallyPath(listOf(successor))
currentStatements += stateAndJump(finallyBlock) currentStatements += stateAndJump(finallyBlock)
@@ -240,8 +242,6 @@ class CoroutineBodyTransformer(
currentStatements += stateAndJump(successor) currentStatements += stateAndJump(successor)
} }
currentCatchBlock = oldCatchBlock
// Handle catch node // Handle catch node
currentBlock = catchBlock currentBlock = catchBlock
@@ -279,6 +279,9 @@ class CoroutineBodyTransformer(
currentBlock = successor 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() { private fun generateFinallyExit() {
val finallyPathRef = JsNameRef(context.finallyPathFieldName, JsLiteral.THIS) val finallyPathRef = JsNameRef(context.finallyPathFieldName, JsLiteral.THIS)
val stateRef = JsNameRef(context.stateFieldName, JsLiteral.THIS) val stateRef = JsNameRef(context.stateFieldName, JsLiteral.THIS)
@@ -320,6 +323,7 @@ class CoroutineBodyTransformer(
override fun visitThrow(x: JsThrow) { override fun visitThrow(x: JsThrow) {
if (throwFunctionName != null) { if (throwFunctionName != null) {
// TODO: what if we catch exception in coroutine?
val methodRef = JsNameRef(throwFunctionName, JsNameRef(context.controllerFieldName, JsLiteral.THIS)) val methodRef = JsNameRef(throwFunctionName, JsNameRef(context.controllerFieldName, JsLiteral.THIS))
val invocation = JsInvocation(methodRef, x.expression).apply { val invocation = JsInvocation(methodRef, x.expression).apply {
source = x.source 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) { override fun visitBreak(x: JsBreak) {
super.visitBreak(x) super.visitBreak(x)
@@ -218,43 +222,24 @@ fun JsBlock.replaceLocalVariables(scope: JsScope, context: CoroutineTransformati
} }
override fun endVisit(x: JsVars, ctx: JsContext<in JsStatement>) { override fun endVisit(x: JsVars, ctx: JsContext<in JsStatement>) {
val declaredNames = x.vars.map { it.name } val assignments = x.vars.mapNotNull {
val totalCount = declaredNames.size val fieldName = scope.getFieldName(it.name)
val localVarCount = declaredNames.count() val initExpression = it.initExpression
if (initExpression != null) {
when { JsAstUtils.assignment(JsNameRef(fieldName, JsLiteral.THIS), it.initExpression)
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()
}
} }
localVarCount > 0 -> { else {
for (declaration in x.vars) { null
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()
} }
} }
if (assignments.isNotEmpty()) {
ctx.replaceMe(JsExpressionStatement(JsAstUtils.newSequence(assignments)))
}
else {
ctx.removeMe()
}
super.endVisit(x, ctx) super.endVisit(x, ctx)
} }
} }
@@ -191,9 +191,9 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
accept(object : RecursiveJsVisitor() { accept(object : RecursiveJsVisitor() {
var defaultBreakTarget: JsStatement? = null var defaultBreakTarget: JsStatement? = null
var breakTargets = mutableMapOf<JsName, JsStatement>() var breakTargets = mutableMapOf<JsName, JsStatement?>()
var defaultContinueTarget: JsStatement? = null var defaultContinueTarget: JsStatement? = null
var continueTargets = mutableMapOf<JsName, JsStatement>() var continueTargets = mutableMapOf<JsName, JsStatement?>()
override fun visitLabel(x: JsLabel) { override fun visitLabel(x: JsLabel) {
val inner = x.statement val inner = x.statement
@@ -204,6 +204,8 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
is JsFor -> handleLoop(inner, inner.body, x.name) is JsFor -> handleLoop(inner, inner.body, x.name)
is JsSwitch -> handleSwitch(inner, x.name)
else -> { else -> {
withBreakAndContinue(x.name, x.statement, null) { withBreakAndContinue(x.name, x.statement, null) {
accept(inner) accept(inner)
@@ -218,6 +220,14 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
override fun visitFor(x: JsFor) = handleLoop(x, x.body, null) 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?) { private fun handleLoop(loop: JsStatement, body: JsStatement, label: JsName?) {
withBreakAndContinue(label, loop, loop) { withBreakAndContinue(label, loop, loop) {
body.accept(this) body.accept(this)
@@ -262,9 +272,7 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
defaultBreakTarget = breakTargetStatement defaultBreakTarget = breakTargetStatement
if (label != null) { if (label != null) {
breakTargets[label] = breakTargetStatement breakTargets[label] = breakTargetStatement
if (continueTargetStatement != null) { continueTargets[label] = continueTargetStatement
continueTargets[label] = continueTargetStatement
}
} }
if (continueTargetStatement != null) { if (continueTargetStatement != null) {
defaultContinueTarget = continueTargetStatement defaultContinueTarget = continueTargetStatement
@@ -275,18 +283,8 @@ fun JsNode.collectBreakContinueTargets(): Map<JsContinue, JsStatement> {
defaultBreakTarget = oldDefaultBreakTarget defaultBreakTarget = oldDefaultBreakTarget
defaultContinueTarget = oldDefaultContinueTarget defaultContinueTarget = oldDefaultContinueTarget
if (label != null) { if (label != null) {
if (oldBreakTarget == null) { breakTargets[label] = oldBreakTarget
breakTargets.keys -= label continueTargets[label] = oldContinueTarget
}
else {
breakTargets[label] = oldBreakTarget
}
if (oldContinueTarget == null) {
continueTargets.keys -= label
}
else {
continueTargets[label] = oldContinueTarget
}
} }
} }
}) })
@@ -5920,6 +5920,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
doTest(fileName); 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") @TestMetadata("forStatement.kt")
public void testForStatement() throws Exception { public void testForStatement() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt"); String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/forStatement.kt");
@@ -5938,6 +5944,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
doTest(fileName); 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") @TestMetadata("throwFromCatch.kt")
public void testThrowFromCatch() throws Exception { public void testThrowFromCatch() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/throwFromCatch.kt"); String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/controlFlow/throwFromCatch.kt");
@@ -49,13 +49,10 @@ class UsageTracker(
// local named function // local named function
if (descriptor is FunctionDescriptor && descriptor.visibility == Visibilities.LOCAL) { if (descriptor is FunctionDescriptor && descriptor.visibility == Visibilities.LOCAL) {
if (descriptor.isCoroutineLambda) { assert(descriptor.isCoroutineLambda || !descriptor.getName().isSpecial) {
captureIfNeed(descriptor) "Function with special name can not be captured, descriptor: $descriptor"
}
else {
assert(!descriptor.getName().isSpecial) { "Function with special name can not be captured, descriptor: $descriptor" }
captureIfNeed(descriptor)
} }
captureIfNeed(descriptor)
} }
// local variable // local variable
else if (descriptor is VariableDescriptor && descriptor !is PropertyDescriptor) { else if (descriptor is VariableDescriptor && descriptor !is PropertyDescriptor) {