diff --git a/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt b/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt index 18f3ba2935a..c1cf52a2ecb 100644 --- a/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt +++ b/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt @@ -37,6 +37,8 @@ var JsParameter.hasDefaultValue: Boolean by MetadataProperty(default = false) var JsInvocation.typeCheck: TypeCheck? by MetadataProperty(default = null) +var HasMetadata.synthetic: Boolean by MetadataProperty(default = false) + enum class TypeCheck { TYPEOF, INSTANCEOF 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 b7b0bb28c8d..1ba3f9843de 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 @@ -14,10 +14,13 @@ * limitations under the License. */ -package org.jetbrains.kotlin.js.inline.util +package org.jetbrains.kotlin.js.inline import com.google.dart.compiler.backend.js.ast.* +import com.google.dart.compiler.backend.js.ast.metadata.synthetic import com.intellij.util.SmartList +import org.jetbrains.kotlin.js.inline.util.IdentitySet +import org.jetbrains.kotlin.js.inline.util.canHaveOwnSideEffect import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.* import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.* @@ -322,8 +325,15 @@ internal class ExpressionDecomposer private constructor( val nameRef: JsExpression get() = name.makeRef() - fun assign(value: JsExpression): JsStatement = - assignment(nameRef, value).makeStmt() + init { + variable.synthetic = true + } + + fun assign(value: JsExpression): JsStatement { + val statement = JsExpressionStatement(assignment(nameRef, value)) + statement.synthetic = true + return statement + } } private val List.indicesOfExtractable: Iterator diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt index fa962f4e0ce..41d1512c711 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt @@ -17,15 +17,13 @@ package org.jetbrains.kotlin.js.inline import com.google.dart.compiler.backend.js.ast.* -import com.google.dart.compiler.util.AstUtil -import com.intellij.util.containers.ContainerUtil +import com.google.dart.compiler.backend.js.ast.metadata.synthetic import org.jetbrains.kotlin.js.inline.clean.removeDefaultInitializers import org.jetbrains.kotlin.js.inline.context.InliningContext import org.jetbrains.kotlin.js.inline.context.NamingContext import org.jetbrains.kotlin.js.inline.util.* import org.jetbrains.kotlin.js.inline.util.rewriters.ReturnReplacingVisitor import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.newVar -import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.* class FunctionInlineMutator private constructor( @@ -33,7 +31,6 @@ private constructor( private val inliningContext: InliningContext ) { private val invokedFunction: JsFunction - private val isResultNeeded: Boolean private val namingContext: NamingContext private val body: JsBlock private var resultExpr: JsExpression? = null @@ -45,7 +42,6 @@ private constructor( val functionContext = inliningContext.functionContext invokedFunction = functionContext.getFunctionDefinition(call) body = invokedFunction.body.deepCopy() - isResultNeeded = isResultNeeded(call) namingContext = inliningContext.newNamingContext() } @@ -84,45 +80,13 @@ private constructor( private fun removeStatementsAfterTopReturn() { val statements = body.statements - val statementsSize = statements.size - for (i in 0..statementsSize - 1) { - val statement = statements.get(i) - - if (statement is JsReturn) { - statements.subList(i + 1, statementsSize).clear() - break - } + val returnIndex = statements.indexOfFirst { it is JsReturn } + if (returnIndex >= 0) { + statements.subList(returnIndex + 1, statements.size).clear() } } private fun processReturns() { - if (currentStatement is JsReturn && currentStatement.expression === call) { - inliningContext.statementContext.removeMe() - return - } - - val returnCount = collectInstances(JsReturn::class.java, body).size - if (returnCount == 0) { - // TODO return Unit (KT-5647) - resultExpr = JsLiteral.UNDEFINED - return - } - - if (returnCount == 1) { - val statements = body.statements - val lastTopLevelStatement = statements[statements.lastIndex] - - if (lastTopLevelStatement is JsReturn) { - resultExpr = lastTopLevelStatement.expression - statements.removeAt(statements.lastIndex) - return - } - } - - doReplaceReturns() - } - - private fun doReplaceReturns() { val resultReference = getResultReference() if (resultReference != null) { resultExpr = resultReference @@ -130,62 +94,22 @@ private constructor( assert(resultExpr == null || resultExpr is JsNameRef) val breakName = namingContext.getFreshName(getBreakLabel()) - breakLabel = JsLabel(breakName) + val breakLabel = JsLabel(breakName) + breakLabel.synthetic = true + this.breakLabel = breakLabel val visitor = ReturnReplacingVisitor(resultExpr as? JsNameRef, breakName.makeRef()) visitor.accept(body) - - val statements = body.statements - val last = statements.lastOrNull() as? JsBreak - - if (last?.label?.name === breakLabel?.name) { - statements.removeAt(statements.lastIndex) - } } private fun getResultReference(): JsNameRef? { - if (!isResultNeeded) return null - - val existingReference = when (currentStatement) { - is JsExpressionStatement -> { - val expression = currentStatement.expression as? JsBinaryOperation - expression?.getResultReference() - } - is JsVars -> currentStatement.getResultReference() - else -> null - } - - if (existingReference != null) return existingReference + if (!isResultNeeded(call)) return null val resultName = namingContext.getFreshName(getResultLabel()) namingContext.newVar(resultName, null) return resultName.makeRef() } - private fun JsBinaryOperation.getResultReference(): JsNameRef? { - if (operator !== JsBinaryOperator.ASG || arg2 !== call) return null - - return arg1 as? JsNameRef - } - - private fun JsVars.getResultReference(): JsNameRef? { - val vars = vars - val variable = vars.first() - - // var a = expr1 + call() is ok, but we don't want to reuse 'a' for result, - // as it means to replace every 'return expr2' to 'a = expr1 + expr2'. - // If there is more than one return, expr1 copies are undesirable. - if (variable.initExpression !== call || vars.size > 1) return null - - val varName = variable.name - with (inliningContext.statementContext) { - removeMe() - addPrevious(newVar(varName, null)) - } - - return varName.makeRef() - } - private fun getArguments(): List { val arguments = call.arguments if (isCallInvocation(call)) { @@ -245,7 +169,7 @@ private constructor( @JvmStatic private fun getThisReplacement(call: JsInvocation): JsExpression? { if (isCallInvocation(call)) { - return call.arguments.get(0) + return call.arguments[0] } if (hasCallerQualifier(call)) { @@ -259,14 +183,5 @@ private constructor( val thisRefs = collectInstances(JsLiteral.JsThisRef::class.java, body) return !thisRefs.isEmpty() } - - @JvmStatic fun canBeExpression(function: JsFunction): Boolean { - return canBeExpression(function.body) - } - - private fun canBeExpression(body: JsBlock): Boolean { - val statements = body.statements - return statements.size == 1 && statements.get(0) is JsReturn - } } } 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 22a071a2012..ef60e62a003 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 @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.descriptors.CallableDescriptor; import org.jetbrains.kotlin.diagnostics.DiagnosticSink; import org.jetbrains.kotlin.diagnostics.Errors; +import org.jetbrains.kotlin.js.inline.clean.FunctionPostProcessor; import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedFunctionDefinitionsKt; import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedLocalFunctionDeclarationsKt; import org.jetbrains.kotlin.js.inline.context.FunctionContext; @@ -36,7 +37,6 @@ import org.jetbrains.kotlin.resolve.inline.InlineStrategy; import java.util.*; -import static org.jetbrains.kotlin.js.inline.FunctionInlineMutator.canBeExpression; import static org.jetbrains.kotlin.js.inline.FunctionInlineMutator.getInlineableCallReplacement; import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.flattenStatement; @@ -59,12 +59,7 @@ public class JsInliner extends JsVisitorWithContextImpl { JsInvocation call = (JsInvocation) node; - if (hasToBeInlined(call)) { - JsFunction function = getFunctionContext().getFunctionDefinition(call); - return !canBeExpression(function); - } - - return false; + return hasToBeInlined(call); } }; @@ -108,6 +103,8 @@ public class JsInliner extends JsVisitorWithContextImpl { RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function); processedFunctions.add(function); + new FunctionPostProcessor(function.getBody()).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 new file mode 100644 index 00000000000..46ac9ab8674 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/FunctionPostProcessor.kt @@ -0,0 +1,28 @@ +/* + * 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 + +class FunctionPostProcessor(private val root: JsBlock) { + fun apply() { + TemporaryAssignmentElimination(root).apply() + RedundantLabelRemoval(root).apply() + TemporaryVariableElimination(root).apply() + RedundantVariableDeclarationElimination(root).apply() + } +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantLabelRemoval.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantLabelRemoval.kt new file mode 100644 index 00000000000..3f62390b180 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantLabelRemoval.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.synthetic +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils + +internal class RedundantLabelRemoval(private val root: JsStatement) { + private val labelUsages = mutableMapOf() + + fun apply() { + analyze() + perform() + } + + private fun analyze() { + object : JsVisitorWithContextImpl() { + override fun endVisit(x: JsBreak, ctx: JsContext<*>) { + super.endVisit(x, ctx) + x.label?.let { useLabel(it.name!!) } + } + + override fun endVisit(x: JsContinue, ctx: JsContext<*>) { + super.endVisit(x, ctx) + x.label?.let { useLabel(it.name!!) } + } + }.accept(root) + } + + private fun perform() { + object : JsVisitorWithContextImpl() { + override fun endVisit(x: JsLabel, ctx: JsContext) { + if (x.synthetic) { + val statementReplacement = perform(x.statement, x.name) + if (statementReplacement == null) { + ctx.removeMe() + } + else if (labelUsages[x.name] ?: 0 == 0) { + val replacement = statementReplacement + if (replacement is JsBlock) { + ctx.addPrevious(replacement.statements) + ctx.removeMe() + } + else { + ctx.replaceMe(replacement) + } + } + else { + x.statement = statementReplacement + } + } + + super.endVisit(x, ctx) + } + }.accept(root) + } + + private fun perform(statement: JsStatement, name: JsName): JsStatement? = when (statement) { + is JsBreak -> + if (name == statement.label?.name) { + unuseLabel(name) + null + } + else { + statement + } + is JsLabel -> { + perform(statement.statement, name)?.let { statement } + } + is JsBlock -> + if (perform(statement.statements, name).isEmpty()) { + null + } + else { + statement + } + is JsIf -> { + val thenRemoved = perform(statement.thenStatement, name) == null + val elseRemoved = statement.elseStatement?.let { perform(it, name) } == null + when { + thenRemoved && elseRemoved -> null + elseRemoved -> { + statement.elseStatement = null + statement + } + thenRemoved -> { + statement.thenStatement = statement.elseStatement + statement.elseStatement = null + statement.ifExpression = JsAstUtils.negated(statement.ifExpression) + statement + } + else -> statement + } + } + is JsTry -> { + val finallyBlock = statement.finallyBlock + val result = perform(statement.tryBlock, name) + if (result != null) { + statement + } + else if (finallyBlock != null && !finallyBlock.isEmpty) { + finallyBlock + } + else { + null + } + } + else -> statement + } + + private fun perform(statements: MutableList, name: JsName): MutableList { + val last = statements.lastOrNull() + val lastOptimized = last?.let { perform(it, name) } + if (lastOptimized != last) { + if (lastOptimized == null) { + statements.removeAt(statements.lastIndex) + } + else if (lastOptimized is JsBlock) { + statements.removeAt(statements.lastIndex) + statements.addAll(lastOptimized.statements) + } + else { + statements[statements.lastIndex] = lastOptimized + } + } + return statements + } + + private fun useLabel(name: JsName) { + labelUsages[name] = (labelUsages[name] ?: 0) + 1 + } + + private fun unuseLabel(name: JsName) { + labelUsages[name] = labelUsages[name]!! - 1 + } +} diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantVariableDeclarationElimination.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantVariableDeclarationElimination.kt new file mode 100644 index 00000000000..bed415a0e8a --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/RedundantVariableDeclarationElimination.kt @@ -0,0 +1,59 @@ +/* + * 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.synthetic + +internal class RedundantVariableDeclarationElimination(private val root: JsStatement) { + private val usages = mutableSetOf() + + fun apply() { + analyze() + perform() + } + + private fun analyze() { + object : JsVisitorWithContextImpl() { + override fun visit(x: JsNameRef, ctx: JsContext<*>): Boolean { + val name = x.name + if (name != null && x.qualifier == null) { + usages += name + } + return super.visit(x, ctx) + } + + override fun visit(x: JsBreak, ctx: JsContext<*>) = false + + override fun visit(x: JsContinue, ctx: JsContext<*>) = false + }.accept(root) + } + + private fun perform() { + object : JsVisitorWithContextImpl() { + override fun endVisit(x: JsVars, ctx: JsContext<*>) { + if (x.synthetic) { + x.vars.removeAll { it.initExpression == null && it.name !in usages } + if (x.vars.isEmpty()) { + ctx.removeMe() + } + } + super.endVisit(x, ctx) + } + }.accept(root) + } +} diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/TemporaryAssignmentElimination.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/TemporaryAssignmentElimination.kt new file mode 100644 index 00000000000..e422cda992a --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/TemporaryAssignmentElimination.kt @@ -0,0 +1,241 @@ +/* + * 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.synthetic +import org.jetbrains.kotlin.js.inline.util.canHaveSideEffect +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils + +internal class TemporaryAssignmentElimination(private val root: JsBlock) { + private val usageCount = mutableMapOf() + private val assignmentCount = mutableMapOf() + private val usages = mutableMapOf() + private val statementsToRemove = mutableSetOf() + private val mappedUsages = mutableMapOf() + private val syntheticNames = mutableSetOf() + + fun apply() { + analyze() + process() + generateDeclarations() + } + + private fun analyze() { + object : JsVisitorWithContextImpl() { + override fun visit(x: JsReturn, ctx: JsContext<*>): Boolean { + val returnExpr = x.expression + if (returnExpr != null) { + tryRecord(returnExpr, Usage.Return(x)) + } + return super.visit(x, ctx) + } + + override fun visit(x: JsExpressionStatement, ctx: JsContext<*>): Boolean { + val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression) + if (assignment != null) { + val (name, value) = assignment + assign(name) + val usage = Usage.Assignment(x, name) + if (x.synthetic) { + syntheticNames += name + } + tryRecord(value, usage) + accept(value) + return false + } + + val mutation = JsAstUtils.decomposeAssignment(x.expression) + if (mutation != null) { + val (target, value) = mutation + if (!target.canHaveSideEffect()) { + val usage = Usage.Mutation(x, target) + tryRecord(value, usage) + accept(value) + return false + } + } + + return super.visit(x, ctx) + } + + override fun visit(x: JsVars, ctx: JsContext<*>): Boolean { + if (x.vars.size == 1) { + val declaration = x.vars[0] + val initExpression = declaration.initExpression + if (initExpression != null) { + tryRecord(initExpression, Usage.Declaration(x, declaration.name)) + } + if (x.synthetic) { + syntheticNames += declaration.name + } + } + + x.vars.forEach { v -> v?.initExpression?.let { accept(it) } } + return false + } + + override fun visit(x: JsNameRef, ctx: JsContext<*>): Boolean { + val name = x.name + if (name != null && x.qualifier == null) { + use(name) + return false + } + return super.visit(x, ctx) + } + }.accept(root) + + usages.keys.retainAll(syntheticNames) + } + + private fun getUsage(name: JsName): Usage? { + return mappedUsages.getOrPut(name) { + if (usageCount[name] ?: 0 != 1) return null + + val usage = usages[name] + return when (usage) { + is Usage.Assignment -> { + val result = getUsage(usage.target) + if (result != null) { + result.statements.addAll(usage.statements) + result + } + else { + usage + } + } + is Usage.Declaration -> { + val result = getUsage(usage.target) + if (result != null) { + result.statements.addAll(usage.statements) + result + } + else { + usage + } + } + else -> usage + } + } + } + + private fun process() { + usages.keys.forEach { getUsage(it) } + + object : JsVisitorWithContextImpl() { + override fun visit(x: JsExpressionStatement, ctx: JsContext): Boolean { + if (x in statementsToRemove) { + ctx.removeMe() + return false + } + + val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression) + if (assignment != null) { + val (name, value) = assignment + val usage = getUsage(name) + if (usage != null) { + val replacement = when (usage) { + is Usage.Return -> JsReturn(value).source(x.expression.source) + is Usage.Assignment -> { + val expr = JsAstUtils.assignment(usage.target.makeRef(), value).source(x.expression.source) + val statement = JsExpressionStatement(expr) + statement.synthetic = usage.target in syntheticNames + statement + } + is Usage.Declaration -> { + val statement: JsStatement = if (assignmentCount[name] ?: 0 != 1) { + usage.replaced = true + val expr = JsAstUtils.assignment(usage.target.makeRef(), value).source(x.expression.source) + val result = JsExpressionStatement(expr) + result.synthetic = usage.target in syntheticNames + result + } else { + val declaration = JsAstUtils.newVar(usage.target, value) + declaration.source(x.expression.source) + declaration.synthetic = usage.target in syntheticNames + declaration + } + statement + } + is Usage.Mutation -> { + JsExpressionStatement(JsAstUtils.assignment(usage.target, value).source(x.expression.source)) + } + } + ctx.replaceMe(replacement) + statementsToRemove += usage.statements + return false; + } + } + return super.visit(x, ctx) + } + + override fun visit(x: JsReturn, ctx: JsContext<*>): Boolean { + if (x in statementsToRemove) { + ctx.removeMe() + return false + } + return super.visit(x, ctx) + } + + override fun visit(x: JsVars, ctx: JsContext<*>): Boolean { + if (x in statementsToRemove) { + ctx.removeMe() + return false + } + return super.visit(x, ctx) + } + }.accept(root) + } + + private fun generateDeclarations() { + var index = 0 + usages.values.asSequence() + .filter { it is Usage.Declaration && it.replaced } + .map { it as Usage.Declaration } + .forEach { root.statements.add(index++, JsAstUtils.newVar(it.target, null)) } + } + + private fun tryRecord(expr: JsExpression, usage: Usage): Boolean { + if (expr !is JsNameRef) return false + val name = expr.name ?: return false + + usages[name] = usage + return true + } + + private fun use(name: JsName) { + usageCount[name] = 1 + (usageCount[name] ?: 0) + } + + private fun assign(name: JsName) { + assignmentCount[name] = 1 + (assignmentCount[name] ?: 0) + } + + private sealed class Usage(statement: JsStatement) { + val statements = mutableSetOf(statement) + + class Return(statement: JsStatement) : Usage(statement) + + class Assignment(statement: JsStatement, val target: JsName) : Usage(statement) + + class Declaration(statement: JsStatement, val target: JsName) : Usage(statement) { + var replaced = false + } + + class Mutation(statement: JsStatement, val target: JsExpression) : Usage(statement) + } +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/TemporaryVariableElimination.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/TemporaryVariableElimination.kt new file mode 100644 index 00000000000..0c3d10b6753 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/clean/TemporaryVariableElimination.kt @@ -0,0 +1,190 @@ +/* + * 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.synthetic +import org.jetbrains.kotlin.js.inline.util.canHaveSideEffect +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils + +internal class TemporaryVariableElimination(private val root: JsStatement) { + private val definitions = mutableMapOf() + private val usages = mutableMapOf() + private val definedValues = mutableMapOf() + private val temporary = mutableSetOf() + + fun apply() { + analyze() + perform() + } + + private fun analyze() { + object : JsVisitorWithContextImpl() { + val lastAssignedVars = mutableListOf() + + override fun visit(x: JsNameRef, ctx: JsContext<*>): Boolean { + val name = x.name + if (name != null && x.qualifier == null) { + val expr = definedValues[name] + useVariable(name) + if (lastAssignedVars.lastOrNull() == name) { + lastAssignedVars.removeAt(lastAssignedVars.lastIndex) + } + else if (expr == null || expr.canHaveSideEffect()) { + temporary -= name + } + } + return super.visit(x, ctx) + } + + override fun visit(x: JsTry, ctx: JsContext<*>): Boolean { + lastAssignedVars.clear() + return super.visit(x, ctx) + } + + override fun visit(x: JsWhile, ctx: JsContext<*>): Boolean { + lastAssignedVars.clear() + return super.visit(x, ctx) + } + + override fun visit(x: JsInvocation, ctx: JsContext<*>): Boolean { + x.arguments.asReversed().forEach { accept(it) } + accept(x.qualifier) + return false + } + + override fun visit(x: JsBreak, ctx: JsContext<*>) = false + + override fun visit(x: JsContinue, ctx: JsContext<*>) = false + + override fun visit(x: JsExpressionStatement, ctx: JsContext): Boolean { + val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression) + if (assignment != null) { + val (name, value) = assignment + assignVariable(name, value) + accept(value) + if (x.synthetic) { + temporary += name + } + else if (value.canHaveSideEffect()) { + lastAssignedVars.clear() + } + lastAssignedVars += name + return false + } + + var statement = x + val result = accept(x.expression) + if (result != x.expression) { + statement = JsExpressionStatement(result) + ctx.replaceMe(statement) + } + + if (statement.expression.canHaveSideEffect()) { + lastAssignedVars.clear() + } + return false + } + + override fun visit(x: JsBinaryOperation, ctx: JsContext<*>): Boolean { + accept(x.arg2) + accept(x.arg1) + return false + } + + override fun visit(x: JsVars, ctx: JsContext<*>): Boolean { + for (v in x.vars) { + val name = v.name + val value = v.initExpression + if (value != null) { + assignVariable(name, value) + accept(value) + if (x.synthetic) { + temporary += name + } + lastAssignedVars += name + } + } + return false + } + }.accept(root) + } + + private fun perform() { + object : JsVisitorWithContextImpl() { + override fun visit(x: JsExpressionStatement, ctx: JsContext<*>): Boolean { + val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression) + if (assignment != null) { + if (shouldConsiderTemporary(assignment.first)) { + ctx.removeMe() + return false + } + } + + return super.visit(x, ctx) + } + + override fun visit(x: JsVars, ctx: JsContext<*>): Boolean { + val filteredVars = x.vars.filter { it.initExpression == null || !shouldConsiderTemporary(it.name) } + x.vars.clear() + x.vars.addAll(filteredVars) + if (x.vars.isEmpty()) { + ctx.removeMe() + } + + return super.visit(x, ctx) + } + + override fun visit(x: JsNameRef, ctx: JsContext): Boolean { + val name = x.name + if (x.qualifier == null && name != null && shouldConsiderTemporary(name)) { + val newExpr = definedValues[name]!! + ctx.replaceMe(accept(newExpr)) + usages[name] = usages[name]!! - 1 + return false + } + return super.visit(x, ctx) + } + + override fun visit(x: JsBreak, ctx: JsContext<*>) = false + + override fun visit(x: JsContinue, ctx: JsContext<*>) = false + }.accept(root) + } + + private fun assignVariable(name: JsName, value: JsExpression) { + definitions[name] = (definitions[name] ?: 0) + 1 + definedValues[name] = value + } + + private fun useVariable(name: JsName) { + usages[name] = (usages[name] ?: 0) + 1 + } + + private fun shouldConsiderTemporary(name: JsName): Boolean { + if (definitions[name] ?: 0 != 1 || name !in temporary) return false + + val expr = definedValues[name] + return (expr != null && isTrivial(expr)) || usages[name] ?: 0 == 1 + } + + private fun isTrivial(expr: JsExpression) = when (expr) { + is JsNameRef -> expr.qualifier == null + is JsLiteral.JsValueLiteral -> true + else -> false + } +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt index e7721c5d976..008eed0a950 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.js.inline.context import com.google.dart.compiler.backend.js.ast.* +import com.google.dart.compiler.backend.js.ast.metadata.synthetic import java.util.ArrayList import java.util.IdentityHashMap @@ -53,6 +54,7 @@ class NamingContext( fun newVar(name: JsName, value: JsExpression? = null) { val vars = JsAstUtils.newVar(name, value) + vars.synthetic = true declarations.add(vars) } } diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/rewriters/ReturnReplacingVisitor.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/rewriters/ReturnReplacingVisitor.kt index 971837dad62..da77925fa29 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/rewriters/ReturnReplacingVisitor.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/rewriters/ReturnReplacingVisitor.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.js.inline.util.rewriters import com.google.dart.compiler.backend.js.ast.* +import com.google.dart.compiler.backend.js.ast.metadata.synthetic import org.jetbrains.kotlin.js.inline.util.canHaveSideEffect import org.jetbrains.kotlin.js.translate.utils.JsAstUtils @@ -37,7 +38,9 @@ class ReturnReplacingVisitor(private val resultRef: JsNameRef?, private val brea val returnReplacement = getReturnReplacement(x.expression) if (returnReplacement != null) { - ctx.addNext(JsExpressionStatement(returnReplacement)) + val statement = JsExpressionStatement(returnReplacement) + statement.synthetic = true + ctx.addNext(statement) } if (breakLabel != null) { @@ -48,8 +51,9 @@ class ReturnReplacingVisitor(private val resultRef: JsNameRef?, private val brea private fun getReturnReplacement(returnExpression: JsExpression?): JsExpression? { if (returnExpression != null) { - if (resultRef != null) + if (resultRef != null) { return JsAstUtils.assignment(resultRef, returnExpression) + } if (returnExpression.canHaveSideEffect()) return returnExpression diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineSizeReductionTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineSizeReductionTestGenerated.java index b40bc986281..df6e6daa1ee 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineSizeReductionTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/InlineSizeReductionTestGenerated.java @@ -35,6 +35,12 @@ public class InlineSizeReductionTestGenerated extends AbstractInlineSizeReductio KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("js/js.translator/testData/inlineSizeReduction/cases"), Pattern.compile("^(.+)\\.kt$"), true); } + @TestMetadata("inlineOrder.kt") + public void testInlineOrder() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/inlineOrder.kt"); + doTest(fileName); + } + @TestMetadata("lastBreak.kt") public void testLastBreak() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/lastBreak.kt"); @@ -47,6 +53,12 @@ public class InlineSizeReductionTestGenerated extends AbstractInlineSizeReductio doTest(fileName); } + @TestMetadata("propertyAssignment.kt") + public void testPropertyAssignment() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/propertyAssignment.kt"); + doTest(fileName); + } + @TestMetadata("returnInlineCall.kt") public void testReturnInlineCall() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/returnInlineCall.kt"); 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 509ea2a8aca..af68085048e 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 @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.js.translate.utils; import com.google.dart.compiler.backend.js.ast.*; import com.intellij.util.SmartList; +import kotlin.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.js.translate.context.Namer; @@ -299,6 +300,27 @@ public final class JsAstUtils { return new JsBinaryOperation(JsBinaryOperator.ASG, left, right); } + @Nullable + public static Pair decomposeAssignment(@NotNull JsExpression expr) { + if (!(expr instanceof JsBinaryOperation)) return null; + + JsBinaryOperation binary = (JsBinaryOperation) expr; + if (binary.getOperator() != JsBinaryOperator.ASG) return null; + + return new Pair(binary.getArg1(), binary.getArg2()); + } + + @Nullable + public static Pair decomposeAssignmentToVariable(@NotNull JsExpression expr) { + Pair assignment = decomposeAssignment(expr); + if (assignment == null || !(assignment.getFirst() instanceof JsNameRef)) return null; + + JsNameRef nameRef = (JsNameRef) assignment.getFirst(); + if (nameRef.getName() == null || nameRef.getQualifier() != null) return null; + + return new Pair(nameRef.getName(), assignment.getSecond()); + } + @NotNull public static JsBinaryOperation sum(@NotNull JsExpression left, @NotNull JsExpression right) { return new JsBinaryOperation(JsBinaryOperator.ADD, left, right); diff --git a/js/js.translator/testData/inlineSizeReduction/cases/inlineOrder.kt b/js/js.translator/testData/inlineSizeReduction/cases/inlineOrder.kt new file mode 100644 index 00000000000..9b7528ddd36 --- /dev/null +++ b/js/js.translator/testData/inlineSizeReduction/cases/inlineOrder.kt @@ -0,0 +1,73 @@ +package foo + +// CHECK_VARS_COUNT: function=test1 count=0 +// CHECK_VARS_COUNT: function=test2 count=0 +// CHECK_VARS_COUNT: function=test3 count=0 +// CHECK_VARS_COUNT: function=test4 count=0 +// CHECK_VARS_COUNT: function=test5 count=0 + +var global = "" +var globalNum = 1 +var returnValue = 0 + +fun pure(n: Int) = n + +fun x(n: Int) = globalNum++ * n + +inline fun a(n: Int) = x(n) + +fun b(n: Int) { + returnValue = n +} + +fun c(first: Int, second: Int, third: Int) = first + second + third + +inline fun d(first: Int, second: Int, third: Int) { + returnValue = first + second + third +} + +fun test1(): Int { + globalNum = 1 + return x(a(1) + a(10) + a(100)) +} + +fun test2() { + globalNum = 1 + b(a(1) + a(10) + a(100)) +} + +fun test3(): Int { + globalNum = 1 + return c(a(1), a(10), a(100)) +} + +fun test4() { + globalNum = 1 + d(a(1), a(10), a(100)) +} + +fun test5(): Int { + globalNum = 1 + return globalNum++ + a(10) + (globalNum++ * 100) +} + +fun box(): String { + var result = test1() + if (result != 1284) return "fail1: $result" + + test2() + result = returnValue + if (result != 321) return "fail2: $result" + + result = test3() + if (result != 321) return "fail3: $result" + + test4() + result = returnValue + if (result != 321) return "fail4: $result" + + result = test5() + if (result != 321) return "fail5: $result" + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/inlineSizeReduction/cases/propertyAssignment.kt b/js/js.translator/testData/inlineSizeReduction/cases/propertyAssignment.kt new file mode 100644 index 00000000000..3de81a4bd1b --- /dev/null +++ b/js/js.translator/testData/inlineSizeReduction/cases/propertyAssignment.kt @@ -0,0 +1,25 @@ +package foo + +// CHECK_CONTAINS_NO_CALLS: test +// CHECK_VARS_COUNT: function=test count=0 + +object SumHolder { + var sum = 0 +} + +internal inline fun sum(x: Int, y: Int): Int { + if (x == 0 || y == 0) return 0 + + return x + y +} + +internal fun test(x: Int, y: Int) { + SumHolder.sum = sum(x, y) +} + +fun box(): String { + test(1, 2) + assertEquals(3, SumHolder.sum) + + return "OK" +} \ No newline at end of file