RewriteAsyncFunctions.java

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

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

import com.google.common.base.Optional;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * Converts async functions to valid ES6 generator functions code.
 *
 * <p>This pass must run before the passes that transpile let declarations, arrow functions, and
 * generators.
 *
 * <p>An async function, foo(a, b), will be rewritten as:
 *
 * <pre> {@code
 * function foo(a, b) {
 *   let $jscomp$async$this = this;
 *   let $jscomp$async$arguments = arguments;
 *   let $jscomp$async$super$get$x = () => super.x;
 *   function* $jscomp$async$generator() {
 *     // original body of foo() with:
 *     // - await (x) replaced with yield (x)
 *     // - arguments replaced with $jscomp$async$arguments
 *     // - this replaced with $jscomp$async$this
 *     // - super.x replaced with $jscomp$async$super$get$x()
 *     // - super.x(5) replaced with $jscomp$async$super$get$x().call($jscomp$async$this, 5)
 *   }
 *   return $jscomp.executeAsyncGenerator($jscomp$async$generator());
 * }}</pre>
 */
public final class RewriteAsyncFunctions implements NodeTraversal.Callback, HotSwapCompilerPass {

  private static final String ASYNC_GENERATOR_NAME = "$jscomp$async$generator";
  private static final String ASYNC_ARGUMENTS = "$jscomp$async$arguments";
  private static final String ASYNC_THIS = "$jscomp$async$this";
  private static final String ASYNC_SUPER_PROP_GETTER_PREFIX = "$jscomp$async$super$get$";


  /**
   * Keeps track of whether we're examining nodes within an async function & what changes are needed
   * for the function currently in context.
   */
  private static final class LexicalContext {
    final Optional<Node> function; // absent for top level
    final LexicalContext thisAndArgumentsContext;
    final Set<String> replacedSuperProperties = new LinkedHashSet<>();
    boolean mustAddAsyncThisVariable = false;
    boolean mustAddAsyncArgumentsVariable = false;

    /** Creates root-level context. */
    LexicalContext() {
      this.function = Optional.absent();
      this.thisAndArgumentsContext = this;
    }

    LexicalContext(LexicalContext outer, Node function) {
      this.function = Optional.of(function);
      // An arrow function shares 'this' and 'arguments' with its outer scope.
      this.thisAndArgumentsContext =
          function.isArrowFunction() ? outer.thisAndArgumentsContext : this;
    }

    boolean isAsyncContext() {
      return function.isPresent() && function.get().isAsyncFunction();
    }

    boolean mustReplaceThisAndArguments() {
      return isAsyncContext() || thisAndArgumentsContext.isAsyncContext();
    }

    LexicalContext getAsyncThisAndArgumentsContext() {
      if (thisAndArgumentsContext.isAsyncContext()) {
        return thisAndArgumentsContext;
      }
      // The current context is an async arrow function within a non-async function,
      // so it must define its own replacement variables.
      checkState(isAsyncContext());
      return this;
    }

    void recordAsyncThisReplacementWasDone() {
      getAsyncThisAndArgumentsContext().mustAddAsyncThisVariable = true;
    }

    void recordAsyncSuperReplacementWasDone(String superFunctionName) {
      getAsyncThisAndArgumentsContext().replacedSuperProperties.add(superFunctionName);
    }

    void recordAsyncArgumentsReplacementWasDone() {
      getAsyncThisAndArgumentsContext().mustAddAsyncArgumentsVariable = true;
    }
  }

  private final Deque<LexicalContext> contextStack;
  private final AbstractCompiler compiler;
  private static final FeatureSet transpiledFeatures =
      FeatureSet.BARE_MINIMUM.with(Feature.ASYNC_FUNCTIONS);

  public RewriteAsyncFunctions(AbstractCompiler compiler) {
    checkNotNull(compiler);
    this.compiler = compiler;
    this.contextStack = new ArrayDeque<>();
    this.contextStack.addFirst(new LexicalContext());
  }

  @Override
  public void process(Node externs, Node root) {
    TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, this);
    TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, this);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, transpiledFeatures, this);
  }

  @Override
  public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
    if (n.isFunction()) {
      contextStack.addFirst(new LexicalContext(contextStack.getFirst(), n));
      if (n.isAsyncFunction()) {
        compiler.ensureLibraryInjected("es6/execute_async_generator", /* force */ false);
      }
    }
    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    LexicalContext context = contextStack.getFirst();

    if (n.isFunction()) {
      checkState(
          context.function.isPresent() && context.function.get() == n,
          "unexpected function context:\nexpected: %s\nactual: %s",
          n,
          context.function);
      contextStack.removeFirst();
    }
    switch (n.getToken()) {
      case FUNCTION:
        if (context.isAsyncContext()) {
          convertAsyncFunction(context);
        }
        break;

      case NAME:
        if (context.mustReplaceThisAndArguments() && n.matchesQualifiedName("arguments")) {
          n.setString(ASYNC_ARGUMENTS);
          context.recordAsyncArgumentsReplacementWasDone();
          compiler.reportChangeToChangeScope(context.function.get());
        }
        break;

      case THIS:
        if (context.mustReplaceThisAndArguments()) {
          parent.replaceChild(n, IR.name(ASYNC_THIS).useSourceInfoIfMissingFrom(n));
          context.recordAsyncThisReplacementWasDone();
          compiler.reportChangeToChangeScope(context.function.get());
        }
        break;

      case SUPER:
        if (context.mustReplaceThisAndArguments()) {
          if (!parent.isGetProp()) {
            compiler.report(
                JSError.make(parent, Es6ToEs3Util.CANNOT_CONVERT_YET, "super expression"));
          }

          Node medhodName = n.getNext();
          String superPropertyName = ASYNC_SUPER_PROP_GETTER_PREFIX + medhodName.getString();

          // super.x   =>   $super$get$x()
          Node getPropReplacement = NodeUtil.newCallNode(IR.name(superPropertyName));
          Node grandparent = parent.getParent();
          if (grandparent.isCall() && grandparent.getFirstChild() == parent) {
            // $super$get$x()(...)   =>   $super$get$x().call($this, ...)
            getPropReplacement = IR.getprop(getPropReplacement, IR.string("call"));
            grandparent.addChildAfter(IR.name(ASYNC_THIS), parent);
            context.recordAsyncThisReplacementWasDone();
          }
          getPropReplacement.useSourceInfoFrom(parent);
          grandparent.replaceChild(parent, getPropReplacement);
          context.recordAsyncSuperReplacementWasDone(medhodName.getString());
          compiler.reportChangeToChangeScope(context.function.get());
        }
        break;

      case AWAIT:
        checkState(context.isAsyncContext(), "await found within non-async function body");
        checkState(n.hasOneChild(), "await should have 1 operand, but has %s", n.getChildCount());
        // Awaits become yields in the converted async function's inner generator function.
        parent.replaceChild(n, IR.yield(n.removeFirstChild()).useSourceInfoIfMissingFrom(n));
        break;

      default:
        break;
    }
  }

  private void convertAsyncFunction(LexicalContext functionContext) {
    Node originalFunction = functionContext.function.get();
    originalFunction.setIsAsyncFunction(false);
    Node originalBody = originalFunction.getLastChild();
    Node newBody = IR.block().useSourceInfoIfMissingFrom(originalBody);
    originalFunction.replaceChild(originalBody, newBody);

    if (functionContext.mustAddAsyncThisVariable) {
      // const this$ = this;
      newBody.addChildToBack(IR.constNode(IR.name(ASYNC_THIS), IR.thisNode()));
    }
    if (functionContext.mustAddAsyncArgumentsVariable) {
      // const arguments$ = arguments;
      newBody.addChildToBack(IR.constNode(IR.name(ASYNC_ARGUMENTS), IR.name("arguments")));
    }
    for (String replacedMethodName : functionContext.replacedSuperProperties) {
      // const super$get$x = () => super.x;
      Node arrowFunction = IR.arrowFunction(
          IR.name(""), IR.paramList(), IR.getprop(IR.superNode(), IR.string(replacedMethodName)));
      compiler.reportChangeToChangeScope(arrowFunction);

      String superReplacementName = ASYNC_SUPER_PROP_GETTER_PREFIX + replacedMethodName;
      newBody.addChildToBack(IR.constNode(IR.name(superReplacementName), arrowFunction));
    }

    // Normalize arrow function short body to block body
    if (!originalBody.isNormalBlock()) {
      originalBody = IR.block(IR.returnNode(originalBody)).useSourceInfoFromForTree(originalBody);
    }
    // NOTE: visit() will already have made appropriate replacements in originalBody so it may
    // be used as the generator function body.
    Node newFunctionName = IR.name(ASYNC_GENERATOR_NAME);
    Node originalName = originalFunction.getFirstChild();
    // Use the source info from the function name. Without this line, we would use the source info
    // from originalBody for the name node near the end of this method.
    newFunctionName.useSourceInfoIfMissingFromForTree(originalName);
    Node generatorFunction = IR.function(newFunctionName, IR.paramList(), originalBody);
    compiler.reportChangeToChangeScope(generatorFunction);
    generatorFunction.setIsGeneratorFunction(true);
    // function* $jscomp$async$generator() { ... }
    newBody.addChildToBack(generatorFunction);

    // return $jscomp.executeAsyncGenerator($jscomp$async$generator());
    Node executeAsyncGenerator = IR.getprop(IR.name("$jscomp"), IR.string("executeAsyncGenerator"));
    newBody.addChildToBack(
        IR.returnNode(
            IR.call(executeAsyncGenerator, NodeUtil.newCallNode(IR.name(ASYNC_GENERATOR_NAME)))));

    newBody.useSourceInfoIfMissingFromForTree(originalBody);
    compiler.reportChangeToEnclosingScope(newBody);
  }
}