KT-12928: decompose invocation like "a.foo(b)" to "$t = a; $s = $t.foo.bind($t); $s(b)" instead of "$t = a.foo; $t(b)", since in the latter case foo won't receive proper this. Add optimization that replaces "foo.bar.bind(baz)(args)" with "baz.bar(args)"

This commit is contained in:
Alexey Andreev
2016-07-01 12:14:05 +03:00
parent 07cb3e09b9
commit 88eef7337e
11 changed files with 157 additions and 3 deletions
@@ -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()
}
}
@@ -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() },
@@ -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
}
}
@@ -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");
@@ -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/");
@@ -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;
@@ -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"
}
@@ -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
}
@@ -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"
}
@@ -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
}
}
@@ -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"
}