JS: extract expressions preceding inline call
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.util
|
||||
|
||||
import com.google.dart.compiler.backend.js.ast.*
|
||||
import com.google.dart.compiler.backend.js.ast.metadata.*
|
||||
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.*
|
||||
|
||||
import kotlin.platform.platformStatic
|
||||
|
||||
import com.intellij.util.SmartList
|
||||
|
||||
/**
|
||||
* If inline function consists of multiple statements,
|
||||
* its statements will be extracted before last visited statement.
|
||||
* This action can potentially break evaluation order.
|
||||
*
|
||||
* An example:
|
||||
* var x = foo() + inlineBox();
|
||||
* Inliner could extract statements from inlineBox():
|
||||
* // inlineBox body..
|
||||
* var x = foo() + inlineBox$result;
|
||||
* So foo is evaluated before inlineBox() after inline.
|
||||
*
|
||||
* So we need to extract all nodes, that evaluate before inline call,
|
||||
* to temporary vars (preserving evaluation order).
|
||||
*
|
||||
* For the example, defined before, evaluation order could be preserved this way:
|
||||
* var tmp = foo();
|
||||
* // inlineBox body..
|
||||
* var x = tmp + inlineBox$result;
|
||||
*
|
||||
* It's desirable to create temporary var only if node can have side effect,
|
||||
* and precedes inline call (in JavaScript evaluation order).
|
||||
*/
|
||||
class ExpressionDecomposer private (
|
||||
private val scope: JsScope,
|
||||
private val containsExtractable: Set<JsNode>,
|
||||
private val containsNodeWithSideEffect: Set<JsNode>
|
||||
) : JsExpressionVisitor() {
|
||||
|
||||
private var additionalStatements: MutableList<JsStatement> = SmartList()
|
||||
|
||||
companion object {
|
||||
platformStatic
|
||||
public fun preserveEvaluationOrder(
|
||||
scope: JsScope, statement: JsStatement, canBeExtractedByInliner: (JsNode)->Boolean
|
||||
): List<JsStatement> {
|
||||
val decomposer = with (statement) {
|
||||
val extractable = match(canBeExtractedByInliner)
|
||||
val containsExtractable = withParentsOfNodes(extractable)
|
||||
val nodesWithSideEffect = match { it is JsExpression && it.canHaveOwnSideEffect() }
|
||||
val containsNodeWithSideEffect = withParentsOfNodes(nodesWithSideEffect)
|
||||
|
||||
ExpressionDecomposer(scope, containsExtractable, containsNodeWithSideEffect)
|
||||
}
|
||||
|
||||
decomposer.accept(statement)
|
||||
return decomposer.additionalStatements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits only expressions that can potentially be extracted by [JsInliner],
|
||||
* that directly referenced by statements.
|
||||
*
|
||||
* For example, won't visit [JsBlock] statements, but will visit test expression of [JsWhile].
|
||||
*/
|
||||
private open class JsExpressionVisitor() : JsVisitorWithContextImpl() {
|
||||
|
||||
override fun visit(x: JsBlock, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsTry, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsDebugger, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsLabel, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsFunction, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsObjectLiteral, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsPropertyInitializer, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsProgramFragment, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsProgram, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsParameter, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsCatch, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsBreak, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsContinue, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsCase, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsDefault, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsEmpty, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsLiteral.JsBooleanLiteral, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsLiteral.JsThisRef, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsNullLiteral, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsNumberLiteral, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsRegExp, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsStringLiteral, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsName, ctx: JsContext<*>): Boolean = false
|
||||
|
||||
// TODO: support these
|
||||
// Not generated by compiler now, (can be generated in future or used in js() block)
|
||||
override fun visit(x: JsForIn, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsSwitch, ctx: JsContext<*>): Boolean = false
|
||||
|
||||
// Compiler generates restricted version of for,
|
||||
// where init and test do not contain inline calls.
|
||||
override fun visit(x: JsFor, ctx: JsContext<*>): Boolean = false
|
||||
|
||||
override fun visit(x: JsIf, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsWhile, ctx: JsContext<*>): Boolean = false
|
||||
override fun visit(x: JsDoWhile, ctx: JsContext<*>): Boolean = false
|
||||
|
||||
override fun visit(x: JsArrayAccess, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsArrayLiteral, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsBinaryOperation, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsConditional, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsInvocation, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsNameRef, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsNew, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsVars.JsVar, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsPostfixOperation, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsPrefixOperation, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsExpressionStatement, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsReturn, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsThrow, ctx: JsContext<*>): Boolean = true
|
||||
override fun visit(x: JsVars, ctx: JsContext<*>): Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns descendants of receiver, matched by [predicate].
|
||||
*/
|
||||
private fun JsNode.match(predicate: (JsNode) -> Boolean): Set<JsNode> {
|
||||
val visitor = object : JsExpressionVisitor() {
|
||||
val matched = IdentitySet<JsNode>()
|
||||
|
||||
override fun <R : JsNode?> doTraverse(node: R, ctx: JsContext<*>?) {
|
||||
super.doTraverse(node, ctx)
|
||||
|
||||
if (node !in matched && predicate(node)) {
|
||||
matched.add(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitor.accept(this)
|
||||
return visitor.matched
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set of nodes, that satisfy transitive closure of `is parent` relation, starting from [nodes].
|
||||
*/
|
||||
private fun JsNode.withParentsOfNodes(nodes: Set<JsNode>): Set<JsNode> {
|
||||
val visitor = object : JsExpressionVisitor() {
|
||||
private val stack = SmartList<JsNode>()
|
||||
val matched = IdentitySet<JsNode>()
|
||||
|
||||
override fun <R : JsNode?> doTraverse(node: R, ctx: JsContext<*>?) {
|
||||
stack.add(node)
|
||||
super.doTraverse(node, ctx)
|
||||
|
||||
if (node in nodes) {
|
||||
addAllUntilMatchedOrStatement(stack)
|
||||
}
|
||||
|
||||
stack.remove(stack.lastIndex)
|
||||
}
|
||||
|
||||
fun addAllUntilMatchedOrStatement(nodesOnStack: List<JsNode>) {
|
||||
for (i in nodesOnStack.lastIndex downTo 0) {
|
||||
val currentNode = nodesOnStack[i]
|
||||
if (currentNode in matched) break
|
||||
|
||||
matched.add(currentNode)
|
||||
if (currentNode is JsStatement) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitor.accept(this)
|
||||
return visitor.matched
|
||||
}
|
||||
@@ -23,11 +23,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticSink;
|
||||
import org.jetbrains.kotlin.js.inline.context.*;
|
||||
import org.jetbrains.kotlin.js.inline.util.ExpressionDecomposer;
|
||||
import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs;
|
||||
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
|
||||
import org.jetbrains.kotlin.resolve.inline.InlineStrategy;
|
||||
|
||||
import java.util.*;
|
||||
import kotlin.Function1;
|
||||
|
||||
import static org.jetbrains.kotlin.js.inline.FunctionInlineMutator.getInlineableCallReplacement;
|
||||
import static org.jetbrains.kotlin.js.inline.clean.CleanPackage.removeUnusedFunctionDefinitions;
|
||||
@@ -47,6 +49,15 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
||||
// these are needed for error reporting, when inliner detects cycle
|
||||
private final Stack<JsFunction> namedFunctionsStack = new Stack<JsFunction>();
|
||||
private final LinkedList<JsCallInfo> inlineCallInfos = new LinkedList<JsCallInfo>();
|
||||
private final Function1<JsNode, Boolean> canBeExtractedByInliner = new Function1<JsNode, Boolean>() {
|
||||
@Override
|
||||
public Boolean invoke(JsNode node) {
|
||||
if (!(node instanceof JsInvocation)) return false;
|
||||
|
||||
JsInvocation call = (JsInvocation) node;
|
||||
return hasToBeInlined(call);
|
||||
}
|
||||
};
|
||||
|
||||
public static JsProgram process(@NotNull TranslationContext context) {
|
||||
JsProgram program = context.program();
|
||||
@@ -100,24 +111,21 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
||||
|
||||
@Override
|
||||
public boolean visit(@NotNull JsInvocation call, @NotNull JsContext context) {
|
||||
if (shouldInline(call) && canInline(call)) {
|
||||
JsFunction containingFunction = getCurrentNamedFunction();
|
||||
if (containingFunction != null) {
|
||||
inlineCallInfos.add(new JsCallInfo(call, containingFunction));
|
||||
}
|
||||
if (!hasToBeInlined(call)) return true;
|
||||
|
||||
JsFunction definition = getFunctionContext().getFunctionDefinition(call);
|
||||
JsFunction containingFunction = getCurrentNamedFunction();
|
||||
|
||||
if (inProcessFunctions.contains(definition)) {
|
||||
reportInlineCycle(call, definition);
|
||||
return false;
|
||||
}
|
||||
if (containingFunction != null) {
|
||||
inlineCallInfos.add(new JsCallInfo(call, containingFunction));
|
||||
}
|
||||
|
||||
if (!processedFunctions.contains(definition)) {
|
||||
accept(definition);
|
||||
}
|
||||
JsFunction definition = getFunctionContext().getFunctionDefinition(call);
|
||||
|
||||
inline(call, context);
|
||||
if (inProcessFunctions.contains(definition)) {
|
||||
reportInlineCycle(call, definition);
|
||||
}
|
||||
else if (!processedFunctions.contains(definition)) {
|
||||
accept(definition);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -125,6 +133,10 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
||||
|
||||
@Override
|
||||
public void endVisit(@NotNull JsInvocation x, @NotNull JsContext ctx) {
|
||||
if (hasToBeInlined(x)) {
|
||||
inline(x, ctx);
|
||||
}
|
||||
|
||||
JsCallInfo lastCallInfo = null;
|
||||
|
||||
if (!inlineCallInfos.isEmpty()) {
|
||||
@@ -136,6 +148,25 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doAcceptStatementList(List<JsStatement> statements) {
|
||||
// at top level of js ast, contexts stack can be empty,
|
||||
// but there is no inline calls anyway
|
||||
if(!inliningContexts.isEmpty()) {
|
||||
JsScope scope = getFunctionContext().getScope();
|
||||
int i = 0;
|
||||
|
||||
while (i < statements.size()) {
|
||||
List<JsStatement> additionalStatements =
|
||||
ExpressionDecomposer.preserveEvaluationOrder(scope, statements.get(i), canBeExtractedByInliner);
|
||||
statements.addAll(i, additionalStatements);
|
||||
i += additionalStatements.size() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
super.doAcceptStatementList(statements);
|
||||
}
|
||||
|
||||
private void inline(@NotNull JsInvocation call, @NotNull JsContext context) {
|
||||
JsInliningContext inliningContext = getInliningContext();
|
||||
FunctionContext functionContext = getFunctionContext();
|
||||
@@ -146,7 +177,9 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
||||
JsExpression resultExpression = inlineableResult.getResultExpression();
|
||||
JsContext<JsStatement> statementContext = inliningContext.getStatementContext();
|
||||
// body of inline function can contain call to lambdas that need to be inlined
|
||||
accept(inlineableBody);
|
||||
JsStatement statement = accept(inlineableBody);
|
||||
assert inlineableBody == statement;
|
||||
|
||||
statementContext.addPrevious(flattenStatement(inlineableBody));
|
||||
|
||||
/**
|
||||
@@ -195,14 +228,11 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canInline(@NotNull JsInvocation call) {
|
||||
FunctionContext functionContext = getFunctionContext();
|
||||
return functionContext.hasFunctionDefinition(call);
|
||||
}
|
||||
|
||||
private static boolean shouldInline(@NotNull JsInvocation call) {
|
||||
public boolean hasToBeInlined(@NotNull JsInvocation call) {
|
||||
InlineStrategy strategy = MetadataPackage.getInlineStrategy(call);
|
||||
return strategy != null && strategy.isInline();
|
||||
if (strategy == null || !strategy.isInline()) return false;
|
||||
|
||||
return getFunctionContext().hasFunctionDefinition(call);
|
||||
}
|
||||
|
||||
private class JsInliningContext implements InliningContext {
|
||||
|
||||
@@ -32,7 +32,7 @@ public fun aliasArgumentsIfNeeded(
|
||||
for ((arg, param) in arguments zip parameters) {
|
||||
val paramName = param.getName()
|
||||
val replacement =
|
||||
if (needToAlias(arg)) {
|
||||
if (arg.needToAlias()) {
|
||||
val freshName = context.getFreshName(paramName)
|
||||
context.newVar(freshName, arg)
|
||||
freshName.makeRef()
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ class ReturnReplacingVisitor(private val resultRef: JsNameRef?, private val brea
|
||||
if (resultRef != null)
|
||||
return JsAstUtils.assignment(resultRef, returnExpression)
|
||||
|
||||
if (canHaveSideEffect(returnExpression))
|
||||
if (returnExpression.canHaveSideEffect())
|
||||
return returnExpression
|
||||
}
|
||||
|
||||
|
||||
@@ -16,67 +16,33 @@
|
||||
|
||||
package org.jetbrains.kotlin.js.inline.util
|
||||
|
||||
import com.google.dart.compiler.backend.js.ast.JsArrayAccess
|
||||
import com.google.dart.compiler.backend.js.ast.JsArrayLiteral
|
||||
import com.google.dart.compiler.backend.js.ast.JsBinaryOperation
|
||||
import com.google.dart.compiler.backend.js.ast.JsConditional
|
||||
import com.google.dart.compiler.backend.js.ast.JsExpression
|
||||
import com.google.dart.compiler.backend.js.ast.JsInvocation
|
||||
import com.google.dart.compiler.backend.js.ast.JsLiteral.JsThisRef
|
||||
import com.google.dart.compiler.backend.js.ast.JsLiteral.JsValueLiteral
|
||||
import com.google.dart.compiler.backend.js.ast.JsNameRef
|
||||
import com.google.dart.compiler.backend.js.ast.JsNode
|
||||
import com.google.dart.compiler.backend.js.ast.RecursiveJsVisitor
|
||||
import com.google.dart.compiler.backend.js.ast.JsLiteral.*
|
||||
import com.google.dart.compiler.backend.js.ast.*
|
||||
import org.jetbrains.kotlin.js.translate.utils.ast.any
|
||||
|
||||
public fun canHaveSideEffect(x: JsExpression): Boolean {
|
||||
return with(SideEffectVisitor()) {
|
||||
accept(x)
|
||||
!sideEffectFree
|
||||
}
|
||||
}
|
||||
public fun JsExpression.canHaveSideEffect(): Boolean =
|
||||
any { it is JsExpression && it.canHaveOwnSideEffect() }
|
||||
|
||||
public fun needToAlias(x: JsExpression): Boolean {
|
||||
return with(NeedToAliasVisitor()) {
|
||||
accept(x)
|
||||
!sideEffectFree
|
||||
}
|
||||
}
|
||||
|
||||
private open class SideEffectVisitor() : RecursiveJsVisitor() {
|
||||
public var sideEffectFree: Boolean = true
|
||||
protected set
|
||||
|
||||
override fun visitElement(node: JsNode) {
|
||||
sideEffectFree = sideEffectFree && isSideEffectFree(node)
|
||||
|
||||
if (sideEffectFree) {
|
||||
super.visitElement(node)
|
||||
}
|
||||
public fun JsExpression.canHaveOwnSideEffect(): Boolean =
|
||||
when (this) {
|
||||
is JsValueLiteral,
|
||||
is JsConditional,
|
||||
is JsArrayAccess,
|
||||
is JsArrayLiteral,
|
||||
is JsNameRef -> false
|
||||
is JsBinaryOperation -> getOperator().isAssignment()
|
||||
else -> true
|
||||
}
|
||||
|
||||
protected open fun isSideEffectFree(node: JsNode): Boolean =
|
||||
when (node) {
|
||||
is JsValueLiteral,
|
||||
is JsConditional,
|
||||
is JsArrayAccess,
|
||||
is JsArrayLiteral,
|
||||
is JsNameRef ->
|
||||
true
|
||||
is JsBinaryOperation ->
|
||||
!node.getOperator().isAssignment()
|
||||
else ->
|
||||
false
|
||||
}
|
||||
}
|
||||
public fun JsExpression.needToAlias(): Boolean =
|
||||
any { it is JsExpression && it.shouldHaveOwnAlias() }
|
||||
|
||||
private class NeedToAliasVisitor() : SideEffectVisitor() {
|
||||
override fun isSideEffectFree(node: JsNode): Boolean =
|
||||
when (node) {
|
||||
public fun JsExpression.shouldHaveOwnAlias(): Boolean =
|
||||
when (this) {
|
||||
is JsThisRef,
|
||||
is JsConditional,
|
||||
is JsBinaryOperation,
|
||||
is JsArrayLiteral -> false
|
||||
is JsInvocation -> isFunctionCreatorInvocation(node)
|
||||
else -> super.isSideEffectFree(node)
|
||||
}
|
||||
}
|
||||
is JsArrayLiteral -> true
|
||||
is JsInvocation -> !isFunctionCreatorInvocation(this)
|
||||
else -> canHaveOwnSideEffect()
|
||||
}
|
||||
@@ -16,9 +16,8 @@
|
||||
|
||||
package org.jetbrains.kotlin.js.translate.utils.ast
|
||||
|
||||
import com.google.dart.compiler.backend.js.ast.JsFunction
|
||||
import com.google.dart.compiler.backend.js.ast.JsStatement
|
||||
import com.google.dart.compiler.backend.js.ast.JsParameter
|
||||
import com.google.dart.compiler.backend.js.ast.*
|
||||
import org.jetbrains.kotlin.js.inline.util.IdentitySet
|
||||
|
||||
public fun JsFunction.addStatement(stmt: JsStatement) {
|
||||
getBody().getStatements().add(stmt)
|
||||
@@ -36,3 +35,23 @@ public fun JsFunction.addParameter(identifier: String, index: Int? = null): JsPa
|
||||
|
||||
return parameter
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests, if any node containing in receiver's AST matches, [predicate].
|
||||
*/
|
||||
public fun JsNode.any(predicate: (JsNode) -> Boolean): Boolean {
|
||||
val visitor = object : RecursiveJsVisitor() {
|
||||
public var matched: Boolean = false
|
||||
|
||||
override fun visitElement(node: JsNode) {
|
||||
matched = matched || predicate(node)
|
||||
|
||||
if (!matched) {
|
||||
super.visitElement(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitor.accept(this)
|
||||
return visitor.matched
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user