
com.google.javascript.jscomp.Es6RewriteGenerators Maven / Gradle / Ivy
/*
* 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.javascript.jscomp.Es6ToEs3Converter.makeIterator;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
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.Token;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Converts ES6 generator functions to valid ES3 code. This pass runs after all ES6 features
* except for yield and generators have been transpiled.
*
* @author [email protected] (Matthew Loring)
*/
public final class Es6RewriteGenerators
extends NodeTraversal.AbstractPostOrderCallback implements HotSwapCompilerPass {
// 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_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;
// 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 currentLoopContext;
private final List currentExceptionContext;
private static int generatorCaseCount;
private Supplier 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;
public Es6RewriteGenerators(AbstractCompiler compiler) {
Preconditions.checkNotNull(compiler);
this.compiler = compiler;
this.currentLoopContext = new ArrayList<>();
this.currentExceptionContext = new ArrayList<>();
generatorCounter = compiler.getUniqueNameIdSupplier();
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseEs6(compiler, root, new DecomposeYields(compiler));
NodeTraversal.traverseEs6(compiler, root, this);
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
NodeTraversal.traverseEs6(compiler, scriptRoot, new DecomposeYields(compiler));
NodeTraversal.traverseEs6(compiler, scriptRoot, this);
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getType()) {
case Token.FUNCTION:
if (n.isGeneratorFunction()) {
generatorCaseCount = 0;
visitGenerator(n, parent);
}
break;
case Token.NAME:
Node enclosing = NodeUtil.getEnclosingFunction(n);
if (enclosing != null
&& enclosing.isGeneratorFunction()
&& n.matchesQualifiedName("arguments")) {
n.setString(GENERATOR_ARGUMENTS);
}
break;
case Token.THIS:
enclosing = NodeUtil.getEnclosingFunction(n);
if (enclosing != null && enclosing.isGeneratorFunction()) {
n.getParent().replaceChild(n, IR.name(GENERATOR_THIS));
}
break;
case Token.YIELD:
if (n.isYieldFor()) {
visitYieldFor(t, n, parent);
} else if (!parent.isExprResult()) {
visitYieldExpr(n, parent);
} else {
visitYieldThrows(parent, parent.getParent());
}
break;
default:
break;
}
}
private void visitYieldThrows(Node n, Node parent) {
Node ifThrows =
IR.ifNode(
IR.shne(IR.name(GENERATOR_THROW_ARG), IR.name("undefined")),
IR.block(IR.throwNode(IR.name(GENERATOR_THROW_ARG))));
parent.addChildAfter(ifThrows, n);
compiler.reportCodeChange();
}
/**
* Translates expressions using the new yield-for syntax.
*
* Sample translation:
*
*
* var i = yield * gen();
*
*
* Is rewritten to:
*
*
* 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;
*
*/
private void visitYieldFor(NodeTraversal t, Node n, Node parent) {
Node enclosingStatement = NodeUtil.getEnclosingStatement(n);
Node generator = IR.var(
IR.name(GENERATOR_YIELD_ALL_NAME),
makeIterator(t, compiler, n.removeFirstChild()));
Node entryDecl = IR.var(IR.name(GENERATOR_YIELD_ALL_ENTRY));
Node assignIterResult =
IR.assign(
IR.name(GENERATOR_YIELD_ALL_ENTRY),
IR.call(
IR.getprop(IR.name(GENERATOR_YIELD_ALL_NAME), IR.string("next")),
IR.name(GENERATOR_NEXT_ARG)));
Node loopCondition = IR.not(IR.getprop(assignIterResult, IR.string("done")));
Node elemValue = IR.getprop(IR.name(GENERATOR_YIELD_ALL_ENTRY), IR.string("value"));
Node yieldStatement = IR.exprResult(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.detachFromParent();
} else {
parent.replaceChild(n, elemValue);
}
visitYieldThrows(yieldStatement, yieldStatement.getParent());
compiler.reportCodeChange();
}
private void visitYieldExpr(Node n, Node parent) {
Node enclosingStatement = NodeUtil.getEnclosingStatement(n);
Node yieldStatement =
IR.exprResult(n.hasChildren() ? IR.yield(n.removeFirstChild()) : IR.yield());
Node yieldResult = IR.name(GENERATOR_NEXT_ARG + generatorCounter.get());
Node yieldResultDecl = IR.var(yieldResult.cloneTree(), IR.name(GENERATOR_NEXT_ARG));
parent.replaceChild(n, yieldResult);
enclosingStatement.getParent().addChildBefore(yieldStatement, enclosingStatement);
enclosingStatement.getParent().addChildBefore(yieldResultDecl, enclosingStatement);
visitYieldThrows(yieldStatement, yieldStatement.getParent());
compiler.reportCodeChange();
}
private void visitGenerator(Node n, Node parent) {
compiler.needsEs6Runtime = true;
hasTranslatedTry = false;
Node genBlock = compiler.parseSyntheticCode(Joiner.on('\n').join(
"function generatorBody() {",
" var " + GENERATOR_STATE + " = " + generatorCaseCount + ";",
" function $jscomp$generator$impl(" + GENERATOR_NEXT_ARG + ", ",
" " + GENERATOR_THROW_ARG + ") {",
" while (1) switch (" + GENERATOR_STATE + ") {",
" case " + generatorCaseCount + ":",
" 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(arg, undefined); },",
" throw: function(arg) { return $jscomp$generator$impl(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
.getLastChild() // function body
.detachFromParent();
generatorCaseCount++;
originalGeneratorBody = n.getLastChild();
n.replaceChild(originalGeneratorBody, genBlock);
n.setIsGeneratorFunction(false);
// 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(IR.assign(IR.name(GENERATOR_STATE), IR.number(-1))));
enclosingBlock = getUnique(genBlock, Token.CASE).getLastChild();
hoistRoot = genBlock.getFirstChild();
if (NodeUtil.isNameReferenced(originalGeneratorBody, GENERATOR_ARGUMENTS)) {
hoistRoot
.getParent()
.addChildAfter(IR.var(IR.name(GENERATOR_ARGUMENTS), IR.name("arguments")), hoistRoot);
}
if (NodeUtil.isNameReferenced(originalGeneratorBody, GENERATOR_THIS)) {
hoistRoot
.getParent()
.addChildAfter(IR.var(IR.name(GENERATOR_THIS), 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 = IR.caseNode(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.reportCodeChange();
}
/** 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.isBlock()) {
visitBlock();
return false;
} else if (controlCanExit(currentStatement)) {
switch (currentStatement.getType()) {
case Token.WHILE:
case Token.DO:
case Token.FOR:
if (NodeUtil.isForIn(currentStatement)) {
visitForIn();
return false;
}
visitLoop(null);
return false;
case Token.LABEL:
visitLabel();
return false;
case Token.SWITCH:
visitSwitch();
return false;
case Token.IF:
if (!currentStatement.isGeneratorSafe()) {
visitIf();
return false;
}
break;
case Token.TRY:
visitTry();
return false;
case Token.EXPR_RESULT:
if (currentStatement.getFirstChild().isYield()) {
visitYieldExprResult();
return true;
}
break;
case Token.RETURN:
visitReturn();
return false;
case Token.CONTINUE:
visitContinue();
return false;
case Token.BREAK:
if (!currentStatement.isGeneratorSafe()) {
visitBreak();
return false;
}
break;
case Token.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: "
+ Token.name(currentStatement.getType()));
}
}
// 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 = 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 = IR.name("$jscomp$generator$" + caughtError.getString());
originalGeneratorBody.addChildToFront(catchStart);
originalGeneratorBody.addChildAfter(catchBody, catchStart);
Node assignError = IR.assign(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 = 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.detachFromParent());
originalGeneratorBody.addChildAfter(finallyStart, catchBody);
originalGeneratorBody.addChildAfter(finallyBody.detachFromParent(), finallyStart);
originalGeneratorBody.addChildAfter(finallyEnd, finallyBody);
originalGeneratorBody.addChildToFront(IR.var(finallyName.cloneTree()));
finallyBody.addChildToBack(
IR.exprResult(IR.assign(IR.name(GENERATOR_STATE), finallyName.cloneTree())));
finallyBody.addChildToBack(createSafeBreak());
tryBody.addChildToBack(
IR.exprResult(IR.assign(finallyName.cloneTree(), IR.number(finallyEndState))));
tryBody.addChildToBack(createStateUpdate(finallyStartState));
tryBody.addChildToBack(createSafeBreak());
catchBody.addChildToBack(
IR.exprResult(IR.assign(finallyName.cloneTree(), 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.detachFromParent());
}
catchBody.addChildToFront(IR.var(caughtError, 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(IR.name(GENERATOR_ERROR)), hoistRoot);
}
}
private void visitContinue() {
Preconditions.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,
Es6ToEs3Converter.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(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.
*
* Sample translation:
*
* switch (i) {
* case 1:
* s;
* case 2:
* t;
* ...
* }
*
*
* Is eventually rewritten to:
*
*
* $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;
* }
* ...
*
*
*/
private void visitSwitch() {
Node didEnter = IR.name(GENERATOR_SWITCH_ENTERED + generatorCounter.get());
Node didEnterDecl = IR.var(didEnter.cloneTree(), IR.falseNode());
Node switchVal = IR.name(GENERATOR_SWITCH_VAL + generatorCounter.get());
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(IR.assign(didEnter.cloneTree(), IR.trueNode())));
if (currCase.isDefaultCase()) {
if (currentStatement.hasChildren()) {
compiler.report(
JSError.make(
currentStatement,
Es6ToEs3Converter.CANNOT_CONVERT_YET,
"Default case as intermediate case"));
}
equivBlock = IR.block(currCase.removeFirstChild());
} else {
equivBlock =
IR.ifNode(
IR.or(
didEnter.cloneTree(),
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.getChildCount() == 0) {
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.
*
* Sample translation:
*
* for (i in j) {
* s;
* }
*
*
* Is eventually rewritten to:
*
*
* $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;
* }
*
*/
private void visitForIn() {
Node variable = currentStatement.removeFirstChild();
Node iterable = currentStatement.removeFirstChild();
Node body = currentStatement.removeFirstChild();
String loopId = generatorCounter.get();
Node arrayName = IR.name(GENERATOR_FOR_IN_ARRAY + loopId);
Node varName = IR.name(GENERATOR_FOR_IN_VAR + loopId);
Node iterableName = IR.name(GENERATOR_FOR_IN_ITER + loopId);
if (variable.isVar()) {
variable = variable.removeFirstChild();
}
body.addChildToFront(
IR.ifNode(
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(IR.assign(arrayName.cloneTree(), IR.arraylit()));
Node iterDef = IR.exprResult(IR.assign(iterableName.cloneTree(), iterable));
Node newForIn =
IR.forIn(
variable.cloneTree(),
iterableName,
IR.block(
IR.exprResult(
IR.call(IR.getprop(arrayName.cloneTree(), IR.string("push")), variable))));
Node newFor =
IR.forNode(
IR.assign(varName.cloneTree(), IR.number(0)),
IR.lt(varName.cloneTree(), IR.getprop(arrayName, IR.string("length"))),
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:
*
* Sample translation:
*
* while (b) {
* s;
* }
*
*
* Is eventually rewritten to:
*
* case n:
* if (b) {
* s;
* state = n;
* break;
* }
*
*/
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.isFor()) {
initializer = currentStatement.removeFirstChild();
if (initializer.isAssign()) {
initializer = IR.exprResult(initializer);
}
guard = currentStatement.removeFirstChild();
incr = currentStatement.removeFirstChild();
body = currentStatement.removeFirstChild();
} else {
Preconditions.checkState(currentStatement.isDo());
initializer = IR.empty();
incr = IR.assign(IR.name(GENERATOR_DO_WHILE_INITIAL), IR.falseNode());
body = currentStatement.removeFirstChild();
guard = currentStatement.removeFirstChild();
}
Node condition, prestatement;
if (guard.isBlock()) {
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.isBlock() ? incr : IR.exprResult(incr));
}
currentLoopContext.add(0, new LoopContext(generatorCaseCount, continueState, label));
Node beginCase = makeGeneratorMarker(loopBeginState);
Node conditionalBranch = IR.ifNode(condition.isEmpty() ? 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(IR.assign(name, name.removeFirstChild())));
}
hoistRoot.getParent().addChildAfter(IR.var(name.cloneTree()), hoistRoot);
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() : 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()
: IR.name("undefined"),
true)));
}
private static Node createStateUpdate() {
return IR.exprResult(IR.assign(IR.name(GENERATOR_STATE), IR.number(generatorCaseCount)));
}
private static Node createStateUpdate(int state) {
return IR.exprResult(IR.assign(IR.name(GENERATOR_STATE), IR.number(state)));
}
private static Node createIteratorResult(Node value, boolean done) {
return IR.objectlit(
IR.propdef(IR.stringKey("value"), value),
IR.propdef(IR.stringKey("done"), done ? IR.trueNode() : IR.falseNode()));
}
private static Node createSafeBreak() {
Node breakNode = IR.breakNode();
breakNode.setGeneratorSafe(true);
return breakNode;
}
private static Node createFinallyJumpBlock(Node finallyName, int finallyStartState) {
int jumpPoint = generatorCaseCount++;
Node setReturnState = IR.exprResult(IR.assign(finallyName.cloneTree(), 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, int type) {
List matches = new ArrayList<>();
insertAll(node, type, matches);
Preconditions.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, int type, List matchingNodes) {
if (node.getType() == 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:
*
*
* var name = yield expr;
*
*
* For example, change the following code:
*
* return x || yield y;
*
* Into:
*
* var temp$$0;
* if (temp$$0 = x); else temp$$0 = yield y;
* return temp$$0;
*
*
* 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 consts = new HashSet<>();
decomposer =
new ExpressionDecomposer(
compiler,
compiler.getUniqueNameIdSupplier(),
consts,
Scope.createGlobalScope(new Node(Token.SCRIPT)));
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
switch (n.getType()) {
case Token.YIELD:
visitYieldExpression(n);
break;
case Token.DO:
case Token.FOR:
case Token.WHILE:
visitLoop(n);
break;
case Token.CASE:
if (controlCanExit(n.getFirstChild())) {
compiler.report(
JSError.make(
n,
Es6ToEs3Converter.CANNOT_CONVERT_YET,
"Case statements that contain yields"));
return false;
}
break;
default:
break;
}
return true;
}
private void visitYieldExpression(Node n) {
if (n.getParent().isExprResult()) {
return;
}
if (decomposer.canExposeExpression(n)
!= ExpressionDecomposer.DecompositionType.UNDECOMPOSABLE) {
decomposer.exposeExpression(n);
compiler.reportCodeChange();
} else {
compiler.report(
JSError.make(n, Es6ToEs3Converter.CANNOT_CONVERT, "Undecomposable expression"));
}
}
private void visitLoop(Node n) {
Node enclosingFunc = NodeUtil.getEnclosingFunction(n);
if (enclosingFunc == null || !enclosingFunc.isGeneratorFunction() || NodeUtil.isForIn(n)) {
return;
}
Node enclosingBlock = NodeUtil.getEnclosingBlock(n);
Node guard = null;
Node incr = null;
switch (n.getType()) {
case Token.FOR:
guard = n.getSecondChild();
incr = guard.getNext();
break;
case Token.WHILE:
guard = n.getFirstChild();
incr = IR.empty();
break;
case Token.DO:
guard = n.getLastChild();
if (!guard.isEmpty()) {
Node firstEntry = IR.name(GENERATOR_DO_WHILE_INITIAL);
enclosingBlock.addChildToFront(IR.var(firstEntry.cloneTree(), IR.trueNode()));
guard = IR.or(firstEntry, n.getLastChild().detachFromParent());
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(IR.assign(guardName.cloneTree(), guard.cloneTree()))));
container.addChildToBack(guardName.cloneTree());
}
if (!incr.isEmpty()) {
n.addChildBefore(IR.block(IR.exprResult(incr.detachFromParent())), n.getLastChild());
}
enclosingBlock.addChildToFront(IR.var(guardName));
compiler.reportCodeChange();
}
}
private static Node makeGeneratorMarker(int i) {
Node n = IR.exprResult(IR.number(i));
n.setGeneratorMarker(true);
return n;
}
private static final class ControlExitsCheck implements NodeTraversal.Callback {
int continueCatchers;
int breakCatchers;
int throwCatchers;
List 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.getType()) {
case Token.FUNCTION:
return false;
case Token.LABEL:
labels.add(0, n.getFirstChild().getString());
break;
case Token.DO:
case Token.WHILE:
case Token.FOR:
continueCatchers++;
breakCatchers++;
break;
case Token.SWITCH:
breakCatchers++;
break;
case Token.BLOCK:
parent = n.getParent();
if (parent != null
&& parent.isTry()
&& parent.getFirstChild() == n
&& n.getNext().hasChildren()) {
throwCatchers++;
}
break;
case Token.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 Token.CONTINUE:
if (continueCatchers == 0
|| (n.hasChildren() && !labels.contains(n.getFirstChild().getString()))) {
exited = true;
if (addJumps) {
parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
}
}
break;
case Token.THROW:
if (throwCatchers == 0) {
exited = true;
if (addJumps && !n.isGeneratorSafe()) {
parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
}
}
break;
case Token.RETURN:
exited = true;
if (addJumps) {
parent.addChildBefore(createFinallyJumpBlock(finallyName, finallyStartState), n);
}
break;
case Token.YIELD:
exited = true;
break;
default:
break;
}
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getType()) {
case Token.LABEL:
labels.remove(0);
break;
case Token.DO:
case Token.WHILE:
case Token.FOR:
continueCatchers--;
breakCatchers--;
break;
case Token.SWITCH:
breakCatchers--;
break;
case Token.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;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy