com.google.javascript.jscomp.LiveVariablesAnalysis Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* 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 extends Var> 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 extends DiGraphEdge> 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()));
}
}
}
}