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

com.google.javascript.jscomp.LiveVariablesAnalysis 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 2017 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 static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.NodeUtil.AllVarsDeclaredInFunction;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.LatticeElement;
import com.google.javascript.rhino.Node;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;

/**
 * Compute the "liveness" of all local variables. A variable is "live" at a point of a program if
 * the value it is currently holding might be read later. Otherwise, the variable is considered
 * "dead" if we know for sure that it will no longer be read. Dead variables are candidates for dead
 * assignment elimination and variable name sharing. The worst case safe assumption is to assume
 * that all variables are live. In that case, we will have no opportunity for optimizations. This is
 * especially the case within a TRY block when an assignment is not guaranteed to take place. We
 * bail out by assuming that all variables are live.
 *
 * 

Due to the possibility of inner functions and closures, certain "local" variables can escape * the function. These variables will be considered as global and they can be retrieved with {@link * #getEscapedLocals()}. */ class LiveVariablesAnalysis extends DataFlowAnalysis { static final int MAX_VARIABLES_TO_ANALYZE = 100; private final class LiveVariableJoinOp implements FlowJoiner { final LiveVariableLattice result = new LiveVariableLattice(orderedVars.size()); @Override public void joinFlow(LiveVariableLattice x) { this.result.liveSet.or(x.liveSet); } @Override public LiveVariableLattice finish() { return result; } } /** * The lattice that stores the liveness of all local variables at a given point in the program. * The whole lattice is the power set of all local variables and a variable is live if it is in * the set. */ static class LiveVariableLattice implements LatticeElement { private final BitSet liveSet; /** @param numVars Number of all local variables. */ private LiveVariableLattice(int numVars) { this.liveSet = new BitSet(numVars); } private LiveVariableLattice(LiveVariableLattice other) { checkNotNull(other); this.liveSet = (BitSet) other.liveSet.clone(); } @Override public boolean equals(Object other) { checkNotNull(other); return (other instanceof LiveVariableLattice) && this.liveSet.equals(((LiveVariableLattice) other).liveSet); } // There is only a version of this function with index since var.index will // return the wrong one. Use an instantiation of // LiveVariablesAnalysis and getVarIndex(var) to get the right index. public boolean isLive(int index) { return liveSet.get(index); } @Override public String toString() { return liveSet.toString(); } // Returns the index of the first bit that is set to true that occurs // on or after the specified starting index. public int nextSetBit(int fromIndex) { return liveSet.nextSetBit(fromIndex); } @Override public int hashCode() { return liveSet.hashCode(); } } // The scope of the function that we are analyzing. private final Scope jsScope; // The scope of the body of the function that we are analyzing. private final Scope jsScopeChild; private final Set escaped; // Maps the variable name to it's position // in this jsScope were we to combine the function and function body scopes. The Integer // represents the equivalent of the variable index property within a scope private final Map scopeVariables; // obtain variables in the order in which they appear in the code private final List orderedVars; private final Map allVarsInFn; /** * Live Variables Analysis using the ES6 scope creator. This analysis should only be done on * function where jsScope is the function scope. If we call LiveVariablesAnalysis from the * function scope of our pass, we can pass a null value for the JsScopeChild, but if we call it * from the function block scope, then JsScopeChild will be the function block scope. * *

We call from the function scope when the pass requires us to traverse nodes beginning at the * function parameters, and it from the function block scope when we are ignoring function * parameters. * * @param jsScope the function scope * @param jsScopeChild null or function block scope * @param scopeCreator Es6 Scope creator * @param allVarsDeclaredInFunction mapping of names to vars of everything reachable in a function */ LiveVariablesAnalysis( ControlFlowGraph cfg, Scope jsScope, @Nullable Scope jsScopeChild, AbstractCompiler compiler, ScopeCreator scopeCreator, AllVarsDeclaredInFunction allVarsDeclaredInFunction) { super(cfg); checkState(jsScope.isFunctionScope(), jsScope); this.jsScope = jsScope; this.jsScopeChild = jsScopeChild; this.escaped = new HashSet<>(); this.scopeVariables = new HashMap<>(); this.orderedVars = allVarsDeclaredInFunction.getAllVariablesInOrder(); this.allVarsInFn = allVarsDeclaredInFunction.getAllVariables(); computeEscaped(jsScope, escaped, compiler, scopeCreator, allVarsInFn); addScopeVariables(); } /** * Parameters belong to the function scope, but variables defined in the function body belong to * the function body scope. Assign a unique index to each variable, regardless of which scope it's * in. */ private void addScopeVariables() { int num = 0; for (Var v : orderedVars) { scopeVariables.put(v.getName(), num); num++; } } public Set getEscapedLocals() { return escaped; } public Map getAllVariables() { return allVarsInFn; } public List getAllVariablesInOrder() { return orderedVars; } public int getVarIndex(String var) { return scopeVariables.get(var); } @Override boolean isForward() { return false; } @Override LiveVariableLattice createEntryLattice() { return new LiveVariableLattice(orderedVars.size()); } @Override LiveVariableLattice createInitialEstimateLattice() { return new LiveVariableLattice(orderedVars.size()); } @Override FlowJoiner createFlowJoiner() { return new LiveVariableJoinOp(); } @Override LiveVariableLattice flowThrough(Node node, LiveVariableLattice input) { final BitSet gen = new BitSet(input.liveSet.size()); final BitSet kill = new BitSet(input.liveSet.size()); // Make kills conditional if the node can end abruptly by an exception. boolean conditional = false; List> edgeList = getCfg().getOutEdges(node); for (DiGraphEdge edge : edgeList) { if (Branch.ON_EX.equals(edge.getValue())) { conditional = true; } } computeGenKill(node, gen, kill, conditional); LiveVariableLattice result = new LiveVariableLattice(input); // L_in = L_out - Kill + Gen result.liveSet.andNot(kill); result.liveSet.or(gen); return result; } /** * Computes the GEN and KILL set. * * @param n Root node. * @param gen Local variables that are live because of the instruction at {@code n} will be added * to this set. * @param kill Local variables that are killed because of the instruction at {@code n} will be * added to this set. * @param conditional {@code true} if any assignments encountered are conditionally executed. * These assignments might not kill a variable. */ private void computeGenKill(Node n, BitSet gen, BitSet kill, boolean conditional) { switch (n.getToken()) { case SCRIPT: case ROOT: case FUNCTION: case BLOCK: return; case WHILE: case DO: case IF: case FOR: computeGenKill(NodeUtil.getConditionExpression(n), gen, kill, conditional); return; case FOR_OF: case FOR_AWAIT_OF: case FOR_IN: { // for (x in y) {...} Node lhs = n.getFirstChild(); if (NodeUtil.isNameDeclaration(lhs)) { // for (var x in y) {...} lhs = lhs.getLastChild(); } // Note that the LHS may never be assigned to or evaluated, like in: // for (x in []) {} // so should not be killed. computeGenKill(lhs, gen, kill, conditional); // rhs is executed only once so we don't go into it every loop. return; } case LET: case CONST: case VAR: for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { if (c.isName()) { if (c.hasChildren()) { computeGenKill(c.getFirstChild(), gen, kill, conditional); if (!conditional) { addToSetIfLocal(c, kill); } } } else { checkState(c.isDestructuringLhs(), c); if (!conditional) { NodeUtil.visitLhsNodesInNode(c, (lhsNode) -> addToSetIfLocal(lhsNode, kill)); } computeGenKill(c.getFirstChild(), gen, kill, conditional); computeGenKill(c.getSecondChild(), gen, kill, conditional); } } return; case AND: case OR: case COALESCE: case OPTCHAIN_GETELEM: case OPTCHAIN_GETPROP: computeGenKill(n.getFirstChild(), gen, kill, conditional); // May short circuit. computeGenKill(n.getLastChild(), gen, kill, true); return; case OPTCHAIN_CALL: computeGenKill(n.getFirstChild(), gen, kill, conditional); // Unlike OPTCHAIN_GETPROP and OPTCHAIN_GETELEM, the OPTCHAIN_CALLs can have multiple // children on rhs which get executed conditionally for (Node c = n.getSecondChild(); c != null; c = c.getNext()) { computeGenKill(c, gen, kill, true); } return; case HOOK: computeGenKill(n.getFirstChild(), gen, kill, conditional); // Assume both sides are conditional. computeGenKill(n.getSecondChild(), gen, kill, true); computeGenKill(n.getLastChild(), gen, kill, true); return; case NAME: if (n.getString().equals("arguments")) { markAllParametersEscaped(); } else if (!NodeUtil.isLhsByDestructuring(n)) { // Only add names in destructuring patterns if they're not lvalues. // e.g. "x" in "const {foo = x} = obj;" addToSetIfLocal(n, gen); } return; default: if (NodeUtil.isAssignmentOp(n) && n.getFirstChild().isName()) { Node lhs = n.getFirstChild(); if (!conditional) { addToSetIfLocal(lhs, kill); } if (!n.isAssign()) { // assignments such as a += 1 reads a. addToSetIfLocal(lhs, gen); } computeGenKill(lhs.getNext(), gen, kill, conditional); } else if (n.isAssign() && n.getFirstChild().isDestructuringPattern()) { if (!conditional) { NodeUtil.visitLhsNodesInNode( n, (child) -> { if (child.isName()) { addToSetIfLocal(child, kill); } }); } computeGenKill(n.getFirstChild(), gen, kill, conditional); computeGenKill(n.getSecondChild(), gen, kill, conditional); } else { for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { computeGenKill(c, gen, kill, conditional); } } return; } } private void addToSetIfLocal(Node node, BitSet set) { checkState(node.isName(), node); String name = node.getString(); Var var = allVarsInFn.get(name); if (var == null) { return; } boolean local; Scope localScope = var.getScope(); // add to the local set if the variable is declared in the function or function body because // ES6 separates the scope but if the variable is declared in the param it should be local // to the function body. if (localScope.isFunctionBlockScope()) { local = isDeclaredInFunctionBlockOrParameter(localScope, name); } else if (localScope == jsScope && jsScopeChild != null) { local = isDeclaredInFunctionBlockOrParameter(jsScopeChild, name); } else { local = localScope.hasOwnSlot(name); } if (!local) { return; } if (!escaped.contains(var)) { set.set(getVarIndex(var.getName())); } } private static boolean isDeclaredInFunctionBlockOrParameter(Scope scope, String name) { // In ES6, we create a separate container scope above the function block scope to handle // default parameters. Since nothing in the function block scope is allowed to shadow // the variables in the function scope, we treat the two scopes as one in this method. checkState(scope.isFunctionBlockScope()); return scope.hasOwnSlot(name) || scope.getParent().hasOwnSlot(name); } /** * Give up computing liveness of formal parameters by putting all the simple parameters in the * escaped set. * *

This only applies to simple parameters, that is NAMEs, because other parameter syntaxes * never need to be escaped in this way. The known applications of this method are for uses of * `arguments`, and for IE8. In a function with non-simple paremeters, `arguments` is not * parameter-mapped, and so referencing it doesn't escape paremeters. IE8 just doess't support * non-simple parameters. * *

We could actaully continue tracking simple parameters if any parameter is non-simple, but it * wasn't worth the complexity or cost to do so. * * @see https://tc39.github.io/ecma262/#sec-functiondeclarationinstantiation */ void markAllParametersEscaped() { Node paramList = NodeUtil.getFunctionParameters(jsScope.getRootNode()); for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) { if (param.isName()) { escaped.add(jsScope.getVar(param.getString())); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy