Package com.google.javascript.jscomp

Source Code of com.google.javascript.jscomp.SpecializeModule$NodeMatcher

/*
* Copyright 2010 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 com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Beginnings of an optimization to specialize the initial module at the cost of
* increasing code in later modules. This is still very experimental.
*
* High-level overview:
*
* This optimization replaces functions in the initial module with specialized
* versions that are only safe in the initial module. The original, general,
* versions of the functions are "fixed up" in later modules. This optimization
* can shrink the initial module significantly but the fixup code in later
* modules increases overall code size.
*
* Implementation approach:
*
* We take a ridiculously naive approach: remove the initial module
* from the rest of the AST, optimize it with existing optimization passes
* (recording which functions have been specialized), put it back in the AST,
* and add fixups restoring the general versions of the functions in each module
* that depends on the initial module.
*
* Since it is only safe to specialize functions that can be fixed up, we
* don't allow specialization of local functions and functions that
* are aliased.
*
* We currently run three optimizations on the isolated AST: InlineFunctions,
* DevirtualizePrototypeMethods, and RemoveUnusedPrototypeProperties.
*
* These optimizations rely on a coarse-grained name-based analysis to
* maintain safety properties and thus are likely to see some benefit when
* applied in isolation.
*
* InlineFunctions is truly specializing -- it replaces functions with
* versions that have calls to other functions inlined into them, while
* RemoveUnusedPrototypeProperties is really just removing properties that
* aren't used in the initial module and adding copies further down in the
* module graph. It would probably be more elegant to give
* CrossModuleMethodMotion permission to make copies of methods instead.
*
* There are additional passes that might benefit from being made
* specialization-aware:
*
* - OptimizeParameters
*
* - Any pass that is too slow to run over the entire AST but might
*      be acceptable on only the initial module:
*  - RemoveUnusedNames
*
*  - Also, any pass that uses the results of PureFunctionIdentifier to
*  determine when it is safe to remove code might benefit (e.g. the peephole
*  passes), since PureFunctionIdentifier relies on SimpleDefinitionFinder,
*  which would be more precise when running on only the initial module.
*
* @author dcc@google.com (Devin Coughlin)
*/
class SpecializeModule implements CompilerPass {
  private AbstractCompiler compiler;

  private Map<Node, Node> specializedInputRootsByOriginal;

  private Map<Node, OriginalFunctionInformation>
      functionInfoBySpecializedFunctionNode;

  private SpecializationState specializationState;

  private final PassFactory[] specializationPassFactories;

  public SpecializeModule(AbstractCompiler compiler,
      PassFactory ...specializationPassFactories) {
    this.compiler = compiler;
    this.specializationPassFactories = specializationPassFactories;
  }

  /**
   * Performs initial module specialization.
   *
   * The process is as follows:
   *
   * 1) Make a copy of each of the inputs in the initial root and put them
   * in a fake AST that looks like it is the whole program.
   *
   * 2) Run the specializing compiler passes over the fake initial module AST
   * until it reaches a fixed point, recording which functions are specialized
   * or removed.
   *
   * 3) Replace the original input roots with the specialized input roots
   *
   * 4) For each module that directly depends on the initial module, add
   * fixups for the specialized and removed functions. Right now we add
   * fixups for for every function that was specialzed or removed -- we could
   * be smarter about this and for each dependent module only add the functions
   * that it needs.
   *
   * 5) Add dummy variables declaring the removed function to the end of
   * the now-specialized initial module. This is needed to keep
   * {@link VarCheck} from complaining.
   */
  @Override
  public void process(Node externs, Node root) {
    JSModuleGraph moduleGraph = compiler.getModuleGraph();

    // Can't perform optimization without a module graph!
    if (moduleGraph == null) {
      return;
    }

    JSModule module = moduleGraph.getRootModule();

    Node fakeModuleRoot = copyModuleInputs(module);

    SimpleDefinitionFinder defFinder = new SimpleDefinitionFinder(compiler);

    defFinder.process(externs, fakeModuleRoot);

    SimpleFunctionAliasAnalysis initialModuleFunctionAliasAnalysis =
        new SimpleFunctionAliasAnalysis();

    initialModuleFunctionAliasAnalysis.analyze(defFinder);

    specializationState =
        new SpecializationState(initialModuleFunctionAliasAnalysis);

    do {
      specializationState.resetHasChanged();

      for (SpecializationAwareCompilerPass pass : createSpecializingPasses()) {
        pass.enableSpecialization(specializationState);
        pass.process(externs, fakeModuleRoot);
      }
    } while(specializationState.hasChanged());

    // We must always add dummy variables before replacing the orginal module.
    addDummyVarDeclarationsToInitialModule(module);
    replaceOriginalModuleInputsWithSpecialized();
    addOriginalFunctionVersionsToDependentModules(module);
  }

  /**
   * Returns a collection of new instances of specializing passes.
   */
  private Collection<SpecializationAwareCompilerPass>
      createSpecializingPasses() {

    Collection<SpecializationAwareCompilerPass> passes = Lists.newLinkedList();

    for (PassFactory passFactory : specializationPassFactories) {
      CompilerPass pass = passFactory.createInternal(compiler);

      Preconditions.checkState(pass instanceof
          SpecializationAwareCompilerPass);

      passes.add((SpecializationAwareCompilerPass) pass);
    }

    return passes;
  }

  /**
   * Creates an AST that consists solely of copies of the input roots for the
   * passed in module.
   *
   * Also records a map in {@link #functionInfoBySpecializedFunctionNode}
   * of information about the original function keyed on the copies of the
   * functions to specialized.
   */
  private Node copyModuleInputs(JSModule module) {

    specializedInputRootsByOriginal = Maps.newLinkedHashMap();

    functionInfoBySpecializedFunctionNode = Maps.newLinkedHashMap();

    Node syntheticModuleJsRoot = new Node(Token.BLOCK);
    syntheticModuleJsRoot.setIsSyntheticBlock(true);

    for (CompilerInput input : module.getInputs()) {
      Node originalInputRoot = input.getAstRoot(compiler);

      Node copiedInputRoot = originalInputRoot.cloneTree();
      copiedInputRoot.copyInformationFromForTree(originalInputRoot);

      specializedInputRootsByOriginal.put(originalInputRoot,
          copiedInputRoot);

      matchTopLevelFunctions(originalInputRoot, copiedInputRoot);

      syntheticModuleJsRoot.addChildToBack(copiedInputRoot);
    }

    // The jsRoot needs a parent (in a normal compilation this would be the
    // node that contains jsRoot and the externs).
    Node syntheticExternsAndJsRoot = new Node(Token.BLOCK);
    syntheticExternsAndJsRoot.addChildToBack(syntheticModuleJsRoot);

    return syntheticModuleJsRoot;
  }

  /**
   * Records information about original functions and creates a map from
   * the specialized functions to this information.
   *
   * This information is only recorded for global functions since non-global
   * functions cannot be inlined.
   *
   * @param original An original input root.
   * @param toBeSpecialized A copy of the input root (the copy to be
   * specialized)
   */
  private void matchTopLevelFunctions(Node original, Node toBeSpecialized) {
    new NodeMatcher() {
      @Override
      public void reportMatch(Node original, Node specialized) {
        if (NodeUtil.isFunction(original)) {
          OriginalFunctionInformation functionInfo =
              new OriginalFunctionInformation(original);

          functionInfoBySpecializedFunctionNode.put(specialized,
              functionInfo);
        }
      }

      @Override
      public boolean shouldTraverse(Node n1, Node n2) {
        return !NodeUtil.isFunction(n1);
      }
    }.match(original, toBeSpecialized);
  }

  /**
   * Replaces the original input roots of the initial module with
   * their specialized versions.
   *
   * (Since {@link JsAst} holds a pointer to original inputs roots, we actually
   * replace the all the children of the root rather than swapping the
   * root pointers).
   */
  private void replaceOriginalModuleInputsWithSpecialized() {
    for (Node original : specializedInputRootsByOriginal.keySet()) {
      Node specialized = specializedInputRootsByOriginal.get(original);

      original.removeChildren();

      List<Node> specializedChildren = Lists.newLinkedList();

      while (specialized.getFirstChild() != null) {
        original.addChildToBack(specialized.removeFirstChild());
      }
    }
  }

  /**
   * Adds dummy variable declarations for all the function declarations we've
   * removed to the end of the initial module.
   *
   * We do this to make {@link VarCheck} happy, since it requires variables to
   * be declared before they are used in the whole program AST and doesn't
   * like it when they are declared multiple times.
   *
   * TODO(dcc): Be smarter about whether we need a VAR here or not.
   */
  private void addDummyVarDeclarationsToInitialModule(JSModule module) {
    for (Node modifiedFunction :
      functionInfoBySpecializedFunctionNode.keySet()) {
     if (specializationState.getRemovedFunctions().contains(modifiedFunction)) {
       OriginalFunctionInformation originalInfo =
         functionInfoBySpecializedFunctionNode.get(modifiedFunction);

       if (originalInfo.name != null && originalInfo.originalWasDeclaration()) {
         Node block = specializationState.removedFunctionToBlock.get(
             modifiedFunction);

         // Declaring block might be null if no fix-up declarations is needed.
         // For example, InlineFunction can inline an anonymous function call or
         // anything with prototype property requires no dummy declaration
         // fix-ups afterward.
         if (block != null) {
           Node originalRoot = specializedInputRootsByOriginal.get(block);
           block.addChildrenToBack(originalInfo.generateDummyDeclaration());
         }
       }
     }
    }
  }

  /**
   * Adds a copy of the original versions of specialized/removed functions
   * to each of the dependents of module.
   *
   * Currently we add all of these functions to all dependents; it
   * would be more efficient to only add the functions that could be used.
   *
   * TODO(dcc): Only add fixup functions where needed.
   */
  private void addOriginalFunctionVersionsToDependentModules(JSModule module) {
    for (JSModule directDependent : getDirectDependents(module)) {
      CompilerInput firstInput = directDependent.getInputs().get(0);
      Node firstInputRootNode = firstInput.getAstRoot(compiler);

      // We don't iterate specializedFunctions directly because want to maintain
      // and specializedFunctions in source order, rather than
      // in the order that some optimization specialized the function.

      // So since we're adding to the front of the module each time, we
      // have to iterate in reverse source order.

      List<Node> possiblyModifiedFunctions =
        Lists.newArrayList(functionInfoBySpecializedFunctionNode.keySet());

      Collections.reverse(possiblyModifiedFunctions);

      for (Node modifiedFunction : possiblyModifiedFunctions) {
        boolean declarationWasSpecialized =
          specializationState.getSpecializedFunctions()
          .contains(modifiedFunction);

        boolean declarationWasRemoved =
            specializationState.getRemovedFunctions()
            .contains(modifiedFunction);

        if (declarationWasSpecialized || declarationWasRemoved) {
          OriginalFunctionInformation originalInfo =
               functionInfoBySpecializedFunctionNode.get(modifiedFunction);

           // Don't add unspecialized versions of anonymous functions
           if (originalInfo.name != null) {
             Node newDefinition =
               originalInfo.generateFixupDefinition();

             firstInputRootNode.addChildrenToFront(newDefinition);
           }
        }
      }
    }
  }

  /**
   * Returns a list of modules that directly depend on the given module.
   *
   * This probably deserves to be in JSModuleGraph.
   */
  public Collection<JSModule> getDirectDependents(JSModule module) {
    Set<JSModule> directDependents = Sets.newHashSet();

    for (JSModule possibleDependent :
          compiler.getModuleGraph().getAllModules()) {
      if (possibleDependent.getDependencies().contains(module)) {
        directDependents.add(possibleDependent);
      }
    }

    return directDependents;
  }

  /**
   * A simple abstract classes that takes two isomorphic ASTs and walks
   * each of them together, reporting matches to subclasses.
   *
   * This could probably be hardened and moved to NodeUtil
   */
  private abstract static class NodeMatcher {

    /**
     * Calls {@link #reportMatch(Node, Node)} for each pair of matching nodes
     * from the two ASTs.
     *
     * The two ASTs must be isomorphic. Currently no error checking is
     * performed to ensure that this is the case.
     */
    public void match(Node ast1, Node ast2) {
      // Just blunder ahead and assume that the two nodes actually match

      reportMatch(ast1, ast2);

      if (shouldTraverse(ast1, ast2)) {
        Node childOf1 = ast1.getFirstChild();
        Node childOf2 = ast2.getFirstChild();

        while (childOf1 != null) {
          match(childOf1, childOf2);
          childOf1 = childOf1.getNext();
          childOf2 = childOf2.getNext();
        }
      }

    }

    /**
     * Subclasses should override to add their own behavior when two nodes
     * are matched.
     * @param n1 A node from the AST passed as ast1 in
     * {@link #match(Node, Node)}.
     * @param n2 A node from the AST passed as ast1 in
     * {@link #match(Node, Node)}.
     */
    public abstract void reportMatch(Node n1, Node n2);

    /**
     * Subclasses should override to determine whether matching should proceed
     * under a subtree.
     */
    public boolean shouldTraverse(Node node1, Node n2) {
      return true;
    }
  }

  /**
   * A class that stores information about the original version of a
   * function that will be/was specialized or removed.
   *
   * This class stores:
   * - how the function was defined
   * - a copy of the original function
   */
  private class OriginalFunctionInformation {
    private String name;

    /**
     *  a = function() {} if true;
     *  function a() {} otherwise
     */
    private boolean isAssignFunction;

    private boolean assignHasVar;

    private Node originalFunctionCopy;

    public OriginalFunctionInformation(Node originalFunction) {
      name = NodeUtil.getFunctionName(originalFunction);

      originalFunctionCopy = originalFunction.cloneTree();
      originalFunctionCopy.copyInformationFromForTree(originalFunction);

      Node originalParent = originalFunction.getParent();

      isAssignFunction = NodeUtil.isAssign(originalParent) ||
          NodeUtil.isName(originalParent);

      assignHasVar =
          isAssignFunction && NodeUtil.isVar(originalParent.getParent());
    }

    private Node copiedOriginalFunction() {
      // Copy of a copy
      Node copy = originalFunctionCopy.cloneTree();
      copy.copyInformationFromForTree(originalFunctionCopy);

      return copy;
    }

    /**
     * Did the original function add its name to scope?
     * (If so, and specialization removes it, then we'll have to
     * add a VAR for it so VarCheck doesn't complain).
     */
    private boolean originalWasDeclaration() {
      return (!isAssignFunction) || (assignHasVar);
    }

    /**
     * Generates a definition of the original function that can be added as
     * a fixup in the modules that directly depend on the specialized module.
     *
     * <PRE>
     * The trick here is that even if the original function is declared as:
     *
     * function foo() {
     *   // stuff
     * }
     *
     * the fixup will have to be of the form
     *
     * foo = function() {
     *   // stuff
     * }
     * </PRE>
     *
     */
    private Node generateFixupDefinition() {
      Node functionCopy = copiedOriginalFunction();

      Node nameNode;

      if (isAssignFunction) {
        nameNode =
           NodeUtil.newQualifiedNameNode(
               compiler.getCodingConvention(), name, functionCopy, name);
      } else {
        // Grab the name node from the original function and make that
        // function anonymous.
        nameNode = functionCopy.getFirstChild();
        functionCopy.replaceChild(nameNode,
            NodeUtil.newName(compiler.getCodingConvention(), "", nameNode));
      }

      Node assignment = new Node(Token.ASSIGN, nameNode, functionCopy);
      assignment.copyInformationFrom(functionCopy);

      return NodeUtil.newExpr(assignment);
    }

    /**
     * Returns a new dummy var declaration for the function with no initial
     * value:
     *
     * var name;
     */
    private Node generateDummyDeclaration() {
      Node declaration = NodeUtil.newVarNode(name, null);
      declaration.copyInformationFromForTree(originalFunctionCopy);

      return declaration;
    }
  }

  /**
   * A class to hold state during SpecializeModule. An instance of this class
   * is passed to specialization-aware compiler passes -- they use it to
   * communicate with SpecializeModule.
   *
   * SpecializationAware optimizations are required to keep track of the
   * functions they remove and the functions that they modify so that the fixups
   * can be added. However, not all functions can be fixed up.
   *
   * Specialization-aware classes *must* call
   * {@link #reportSpecializedFunction} when a function is modified during
   * specialization and {@link #reportRemovedFunction} when one is removed.
   *
   * Also, when specializing, they must query {@link #canFixupFunction}
   * before modifying a function.
   *
   * This two-way communication, is the reason we don't use
   * {@link AstChangeProxy} to report code changes.
   */
  public static class SpecializationState {

    /**
     * The functions that the pass has specialized. These functions will
     * be fixed up in non-specialized modules to their more general versions.
     *
     * This field is also used to determine whether specialization is enabled.
     * If not null, specialization is enabled, otherwise it is disabled.
     */
    private Set<Node> specializedFunctions;

    /**
     * The functions that the pass has removed. These functions will be
     * redefined in non-specialized modules.
     */
    private Set<Node> removedFunctions;

    private Map<Node, Node> removedFunctionToBlock;

    private SimpleFunctionAliasAnalysis initialModuleAliasAnalysis;

    /** Will be true if any new functions have been removed or specialized since
     * {@link #resetHasChanged}.
     */
    private boolean hasChanged = false;

    public SpecializationState(SimpleFunctionAliasAnalysis
        initialModuleAliasAnalysis) {

      this.initialModuleAliasAnalysis = initialModuleAliasAnalysis;

      specializedFunctions = Sets.newLinkedHashSet();
      removedFunctions = Sets.newLinkedHashSet();
      removedFunctionToBlock = Maps.newLinkedHashMap();
    }

    /**
     * Returns true if any new functions have been reported as removed or
     * specialized since {@link #resetHasChanged()} was last called.
     */
    private boolean hasChanged() {
      return hasChanged;
    }

    private void resetHasChanged() {
      hasChanged = false;
    }

    /**
     * Returns the functions specialized by this compiler pass.
     */
    public Set<Node> getSpecializedFunctions() {
      return specializedFunctions;
    }

    /**
     * Reports that a function has been specialized.
     *
     * @param functionNode A specialized AST node with type Token.FUNCTION
     */
    public void reportSpecializedFunction(Node functionNode) {
      if (specializedFunctions.add(functionNode)) {
        hasChanged = true;
      }
    }

    /**
     * Reports that the function containing the node has been specialized.
     */
    public void reportSpecializedFunctionContainingNode(Node node) {
      Node containingFunction = containingFunction(node);

      if (containingFunction != null) {
        reportSpecializedFunction(containingFunction);
      }
    }

    /**
     * The functions removed by this compiler pass.
     */
    public Set<Node> getRemovedFunctions() {
      return removedFunctions;
    }

    /**
     * Reports that a function has been removed.
     *
     * @param functionNode A removed AST node with type Token.FUNCTION
     * @param declaringBlock If the function declaration puts a variable in the
     *    scope, we need to have a VAR statement in the scope where the
     *    function is declared. Null if the function does not put a name
     *    in the scope.
     */
    public void reportRemovedFunction(Node functionNode, Node declaringBlock) {
      // Depends when we were notified, functionNode.getParent might or might
      // not be null. We are going to force the user to tell us the parent
      // instead.
      if (removedFunctions.add(functionNode)) {
        hasChanged = true;
        removedFunctionToBlock.put(functionNode, declaringBlock);
      }
    }

    /**
     * Returns true if the function can be fixed up (that is, if it can be
     * safely removed or specialized).
     *
     * <p>In order to be safely fixed up, a function must be:
     * <PRE>
     * - in the global scope
     * - not aliased in the initial module
     * - of one of the following forms:
     *    function f() {}
     *    var f = function() {}
     *    f = function(){}
     *    var ns = {}; ns.f = function() {}
     *    SomeClass.prototype.foo = function() {};
     * </PRE>
     *
     * <p>Anonymous functions cannot be safely fixed up, nor can functions
     * that have been aliased.
     *
     * <p>Some functions declared as object literals could be safely fixed up,
     * however we do not currently support this.
     */
    public boolean canFixupFunction(Node functionNode) {
      Preconditions.checkArgument(NodeUtil.isFunction(functionNode));

      if (!nodeIsInGlobalScope(functionNode) ||
          initialModuleAliasAnalysis.isAliased(functionNode)) {
        return false;
      }

      if (NodeUtil.isStatement(functionNode)) {
        // function F() {}
        return true;
      }

      Node parent = functionNode.getParent();
      Node gramps = parent.getParent();

      if (NodeUtil.isName(parent) && NodeUtil.isVar(gramps)) {
        // var f = function() {}
        return true;
      }

      if (NodeUtil.isExprAssign(gramps)
          && parent.getChildAtIndex(1) == functionNode) {
        // f = function() {}
        // ns.f = function() {}
        return true;
      }

      return false;
    }

    /**
     * Returns true if the function containing n can be fixed up.
     * Also returns true if n is in the global scope -- since it is always safe
     * to specialize the global scope.
     */
    public boolean canFixupSpecializedFunctionContainingNode(Node n) {
      Node containingFunction = containingFunction(n);
      if (containingFunction != null) {
        return canFixupFunction(containingFunction);
      } else {
        // Always safe to specialize the global scope
        return true;
      }
    }

    /**
     * Returns true if a node is in the global scope; false otherwise.
     */
    private boolean nodeIsInGlobalScope(Node node) {
      return containingFunction(node) == null;
    }

    /**
     * Returns the function containing the node, or null if none exists.
     */
    private Node containingFunction(Node node) {
      for (Node ancestor : node.getAncestors()) {
        if (NodeUtil.isFunction(ancestor)) {
          return ancestor;
        }
      }

      return null;
    }
  }
}
TOP

Related Classes of com.google.javascript.jscomp.SpecializeModule$NodeMatcher

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.