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

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

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * A set of utility functions that replaces CALL with a specified FUNCTION body, replacing and
 * aliasing function parameters as necessary.
 */
class FunctionInjector {

  /** Sentinel value indicating that the key contains no functions. */
  private static final Node NO_FUNCTIONS = new Node(Token.FUNCTION);
  /** Sentinel value indicating that the key contains multiple distinct functions. */
  private static final Node MULTIPLE_FUNCTIONS = new Node(Token.FUNCTION);

  private final AbstractCompiler compiler;
  private final boolean allowDecomposition;
  private ImmutableSet knownConstantFunctions = ImmutableSet.of();
  private final boolean assumeStrictThis;
  private final boolean assumeMinimumCapture;
  private final Supplier safeNameIdSupplier;
  private final Supplier throwawayNameSupplier =
      new Supplier() {
        private int nextId = 0;

        @Override
        public String get() {
          return String.valueOf(nextId++);
        }
      };
  private final FunctionArgumentInjector functionArgumentInjector;

  /** Cache of function node to whether it deeply contains an {@code eval} call. */
  private final LinkedHashMap referencesEvalCache = new LinkedHashMap<>();

  /** Cache of function node to any inner function. */
  private final LinkedHashMap innerFunctionCache = new LinkedHashMap<>();

  private FunctionInjector(Builder builder) {
    this.compiler = checkNotNull(builder.compiler);
    this.safeNameIdSupplier = checkNotNull(builder.safeNameIdSupplier);
    this.assumeStrictThis = builder.assumeStrictThis;
    this.assumeMinimumCapture = builder.assumeMinimumCapture;
    this.allowDecomposition = builder.allowDecomposition;
    this.functionArgumentInjector = checkNotNull(builder.functionArgumentInjector);
  }

  static class Builder {

    private final AbstractCompiler compiler;
    private Supplier safeNameIdSupplier = null;
    private boolean assumeStrictThis = true;
    private boolean assumeMinimumCapture = true;
    private boolean allowDecomposition = true;
    private FunctionArgumentInjector functionArgumentInjector = null;

    Builder(AbstractCompiler compiler) {
      this.compiler = checkNotNull(compiler);
    }

    /**
     * Provide the name supplier to use for injection.
     *
     * 

If this method is not called, {@code compiler.getUniqueNameIdSupplier()} will be used. */ Builder safeNameIdSupplier(Supplier safeNameIdSupplier) { this.safeNameIdSupplier = checkNotNull(safeNameIdSupplier); return this; } /** * Allow decomposition of expressions. * *

Default is {@code true}. */ Builder allowDecomposition(boolean allowDecomposition) { this.allowDecomposition = allowDecomposition; return this; } Builder assumeStrictThis(boolean assumeStrictThis) { this.assumeStrictThis = assumeStrictThis; return this; } Builder assumeMinimumCapture(boolean assumeMinimumCapture) { this.assumeMinimumCapture = assumeMinimumCapture; return this; } /** * Specify the {@code FunctionArgumentInjector} to be used. * *

Default is for the builder to create this. This method exists for testing purposes. */ public Builder functionArgumentInjector(FunctionArgumentInjector functionArgumentInjector) { this.functionArgumentInjector = checkNotNull(functionArgumentInjector); return this; } public FunctionInjector build() { if (safeNameIdSupplier == null) { safeNameIdSupplier = compiler.getUniqueNameIdSupplier(); } if (functionArgumentInjector == null) { functionArgumentInjector = new FunctionArgumentInjector(checkNotNull(compiler.getAstAnalyzer())); } return new FunctionInjector(this); } } /** The type of inlining to perform. */ enum InliningMode { /** * Directly replace the call expression. Only functions of meeting strict preconditions can be * inlined. */ DIRECT, /** * Replaces the call expression with a block of statements. Conditions on the function are * looser in mode, but stricter on the call site. */ BLOCK } /** Holds a reference to the call node of a function call */ static class Reference { final Node callNode; final Scope scope; final JSChunk module; final InliningMode mode; Reference(Node callNode, Scope scope, JSChunk module, InliningMode mode) { this.callNode = callNode; this.scope = scope; this.module = module; this.mode = mode; } @Override public String toString() { return "Reference @ " + callNode; } } /** * In order to estimate the cost of lining, we make the assumption that Identifiers are reduced 2 * characters. For the call arguments, the important thing is that the cost is assumed to be the * same in the call and the function, so the actual length doesn't matter in most cases. */ private static final int NAME_COST_ESTIMATE = InlineCostEstimator.ESTIMATED_IDENTIFIER_COST; /** The cost of a argument separator (a comma). */ private static final int COMMA_COST = 1; /** The cost of the parentheses needed to make a call. */ private static final int PAREN_COST = 2; /** * @param fnName The name of this function. This either the name of the variable to which the * function is assigned or the name from the FUNCTION node. * @param fnNode The FUNCTION node of the function to inspect. * @return Whether the function node meets the minimum requirements for inlining. */ boolean doesFunctionMeetMinimumRequirements(final String fnName, Node fnNode) { Node block = NodeUtil.getFunctionBody(fnNode); // Basic restrictions on functions that can be inlined: // 0) The function is inlinable by convention // 1) It contains a reference to itself. // 2) It uses its parameters indirectly using "arguments" (it isn't // handled yet. // 3) It references "eval". Inline a function containing eval can have // large performance implications. if (!compiler.getCodingConvention().isInlinableFunction(fnNode)) { return false; } final String fnRecursionName = fnNode.getFirstChild().getString(); checkState(fnRecursionName != null); // If the function references "arguments" directly in the function or in an arrow function boolean referencesArguments = NodeUtil.isNameReferenced( block, "arguments", NodeUtil.MATCH_ANYTHING_BUT_NON_ARROW_FUNCTION); Predicate blocksInjection = (Node n) -> { if (n.isName()) { // References "eval" or one of its names anywhere. return n.getString().equals("eval") || (!fnName.isEmpty() && n.getString().equals(fnName)) || (!fnRecursionName.isEmpty() && n.getString().equals(fnRecursionName)); } else if (n.isSuper()) { // Don't inline if this function or its inner functions contains super return true; } return false; }; return !referencesArguments && !NodeUtil.has(block, blocksInjection, Predicates.alwaysTrue()); } /** * @param fnNode The function to evaluate for inlining. * @param needAliases A set of function parameter names that can not be used without aliasing. * Returned by getUnsafeParameterNames(). * @param referencesThis Whether fnNode contains references to its this object. * @param containsFunctions Whether fnNode contains inner functions. * @return Whether the inlining can occur. */ CanInlineResult canInlineReferenceToFunction( Reference ref, Node fnNode, ImmutableSet needAliases, boolean referencesThis, boolean containsFunctions) { // TODO(johnlenz): This function takes too many parameter, without // context. Modify the API to take a structure describing the function. // Allow direct function calls or "fn.call" style calls. Node callNode = ref.callNode; if (!isSupportedCallType(callNode)) { return CanInlineResult.NO; } if (hasSpreadCallArgument(callNode)) { return CanInlineResult.NO; } // Limit where functions that contain functions can be inline. Introducing // an inner function into another function can capture a variable and cause // a memory leak. This isn't a problem in the global scope as those values // last until explicitly cleared. if (containsFunctions) { if (!assumeMinimumCapture && !ref.scope.isGlobal()) { // TODO(johnlenz): Allow inlining into any scope without local names or inner functions. return CanInlineResult.NO; } else if (NodeUtil.isWithinLoop(callNode)) { // An inner closure maybe relying on a local value holding a value for a // single iteration through a loop. return CanInlineResult.NO; } } // TODO(johnlenz): Add support for 'apply' if (referencesThis && !NodeUtil.isFunctionObjectCall(callNode)) { // TODO(johnlenz): Allow 'this' references to be replaced with a // global 'this' object. return CanInlineResult.NO; } if (ref.mode == InliningMode.DIRECT) { return canInlineReferenceDirectly(ref, fnNode, needAliases); } else { return canInlineReferenceAsStatementBlock(ref, fnNode, needAliases); } } /** * Only ".call" calls and direct calls to functions are supported. * * @param callNode The call evaluate. * @return Whether the call is of a type that is supported. */ private boolean isSupportedCallType(Node callNode) { if (!callNode.getFirstChild().isName()) { if (NodeUtil.isFunctionObjectCall(callNode)) { if (!assumeStrictThis) { Node thisValue = callNode.getSecondChild(); if (thisValue == null || !thisValue.isThis()) { return false; } } } else if (NodeUtil.isFunctionObjectApply(callNode)) { return false; } } return true; } private static boolean hasSpreadCallArgument(Node callNode) { checkArgument(NodeUtil.isNormalOrOptChainCall(callNode), callNode); for (Node arg = callNode.getSecondChild(); arg != null; arg = arg.getNext()) { if (arg.isSpread()) { return true; } } return false; } /** Inline a function into the call site. */ Node inline(Reference ref, String fnName, Node fnNode) { checkState(compiler.getLifeCycleStage().isNormalized()); return internalInline(ref, fnName, fnNode); } /** * Inline a function into the call site. Note that this unsafe version doesn't verify if the AST * is normalized. You should use {@link #inline} instead, unless you are 100% certain that the bit * of code you're inlining is safe without being normalized first. */ Node unsafeInline(Reference ref, String fnName, Node fnNode) { return internalInline(ref, fnName, fnNode); } private Node internalInline(Reference ref, String fnName, Node fnNode) { Node result; if (ref.mode == InliningMode.DIRECT) { result = inlineReturnValue(ref, fnNode); } else { result = inlineFunction(ref, fnNode, fnName); } compiler.reportChangeToEnclosingScope(result); return result; } /** * Inline a function that fulfills the requirements of canInlineReferenceDirectly into the call * site, replacing only the CALL node. */ private Node inlineReturnValue(Reference ref, Node fnNode) { Node callNode = ref.callNode; Node block = fnNode.getLastChild(); // NOTE: As the normalize pass guarantees globals aren't being // shadowed and an expression can't introduce new names, there is // no need to check for conflicts. // Create an argName -> expression map, checking for side effects. Map argMap = functionArgumentInjector.getFunctionCallParameterMap( fnNode, callNode, this.safeNameIdSupplier); Node newExpression; if (!block.hasChildren()) { Node srcLocation = block; newExpression = NodeUtil.newUndefinedNode(srcLocation); } else { Node returnNode = block.getFirstChild(); checkArgument(returnNode.isReturn(), returnNode); // Clone the return node first. Node safeReturnNode = returnNode.cloneTree(); Node inlineResult = functionArgumentInjector.inject(null, safeReturnNode, null, argMap); checkArgument(safeReturnNode == inlineResult); newExpression = safeReturnNode.removeFirstChild(); NodeUtil.markNewScopesChanged(newExpression, compiler); } // If the call site had a cast ensure it's persisted to the new expression that replaces it. JSType typeBeforeCast = callNode.getJSTypeBeforeCast(); if (typeBeforeCast != null) { newExpression.setJSTypeBeforeCast(typeBeforeCast); newExpression.setJSType(callNode.getJSType()); } // If the new expression has no color or the UNKNOWN color, attach the color the call node. It // may be more accurate if the call node was in a cast (we don't track information about casts, // though) if (callNode.getColor() != null && callNode.isColorFromTypeCast()) { newExpression.setColor(callNode.getColor()); newExpression.setColorFromTypeCast(); } callNode.replaceWith(newExpression); NodeUtil.markFunctionsDeleted(callNode, compiler); return newExpression; } /** Supported call site types. */ private static enum CallSiteType { /** Used for a call site for which there does not exist a method to inline it. */ UNSUPPORTED() { @Override public void prepare(FunctionInjector injector, Reference ref) { throw new IllegalStateException("unexpected: " + ref); } }, /** A call as a statement. For example: "foo();". EXPR_RESULT CALL */ SIMPLE_CALL() { @Override public void prepare(FunctionInjector injector, Reference ref) { // Nothing to do. } }, /** * An assignment, where the result of the call is assigned to a simple name. For example: "a = * foo();". EXPR_RESULT NAME A CALL FOO */ SIMPLE_ASSIGNMENT() { @Override public void prepare(FunctionInjector injector, Reference ref) { // Nothing to do. } }, /** * An var declaration and initialization, where the result of the call is assigned to the * declared name name. For example: "var a = foo();". VAR NAME A CALL FOO */ VAR_DECL_SIMPLE_ASSIGNMENT() { @Override public void prepare(FunctionInjector injector, Reference ref) { // Nothing to do. } }, /** * An arbitrary expression, the root of which is a EXPR_RESULT, IF, RETURN, SWITCH or VAR. The * call must be the first side-effect in the expression. * *

Examples include: "if (foo()) {..." "return foo();" "var a = 1 + foo();" "a = 1 + foo()" * "foo() ? 1:0" "foo() && x" */ EXPRESSION() { @Override public void prepare(FunctionInjector injector, Reference ref) { Node callNode = ref.callNode; injector.getDecomposer(ref.scope).moveExpression(callNode); // Reclassify after move CallSiteType callSiteType = injector.classifyCallSite(ref); checkState(this != callSiteType); callSiteType.prepare(injector, ref); } }, /** * An arbitrary expression, the root of which is a EXPR_RESULT, IF, RETURN, SWITCH or VAR. Where * the call is not the first side-effect in the expression. */ DECOMPOSABLE_EXPRESSION() { @Override public void prepare(FunctionInjector injector, Reference ref) { Node callNode = ref.callNode; injector.getDecomposer(ref.scope).maybeExposeExpression(callNode); // Reclassify after decomposition CallSiteType callSiteType = injector.classifyCallSite(ref); checkState(this != callSiteType); callSiteType.prepare(injector, ref); } }; public abstract void prepare(FunctionInjector injector, Reference ref); } /** * Determine which, if any, of the supported types the call site is. * *

Constant vars are treated differently so that we don't break their const-ness when we * decompose the expression. Once the CONSTANT_VAR annotation is used everywhere instead of coding * conventions, we should just teach this pass how to remove the annotation. */ private CallSiteType classifyCallSite(Reference ref) { Node callNode = ref.callNode; Node parent = callNode.getParent(); Node grandParent = parent.getParent(); // Verify the call site: if (NodeUtil.isExprCall(parent)) { // This is a simple call. Example: "foo();". return CallSiteType.SIMPLE_CALL; } else if (NodeUtil.isExprAssign(grandParent) && !NodeUtil.isNameDeclOrSimpleAssignLhs(callNode, parent) && parent.getFirstChild().isName() // TODO(nicksantos): Remove this once everyone is using // the CONSTANT_VAR annotation. We know how to remove that. && !NodeUtil.isConstantName(parent.getFirstChild())) { // This is a simple assignment. Example: "x = foo();" return CallSiteType.SIMPLE_ASSIGNMENT; } else if (parent.isName() // TODO(nicksantos): Remove this once everyone is using the CONSTANT_VAR annotation. && !NodeUtil.isConstantName(parent) // Note: not let or const. See InlineFunctionsTest.testInlineFunctions35 && grandParent.isVar() && grandParent.hasOneChild()) { // This is a var declaration. Example: "var x = foo();" // TODO(johnlenz): Should we be checking for constants on the // left-hand-side of the assignments and handling them as EXPRESSION? return CallSiteType.VAR_DECL_SIMPLE_ASSIGNMENT; } else { ExpressionDecomposer decomposer = getDecomposer(ref.scope); switch (decomposer.canExposeExpression(callNode)) { case MOVABLE: return CallSiteType.EXPRESSION; case DECOMPOSABLE: return CallSiteType.DECOMPOSABLE_EXPRESSION; case UNDECOMPOSABLE: break; } } return CallSiteType.UNSUPPORTED; } private ExpressionDecomposer getDecomposer(Scope scope) { return compiler.createExpressionDecomposer(safeNameIdSupplier, knownConstantFunctions, scope); } /** * If required, rewrite the statement containing the call expression. * * @see ExpressionDecomposer#canExposeExpression */ void maybePrepareCall(Reference ref) { CallSiteType callSiteType = classifyCallSite(ref); callSiteType.prepare(this, ref); } /** * Inline a function which fulfills the requirements of canInlineReferenceAsStatementBlock into * the call site, replacing the parent expression. */ private Node inlineFunction(Reference ref, Node fnNode, String fnName) { Node callNode = ref.callNode; Node parent = callNode.getParent(); Node grandParent = parent.getParent(); // TODO(johnlenz): Consider storing the callSite classification in the // reference object and passing it in here. CallSiteType callSiteType = classifyCallSite(ref); checkArgument(callSiteType != CallSiteType.UNSUPPORTED); // Store the name for the result. This will be used to // replace "return expr" statements with "resultName = expr" // to replace String resultName = null; boolean needsDefaultReturnResult = true; switch (callSiteType) { case SIMPLE_ASSIGNMENT: resultName = parent.getFirstChild().getString(); removeConstantVarAnnotation(ref.scope, resultName); break; case VAR_DECL_SIMPLE_ASSIGNMENT: resultName = parent.getString(); removeConstantVarAnnotation(ref.scope, resultName); break; case SIMPLE_CALL: resultName = null; // "foo()" doesn't need a result. needsDefaultReturnResult = false; break; case EXPRESSION: throw new IllegalStateException("Movable expressions must be moved before inlining."); case DECOMPOSABLE_EXPRESSION: throw new IllegalStateException( "Decomposable expressions must be decomposed before inlining."); default: throw new IllegalStateException("Unexpected call site type."); } FunctionToBlockMutator mutator = new FunctionToBlockMutator(compiler, this.safeNameIdSupplier); boolean isCallInLoop = NodeUtil.isWithinLoop(callNode); Node newBlock = mutator.mutate( fnName, fnNode, callNode, resultName, needsDefaultReturnResult, isCallInLoop); NodeUtil.markNewScopesChanged(newBlock, compiler); // TODO(nicksantos): Create a common mutation function that // can replace either a VAR name assignment, assignment expression or // a EXPR_RESULT. switch (callSiteType) { case VAR_DECL_SIMPLE_ASSIGNMENT: // Remove the call from the name node. Node firstChild = parent.removeFirstChild(); NodeUtil.markFunctionsDeleted(firstChild, compiler); Preconditions.checkState(!parent.hasChildren()); // Add the call, after the VAR. newBlock.insertAfter(grandParent); break; case SIMPLE_ASSIGNMENT: // The assignment is now part of the inline function so // replace it completely. Preconditions.checkState(grandParent.isExprResult()); grandParent.replaceWith(newBlock); NodeUtil.markFunctionsDeleted(grandParent, compiler); break; case SIMPLE_CALL: // If nothing is looking at the result just replace the call. Preconditions.checkState(parent.isExprResult()); parent.replaceWith(newBlock); NodeUtil.markFunctionsDeleted(parent, compiler); break; default: throw new IllegalStateException("Unexpected call site type."); } return newBlock; } private static void removeConstantVarAnnotation(Scope scope, String name) { Var var = scope.getVar(name); Node nameNode = var == null ? null : var.getNameNode(); if (nameNode == null) { return; } if (nameNode.isDeclaredConstantVar()) { nameNode.setDeclaredConstantVar(false); } } /** * Checks if the given function matches the criteria for an inlinable function, and if so, adds it * to our set of inlinable functions. */ static boolean isDirectCallNodeReplacementPossible(Node fnNode) { // Only inline single-statement functions Node block = NodeUtil.getFunctionBody(fnNode); // Check if this function is suitable for direct replacement of a CALL node: // a function that consists of single return that returns an expression. if (!block.hasChildren()) { // special case empty functions. return true; } else if (block.hasOneChild()) { // Only inline functions that return something. if (block.getFirstChild().isReturn() && block.getFirstFirstChild() != null) { return true; } } return false; } enum CanInlineResult { YES, AFTER_PREPARATION, NO } /** * Determines whether a function can be inlined at a particular call site. There are several * criteria that the function and reference must hold in order for the functions to be inlined: - * It must be a simple call, or assignment, or var initialization. * *

   *    f();
   *    a = foo();
   *    var a = foo();
   * 
*/ private CanInlineResult canInlineReferenceAsStatementBlock( Reference ref, Node fnNode, ImmutableSet namesToAlias) { CallSiteType callSiteType = classifyCallSite(ref); if (callSiteType == CallSiteType.UNSUPPORTED) { return CanInlineResult.NO; } if (!allowDecomposition && (callSiteType == CallSiteType.DECOMPOSABLE_EXPRESSION || callSiteType == CallSiteType.EXPRESSION)) { return CanInlineResult.NO; } if (!callMeetsBlockInliningRequirements(ref, fnNode, namesToAlias)) { return CanInlineResult.NO; } if (callSiteType == CallSiteType.DECOMPOSABLE_EXPRESSION || callSiteType == CallSiteType.EXPRESSION) { return CanInlineResult.AFTER_PREPARATION; } else { return CanInlineResult.YES; } } /** * Returns whether or not {@code fn} includes a call to `eval` in its scope. * *

Results are cached to make subsequent calls faster. */ private boolean referencesEval(Node fn) { checkState(fn.isFunction()); @Nullable Boolean cached = this.referencesEvalCache.get(fn); if (cached != null) { return cached; } boolean result = NodeUtil.has( fn, // (n) -> n.isName() && n.getString().equals("eval"), // Match predicate (n) -> !n.isFunction() || n.equals(fn)); // Explore node predicate this.referencesEvalCache.put(fn, result); return result; } /** * Returns any inner function of {@code containerFn}. * *

If there are no inner functions, or multilple inner functions, the sentinel values {@link * #NO_FUNCTIONS}, {@link #MULTIPLE_FUNCTIONS} are returned respectively. */ private Node innerFunctionOf(Node containerFn) { checkState(containerFn.isFunction()); @Nullable Node cached = this.innerFunctionCache.get(containerFn); if (cached != null) { return cached; } ArrayList innerFns = new ArrayList<>(); NodeUtil.visitPreOrder( containerFn, (n) -> { if (n.equals(containerFn)) { return; } if (n.isFunction()) { innerFns.add(n); } }); switch (innerFns.size()) { case 0: cached = NO_FUNCTIONS; break; case 1: cached = innerFns.get(0); break; default: cached = MULTIPLE_FUNCTIONS; break; } this.innerFunctionCache.put(containerFn, cached); return cached; } /** * Determines whether a function can be inlined at a particular call site. - Don't inline if the * calling function contains an inner function and inlining would introduce new globals. */ private boolean callMeetsBlockInliningRequirements( Reference callRef, final Node calleeFn, ImmutableSet namesToAlias) { // Note: functions that contain function definitions are filtered out // in isCandidateFunction. // TODO(johnlenz): Determining if the called function contains VARs // or if the caller contains inner functions accounts for 20% of the // run-time cost of this pass. // // Note that as of 2019-10-11, "eval" and function checking are cached so this may be less // of an issue. // Don't inline functions with var declarations into a scope with inner // functions as the new vars would leak into the inner function and // cause memory leaks. boolean calleeContainsVars = NodeUtil.has( NodeUtil.getFunctionBody(calleeFn), new NodeUtil.MatchDeclaration(), new NodeUtil.MatchShallowStatement()); boolean forbidTemps = false; if (!callRef.scope.getClosestHoistScope().isGlobal()) { Node callerFn = callRef.scope.getClosestHoistScope().getRootNode().getParent(); // Don't allow any new vars into a scope that contains eval or one // that contains functions (excluding the function being inlined). if (this.referencesEval(callerFn)) { forbidTemps = true; } else if (!this.assumeMinimumCapture) { Node innerFn = this.innerFunctionOf(callerFn); boolean calleeIsOnlyInnerFn = innerFn.equals(NO_FUNCTIONS) || innerFn.equals(calleeFn); forbidTemps = !calleeIsOnlyInnerFn; } } if (calleeContainsVars && forbidTemps) { return false; } // If the caller contains functions or evals, verify we aren't adding any // additional VAR declarations because aliasing is needed. if (forbidTemps) { ImmutableMap args = functionArgumentInjector.getFunctionCallParameterMap( calleeFn, callRef.callNode, this.safeNameIdSupplier); boolean hasArgs = !args.isEmpty(); if (hasArgs) { // Limit the inlining Set allNamesToAlias = new HashSet<>(namesToAlias); functionArgumentInjector.maybeAddTempsForCallArguments( compiler, calleeFn, args, allNamesToAlias, compiler.getCodingConvention()); if (!allNamesToAlias.isEmpty()) { return false; } } } return true; } /** * Determines whether a function can be inlined at a particular call site. There are several * criteria that the function and reference must hold in order for the functions to be inlined: 1) * If a call's arguments have side effects, the corresponding argument in the function must only * be referenced once. For instance, this will not be inlined: * *

   *     function foo(a) { return a + a }
   *     x = foo(i++);
   * 
*/ private CanInlineResult canInlineReferenceDirectly( Reference ref, Node fnNode, Set namesToAlias) { if (!isDirectCallNodeReplacementPossible(fnNode)) { return CanInlineResult.NO; } // CALL NODE: [ NAME, ARG1, ARG2, ... ] Node callNode = ref.callNode; Node cArg = callNode.getSecondChild(); // Functions called via 'call' and 'apply' have a this-object as // the first parameter, but this is not part of the called function's // parameter list. if (!callNode.getFirstChild().isName()) { if (NodeUtil.isFunctionObjectCall(callNode)) { // TODO(johnlenz): Support replace this with a value. if (cArg == null || !cArg.isThis()) { return CanInlineResult.NO; } cArg = cArg.getNext(); } else { // ".apply" call should be filtered before this. checkState(!NodeUtil.isFunctionObjectApply(callNode)); } } ImmutableMap args = functionArgumentInjector.getFunctionCallParameterMap( fnNode, callNode, this.throwawayNameSupplier); boolean hasArgs = !args.isEmpty(); if (hasArgs) { // Limit the inlining Set allNamesToAlias = new HashSet<>(namesToAlias); functionArgumentInjector.maybeAddTempsForCallArguments( compiler, fnNode, args, allNamesToAlias, compiler.getCodingConvention()); if (!allNamesToAlias.isEmpty()) { return CanInlineResult.NO; } } return CanInlineResult.YES; } /** Determine if inlining the function is likely to reduce the code size. */ boolean inliningLowersCost( JSChunk fnModule, Node fnNode, Collection refs, Set namesToAlias, boolean isRemovable, boolean referencesThis) { int referenceCount = refs.size(); if (referenceCount == 0) { return true; } int referencesUsingBlockInlining = 0; boolean checkModules = isRemovable && fnModule != null; JSChunkGraph moduleGraph = compiler.getModuleGraph(); for (Reference ref : refs) { if (ref.mode == InliningMode.BLOCK) { referencesUsingBlockInlining++; } // Check if any of the references cross the module boundaries. if (checkModules && ref.module != null) { if (ref.module != fnModule && !moduleGraph.dependsOn(ref.module, fnModule)) { // Calculate the cost as if the function were non-removable, // if it still lowers the cost inline it. isRemovable = false; checkModules = false; // no need to check additional modules. } } } int referencesUsingDirectInlining = referenceCount - referencesUsingBlockInlining; // Don't bother calculating the cost of function for simple functions where // possible. // However, when inlining a complex function, even a single reference may be // larger than the original function if there are many returns (resulting // in additional assignments) or many parameters that need to be aliased // so use the cost estimating. if (referenceCount == 1 && isRemovable && referencesUsingDirectInlining == 1) { return true; } int callCost = estimateCallCost(fnNode, referencesThis); int overallCallCost = callCost * referenceCount; int costDeltaDirect = inlineCostDelta(fnNode, namesToAlias, InliningMode.DIRECT); int costDeltaBlock = inlineCostDelta(fnNode, namesToAlias, InliningMode.BLOCK); return doesLowerCost( fnNode, overallCallCost, referencesUsingDirectInlining, costDeltaDirect, referencesUsingBlockInlining, costDeltaBlock, isRemovable); } /** @return Whether inlining will lower cost. */ private static boolean doesLowerCost( Node fnNode, int callCost, int directInlines, int costDeltaDirect, int blockInlines, int costDeltaBlock, boolean removable) { // Determine the threshold value for this inequality: // inline_cost < call_cost // But solve it for the function declaration size so the size of it // is only calculated once and terminated early if possible. int fnInstanceCount = directInlines + blockInlines - (removable ? 1 : 0); // Prevent division by zero. if (fnInstanceCount == 0) { // Special case single reference function that are being block inlined: // If the cost of the inline is greater than the function definition size, // don't inline. return blockInlines <= 0 || costDeltaBlock <= 0; } int costDelta = (directInlines * -costDeltaDirect) + (blockInlines * -costDeltaBlock); int threshold = (callCost + costDelta) / fnInstanceCount; return InlineCostEstimator.getCost(fnNode, threshold + 1) <= threshold; } /** * Gets an estimate of the cost in characters of making the function call: the sum of the * identifiers and the separators. * * @param referencesThis */ private static int estimateCallCost(Node fnNode, boolean referencesThis) { Node argsNode = NodeUtil.getFunctionParameters(fnNode); int numArgs = argsNode.getChildCount(); int callCost = NAME_COST_ESTIMATE + PAREN_COST; if (numArgs > 0) { callCost += (numArgs * NAME_COST_ESTIMATE) + ((numArgs - 1) * COMMA_COST); } if (referencesThis) { // TODO(johnlenz): Update this if we start supporting inlining // other functions that reference this. // The only functions that reference this that are currently inlined // are those that are called via ".call" with an explicit "this". callCost += 5 + 5; // ".call" + "this," } return callCost; } /** @return The difference between the function definition cost and inline cost. */ private static int inlineCostDelta(Node fnNode, Set namesToAlias, InliningMode mode) { // The part of the function that is never inlined: // "function xx(xx,xx){}" (15 + (param count * 3) -1; int paramCount = NodeUtil.getFunctionParameters(fnNode).getChildCount(); int commaCount = (paramCount > 1) ? paramCount - 1 : 0; int costDeltaFunctionOverhead = 15 + commaCount + (paramCount * InlineCostEstimator.ESTIMATED_IDENTIFIER_COST); Node block = fnNode.getLastChild(); if (!block.hasChildren()) { // Assume the inline cost is zero for empty functions. return -costDeltaFunctionOverhead; } if (mode == InliningMode.DIRECT) { // The part of the function that is inlined using direct inlining: // "return " (7) return -(costDeltaFunctionOverhead + 7); } else { int aliasCount = namesToAlias.size(); // Originally, we estimated purely base on the function code size, relying // on later optimizations. But that did not produce good results, so here // we try to estimate the something closer to the actual inlined coded. // NOTE 1: Result overhead is only if there is an assignment, but // getting that information would require some refactoring. // NOTE 2: The aliasing overhead is currently an under-estimate, // as some parameters are aliased because of the parameters used. // Perhaps we should just assume all parameters will be aliased? final int inlineBlockOverhead = 4; // "X:{}" final int perReturnOverhead = 2; // "return" --> "break X" final int perReturnResultOverhead = 3; // "XX=" final int perAliasOverhead = 3; // "XX=" // TODO(johnlenz): Counting the number of returns is relatively expensive. // This information should be determined during the traversal and cached. int returnCount = NodeUtil.getNodeTypeReferenceCount( block, Token.RETURN, new NodeUtil.MatchShallowStatement()); int resultCount = (returnCount > 0) ? returnCount - 1 : 0; int baseOverhead = (returnCount > 0) ? inlineBlockOverhead : 0; int overhead = baseOverhead + returnCount * perReturnOverhead + resultCount * perReturnResultOverhead + aliasCount * perAliasOverhead; return (overhead - costDeltaFunctionOverhead); } } /** Store the names of known constants to be used when classifying call-sites in expressions. */ public void setKnownConstantFunctions(ImmutableSet knownConstantFunctions) { // This is only expected to be set once. The same set should be used // when evaluating call-sites and inlining calls. checkState(this.knownConstantFunctions.isEmpty()); this.knownConstantFunctions = knownConstantFunctions; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy