Es6RewriteGenerators.java

/*
 * Copyright 2014 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 static com.google.javascript.jscomp.Es6ToEs3Util.createType;
import static com.google.javascript.jscomp.Es6ToEs3Util.makeIterator;
import static com.google.javascript.jscomp.Es6ToEs3Util.withType;

import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.AbstractCompiler.MostRecentTypechecker;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.FunctionTypeI;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.ObjectTypeI;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TypeI;
import com.google.javascript.rhino.TypeIRegistry;
import com.google.javascript.rhino.jstype.JSTypeNative;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Converts ES6 generator functions to valid ES3 code. This pass runs after all ES6 features
 * except for yield and generators have been transpiled.
 *
 * @author mattloring@google.com (Matthew Loring)
 */
public final class Es6RewriteGenerators
    extends NodeTraversal.AbstractPostOrderCallback implements HotSwapCompilerPass {

  static final String GENERATOR_PRELOAD_FUNCTION_NAME = "$jscomp$generator$function$name";

  // Name of the variable that holds the state at which the generator
  // should resume execution after a call to yield or return.
  // The beginning state is 0 and the end state is -1.
  private static final String GENERATOR_STATE = "$jscomp$generator$state";
  private static final String GENERATOR_DO_WHILE_INITIAL = "$jscomp$generator$first$do";
  private static final String GENERATOR_YIELD_ALL_NAME = "$jscomp$generator$yield$all";
  private static final String GENERATOR_YIELD_ALL_ENTRY = "$jscomp$generator$yield$entry";
  private static final String GENERATOR_ARGUMENTS = "$jscomp$generator$arguments";
  private static final String GENERATOR_THIS = "$jscomp$generator$this";
  private static final String GENERATOR_ACTION_ARG = "$jscomp$generator$action$arg";
  private static final double GENERATOR_ACTION_NEXT = 0;
  private static final double GENERATOR_ACTION_THROW = 1;
  private static final String GENERATOR_NEXT_ARG = "$jscomp$generator$next$arg";
  private static final String GENERATOR_THROW_ARG = "$jscomp$generator$throw$arg";
  private static final String GENERATOR_SWITCH_ENTERED = "$jscomp$generator$switch$entered";
  private static final String GENERATOR_SWITCH_VAL = "$jscomp$generator$switch$val";
  private static final String GENERATOR_FINALLY_JUMP = "$jscomp$generator$finally";
  private static final String GENERATOR_ERROR = "$jscomp$generator$global$error";
  private static final String GENERATOR_FOR_IN_ARRAY = "$jscomp$generator$forin$array";
  private static final String GENERATOR_FOR_IN_VAR = "$jscomp$generator$forin$var";
  private static final String GENERATOR_FOR_IN_ITER = "$jscomp$generator$forin$iter";
  private static final String GENERATOR_LOOP_GUARD = "$jscomp$generator$loop$guard";

  private final AbstractCompiler compiler;
  private static final FeatureSet transpiledFeatures =
      FeatureSet.BARE_MINIMUM.with(Feature.GENERATORS);

  // Maintains a stack of numbers which identify the cases which mark the end of loops. These
  // are used to manage jump destinations for break and continue statements.
  private final List<LoopContext> currentLoopContext;

  private final List<ExceptionContext> currentExceptionContext;

  private static int generatorCaseCount;

  private final Supplier<String> generatorCounter;

  // Current case statement onto which translated statements from the
  // body of a generator will be appended.
  private Node enclosingBlock;

  // Destination for vars defined in the body of a generator.
  private Node hoistRoot;

  // Body of the generator function currently being translated.
  private Node originalGeneratorBody;

  // Current statement being translated.
  private Node currentStatement;

  private boolean hasTranslatedTry;

  // Whether we should preserve type information during transpilation.
  private final boolean addTypes;

  private final TypeIRegistry registry;

  private final TypeI unknownType;
  private final TypeI undefinedType;
  private final TypeI stringType;
  private final TypeI booleanType;
  private final TypeI falseType;
  private final TypeI trueType;
  private final TypeI numberType;

  public Es6RewriteGenerators(AbstractCompiler compiler) {
    checkNotNull(compiler);
    this.compiler = compiler;
    this.currentLoopContext = new ArrayList<>();
    this.currentExceptionContext = new ArrayList<>();
    generatorCounter = compiler.getUniqueNameIdSupplier();
    this.addTypes = MostRecentTypechecker.NTI.equals(compiler.getMostRecentTypechecker());
    this.registry = compiler.getTypeIRegistry();
    this.unknownType = createType(addTypes, registry, JSTypeNative.UNKNOWN_TYPE);
    this.undefinedType = createType(addTypes, registry, JSTypeNative.VOID_TYPE);
    this.stringType = createType(addTypes, registry, JSTypeNative.STRING_TYPE);
    this.booleanType = createType(addTypes, registry, JSTypeNative.BOOLEAN_TYPE);
    this.falseType = createType(addTypes, registry, JSTypeNative.FALSE_TYPE);
    this.trueType = createType(addTypes, registry, JSTypeNative.TRUE_TYPE);
    this.numberType = createType(addTypes, registry, JSTypeNative.NUMBER_TYPE);
  }

  @Override
  public void process(Node externs, Node root) {
    // Report change only if the generator function is preloaded. See #cleanUpGeneratorSkeleton.
    boolean reportChange = getPreloadedGeneratorFunc(compiler.getJsRoot()) != null;
    TranspilationPasses.processTranspile(
        compiler, root, transpiledFeatures, new DecomposeYields(compiler), this);
    cleanUpGeneratorSkeleton(reportChange);
  }

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

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case FUNCTION:
        if (n.isGeneratorFunction()) {
          generatorCaseCount = 0;
          visitGenerator(n, parent);
        }
        break;
      case NAME:
        Node enclosing = NodeUtil.getEnclosingFunction(n);
        if (enclosing != null
            && enclosing.isGeneratorFunction()
            && n.matchesQualifiedName("arguments")) {
          n.setString(GENERATOR_ARGUMENTS);
        }
        break;
      case THIS:
        enclosing = NodeUtil.getEnclosingFunction(n);
        if (enclosing != null && enclosing.isGeneratorFunction()) {
          n.replaceWith(withType(IR.name(GENERATOR_THIS), n.getTypeI()));
        }
        break;
      case YIELD:
        if (n.isYieldAll()) {
          visitYieldAll(t, n, parent);
        } else if (!parent.isExprResult()) {
          visitYieldExpr(t, n, parent);
        } else {
          visitYieldThrows(t, parent, parent.getParent());
        }
        break;
      default:
        break;
    }
  }

  private void visitYieldThrows(NodeTraversal t, Node n, Node parent) {
    Node ifThrows =
        IR.ifNode(
            withBooleanType(
                IR.eq(
                    withNumberType(IR.name(GENERATOR_ACTION_ARG)),
                    withNumberType(IR.number(GENERATOR_ACTION_THROW)))),
            IR.block(IR.throwNode(withUnknownType(IR.name(GENERATOR_THROW_ARG)))));
    parent.addChildAfter(ifThrows, n);
    t.reportCodeChange();
  }

  /**
   * Translates expressions using the new yield-for syntax.
   *
   * <p>Sample translation:
   *
   * <pre>
   * var i = yield * gen();
   * </pre>
   *
   * <p>Is rewritten to:
   *
   * <pre>
   * var $jscomp$generator$yield$all = gen();
   * var $jscomp$generator$yield$entry;
   * while (!($jscomp$generator$yield$entry =
   *     $jscomp$generator$yield$all.next($jscomp$generator$next$arg)).done) {
   *   yield $jscomp$generator$yield$entry.value;
   * }
   * var i = $jscomp$generator$yield$entry.value;
   * </pre>
   */
  private void visitYieldAll(NodeTraversal t, Node n, Node parent) {
    ObjectTypeI yieldAllType = null;
    TypeI typeParam = unknownType;
    if (addTypes) {
      yieldAllType = n.getFirstChild().getTypeI().autobox().toMaybeObjectType();
      typeParam = yieldAllType.getTemplateTypes().get(0);
    }
    TypeI iteratorType = createGenericType(JSTypeNative.ITERATOR_TYPE, typeParam);
    TypeI iteratorNextType =
        addTypes ? iteratorType.toMaybeObjectType().getPropertyType("next") : null;
    TypeI iIterableResultType = createGenericType(JSTypeNative.I_ITERABLE_RESULT_TYPE, typeParam);
    TypeI iIterableResultDoneType =
        addTypes ? iIterableResultType.toMaybeObjectType().getPropertyType("done") : null;
    TypeI iIterableResultValueType =
        addTypes ? iIterableResultType.toMaybeObjectType().getPropertyType("value") : null;

    Node enclosingStatement = NodeUtil.getEnclosingStatement(n);
    Node iterator = makeIterator(compiler, n.removeFirstChild());
    if (addTypes) {
      TypeI jscompType = t.getScope().getVar("$jscomp").getNode().getTypeI();
      TypeI makeIteratorType = jscompType.toMaybeObjectType().getPropertyType("makeIterator");
      iterator.getFirstChild().setTypeI(makeIteratorType);
      iterator.getFirstFirstChild().setTypeI(jscompType);
    }
    Node generator =
        IR.var(
            withType(IR.name(GENERATOR_YIELD_ALL_NAME), iteratorType),
            withType(iterator, iteratorType));
    Node entryDecl = IR.var(withType(IR.name(GENERATOR_YIELD_ALL_ENTRY), iIterableResultType));
    Node assignIterResult =
        withType(
            IR.assign(
                withType(IR.name(GENERATOR_YIELD_ALL_ENTRY), iIterableResultType),
                withType(
                    IR.call(
                        withType(
                            IR.getprop(
                                withType(IR.name(GENERATOR_YIELD_ALL_NAME), iteratorType),
                                withStringType(IR.string("next"))),
                            iteratorNextType),
                        withUnknownType(IR.name(GENERATOR_NEXT_ARG))),
                    iIterableResultType)),
            iIterableResultType);
    Node loopCondition =
        withBooleanType(
            IR.not(
                withType(
                    IR.getprop(assignIterResult, withStringType(IR.string("done"))),
                    iIterableResultDoneType)));
    Node elemValue =
        withType(
            IR.getprop(
                withType(IR.name(GENERATOR_YIELD_ALL_ENTRY), iIterableResultType),
                withStringType(IR.string("value"))),
            iIterableResultValueType);
    Node yieldStatement = IR.exprResult(withUnknownType(IR.yield(elemValue.cloneTree())));
    Node loop = IR.whileNode(loopCondition, IR.block(yieldStatement));

    enclosingStatement.getParent().addChildBefore(generator, enclosingStatement);
    enclosingStatement.getParent().addChildBefore(entryDecl, enclosingStatement);
    enclosingStatement.getParent().addChildBefore(loop, enclosingStatement);
    if (parent.isExprResult()) {
      parent.detach();
    } else {
      parent.replaceChild(n, elemValue);
    }

    visitYieldThrows(t, yieldStatement, yieldStatement.getParent());
    t.reportCodeChange();
  }

  private void visitYieldExpr(NodeTraversal t, Node n, Node parent) {
    Node enclosingStatement = NodeUtil.getEnclosingStatement(n);
    Node yieldStatement =
        IR.exprResult(
            n.hasChildren()
                ? withType(IR.yield(n.removeFirstChild()), n.getTypeI())
                : withType(IR.yield(), n.getTypeI()));
    Node yieldResult = withUnknownType(IR.name(GENERATOR_NEXT_ARG + generatorCounter.get()));
    Node yieldResultDecl =
        IR.var(yieldResult.cloneTree(), withUnknownType(IR.name(GENERATOR_NEXT_ARG)));

    parent.replaceChild(n, yieldResult);
    enclosingStatement.getParent().addChildBefore(yieldStatement, enclosingStatement);
    enclosingStatement.getParent().addChildBefore(yieldResultDecl, enclosingStatement);

    visitYieldThrows(t, yieldStatement, yieldStatement.getParent());
    t.reportCodeChange();
  }

  private void visitGenerator(Node n, Node parent) {
    Es6ToEs3Util.preloadEs6Symbol(compiler);
    hasTranslatedTry = false;
    Node genBlock = preloadGeneratorSkeleton(compiler, false).getLastChild().cloneTree();
    generatorCaseCount++;

    originalGeneratorBody = n.getLastChild();
    n.replaceChild(originalGeneratorBody, genBlock);
    NodeUtil.markNewScopesChanged(genBlock, compiler);
    n.setIsGeneratorFunction(false);

    TypeI generatorFuncType = n.getTypeI();
    TypeI generatorReturnType =
        addTypes ? generatorFuncType.toMaybeFunctionType().getReturnType() : null;
    TypeI yieldType = unknownType;
    if (addTypes) {
      if (generatorReturnType.isGenericObjectType()) {
        yieldType = generatorReturnType.autobox().toMaybeObjectType().getTemplateTypes().get(0);
      }
      addTypesToGeneratorSkeleton(genBlock, yieldType);
    }

    // TODO(mattloring): remove this suppression once we can optimize the switch statement to
    // remove unused cases.
    JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
    // TODO(mattloring): copy existing suppressions.
    builder.recordSuppressions(ImmutableSet.of("uselessCode"));
    JSDocInfo info = builder.build();
    n.setJSDocInfo(info);

    // Set state to the default after the body of the function has completed.
    originalGeneratorBody.addChildToBack(
        IR.exprResult(
            withNumberType(
                IR.assign(
                    withNumberType(IR.name(GENERATOR_STATE)), withNumberType(IR.number(-1))))));

    enclosingBlock = getUnique(genBlock, Token.CASE).getLastChild();
    hoistRoot = genBlock.getFirstChild();

    if (NodeUtil.isNameReferenced(originalGeneratorBody, GENERATOR_ARGUMENTS)) {
      hoistRoot
          .getParent()
          .addChildAfter(
              IR.var(
                  withUnknownType(IR.name(GENERATOR_ARGUMENTS)),
                  withUnknownType(IR.name("arguments"))),
              hoistRoot);
    }
    if (NodeUtil.isNameReferenced(originalGeneratorBody, GENERATOR_THIS)) {
      hoistRoot
          .getParent()
          .addChildAfter(
              IR.var(withUnknownType(IR.name(GENERATOR_THIS)), withUnknownType(IR.thisNode())),
              hoistRoot);
    }

    while (originalGeneratorBody.hasChildren()) {
      currentStatement = originalGeneratorBody.removeFirstChild();
      boolean advanceCase = translateStatementInOriginalBody();

      if (advanceCase) {
        int caseNumber;
        if (currentStatement.isGeneratorMarker()) {
          caseNumber = (int) currentStatement.getFirstChild().getDouble();
        } else {
          caseNumber = generatorCaseCount;
          generatorCaseCount++;
        }
        Node oldCase = enclosingBlock.getParent();
        Node newCase =
            withBooleanType(IR.caseNode(withNumberType(IR.number(caseNumber)), IR.block()));
        enclosingBlock = newCase.getLastChild();
        if (oldCase.isTry()) {
          oldCase = oldCase.getGrandparent();
          if (!currentExceptionContext.isEmpty()) {
            Node newTry =
                IR.tryCatch(IR.block(), currentExceptionContext.get(0).catchBlock.cloneTree());
            newCase.getLastChild().addChildToBack(newTry);
            enclosingBlock = newCase.getLastChild().getLastChild().getFirstChild();
          }
        }
        oldCase.getParent().addChildAfter(newCase, oldCase);
      }
    }

    parent.useSourceInfoIfMissingFromForTree(parent);
    compiler.reportChangeToEnclosingScope(genBlock);
  }

  /** Returns {@code true} if a new case node should be added */
  private boolean translateStatementInOriginalBody() {
    if (currentStatement.isVar()) {
      visitVar();
      return false;
    } else if (currentStatement.isGeneratorMarker()) {
      visitGeneratorMarker();
      return true;
    } else if (currentStatement.isFunction()) {
      visitFunctionStatement();
      return false;
    } else if (currentStatement.isNormalBlock()) {
      visitBlock();
      return false;
    } else if (controlCanExit(currentStatement)) {
      switch (currentStatement.getToken()) {
        case WHILE:
        case DO:
        case FOR:
          visitLoop(null);
          return false;
        case FOR_IN:
          visitForIn();
          return false;
        case LABEL:
          visitLabel();
          return false;
        case SWITCH:
          visitSwitch();
          return false;
        case IF:
          if (!currentStatement.isGeneratorSafe()) {
            visitIf();
            return false;
          }
          break;
        case TRY:
          visitTry();
          return false;
        case EXPR_RESULT:
          if (currentStatement.getFirstChild().isYield()) {
            visitYieldExprResult();
            return true;
          }
          break;
        case RETURN:
          visitReturn();
          return false;
        case CONTINUE:
          visitContinue();
          return false;
        case BREAK:
          if (!currentStatement.isGeneratorSafe()) {
            visitBreak();
            return false;
          }
          break;
        case THROW:
          visitThrow();
          return false;
        default:
          // We never want to copy over an untranslated statement for which control exits.
          throw new RuntimeException(
              "Untranslatable control-exiting statement in generator function: "
                  + currentStatement.getToken());
      }
    }

    // In the default case, add the statement to the current case block unchanged.
    enclosingBlock.addChildToBack(currentStatement);
    return false;
  }

  private void visitFunctionStatement() {
    hoistRoot.getParent().addChildAfter(currentStatement, hoistRoot);
  }

  private void visitTry() {
    Node tryBody = currentStatement.getFirstChild();
    Node caughtError;
    Node catchBody;
    Node catchBlock = tryBody.getNext();
    if (catchBlock.hasChildren()) {
      // There is a catch block
      caughtError = catchBlock.getFirstChild().removeFirstChild();
      catchBody = catchBlock.getFirstChild().removeFirstChild();
    } else {
      caughtError = withUnknownType(IR.name(GENERATOR_ERROR + "temp"));
      catchBody = IR.block(IR.throwNode(caughtError.cloneTree()));
      catchBody.getFirstChild().setGeneratorSafe(true);
    }
    Node finallyBody = catchBlock.getNext();
    int catchStartState = generatorCaseCount++;
    Node catchStart = makeGeneratorMarker(catchStartState);

    Node errorNameGenerated =
        withUnknownType(IR.name("$jscomp$generator$" + caughtError.getString()));

    originalGeneratorBody.addChildToFront(catchStart);
    originalGeneratorBody.addChildAfter(catchBody, catchStart);

    Node assignError =
        withUnknownType(
            IR.assign(withUnknownType(IR.name(GENERATOR_ERROR)), errorNameGenerated.cloneTree()));
    Node newCatchBody =
        IR.block(IR.exprResult(assignError), createStateUpdate(catchStartState), createSafeBreak());
    Node newCatch = IR.catchNode(errorNameGenerated, newCatchBody);

    currentExceptionContext.add(0, new ExceptionContext(catchStartState, newCatch));

    if (finallyBody != null) {
      Node finallyName = withNumberType(IR.name(GENERATOR_FINALLY_JUMP + generatorCounter.get()));
      int finallyStartState = generatorCaseCount++;
      Node finallyStart = makeGeneratorMarker(finallyStartState);
      int finallyEndState = generatorCaseCount++;
      Node finallyEnd = makeGeneratorMarker(finallyEndState);

      NodeTraversal.traverseEs6(
          compiler, tryBody, new ControlExitsCheck(finallyName, finallyStartState));
      NodeTraversal.traverseEs6(
          compiler, catchBody, new ControlExitsCheck(finallyName, finallyStartState));
      originalGeneratorBody.addChildToFront(tryBody.detach());

      originalGeneratorBody.addChildAfter(finallyStart, catchBody);
      originalGeneratorBody.addChildAfter(finallyBody.detach(), finallyStart);
      originalGeneratorBody.addChildAfter(finallyEnd, finallyBody);
      originalGeneratorBody.addChildToFront(IR.var(finallyName.cloneTree()));

      finallyBody.addChildToBack(
          IR.exprResult(
              withNumberType(
                  IR.assign(withNumberType(IR.name(GENERATOR_STATE)), finallyName.cloneTree()))));
      finallyBody.addChildToBack(createSafeBreak());
      tryBody.addChildToBack(
          IR.exprResult(
              withNumberType(
                  IR.assign(finallyName.cloneTree(), withNumberType(IR.number(finallyEndState))))));
      tryBody.addChildToBack(createStateUpdate(finallyStartState));
      tryBody.addChildToBack(createSafeBreak());
      catchBody.addChildToBack(
          IR.exprResult(
              withNumberType(
                  IR.assign(finallyName.cloneTree(), withNumberType(IR.number(finallyEndState))))));
    } else {
      int catchEndState = generatorCaseCount++;
      Node catchEnd = makeGeneratorMarker(catchEndState);
      originalGeneratorBody.addChildAfter(catchEnd, catchBody);
      tryBody.addChildToBack(createStateUpdate(catchEndState));
      tryBody.addChildToBack(createSafeBreak());
      originalGeneratorBody.addChildToFront(tryBody.detach());
    }

    catchBody.addChildToFront(IR.var(caughtError, withUnknownType(IR.name(GENERATOR_ERROR))));

    if (enclosingBlock.getParent().isTry()) {
      enclosingBlock = enclosingBlock.getGrandparent();
    }

    enclosingBlock.addChildToBack(IR.tryCatch(IR.block(), newCatch));
    enclosingBlock = enclosingBlock.getLastChild().getFirstChild();
    if (!hasTranslatedTry) {
      hasTranslatedTry = true;
      hoistRoot
          .getParent()
          .addChildAfter(IR.var(withUnknownType(IR.name(GENERATOR_ERROR))), hoistRoot);
    }
  }

  private void visitContinue() {
    checkState(currentLoopContext.get(0).continueCase != -1);
    int continueCase;
    if (currentStatement.hasChildren()) {
      continueCase = getLoopContext(currentStatement.removeFirstChild().getString()).continueCase;
    } else {
      continueCase = currentLoopContext.get(0).continueCase;
    }
    enclosingBlock.addChildToBack(createStateUpdate(continueCase));
    enclosingBlock.addChildToBack(createSafeBreak());
  }

  private void visitThrow() {
    enclosingBlock.addChildToBack(createStateUpdate(-1));
    enclosingBlock.addChildToBack(currentStatement);
  }

  private void visitBreak() {
    int breakCase;
    if (currentStatement.hasChildren()) {
      LoopContext loop = getLoopContext(currentStatement.removeFirstChild().getString());
      if (loop == null) {
        compiler.report(
            JSError.make(
                currentStatement,
                Es6ToEs3Util.CANNOT_CONVERT_YET,
                "Breaking to a label that is not a loop"));
        return;
      }
      breakCase = loop.breakCase;
    } else {
      breakCase = currentLoopContext.get(0).breakCase;
    }
    enclosingBlock.addChildToBack(createStateUpdate(breakCase));
    enclosingBlock.addChildToBack(createSafeBreak());
  }

  private void visitLabel() {
    Node labelName = currentStatement.removeFirstChild();
    Node child = currentStatement.removeFirstChild();
    if (NodeUtil.isLoopStructure(child)) {
      currentStatement = child;
      visitLoop(labelName.getString());
    } else {
      originalGeneratorBody.addChildToFront(child);
    }
  }

  /**
   * Pops the loop information off of our stack if we reach the marker cooresponding
   * to the end of the current loop.
   */
  private void visitGeneratorMarker() {
    if (!currentLoopContext.isEmpty()
        && currentLoopContext.get(0).breakCase == currentStatement.getFirstChild().getDouble()) {
      currentLoopContext.remove(0);
    }
    if (!currentExceptionContext.isEmpty()
        && currentExceptionContext.get(0).catchStartCase
            == currentStatement.getFirstChild().getDouble()) {
      currentExceptionContext.remove(0);
    }
  }

  /**
   * Uses a case statement to jump over the body if the condition of the
   * if statement is false. Additionally, lift the body of the {@code if}
   * statement to the top level.
   */
  private void visitIf() {
    Node condition = currentStatement.removeFirstChild();
    Node ifBody = currentStatement.removeFirstChild();
    boolean hasElse = currentStatement.hasChildren();

    int ifEndState = generatorCaseCount++;

    Node invertedConditional =
        IR.ifNode(
            withBooleanType(IR.not(condition)),
            IR.block(createStateUpdate(ifEndState), createSafeBreak()));
    invertedConditional.setGeneratorSafe(true);
    Node endIf = makeGeneratorMarker(ifEndState);

    originalGeneratorBody.addChildToFront(invertedConditional);
    originalGeneratorBody.addChildAfter(ifBody, invertedConditional);
    originalGeneratorBody.addChildAfter(endIf, ifBody);

    if (hasElse) {
      Node elseBlock = currentStatement.removeFirstChild();

      int elseEndState = generatorCaseCount++;

      Node endElse = makeGeneratorMarker(elseEndState);

      ifBody.addChildToBack(createStateUpdate(elseEndState));
      ifBody.addChildToBack(createSafeBreak());
      originalGeneratorBody.addChildAfter(elseBlock, endIf);
      originalGeneratorBody.addChildAfter(endElse, elseBlock);
    }
  }

  /**
   * Translates switch statements into a series of if statements.
   *
   * <p>Sample translation:
   * <pre>
   * switch (i) {
   *   case 1:
   *     s;
   *   case 2:
   *     t;
   *   ...
   * }
   * </pre>
   *
   * <p>Is eventually rewritten to:
   *
   * <pre>
   * $jscomp$generator$switch$entered0 = false;
   * if ($jscomp$generator$switch$entered0 || i == 1) {
   *   $jscomp$generator$switch$entered0 = true;
   *   s;
   * }
   * if ($jscomp$generator$switch$entered0 || i == 2) {
   *   $jscomp$generator$switch$entered0 = true;
   *   t;
   * }
   * ...
   *
   * </pre>
   */
  private void visitSwitch() {
    Node didEnter = withBooleanType(IR.name(GENERATOR_SWITCH_ENTERED + generatorCounter.get()));
    Node didEnterDecl = IR.var(didEnter.cloneTree(), withFalseType(IR.falseNode()));
    Node switchVal =
        withType(
            IR.name(GENERATOR_SWITCH_VAL + generatorCounter.get()),
            currentStatement.getFirstChild().getTypeI());
    Node switchValDecl = IR.var(switchVal.cloneTree(), currentStatement.removeFirstChild());
    originalGeneratorBody.addChildToFront(didEnterDecl);
    originalGeneratorBody.addChildAfter(switchValDecl, didEnterDecl);
    Node insertionPoint = switchValDecl;

    while (currentStatement.hasChildren()) {
      Node currCase = currentStatement.removeFirstChild();
      Node equivBlock;
      currCase
          .getLastChild()
          .addChildToFront(
              IR.exprResult(
                  withBooleanType(IR.assign(didEnter.cloneTree(), withTrueType(IR.trueNode())))));
      if (currCase.isDefaultCase()) {
        if (currentStatement.hasChildren()) {
          compiler.report(
              JSError.make(
                  currentStatement,
                  Es6ToEs3Util.CANNOT_CONVERT_YET,
                  "Default case as intermediate case"));
        }
        equivBlock = IR.block(currCase.removeFirstChild());
      } else {
        equivBlock =
            IR.ifNode(
                withBooleanType(
                    IR.or(
                        didEnter.cloneTree(),
                        withBooleanType(
                            IR.sheq(switchVal.cloneTree(), currCase.removeFirstChild())))),
                currCase.removeFirstChild());
      }
      originalGeneratorBody.addChildAfter(equivBlock, insertionPoint);
      insertionPoint = equivBlock;
    }

    int breakTarget = generatorCaseCount++;
    int cont = currentLoopContext.isEmpty() ? -1 : currentLoopContext.get(0).continueCase;
    currentLoopContext.add(0, new LoopContext(breakTarget, cont, null));
    Node breakCase = makeGeneratorMarker(breakTarget);
    originalGeneratorBody.addChildAfter(breakCase, insertionPoint);
  }

  /**
   * Lifts all children to the body of the original generator to flatten the block.
   */
  private void visitBlock() {
    if (!currentStatement.hasChildren()) {
      return;
    }
    Node insertionPoint = currentStatement.removeFirstChild();
    originalGeneratorBody.addChildToFront(insertionPoint);
    for (Node child = currentStatement.removeFirstChild();
        child != null;
        child = currentStatement.removeFirstChild()) {
      originalGeneratorBody.addChildAfter(child, insertionPoint);
      insertionPoint = child;
    }
  }

  /**
   * Translates for in loops to a for in loop which produces an array of
   * values iterated over followed by a plain for loop which performs the logic
   * contained in the body of the original for in.
   *
   * <p>Sample translation:
   * <pre>
   * for (i in j) {
   *   s;
   * }
   * </pre>
   *
   * <p>Is eventually rewritten to:
   *
   * <pre>
   * $jscomp$arr = [];
   * $jscomp$iter = j;
   * for (i in $jscomp$iter) {
   *   $jscomp$arr.push(i);
   * }
   * for ($jscomp$var = 0; $jscomp$var < $jscomp$arr.length; $jscomp$var++) {
   *   i = $jscomp$arr[$jscomp$var];
   *   if (!(i in $jscomp$iter)) {
   *     continue;
   *   }
   *   s;
   * }
   * </pre>
   */
  private void visitForIn() {
    Node variable = currentStatement.removeFirstChild();
    Node iterable = currentStatement.removeFirstChild();
    Node body = currentStatement.removeFirstChild();

    TypeI iterableType = iterable.getTypeI();
    TypeI typeParam = unknownType;
    if (addTypes) {
      typeParam = iterableType.autobox().toMaybeObjectType().getTemplateTypes().get(0);
    }
    TypeI arrayType = createGenericType(JSTypeNative.ARRAY_TYPE, typeParam);
    String loopId = generatorCounter.get();
    Node arrayName = withType(IR.name(GENERATOR_FOR_IN_ARRAY + loopId), arrayType);
    Node varName = withNumberType(IR.name(GENERATOR_FOR_IN_VAR + loopId));
    Node iterableName = withType(IR.name(GENERATOR_FOR_IN_ITER + loopId), iterableType);

    if (variable.isVar()) {
      variable = variable.removeFirstChild();
    }
    body.addChildToFront(
        IR.ifNode(
            withBooleanType(IR.not(IR.in(variable.cloneTree(), iterableName.cloneTree()))),
            IR.block(IR.continueNode())));
    body.addChildToFront(
        IR.var(variable.cloneTree(), IR.getelem(arrayName.cloneTree(), varName.cloneTree())));
    hoistRoot.getParent().addChildAfter(IR.var(arrayName.cloneTree()), hoistRoot);
    hoistRoot.getParent().addChildAfter(IR.var(varName.cloneTree()), hoistRoot);
    hoistRoot.getParent().addChildAfter(IR.var(iterableName.cloneTree()), hoistRoot);

    Node arrayDef =
        IR.exprResult(
            withType(
                IR.assign(arrayName.cloneTree(), withType(IR.arraylit(), arrayType)), arrayType));
    Node iterDef =
        IR.exprResult(withType(IR.assign(iterableName.cloneTree(), iterable), iterableType));
    Node newForIn =
        IR.forIn(
            variable.cloneTree(),
            iterableName,
            IR.block(
                IR.exprResult(
                    withNumberType(
                        IR.call(IR.getprop(arrayName.cloneTree(), IR.string("push")), variable)))));
    Node newFor =
        IR.forNode(
            withNumberType(IR.assign(varName.cloneTree(), withNumberType(IR.number(0)))),
            withBooleanType(
                IR.lt(
                    varName.cloneTree(),
                    withNumberType(IR.getprop(arrayName, IR.string("length"))))),
            withNumberType(IR.inc(varName, true)),
            body);

    enclosingBlock.addChildToBack(arrayDef);
    enclosingBlock.addChildToBack(iterDef);
    enclosingBlock.addChildToBack(newForIn);
    originalGeneratorBody.addChildToFront(newFor);
  }

  /**
   * Translates loops to a case statement followed by an if statement
   * containing the loop body. The if statement finishes by
   * jumping back to the initial case statement to enter the loop again.
   * In the case of for and do loops, initialization and post loop statements are inserted
   * before and after the if statement. Below is a sample translation for a while loop:
   *
   * <p>Sample translation:
   * <pre>
   * while (b) {
   *   s;
   * }
   * </pre>
   *
   * <p>Is eventually rewritten to:
   * <pre>
   * case n:
   *   if (b) {
   *     s;
   *     state = n;
   *     break;
   *   }
   * </pre>
   */
  private void visitLoop(String label) {
    Node initializer;
    Node guard;
    Node incr;
    Node body;

    if (currentStatement.isWhile()) {
      guard = currentStatement.removeFirstChild();
      body = currentStatement.removeFirstChild();
      initializer = IR.empty();
      incr = IR.empty();
    } else if (currentStatement.isVanillaFor()) {
      initializer = currentStatement.removeFirstChild();
      if (initializer.isAssign()) {
        initializer = IR.exprResult(initializer);
      }
      guard = currentStatement.removeFirstChild();
      incr = currentStatement.removeFirstChild();
      body = currentStatement.removeFirstChild();
    } else {
      checkState(currentStatement.isDo());
      initializer = IR.empty();
      incr =
          withBooleanType(
              IR.assign(
                  withBooleanType(IR.name(GENERATOR_DO_WHILE_INITIAL)),
                  withFalseType(IR.falseNode())));

      body = currentStatement.removeFirstChild();
      guard = currentStatement.removeFirstChild();
    }

    Node condition;
    Node prestatement;

    if (guard.isNormalBlock()) {
      prestatement = guard.removeFirstChild();
      condition = guard.removeFirstChild();
    } else {
      prestatement = IR.block();
      condition = guard;
    }

    int loopBeginState = generatorCaseCount++;
    int continueState = loopBeginState;

    if (!incr.isEmpty()) {
      continueState = generatorCaseCount++;
      Node continueCase = makeGeneratorMarker(continueState);
      body.addChildToBack(continueCase);
      body.addChildToBack(incr.isNormalBlock() ? incr : IR.exprResult(incr));
    }

    currentLoopContext.add(0, new LoopContext(generatorCaseCount, continueState, label));

    Node beginCase = makeGeneratorMarker(loopBeginState);
    Node conditionalBranch =
        IR.ifNode(condition.isEmpty() ? withTrueType(IR.trueNode()) : condition, body);
    Node setStateLoopStart = createStateUpdate(loopBeginState);
    Node breakToStart = createSafeBreak();

    originalGeneratorBody.addChildToFront(conditionalBranch);
    if (!prestatement.isEmpty()) {
      originalGeneratorBody.addChildToFront(prestatement);
    }
    originalGeneratorBody.addChildToFront(beginCase);
    if (!initializer.isEmpty()) {
      originalGeneratorBody.addChildToFront(initializer);
    }
    body.addChildToBack(setStateLoopStart);
    body.addChildToBack(breakToStart);
  }

  /**
   * Hoists {@code var} statements into the closure containing the iterator
   * to preserve their state across
   * multiple calls to next().
   */
  private void visitVar() {
    Node name = currentStatement.removeFirstChild();
    while (name != null) {
      if (name.hasChildren()) {
        enclosingBlock.addChildToBack(
            IR.exprResult(withType(IR.assign(name, name.removeFirstChild()), name.getTypeI())));
      }
      hoistRoot.getParent().addChildAfter(IR.var(name.cloneTree()), hoistRoot);
      // name now refers to "generated" assignment which is not visible to end user. Don't index it.
      name.makeNonIndexable();
      name = currentStatement.removeFirstChild();
    }
  }

  /**
   * Translates {@code yield} to set the state so that execution resume at the next statement
   * when the function is next called and then returns an iterator result with
   * the desired value.
   */
  private void visitYieldExprResult() {
    enclosingBlock.addChildToBack(createStateUpdate());
    Node yield = currentStatement.getFirstChild();
    Node value =
        yield.hasChildren() ? yield.removeFirstChild() : withUndefinedType(IR.name("undefined"));
    enclosingBlock.addChildToBack(IR.returnNode(createIteratorResult(value, false)));
  }

  /**
   * Translates {@code return} statements to set the state to done before returning the
   * desired value.
   */
  private void visitReturn() {
    enclosingBlock.addChildToBack(createStateUpdate(-1));
    enclosingBlock.addChildToBack(
        IR.returnNode(
            createIteratorResult(
                currentStatement.hasChildren()
                    ? currentStatement.removeFirstChild()
                    : withUndefinedType(IR.name("undefined")),
                true)));
  }

  private Node createStateUpdate() {
    return IR.exprResult(
        withNumberType(
            IR.assign(
                withNumberType(IR.name(GENERATOR_STATE)),
                withNumberType(IR.number(generatorCaseCount)))));
  }

  private Node createStateUpdate(int state) {
    return IR.exprResult(
        withNumberType(
            IR.assign(withNumberType(IR.name(GENERATOR_STATE)), withNumberType(IR.number(state)))));
  }

  private Node createIteratorResult(Node value, boolean done) {
    TypeI iIterableResultType =
        createGenericType(JSTypeNative.I_ITERABLE_RESULT_TYPE, value.getTypeI());
    return withType(
        IR.objectlit(
            IR.propdef(IR.stringKey("value"), value),
            IR.propdef(
                IR.stringKey("done"),
                done ? withTrueType(IR.trueNode()) : withFalseType(IR.falseNode()))),
        iIterableResultType);
  }

  private static Node createSafeBreak() {
    Node breakNode = IR.breakNode();
    breakNode.setGeneratorSafe(true);
    return breakNode;
  }

  private Node createFinallyJumpBlock(Node finallyName, int finallyStartState) {
    int jumpPoint = generatorCaseCount++;
    Node setReturnState =
        IR.exprResult(
            withNumberType(
                IR.assign(finallyName.cloneTree(), withNumberType(IR.number(jumpPoint)))));
    Node toFinally = createStateUpdate(finallyStartState);
    Node returnPoint = makeGeneratorMarker(jumpPoint);
    Node returnBlock = IR.block(setReturnState, toFinally, createSafeBreak());
    returnBlock.addChildToBack(returnPoint);
    return returnBlock;
  }

  private LoopContext getLoopContext(String label) {
    for (LoopContext context : currentLoopContext) {
      if (label.equals(context.label)) {
        return context;
      }
    }
    return null;
  }

  private boolean controlCanExit(Node n) {
    ControlExitsCheck exits = new ControlExitsCheck();
    NodeTraversal.traverseEs6(compiler, n, exits);
    return exits.didExit();
  }

  /**
   * Finds the only child of the {@code node} of the given type.
   */
  private Node getUnique(Node node, Token type) {
    List<Node> matches = new ArrayList<>();
    insertAll(node, type, matches);
    checkState(matches.size() == 1, matches);
    return matches.get(0);
  }

  /**
   * Adds all children of the {@code node} of the given type to given list.
   */
  private void insertAll(Node node, Token type, List<Node> matchingNodes) {
    if (node.getToken() == type) {
      matchingNodes.add(node);
    }
    for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
      insertAll(c, type, matchingNodes);
    }
  }

  /**
   * Decomposes expressions with yields inside of them to equivalent
   * sequence of expressions in which all non-statement yields are
   * of the form:
   *
   * <pre>
   *   var name = yield expr;
   * </pre>
   *
   * <p>For example, change the following code:
   * <pre>
   *   return x || yield y;
   * </pre>
   * <p>Into:
   * <pre>
   *  var temp$$0;
   *  if (temp$$0 = x); else temp$$0 = yield y;
   *  return temp$$0;
   * </pre>
   *
   * This uses the {@link ExpressionDecomposer} class
   */
  private final class DecomposeYields extends NodeTraversal.AbstractPreOrderCallback {

    private final AbstractCompiler compiler;
    private final ExpressionDecomposer decomposer;

    DecomposeYields(AbstractCompiler compiler) {
      this.compiler = compiler;
      Set<String> consts = new HashSet<>();
      decomposer =
          new ExpressionDecomposer(
              compiler,
              compiler.getUniqueNameIdSupplier(),
              consts,
              Scope.createGlobalScope(new Node(Token.SCRIPT)),
              compiler.getOptions().allowMethodCallDecomposing());
    }

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      switch (n.getToken()) {
        case YIELD:
          visitYieldExpression(t, n);
          break;
        case DO:
        case FOR:
        case WHILE:
          visitLoop(t, n);
          break;
        case CASE:
          if (controlCanExit(n.getFirstChild())) {
            compiler.report(
                JSError.make(
                    n, Es6ToEs3Util.CANNOT_CONVERT_YET, "Case statements that contain yields"));
            return false;
          }
          break;
        default:
          break;
      }
      return true;
    }

    private void visitYieldExpression(NodeTraversal t, Node n) {
      if (n.getParent().isExprResult()) {
        return;
      }
      if (decomposer.canExposeExpression(n)
          != ExpressionDecomposer.DecompositionType.UNDECOMPOSABLE) {
        decomposer.exposeExpression(n);
        t.reportCodeChange();
      } else {
        String link = "https://github.com/google/closure-compiler/wiki/FAQ"
            + "#i-get-an-undecomposable-expression-error-for-my-yield-or-await-expression"
            + "-what-do-i-do";
        String suggestion = "Please rewrite the yield or await as a separate statement.";
        String message = "Undecomposable expression: " + suggestion + "\nSee " + link;
        compiler.report(JSError.make(n, Es6ToEs3Util.CANNOT_CONVERT, message));
      }
    }

    private void visitLoop(NodeTraversal t, Node n) {
      Node enclosingFunc = NodeUtil.getEnclosingFunction(n);
      if (enclosingFunc == null || !enclosingFunc.isGeneratorFunction() || n.isForIn()) {
        return;
      }
      Node enclosingBlock = NodeUtil.getEnclosingBlock(n);
      Node guard = null;
      Node incr = null;
      switch (n.getToken()) {
        case FOR:
          guard = n.getSecondChild();
          incr = guard.getNext();
          break;
        case WHILE:
          guard = n.getFirstChild();
          incr = IR.empty();
          break;
        case DO:
          guard = n.getLastChild();
          if (!guard.isEmpty()) {
            Node firstEntry = IR.name(GENERATOR_DO_WHILE_INITIAL);
            enclosingBlock.addChildToFront(
                IR.var(firstEntry.cloneTree(), withTrueType(IR.trueNode())));
            guard = withBooleanType(IR.or(firstEntry, n.getLastChild().detach()));
            n.addChildToBack(guard);
          }
          incr = IR.empty();
          break;
        default:
          break;
      }
      if (!controlCanExit(guard) && !controlCanExit(incr)) {
        return;
      }
      Node guardName = IR.name(GENERATOR_LOOP_GUARD + generatorCounter.get());
      if (!guard.isEmpty()) {
        Node container = new Node(Token.BLOCK);
        n.replaceChild(guard, container);
        container.addChildToFront(
            IR.block(
                IR.exprResult(
                    withType(
                        IR.assign(guardName.cloneTree(), guard.cloneTree()), guard.getTypeI()))));
        container.addChildToBack(guardName.cloneTree());
      }
      if (!incr.isEmpty()) {
        n.addChildBefore(IR.block(IR.exprResult(incr.detach())), n.getLastChild());
      }
      enclosingBlock.addChildToFront(IR.var(guardName));
      t.reportCodeChange();
    }
  }

  private Node makeGeneratorMarker(int i) {
    Node n = IR.exprResult(withNumberType(IR.number(i)));
    n.setGeneratorMarker(true);
    return n;
  }

  private final class ControlExitsCheck implements NodeTraversal.Callback {

    int continueCatchers;
    int breakCatchers;
    int throwCatchers;
    List<String> labels = new ArrayList<>();
    boolean exited;
    boolean addJumps;
    private Node finallyName;
    private int finallyStartState;

    ControlExitsCheck(Node finallyName, int finallyStartState) {
      this.finallyName = finallyName;
      this.finallyStartState = finallyStartState;
      addJumps = true;
    }

    ControlExitsCheck() {
      addJumps = false;
    }

    @Override
    public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      switch (n.getToken()) {
        case FUNCTION:
          return false;
        case LABEL:
          labels.add(0, n.getFirstChild().getString());
          break;
        case DO:
        case WHILE:
        case FOR:
        case FOR_IN:
          continueCatchers++;
          breakCatchers++;
          break;
        case SWITCH:
          breakCatchers++;
          break;
        case BLOCK:
          parent = n.getParent();
          if (parent != null
              && parent.isTry()
              && parent.getFirstChild() == n
              && n.getNext().hasChildren()) {
            throwCatchers++;
          }
          break;
        case BREAK:
          if (!n.isGeneratorSafe()
              && ((breakCatchers == 0 && !n.hasChildren())
                  || (n.hasChildren() && !labels.contains(n.getFirstChild().getString())))) {
            exited = true;
            if (addJumps) {
              parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
            }
          }
          break;
        case CONTINUE:
          if (continueCatchers == 0
              || (n.hasChildren() && !labels.contains(n.getFirstChild().getString()))) {
            exited = true;
            if (addJumps) {
              parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
            }
          }
          break;
        case THROW:
          if (throwCatchers == 0) {
            exited = true;
            if (addJumps && !n.isGeneratorSafe()) {
              parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
            }
          }
          break;
        case RETURN:
          exited = true;
          if (addJumps) {
            parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
          }
          break;
        case YIELD:
          exited = true;
          break;
        default:
          break;
      }
      return true;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      switch (n.getToken()) {
        case LABEL:
          labels.remove(0);
          break;
        case DO:
        case WHILE:
        case FOR:
        case FOR_IN:
          continueCatchers--;
          breakCatchers--;
          break;
        case SWITCH:
          breakCatchers--;
          break;
        case BLOCK:
          parent = n.getParent();
          if (parent != null
              && parent.isTry()
              && parent.getFirstChild() == n
              && n.getNext().hasChildren()) {
            throwCatchers--;
          }
          break;
        default:
          break;
      }
    }

    public boolean didExit() {
      return exited;
    }
  }

  private static final class LoopContext {
    int breakCase;
    int continueCase;
    String label;

    LoopContext(int breakCase, int continueCase, String label) {
      this.breakCase = breakCase;
      this.continueCase = continueCase;
      this.label = label;
    }
  }

  private static final class ExceptionContext {
    int catchStartCase;
    Node catchBlock;

    ExceptionContext(int catchStartCase, Node catchBlock) {
      this.catchStartCase = catchStartCase;
      this.catchBlock = catchBlock;
    }
  }

  /**
   * Preloads the skeleton AST function that is needed for generators,
   * reports change to enclosing scope, and returns it.
   * If the skeleton is already preloaded, does not do anything, just returns the node.
   */
  static Node preloadGeneratorSkeletonAndReportChange(AbstractCompiler compiler) {
    return preloadGeneratorSkeleton(compiler, true);
  }

  /**
   * Preloads the skeleton AST function that is needed for generators and returns it.
   * If the skeleton is already preloaded, does not do anything, just returns the node.
   * reportChange tells the function whether to report a code change in the enclosing scope.
   *
   * Because validity checks happen between passes, we need to report the change if the generator
   * was preloaded in the {@link EarlyEs6ToEs3Converter} class.
   * However, if the generator was preloaded in this {@link Es6RewriteGenerators} class, we do not
   * want to report the change since it will be removed by {@link #cleanUpGeneratorSkeleton}
   */
  private static Node preloadGeneratorSkeleton(AbstractCompiler compiler, boolean reportChange) {
    Node root = compiler.getJsRoot();
    Node generatorFunc = getPreloadedGeneratorFunc(root);
    if (generatorFunc != null) {
      return generatorFunc;
    }
    Node genFunc = compiler.parseSyntheticCode(Joiner.on('\n').join(
        "function " + GENERATOR_PRELOAD_FUNCTION_NAME + "() {",
        "  var " + GENERATOR_STATE + " = 0;",
        "  function $jscomp$generator$impl(",
        "      " + GENERATOR_ACTION_ARG + ",",
        "      " + GENERATOR_NEXT_ARG + ",",
        "      " + GENERATOR_THROW_ARG + ") {",
        "    while (1) switch (" + GENERATOR_STATE + ") {",
        "      case 0:",
        "      default:",
        "        return {value: undefined, done: true};",
        "    }",
        "  }",
        // TODO(tbreisacher): Remove this cast if we start returning an actual
        // Generator object.
        "  var iterator = /** @type {!Generator<?>} */ ({",
        "    next: function(arg) {",
        "      return $jscomp$generator$impl("
            + GENERATOR_ACTION_NEXT
            + ", arg, undefined);",
        "    },",
        "    throw: function(arg) {",
        "      return $jscomp$generator$impl("
            + GENERATOR_ACTION_THROW
            + ", undefined, arg);",
        "    },",
        // TODO(tbreisacher): Implement Generator.return:
        // http://www.ecma-international.org/ecma-262/6.0/#sec-generator.prototype.return
        "    return: function(arg) { throw Error('Not yet implemented'); },",
        "  });",
        "  $jscomp.initSymbolIterator();",
        "  /** @this {!Generator<?>} */",
        "  iterator[Symbol.iterator] = function() { return this; };",
        "  return iterator;",
        "}"))
    .getFirstChild() // function
    .detach();
    root.getFirstChild().addChildToFront(genFunc);
    if (reportChange) {
      NodeUtil.markNewScopesChanged(genFunc, compiler);
      compiler.reportChangeToEnclosingScope(genFunc);
    }
    return genFunc;
  }

  /**
   * Add types to key nodes in the generator AST created by {@link #preloadGeneratorSkeleton} For
   * example, changes {@code Generator<?>} to {@code Generator<yieldType>}, where yieldType is the
   * inferred yield type of the original user-defined generator function.
   */
  private void addTypesToGeneratorSkeleton(Node genBlock, TypeI yieldType) {
    TypeI generatorType = createGenericType(JSTypeNative.GENERATOR_TYPE, yieldType);
    TypeI iIterableResultType = createGenericType(JSTypeNative.I_ITERABLE_RESULT_TYPE, yieldType);

    // Add type to the generator implementation function node.
    Node impl = genBlock.getSecondChild();
    checkState(impl.isFunction());
    FunctionTypeI implFuncType = impl.getTypeI().toMaybeFunctionType();
    implFuncType = implFuncType.toBuilder().withReturnType(iIterableResultType).build();
    impl.setTypeI(implFuncType);
    impl.getFirstChild().setTypeI(implFuncType);

    Node objectLit =
        impl.getChildAtIndex(2)
            .getFirstChild()
            .getSecondChild()
            .getFirstChild()
            .getChildAtIndex(2)
            .getFirstFirstChild() // RETURN node in default case
            .getFirstChild();
    checkState(objectLit.isObjectLit());
    objectLit.setTypeI(iIterableResultType);
    objectLit.getFirstChild().setTypeI(iIterableResultType);

    // Add type to the var iterator = {next: function (...) {...}, throw: ..., return: ... } node
    Node iteratorVar = impl.getNext();
    checkState(iteratorVar.isVar());
    iteratorVar.getFirstChild().setTypeI(generatorType);
    iteratorVar.getFirstFirstChild().setTypeI(generatorType);
    iteratorVar.getFirstFirstChild().getFirstChild().setTypeI(generatorType);

    Node next = iteratorVar.getFirstFirstChild().getFirstFirstChild(); // String key "next"
    checkState(next.isStringKey());
    FunctionTypeI nextFunctionType = next.getTypeI().toMaybeFunctionType();
    nextFunctionType = nextFunctionType.toBuilder().withReturnType(iIterableResultType).build();
    next.setTypeI(nextFunctionType);
    next.getFirstChild().setTypeI(nextFunctionType);

    // CALL node of function $jscomp$generator$impl within RETURN node in function of "next"
    Node call = next.getFirstChild().getChildAtIndex(2).getFirstFirstChild();
    checkState(call.isCall());
    call.setTypeI(iIterableResultType);

    Node genImplName = call.getFirstChild();
    checkState(genImplName.isName());
    FunctionTypeI genImplType = genImplName.getTypeI().toMaybeFunctionType();
    genImplType = genImplType.toBuilder().withReturnType(iIterableResultType).build();
    genImplName.setTypeI(genImplType);

    // Add type to the iterator[Symbol.iterator] = function () { return this; } node
    Node exprResult = iteratorVar.getNext().getNext();
    checkState(exprResult.isExprResult());
    FunctionTypeI funcType = exprResult.getFirstChild().getTypeI().toMaybeFunctionType();
    // Set function type to be function(this:Generator<?>):Generator<inferred yield type>
    funcType = funcType.toBuilder().withReturnType(iIterableResultType).build();
    exprResult.getFirstChild().setTypeI(funcType);
    exprResult.getFirstFirstChild().setTypeI(funcType);
    exprResult.getFirstFirstChild().getFirstChild().setTypeI(generatorType);
    exprResult.getFirstChild().getSecondChild().setTypeI(funcType); // FUNCTION node
    exprResult
        .getFirstChild()
        .getSecondChild()
        .getChildAtIndex(2)
        .getFirstFirstChild() // THIS node
        .setTypeI(generatorType);

    // Add type to the final return node of genBlock
    exprResult.getNext().getFirstChild().setTypeI(generatorType);
  }

  /** Returns the generator function that was preloaded, or null if not found. */
  @Nullable
  private static Node getPreloadedGeneratorFunc(Node root) {
    if (root.getFirstChild() == null) {
      return null;
    }
    for (Node c = root.getFirstFirstChild(); c != null; c = c.getNext()) {
      if (c.isFunction() && GENERATOR_PRELOAD_FUNCTION_NAME.equals(c.getFirstChild().getString())) {
        return c;
      }
    }
    return null;
  }

  /**
   * Delete the preloaded generator function, and report code change if reportChange is true.
   *
   * We only want to reportChange if the generator function was preloaded in the
   * {@link EarlyEs6ToEs3Converter} class, since a change was reported there.
   * If we preload the generator function in this class, it will be an addition and deletion of the
   * same node, which means we do not have to report code change in either case since the code was
   * ultimately not changed.
   */
  private void cleanUpGeneratorSkeleton(boolean reportChange) {
    Node genFunc = getPreloadedGeneratorFunc(compiler.getJsRoot());
    if (genFunc != null) {
      if (reportChange) {
        NodeUtil.deleteNode(genFunc, compiler);
      } else {
        genFunc.detach();
      }
    }
  }

  private TypeI createGenericType(JSTypeNative typeName, TypeI typeArg) {
    return Es6ToEs3Util.createGenericType(addTypes, registry, typeName, typeArg);
  }

  private Node withStringType(Node n) {
    return withType(n, stringType);
  }

  private Node withBooleanType(Node n) {
    return withType(n, booleanType);
  }

  private Node withFalseType(Node n) {
    return withType(n, falseType);
  }

  private Node withTrueType(Node n) {
    return withType(n, trueType);
  }

  private Node withUnknownType(Node n) {
    return withType(n, unknownType);
  }

  private Node withNumberType(Node n) {
    return withType(n, numberType);
  }

  private Node withUndefinedType(Node n) {
    return withType(n, undefinedType);
  }
}