From 65876c36eb8d297345e2e5507fd21aa34451ceb9 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 5 May 2016 12:27:33 +0300 Subject: [PATCH] JS/Inlining: introduce removal of expression statements without side effects --- .../jetbrains/kotlin/js/inline/JsInliner.java | 2 +- .../js/inline/clean/FunctionPostProcessor.kt | 17 +- .../clean/IneffectiveStatementElimination.kt | 151 ++++++++++++++++++ .../kotlin/js/inline/util/collectUtils.kt | 4 +- .../js/test/optimizer/BasicOptimizerTest.kt | 2 +- .../IneffectiveStatementEliminationTest.kt | 29 ++++ .../binary.optimized.js | 67 ++++++++ .../binary.original.js | 76 +++++++++ .../conditional.optimized.js | 22 +++ .../conditional.original.js | 23 +++ .../literal.optimized.js | 23 +++ .../literal.original.js | 21 +++ .../unary.optimized.js | 41 +++++ .../unary.original.js | 41 +++++ 14 files changed, 508 insertions(+), 11 deletions(-) create mode 100644 js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/IneffectiveStatementElimination.kt create mode 100644 js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/IneffectiveStatementEliminationTest.kt create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.original.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.original.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.original.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.optimized.js create mode 100644 js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.original.js diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java index 9d87f391fb1..04e673d31aa 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java @@ -103,7 +103,7 @@ public class JsInliner extends JsVisitorWithContextImpl { RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function); processedFunctions.add(function); - new FunctionPostProcessor(function.getBody()).apply(); + new FunctionPostProcessor(function).apply(); assert inProcessFunctions.contains(function); inProcessFunctions.remove(function); 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 73bcac64472..eb1cc864ef0 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 @@ -16,16 +16,17 @@ package org.jetbrains.kotlin.js.inline.clean -import com.google.dart.compiler.backend.js.ast.JsBlock +import com.google.dart.compiler.backend.js.ast.JsFunction -class FunctionPostProcessor(private val root: JsBlock) { +class FunctionPostProcessor(root: JsFunction) { val optimizations = listOf( - { TemporaryAssignmentElimination(root).apply() }, - { RedundantLabelRemoval(root).apply() }, - { TemporaryVariableElimination(root).apply() }, - { IfStatementReduction(root).apply() }, - { DeadCodeElimination(root).apply() }, - { RedundantVariableDeclarationElimination(root).apply() } + { TemporaryAssignmentElimination(root.body).apply() }, + { RedundantLabelRemoval(root.body).apply() }, + { TemporaryVariableElimination(root.body).apply() }, + { IfStatementReduction(root.body).apply() }, + { DeadCodeElimination(root.body).apply() }, + { RedundantVariableDeclarationElimination(root.body).apply() }, + { IneffectiveStatementElimination(root).apply() } ) // TODO: reduce to A || B, A && B if possible diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/IneffectiveStatementElimination.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/IneffectiveStatementElimination.kt new file mode 100644 index 00000000000..742a73212e7 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/IneffectiveStatementElimination.kt @@ -0,0 +1,151 @@ +/* + * 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.* +import com.google.dart.compiler.backend.js.ast.metadata.sideEffects +import com.google.dart.compiler.backend.js.ast.metadata.synthetic +import org.jetbrains.kotlin.js.inline.util.collectLocalVariables +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils + +class IneffectiveStatementElimination(private val root: JsFunction) { + private val localVars = mutableSetOf() + private var hasChanges = false + + fun apply(): Boolean { + analyze() + process() + return hasChanges + } + + private fun analyze() { + localVars += root.collectLocalVariables() + } + + private fun process() { + object : JsVisitorWithContextImpl() { + override fun visit(x: JsExpressionStatement, ctx: JsContext): Boolean { + if (x.synthetic) { + val replacement = replace(x.expression) + if (replacement.size != 1 || replacement[0] != x.expression) { + hasChanges = true + ctx.addPrevious(replacement.map { JsExpressionStatement(it).apply { synthetic = true } }.toList()) + ctx.removeMe() + } + } + return super.visit(x, ctx) + } + }.accept(root.body) + } + + private fun replace(expression: JsExpression): List { + return when (expression) { + is JsNameRef -> { + val qualifier = expression.qualifier + if (qualifier == null && expression.name in localVars) { + listOf() + } + else if (!expression.sideEffects) { + if (qualifier != null) replace(qualifier) else listOf() + } + else { + listOf(expression) + } + } + + is JsUnaryOperation -> { + when (expression.operator) { + JsUnaryOperator.DEC, JsUnaryOperator.INC, JsUnaryOperator.DELETE -> listOf(expression) + else -> replace(expression.arg) + } + } + + is JsBinaryOperation -> { + if (expression.sideEffects) { + when (expression.operator) { + JsBinaryOperator.AND, JsBinaryOperator.OR -> { + val right = replace(expression.arg2) + if (right.isEmpty()) replace(expression.arg1) else listOf(expression) + } + JsBinaryOperator.INOP, JsBinaryOperator.INSTANCEOF -> listOf(expression) + else -> { + if (!expression.operator.isAssignment) { + replace(expression.arg1) + replace(expression.arg2) + } + else { + listOf(expression) + } + } + } + } + else { + listOf(expression) + } + } + + is JsInvocation -> { + if (!expression.sideEffects) { + replace(expression.qualifier) + replaceMany(expression.arguments) + } + else { + listOf(expression) + } + } + + is JsNew -> { + if (!expression.sideEffects) { + replace(expression.constructorExpression) + replaceMany(expression.arguments) + } + else { + listOf(expression) + } + } + + is JsConditional -> { + val thenExpr = replace(expression.thenExpression) + val elseExpr = replace(expression.elseExpression) + when { + thenExpr.isEmpty() && elseExpr.isEmpty() -> replace(expression.testExpression) + thenExpr.isEmpty() -> listOf(JsAstUtils.or(expression.testExpression, expression.elseExpression)) + elseExpr.isEmpty() -> listOf(JsAstUtils.and(expression.testExpression, expression.thenExpression)) + else -> listOf(expression) + } + } + + is JsArrayAccess -> { + if (!expression.sideEffects) { + replace(expression.arrayExpression) + replace(expression.indexExpression) + } + else { + listOf(expression) + } + } + + is JsLiteral.JsValueLiteral -> listOf() + + is JsArrayLiteral -> replaceMany(expression.expressions) + + is JsObjectLiteral -> expression.propertyInitializers.map { replace(it.labelExpr) + replace(it.valueExpr) }.flatten() + + is JsFunction -> if (expression.name == null) listOf() else listOf(expression) + + else -> listOf(expression) + } + } + + private fun replaceMany(expressions: List) = expressions.map { replace(it) }.flatten() +} diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt index 23418fc922b..511742546ad 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 JetBrains s.r.o. + * 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. @@ -125,6 +125,8 @@ fun collectDefinedNames(scope: JsNode): Set { fun JsFunction.collectFreeVariables() = collectUsedNames(body) - collectDefinedNames(body) - parameters.map { it.name } +fun JsFunction.collectLocalVariables() = collectDefinedNames(body) + parameters.map { it.name } + fun collectJsProperties(scope: JsNode): IdentityHashMap { val collector = PropertyCollector() collector.accept(scope) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt index 4ca4db60ebe..996e89e537c 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/BasicOptimizerTest.kt @@ -72,7 +72,7 @@ abstract class BasicOptimizerTest(private var basePath: String) { for (statement in unoptimizedAst) { object : RecursiveJsVisitor() { override fun visitFunction(x: JsFunction) { - FunctionPostProcessor(x.body).apply() + FunctionPostProcessor(x).apply() super.visitFunction(x) } }.accept(statement) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/IneffectiveStatementEliminationTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/IneffectiveStatementEliminationTest.kt new file mode 100644 index 00000000000..f2e679d2293 --- /dev/null +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/optimizer/IneffectiveStatementEliminationTest.kt @@ -0,0 +1,29 @@ +/* + * 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 IneffectiveStatementEliminationTest() : BasicOptimizerTest("ineffective-statement-elimination") { + @Test fun binary() = box() + + @Test fun unary() = box() + + @Test fun conditional() = box() + + @Test fun literal() = box() +} diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.optimized.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.optimized.js new file mode 100644 index 00000000000..e1968dd3d2f --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.optimized.js @@ -0,0 +1,67 @@ +var global = 0; + +function se() { + return ++global; +} + +function se2(value) { + ++global; + return value; +} + +function test1() { + se(); se(); se(); se(); se(); + se(); se(); se(); se(); se(); + se(); se(); se(); se(); se(); + se(); se(); se(); se(); se(); + se(); se(); se(); se(); se(); + se(); se(); se(); se(); se(); + se(); se(); se(); +} + +function test2() { + try { + se() in se(); + return "fail2a: `in` should not be removed" + } + catch (e) { + // Do nothing + } + + try { + se() instanceof se(); + return "fail2b: `instanceof` should not be removed" + } + catch (e) { + // Do nothing + } + + return "OK" +} + +function test3() { + var x = true; + var y = false; + + se2(true); + x && se2(true); + se2(true) && se2(true); + + se2(false); + y || se2(false); + se2(false) || se2(false); +} + +function box() { + test1(); + if (global != 33) return "fail1: " + global; + + var result = test2(); + if (result !== "OK") return result; + if (global != 37) return "fail2: " + global; + + test3(); + if (global != 45) return "fail3: " + global; + + return "OK"; +} diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.original.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.original.js new file mode 100644 index 00000000000..44d6d6b45a8 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/binary.original.js @@ -0,0 +1,76 @@ +var global = 0; + +function se() { + return ++global; +} + +function se2(value) { + ++global; + return value; +} + +function test1() { + var $a = (se() - se() + se()) * se(); + var $b = se() % se() / se(); + var $c = se() >> se(); + var $d = se() << se(); + var $e = se() >>> se(); + var $f = se() | se() & se() ^ se(); + var $g1 = se() == se(); + var $g2 = se() === se(); + var $g3 = se() != se(); + var $g4 = se() !== se(); + var $h1 = se() > se(); + var $h2 = se() >= se(); + var $h3 = se() < se(); + var $h4 = se() <= se(); +} + +function test2() { + try { + var $a = se() in se(); + return "fail2a: `in` should not be removed" + } + catch (e) { + // Do nothing + } + + try { + var $b = se() instanceof se(); + return "fail2b: `instanceof` should not be removed" + } + catch (e) { + // Do nothing + } + + return "OK" +} + +function test3() { + var x = true; + var y = false; + + var $a1 = se2(true) && x; + var $a2 = x && se2(true); + var $a3 = se2(true) && se2(true); + var $a4 = x && x; + + var $b1 = se2(false) || y; + var $b2 = y || se2(false); + var $b3 = se2(false) || se2(false); + var $b4 = y || y; +} + +function box() { + test1(); + if (global != 33) return "fail1: " + global; + + var result = test2(); + if (result !== "OK") return result; + if (global != 37) return "fail2: " + global; + + test3(); + if (global != 45) return "fail3: " + global; + + return "OK"; +} diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.optimized.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.optimized.js new file mode 100644 index 00000000000..12c6bd06756 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.optimized.js @@ -0,0 +1,22 @@ +var global = 0; + +function se(value) { + ++global; + return value; +} + +function test() { + var x = true; + se(x); + x ? se("foo") : se("bar"); + x || se("bar"); + x && se("foo"); + se(x) ? se("foo") : se("bar"); +} + +function box() { + test(); + if (global != 5) return "fail1: " + global; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.original.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.original.js new file mode 100644 index 00000000000..b083d9f5ea8 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/conditional.original.js @@ -0,0 +1,23 @@ +var global = 0; + +function se(value) { + ++global; + return value; +} + +function test() { + var x = true; + var $a = se(x) ? "foo" : "bar"; + var $b = x ? "foo" : "bar"; + var $c = x ? se("foo") : se("bar"); + var $d = x ? "foo" : se("bar"); + var $e = x ? se("foo") : "bar"; + var $f = se(x) ? se("foo") : se("bar"); +} + +function box() { + test(); + if (global != 5) return "fail1: " + global; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.optimized.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.optimized.js new file mode 100644 index 00000000000..06ffcc327fb --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.optimized.js @@ -0,0 +1,23 @@ +var global = 0; + +function se(value) { + ++global; + return value; +} + +function test() { + se(2); + se(3); + se('foo'); + se('bar'); + se('y'); + se(3); + se('null'); +} + +function box() { + test(); + if (global != 7) return "fail: " + test(); + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.original.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.original.js new file mode 100644 index 00000000000..b59ce788411 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/literal.original.js @@ -0,0 +1,21 @@ +var global = 0; + +function se(value) { + ++global; + return value; +} + +function test() { + var $a = { x: se(2), y: se(3) }; + var $b = [se("foo"), se("bar")]; + var $c = "x" + se("y"); + var $d = 2 + se(3); + var $e = se("null") + null; +} + +function box() { + test(); + if (global != 7) return "fail: " + test(); + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.optimized.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.optimized.js new file mode 100644 index 00000000000..eda95e7c3f2 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.optimized.js @@ -0,0 +1,41 @@ +var global = 0; + +function se(value) { + ++global; + return value; +} + +function test1() { + se(23); + se(23); + se(false); + se(-1); + se(23); + se(23); +} + +function test2() { + ++global; + global++; + --global; + global--; +} + +function test3() { + var obj = { x: 2, y: 3}; + delete obj.x; + return ("x" in obj) + ":" + ("y" in obj); +} + +function box() { + test1(); + if (global != 6) return "fail1: " + global; + + test2(); + if (global != 6) return "fail2: " + global; + + var result = test3(); + if (result != "false:true") return "fail3: " + result; + + return "OK"; +} \ No newline at end of file diff --git a/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.original.js b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.original.js new file mode 100644 index 00000000000..835ce7cc9b6 --- /dev/null +++ b/js/js.translator/testData/js-optimizer/ineffective-statement-elimination/unary.original.js @@ -0,0 +1,41 @@ +var global = 0; + +function se(value) { + ++global; + return value; +} + +function test1() { + var $a = +se(23); + var $b = -se(23); + var $c = !se(false); + var $d = ~se(-1); + var $e = typeof se(23); + var $f = void se(23); +} + +function test2() { + var $a = ++global; + var $b = global++; + var $c = --global; + var $d = global--; +} + +function test3() { + var obj = { x: 2, y: 3}; + var $tmp = delete obj.x; + return ("x" in obj) + ":" + ("y" in obj); +} + +function box() { + test1(); + if (global != 6) return "fail1: " + global; + + test2(); + if (global != 6) return "fail2: " + global; + + var result = test3(); + if (result != "false:true") return "fail3: " + result; + + return "OK"; +} \ No newline at end of file