
com.google.javascript.jscomp.OptimizeParameters Maven / Gradle / Ivy
/*
* Copyright 2009 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.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.javascript.jscomp.AbstractCompiler.LifeCycleStage;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.OptimizeCalls.ReferenceMap;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.Map.Entry;
import javax.annotation.Nullable;
/**
* Optimize function calls and function signatures.
*
*
* - Removes optional parameters if no caller specifies it as argument.
* - Removes arguments at call site to function that ignores the parameter.
* - Inline a parameter if the function is always called with that constant.
*
*
* @author [email protected] (John Lenz)
*/
class OptimizeParameters implements CompilerPass, OptimizeCalls.CallGraphCompilerPass {
private final AbstractCompiler compiler;
private Scope globalScope;
OptimizeParameters(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
@VisibleForTesting
public void process(Node externs, Node root) {
checkState(compiler.getLifeCycleStage() == LifeCycleStage.NORMALIZED);
ReferenceMap refMap = OptimizeCalls.buildPropAndGlobalNameReferenceMap(
compiler, externs, root);
process(externs, root, refMap);
}
@Override
public void process(Node externs, Node root, ReferenceMap refMap) {
this.globalScope = refMap.getGlobalScope();
// Find all function nodes whose callers ignore the return values.
List> toOptimize = new ArrayList<>();
for (Entry> entry : refMap.getNameReferences()) {
String key = entry.getKey();
ArrayList refs = entry.getValue();
if (isCandidate(key, refs)) {
toOptimize.add(refs);
}
}
for (Entry> entry : refMap.getPropReferences()) {
String key = entry.getKey();
ArrayList refs = entry.getValue();
if (isCandidate(key, refs)) {
toOptimize.add(refs);
}
}
// NOTE: The optimization that are perform must be careful to keep the
// ReferenceMap in a consistent state. They should be careful to not
// remove or add global references without update the reference map.
// While adding references to the map is O(1), removing references
// O(n) where (n) is the number of references.
//
// So the most transformative pass should be last. So:
//
// - Removing parameters not provided by any call-site only
// moves the name and default values from the parameter list to the
// functions' bodies, so no updates are needed if the same node
// are reused.
//
// - Moving parameters that are provided the same value by every
// call-site to the function bodies, is currently limited to cases
// where there are only one function definition, so no updates
// are needed if the same nodes are reused.
//
// - Removing parameters that are unreferenced from call-sites, may
// remove references, as it is run last.
for (ArrayList refs : toOptimize) {
tryEliminateOptionalArgs(refs);
}
for (ArrayList refs : toOptimize) {
tryEliminateConstantArgs(refs);
}
// tryEliminateUnusedArgs may mutate
UnusedParameterOptimizer optimizer = new UnusedParameterOptimizer();
for (ArrayList refs : toOptimize) {
optimizer.tryEliminateUnusedArgs(refs);
}
optimizer.applyChanges();
}
class UnusedParameterOptimizer {
final List toRemove = new ArrayList<>();
final List toReplaceWithZero = new ArrayList<>();
/**
* Attempt to eliminate unused parameters by removing them from both the call sites
* and the function definitions.
*
* An unused first parameter:
* function foo(a, b) {use(b);}
* foo(1,2);
* foo(1,3)
* becomes
* function foo(b) {use(b);}
* foo(2);
* foo(3);
*
* @param refs A list of references to the symbol (name or property) as vetted by
* #isCandidate.
*/
void tryEliminateUnusedArgs(ArrayList refs) {
// An argument is unused if it is than the number of declare parameters
// or if it marked as unused.
List fns = ReferenceMap.getFunctionNodes(refs);
Preconditions.checkState(!fns.isEmpty());
int maxFormals = 0;
int lowestUsedRest = Integer.MAX_VALUE;
BitSet used = new BitSet();
for (Node fn : fns) {
Node paramList = NodeUtil.getFunctionParameters(fn);
int index = -1;
for (Node c = paramList.getFirstChild(); c != null; c = c.getNext()) {
index++;
if (!c.isUnusedParameter()) {
used.set(index);
if (c.isRest()) {
lowestUsedRest = Math.min(lowestUsedRest, index);
if (lowestUsedRest == 0) {
// don't bother doing anything more, all the parameters are used.
return;
}
}
}
}
maxFormals = Math.max(maxFormals, index + 1);
}
BitSet unused = new BitSet();
if (maxFormals > 0) {
unused = ((BitSet) used.clone());
unused.flip(0, maxFormals);
// There was a use "rest" declaration
if (lowestUsedRest < maxFormals) {
// everything above lowestUsedRest is used
unused.clear(lowestUsedRest, maxFormals - 1);
if (unused.cardinality() == 0) {
// Nothing can possibly be removed
return;
}
}
}
int lowestUnused = Integer.MAX_VALUE;
for (int i = 0; i < maxFormals; i++) {
if (unused.get(i)) {
lowestUnused = i;
break;
}
}
// NOTE: RemoveUnusedVars removes any trailing unused formals, so we don't need to
// do for that case.
BitSet unremovableAtAnyCall = ((BitSet) used.clone());
// A parameter is removable from a call-site if the parameter value
// has no side-effects. If all call-sites can be updated, the
// parameter can be removed from the function definitions.
// Regardless, the side-effect free values can still be replaced
// with a simpler expression.
// 3 values determine the range of removable parameters:
// the number of formal parameters, the unused parameters bitset,
// and the position of a used rest parameter (if any).
// - If the parameter index is higher >= the "rest", it
// is not a candidate, otherwise if it is in bitset or
// the greater than the declared formals.
// To be removed, the candidate must be side-effect free.
// If not all candidates can be removed, the removable
// candidates are still removed if there are no following parameters.
// If there are following parameters the removable candidates are
// replaced with a literal zero instead.
// Build a list of parameters to remove
int lowestSpread = Integer.MAX_VALUE;
for (Node n : refs) {
if (ReferenceMap.isCallOrNewTarget(n)) {
Node param = ReferenceMap.getFirstArgumentForCallOrNewOrDotCall(n);
int paramIndex = 0;
while (param != null) {
if (param.isSpread()) {
lowestSpread = Math.min(lowestSpread, paramIndex);
unremovableAtAnyCall.set(paramIndex);
if (lowestSpread < lowestUnused) {
break;
}
}
if (unused.get(paramIndex) && NodeUtil.mayHaveSideEffects(param, compiler)) {
unremovableAtAnyCall.set(paramIndex);
}
param = param.getNext();
paramIndex++;
}
}
}
// TODO: if spread < maxformal, set unremovable from spread...maxformal
for (Node n : refs) {
if (ReferenceMap.isCallOrNewTarget(n) && !alreadyRemoved(n)) {
Node arg = ReferenceMap.getFirstArgumentForCallOrNewOrDotCall(n);
recordRemovalCallArguments(
lowestUsedRest, maxFormals, unused, unremovableAtAnyCall, arg, 0);
}
}
for (Node fn : fns) {
Node paramList = NodeUtil.getFunctionParameters(fn);
Node param = paramList.getFirstChild();
removeUnusedFunctionPameters(lowestUsedRest, unused, unremovableAtAnyCall, param, 0);
}
}
// max (rest is used) is or removeAllAfter (last formal) is but not both.
void recordRemovalCallArguments(
int max, int removeAllAfter, BitSet unused, BitSet unremovable, Node arg, int index) {
if (arg != null && index < max) {
if (arg.isSpread() && index < removeAllAfter) {
// Unless we can remove everything, we can't remove anything.
return;
}
recordRemovalCallArguments(
max, removeAllAfter, unused, unremovable, arg.getNext(), index + 1);
if (index >= removeAllAfter || unused.get(index)) {
if (!NodeUtil.mayHaveSideEffects(arg, compiler)) {
if (unremovable.get(index)) {
toReplaceWithZero.add(arg);
} else {
toRemove.add(arg);
}
}
}
}
}
void removeUnusedFunctionPameters(
int max, BitSet unused, BitSet unremovable, Node param, int index) {
if (param != null && index < max) {
removeUnusedFunctionPameters(max, unused, unremovable, param.getNext(), index + 1);
if (unused.get(index) && !unremovable.get(index)) {
checkState(param.isName()); // update for ES6
// params are not otherwise referenceable.
compiler.reportChangeToEnclosingScope(param);
param.detach();
}
}
}
/**
* Applies optimizations to all previously marked nodes.
*/
public void applyChanges() {
for (Node n : toRemove) {
// Don't remove any nodes twice since doing so would violate change reporting constraints.
if (alreadyRemoved(n)) {
continue;
}
compiler.reportChangeToEnclosingScope(n);
n.detach();
NodeUtil.markFunctionsDeleted(n, compiler);
}
for (Node n : toReplaceWithZero) {
// Don't remove any nodes twice since doing so would violate change reporting constraints.
if (alreadyRemoved(n)) {
continue;
}
compiler.reportChangeToEnclosingScope(n);
n.replaceWith(IR.number(0).srcref(n));
NodeUtil.markFunctionsDeleted(n, compiler);
}
}
}
private static boolean alreadyRemoved(Node n) {
Node parent = n.getParent();
if (parent == null) {
return true;
}
if (parent.isRoot()) {
return false;
}
return alreadyRemoved(parent);
}
/**
* This reference set is a candidate for parameter-moving if:
* - if all call sites are known (no aliasing)
* - if all definition sites are known (the possible values are known functions)
* - there is at least one definition
*/
private boolean isCandidate(String name, ArrayList refs) {
if (!OptimizeCalls.mayBeOptimizableName(compiler, name)) {
return false;
}
boolean seenCandidateDefiniton = false;
boolean seenCandidateUse = false;
for (Node n : refs) {
// TODO(johnlenz): Determine what to do about ".constructor" references.
// Currently classes that are super classes or have superclasses aren't optimized
//
// if (parent.isCall() && n != parent.getFirstChild() && isClassDefiningCall(parent)) {
// continue;
// } else
if (ReferenceMap.isCallOrNewTarget(n)) {
// TODO(johnlenz): filter .apply when we support it
seenCandidateUse = true;
} else if (isCandidateDefinition(n)) {
seenCandidateDefiniton = true;
} else {
// If this isn't an non-aliasing reference (typeof, instanceof, etc)
// then there is nothing that can be done.
if (!OptimizeCalls.isAllowedReference(n)) {
// TODO(johnlenz): allow extends clauses.
return false;
}
}
}
return seenCandidateDefiniton && seenCandidateUse;
}
/**
* Determines if a call defines a class inheritance or mixing
* relation, according to the current coding convention.
*/
private boolean isClassDefiningCall(Node callNode) {
SubclassRelationship classes =
compiler.getCodingConvention().getClassesDefinedByCall(callNode);
return classes != null;
}
private boolean isCandidateDefinition(Node n) {
Node parent = n.getParent();
if (parent.isFunction() && NodeUtil.isFunctionDeclaration(parent)) {
return allDefinitionsAreCandidateFunctions(parent);
} else if (ReferenceMap.isSimpleAssignmentTarget(n)) {
if (allDefinitionsAreCandidateFunctions(parent.getLastChild())) {
return true;
}
} else if (n.isName() && n.hasChildren()) {
if (allDefinitionsAreCandidateFunctions(n.getFirstChild())) {
return true;
}
}
return false;
}
private static boolean allDefinitionsAreCandidateFunctions(Node n) {
switch (n.getToken()) {
case FUNCTION:
// Named function expression can refer to themselves,
// "arguments" can refer to all parameters or their count,
// so they are not candidates.
return !NodeUtil.isNamedFunctionExpression(n)
&& !NodeUtil.doesFunctionReferenceOwnArgumentsObject(n);
case CAST:
case COMMA:
return allDefinitionsAreCandidateFunctions(n.getLastChild());
case HOOK:
return allDefinitionsAreCandidateFunctions(n.getSecondChild())
&& allDefinitionsAreCandidateFunctions(n.getLastChild());
case OR:
case AND:
return allDefinitionsAreCandidateFunctions(n.getFirstChild())
&& allDefinitionsAreCandidateFunctions(n.getLastChild());
default:
return false;
}
}
/**
* Removes any optional parameters if no callers specifies it as an argument.
*/
private void tryEliminateOptionalArgs(ArrayList refs) {
// Count the maximum number of arguments passed into this function all
// all points of the program.
int maxArgs = -1;
for (Node n : refs) {
if (ReferenceMap.isCallOrNewTarget(n)) {
int numArgs = 0;
Node firstArg = ReferenceMap.getFirstArgumentForCallOrNewOrDotCall(n);
for (Node c = firstArg; c != null; c = c.getNext()) {
numArgs++;
if (c.isSpread()) {
// Bail: with spread we must assume all parameters are used, don't waste
// any more time.
return;
}
}
if (numArgs > maxArgs) {
maxArgs = numArgs;
}
}
}
for (Node fn : ReferenceMap.getFunctionNodes(refs)) {
eliminateParamsAfter(fn, maxArgs);
}
}
/**
* Eliminate parameters if they are always constant.
*
* function foo(a, b) {...}
* foo(1,2);
* foo(1,3)
* becomes
* function foo(b) { var a = 1 ... }
* foo(2);
* foo(3);
*
* @param refs A list of references to the symbol (name or property) as vetted by
* #isCandidate.
*/
private void tryEliminateConstantArgs(ArrayList refs) {
List parameters = new ArrayList<>();
boolean firstCall = true;
// Build a list of parameters to remove
boolean continueLooking = false;
for (Node n : refs) {
if (ReferenceMap.isCallOrNewTarget(n)) {
Node call = n.getParent();
Node firstParam = call.getFirstChild();
// Look at the first param in the case of a ".call", if it is a spread, the order
// of the parameters is unknown.
if (firstParam.isSpread()) {
return; // stop looking
}
Node cur = ReferenceMap.getFirstArgumentForCallOrNewOrDotCall(n);
if (firstCall) {
// Use the first call to construct a list of parameter values of the
// function.
continueLooking = buildParameterList(parameters, cur);
firstCall = false;
} else {
// All the rest must match
continueLooking = findFixedParameters(parameters, cur);
}
if (!continueLooking) {
return;
}
}
}
continueLooking = adjustForSideEffects(parameters);
if (!continueLooking) {
return;
}
List fns = ReferenceMap.getFunctionNodes(refs);
if (fns.size() > 1) {
// TODO(johnlenz): support moving simple constants.
// This requires cloning the tree and avoiding adding additional calls/definitions that will
// invalidate the reference map
return;
}
// Found something to do, move the values from the call sites to the function definitions.
for (Node n : refs) {
if (ReferenceMap.isCallOrNewTarget(n) && !alreadyRemoved(n)) {
optimizeCallSite(parameters, n);
}
}
for (Node fn : fns) {
optimizeFunctionDefinition(parameters, fn);
}
}
/**
* Adjust the parameters to move based on the side-effects seen.
* @return Whether there are any movable parameters.
*/
private static boolean adjustForSideEffects(List parameters) {
// A parameter with side-effects can move if there are no following parameters
// that can be effected.
// A parameter can be moved if it can't be side-effected (a literal),
// or there are no following side-effects, that aren't moved.
boolean anyMovable = false;
boolean seenUnmovableSideEffects = false;
boolean seenUnmoveableSideEffected = false;
for (int i = parameters.size() - 1; i >= 0; i--) {
Parameter current = parameters.get(i);
// Preserve side-effect ordering, don't move this parameter if:
// * the current parameter has side-effects and a following
// parameters that will not be move can be effected.
// * the current parameter can be effected and a following
// parameter that will not be moved has side-effects
if (current.shouldRemove
&& ((seenUnmovableSideEffects && current.canBeSideEffected())
|| (seenUnmoveableSideEffected && current.hasSideEffects()))) {
current.shouldRemove = false;
}
if (current.shouldRemove) {
anyMovable = true;
} else {
if (current.canBeSideEffected) {
seenUnmoveableSideEffected = true;
}
if (current.hasSideEffects) {
seenUnmovableSideEffects = true;
}
}
}
return anyMovable;
}
/**
* Determine which parameters use the same expression.
* @return Whether any parameter was found that can be updated.
*/
private boolean findFixedParameters(List parameters, Node cur) {
boolean anyMovable = false;
int index = 0;
while (cur != null) {
Parameter p;
if (index >= parameters.size()) {
p = new Parameter(cur, false);
parameters.add(p);
} else {
p = parameters.get(index);
if (p.shouldRemove()) {
Node value = p.getArg();
if (!cur.isEquivalentTo(value)) {
p.setShouldRemove(false);
} else {
anyMovable = true;
}
}
}
setParameterSideEffectInfo(p, cur);
cur = cur.getNext();
index++;
}
for (; index < parameters.size(); index++) {
parameters.get(index).setShouldRemove(false);
}
return anyMovable;
}
/**
* @return Whether any parameter was movable.
*/
private boolean buildParameterList(List parameters, Node cur) {
boolean anyMovable = false;
while (cur != null) {
if (cur.isSpread()) {
break;
}
boolean movable = isMovableValue(cur, globalScope);
Parameter p = new Parameter(cur, movable);
setParameterSideEffectInfo(p, cur);
parameters.add(p);
if (movable) {
anyMovable = true;
}
cur = cur.getNext();
}
return anyMovable;
}
private void setParameterSideEffectInfo(Parameter p, Node value) {
if (!p.hasSideEffects()) {
p.setHasSideEffects(NodeUtil.mayHaveSideEffects(value, compiler));
}
if (!p.canBeSideEffected()) {
p.setCanBeSideEffected(NodeUtil.canBeSideEffected(value));
}
}
/**
* @return Whether the expression can be safely moved to another function
* in another scope.
*/
private static boolean isMovableValue(Node n, Scope globalScope) {
// Things that can change value or are inaccessible can't be moved, these
// are "this", "arguments", local names, and functions that capture local
// values.
switch (n.getToken()) {
case THIS:
return false;
case FUNCTION:
// Don't move function closures.
// TODO(johnlenz): Closure that only contain global reference can be
// moved.
return false;
case NAME:
if (n.getString().equals("arguments")) {
return false;
} else {
Var v = globalScope.getVar(n.getString());
if (v == null) {
return false;
}
}
break;
default:
break;
}
for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
if (!isMovableValue(c, globalScope)) {
return false;
}
}
return true;
}
private void optimizeFunctionDefinition(List parameters, Node function) {
for (int index = parameters.size() - 1; index >= 0; index--) {
if (parameters.get(index).shouldRemove()) {
Node paramName = eliminateFunctionParamAt(function, index);
addVariableToFunction(function, paramName, parameters.get(index).getArg());
}
}
}
private void optimizeCallSite(List parameters, Node target) {
Node call = ReferenceMap.getCallOrNewNodeForTarget(target);
boolean mayMutateArgs = call.mayMutateArguments();
boolean mayMutateGlobalsOrThrow = call.mayMutateGlobalStateOrThrow();
for (int index = parameters.size() - 1; index >= 0; index--) {
Parameter p = parameters.get(index);
if (p.shouldRemove()) {
eliminateCallTargetArgAt(target, index);
if (mayMutateArgs && !mayMutateGlobalsOrThrow
// We want to cover both global-state arguments, and
// expressions that might throw exceptions.
// We're deliberately conservative here b/c it's
// difficult to test all the edge cases.
&& !NodeUtil.isImmutableValue(p.getArg())) {
mayMutateGlobalsOrThrow = true;
call.setSideEffectFlags(
new Node.SideEffectFlags(call.getSideEffectFlags()).setMutatesGlobalState());
}
}
}
}
/**
* Simple container class that keeps tracks of a parameter and whether it
* should be removed.
*/
private static class Parameter {
private final Node arg;
private boolean shouldRemove;
private boolean hasSideEffects;
private boolean canBeSideEffected;
public Parameter(Node arg, boolean shouldRemove) {
this.shouldRemove = shouldRemove;
this.arg = arg;
}
public Node getArg() {
return arg;
}
public boolean shouldRemove() {
return shouldRemove;
}
public void setShouldRemove(boolean value) {
shouldRemove = value;
}
public void setHasSideEffects(boolean hasSideEffects) {
this.hasSideEffects = hasSideEffects;
}
public boolean hasSideEffects() {
return hasSideEffects;
}
public void setCanBeSideEffected(boolean canBeSideEffected) {
this.canBeSideEffected = canBeSideEffected;
}
public boolean canBeSideEffected() {
return canBeSideEffected;
}
}
/**
* Adds a variable to the top of a function block.
* @param function A function node.
* @param varName The name of the variable.
* @param value The initial value of the variable.
*/
private void addVariableToFunction(Node function, @Nullable Node varName, Node value) {
Preconditions.checkArgument(function.isFunction(), "Expected function, got: %s", function);
Node block = NodeUtil.getFunctionBody(function);
checkState(value.getParent() == null);
Node stmt;
if (varName != null) {
stmt = NodeUtil.newVarNode(varName.getString(), value);
} else {
stmt = IR.exprResult(value).useSourceInfoFrom(value);
}
block.addChildToFront(stmt);
compiler.reportChangeToEnclosingScope(stmt);
}
/**
* Removes all formal parameters starting at argIndex.
*/
private void eliminateParamsAfter(Node fnNode, int argIndex) {
Node formalArgPtr = NodeUtil.getFunctionParameters(fnNode).getFirstChild();
while (argIndex != 0 && formalArgPtr != null) {
formalArgPtr = formalArgPtr.getNext();
argIndex--;
}
eliminateParamsAfter(fnNode, formalArgPtr);
}
private void eliminateParamsAfter(Node fnNode, Node argNode) {
if (argNode != null) {
// Keep the args in the same order, do the last first.
eliminateParamsAfter(fnNode, argNode.getNext());
argNode.detach();
Node var = IR.var(argNode).useSourceInfoIfMissingFrom(argNode);
fnNode.getLastChild().addChildToFront(var);
compiler.reportChangeToEnclosingScope(var);
}
}
/**
* Eliminates the parameter from a function definition.
*
* @param function The function node
* @param argIndex The index of the argument to remove.
* @param definitionFinder The definition and use sites index.
* @return The Node of the argument removed.
*/
@Nullable
private Node eliminateFunctionParamAt(Node function, int argIndex) {
checkArgument(function.isFunction(), "Node must be a function.");
Node formalParamNode = NodeUtil.getArgumentForFunction(function, argIndex);
if (formalParamNode != null) {
NodeUtil.deleteNode(formalParamNode, compiler);
}
return formalParamNode;
}
/**
* Eliminates the parameter from a function call.
* @param definitionFinder The definition and use sites index.
* @param p
* @param call The function call node
* @param argIndex The index of the argument to remove.
*/
private void eliminateCallTargetArgAt(Node ref, int argIndex) {
Node callArgNode = ReferenceMap.getArgumentForCallOrNewOrDotCall(ref, argIndex);
if (callArgNode != null) {
NodeUtil.deleteNode(callArgNode, compiler);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy