From b3d29adad922c9f237154ee79985e84ba54e1484 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Fri, 13 May 2016 19:06:17 +0300 Subject: [PATCH] KT-12275 Add JS optimization that transforms the following code ``` do { guard: { // do something break guard; // do something } } while (condition) ``` to ``` do { // do something continue; // do something } while (condition) ``` --- .../kotlin/js/inline/ExpressionDecomposer.kt | 2 +- .../inline/clean/DoWhileGuardElimination.kt | 175 ++++++++++++++++++ .../js/inline/clean/FunctionPostProcessor.kt | 1 + .../optimizer/DoWhileGuardEliminationTest.kt | 27 +++ .../innerBreakInLoopWithoutLabel.optimized.js | 22 +++ .../innerBreakInLoopWithoutLabel.original.js | 22 +++ .../innerContinue.optimized.js | 20 ++ .../innerContinue.original.js | 22 +++ .../simple.optimized.js | 15 ++ .../simple.original.js | 17 ++ 10 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/DoWhileGuardElimination.kt create mode 100644 js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/DoWhileGuardEliminationTest.kt create mode 100644 js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.original.js create mode 100644 js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.original.js create mode 100644 js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.original.js diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ExpressionDecomposer.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ExpressionDecomposer.kt index fe9c8e883d8..acb51b2cd0e 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ExpressionDecomposer.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ExpressionDecomposer.kt @@ -130,7 +130,7 @@ internal class ExpressionDecomposer private constructor( } else { // See KT-12275 - val guardName = scope.declareFreshName("guard$") + val guardName = scope.declareFreshName("guard\$${(loopLabel?.ident.orEmpty())}") val label = JsLabel(guardName).apply { synthetic = true } label.statement = JsBlock(bodyStatements) addStatements(0, listOf(label)) diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/DoWhileGuardElimination.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/DoWhileGuardElimination.kt new file mode 100644 index 00000000000..f56a139bb5d --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/DoWhileGuardElimination.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.js.inline.clean + +import com.google.dart.compiler.backend.js.ast.* + +/** + * During inlining we can sometimes get the following representation for do..while statement: + * + * do { + * guard: { + * // some logic + * break guard; + * // some logic + * } + * } + * while (condition) + * + * A block labeled `guard` can be eliminated with `break guard` converted to `continue`. + */ +internal class DoWhileGuardElimination(private val root: JsStatement) { + private val guardLabels = mutableSetOf() + private var hasChanges = false + private val loopGuardMap = mutableMapOf() + private val guardToLoopLabel = mutableMapOf() + + fun apply(): Boolean { + analyze() + perform() + return hasChanges + } + + private fun analyze() { + object : RecursiveJsVisitor() { + override fun visitLabel(x: JsLabel) { + val statement = x.statement + if (statement is JsDoWhile) { + processDoWhile(statement, x.name) + } + else { + super.visitLabel(x) + } + } + + override fun visitDoWhile(x: JsDoWhile) = processDoWhile(x, null) + + private fun processDoWhile(x: JsDoWhile, label: JsName?) { + val body = x.body + val guard = when (body) { + is JsBlock -> { + val firstStatement = body.statements.first() + if (firstStatement is JsLabel && body.statements.size == 1) { + firstStatement + } + else { + null + } + } + is JsLabel -> body + else -> null + } + + if (guard != null) { + + // When do..while loop has no label and we encounter break guard from nested loop, we can't + // replace this break with continue. Example: + // + // do { + // guard: { + // for (;;) { + // break guard; + // } + // } + // } + // while (condition) + // + // In this case we get simple `continue` that goes to beginning of `for`, not `do`. + // We can't specify label explicitly, since there's no label on `do`. + // See `js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.original.js` + // + // So we don't apply optimization if `do` statement does not have label on it and there's + // a nested loop which has `break guard` statement. + + if (label != null || !findBreakInNestedLoop(guard, guard.name)) { + guardLabels += guard.name + loopGuardMap[x] = guard + guardToLoopLabel[guard.name] = label + } + } + + body.accept(this) + } + + override fun visitFunction(x: JsFunction) { } + }.accept(root) + } + + private fun findBreakInNestedLoop(statement: JsStatement, name: JsName): Boolean { + var result = false + statement.accept(object : RecursiveJsVisitor() { + private var loopLevel = 0 + + override fun visitBreak(x: JsBreak) { + val guardLabel = x.label?.name ?: return + if (guardLabel == name && isInLoop()) { + result = true + } + } + + private fun isInLoop() = loopLevel > 0 + + override fun visitDoWhile(x: JsDoWhile) = enterLoop { super.visitDoWhile(x) } + + override fun visitWhile(x: JsWhile) = enterLoop { super.visitWhile(x) } + + override fun visitFor(x: JsFor) = enterLoop { super.visitFor(x) } + + override fun visitForIn(x: JsForIn) = enterLoop { super.visitForIn(x) } + + private inline fun enterLoop(action: () -> Unit) { + loopLevel++ + action() + loopLevel-- + } + + override fun visitFunction(x: JsFunction) { } + + override fun visitElement(node: JsNode) { + if (!result) { + super.visitElement(node) + } + } + }) + return result + } + + private fun perform() { + object : JsVisitorWithContextImpl() { + override fun visit(x: JsDoWhile, ctx: JsContext): Boolean { + loopGuardMap[x]?.let { guard -> + if (guard.name in guardLabels) { + x.body = accept(guard.statement) + hasChanges = true + return false + } + } + return super.visit(x, ctx) + } + + override fun visit(x: JsBreak, ctx: JsContext): Boolean { + val name = x.label?.name + if (name in guardLabels) { + val target = guardToLoopLabel[name] + ctx.replaceMe(JsContinue(target?.makeRef())) + hasChanges = true + } + return false + } + }.accept(root) + } +} diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/FunctionPostProcessor.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/FunctionPostProcessor.kt index 7ef559094a6..f8b9179477c 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/FunctionPostProcessor.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/FunctionPostProcessor.kt @@ -22,6 +22,7 @@ class FunctionPostProcessor(root: JsFunction) { val optimizations = listOf( { TemporaryAssignmentElimination(root.body).apply() }, { RedundantLabelRemoval(root.body).apply() }, + { DoWhileGuardElimination(root.body).apply() }, { TemporaryVariableElimination(root).apply() }, { RedundantBindElimination(root.body).apply() }, { IfStatementReduction(root.body).apply() }, diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/DoWhileGuardEliminationTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/DoWhileGuardEliminationTest.kt new file mode 100644 index 00000000000..01183662217 --- /dev/null +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/DoWhileGuardEliminationTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.js.test.optimizer + +import org.junit.Test + +class DoWhileGuardEliminationTest : BasicOptimizerTest("do-while-guard-elimination") { + @Test fun simple() = box() + + @Test fun innerContinue() = box() + + @Test fun innerBreakInLoopWithoutLabel() = box() +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.optimized.js b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.optimized.js new file mode 100644 index 00000000000..297a8766458 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.optimized.js @@ -0,0 +1,22 @@ +function box() { + var i = 0; + var counter = 0; + var sum = 0; + do { + guard$: { + i++; + if (i < 5) { + for (var j = 0; j < 10; ++j) { + counter += j; + if (j == 3 && i == 2) break guard$; + } + } + sum += i; + } + } while (i < 10); + + if (sum != 53) return "fail1: " + sum; + if (counter != 141) return "fail2: " + counter; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.original.js b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.original.js new file mode 100644 index 00000000000..297a8766458 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerBreakInLoopWithoutLabel.original.js @@ -0,0 +1,22 @@ +function box() { + var i = 0; + var counter = 0; + var sum = 0; + do { + guard$: { + i++; + if (i < 5) { + for (var j = 0; j < 10; ++j) { + counter += j; + if (j == 3 && i == 2) break guard$; + } + } + sum += i; + } + } while (i < 10); + + if (sum != 53) return "fail1: " + sum; + if (counter != 141) return "fail2: " + counter; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.optimized.js b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.optimized.js new file mode 100644 index 00000000000..04ac6624edd --- /dev/null +++ b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.optimized.js @@ -0,0 +1,20 @@ +function box() { + var i = 0; + var counter = 0; + var sum = 0; + loop: do { + i++; + if (i < 5) { + for (var j = 0; j < 10; ++j) { + counter += j; + if (j == 3 && i == 2) continue loop; + } + } + sum += i; + } while (i < 10); + + if (sum != 53) return "fail1: " + sum; + if (counter != 141) return "fail2: " + counter; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.original.js b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.original.js new file mode 100644 index 00000000000..6852e155851 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/innerContinue.original.js @@ -0,0 +1,22 @@ +function box() { + var i = 0; + var counter = 0; + var sum = 0; + loop: do { + guard$: { + i++; + if (i < 5) { + for (var j = 0; j < 10; ++j) { + counter += j; + if (j == 3 && i == 2) break guard$; + } + } + sum += i; + } + } while (i < 10); + + if (sum != 53) return "fail1: " + sum; + if (counter != 141) return "fail2: " + counter; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.optimized.js b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.optimized.js new file mode 100644 index 00000000000..79f34c53706 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.optimized.js @@ -0,0 +1,15 @@ +function box() { + var i = 0; + var sum = 0; + do { + i++; + if (i == 5) { + continue; + } + sum += i; + } while (i < 10); + + if (sum != 50) return "fail: " + sum; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.original.js b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.original.js new file mode 100644 index 00000000000..07290626afa --- /dev/null +++ b/js/js.translator/testData/js-optimizer/do-while-guard-elimination/simple.original.js @@ -0,0 +1,17 @@ +function box() { + var i = 0; + var sum = 0; + do { + guard$: { + i++; + if (i == 5) { + break guard$; + } + sum += i; + } + } while (i < 10); + + if (sum != 50) return "fail: " + sum; + + return "OK"; +} \ No newline at end of file