From 9e89213d66c49e1e8a276cc9ffe8557bcfd2c1cb Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Fri, 31 Mar 2017 20:03:41 +0300 Subject: [PATCH] Prototyping DCE tool for JS --- .idea/modules.xml | 1 + build.xml | 3 + compiler/cli/cli.iml | 1 + .../jetbrains/kotlin/cli/js/dce/K2JSDce.kt | 33 ++ js/js.dce/js.dce.iml | 16 + .../jetbrains/kotlin/js/dce/AnalysisResult.kt | 34 ++ .../org/jetbrains/kotlin/js/dce/Analyzer.kt | 387 ++++++++++++++++++ .../org/jetbrains/kotlin/js/dce/Context.kt | 221 ++++++++++ .../kotlin/js/dce/DeadCodeElimination.kt | 107 +++++ .../js/dce/DeadCodeEliminationResult.kt | 19 + .../org/jetbrains/kotlin/js/dce/Eliminator.kt | 50 +++ .../org/jetbrains/kotlin/js/dce/InputFile.kt | 19 + .../kotlin/js/dce/ReachabilityTracker.kt | 232 +++++++++++ .../org/jetbrains/kotlin/js/dce/printTree.kt | 52 +++ .../src/org/jetbrains/kotlin/js/dce/util.kt | 82 ++++ .../js/resolve/diagnostics/JsCallChecker.kt | 2 +- .../kotlin/js/inline/FunctionReader.kt | 8 +- .../inline/util/fixForwardNameReferences.kt | 79 ++++ .../com/google/gwt/dev/js/JsAstMapper.java | 24 +- .../jetbrains/kotlin/js/parser/parserUtils.kt | 14 +- .../serialization/js/ast/JsAstSerializer.kt | 5 +- js/js.tests/js.tests.iml | 1 + .../jetbrains/kotlin/js/test/BasicBoxTest.kt | 67 ++- .../kotlin/js/test/ast/NameResolutionTest.kt | 4 +- .../js/test/optimizer/BasicOptimizerTest.kt | 4 +- .../reference/CallExpressionTranslator.java | 2 +- 26 files changed, 1445 insertions(+), 22 deletions(-) create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt create mode 100644 js/js.dce/js.dce.iml create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt create mode 100644 js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt create mode 100644 js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 3585088a404..d9a277ce3b1 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -68,6 +68,7 @@ + diff --git a/build.xml b/build.xml index dbddee457bf..11ed6dce1cc 100644 --- a/build.xml +++ b/build.xml @@ -112,6 +112,7 @@ + @@ -152,6 +153,7 @@ + @@ -265,6 +267,7 @@ + diff --git a/compiler/cli/cli.iml b/compiler/cli/cli.iml index d9040ff4b01..0dd3db7abef 100644 --- a/compiler/cli/cli.iml +++ b/compiler/cli/cli.iml @@ -24,5 +24,6 @@ + \ No newline at end of file diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt new file mode 100644 index 00000000000..fff1eaf5981 --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2017 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. + */ + +@file:JvmName("K2JSDce") +package org.jetbrains.kotlin.cli.js.dce + +import org.jetbrains.kotlin.js.dce.DeadCodeElimination +import org.jetbrains.kotlin.js.dce.InputFile +import org.jetbrains.kotlin.js.dce.extractRoots +import org.jetbrains.kotlin.js.dce.printTree + +fun main(args: Array) { + val files = args.map { InputFile(it) } + val nodes = DeadCodeElimination.run(files, emptySet()) { println(it) } + + println() + for (node in nodes.extractRoots()) { + printTree(node, { println(it) }, printNestedMembers = false, showLocations = true) + } +} \ No newline at end of file diff --git a/js/js.dce/js.dce.iml b/js/js.dce/js.dce.iml new file mode 100644 index 00000000000..50c3196e4e0 --- /dev/null +++ b/js/js.dce/js.dce.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt new file mode 100644 index 00000000000..a93bce9c41e --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/AnalysisResult.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.backend.ast.JsFunction +import org.jetbrains.kotlin.js.backend.ast.JsInvocation +import org.jetbrains.kotlin.js.backend.ast.JsNode +import org.jetbrains.kotlin.js.dce.Context.Node + +interface AnalysisResult { + val nodeMap: Map + + val astNodesToEliminate: Set + + val astNodesToSkip: Set + + val functionsToEnter: Set + + val invocationsToSkip: Set +} diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt new file mode 100644 index 00000000000..16d7967a578 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Analyzer.kt @@ -0,0 +1,387 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.dce.Context.Node +import org.jetbrains.kotlin.js.inline.util.collectLocalVariables +import org.jetbrains.kotlin.js.translate.context.Namer + +class Analyzer(private val context: Context) : JsVisitor() { + private val processedFunctions = mutableSetOf() + private val postponedFunctions = mutableMapOf() + private val nodeMap = mutableMapOf() + private val astNodesToEliminate = mutableSetOf() + private val astNodesToSkip = mutableSetOf() + private val invocationsToSkip = mutableSetOf() + val moduleMapping = mutableMapOf() + private val functionsToEnter = mutableSetOf() + + val analysisResult = object : AnalysisResult { + override val nodeMap: Map get() = this@Analyzer.nodeMap + + override val astNodesToEliminate: Set get() = this@Analyzer.astNodesToEliminate + + override val astNodesToSkip: Set get() = this@Analyzer.astNodesToSkip + + override val functionsToEnter: Set get() = this@Analyzer.functionsToEnter + + override val invocationsToSkip: Set get() = this@Analyzer.invocationsToSkip + } + + override fun visitVars(x: JsVars) { + x.vars.forEach { accept(it) } + } + + override fun visit(x: JsVars.JsVar) { + val rhs = x.initExpression + if (rhs != null) { + processAssignment(x, x.name.makeRef(), rhs)?.let { nodeMap[x] = it } + } + } + + override fun visitExpressionStatement(x: JsExpressionStatement) { + val expression = x.expression + if (expression is JsBinaryOperation) { + if (expression.operator == JsBinaryOperator.ASG) { + processAssignment(x, expression.arg1, expression.arg2)?.let { + // Mark this statement with FQN extracted from assignment. + // Later, we eliminate such statements if corresponding FQN is reachable + nodeMap[x] = it + } + } + } + else if (expression is JsFunction) { + expression.name?.let { context.nodes[it]?.original }?.let { + nodeMap[x] = it + it.functions += expression + } + } + else if (expression is JsInvocation) { + val function = expression.qualifier + + // (function(params) { ... })(arguments), assume that params = arguments and walk its body + if (function is JsFunction) { + enterFunction(function, expression.arguments) + return + } + + // f(arguments), where f is a parameter of outer function and it always receives function() { } as an argument. + if (function is JsNameRef && function.qualifier == null) { + val postponedFunction = function.name?.let { postponedFunctions[it] } + if (postponedFunction != null) { + enterFunction(postponedFunction, expression.arguments) + invocationsToSkip += expression + return + } + } + + // Object.defineProperty() + if (context.isObjectDefineProperty(function)) { + handleObjectDefineProperty(x, expression.arguments.getOrNull(0), expression.arguments.getOrNull(1), + expression.arguments.getOrNull(2)) + } + + // Kotlin.defineModule() + else if (context.isDefineModule(function)) { + // (just remove it) + astNodesToEliminate += x + } + + else if (context.isAmdDefine(function)) { + handleAmdDefine(expression, expression.arguments) + } + } + } + + private fun handleObjectDefineProperty(statement: JsStatement, target: JsExpression?, propertyName: JsExpression?, + propertyDescriptor: JsExpression?) { + if (target == null || propertyName !is JsStringLiteral || propertyDescriptor == null) return + val targetNode = context.extractNode(target) ?: return + + val memberNode = targetNode.member(propertyName.value) + nodeMap[statement] = memberNode + memberNode.hasSideEffects = true + + // Object.defineProperty(instance, name, { get: value, ... }) + if (propertyDescriptor is JsObjectLiteral) { + for (initializer in propertyDescriptor.propertyInitializers) { + // process as if it was instance.name = value + processAssignment(statement, JsNameRef(propertyName.value, target), initializer.valueExpr) + } + } + // Object.defineProperty(instance, name, Object.getOwnPropertyDescriptor(otherInstance)) + else if (propertyDescriptor is JsInvocation) { + val function = propertyDescriptor.qualifier + if (context.isObjectGetOwnPropertyDescriptor(function)) { + val source = propertyDescriptor.arguments.getOrNull(0) + val sourcePropertyName = propertyDescriptor.arguments.getOrNull(1) + if (source != null && sourcePropertyName is JsStringLiteral) { + // process as if it was instance.name = otherInstance.name + processAssignment(statement, JsNameRef(propertyName.value, target), JsNameRef(sourcePropertyName.value, source)) + } + } + } + } + + private fun handleAmdDefine(invocation: JsInvocation, arguments: List) { + // Handle both named and anonymous modules + val argumentsWithoutName = when (arguments.size) { + 2 -> arguments + 3 -> arguments.drop(1) + else -> return + } + + val dependencies = argumentsWithoutName[0] as? JsArrayLiteral ?: return + + // Function can be either a function() { ... } or a reference to parameter out outer function which is known to take + // function literal + val functionRef = argumentsWithoutName[1] + val function = when (functionRef) { + is JsFunction -> functionRef + is JsNameRef -> { + if (functionRef.qualifier != null) return + postponedFunctions[functionRef.name] ?: return + } + else -> return + } + + val dependencyNodes = dependencies.expressions + .map { it as? JsStringLiteral ?: return } + .map { if (it.value == "exports") context.currentModule else context.globalScope.member(it.value) } + + enterFunctionWithGivenNodes(function, dependencyNodes) + astNodesToSkip += invocation.qualifier + } + + override fun visitBlock(x: JsBlock) { + val newModule = moduleMapping[x] + if (newModule != null) { + context.currentModule = context.globalScope.member(newModule) + } + x.statements.forEach { accept(it) } + } + + override fun visitIf(x: JsIf) { + accept(x.thenStatement) + x.elseStatement?.accept(this) + } + + override fun visitReturn(x: JsReturn) { + val expr = x.expression + if (expr != null) { + context.extractNode(expr)?.let { + nodeMap[x] = it + } + } + } + + private fun processAssignment(node: JsNode?, lhs: JsExpression, rhs: JsExpression): Node? { + val leftNode = context.extractNode(lhs) + val rightNode = context.extractNode(rhs) + + if (leftNode != null && rightNode != null) { + // If both left and right expressions are fully-qualified names, alias them + leftNode.alias(rightNode) + return leftNode + } + else if (leftNode != null) { + // lhs = foo() + if (rhs is JsInvocation) { + val function = rhs.qualifier + + // lhs = function(params) { ... }(arguments) + // see corresponding case in visitExpressionStatement + if (function is JsFunction) { + enterFunction(function, rhs.arguments) + astNodesToSkip += lhs + return null + } + + // lhs = foo(arguments), where foo is a parameter of outer function that always take function literal + // see corresponding case in visitExpressionStatement + if (function is JsNameRef && function.qualifier == null) { + function.name?.let { postponedFunctions[it] }?.let { + enterFunction(it, rhs.arguments) + astNodesToSkip += lhs + return null + } + } + + // lhs = Object.create(constructor) + if (context.isObjectFunction(function, "create")) { + // Do not alias lhs and constructor, make unidirectional dependency lhs -> constructor instead. + // Motivation: reachability of a base class does not imply reachability of its derived class + handleObjectCreate(leftNode, rhs.arguments.getOrNull(0)) + return leftNode + } + + // lhs = Kotlin.defineInlineFunction('fqn', function() { ... }) + if (context.isDefineInlineFunction(function) && rhs.arguments.size == 2) { + leftNode.functions += rhs.arguments[1] as JsFunction + val defineInlineFunctionNode = context.extractNode(function) + if (defineInlineFunctionNode != null) { + leftNode.dependencies += defineInlineFunctionNode + } + return leftNode + } + } + else if (rhs is JsBinaryOperation) { + // Detect lhs = parent.child || (parent.child = {}), which is used to declare packages. + // Assume lhs = parent.child + if (rhs.operator == JsBinaryOperator.OR) { + val secondNode = context.extractNode(rhs.arg1) + val reassignment = rhs.arg2 + if (reassignment is JsBinaryOperation && reassignment.operator == JsBinaryOperator.ASG) { + val reassignNode = context.extractNode(reassignment.arg1) + val reassignValue = reassignment.arg2 + if (reassignNode == secondNode && reassignNode != null && reassignValue is JsObjectLiteral && + reassignValue.propertyInitializers.isEmpty() + ) { + return processAssignment(node, lhs, rhs.arg1) + } + } + } + } + else if (rhs is JsFunction) { + // lhs = function() { ... } + // During reachability tracking phase: eliminate it if lhs is unreachable, traverse function otherwise + leftNode.functions += rhs + return leftNode + } + else if (leftNode.qualifier?.memberName == Namer.METADATA) { + // lhs.$metadata$ = expression + // During reachability tracking phase: eliminate it if lhs is unreachable, traverse expression + // It's commonly used to supply class's metadata + leftNode.expressions += rhs + return leftNode + } + else if (rhs is JsObjectLiteral && rhs.propertyInitializers.isEmpty()) { + return leftNode + } + + val nodeInitializedByEmptyObject = extractVariableInitializedByEmptyObject(rhs) + if (nodeInitializedByEmptyObject != null) { + astNodesToSkip += rhs + leftNode.alias(nodeInitializedByEmptyObject) + return leftNode + } + } + return null + } + + private fun handleObjectCreate(target: Node, arg: JsExpression?) { + if (arg == null) return + + val prototypeNode = context.extractNode(arg) ?: return + target.dependencies += prototypeNode.original + target.expressions += arg + } + + // Handle typeof foo === 'undefined' ? {} : foo, where foo is FQN + // Assume foo + // This is used by UMD wrapper + private fun extractVariableInitializedByEmptyObject(expression: JsExpression): Node? { + if (expression !is JsConditional) return null + + val testExpr = expression.testExpression as? JsBinaryOperation ?: return null + if (testExpr.operator != JsBinaryOperator.REF_EQ) return null + + val testExprLhs = testExpr.arg1 as? JsPrefixOperation ?: return null + if (testExprLhs.operator != JsUnaryOperator.TYPEOF) return null + val testExprNode = context.extractNode(testExprLhs.arg) ?: return null + + val testExprRhs = testExpr.arg2 as? JsStringLiteral ?: return null + if (testExprRhs.value != "undefined") return null + + val thenExpr = expression.thenExpression as? JsObjectLiteral ?: return null + if (thenExpr.propertyInitializers.isNotEmpty()) return null + + val elseNode = context.extractNode(expression.elseExpression) ?: return null + + if (testExprNode.original != elseNode.original) return null + return testExprNode.original + } + + // foo(), where foo is either function literal or parameter of outer function that takes function literal. + // The latter case is required to handle UMD wrapper + // Skip arguments during reachability tracker phase + // Traverse function's body + private fun enterFunction(function: JsFunction, arguments: List) { + functionsToEnter += function + context.addNodesForLocalVars(function.collectLocalVariables()) + + for ((param, arg) in function.parameters.zip(arguments)) { + if (arg is JsFunction && arg.name == null && isProperFunctionalParameter(arg.body, param)) { + postponedFunctions[param.name] = arg + } + else { + if (processAssignment(function, param.name.makeRef(), arg) != null) { + astNodesToSkip += arg + } + } + } + + processFunction(function) + } + + private fun enterFunctionWithGivenNodes(function: JsFunction, arguments: List) { + functionsToEnter += function + context.addNodesForLocalVars(function.collectLocalVariables()) + + for ((param, arg) in function.parameters.zip(arguments)) { + val paramNode = context.nodes[param.name]!! + paramNode.alias(arg) + } + + processFunction(function) + } + + private fun processFunction(function: JsFunction) { + if (processedFunctions.add(function)) { + accept(function.body) + } + } + + // Consider the case: (function(f) { A })(function() { B }) (commonly used in UMD wrapper) + // f = function() { B }. + // Assume A with all occurrences of f() replaced by B. + // However, we need first to ensure that f always occurs as an invocation qualifier, which is checked with this function + private fun isProperFunctionalParameter(body: JsStatement, parameter: JsParameter): Boolean { + var result = true + body.accept(object : RecursiveJsVisitor() { + override fun visitInvocation(invocation: JsInvocation) { + val qualifier = invocation.qualifier + if (qualifier is JsNameRef && qualifier.qualifier == null && qualifier.name == parameter.name) { + if (invocation.arguments.all { context.extractNode(it) != null }) { + return + } + } + if (context.isAmdDefine(qualifier)) return + super.visitInvocation(invocation) + } + + override fun visitNameRef(nameRef: JsNameRef) { + if (nameRef.name == parameter.name) { + result = false + } + super.visitNameRef(nameRef) + } + }) + return result + } +} \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt new file mode 100644 index 00000000000..8f3c7e06cd3 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Context.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.array +import org.jetbrains.kotlin.js.translate.utils.jsAstUtils.index + +class Context { + val globalScope = Node() + val moduleExportsNode = globalScope.member("module").member("exports") + var currentModule = globalScope + val nodes = mutableMapOf() + var thisNode: Node? = globalScope + val namesOfLocalVars = mutableSetOf() + + fun addNodesForLocalVars(names: Collection) { + nodes += names.filter { it !in nodes }.associate { it to Node(it) } + } + + fun extractNode(expression: JsExpression): Node? { + val node = extractNodeImpl(expression)?.original + return if (node != null && moduleExportsNode in generateSequence(node) { it.qualifier?.parent }) { + val path = node.pathFromRoot().drop(2) + path.fold(currentModule.original) { n, memberName -> n.member(memberName) } + } + else { + node + } + } + + private fun extractNodeImpl(expression: JsExpression): Node? { + return when (expression) { + is JsNameRef -> { + val qualifier = expression.qualifier + if (qualifier == null) { + val name = expression.name + if (name != null) { + if (name in namesOfLocalVars) return null + nodes[name]?.original?.let { return it } + } + globalScope.member(expression.ident) + } + else { + extractNodeImpl(qualifier)?.member(expression.ident) + } + } + is JsArrayAccess -> { + val index = expression.index + if (index is JsStringLiteral) extractNodeImpl(expression.array)?.member(index.value) else null + } + is JsLiteral.JsThisRef -> { + thisNode + } + is JsInvocation -> { + val qualifier = expression.qualifier + if (qualifier is JsNameRef && qualifier.qualifier == null && qualifier.ident == "require" && + qualifier.name !in nodes && expression.arguments.size == 1 + ) { + val argument = expression.arguments[0] + if (argument is JsStringLiteral) { + return globalScope.member(argument.value) + } + } + null + } + else -> { + null + } + } + } + + class Node private constructor(val localName: JsName?, qualifier: Qualifier?) { + private val dependenciesImpl = mutableSetOf() + private val expressionsImpl = mutableSetOf() + private val functionsImpl = mutableSetOf() + private var hasSideEffectsImpl = false + private var reachableImpl = false + private var declarationReachableImpl = false + private val membersImpl = mutableMapOf() + private val usedByAstNodesImpl = mutableSetOf() + private var rank = 0 + + val dependencies: MutableSet get() = original.dependenciesImpl + + val expressions: MutableSet get() = original.expressionsImpl + + val functions: MutableSet get() = original.functionsImpl + + var hasSideEffects: Boolean + get() = original.hasSideEffectsImpl + set(value) { + original.hasSideEffectsImpl = value + } + + var reachable: Boolean + get() = original.reachableImpl + set(value) { + original.reachableImpl = value + } + + var declarationReachable: Boolean + get() = original.declarationReachableImpl + set(value) { + original.declarationReachableImpl = value + } + + var qualifier: Qualifier? = qualifier + get + private set + + val usedByAstNodes: MutableSet get() = original.usedByAstNodesImpl + + val memberNames: MutableSet get() = original.membersImpl.keys + + constructor(localName: JsName? = null) : this(localName, null) + + var original: Node = this + get() { + if (field != this) { + field = field.original + } + return field + } + private set + + val members: Map get() = original.membersImpl + + fun member(name: String): Node = original.membersImpl.getOrPut(name) { Node(null, Qualifier(this, name)) }.original + + fun alias(other: Node) { + val a = original + val b = other.original + if (a == b) return + + if (a.qualifier == null && b.qualifier == null) { + a.merge(b) + } + else if (a.qualifier == null) { + if (b.root() == a) a.makeDependencies(b) else b.evacuateFrom(a) + } + else if (b.qualifier == null) { + if (a.root() == b) a.makeDependencies(b) else a.evacuateFrom(b) + } + else { + a.makeDependencies(b) + } + } + + private fun makeDependencies(other: Node) { + dependenciesImpl += other + other.dependenciesImpl += this + } + + private fun evacuateFrom(other: Node) { + val (existingMembers, newMembers) = other.members.toList().partition { (name, _) -> name in membersImpl } + other.original = this + + for ((name, member) in newMembers) { + membersImpl[name] = member + member.original.qualifier = Qualifier(this, member.original.qualifier!!.memberName) + } + for ((name, member) in existingMembers) { + membersImpl[name]!!.original.merge(member.original) + membersImpl[name] = member.original + member.original.qualifier = Qualifier(this, member.original.qualifier!!.memberName) + } + other.membersImpl.clear() + + hasSideEffectsImpl = hasSideEffectsImpl || other.hasSideEffectsImpl + expressionsImpl += other.expressionsImpl + functionsImpl += other.functionsImpl + dependenciesImpl += other.dependenciesImpl + usedByAstNodesImpl += other.usedByAstNodesImpl + + other.expressionsImpl.clear() + other.functionsImpl.clear() + other.dependenciesImpl.clear() + other.usedByAstNodesImpl.clear() + } + + private fun merge(other: Node) { + if (this == other) return + + if (rank < other.rank) { + other.evacuateFrom(this) + } + else { + evacuateFrom(other) + } + + if (rank == other.rank) { + rank++ + } + } + + fun root(): Node = generateSequence(original) { it.qualifier?.parent?.original }.last() + + fun pathFromRoot(): List = + generateSequence(original) { it.qualifier?.parent?.original }.mapNotNull { it.qualifier?.memberName } + .toList().asReversed() + + override fun toString(): String = (root().localName?.ident ?: "") + pathFromRoot().joinToString("") { ".$it" } + } + + class Qualifier(val parent: Node, val memberName: String) +} \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt new file mode 100644 index 00000000000..5815a3e6914 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2017 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.dce + +import com.google.gwt.dev.js.rhino.CodePosition +import com.google.gwt.dev.js.rhino.ErrorReporter +import org.jetbrains.kotlin.js.backend.ast.JsBlock +import org.jetbrains.kotlin.js.backend.ast.JsGlobalBlock +import org.jetbrains.kotlin.js.backend.ast.JsNode +import org.jetbrains.kotlin.js.backend.ast.JsProgram +import org.jetbrains.kotlin.js.dce.Context.Node +import org.jetbrains.kotlin.js.inline.util.collectDefinedNames +import org.jetbrains.kotlin.js.inline.util.fixForwardNameReferences +import org.jetbrains.kotlin.js.parser.parse +import java.io.File + +class DeadCodeElimination(val logConsumer: (String) -> Unit) { + val moduleMapping = mutableMapOf() + private val reachableNames = mutableSetOf() + + var reachableNodes = setOf() + get + private set + + fun apply(root: JsNode) { + val context = Context() + + val topLevelVars = collectDefinedNames(root) + context.addNodesForLocalVars(topLevelVars) + for (name in topLevelVars) { + context.nodes[name]!!.alias(context.globalScope.member(name.ident)) + } + + val analyzer = Analyzer(context) + analyzer.moduleMapping += moduleMapping + root.accept(analyzer) + + val usageFinder = ReachabilityTracker(context, analyzer.analysisResult, logConsumer) + root.accept(usageFinder) + + for (reachableName in reachableNames) { + val path = reachableName.split(".") + val node = path.fold(context.globalScope) { node, part -> node.member(part) } + usageFinder.reach(node) + } + reachableNodes = usageFinder.reachableNodes + + Eliminator(analyzer.analysisResult).accept(root) + } + + companion object { + fun run( + inputFiles: Collection, + rootReachableNames: Set, + logConsumer: (String) -> Unit + ): DeadCodeEliminationResult { + val program = JsProgram() + val dce = DeadCodeElimination(logConsumer) + + val blocks = inputFiles.map { file -> + val block = JsGlobalBlock() + val code = File(file.path).readText() + block.statements += parse(code, reporter, program.scope, file.path) + file.moduleName?.let { dce.moduleMapping[block] = it } + block + } + program.globalBlock.statements += blocks + program.globalBlock.fixForwardNameReferences() + + dce.reachableNames += rootReachableNames + dce.apply(program.globalBlock) + + for ((file, block) in inputFiles.zip(blocks)) { + with(File(file.outputPath)) { + parentFile.mkdirs() + writeText(block.toString()) + } + } + + return DeadCodeEliminationResult(dce.reachableNodes) + } + + private val reporter = object : ErrorReporter { + override fun warning(message: String, startPosition: CodePosition, endPosition: CodePosition) { + println("[WARN] at ${startPosition.line}, ${startPosition.offset}: $message") + } + + override fun error(message: String, startPosition: CodePosition, endPosition: CodePosition) { + println("[ERRO] at ${startPosition.line}, ${startPosition.offset}: $message") + } + } + } +} \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt new file mode 100644 index 00000000000..9479280be78 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeEliminationResult.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2017 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.dce + +class DeadCodeEliminationResult(val reachableNodes: Set) \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt new file mode 100644 index 00000000000..c4306424c56 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/Eliminator.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.backend.ast.* + +class Eliminator(private val analysisResult: AnalysisResult) : JsVisitorWithContextImpl() { + override fun visit(x: JsVars.JsVar, ctx: JsContext<*>): Boolean = removeIfNecessary(x, ctx) + + override fun visit(x: JsExpressionStatement, ctx: JsContext<*>): Boolean = removeIfNecessary(x, ctx) + + override fun visit(x: JsReturn, ctx: JsContext<*>): Boolean = removeIfNecessary(x, ctx) + + private fun removeIfNecessary(x: JsNode, ctx: JsContext<*>): Boolean { + if (x in analysisResult.astNodesToEliminate) { + ctx.removeMe() + return false + } + val node = analysisResult.nodeMap[x]?.original + return if (!isUsed(node)) { + ctx.removeMe() + false + } + else { + true + } + } + + override fun endVisit(x: JsVars, ctx: JsContext<*>) { + if (x.vars.isEmpty()) { + ctx.removeMe() + } + } + + private fun isUsed(node: Context.Node?): Boolean = node == null || node.declarationReachable +} \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt new file mode 100644 index 00000000000..1e6e7b08857 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2017 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.dce + +class InputFile(val path: String, val outputPath: String, val moduleName: String? = null) \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt new file mode 100644 index 00000000000..2566e47e9c0 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/ReachabilityTracker.kt @@ -0,0 +1,232 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.dce.Context.Node +import org.jetbrains.kotlin.js.inline.util.collectLocalVariables + +class ReachabilityTracker( + private val context: Context, + private val analysisResult: AnalysisResult, + private val logConsumer: (String) -> Unit +) : RecursiveJsVisitor() { + companion object { + private val CALL_FUNCTIONS = setOf("call", "apply") + } + + private var currentNodeWithLocation: JsNode? = null + private var depth = 0 + private val reachableNodesImpl = mutableSetOf() + + val reachableNodes: Set get() = reachableNodesImpl + + override fun visit(x: JsVars.JsVar) { + if (shouldTraverse(x)) { + super.visit(x) + } + } + + override fun visitExpressionStatement(x: JsExpressionStatement) { + if (shouldTraverse(x)) { + super.visitExpressionStatement(x) + } + } + + override fun visitReturn(x: JsReturn) { + if (shouldTraverse(x)) { + super.visitReturn(x) + } + } + + private fun shouldTraverse(x: JsNode): Boolean = + analysisResult.nodeMap[x] == null && x !in analysisResult.astNodesToEliminate + + override fun visitNameRef(nameRef: JsNameRef) { + if (nameRef in analysisResult.astNodesToSkip) return + + val node = context.extractNode(nameRef) + if (node != null) { + if (!node.reachable) { + reportAndNest("reach: referenced name $node", currentNodeWithLocation) { + reach(node) + currentNodeWithLocation?.let { node.usedByAstNodes += it } + } + } + } + else { + super.visitNameRef(nameRef) + } + } + + override fun visitInvocation(invocation: JsInvocation) { + val function = invocation.qualifier + when { + function is JsFunction && function in analysisResult.functionsToEnter -> { + accept(function.body) + for (argument in invocation.arguments.filter { it is JsFunction && it in analysisResult.functionsToEnter }) { + accept(argument) + } + } + invocation in analysisResult.invocationsToSkip -> {} + else -> { + val node = context.extractNode(invocation.qualifier) + if (node != null && node.qualifier?.memberName in CALL_FUNCTIONS) { + val parent = node.qualifier!!.parent + reach(parent) + currentNodeWithLocation?.let { parent.usedByAstNodes += it } + } + super.visitInvocation(invocation) + } + } + } + + override fun visitFunction(x: JsFunction) { + if (x !in analysisResult.functionsToEnter) { + x.collectLocalVariables().let { + context.addNodesForLocalVars(it) + context.namesOfLocalVars += it + } + withErasedThis { + super.visitFunction(x) + } + } + else { + super.visitFunction(x) + } + } + + private fun withErasedThis(action: () -> Unit) { + val oldThis = context.thisNode + context.thisNode = null + action() + context.thisNode = oldThis + } + + override fun visitBreak(x: JsBreak) { } + + override fun visitContinue(x: JsContinue) { } + + fun reach(node: Node) { + if (node.reachable) return + node.reachable = true + reachableNodesImpl += node + + reachDeclaration(node) + + reachDependencies(node) + node.members.toList().forEach { (name, member) -> + if (!member.reachable) { + reportAndNest("reach: member $name", null) { reach(member) } + } + } + + for (expr in node.functions) { + reportAndNest("traverse: function", expr) { + expr.collectLocalVariables().let { + context.addNodesForLocalVars(it) + context.namesOfLocalVars += it + } + withErasedThis { expr.body.accept(this) } + } + } + for (expr in node.expressions) { + reportAndNest("traverse: value", expr) { + expr.accept(this) + } + } + } + + private fun reachDependencies(node: Node) { + val path = mutableListOf() + var current = node + while (true) { + for (ancestorDependency in current.dependencies) { + if (current in generateSequence(ancestorDependency) { it.qualifier?.parent }) continue + val dependency = path.asReversed().fold(ancestorDependency) { n, memberName -> n.member(memberName) } + if (!dependency.reachable) { + reportAndNest("reach: dependency $dependency", null) { reach(dependency) } + } + } + val qualifier = current.qualifier ?: break + path += qualifier.memberName + current = qualifier.parent + } + } + + private fun reachDeclaration(node: Node) { + if (node.hasSideEffects && !node.reachable) { + reportAndNest("reach: because of side effect", null) { + reach(node) + } + } + else if (!node.declarationReachable) { + node.declarationReachable = true + + node.original.qualifier?.parent?.let { + reportAndNest("reach-decl: parent $it", null) { + reachDeclaration(it) + } + } + + for (expr in node.expressions) { + reportAndNest("traverse: value", expr) { + expr.accept(this) + } + } + } + } + + override fun visitPrefixOperation(x: JsPrefixOperation) { + if (x.operator == JsUnaryOperator.TYPEOF) { + val arg = x.arg + if (arg is JsNameRef && arg.qualifier == null) { + context.extractNode(arg)?.let { reachDeclaration(it) } + return + } + } + super.visitPrefixOperation(x) + } + + override fun visitElement(node: JsNode) { + if (node in analysisResult.astNodesToSkip) return + val newLocation = node.extractLocation() + val old = currentNodeWithLocation + if (newLocation != null) { + currentNodeWithLocation = node + } + super.visitElement(node) + currentNodeWithLocation = old + } + + private fun report(message: String) { + logConsumer(" ".repeat(depth) + message) + } + + private fun reportAndNest(message: String, dueTo: JsNode?, action: () -> Unit) { + val location = dueTo?.extractLocation() + val fullMessage = if (location != null) "$message (due to ${location.asString()})" else message + report(fullMessage) + nested(action) + } + + private fun nested(action: () -> Unit) { + depth++ + action() + depth-- + } +} \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt new file mode 100644 index 00000000000..7da25da5839 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/printTree.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.dce.Context.Node + +fun printTree(root: Node, consumer: (String) -> Unit, printNestedMembers: Boolean = false, showLocations: Boolean = false) { + printTree(root, consumer, 0, Settings(printNestedMembers, showLocations)) +} + +private fun printTree(node: Node, consumer: (String) -> Unit, depth: Int, settings: Settings) { + val sb = StringBuilder() + sb.append(" ".repeat(depth)).append(node.qualifier?.memberName ?: node.toString()) + + if (node.reachable) { + sb.append(" (reachable") + if (settings.showLocations) { + val locations = node.usedByAstNodes.mapNotNull { it.extractLocation() } + if (locations.isNotEmpty()) { + sb.append(" from ").append(locations.joinToString { it.asString() }) + } + } + sb.append(")") + } + + consumer(sb.toString()) + + for (memberName in node.memberNames.sorted()) { + val member = node.member(memberName) + if (!member.declarationReachable) continue + + if ((!node.reachable || !member.reachable) || settings.printNestedMembers) { + printTree(member, consumer, depth + 1, settings) + } + } +} + +private class Settings(val printNestedMembers: Boolean, val showLocations: Boolean) \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt new file mode 100644 index 00000000000..a5c1a67fe95 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/util.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2017 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.dce + +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.dce.Context.Node + +fun Context.isObjectDefineProperty(function: JsExpression) = isObjectFunction(function, "defineProperty") + +fun Context.isObjectGetOwnPropertyDescriptor(function: JsExpression) = isObjectFunction(function, "getOwnPropertyDescriptor") + +fun Context.isDefineModule(function: JsExpression): Boolean = isKotlinFunction(function, "defineModule") + +fun Context.isDefineInlineFunction(function: JsExpression): Boolean = isKotlinFunction(function, "defineInlineFunction") + +fun Context.isObjectFunction(function: JsExpression, functionName: String): Boolean { + if (function !is JsNameRef) return false + if (function.ident != functionName) return false + + val receiver = function.qualifier as? JsNameRef ?: return false + if (receiver.name?.let { nodes[it] } != null) return false + + return receiver.ident == "Object" +} + +fun Context.isKotlinFunction(function: JsExpression, name: String): Boolean { + if (function !is JsNameRef || function.ident != name) return false + val receiver = (function.qualifier as? JsNameRef)?.name ?: return false + return receiver in nodes && receiver.ident.toLowerCase() == "kotlin" +} + +fun Context.isAmdDefine(function: JsExpression): Boolean = isTopLevelFunction(function, "define") + +fun Context.isTopLevelFunction(function: JsExpression, name: String): Boolean { + if (function !is JsNameRef || function.qualifier != null) return false + return function.ident == name && function.name !in nodes.keys +} + +fun JsNode.extractLocation(): JsLocation? { + return when (this) { + is SourceInfoAwareJsNode -> source as? JsLocation + is JsExpressionStatement -> expression.source as? JsLocation + else -> null + } +} + +fun JsLocation.asString(): String { + val simpleFileName = file.substring(file.lastIndexOf("/") + 1) + return "$simpleFileName:${startLine + 1}" +} + +fun Set.extractRoots(): Set { + val result = mutableSetOf() + val visited = mutableSetOf() + forEach { it.original.extractRootsImpl(result, visited) } + return result +} + +private fun Node.extractRootsImpl(target: MutableSet, visited: MutableSet) { + if (!visited.add(original)) return + val qualifier = original.qualifier + if (qualifier == null) { + target += original + } + else { + qualifier.parent.extractRootsImpl(target, visited) + } +} diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt index 487c7f62537..988de5b76d8 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt @@ -90,7 +90,7 @@ class JsCallChecker( try { val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "") - val statements = parse(code, errorReporter, parserScope) + val statements = parse(code, errorReporter, parserScope, reportOn.containingFile?.name ?: "") if (statements.isEmpty()) { context.trace.report(ErrorsJs.JSCODE_NO_JAVASCRIPT_PRODUCED.on(argument)) diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt index de201c476eb..cca07ac8fbf 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt @@ -57,7 +57,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName * kotlinVariable: kotlin object variable. * The default variable is Kotlin, but it can be renamed by minifier. */ - data class ModuleInfo(val fileContent: String, val moduleVariable: String, val kotlinVariable: String) + data class ModuleInfo(val filePath: String, val fileContent: String, val moduleVariable: String, val kotlinVariable: String) private val moduleNameToInfo = HashMultimap.create() @@ -68,7 +68,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName moduleNameMap = buildModuleNameMap(fragments) - JsLibraryUtils.traverseJsLibraries(libs) { fileContent, _ -> + JsLibraryUtils.traverseJsLibraries(libs) { fileContent, filePath -> var current = 0 while (true) { @@ -83,7 +83,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName val moduleName = preciseMatcher.group(3) val moduleVariable = preciseMatcher.group(4) val kotlinVariable = preciseMatcher.group(1) - moduleNameToInfo.put(moduleName, ModuleInfo(fileContent, moduleVariable, kotlinVariable)) + moduleNameToInfo.put(moduleName, ModuleInfo(filePath, fileContent, moduleVariable, kotlinVariable)) } } } @@ -152,7 +152,7 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName offset++ } - val function = parseFunction(source, offset, ThrowExceptionOnErrorReporter, JsRootScope(JsProgram())) + val function = parseFunction(source, info.filePath, offset, ThrowExceptionOnErrorReporter, JsRootScope(JsProgram())) val moduleReference = moduleNameMap[tag] ?: currentModuleName.makeRef() val replacements = hashMapOf(info.moduleVariable to moduleReference, diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt new file mode 100644 index 00000000000..81f6debdd35 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/fixForwardNameReferences.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2017 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 org.jetbrains.kotlin.js.backend.ast.* + +fun JsNode.fixForwardNameReferences() { + accept(object : RecursiveJsVisitor() { + val currentScope = mutableMapOf() + + init { + currentScope += collectDefinedNames(this@fixForwardNameReferences).associateBy { it.ident } + } + + override fun visitFunction(x: JsFunction) { + val scopeBackup = mutableMapOf() + val localVars = x.collectLocalVariables() + for (localVar in localVars) { + scopeBackup[localVar.ident] = currentScope[localVar.ident] + currentScope[localVar.ident] = localVar + } + + super.visitFunction(x) + + for ((ident, oldName) in scopeBackup) { + if (oldName == null) { + currentScope -= ident + } + else { + currentScope[ident] = oldName + } + } + } + + override fun visitCatch(x: JsCatch) { + val name = x.parameter.name + val oldName = currentScope[name.ident] + currentScope[name.ident] = name + + super.visitCatch(x) + + if (oldName != null) { + currentScope[name.ident] = name + } + else { + currentScope -= name.ident + } + } + + override fun visitNameRef(nameRef: JsNameRef) { + super.visitNameRef(nameRef) + if (nameRef.qualifier == null) { + val ident = nameRef.ident + val name = currentScope[ident] + if (name != null) { + nameRef.name = name + } + } + } + + override fun visitBreak(x: JsBreak) {} + + override fun visitContinue(x: JsContinue) {} + }) +} \ No newline at end of file diff --git a/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java b/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java index 9e47f4221f7..078ba9c7977 100644 --- a/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java +++ b/js/js.parser/src/com/google/gwt/dev/js/JsAstMapper.java @@ -32,9 +32,13 @@ public class JsAstMapper { private final JsProgram program; private final ScopeContext scopeContext; - public JsAstMapper(@NotNull JsScope scope) { + @NotNull + private final String fileName; + + public JsAstMapper(@NotNull JsScope scope, @NotNull String fileName) { scopeContext = new ScopeContext(scope); program = scope.getProgram(); + this.fileName = fileName; } private static JsParserException createParserException(String msg, Node offender) { @@ -43,6 +47,10 @@ public class JsAstMapper { } private JsNode map(Node node) throws JsParserException { + return withLocation(mapWithoutLocation(node), node); + } + + private JsNode mapWithoutLocation(Node node) throws JsParserException { switch (node.getType()) { case TokenStream.SCRIPT: { JsBlock block = new JsBlock(); @@ -1115,4 +1123,18 @@ public class JsAstMapper { int type = jsNode.getType(); return type == TokenStream.NUMBER || type == TokenStream.NUMBER; } + + private T withLocation(T astNode, Node node) { + int lineNumber = node.getLineno(); + if (lineNumber >= 0) { + JsLocation location = new JsLocation(fileName, lineNumber, 0); + if (astNode instanceof SourceInfoAwareJsNode) { + astNode.setSource(location); + } + else if (astNode instanceof JsExpressionStatement) { + ((JsExpressionStatement) astNode).getExpression().setSource(location); + } + } + return astNode; + } } diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt index aa28ee802ab..9066a42b5b8 100644 --- a/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt @@ -29,17 +29,19 @@ import java.util.* private val FAKE_SOURCE_INFO = SourceInfoImpl(null, 0, 0, 0, 0) -fun parse(code: String, reporter: ErrorReporter, scope: JsScope): List { +fun parse(code: String, reporter: ErrorReporter, scope: JsScope, fileName: String): List { val insideFunction = scope is JsFunctionScope val node = parse(code, 0, reporter, insideFunction, Parser::parse) - return node.toJsAst(scope, JsAstMapper::mapStatements) + return node.toJsAst(scope, fileName) { + mapStatements(it) + } } -fun parseFunction(code: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction = +fun parseFunction(code: String, fileName: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction = parse(code, offset, reporter, insideFunction = false) { addObserver(FunctionParsingObserver()) primaryExpr(it) - }.toJsAst(scope, JsAstMapper::mapFunction) + }.toJsAst(scope, fileName, JsAstMapper::mapFunction) private class FunctionParsingObserver : Observer { var functionsStarted = 0 @@ -80,8 +82,8 @@ private fun parse( } inline -private fun Node.toJsAst(scope: JsScope, mapAction: JsAstMapper.(Node)->T): T = - JsAstMapper(scope).mapAction(this) +private fun Node.toJsAst(scope: JsScope, fileName: String, mapAction: JsAstMapper.(Node)->T): T = + JsAstMapper(scope, fileName).mapAction(this) private fun StringReader(string: String, offset: Int): Reader { val reader = StringReader(string) diff --git a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt index 3cb0ff3a63e..c5719d78de7 100644 --- a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt +++ b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt @@ -564,9 +564,10 @@ class JsAstSerializer { var fileChanged = false if (location != null) { val lastFile = fileStack.peek() - fileChanged = lastFile != location.file + val newFile = location.file + fileChanged = lastFile != newFile && newFile != null if (fileChanged) { - fileConsumer(serialize(location.file)) + fileConsumer(serialize(newFile!!)) fileStack.push(location.file) } val locationBuilder = Location.newBuilder() diff --git a/js/js.tests/js.tests.iml b/js/js.tests/js.tests.iml index cc7aada55d3..30433c5086e 100644 --- a/js/js.tests/js.tests.iml +++ b/js/js.tests/js.tests.iml @@ -16,5 +16,6 @@ + \ No newline at end of file diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt index 74d352d03db..3d4294eb8ef 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.js.test import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.CharsetToolkit import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.psi.PsiManager @@ -35,6 +36,8 @@ import org.jetbrains.kotlin.js.backend.ast.JsProgram import org.jetbrains.kotlin.js.config.EcmaVersion import org.jetbrains.kotlin.js.config.JSConfigurationKeys import org.jetbrains.kotlin.js.config.JsConfig +import org.jetbrains.kotlin.js.dce.DeadCodeElimination +import org.jetbrains.kotlin.js.dce.InputFile import org.jetbrains.kotlin.js.facade.K2JSTranslator import org.jetbrains.kotlin.js.facade.MainCallParameters import org.jetbrains.kotlin.js.facade.TranslationResult @@ -53,6 +56,7 @@ import org.jetbrains.kotlin.test.KotlinTestUtils.TestFileFactory import org.jetbrains.kotlin.test.KotlinTestWithEnvironment import org.jetbrains.kotlin.test.TargetBackend import org.jetbrains.kotlin.utils.DFS +import org.mozilla.javascript.Context import java.io.ByteArrayOutputStream import java.io.Closeable import java.io.File @@ -102,8 +106,9 @@ abstract class BasicBoxTest( generateJavaScriptFile(file.parent, module, outputFileName, dependencies, friends, modules.size > 1, outputPrefixFile, outputPostfixFile, mainCallParameters) - if (!module.name.endsWith(OLD_MODULE_SUFFIX)) outputFileName else null + if (!module.name.endsWith(OLD_MODULE_SUFFIX)) Pair(outputFileName, module) else null } + val mainModuleName = if (TEST_MODULE in modules) TEST_MODULE else DEFAULT_MODULE val mainModule = modules[mainModuleName]!! @@ -140,7 +145,7 @@ abstract class BasicBoxTest( additionalFiles += additionalJsFile } - val allJsFiles = additionalFiles + inputJsFiles + generatedJsFiles + globalCommonFiles + localCommonFiles + + val allJsFiles = additionalFiles + inputJsFiles + generatedJsFiles.map { it.first } + globalCommonFiles + localCommonFiles + additionalCommonFiles if (generateNodeJsRunner && !SKIP_NODE_JS.matcher(fileContent).find()) { @@ -152,7 +157,10 @@ abstract class BasicBoxTest( runGeneratedCode(allJsFiles, mainModuleName, testFactory.testPackage, TEST_FUNCTION, expectedResult, withModuleSystem) - performAdditionalChecks(generatedJsFiles, outputPrefixFile, outputPostfixFile) + performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile) + + minifyAndRun(File(File(outputDir, "min"), file.nameWithoutExtension), allJsFiles, generatedJsFiles, expectedResult, + mainModuleName, testFactory.testPackage, TEST_FUNCTION, withModuleSystem) } } @@ -397,6 +405,59 @@ abstract class BasicBoxTest( return JsConfig(project, configuration) } + private fun minifyAndRun( + workDir: File, allJsFiles: List, generatedJsFiles: List>, + expectedResult: String, testModuleName: String, testPackage: String?, testFunction: String, withModuleSystem: Boolean + ) { + val kotlinJsLib = DIST_DIR_JS_PATH + "kotlin.js" + val kotlinTestJsLib = DIST_DIR_JS_PATH + "kotlin-test.js" + val kotlinJsLibOutput = File(workDir, "kotlin.min.js").path + val kotlinTestJsLibOutput = File(workDir, "kotlin-test.min.js").path + + val kotlinJsInputFile = InputFile(kotlinJsLib, kotlinJsLibOutput, "kotlin") + val kotlinTestJsInputFile = InputFile(kotlinTestJsLib, kotlinTestJsLibOutput, "kotlin-test") + + val filesToMinify = generatedJsFiles.associate { (fileName, module) -> + val inputFileName = File(fileName).nameWithoutExtension + fileName to InputFile(fileName, File(workDir, inputFileName + ".min.js").absolutePath, module.name) + } + + val testFunctionFqn = testModuleName + (if (testPackage.isNullOrEmpty()) "" else ".$testPackage") + ".$testFunction" + val additionalReachableNodes = setOf( + testFunctionFqn, "kotlin.kotlin.io.BufferedOutput", "kotlin.kotlin.io.output.flush", + "kotlin.kotlin.io.output.buffer" + ) + val allFilesToMinify = filesToMinify.values + kotlinJsInputFile + kotlinTestJsInputFile + val reachableNodes = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { } + if (reachableNodes.size > 1500) println("!!!") + println(reachableNodes.size.toString() + ": " + workDir.path) + + val runList = mutableListOf() + runList += kotlinJsLibOutput + runList += kotlinTestJsLibOutput + + val context = Context.enter() + context.languageVersion = Context.VERSION_1_8 + context.optimizationLevel = -1 + val scope = context.initStandardObjects() + + fun applyFile(fileToRun: String) { + val code = FileUtil.loadFile(File(fileToRun), CharsetToolkit.UTF8, true) + context.evaluateString(scope, code, fileToRun, 1, null) + } + + applyFile(DIST_DIR_JS_PATH + RhinoUtils.RHINO_POLYFILLS_RELATIVE_PATH) + applyFile(kotlinJsLibOutput) + applyFile(kotlinTestJsLibOutput) + context.evaluateString(scope, "function ok() {}", "setup assertions", 0, null) + context.evaluateString(scope, RhinoUtils.SETUP_KOTLIN_OUTPUT, "setup kotlin output", 0, null) + + allJsFiles.map { filesToMinify[it]?.outputName ?: it }.forEach(::applyFile) + + val checker = RhinoFunctionResultChecker(testModuleName, testPackage, testFunction, expectedResult, withModuleSystem) + checker.runChecks(context, scope) + } + private inner class TestFileFactoryImpl : TestFileFactory, Closeable { var testPackage: String? = null val tmpDir = KotlinTestUtils.tmpDir("js-tests") diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt index 91c3ea54985..294e6208190 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ast/NameResolutionTest.kt @@ -57,8 +57,8 @@ class NameResolutionTest { val expectedCode = FileUtil.loadFile(File(expectedName)) val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "") - val originalAst = JsGlobalBlock().apply { statements += parse(originalCode, errorReporter, parserScope) } - val expectedAst = JsGlobalBlock().apply { statements += parse(expectedCode, errorReporter, parserScope) } + val originalAst = JsGlobalBlock().apply { statements += parse(originalCode, errorReporter, parserScope, originalName) } + val expectedAst = JsGlobalBlock().apply { statements += parse(expectedCode, errorReporter, parserScope, expectedName) } originalAst.accept(object : RecursiveJsVisitor() { val cache = mutableMapOf() 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 728dd928cd0..3dd1bddf964 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 @@ -55,7 +55,7 @@ abstract class BasicOptimizerTest(private var basePath: String) { private fun checkOptimizer(unoptimizedCode: String, optimizedCode: String) { val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "") - val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope) + val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope, "") updateMetadata(unoptimizedCode, unoptimizedAst) @@ -63,7 +63,7 @@ abstract class BasicOptimizerTest(private var basePath: String) { process(statement) } - val optimizedAst = parse(optimizedCode, errorReporter, parserScope) + val optimizedAst = parse(optimizedCode, errorReporter, parserScope, "") Assert.assertEquals(astToString(optimizedAst), astToString(unoptimizedAst)) } diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java index 23ace4b8742..c7219edeadf 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java @@ -175,6 +175,6 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl assert currentScope instanceof JsFunctionScope : "Usage of js outside of function is unexpected"; JsScope temporaryRootScope = new JsRootScope(new JsProgram()); JsScope scope = new DelegatingJsFunctionScopeWithTemporaryParent((JsFunctionScope) currentScope, temporaryRootScope); - return ParserUtilsKt.parse(jsCode, ThrowExceptionOnErrorReporter.INSTANCE, scope); + return ParserUtilsKt.parse(jsCode, ThrowExceptionOnErrorReporter.INSTANCE, scope, jsCodeExpression.getContainingKtFile().getName()); } }