Prototyping DCE tool for JS

This commit is contained in:
Alexey Andreev
2017-03-31 20:03:41 +03:00
parent d617b1d869
commit 9e89213d66
26 changed files with 1445 additions and 22 deletions
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="PROVIDED" name="intellij-core" level="project" />
<orderEntry type="module" module-name="js.ast" />
<orderEntry type="module" module-name="js.inliner" />
<orderEntry type="module" module-name="js.translator" />
<orderEntry type="module" module-name="util" />
</component>
</module>
@@ -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<JsNode, Node>
val astNodesToEliminate: Set<JsNode>
val astNodesToSkip: Set<JsNode>
val functionsToEnter: Set<JsFunction>
val invocationsToSkip: Set<JsInvocation>
}
@@ -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<JsFunction>()
private val postponedFunctions = mutableMapOf<JsName, JsFunction>()
private val nodeMap = mutableMapOf<JsNode, Node>()
private val astNodesToEliminate = mutableSetOf<JsNode>()
private val astNodesToSkip = mutableSetOf<JsNode>()
private val invocationsToSkip = mutableSetOf<JsInvocation>()
val moduleMapping = mutableMapOf<JsStatement, String>()
private val functionsToEnter = mutableSetOf<JsFunction>()
val analysisResult = object : AnalysisResult {
override val nodeMap: Map<JsNode, Node> get() = this@Analyzer.nodeMap
override val astNodesToEliminate: Set<JsNode> get() = this@Analyzer.astNodesToEliminate
override val astNodesToSkip: Set<JsNode> get() = this@Analyzer.astNodesToSkip
override val functionsToEnter: Set<JsFunction> get() = this@Analyzer.functionsToEnter
override val invocationsToSkip: Set<JsInvocation> 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<JsExpression>) {
// 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<JsExpression>) {
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<Node>) {
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
}
}
@@ -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<JsName, Node>()
var thisNode: Node? = globalScope
val namesOfLocalVars = mutableSetOf<JsName>()
fun addNodesForLocalVars(names: Collection<JsName>) {
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<Node>()
private val expressionsImpl = mutableSetOf<JsExpression>()
private val functionsImpl = mutableSetOf<JsFunction>()
private var hasSideEffectsImpl = false
private var reachableImpl = false
private var declarationReachableImpl = false
private val membersImpl = mutableMapOf<String, Node>()
private val usedByAstNodesImpl = mutableSetOf<JsNode>()
private var rank = 0
val dependencies: MutableSet<Node> get() = original.dependenciesImpl
val expressions: MutableSet<JsExpression> get() = original.expressionsImpl
val functions: MutableSet<JsFunction> 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<JsNode> get() = original.usedByAstNodesImpl
val memberNames: MutableSet<String> 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<String, Node> 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<String> =
generateSequence(original) { it.qualifier?.parent?.original }.mapNotNull { it.qualifier?.memberName }
.toList().asReversed()
override fun toString(): String = (root().localName?.ident ?: "<unknown>") + pathFromRoot().joinToString("") { ".$it" }
}
class Qualifier(val parent: Node, val memberName: String)
}
@@ -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<JsBlock, String>()
private val reachableNames = mutableSetOf<String>()
var reachableNodes = setOf<Node>()
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<InputFile>,
rootReachableNames: Set<String>,
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")
}
}
}
}
@@ -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<Context.Node>)
@@ -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
}
@@ -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)
@@ -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<Node>()
val reachableNodes: Set<Node> 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<String>()
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--
}
}
@@ -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)
@@ -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<Node>.extractRoots(): Set<Node> {
val result = mutableSetOf<Node>()
val visited = mutableSetOf<Node>()
forEach { it.original.extractRootsImpl(result, visited) }
return result
}
private fun Node.extractRootsImpl(target: MutableSet<Node>, visited: MutableSet<Node>) {
if (!visited.add(original)) return
val qualifier = original.qualifier
if (qualifier == null) {
target += original
}
else {
qualifier.parent.extractRootsImpl(target, visited)
}
}