/* * Copyright 2008 Google Inc. * * 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.gwt.dev.js; import com.google.gwt.dev.jjs.HasSourceInfo; import com.google.gwt.dev.jjs.InternalCompilerException; import com.google.gwt.dev.jjs.SourceInfo; import com.google.gwt.dev.jjs.impl.OptimizerStats; import com.google.gwt.dev.js.ast.JsArrayAccess; import com.google.gwt.dev.js.ast.JsArrayLiteral; import com.google.gwt.dev.js.ast.JsBinaryOperation; import com.google.gwt.dev.js.ast.JsBinaryOperator; import com.google.gwt.dev.js.ast.JsBlock; import com.google.gwt.dev.js.ast.JsBooleanLiteral; import com.google.gwt.dev.js.ast.JsCase; import com.google.gwt.dev.js.ast.JsCatchScope; import com.google.gwt.dev.js.ast.JsConditional; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsDefault; import com.google.gwt.dev.js.ast.JsEmpty; import com.google.gwt.dev.js.ast.JsExprStmt; import com.google.gwt.dev.js.ast.JsExpression; import com.google.gwt.dev.js.ast.JsFor; import com.google.gwt.dev.js.ast.JsForIn; import com.google.gwt.dev.js.ast.JsFunction; import com.google.gwt.dev.js.ast.JsIf; import com.google.gwt.dev.js.ast.JsInvocation; import com.google.gwt.dev.js.ast.JsModVisitor; import com.google.gwt.dev.js.ast.JsName; import com.google.gwt.dev.js.ast.JsNameRef; import com.google.gwt.dev.js.ast.JsNew; import com.google.gwt.dev.js.ast.JsNode; import com.google.gwt.dev.js.ast.JsNullLiteral; import com.google.gwt.dev.js.ast.JsNumberLiteral; import com.google.gwt.dev.js.ast.JsObjectLiteral; import com.google.gwt.dev.js.ast.JsParameter; import com.google.gwt.dev.js.ast.JsPostfixOperation; import com.google.gwt.dev.js.ast.JsPrefixOperation; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.js.ast.JsRegExp; import com.google.gwt.dev.js.ast.JsReturn; import com.google.gwt.dev.js.ast.JsRootScope; import com.google.gwt.dev.js.ast.JsScope; import com.google.gwt.dev.js.ast.JsStatement; import com.google.gwt.dev.js.ast.JsStringLiteral; import com.google.gwt.dev.js.ast.JsThisRef; import com.google.gwt.dev.js.ast.JsVars; import com.google.gwt.dev.js.ast.JsVars.JsVar; import com.google.gwt.dev.js.ast.JsVisitor; import com.google.gwt.dev.js.ast.JsWhile; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.Stack; /** * Perform inlining optimizations on the JavaScript AST. * * TODO(bobv): remove anything that's duplicating work with {@link JsStaticEval} * migrate other stuff to that class perhaps. */ public class JsInliner { private static final String NAME = JsInliner.class.getSimpleName(); /** * Determines if the evaluation of a JsNode may be affected by side effects. */ private static class AffectedBySideEffectsVisitor extends JsVisitor { private boolean affectedBySideEffects; private final JsScope safeScope; public AffectedBySideEffectsVisitor(JsScope safeScope) { this.safeScope = safeScope; } public boolean affectedBySideEffects() { return affectedBySideEffects; } @Override public void endVisit(JsArrayLiteral x, JsContext ctx) { affectedBySideEffects = true; } @Override public void endVisit(JsFunction x, JsContext ctx) { affectedBySideEffects = true; } @Override public void endVisit(JsInvocation x, JsContext ctx) { /* * We could make this more accurate by analyzing the function that's being * executed, but we'll bank on subsequent passes inlining simple function * invocations. */ affectedBySideEffects = true; } @Override public void endVisit(JsNameRef x, JsContext ctx) { if (x.getQualifier() == null && x.getName() != null) { // Special case the undefined literal. if (x.getName() == JsRootScope.INSTANCE.getUndefined()) { return; } // Locals in a safe scope are unaffected. if (x.getName().getEnclosing() == safeScope) { return; } } /* * We can make this more accurate if we had single-assignment information * (e.g. static final fields). */ affectedBySideEffects = true; } @Override public void endVisit(JsObjectLiteral x, JsContext ctx) { affectedBySideEffects = true; } } /** * Make comma binary operations left-nested since commas are naturally * left-associative. We will define the comma-normal form such that a comma * expression should never have a comma expression as its RHS and contains no * side-effect-free expressions save for the outer, right-hand expression. * This form has a nice side-effect of minimizing the number of generated * parentheses. * * <pre> * (X, b) is unchanged * (X, (b, c) becomes ((X, b), c); b is guaranteed to have a side-effect * (X, ((b, c), d)) becomes (((X, b), c), d) * </pre> */ private static class CommaNormalizer extends JsModVisitor { /** * Returns an expression as a JsBinaryOperation if it is a comma expression. */ private static JsBinaryOperation isComma(JsExpression x) { if (!(x instanceof JsBinaryOperation)) { return null; } JsBinaryOperation op = (JsBinaryOperation) x; return op.getOperator().equals(JsBinaryOperator.COMMA) ? op : null; } private final List<JsName> localVariableNames; public CommaNormalizer(List<JsName> localVariableNames) { this.localVariableNames = localVariableNames; } @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { if (isComma(x) == null) { return; } // If (X, a) and X has no side effects, replace with a if (!x.getArg1().hasSideEffects()) { ctx.replaceMe(x.getArg2()); return; } JsBinaryOperation toUpdate = isComma(x.getArg2()); if (toUpdate == null) { /* * We have a JsBinaryOperation that's structurally normal: (X, a). Now * it may be the case that the inner expression X is a comma expression * (Y, b). If b creates no side-effects, we can remove it, leaving (Y, * a) as the expression. */ JsBinaryOperation inner = isComma(x.getArg1()); if (inner != null && !inner.getArg2().hasSideEffects()) { x.setArg1(inner.getArg1()); didChange = true; } /* * Eliminate the pattern (localVar = expr, localVar). This tends to * occur when a method interacted with pruned fields or had statements * removed. */ JsName assignmentRef = null; JsExpression expr = null; JsName returnRef = null; if (x.getArg1() instanceof JsBinaryOperation) { JsBinaryOperation op = (JsBinaryOperation) x.getArg1(); if (op.getOperator() == JsBinaryOperator.ASG && op.getArg1() instanceof JsNameRef) { JsNameRef nameRef = (JsNameRef) op.getArg1(); if (nameRef.getQualifier() == null) { assignmentRef = nameRef.getName(); expr = op.getArg2(); } } } if (x.getArg2() instanceof JsNameRef) { JsNameRef nameRef = (JsNameRef) x.getArg2(); if (nameRef.getQualifier() == null) { returnRef = nameRef.getName(); } } if (assignmentRef != null && assignmentRef.equals(returnRef) && localVariableNames.contains(assignmentRef)) { assert expr != null; localVariableNames.remove(assignmentRef); ctx.replaceMe(expr); } return; } // Find the left-most, nested comma expression while (isComma(toUpdate.getArg1()) != null) { toUpdate = (JsBinaryOperation) toUpdate.getArg1(); } /* * Create a new comma expression with the original LHS and the LHS of the * nested comma expression. */ JsBinaryOperation newOp = new JsBinaryOperation(x.getSourceInfo(), JsBinaryOperator.COMMA); newOp.setArg1(x.getArg1()); newOp.setArg2(toUpdate.getArg1()); // Set the LHS of the nested comma expression to the new comma expression toUpdate.setArg1(newOp); // Replace the original node with its updated RHS ctx.replaceMe(x.getArg2()); } } /** * Provides a relative metric by which the syntactic complexity of a * JsExpression can be gauged. */ private static class ComplexityEstimator extends JsVisitor { /** * The current measure of complexity. This measures the number of * expressions that have been encountered by the visitor. */ private int complexity = 0; @Override public void endVisit(JsArrayAccess x, JsContext ctx) { complexity++; } @Override public void endVisit(JsArrayLiteral x, JsContext ctx) { complexity++; } @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { complexity++; } @Override public void endVisit(JsBooleanLiteral x, JsContext ctx) { complexity++; } @Override public void endVisit(JsConditional x, JsContext ctx) { complexity++; } @Override public void endVisit(JsFunction x, JsContext ctx) { complexity++; } @Override public void endVisit(JsInvocation x, JsContext ctx) { complexity++; } @Override public void endVisit(JsNameRef x, JsContext ctx) { complexity++; } @Override public void endVisit(JsNew x, JsContext ctx) { complexity++; } @Override public void endVisit(JsNullLiteral x, JsContext ctx) { complexity++; } @Override public void endVisit(JsNumberLiteral x, JsContext ctx) { complexity++; } @Override public void endVisit(JsObjectLiteral x, JsContext ctx) { complexity++; } @Override public void endVisit(JsPostfixOperation x, JsContext ctx) { complexity++; } @Override public void endVisit(JsPrefixOperation x, JsContext ctx) { complexity++; } @Override public void endVisit(JsRegExp x, JsContext ctx) { complexity++; } @Override public void endVisit(JsStringLiteral x, JsContext ctx) { complexity++; } @Override public void endVisit(JsThisRef x, JsContext ctx) { complexity++; } public int getComplexity() { return complexity; } } /** * This is used to clean up duplication invocations of functions that should * only be executed once, such as clinit functions. Whenever there is a * possible branch in program flow, the remover will create a new instance of * itself to handle the possible branches. * * We don't look at combining branch choices. This will not produce the most * efficient elimination of duplicated calls, but it handles the general case * and is simple to verify. */ private static class DuplicateXORemover extends JsModVisitor { /* * TODO: Most of the special casing below can be removed if complex * statements always use blocks, rather than plain statements. */ /** * Retains the the functions that we know have been called. */ private final Set<JsFunction> called; private final JsProgram program; public DuplicateXORemover(JsProgram program) { this.program = program; called = new HashSet<JsFunction>(); } public DuplicateXORemover(JsProgram program, Set<JsFunction> alreadyCalled) { this.program = program; called = new HashSet<JsFunction>(alreadyCalled); } /** * Look for comma expressions that contain duplicate calls and handle the * conditional-evaluation case of logical and/or operations. */ @Override public boolean visit(JsBinaryOperation x, JsContext ctx) { if (x.getOperator() == JsBinaryOperator.COMMA) { boolean left = isDuplicateCall(x.getArg1()); boolean right = isDuplicateCall(x.getArg2()); if (left && right) { /* * (clinit(), clinit()) --> delete or null. * * This construct is very unlikely since the InliningVisitor builds * the comma expressions in a right-nested manner. */ if (ctx.canRemove()) { ctx.removeMe(); return false; } else { // The return value from an XO function is never used ctx.replaceMe(JsNullLiteral.INSTANCE); return false; } } else if (left) { // (clinit(), xyz) --> xyz // This is the common case ctx.replaceMe(accept(x.getArg2())); return false; } else if (right) { // (xyz, clinit()) --> xyz // Possible if a clinit() were the last element ctx.replaceMe(accept(x.getArg1())); return false; } } else if (x.getOperator().equals(JsBinaryOperator.AND) || x.getOperator().equals(JsBinaryOperator.OR)) { x.setArg1(accept(x.getArg1())); // Possibility of conditional evaluation of second parameter x.setArg2(branch(x.getArg2())); return false; } return true; } /** * Most of the branching statements (as well as JsFunctions) will visit with * a JsBlock, so we don't need to explicitly enumerate all JsStatement * subtypes. */ @Override public boolean visit(JsBlock x, JsContext ctx) { branch(x.getStatements()); return false; } @Override public boolean visit(JsCase x, JsContext ctx) { x.setCaseExpr(accept(x.getCaseExpr())); branch(x.getStmts()); return false; } @Override public boolean visit(JsConditional x, JsContext ctx) { x.setTestExpression(accept(x.getTestExpression())); x.setThenExpression(branch(x.getThenExpression())); x.setElseExpression(branch(x.getElseExpression())); return false; } @Override public boolean visit(JsDefault x, JsContext ctx) { branch(x.getStmts()); return false; } @Override public boolean visit(JsExprStmt x, JsContext ctx) { if (isDuplicateCall(x.getExpression())) { if (ctx.canRemove()) { ctx.removeMe(); } else { ctx.replaceMe(new JsEmpty(x.getSourceInfo())); } return false; } else { return true; } } @Override public boolean visit(JsFor x, JsContext ctx) { // The JsFor may have an expression xor a variable declaration. if (x.getInitExpr() != null) { x.setInitExpr(accept(x.getInitExpr())); } else if (x.getInitVars() != null) { x.setInitVars(accept(x.getInitVars())); } // The condition is optional if (x.getCondition() != null) { x.setCondition(accept(x.getCondition())); } // The increment expression is optional if (x.getIncrExpr() != null) { x.setIncrExpr(branch(x.getIncrExpr())); } // The body is not guaranteed to be a JsBlock x.setBody(branch(x.getBody())); return false; } @Override public boolean visit(JsForIn x, JsContext ctx) { if (x.getIterExpr() != null) { x.setIterExpr(accept(x.getIterExpr())); } x.setObjExpr(accept(x.getObjExpr())); // The body is not guaranteed to be a JsBlock x.setBody(branch(x.getBody())); return false; } @Override public boolean visit(JsIf x, JsContext ctx) { x.setIfExpr(accept(x.getIfExpr())); x.setThenStmt(branch(x.getThenStmt())); if (x.getElseStmt() != null) { x.setElseStmt(branch(x.getElseStmt())); } return false; } /** * Possibly record that we've seen a call in the current context. */ @Override public boolean visit(JsInvocation x, JsContext ctx) { JsFunction func = isExecuteOnce(x); while (func != null) { called.add(func); func = func.getImpliedExecute(); } return true; } @Override public boolean visit(JsWhile x, JsContext ctx) { x.setCondition(accept(x.getCondition())); // The body is not guaranteed to be a JsBlock x.setBody(branch(x.getBody())); return false; } private <T extends JsNode> void branch(List<T> x) { DuplicateXORemover dup = new DuplicateXORemover(program, called); dup.acceptWithInsertRemove(x); didChange |= dup.didChange(); } private <T extends JsNode> T branch(T x) { DuplicateXORemover dup = new DuplicateXORemover(program, called); T toReturn = dup.accept(x); if ((toReturn != x) && !dup.didChange()) { throw new InternalCompilerException( "node replacement should imply didChange()"); } didChange |= dup.didChange(); return toReturn; } private boolean isDuplicateCall(JsExpression x) { if (!(x instanceof JsInvocation)) { return false; } JsFunction func = isExecuteOnce((JsInvocation) x); return (func != null && called.contains(func)); } } /** * Determines that a list of names is guaranteed to be evaluated in a * particular order. Also ensures that all names are evaluated before any * invocations occur. */ private static class EvaluationOrderVisitor extends JsVisitor { /** * A dummy name to represent 'this' refs. */ public static final JsName THIS_NAME = new JsCatchScope( JsRootScope.INSTANCE, "this").getAllNames().next(); private boolean maintainsOrder = true; private final List<JsName> toEvaluate; private final List<JsName> unevaluated; private final Set<JsName> paramsOrLocals = new HashSet<JsName>(); public EvaluationOrderVisitor(List<JsName> toEvaluate, JsFunction callee) { this.toEvaluate = toEvaluate; this.unevaluated = new ArrayList<JsName>(toEvaluate); // collect params and locals from callee function new JsVisitor() { @Override public void endVisit(JsParameter x, JsContext ctx) { paramsOrLocals.add(x.getName()); } @Override public boolean visit(JsVar x, JsContext ctx) { // record this before visiting initializer paramsOrLocals.add(x.getName()); return true; } }.accept(callee); } @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { JsBinaryOperator op = x.getOperator(); /* * We don't care about the left-hand expression, because it is guaranteed * to be evaluated. */ boolean rightStrict = refersToRequiredName(x.getArg2()); boolean conditionalEvaluation = JsBinaryOperator.AND.equals(op) || JsBinaryOperator.OR.equals(op); if (rightStrict && conditionalEvaluation) { maintainsOrder = false; } } /** * If the condition would cause conditional evaluation of strict parameters, * don't allow inlining. */ @Override public void endVisit(JsConditional x, JsContext ctx) { boolean thenStrict = refersToRequiredName(x.getThenExpression()); boolean elseStrict = refersToRequiredName(x.getElseExpression()); if (thenStrict || elseStrict) { maintainsOrder = false; } } /** * The statement declares a function closure. This makes actual evaluation * order of the parameters difficult or impossible to determine, so we'll * just ignore them. */ @Override public void endVisit(JsFunction x, JsContext ctx) { maintainsOrder = false; } /** * The innermost invocation we see must consume all presently unevaluated * parameters to ensure that an exception does not prevent their evaluation. * * In the case of a nested invocation, such as * <code>F(r1, r2, G(r3, r4), f1);</code> the evaluation order is guaranteed * to be maintained, provided that no required parameters occur after the * nested invocation. */ @Override public void endVisit(JsInvocation x, JsContext ctx) { if (unevaluated.size() > 0) { maintainsOrder = false; } } @Override public void endVisit(JsNameRef x, JsContext ctx) { checkName(x.getName()); } @Override public void endVisit(JsNew x, JsContext ctx) { /* * Unless all arguments have already been evaluated, assume that invoking * the new expression might interfere with the evaluation of the argument. * * It would be possible to allow this if the invoked function either does * nothing or does nothing that affects the remaining arguments. However, * currently there is no analysis of the invoked function. */ if (unevaluated.size() > 0) { maintainsOrder = false; } } @Override public void endVisit(JsThisRef x, JsContext ctx) { checkName(THIS_NAME); } public boolean maintainsOrder() { return maintainsOrder && unevaluated.size() == 0; } /** * Check to see if the evaluation of this JsName will break program order assumptions given * the parameters left to be substituted. * * The cases are as follows: * 1) JsName is a function parameter name which has side effects or is affected by side effects * (hereafter called 'volatile'), so it will be in 'toEvaluate' * 2) JsName is a function parameter which is not volatile (not in toEvaluate) * 3) JsName is a reference to a global variable * 4) JsName is a reference to a local variable * * A reference to a global while there are still parameters left to evaluate / substitute * implies an order violation. * * A reference to a volatile parameter is ok if it is the next parameter in sequence to * be evaluated (beginning of unevaluated list). Else, it is either being evaluated out of * order with respect to other parameters, or it is being evaluated more than once. */ private void checkName(JsName name) { if (!toEvaluate.contains(name)) { // if the name is a non-local/non-parameter (e.g. global) and there are params left to eval if (!paramsOrLocals.contains(name) && unevaluated.size() > 0) { maintainsOrder = false; } // else this may be a local, or all volatile params have already been evaluated, so it's ok. return; } // either this param is being evaled twice, or out of order if (unevaluated.size() == 0 || !unevaluated.remove(0).equals(name)) { maintainsOrder = false; } } /** * Determine if an expression contains a reference to a strict parameter. */ private boolean refersToRequiredName(JsExpression e) { RefersToNameVisitor v = new RefersToNameVisitor(toEvaluate); v.accept(e); return v.refersToName(); } } /** * Collect names in a hoisted statement that are local to the original * scope. These names will need to be copied to the destination scope * once the statement becomes hoisted. */ private static class HoistedNameVisitor extends JsVisitor { private final JsScope toScope; private final JsScope fromScope; private final List<JsName> hoistedNames; public HoistedNameVisitor(JsScope toScope, JsScope fromScope) { this.toScope = toScope; this.fromScope = fromScope; this.hoistedNames = new ArrayList<JsName>(); } public List<JsName> getHoistedNames() { return hoistedNames; } /* * We need to hoist names that are only visible in fromScope, but not in * toScope (i.e. we don't want to hoist names that are visible to both * scopes, such as a global). Also, we don't want to hoist names that have a * staticRef, which indicates a formal parameter, or a function name. */ @Override public boolean visit(JsNameRef nameRef, JsContext ctx) { JsName name = nameRef.getName(); JsName fromScopeName = fromScope.findExistingName(name.getIdent()); JsName toScopeName = toScope.findExistingName(name.getIdent()); if (name.getStaticRef() == null && name == fromScopeName && name != toScopeName && !hoistedNames.contains(name)) { hoistedNames.add(name); } return true; } } /** * Collect all of the idents used in an AST node. The collector can be * configured to collect idents from qualified xor unqualified JsNameRefs. */ private static class IdentCollector extends JsVisitor { private final boolean collectQualified; private final Set<String> idents = new HashSet<String>(); public IdentCollector(boolean collectQualified) { this.collectQualified = collectQualified; } @Override public void endVisit(JsNameRef x, JsContext ctx) { boolean hasQualifier = x.getQualifier() != null; if ((collectQualified && !hasQualifier) || (!collectQualified && hasQualifier)) { return; } assert x.getIdent() != null; idents.add(x.getIdent()); } public Set<String> getIdents() { return idents; } } /** * This class looks for function invocations that can be inlined and performs * the replacement by replacing the JsInvocation with a comma expression * consisting of the expressions evaluated by the target function. A second * step may convert the expressions in the comma expression back to multiple * statements if the context of the invocation would allow this. */ private static class InliningVisitor extends JsModVisitor { private final Set<JsFunction> blacklist = new HashSet<JsFunction>(); /** * This reflects the functions that are currently being inlined to prevent * infinite expansion. */ private final Stack<JsFunction> inlining = new Stack<JsFunction>(); /** * This reflects which function the visitor is currently visiting. */ private final Stack<JsFunction> functionStack = new Stack<JsFunction>(); private final InvocationCountingVisitor invocationCountingVisitor = new InvocationCountingVisitor(); private final Stack<List<JsName>> newLocalVariableStack = new Stack<List<JsName>>(); /** * A map containing the next integer to try as an identifier suffix for a * given JsScope. */ private IdentityHashMap<JsScope, HashMap<String, Integer>> startIdentForScope = new IdentityHashMap<JsScope, HashMap<String, Integer>>(); /** * Not a stack because program fragments aren't nested. */ private JsFunction programFunction; public InliningVisitor(JsProgram program) { invocationCountingVisitor.accept(program); } /** * Add to the list of JsFunctions that should not be inlined, regardless of * whether or not they would normally be inlinable. */ public void blacklist(Collection<JsFunction> functions) { blacklist.addAll(functions); } /** * This normalizes the comma expressions into multiple statements and * removes statements with no side-effects. */ @Override public void endVisit(JsExprStmt x, JsContext ctx) { JsExpression e = x.getExpression(); // We will occasionally create JsExprStmts that have no side-effects. if (ctx.canRemove() && !x.getExpression().hasSideEffects()) { ctx.removeMe(); return; } List<JsExprStmt> statements = new ArrayList<JsExprStmt>(); /* * Assemble the expressions back into a list of JsExprStmts. We will * iteratively disassemble the nested comma expressions, stopping when the * LHS is not a comma expression. */ while (e instanceof JsBinaryOperation) { JsBinaryOperation op = (JsBinaryOperation) e; if (!op.getOperator().equals(JsBinaryOperator.COMMA)) { break; } /* * We can ignore intermediate expressions as long as they have no * side-effects. */ if (op.getArg2().hasSideEffects()) { statements.add(0, op.getArg2().makeStmt()); } e = op.getArg1(); } /* * We know the return value from the original invocation was ignored, so * it may be possible to ignore the final expressions as long as it has no * side-effects. */ if (e.hasSideEffects()) { statements.add(0, e.makeStmt()); } if (statements.size() == 0) { // The expression contained no side effects at all. if (ctx.canRemove()) { ctx.removeMe(); } else { ctx.replaceMe(new JsEmpty(x.getSourceInfo())); } } else if (x.getExpression() != statements.get(0).getExpression()) { // Something has changed if (!ctx.canInsert()) { /* * This indicates that the function was attached to a clause of a * control function and not into an existing block. We'll replace the * single JsExprStmt with a JsBlock that contains all of the * statements. */ JsBlock b = new JsBlock(x.getSourceInfo()); b.getStatements().addAll(statements); ctx.replaceMe(b); return; } else { // Insert the new statements into the original context for (JsStatement s : statements) { ctx.insertBefore(s); } ctx.removeMe(); } } } @Override public void endVisit(JsFunction x, JsContext ctx) { if (!functionStack.pop().equals(x)) { throw new InternalCompilerException("Unexpected function popped"); } JsBlock body = x.getBody(); List<JsName> newLocalVariables = newLocalVariableStack.pop(); addVars(x, body, newLocalVariables); } @Override public void endVisit(JsInvocation x, JsContext ctx) { if (functionStack.isEmpty()) { return; } JsFunction callerFunction = functionStack.peek(); /* * We only want to look at invocations of things that we statically know * to be functions. Otherwise, we can't know what statements the * invocation would actually invoke. The static reference would be null * when trying operate on references to external functions, or functions * as arguments to another function. */ JsFunction invokedFunction = isFunction(x.getQualifier()); if (invokedFunction == null) { return; } /* * Don't inline huge functions into huge multi-expressions. Some JS * engines will blow up. */ if (invokedFunction.getBody().getStatements().size() > 50) { return; } // Don't inline blacklisted functions if (blacklist.contains(invokedFunction)) { return; } /* * The current function has been mutated so as to be self-recursive. Ban * it from any future inlining to prevent infinite expansion. */ if (invokedFunction == callerFunction) { blacklist.add(invokedFunction); return; } /* * We are already in the middle of attempting to inline a call to this * function. This check prevents infinite expansion across * mutually-recursive, inlinable functions. Any invocation skipped by this * logic will be re-visited in the <code>op = accept(op)</code> call in * the outermost JsInvocation. */ if (inlining.contains(invokedFunction)) { return; } inlining.push(invokedFunction); x = tryToUnravelExplicitCall(x); JsExpression op = process(x, callerFunction, invokedFunction); if (x != op) { /* * See if any further inlining can be performed in the current context. * By attempting to maximize the level of inlining now, we can reduce * the total number of passes required to finalize the AST. */ op = accept(op); ctx.replaceMe(op); } if (inlining.pop() != invokedFunction) { throw new RuntimeException("Unexpected function popped"); } } @Override public void endVisit(JsProgram x, JsContext ctx) { if (!functionStack.pop().equals(programFunction)) { throw new InternalCompilerException("Unexpected function popped"); } assert programFunction.getBody().getStatements().size() == 0 : "Should not have moved statements into program"; List<JsName> newLocalVariables = newLocalVariableStack.pop(); assert newLocalVariables.size() == 0 : "Should not have tried to create variables in program"; } @Override public boolean visit(JsExprStmt x, JsContext ctx) { if (functionStack.peek() == programFunction) { /* Don't inline top-level invocations. */ if (x.getExpression() instanceof JsInvocation) { return false; } } return true; } @Override public boolean visit(JsFunction x, JsContext ctx) { functionStack.push(x); newLocalVariableStack.push(new ArrayList<JsName>()); return true; } /** * Create a synthetic context to attempt to simplify statements in the * top-level of the program. */ @Override public boolean visit(JsProgram x, JsContext ctx) { programFunction = new JsFunction(x.getSourceInfo(), x.getScope()); programFunction.setBody(new JsBlock(x.getSourceInfo())); functionStack.push(programFunction); newLocalVariableStack.push(new ArrayList<JsName>()); return true; } private void addVars(HasSourceInfo x, JsBlock body, List<JsName> newLocalVariables) { // Nothing to do if (newLocalVariables.isEmpty()) { return; } List<JsStatement> statements = body.getStatements(); // The body can't be empty if we have local variables to create assert !statements.isEmpty(); // Find or create the JsVars as the first statement SourceInfo sourceInfo = x.getSourceInfo(); JsVars vars; if (statements.get(0) instanceof JsVars) { vars = (JsVars) statements.get(0); } else { vars = new JsVars(sourceInfo); statements.add(0, vars); } // Add all variables for (JsName name : newLocalVariables) { vars.add(new JsVar(sourceInfo, name)); } } private boolean isInvokedMoreThanOnce(JsFunction f) { Integer count = invocationCountingVisitor.invocationCount(f); return count == null || count > 1; } /** * Determine if <code>invokedFunction</code> can be inlined into * <code>callerFunction</code> at callsite <code>x</code>. * * @return An expression equivalent to <code>x</code> */ private JsExpression process(JsInvocation x, JsFunction callerFunction, JsFunction invokedFunction) { List<JsStatement> statements; if (invokedFunction.getBody() != null) { statements = new ArrayList<JsStatement>( invokedFunction.getBody().getStatements()); } else { /* * Will see this with certain classes whose clinits are folded into the * main JsProgram body. */ statements = Collections.emptyList(); } List<JsExpression> hoisted = new ArrayList<JsExpression>(statements.size()); JsExpression thisExpr = ((JsNameRef) x.getQualifier()).getQualifier(); HoistedNameVisitor hoistedNameVisitor = new HoistedNameVisitor(callerFunction.getScope(), invokedFunction.getScope()); boolean sawReturnStatement = false; for (JsStatement statement : statements) { if (sawReturnStatement) { /* * We've already seen a return statement, but there are still more * statements. The target is unsafe to inline, so bail. Note: in most * cases JsStaticEval will have removed any statements following a * return statement. * * The reason we have to bail is that the return statement's * expression MUST be the last thing evaluated. * * TODO(bobv): maybe it could still be inlined with smart * transformation? */ return x; } /* * Create replacement expressions to use in place of the original * statements. It is important that the replacement is newly-minted and * therefore not referenced by any other AST nodes. Consider the case of * a common, delegating function. If the hoisted expressions were not * distinct objects, it would not be possible to substitute different * JsNameRefs at different call sites. */ JsExpression h = hoistedExpression(statement); if (h == null) { return x; } /* * Visit the statement to find names that will be moved to the caller's * scope from the invoked function. */ hoistedNameVisitor.accept(statement); if (isReturnStatement(statement)) { sawReturnStatement = true; hoisted.add(h); } else if (hasSideEffects(Collections.singletonList(h))) { hoisted.add(h); } } /* * Get the referenced names that need to be copied to the caller's scope. */ List<JsName> hoistedNames = hoistedNameVisitor.getHoistedNames(); /* * If the inlined method has no return statement, synthesize an undefined * reference. It will be reclaimed if the method call is from a * JsExprStmt. */ if (!sawReturnStatement) { hoisted.add(new JsNameRef(x.getSourceInfo(), JsRootScope.INSTANCE.getUndefined())); } assert (hoisted.size() > 0); /* * Build up the new comma expression from right-to-left; building the * rightmost comma expressions first. Bootstrapping with i.previous() * ensures that this logic will function correctly in the case of a single * expression. */ SourceInfo sourceInfo = x.getSourceInfo(); ListIterator<JsExpression> i = hoisted.listIterator(hoisted.size()); JsExpression op = i.previous(); while (i.hasPrevious()) { JsBinaryOperation outerOp = new JsBinaryOperation(sourceInfo, JsBinaryOperator.COMMA); outerOp.setArg1(i.previous()); outerOp.setArg2(op); op = outerOp; } // Confirm that the expression conforms to the desired heuristics if (!isInlinable(callerFunction, invokedFunction, thisExpr, x.getArguments(), op)) { return x; } // Perform the name replacement NameRefReplacerVisitor v = new NameRefReplacerVisitor(thisExpr, x.getArguments(), invokedFunction.getParameters()); for (ListIterator<JsName> nameIterator = hoistedNames.listIterator(); nameIterator.hasNext();) { JsName name = nameIterator.next(); /* * Find an unused identifier in the caller's scope. It's possible that * the same function has been inlined in multiple places within the * function so we'll use a counter for disambiguation. */ String ident; String base = invokedFunction.getName() + "_" + name.getIdent(); JsScope scope = callerFunction.getScope(); HashMap<String, Integer> startIdent = startIdentForScope.get(scope); if (startIdent == null) { startIdent = new HashMap<String, Integer>(); startIdentForScope.put(scope, startIdent); } Integer s = startIdent.get(base); int suffix = (s == null) ? 0 : s.intValue(); do { ident = base + "_" + suffix++; } while (scope.findExistingName(ident) != null); startIdent.put(base, suffix); JsName newName = scope.declareName(ident, name.getShortIdent()); v.setReplacementName(name, newName); nameIterator.set(newName); } op = v.accept(op); // Normalize any nested comma expressions that we may have generated. op = (new CommaNormalizer(hoistedNames)).accept(op); /* * Compare the relative complexity of the original invocation versus the * inlined form. */ int originalComplexity = complexity(x); int inlinedComplexity = complexity(op); double ratio = ((double) inlinedComplexity) / originalComplexity; if (ratio > MAX_COMPLEXITY_INCREASE && isInvokedMoreThanOnce(invokedFunction)) { return x; } if (callerFunction == programFunction && hoistedNames.size() > 0) { // Don't add additional variables to the top-level program. return x; } // We've committed to the inlining, ensure the vars are created newLocalVariableStack.peek().addAll(hoistedNames); // update invocation counts according to this inlining invocationCountingVisitor.removeCountsFor(x); invocationCountingVisitor.accept(op); return op; } } /** * Counts the number of times a function is invoked. Functions that only have * a single call site in the whole program are inlined, regardless of * complexity. */ private static class InvocationCountingVisitor extends JsVisitor { private boolean removingCounts = false; private final Map<JsFunction, Integer> invocationCount = new IdentityHashMap<JsFunction, Integer>(); @Override public void endVisit(JsInvocation x, JsContext ctx) { checkFunctionCall(x.getQualifier()); } @Override public void endVisit(JsNew x, JsContext ctx) { checkFunctionCall(x.getConstructorExpression()); } public Integer invocationCount(JsFunction f) { return invocationCount.get(f); } /** * Like accept(), but remove counts for all invocations in expr. */ public void removeCountsFor(JsExpression expr) { assert (!removingCounts); removingCounts = true; accept(expr); removingCounts = false; } private void checkFunctionCall(JsExpression qualifier) { JsFunction function = isFunction(qualifier); if (function != null) { Integer count = invocationCount.get(function); if (count == null) { assert (!removingCounts); count = 1; } else { if (removingCounts) { count -= 1; } else { count += 1; } } invocationCount.put(function, count); } } } /** * Replace references to JsNames with the inlined JsExpression. */ private static class NameRefReplacerVisitor extends JsModVisitor { /** * Set up a map to record name replacements to perform. */ final Map<JsName, JsName> nameReplacements = new IdentityHashMap<JsName, JsName>(); /** * Set up a map of parameter names back to the expressions that will be * passed in from the outer call site. */ final Map<JsName, JsExpression> paramsToArgsMap = new IdentityHashMap<JsName, JsExpression>(); /** * A replacement expression for this references. */ private JsExpression thisExpr; public NameRefReplacerVisitor(JsExpression thisExpr, List<JsExpression> arguments, List<JsParameter> parameters) { this.thisExpr = thisExpr; if (parameters.size() != arguments.size()) { // This shouldn't happen if the cloned JsInvocation has been properly // configured throw new InternalCompilerException( "Mismatch on parameters and arguments"); } for (int i = 0; i < parameters.size(); i++) { JsParameter p = parameters.get(i); JsExpression e = arguments.get(i); paramsToArgsMap.put(p.getName(), e); } } /** * Replace JsNameRefs that refer to parameters with the expression passed * into the function invocation. */ @Override public void endVisit(JsNameRef x, JsContext ctx) { if (x.getQualifier() != null) { return; } JsExpression replacement = tryGetReplacementExpression(x.getSourceInfo(), x.getName()); if (replacement != null) { ctx.replaceMe(replacement); } } @Override public void endVisit(JsThisRef x, JsContext ctx) { assert thisExpr != null; ctx.replaceMe(thisExpr); } /** * Set a replacement JsName for all references to a JsName. * * @param name the name to replace * @param newName the new name that should be used in place of references to * <code>name</code> * @return the previous JsName the name would have been replaced with or * <code>null</code> if one was not previously set */ public JsName setReplacementName(JsName name, JsName newName) { return nameReplacements.put(name, newName); } /** * Determine the replacement expression to use in place of a reference to a * given name. Returns <code>null</code> if no replacement has been set for * the name. */ private JsExpression tryGetReplacementExpression(SourceInfo sourceInfo, JsName name) { if (paramsToArgsMap.containsKey(name)) { /* * TODO if we ever allow mutable JsExpression types to be considered * always flexible, then it would be necessary to clone the expression. */ return paramsToArgsMap.get(name); } else if (nameReplacements.containsKey(name)) { return nameReplacements.get(name).makeRef(sourceInfo); } else { return null; } } } /** * Detects function declarations. */ private static class NestedFunctionVisitor extends JsVisitor { private boolean containsNestedFunctions = false; public boolean containsNestedFunctions() { return containsNestedFunctions; } @Override public void endVisit(JsFunction x, JsContext ctx) { containsNestedFunctions = true; } } /** * Detects uses of parameters that would produce incorrect results if inlined. * Generally speaking, we disallow the use of parameters as lvalues. Also * detects trying to inline a method which references 'this' where the call * site has no qualifier. */ private static class ParameterUsageVisitor extends JsVisitor { private final boolean hasThisExpr; private final Set<JsName> parameterNames; private boolean violation = false; public ParameterUsageVisitor(boolean hasThisExpr, Set<JsName> parameterNames) { this.hasThisExpr = hasThisExpr; this.parameterNames = parameterNames; } @Override public void endVisit(JsNameRef x, JsContext ctx) { if (ctx.isLvalue() && isParameter(x)) { violation = true; } } @Override public void endVisit(JsThisRef x, JsContext ctx) { if (!hasThisExpr) { violation = true; } } public boolean hasViolation() { return violation; } /** * Determine if a JsExpression is a JsNameRef that refers to a parameter. */ private boolean isParameter(JsNameRef ref) { if (ref.getQualifier() != null) { return false; } JsName name = ref.getName(); return parameterNames.contains(name); } } /** * Collect self-recursive functions. This visitor does not look for * mutually-recursive functions because inlining one of the functions into the * other would make the single resultant function self-recursive and not * eligible for inlining in a subsequent pass. */ private static class RecursionCollector extends JsVisitor { private final Stack<JsFunction> functionStack = new Stack<JsFunction>(); private final Set<JsFunction> recursive = new HashSet<JsFunction>(); @Override public void endVisit(JsFunction x, JsContext ctx) { if (!functionStack.pop().equals(x)) { throw new InternalCompilerException("Unexpected function popped"); } } @Override public void endVisit(JsInvocation x, JsContext ctx) { /* * Because functions can encapsulate other functions, we look at the * entire stack and not just the top element. This would prevent inlining * * function a() { function b() { a(); } b(); } * * in the case that we generally allow nested functions to be inlinable. */ JsFunction f = isFunction(x.getQualifier()); if (functionStack.contains(f)) { recursive.add(f); } } public Set<JsFunction> getRecursive() { return recursive; } @Override public boolean visit(JsFunction x, JsContext ctx) { functionStack.push(x); return true; } } /** * Determine which functions should not be inlined because they are redefined * during program execution. This would violate the assumption that the * statements to be executed by any given function invocation are stable over * the lifetime of the program. */ private static class RedefinedFunctionCollector extends JsVisitor { private final Map<JsName, JsFunction> nameMap = new IdentityHashMap<JsName, JsFunction>(); private final Set<JsFunction> redefined = new HashSet<JsFunction>(); /** * Look for assignments to JsNames whose static references are JsFunctions. */ @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { if (!x.getOperator().equals(JsBinaryOperator.ASG)) { return; } JsFunction f = isFunction(x.getArg1()); if (f != null) { redefined.add(f); } } /** * Look for the case where a function is declared with the same name as an * existing function. */ @Override public void endVisit(JsFunction x, JsContext ctx) { JsName name = x.getName(); if (name == null) { // Ignore anonymous functions return; } else if (nameMap.containsKey(name)) { /* * We have to add the current function as well as the original * JsFunction that was declared to use that name. */ redefined.add(nameMap.get(name)); redefined.add(x); } else { nameMap.put(name, x); } } public Collection<JsFunction> getRedefined() { return redefined; } } /** * Given a collection of JsNames, determine if an AST node refers to any of * those names. */ private static class RefersToNameVisitor extends JsVisitor { private final Collection<JsName> names; private boolean refersToName; public RefersToNameVisitor(Collection<JsName> names) { this.names = names; } @Override public void endVisit(JsNameRef x, JsContext ctx) { JsName name = x.getName(); if (name != null) { refersToName = refersToName || names.contains(name); } } public boolean refersToName() { return refersToName; } } /** * This ensures that changing the scope of an expression from its enclosing * function into the scope of the call site will not cause unqualified * identifiers to resolve to different values. */ private static class StableNameChecker extends JsVisitor { private final JsScope calleeScope; private final JsScope callerScope; private final Collection<JsName> parameterNames; private boolean stable = true; public StableNameChecker(JsScope callerScope, JsScope calleeScope, Collection<JsName> parameterNames) { this.callerScope = callerScope; this.calleeScope = calleeScope; this.parameterNames = parameterNames; } @Override public void endVisit(JsNameRef x, JsContext ctx) { /* * We can ignore qualified reference, since their scope is always that of * the qualifier. */ if (x.getQualifier() != null) { return; } /* * Attempt to resolve the ident in both scopes */ JsName callerName = callerScope.findExistingName(x.getIdent()); JsName calleeName = calleeScope.findExistingName(x.getIdent()); if (callerName == null && calleeName == null) { // They both reference out-of-module names } else if (parameterNames.contains(calleeName)) { // A reference to a parameter, which will be replaced by an argument } else if (callerName != null && callerName.equals(calleeName)) { // The names are known to us and are the same } else if (calleeName.getEnclosing().equals(calleeScope)) { // It's a local variable in the callee } else { stable = false; } } public boolean isStable() { return stable; } } /** * When attempting to inline an invocation, this constant determines the * maximum allowable ratio of potential inlined complexity to initial * complexity. This acts as a brake on very large expansions from bloating the * the generated output. Increasing this number will allow larger sections of * code to be inlined, but at a cost of larger JS output. */ private static final double MAX_COMPLEXITY_INCREASE = Double.parseDouble(System.getProperty( "gwt.jsinlinerRatio", "1.7")); /** * Static entry point used by JavaToJavaScriptCompiler. */ public static OptimizerStats exec(JsProgram program) { Event optimizeJsEvent = SpeedTracerLogger.start( CompilerEventType.OPTIMIZE_JS, "optimizer", NAME); OptimizerStats stats = execImpl(program); optimizeJsEvent.end("didChange", "" + stats.didChange()); return stats; } /** * Determine whether or not a list of AST nodes are affected by side effects. * The context parameter provides a scope in which local (and therefore * immutable) variables are defined. */ private static boolean affectedBySideEffects(List<JsExpression> list, JsFunction context) { /* * If the caller contains no nested functions, none of its locals can * possibly be affected by side effects. */ JsScope safeScope = null; if (context != null && !containsNestedFunctions(context)) { safeScope = context.getScope(); } AffectedBySideEffectsVisitor v = new AffectedBySideEffectsVisitor(safeScope); v.acceptList(list); return v.affectedBySideEffects(); } /** * Generate an estimated measure of the syntactic complexity of a JsNode. */ private static int complexity(JsNode toEstimate) { ComplexityEstimator e = new ComplexityEstimator(); e.accept(toEstimate); return e.getComplexity(); } /** * Examine a JsFunction to determine if it contains nested functions. */ private static boolean containsNestedFunctions(JsFunction func) { NestedFunctionVisitor v = new NestedFunctionVisitor(); v.accept(func.getBody()); return v.containsNestedFunctions(); } /** * @param program * @return stats */ private static OptimizerStats execImpl(JsProgram program) { OptimizerStats stats = new OptimizerStats(NAME); RedefinedFunctionCollector d = new RedefinedFunctionCollector(); d.accept(program); RecursionCollector rc = new RecursionCollector(); rc.accept(program); InliningVisitor v = new InliningVisitor(program); v.blacklist(d.getRedefined()); v.blacklist(rc.getRecursive()); v.accept(program); if (v.didChange()) { stats.recordModified(); } DuplicateXORemover r = new DuplicateXORemover(program); r.accept(program); if (r.didChange()) { stats.recordModified(); } return stats; } /** * Check to see if the to-be-inlined statement shares any idents with the * call-side arguments. Two passes are made: the first one looks for qualified * names; the second pass looks for unqualified names, but ignores identifiers * that refer to function parameters. */ private static boolean hasCommonIdents(List<JsExpression> arguments, JsNode toInline, Collection<String> parameterIdents) { // This is a fire-twice loop boolean checkQualified = false; do { checkQualified = !checkQualified; // Collect the idents used in the arguments and the statement IdentCollector argCollector = new IdentCollector(checkQualified); argCollector.acceptList(arguments); IdentCollector statementCollector = new IdentCollector(checkQualified); statementCollector.accept(toInline); Set<String> idents = argCollector.getIdents(); // Unqualified idents may be references to parameters, thus ignored if (!checkQualified) { idents.removeAll(parameterIdents); } // Perform the set difference idents.retainAll(statementCollector.getIdents()); if (idents.size() > 0) { return true; } } while (checkQualified); return false; } /** * Determine whether or not a list of AST nodes have side effects. */ private static boolean hasSideEffects(List<JsExpression> list) { for (JsExpression expr : list) { if (expr.hasSideEffects()) { return true; } } return false; } /** * Given a delegated JsStatement, construct an expression to hoist into the * outer caller. This does not perform any name replacement, but simply * constructs a mutable copy of the expression that can be manipulated * at-will. * * @param statement the statement from which to extract the expressions * @return a JsExpression representing all expressions that would have been * evaluated by the statement */ private static JsExpression hoistedExpression(JsStatement statement) { JsExpression expression; if (statement instanceof JsExprStmt) { // Extract the expression JsExprStmt exprStmt = (JsExprStmt) statement; expression = exprStmt.getExpression(); } else if (statement instanceof JsReturn) { // Extract the return value JsReturn ret = (JsReturn) statement; expression = ret.getExpr(); if (expression == null) { expression = new JsNameRef(ret.getSourceInfo(), JsRootScope.INSTANCE.getUndefined()); } } else if (statement instanceof JsVars) { // Create a comma expression for variable initializers JsVars vars = (JsVars) statement; // Rely on comma expression cleanup to remove this later. expression = JsNullLiteral.INSTANCE; for (JsVar var : vars) { // Extract the initialization expression JsExpression init = var.getInitExpr(); if (init != null) { SourceInfo sourceInfo = var.getSourceInfo(); JsBinaryOperation assignment = new JsBinaryOperation(sourceInfo, JsBinaryOperator.ASG); assignment.setArg1(var.getName().makeRef(sourceInfo)); assignment.setArg2(init); // Multiple initializers go into a comma expression JsBinaryOperation comma = new JsBinaryOperation(sourceInfo, JsBinaryOperator.COMMA); comma.setArg1(expression); comma.setArg2(assignment); expression = comma; } } } else { return null; } assert expression != null; return JsHoister.hoist(expression); } /** * Given a JsInvocation, determine if it is invoking a JsFunction that is * specified to be executed only once during the program's lifetime. */ private static JsFunction isExecuteOnce(JsInvocation invocation) { JsFunction f = isFunction(invocation.getQualifier()); if (f != null && f.getExecuteOnce()) { return f; } return null; } /** * Given an expression, determine if it is a JsNameRef that refers to a * statically-defined JsFunction. */ private static JsFunction isFunction(JsExpression e) { if (e instanceof JsNameRef) { JsNameRef ref = (JsNameRef) e; // Unravel foo.call(...). if (!ref.getName().isObfuscatable() && "call".equals(ref.getIdent())) { if (ref.getQualifier() instanceof JsNameRef) { ref = (JsNameRef) ref.getQualifier(); } } JsNode staticRef = ref.getName().getStaticRef(); if (staticRef instanceof JsFunction) { return (JsFunction) staticRef; } } return null; } /** * Determine if a statement can be inlined into a call site. */ private static boolean isInlinable(JsFunction caller, JsFunction callee, JsExpression thisExpr, List<JsExpression> arguments, JsNode toInline) { /* * This will happen with varargs-style JavaScript functions that rely on the * "arguments" array. The reference to arguments would be detected in * BoundedScopeVisitor, but the code below assumes the same number of * parameters and arguments. */ if (arguments.size() != callee.getParameters().size()) { return false; } // Build up a list of all parameter names Set<JsName> parameterNames = new HashSet<JsName>(); Set<String> parameterIdents = new HashSet<String>(); for (JsParameter param : callee.getParameters()) { parameterNames.add(param.getName()); parameterIdents.add(param.getName().getIdent()); } /* * Make sure that inlining won't change the final name of non-parameter * idents due to the change of scope. The most likely cause would be the use * of an unqualified variable reference in a JSNI block that happened to * conflict with a Java-derived identifier. */ StableNameChecker detector = new StableNameChecker(caller.getScope(), callee.getScope(), parameterNames); detector.accept(toInline); if (!detector.isStable()) { return false; } /* * Ensure that the names referred to by the argument list and the statement * are disjoint. This prevents inlining of the following: * * static int i; public void add(int a) { i += a; }; add(i++); */ if (hasCommonIdents(arguments, toInline, parameterIdents)) { return false; } List<JsExpression> evalArgs; if (thisExpr == null) { evalArgs = arguments; } else { evalArgs = new ArrayList<JsExpression>(1 + arguments.size()); evalArgs.add(thisExpr); evalArgs.addAll(arguments); } /* * Determine if the evaluation of the invocation's arguments may create side * effects. This will determine how aggressively the parameters may be * reordered. */ if (isVolatile(evalArgs, caller)) { /* * Determine the order in which the parameters must be evaluated. This * will vary between call sites, based on whether or not the invocation's * arguments can be repeated without ill effect. */ List<JsName> requiredOrder = new ArrayList<JsName>(); if (thisExpr != null && isVolatile(thisExpr, callee)) { requiredOrder.add(EvaluationOrderVisitor.THIS_NAME); } for (int i = 0; i < arguments.size(); i++) { JsExpression e = arguments.get(i); JsParameter p = callee.getParameters().get(i); if (isVolatile(e, callee)) { requiredOrder.add(p.getName()); } } // This would indicate that isVolatile changed its output between // the if statement and the loop. assert requiredOrder.size() > 0; /* * Verify that the non-reorderable arguments are evaluated in the right * order. */ EvaluationOrderVisitor orderVisitor = new EvaluationOrderVisitor( requiredOrder, callee); orderVisitor.accept(toInline); if (!orderVisitor.maintainsOrder()) { return false; } } // Check that parameters aren't used in such a way as to prohibit inlining ParameterUsageVisitor v = new ParameterUsageVisitor(thisExpr != null, parameterNames); v.accept(toInline); if (v.hasViolation()) { return false; } // Hooray! return true; } /** * This is used in combination with {@link #hoistedExpression(JsStatement)} to * indicate if a given statement would terminate the list of hoisted * expressions. */ private static boolean isReturnStatement(JsStatement statement) { return statement instanceof JsReturn; } /** * Indicates if an expression would create side effects or possibly be * affected by side effects when evaluated within a particular function * context. */ private static boolean isVolatile(JsExpression e, JsFunction context) { return isVolatile(Collections.singletonList(e), context); } /** * Indicates if a list of expressions would create side effects or possibly be * affected by side effects when evaluated within a particular function * context. */ private static boolean isVolatile(List<JsExpression> list, JsFunction context) { return hasSideEffects(list) || affectedBySideEffects(list, context); } /** * Transforms any <code>foo.call(this)</code> into <code>this.foo()</code> to * be compatible with our inlining algorithm. */ private static JsInvocation tryToUnravelExplicitCall(JsInvocation x) { if (!(x.getQualifier() instanceof JsNameRef)) { return x; } JsNameRef ref = (JsNameRef) x.getQualifier(); if (ref.getName().isObfuscatable() || !"call".equals(ref.getIdent())) { return x; } List<JsExpression> oldArgs = x.getArguments(); if (oldArgs.size() < 1) { return x; } JsNameRef oldTarget = (JsNameRef) ref.getQualifier(); JsNameRef newTarget = new JsNameRef(oldTarget.getSourceInfo(), oldTarget.getName()); newTarget.setQualifier(oldArgs.get(0)); JsInvocation newCall = new JsInvocation(x.getSourceInfo()); newCall.setQualifier(newTarget); // Don't have to clone because the returned invocation is transient. newCall.getArguments().addAll(oldArgs.subList(1, oldArgs.size())); return newCall; } /** * Utility class. */ private JsInliner() { } }