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 3b2202c0354..6157dda7acb 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 @@ -20,6 +20,7 @@ import com.google.dart.compiler.backend.js.ast.* import com.google.dart.compiler.backend.js.ast.metadata.* import com.intellij.util.SmartList import org.jetbrains.kotlin.js.inline.util.IdentitySet +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.* import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.* @@ -240,6 +241,7 @@ internal class ExpressionDecomposer private constructor( private abstract class Callable(hasArguments: HasArguments) { abstract var qualifier: JsExpression + abstract val applyBindIfNecessary: Boolean val arguments = hasArguments.arguments } @@ -247,12 +249,14 @@ internal class ExpressionDecomposer private constructor( override var qualifier: JsExpression get() = invocation.qualifier set(value) { invocation.qualifier = value } + override val applyBindIfNecessary = true } private class CallableNewAdapter(val jsnew: JsNew) : Callable(jsnew) { override var qualifier: JsExpression get() = jsnew.constructorExpression set(value) { jsnew.constructorExpression = value } + override val applyBindIfNecessary = false } private fun Callable.process() { @@ -267,13 +271,17 @@ internal class ExpressionDecomposer private constructor( // Qualifier might be a reference to lambda property. See KT-7674 // An exception here is `fn.call()`, which are marked as side effect free. Further recognition of such // case in inliner might be quite difficult, so never extract such call (and other calls marked this way). - if ((qualifier !in containsNodeWithSideEffect || (qualifier as? HasMetadata)?.sideEffects == SideEffectKind.PURE) && - (callee != null && receiver != null && receiver in containsNodeWithSideEffect) - ) { + if ((qualifier as? HasMetadata)?.sideEffects == SideEffectKind.PURE && + callee != null && receiver != null && receiver in containsNodeWithSideEffect + ) { val receiverTmp = receiver.extractToTemporary() callee.qualifier = receiverTmp } else { + if (receiver != null && callee != null && applyBindIfNecessary) { + val receiverTmp = receiver.extractToTemporary() + qualifier = JsAstUtils.invokeBind(receiverTmp, pureFqn(callee.ident, receiverTmp)) + } qualifier = qualifier.extractToTemporary() } } 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 9d06f8fb366..7ef559094a6 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 @@ -23,6 +23,7 @@ class FunctionPostProcessor(root: JsFunction) { { TemporaryAssignmentElimination(root.body).apply() }, { RedundantLabelRemoval(root.body).apply() }, { TemporaryVariableElimination(root).apply() }, + { RedundantBindElimination(root.body).apply() }, { IfStatementReduction(root.body).apply() }, { DeadCodeElimination(root.body).apply() }, { RedundantVariableDeclarationElimination(root.body).apply() }, diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantBindElimination.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantBindElimination.kt new file mode 100644 index 00000000000..0a2755c6ad0 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantBindElimination.kt @@ -0,0 +1,54 @@ +/* + * 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.JsBlock +import com.google.dart.compiler.backend.js.ast.JsInvocation +import com.google.dart.compiler.backend.js.ast.JsNameRef +import com.google.dart.compiler.backend.js.ast.RecursiveJsVisitor + +// TODO: this optimization is a little unfair. It tries to recognize pattern like this a.bind(b)(args) and +// replace it with b.a(args). However, we can't be completely sure that `a` is a Function. +// Using JS-independent AST should solve the issue (as well as many other issues). +class RedundantBindElimination(private val root: JsBlock) { + private var changed = false + + fun apply(): Boolean { + root.accept(object : RecursiveJsVisitor() { + override fun visitInvocation(invocation: JsInvocation) { + tryEliminate(invocation) + super.visitInvocation(invocation) + } + + private fun tryEliminate(invocation: JsInvocation) { + val qualifier = invocation.qualifier + if (qualifier !is JsInvocation) return + + val outerQualifier = qualifier.qualifier + if (outerQualifier !is JsNameRef) return + val name = outerQualifier.name?.ident ?: outerQualifier.ident + if (name != "bind") return + + val qualifierReplacement = outerQualifier.qualifier ?: return + invocation.qualifier = qualifierReplacement + changed = true + } + }) + + return changed + } +} diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineEvaluationOrderTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineEvaluationOrderTestGenerated.java index edd990582a9..f9fd0a2988d 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineEvaluationOrderTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineEvaluationOrderTestGenerated.java @@ -221,6 +221,12 @@ public class InlineEvaluationOrderTestGenerated extends AbstractInlineEvaluation doTest(fileName); } + @TestMetadata("methodDecomposedWithBind.kt") + public void testMethodDecomposedWithBind() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineEvaluationOrder/cases/methodDecomposedWithBind.kt"); + doTest(fileName); + } + @TestMetadata("methodInlineCallQualifierWithSideEffect.kt") public void testMethodInlineCallQualifierWithSideEffect() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineEvaluationOrder/cases/methodInlineCallQualifierWithSideEffect.kt"); diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineMultiModuleTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineMultiModuleTestGenerated.java index 3716a2730dc..b3d9197c943 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineMultiModuleTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineMultiModuleTestGenerated.java @@ -59,6 +59,18 @@ public class InlineMultiModuleTestGenerated extends AbstractInlineMultiModuleTes doTest(fileName); } + @TestMetadata("externalInlineCallDecomposed") + public void testExternalInlineCallDecomposed() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/"); + doTest(fileName); + } + + @TestMetadata("externalInlineNewDecomposed") + public void testExternalInlineNewDecomposed() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/"); + doTest(fileName); + } + @TestMetadata("lambda") public void testLambda() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineMultiModule/cases/lambda/"); diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsAstUtils.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsAstUtils.java index f6e00886dfd..7879634d7c9 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsAstUtils.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsAstUtils.java @@ -490,6 +490,11 @@ public final class JsAstUtils { return result; } + @NotNull + public static JsInvocation invokeBind(@NotNull JsExpression receiver, @NotNull JsExpression method) { + return invokeMethod(method, "bind", receiver); + } + public static boolean isUndefinedExpression(JsExpression expression) { if (!(expression instanceof JsUnaryOperation)) return false; diff --git a/js/js.translator/testData/inlineEvaluationOrder/cases/methodDecomposedWithBind.kt b/js/js.translator/testData/inlineEvaluationOrder/cases/methodDecomposedWithBind.kt new file mode 100644 index 00000000000..7663a7397c3 --- /dev/null +++ b/js/js.translator/testData/inlineEvaluationOrder/cases/methodDecomposedWithBind.kt @@ -0,0 +1,8 @@ +package foo + +fun box(): String { + val v = mapOf(1 to "1", 2 to "2").mapValues { it.value.map { it.toString() } } + assertEquals(2, v.size) + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/lib/lib.kt b/js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/lib/lib.kt new file mode 100644 index 00000000000..f7405f27e03 --- /dev/null +++ b/js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/lib/lib.kt @@ -0,0 +1,9 @@ +package lib + +var global = "" + +inline fun baz(x: () -> Int) = A(1).bar(x()) + +class A(val y: Int) { + fun bar(x: Int) = x + y +} \ No newline at end of file diff --git a/js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/main/main.kt b/js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/main/main.kt new file mode 100644 index 00000000000..cfdcd075080 --- /dev/null +++ b/js/js.translator/testData/inlineMultiModule/cases/externalInlineCallDecomposed/main/main.kt @@ -0,0 +1,20 @@ +package foo + +import lib.* + +fun qqq(): Int { + global += "qqq;" + return 23 +} + +fun box(): String { + assertEquals(24, baz { + global += "before;" + val result = qqq() + global += "after;" + result + }) + assertEquals("before;qqq;after;", global) + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/lib/lib.kt b/js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/lib/lib.kt new file mode 100644 index 00000000000..887cf0bebd4 --- /dev/null +++ b/js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/lib/lib.kt @@ -0,0 +1,11 @@ +package lib + +var global = "" + +inline fun baz(x: () -> Int) = ((A(1).B(x()) as Any) as A.B).bar() + +class A(val y: Int) { + inner class B(val x: Int) { + fun bar() = x + y + } +} \ No newline at end of file diff --git a/js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/main/main.kt b/js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/main/main.kt new file mode 100644 index 00000000000..cfdcd075080 --- /dev/null +++ b/js/js.translator/testData/inlineMultiModule/cases/externalInlineNewDecomposed/main/main.kt @@ -0,0 +1,20 @@ +package foo + +import lib.* + +fun qqq(): Int { + global += "qqq;" + return 23 +} + +fun box(): String { + assertEquals(24, baz { + global += "before;" + val result = qqq() + global += "after;" + result + }) + assertEquals("before;qqq;after;", global) + + return "OK" +} \ No newline at end of file