All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.CoalesceVariableNames Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * 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 com.google.javascript.jscomp;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.ControlFlowGraph.AbstractCfgNodeTraversalCallback;
import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.DataFlowAnalysis.FlowState;
import com.google.javascript.jscomp.LiveVariablesAnalysis.LiveVariableLattice;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.jscomp.Scope.Var;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.jscomp.graph.GraphColoring;
import com.google.javascript.jscomp.graph.GraphColoring.GreedyGraphColoring;
import com.google.javascript.jscomp.graph.GraphNode;
import com.google.javascript.jscomp.graph.LinkedUndirectedGraph;
import com.google.javascript.jscomp.graph.UndiGraph;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;

import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.Set;

/**
 * Reuse variable names if possible.
 *
 * 

For example, from var x = 1; print(x); var y = 2; print(y); * to var x = 1; print(x); x = 2; print(x). The benefits are * slightly shorter code because of the removed var declaration, * less unique variables in hope for better renaming, and finally better gzip * compression. * *

The pass operates similar to a typical register allocator found in an * optimizing compiler by first computing live ranges with * {@link LiveVariablesAnalysis} and a variable interference graph. Then it uses * graph coloring in {@link GraphColoring} to determine which two variables can * be merge together safely. * */ class CoalesceVariableNames extends AbstractPostOrderCallback implements CompilerPass, ScopedCallback { private final AbstractCompiler compiler; private final Deque> colorings; private final boolean usePseudoNames; private static final Comparator coloringTieBreaker = new Comparator() { @Override public int compare(Var v1, Var v2) { return v1.index - v2.index; } }; /** * @param usePseudoNames For debug purposes, when merging variable foo and bar * to foo, rename both variable to foo_bar. */ CoalesceVariableNames(AbstractCompiler compiler, boolean usePseudoNames) { Preconditions.checkState(!compiler.getLifeCycleStage().isNormalized()); this.compiler = compiler; colorings = Lists.newLinkedList(); this.usePseudoNames = usePseudoNames; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); } private static boolean shouldOptimizeScope(Scope scope) { // TODO(user): We CAN do this in the global scope, just need to be // careful when something is exported. Liveness uses bit-vector for live // sets so I don't see compilation time will be a problem for running this // pass in the global scope. if (scope.isGlobal()) { return false; } if (LiveVariablesAnalysis.MAX_VARIABLES_TO_ANALYZE < scope.getVarCount()) { return false; } return true; } @Override public void enterScope(NodeTraversal t) { Scope scope = t.getScope(); if (!shouldOptimizeScope(scope)) { return; } ControlFlowGraph cfg = t.getControlFlowGraph(); LiveVariablesAnalysis liveness = new LiveVariablesAnalysis(cfg, scope, compiler); // If the function has exactly 2 params, mark them as escaped. This is // a work-around for an IE bug where it throws an exception if you // write to the parameters of the callback in a sort(). See: // http://code.google.com/p/closure-compiler/issues/detail?id=58 if (scope.getRootNode().getFirstChild().getNext().getChildCount() == 2) { liveness.markAllParametersEscaped(); } liveness.analyze(); UndiGraph interferenceGraph = computeVariableNamesInterferenceGraph( t, cfg, liveness.getEscapedLocals()); GraphColoring coloring = new GreedyGraphColoring<>(interferenceGraph, coloringTieBreaker); coloring.color(); colorings.push(coloring); } @Override public void exitScope(NodeTraversal t) { if (!shouldOptimizeScope(t.getScope())) { return; } colorings.pop(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (colorings.isEmpty() || !n.isName() || parent.isFunction()) { // Don't rename named functions. return; } Var var = t.getScope().getVar(n.getString()); GraphNode vNode = colorings.peek().getGraph().getNode(var); if (vNode == null) { // This is not a local. return; } Var coalescedVar = colorings.peek().getPartitionSuperNode(var); if (!usePseudoNames) { if (vNode.getValue().equals(coalescedVar)) { // The coalesced name is itself, nothing to do. return; } // Rename. n.setString(coalescedVar.name); compiler.reportCodeChange(); if (parent.isVar()) { removeVarDeclaration(n); } } else { // This code block is slow but since usePseudoName is for debugging, // we should not sacrifice performance for non-debugging compilation to // make this fast. String pseudoName = null; Set allMergedNames = Sets.newTreeSet(); for (Iterator i = t.getScope().getVars(); i.hasNext();) { Var iVar = i.next(); // Look for all the variables that can be merged (in the graph by now) // and it is merged with the current coalescedVar. if (colorings.peek().getGraph().getNode(iVar) != null && coalescedVar.equals(colorings.peek().getPartitionSuperNode(iVar))) { allMergedNames.add(iVar.name); } } // Keep its original name. if (allMergedNames.size() == 1) { return; } pseudoName = Joiner.on("_").join(allMergedNames); while (t.getScope().isDeclared(pseudoName, true)) { pseudoName += "$"; } n.setString(pseudoName); compiler.reportCodeChange(); if (!vNode.getValue().equals(coalescedVar) && parent.isVar()) { removeVarDeclaration(n); } } } private UndiGraph computeVariableNamesInterferenceGraph( NodeTraversal t, ControlFlowGraph cfg, Set escaped) { UndiGraph interferenceGraph = LinkedUndirectedGraph.create(); Scope scope = t.getScope(); // First create a node for each non-escaped variable. for (Iterator i = scope.getVars(); i.hasNext();) { Var v = i.next(); if (!escaped.contains(v)) { // TODO(user): In theory, we CAN coalesce function names just like // any variables. Our Liveness analysis captures this just like it as // described in the specification. However, we saw some zipped and // and unzipped size increase after this. We are not totally sure why // that is but, for now, we will respect the dead functions and not play // around with it. if (!v.getParentNode().isFunction()) { interferenceGraph.createNode(v); } } } // Go through each variable and try to connect them. for (Iterator i1 = scope.getVars(); i1.hasNext();) { Var v1 = i1.next(); NEXT_VAR_PAIR: for (Iterator i2 = scope.getVars(); i2.hasNext();) { Var v2 = i2.next(); // Skip duplicate pairs. if (v1.index >= v2.index) { continue; } if (!interferenceGraph.hasNode(v1) || !interferenceGraph.hasNode(v2)) { // Skip nodes that were not added. They are globals and escaped // locals. Also avoid merging a variable with itself. continue NEXT_VAR_PAIR; } if (v1.getParentNode().isParamList() && v2.getParentNode().isParamList()) { interferenceGraph.connectIfNotFound(v1, null, v2); continue NEXT_VAR_PAIR; } // Go through every CFG node in the program and look at // this variable pair. If they are both live at the same // time, add an edge between them and continue to the next pair. NEXT_CROSS_CFG_NODE: for (DiGraphNode cfgNode : cfg.getDirectedGraphNodes()) { if (cfg.isImplicitReturn(cfgNode)) { continue NEXT_CROSS_CFG_NODE; } FlowState state = cfgNode.getAnnotation(); // Check the live states and add edge when possible. if ((state.getIn().isLive(v1) && state.getIn().isLive(v2)) || (state.getOut().isLive(v1) && state.getOut().isLive(v2))) { interferenceGraph.connectIfNotFound(v1, null, v2); continue NEXT_VAR_PAIR; } } // v1 and v2 might not have an edge between them! woohoo. there's // one last sanity check that we have to do: we have to check // if there's a collision *within* the cfg node. NEXT_INTRA_CFG_NODE: for (DiGraphNode cfgNode : cfg.getDirectedGraphNodes()) { if (cfg.isImplicitReturn(cfgNode)) { continue NEXT_INTRA_CFG_NODE; } FlowState state = cfgNode.getAnnotation(); boolean v1OutLive = state.getOut().isLive(v1); boolean v2OutLive = state.getOut().isLive(v2); CombinedLiveRangeChecker checker = new CombinedLiveRangeChecker( new LiveRangeChecker(v1, v2OutLive ? null : v2), new LiveRangeChecker(v2, v1OutLive ? null : v1)); NodeTraversal.traverse( compiler, cfgNode.getValue(), checker); if (checker.connectIfCrossed(interferenceGraph)) { continue NEXT_VAR_PAIR; } } } } return interferenceGraph; } /** * A simple wrapper calls to call two AbstractCfgNodeTraversalCallback * callback during the same traversal. Both traversals must have the same * "shouldTraverse" conditions. */ private static class CombinedLiveRangeChecker extends AbstractCfgNodeTraversalCallback { private final LiveRangeChecker callback1; private final LiveRangeChecker callback2; CombinedLiveRangeChecker( LiveRangeChecker callback1, LiveRangeChecker callback2) { this.callback1 = callback1; this.callback2 = callback2; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (LiveRangeChecker.shouldVisit(n)) { callback1.visit(t, n, parent); callback2.visit(t, n, parent); } } boolean connectIfCrossed(UndiGraph interferenceGraph) { if (callback1.crossed || callback2.crossed) { Var v1 = callback1.getDef(); Var v2 = callback2.getDef(); interferenceGraph.connectIfNotFound(v1, null, v2); return true; } return false; } } /** * Tries to remove variable declaration if the variable has been coalesced * with another variable that has already been declared. */ private static void removeVarDeclaration(Node name) { Node var = name.getParent(); Node parent = var.getParent(); // Special case when we are in FOR-IN loop. if (NodeUtil.isForIn(parent)) { var.removeChild(name); parent.replaceChild(var, name); } else if (var.hasOneChild()) { // The removal is easy when there is only one variable in the VAR node. if (name.hasChildren()) { Node value = name.removeFirstChild(); var.removeChild(name); Node assign = IR.assign(name, value).srcref(name); // We don't need to wrapped it with EXPR node if it is within a FOR. if (!parent.isFor()) { assign = NodeUtil.newExpr(assign); } parent.replaceChild(var, assign); } else { // In a FOR( ; ; ) node, we must replace it with an EMPTY or else it // becomes a FOR-IN node. NodeUtil.removeChild(parent, var); } } else { if (!name.hasChildren()) { var.removeChild(name); } // We are going to leave duplicated declaration otherwise. } } private static class LiveRangeChecker extends AbstractCfgNodeTraversalCallback { boolean defFound = false; boolean crossed = false; private final Var def; private final Var use; public LiveRangeChecker(Var def, Var use) { this.def = def; this.use = use; } Var getDef() { return def; } /** * @return Whether any LiveRangeChecker would be interested in the node. */ public static boolean shouldVisit(Node n) { return (n.isName() || (n.hasChildren() && n.getFirstChild().isName())); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!defFound && isAssignTo(def, n, parent)) { defFound = true; } if (defFound && (use == null || isReadFrom(use, n))) { crossed = true; } } private static boolean isAssignTo(Var var, Node n, Node parent) { if (n.isName() && var.getName().equals(n.getString()) && parent != null) { if (parent.isParamList()) { // In a function declaration, the formal parameters are assigned. return true; } else if (parent.isVar()) { // If this is a VAR declaration, if the name node has a child, we are // assigning to that name. return n.hasChildren(); } return false; // Definitely a read. } else { // Lastly, any assignmentOP is also an assign. Node name = n.getFirstChild(); return name != null && name.isName() && var.getName().equals(name.getString()) && NodeUtil.isAssignmentOp(n); } } private static boolean isReadFrom(Var var, Node name) { return name != null && name.isName() && var.getName().equals(name.getString()) && !NodeUtil.isVarOrSimpleAssignLhs(name, name.getParent()); } } }