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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * 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.AstFactory.type;

import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.colors.Color;
import com.google.javascript.jscomp.colors.StandardColors;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.StaticScope;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import org.jspecify.nullness.Nullable;

/**
 * Converts ES6 generator functions to valid ES3 code. This pass runs after all ES6 features except
 * for yield and generators have been transpiled.
 *
 * 

Genertor transpilation pass uses two sets of node properties: * *

    *
  • generatorMarker property - to indicate that subtee contains YIELD nodes; *
  • generatorSafe property - the node is known to require no further modifications to work in * the transpiled form of the generator body. *
* *

The conversion is done in the following steps: * *

    *
  • Find a generator function: function *() {} *
  • Replace its original body with a template *
  • Mark all nodes in original body that contain any YIELD nodes *
  • Transpile every statement of the original body into replaced template *
      *
    • unmarked nodes may be copied into the template with a trivial transpilation of * "this", "break", "continue", "return" and "arguments" keywords. *
    • marked nodes must be broken up into multiple states to support the yields they * contain. *
    *
* *

{@code Es6RewriteGenerators} depends on {@link InjectTranspilationRuntimeLibraries} to inject * generator_engine.js template. */ final class Es6RewriteGenerators implements CompilerPass { private static final String GENERATOR_FUNCTION = "$jscomp$generator$function"; private static final String GENERATOR_CONTEXT = "$jscomp$generator$context"; private static final String GENERATOR_ARGUMENTS = "$jscomp$generator$arguments"; private static final String GENERATOR_THIS = "$jscomp$generator$this"; private static final String GENERATOR_FORIN_PREFIX = "$jscomp$generator$forin$"; private static final QualifiedName JSCOMP_ASYNC_EXECUTE = QualifiedName.of("$jscomp.asyncExecutePromiseGeneratorFunction"); private static final FeatureSet transpiledFeatures = FeatureSet.BARE_MINIMUM.with(Feature.GENERATORS); private final AbstractCompiler compiler; private final StaticScope namespace; private final AstFactory astFactory; private final @Nullable Color nullableStringType; private final Supplier generatorContextType; private final Supplier propertyIteratorType; Es6RewriteGenerators(AbstractCompiler compiler) { checkNotNull(compiler); this.compiler = compiler; this.astFactory = compiler.createAstFactory(); this.namespace = compiler.getTranspilationNamespace(); if (compiler.hasOptimizationColors()) { // typechecking has run, so we must preserve and propagate type information nullableStringType = Color.createUnion(ImmutableSet.of(StandardColors.NULL_OR_VOID, StandardColors.STRING)); } else { nullableStringType = null; } generatorContextType = Suppliers.memoize( () -> type( astFactory.createNewNode( astFactory.createQName(this.namespace, "$jscomp.generator.Context")))); propertyIteratorType = Suppliers.memoize( () -> type( astFactory.createNewNode( astFactory.createQName( this.namespace, "$jscomp.generator.Context.PropertyIterator")))); } @Override public void process(Node externs, Node root) { TranspilationPasses.processTranspile( compiler, root, transpiledFeatures, new GeneratorFunctionsTranspiler()); TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures); } /** * Exposes expression with yield inside to an equivalent expression in which yield is of the form: * *

   * var name = yield expr;
   * 
* *

For example, changes the following code: * *

   * { return x || yield y; }
   * 
* * into: * *
   * {
   *   var temp$$0;
   *   if (temp$$0 = x); else temp$$0 = yield y;
   *   return temp$$0;
   * }
   * 
* *

Expression should always be inside a block, so that other statements could be added at need. * *

Uses the {@link ExpressionDecomposer} class. */ private class YieldExposer extends NodeTraversal.AbstractPreOrderCallback { final ExpressionDecomposer decomposer; YieldExposer() { decomposer = compiler.createDefaultExpressionDecomposer(); } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { n.setGeneratorMarker(false); if (n.isFunction()) { return false; } if (n.isYield()) { visitYield(n); return false; } return true; } void visitYield(Node n) { if (n.getParent().isExprResult()) { return; } if (decomposer.canExposeExpression(n) != ExpressionDecomposer.DecompositionType.UNDECOMPOSABLE) { decomposer.maybeExposeExpression(n); } 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, TranspilationUtil.CANNOT_CONVERT, message)); } } } /** Finds generator functions and performs ES6 -> ES3 trnspilation */ private class GeneratorFunctionsTranspiler implements NodeTraversal.Callback { int generatorNestingLevel = 0; @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (n.isGeneratorFunction()) { ++generatorNestingLevel; } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isGeneratorFunction()) { new SingleGeneratorFunctionTranspiler(n, --generatorNestingLevel).transpile(); } } } /** Transpiles a single generator function into a state machine program. */ private class SingleGeneratorFunctionTranspiler { final int generatorNestingLevel; /** The transpilation context for the state machine program. */ final TranspilationContext context; /** The body of original generator function that should be transpiled */ final Node originalGeneratorBody; /** * The body of a replacement function. * *

It's a block node that hoists local variables of a generator program and returns an actual * generator object created from that program: * *

     * {
     *   var a;
     *   var b;
     *   ...
     *   return createGenerator(function ($jscomp$generator$context) { ... });
     * }
     * 
* * The assumtion is that the hoist block always ends with a return statement, and all local * variables are added before this "return" statement. */ Node newGeneratorHoistBlock; SingleGeneratorFunctionTranspiler(Node genFunc, int genaratorNestingLevel) { this.generatorNestingLevel = genaratorNestingLevel; this.originalGeneratorBody = genFunc.getLastChild(); this.context = new TranspilationContext(); } /** * Hoists a node inside {@link #newGeneratorHoistBlock}. * * @see #newGeneratorHoistBlock */ private void hoistNode(Node node) { node.insertBefore(newGeneratorHoistBlock.getLastChild()); } /** * Detects whether the generator function was generated by async function transpilation: * *
     *   function() {
     *     ...
     *     return $jscomp.asyncExecutePromiseGeneratorFunction(function* genFunc() {...});
     *   }
     * 
*/ private boolean isTranspiledAsyncFunction(Node generatorFunction) { if (generatorFunction.getParent().isCall() && generatorFunction.getPrevious() != null) { Node callTarget = generatorFunction.getParent().getFirstChild(); if (generatorFunction.getPrevious() == callTarget && generatorFunction.getNext() == null && JSCOMP_ASYNC_EXECUTE.matches(callTarget)) { checkState(generatorFunction.getGrandparent().isReturn()); checkState(generatorFunction.getGrandparent().getNext() == null); return true; } } return false; } public void transpile() { Node generatorFunction = originalGeneratorBody.getParent(); checkState(generatorFunction.isGeneratorFunction()); generatorFunction.putBooleanProp(Node.GENERATOR_FN, false); // A "program" function: // function ($jscomp$generator$context) { // } final Node program; // function(!Context): (void|{value: YIELD_TYPE}) Color programType = StandardColors.TOP_OBJECT; Node generatorBody = astFactory.createBlock(); final Node changeScopeNode; if (isTranspiledAsyncFunction(generatorFunction)) { // Our generatorFunction is a transpiled async function // $jscomp.asyncExecutePromiseGeneratorFunction Node callTarget = generatorFunction.getPrevious(); checkState(callTarget.isGetProp()); // Use original async function as a hoist block for local generator variables: // generator function -> call -> return -> async function body newGeneratorHoistBlock = generatorFunction.getGrandparent().getParent(); checkState(newGeneratorHoistBlock.isBlock(), newGeneratorHoistBlock); changeScopeNode = NodeUtil.getEnclosingFunction(newGeneratorHoistBlock); checkState(changeScopeNode.isFunction(), changeScopeNode); // asyncExecutePromiseGeneratorFunction => asyncExecutePromiseGeneratorProgram callTarget.setString("asyncExecutePromiseGeneratorProgram"); program = originalGeneratorBody.getParent(); // function *() {...} => function *(context) {} originalGeneratorBody.getPrevious().addChildToBack( context.getJsContextNameNode(originalGeneratorBody)); originalGeneratorBody.replaceWith(generatorBody); } else { changeScopeNode = generatorFunction; Node genFuncName = generatorFunction.getFirstChild(); checkState(genFuncName.isName()); // The transpiled function needs to be able to refer to itself, so make sure it has a name. if (genFuncName.getString().isEmpty()) { genFuncName.setString(context.getScopedName(GENERATOR_FUNCTION)); if (astFactory.isAddingColors()) { genFuncName.setColor(StandardColors.TOP_OBJECT); } } // Prepare a "program" function: // function ($jscomp$generator$context) { // } program = astFactory.createFunction( "", IR.paramList(context.getJsContextNameNode(originalGeneratorBody)), generatorBody, type(programType)); // $jscomp.generator.createGenerator Node createGenerator = astFactory.createQName(namespace, "$jscomp.generator.createGenerator"); // Replace original generator function body with: // return $jscomp.generator.createGenerator(, ); newGeneratorHoistBlock = astFactory.createBlock( astFactory .createReturn( // TODO(b/142881197): we can't give a more accurate type right now. astFactory.createCallWithUnknownType( createGenerator, // function name passed as parameter must have the type of the // generator function itself astFactory .createName(genFuncName.getString(), type(generatorFunction)) .srcref(genFuncName), program)) .srcrefTree(originalGeneratorBody)); originalGeneratorBody.replaceWith(newGeneratorHoistBlock); } // New scopes and any changes to scopes should be reported individually. compiler.reportChangeToChangeScope(program); NodeTraversal.traverse(compiler, originalGeneratorBody, new YieldNodeMarker()); // Test if end of generator function is reachable boolean shouldAddFinalJump = !isEndOfBlockUnreachable(originalGeneratorBody); // Transpile statements from original generator function while (originalGeneratorBody.hasChildren()) { transpileStatement(originalGeneratorBody.removeFirstChild()); } // Ensure that the state machine program ends Node finalBlock = astFactory.createBlock(); if (shouldAddFinalJump) { finalBlock.addChildToBack( context.callContextMethodResult( originalGeneratorBody, "jumpToEnd", type(StandardColors.NULL_OR_VOID))); } context.currentCase.jumpTo(context.programEndCase, finalBlock); context.currentCase.mayFallThrough = true; context.finalizeTransformation(generatorBody); context.checkStateIsEmpty(); compiler.reportChangeToChangeScope(changeScopeNode); } /** @see #transpileStatement(Node, TranspilationContext.Case, TranspilationContext.Case) */ void transpileStatement(Node statement) { transpileStatement(statement, null, null); } /** * Transpiles a detached node and adds transpiled version of it to the {@link * TranspilationContext.Case#currentCase currentCase} of the {@link #context}. * * @param statement Node to transpile */ void transpileStatement( Node statement, TranspilationContext.@Nullable Case breakCase, TranspilationContext.@Nullable Case continueCase) { checkState(IR.mayBeStatement(statement)); checkState(statement.getParent() == null); if (!statement.isGeneratorMarker()) { transpileUnmarkedNode(statement); return; } switch (statement.getToken()) { case LABEL: transpileLabel(statement); break; case BLOCK: transpileBlock(statement); break; case EXPR_RESULT: transpileExpressionResult(statement); break; case VAR: transpileVar(statement); break; case RETURN: transpileReturn(statement); break; case THROW: transpileThrow(statement); break; case IF: transpileIf(statement, breakCase); break; case FOR: transpileFor(statement, breakCase, continueCase); break; case FOR_IN: transpileForIn(statement, breakCase, continueCase); break; case WHILE: transpileWhile(statement, breakCase, continueCase); break; case DO: transpileDo(statement, breakCase, continueCase); break; case TRY: transpileTry(statement, breakCase); break; case SWITCH: transpileSwitch(statement, breakCase); break; default: throw new IllegalStateException("Unsupported token: " + statement.getToken()); } } /** Transpiles code that doesn't contain yields. */ void transpileUnmarkedNode(Node n) { checkState(!n.isGeneratorMarker()); if (n.isFunction()) { // All function statemnts will be normalized: // "function a() {}" => "var a = function() {};" // so we have to move them to the outer scope. String functionName = n.getFirstChild().getString(); // Make sure there are no "function (...) {...}" statements (note that // "function *(...) {...}" becomes "function $jscomp$generator$function(...) {...}" as // inner generator functions are transpiled first). checkState(!functionName.isEmpty() && !functionName.startsWith(GENERATOR_FUNCTION)); hoistNode(n); return; } context.transpileUnmarkedBlock(n.isBlock() || n.isAddedBlock() ? n : IR.block(n)); } /** Transpiles a label with marked statement. */ void transpileLabel(Node n) { // Collect all labels names in "a: b: c: {}" statement ArrayList labelNames = new ArrayList<>(); while (n.isLabel()) { labelNames.add(n.removeFirstChild()); n = n.removeFirstChild(); } // Push label names and continue transpilation TranspilationContext.Case continueCase = NodeUtil.isLoopStructure(n) ? context.createCase() : null; TranspilationContext.Case breakCase = context.createCase(); context.pushLabels(labelNames, breakCase, continueCase); transpileStatement(n, breakCase, continueCase); context.popLabels(labelNames); // Switch to endCase if it's not yet active. if (breakCase != context.currentCase) { context.switchCaseTo(breakCase); } } /** Transpiles a block. */ void transpileBlock(Node n) { while (n.hasChildren()) { transpileStatement(n.removeFirstChild()); } } /** Transpiles marked expression result statement. */ void transpileExpressionResult(Node n) { Node exposedExpression = exposeYieldAndTranspileRest(n.removeFirstChild()); Node decomposed = transpileYields(exposedExpression); // Tanspile "a = yield;" into "a = $context.yieldResult;" // But don't transpile "yield;" into "$context.yieldResult;" // As it influences the collapsing of empty case sections. if (!exposedExpression.isYield()) { n.addChildToFront(prepareNodeForWrite(decomposed)); n.setGeneratorMarker(false); context.writeGeneratedNode(n); } } /** Transpiles marked "var" statement. */ void transpileVar(Node n) { n.setGeneratorMarker(false); Node newVars = n.cloneNode(); while (n.hasChildren()) { Node var; // Just collect all unmarked vars and transpile them together. while ((var = n.removeFirstChild()) != null && !var.isGeneratorMarker()) { newVars.addChildToBack(var); } if (newVars.hasChildren()) { transpileUnmarkedNode(newVars); newVars = n.cloneNode(); } // Transpile marked var if (var != null) { checkState(var.isGeneratorMarker()); var.addChildToFront(maybeDecomposeExpression(var.removeFirstChild())); var.setGeneratorMarker(false); newVars.addChildToBack(var); } } // Flush the vars if not empty if (newVars.hasChildren()) { transpileUnmarkedNode(newVars); } } /** Transpiles marked "return" statement. */ void transpileReturn(Node n) { n.addChildToFront( context.returnExpression( n, prepareNodeForWrite(maybeDecomposeExpression(n.removeFirstChild())))); context.writeGeneratedNode(n); context.currentCase.mayFallThrough = false; } /** Transpiles marked "throw" statement. */ void transpileThrow(Node n) { n.addChildToFront(prepareNodeForWrite(maybeDecomposeExpression(n.removeFirstChild()))); context.writeGeneratedNode(n); context.currentCase.mayFallThrough = false; } /** Exposes YIELD operator so it's free of side effects transpiling some code on the way. */ Node exposeYieldAndTranspileRest(Node n) { checkState(n.isGeneratorMarker()); if (n.isYield()) { return n; } // Assuming the initial node is "a + (a = b) + (b = yield) + a". // YieldExposer may break n up into multiple statements. // Place n into a temporary block to hold those statements: // { // var JSCompiler_temp_const$jscomp$0 = a + (a = b); // return JSCompiler_temp_const$jscomp$0 + (b = yield) + a; // } // Need to put expression nodes into return node so that they always stay expression nodes // If expression put into expression result YieldExposer may turn it into an "if" statement. boolean isExpression = IR.mayBeExpression(n); Node block = astFactory.createBlock(isExpression ? astFactory.createReturn(n) : n); NodeTraversal.traverse(compiler, n, new YieldExposer()); // Make sure newly created statements are correctly marked for recursive transpileStatement() // calls. NodeTraversal.traverse(compiler, block, new YieldNodeMarker()); // The last child of decomposed block free of side effects. Node decomposed = block.getLastChild().detach(); transpileStatement(block); return isExpression ? decomposed.removeFirstChild() : decomposed; } /** Converts an expression node containing YIELD into an unmarked analogue. */ Node maybeDecomposeExpression(@Nullable Node n) { if (n == null || !n.isGeneratorMarker()) { return n; } return transpileYields(exposeYieldAndTranspileRest(n)); } /** * Makes unmarked node containing arbitary code suitable to write using {@link * TranspilationContext#writeGeneratedNode} method. */ @Nullable Node prepareNodeForWrite(@Nullable Node n) { if (n == null) { return null; } // Need to wrap a node so it can be replaced in the tree with some other node if nessesary. Node wrapper = IR.mayBeStatement(n) ? astFactory.createBlock(n) : astFactory.exprResult(n); NodeTraversal.traverse(compiler, wrapper, context.new UnmarkedNodeTranspiler()); checkState(wrapper.hasOneChild()); return wrapper.removeFirstChild(); } /** Converts node with YIELD into $jscomp$generator$context.yieldResult. */ Node transpileYields(Node n) { if (!n.isGeneratorMarker()) { // In some cases exposing yield causes it to disapear from the resulting statement. // I.e. the following node: "0 || yield;" becomes: // { // var JSCompiler_temp$jscomp$0; // if (JSCompiler_temp$jscomp$0 = 0); else JSCompiler_temp$jscomp$0 = yield; // } // JSCompiler_temp$jscomp$0; // This is our resulting statement. return n; } TranspilationContext.Case jumpToSection = context.createCase(); Node yieldNode = findYield(n); Node yieldExpression = prepareNodeForWrite(maybeDecomposeExpression(yieldNode.removeFirstChild())); if (yieldNode.isYieldAll()) { context.yieldAll(yieldExpression, jumpToSection, yieldNode); } else { context.yield(yieldExpression, jumpToSection, yieldNode); } context.switchCaseTo(jumpToSection); Node yieldResult = context.yieldResult(yieldNode); if (yieldNode == n) { return yieldResult; } // Replace YIELD with $context.yeildResult yieldNode.replaceWith(yieldResult); // Remove generator markings from subtree while (yieldResult != n) { yieldResult = yieldResult.getParent(); yieldResult.setGeneratorMarker(false); } return n; } /** Transpiles marked "if" stetement. */ void transpileIf(Node n, TranspilationContext.@Nullable Case breakCase) { // Decompose condition first Node condition = maybeDecomposeExpression(n.removeFirstChild()); Node ifBlock = n.getFirstChild(); Node elseBlock = ifBlock.getNext(); // No transpilation is needed if (!ifBlock.isGeneratorMarker() && (elseBlock == null || !elseBlock.isGeneratorMarker())) { n.addChildToFront(condition); n.setGeneratorMarker(false); transpileUnmarkedNode(n); return; } ifBlock.detach(); if (elseBlock == null) { // No "else" block, just create an empty one as will need it anyway. elseBlock = astFactory.createBlock().srcref(n); } else { elseBlock.detach(); } // Only "else" block is unmarked, swap "if" and "else" blocks and negate the condition. if (ifBlock.isGeneratorMarker() && !elseBlock.isGeneratorMarker()) { condition = astFactory.createNot(condition).srcref(condition); Node tmpNode = ifBlock; ifBlock = elseBlock; elseBlock = tmpNode; } // Unmarked "if" block (marked "else") if (!ifBlock.isGeneratorMarker()) { TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); Node jumoToBlock = context.createJumpToBlock(endCase, /** allowEmbedding=*/ false, ifBlock); while (jumoToBlock.hasChildren()) { Node jumpToNode = jumoToBlock.removeFirstChild(); jumpToNode.setGeneratorSafe(true); ifBlock.addChildToBack(jumpToNode); } transpileUnmarkedNode(astFactory.createIf(condition, ifBlock).srcref(n)); transpileStatement(elseBlock); context.switchCaseTo(endCase); return; } TranspilationContext.Case ifCase = context.createCase(); TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); // "if" and "else" blocks marked condition = prepareNodeForWrite(condition); Node newIfBlock = context.createJumpToBlock(ifCase, /** allowEmbedding=*/ true, n); context.writeGeneratedNode( astFactory.createIf(prepareNodeForWrite(condition), newIfBlock).srcref(n)); transpileStatement(elseBlock); context.writeJumpTo(endCase, elseBlock); context.switchCaseTo(ifCase); transpileStatement(ifBlock); context.switchCaseTo(endCase); } /** Transpiles marked "for" statement. */ void transpileFor( Node n, TranspilationContext.@Nullable Case breakCase, TranspilationContext.@Nullable Case continueCase) { // Decompose init first Node init = maybeDecomposeExpression(n.removeFirstChild()); Node condition = n.getFirstChild(); Node increment = condition.getNext(); Node body = increment.getNext(); // No transpilation is needed if (!condition.isGeneratorMarker() && !increment.isGeneratorMarker() && !body.isGeneratorMarker()) { n.addChildToFront(init); n.setGeneratorMarker(false); transpileUnmarkedNode(n); return; } // Move init expression out of for loop. if (!init.isEmpty()) { if (IR.mayBeExpression(init)) { // Convert expression into expression result. init = astFactory.exprResult(init).srcref(init); } transpileUnmarkedNode(init); } TranspilationContext.Case startCase = context.createCase(); TranspilationContext.Case incrementCase = context.maybeCreateCase(continueCase); TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); context.switchCaseTo(startCase); // Transpile condition expression if (!condition.isEmpty()) { condition = prepareNodeForWrite(maybeDecomposeExpression(condition.detach())); context.writeGeneratedNode( astFactory .createIf( astFactory.createNot(condition).srcref(condition), context.createJumpToBlock( endCase, /** allowEmbedding= */ true, n)) .srcref(n)); } // Transpile "for" body context.pushBreakContinueContext(endCase, incrementCase); transpileStatement(body.detach()); context.popBreakContinueContext(); // Transpile increment expression context.switchCaseTo(incrementCase); if (!increment.isEmpty()) { increment = maybeDecomposeExpression(increment.detach()); transpileUnmarkedNode(astFactory.exprResult(increment).srcref(increment)); } context.writeJumpTo(startCase, n); context.switchCaseTo(endCase); } /** * Transpile "for in" statement by converting it into "for". * *

for (var i in expr) {} will be converted into * for (var i, $for$in = $context.forIn(expr); i = $for$in.getNext(); ) {} */ void transpileForIn( Node n, TranspilationContext.@Nullable Case breakCase, TranspilationContext.@Nullable Case continueCase) { // Decompose condition first Node detachedExpr = maybeDecomposeExpression(n.getSecondChild().detach()); Node target = n.getFirstChild(); Node body = n.getSecondChild(); // No transpilation is needed if (!target.isGeneratorMarker() && !body.isGeneratorMarker()) { detachedExpr.insertAfter(target); n.setGeneratorMarker(false); transpileUnmarkedNode(n); return; } // Prepare a new init statement final Node init; if (target.detach().isVar()) { // "var i in x" => "var i" checkState(!target.isGeneratorMarker()); init = target; checkState(!init.getFirstChild().hasChildren()); target = init.getFirstChild().cloneNode(); } else { // "i in x" => "var" init = new Node(Token.VAR).srcref(target); } // "$for$in" Node child = context.callContextMethod(target, "forIn", propertyIteratorType.get(), detachedExpr); Node forIn = astFactory .createName( context.getScopedName( GENERATOR_FORIN_PREFIX + compiler.getUniqueNameIdSupplier().get()), type(child)) .srcref(target); // "$context.forIn(x)" forIn.addChildToFront(child); // "var ..., $for$in = $context.forIn(expr)" init.addChildToBack(forIn); // "$for$in.getNext()" Node forInGetNext = astFactory .createGetProp(forIn.cloneNode(), "getNext", type(StandardColors.TOP_OBJECT)) .srcref(detachedExpr); // "(i = $for$in.getNext()) != null" Node forCond = astFactory .createNe( astFactory .createAssign( target.setColor(nullableStringType), astFactory .createCall(forInGetNext, type(nullableStringType)) .srcref(detachedExpr)) .srcref(detachedExpr), astFactory.createNull().srcref(forIn)) .srcref(detachedExpr); forCond.setGeneratorMarker(target.isGeneratorMarker()); // Prepare "for" statement. // "for (var i, $for$in = $context.forIn(expr); (i = $for$in.getNext()) != null; ) {}" Node forNode = IR.forNode(init, forCond, IR.empty().srcref(n), body.detach()).srcref(n); // Transpile "for" instead of "for in". transpileFor(forNode, breakCase, continueCase); } /** Transpiles "while" statement. */ void transpileWhile( Node n, TranspilationContext.@Nullable Case breakCase, TranspilationContext.@Nullable Case continueCase) { TranspilationContext.Case startCase = context.maybeCreateCase(continueCase); TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); context.switchCaseTo(startCase); // Transpile condition Node condition = prepareNodeForWrite(maybeDecomposeExpression(n.removeFirstChild())); Node body = n.removeFirstChild(); context.writeGeneratedNode( astFactory .createIf( astFactory.createNot(condition).srcref(condition), context.createJumpToBlock( endCase, /** allowEmbedding= */ true, n)) .srcref(n)); // Transpile "while" body context.pushBreakContinueContext(endCase, startCase); transpileStatement(body); context.popBreakContinueContext(); context.writeJumpTo(startCase, n); context.switchCaseTo(endCase); } /** Transpiles "do while" statement. */ void transpileDo( Node n, TranspilationContext.@Nullable Case breakCase, TranspilationContext.@Nullable Case continueCase) { TranspilationContext.Case startCase = context.createCase(); breakCase = context.maybeCreateCase(breakCase); continueCase = context.maybeCreateCase(continueCase); context.switchCaseTo(startCase); // Transpile body Node body = n.removeFirstChild(); context.pushBreakContinueContext(breakCase, continueCase); transpileStatement(body); context.popBreakContinueContext(); // Transpile condition context.switchCaseTo(continueCase); Node condition = prepareNodeForWrite(maybeDecomposeExpression(n.removeFirstChild())); context.writeGeneratedNode( astFactory .createIf( condition, context.createJumpToBlock( startCase, /** allowEmbedding= */ false, n)) .srcref(n)); context.switchCaseTo(breakCase); } /** Transpiles "try" statement */ void transpileTry(Node n, TranspilationContext.@Nullable Case breakCase) { Node tryBlock = n.removeFirstChild(); Node catchBlock = n.removeFirstChild(); Node finallyBlock = n.removeFirstChild(); TranspilationContext.Case catchCase = catchBlock.hasChildren() ? context.createCase() : null; TranspilationContext.Case finallyCase = finallyBlock == null ? null : context.createCase(); TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); // Transpile "try" block context.enterTryBlock(catchCase, finallyCase, tryBlock); transpileStatement(tryBlock); if (finallyBlock == null) { context.leaveTryBlock(catchCase, endCase, tryBlock); } else { // Transpile "finally" block context.switchCaseTo(finallyCase); context.enterFinallyBlock(catchCase, finallyCase, finallyBlock); transpileStatement(finallyBlock); context.leaveFinallyBlock(endCase, finallyBlock); } // Transpile "catch" block if (catchBlock.hasChildren()) { checkState(catchBlock.getFirstChild().isCatch()); context.switchCaseTo(catchCase); Node exceptionName = catchBlock.getFirstFirstChild().detach(); context.enterCatchBlock(finallyCase, exceptionName); Node catchBody = catchBlock.getFirstFirstChild().detach(); checkState(catchBody.isBlock()); transpileStatement(catchBody); context.leaveCatchBlock(finallyCase, catchBody); } context.switchCaseTo(endCase); } // Transpiles "switch" statement. void transpileSwitch(Node n, TranspilationContext.@Nullable Case breakCase) { // Transpile condition first n.addChildToFront(maybeDecomposeExpression(n.removeFirstChild())); // Are all "switch" cases unmarked? boolean hasGeneratorMarker = false; for (Node caseSection = n.getSecondChild(); caseSection != null; caseSection = caseSection.getNext()) { if (caseSection.isGeneratorMarker()) { hasGeneratorMarker = true; break; } } // No transpilation is needed if (!hasGeneratorMarker) { n.setGeneratorMarker(false); transpileUnmarkedNode(n); return; } /** Stores a detached body of a case statement and a case section assosiated with it. */ class SwitchCase { private final TranspilationContext.Case generatedCase; private final Node body; SwitchCase(TranspilationContext.Case generatedCase, Node caseNode) { this.generatedCase = generatedCase; this.body = caseNode; } } // TODO(skill): Don't move all case sections. ArrayList detachedCases = new ArrayList<>(); // We don't have to transpile unmarked cases at the beginning of "switch". boolean canSkipUnmarkedCases = true; for (Node caseSection = n.getSecondChild(); caseSection != null; caseSection = caseSection.getNext()) { if (!caseSection.isDefaultCase() && caseSection.getFirstChild().isGeneratorMarker()) { // Following example is possible to transpile, but it's not trivial. // switch (cond) { // case yield "test": break; // case 5 + yield: break; // } compiler.report( JSError.make( n, TranspilationUtil.CANNOT_CONVERT_YET, "Case statements that contain yields")); return; } Node body = caseSection.getLastChild(); if (!body.hasChildren() || (canSkipUnmarkedCases && !body.isGeneratorMarker())) { // Can skip empty or unmarked case. continue; } // Check whether we can start skipping unmarked cases again canSkipUnmarkedCases = isEndOfBlockUnreachable(body); // Move case's body under a global switch statement... // Allocate a new case TranspilationContext.Case generatedCase = context.createCase(); generatedCase.caseBlock.srcref(body); // Replace old body with a jump instruction. Node newBody = IR.block(context.createJumpToNode(generatedCase, body)); newBody.setIsAddedBlock(true); newBody.setGeneratorSafe(true); // make sure we don't transpile generated "jump" instruction body.replaceWith(newBody); // Remember the body and the case under which the body will be moved. detachedCases.add(new SwitchCase(generatedCase, body)); caseSection.setGeneratorMarker(false); } TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); // Transpile the barebone of original "switch" statement n.setGeneratorMarker(false); transpileUnmarkedNode(n); context.writeJumpTo(endCase, n); // TODO(skill): do not always add this. // Transpile all detached case bodies context.pushBreakContext(endCase); for (SwitchCase detachedCase : detachedCases) { TranspilationContext.Case generatedCase = detachedCase.generatedCase; context.switchCaseTo(generatedCase); transpileStatement(detachedCase.body); } context.popBreakContext(); context.switchCaseTo(endCase); } /** Finds the only YIELD node in a tree. */ Node findYield(Node n) { YieldFinder yieldFinder = new YieldFinder(); NodeTraversal.traverse(compiler, n, yieldFinder); return yieldFinder.getYieldNode(); } /** Finds the only YIELD node in a tree. */ private class YieldFinder extends NodeTraversal.AbstractPreOrderCallback { private Node yieldNode; @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (n.isFunction()) { return false; } if (n.isYield()) { checkState(yieldNode == null); yieldNode = n; return false; } return true; } Node getYieldNode() { checkNotNull(yieldNode); return yieldNode; } } /** * Returns whether any statements added to the end of the block would be unreachable. * *

It's OK for this method to return false-negatives. */ private boolean isEndOfBlockUnreachable(Node block) { checkState(block.isBlock()); if (!block.hasChildren()) { return false; } switch (block.getLastChild().getToken()) { case BLOCK: return isEndOfBlockUnreachable(block.getLastChild()); case RETURN: case THROW: case CONTINUE: case BREAK: return true; default: return false; } } /** State machine context that is used during generator function transpilation. */ private class TranspilationContext { final HashMap namedLabels = new HashMap<>(); final ArrayDeque breakCases = new ArrayDeque<>(); final ArrayDeque continueCases = new ArrayDeque<>(); final ArrayDeque catchCases = new ArrayDeque<>(); final ArrayDeque finallyCases = new ArrayDeque<>(); final HashSet catchNames = new HashSet<>(); /** All "case" sections that will be added to generator program. */ final ArrayList allCases = new ArrayList<>(); /** All "break" nodes that exit from the generator primary switch statement */ final ArrayList switchBreaks = new ArrayList<>(); /** A virtual case that indicates end of program */ final Case programEndCase; /** Most recently assigned id. */ int caseIdCounter; /** * Points to the switch case that is being populated with transpiled instructions from the * original generator function that is being transpiled. */ Case currentCase; int nestedFinallyBlockCount = 0; boolean thisReferenceFound; boolean argumentsReferenceFound; TranspilationContext() { programEndCase = new Case(); checkState(programEndCase.id == 0); currentCase = new Case(); checkState(currentCase.id == 1); allCases.add(currentCase); } /** * Removes unnesesary cases. * *

This optimization is needed to reduce number of switch cases, which is used then to * generate even shorter state machine programs. */ void optimizeCaseIds() { checkState(!allCases.isEmpty(), allCases); // Shortcut jump chains: // case 100: // $context.yield("something", 101); // break; // case 101: // $context.jumpTo(102); // break; // case 102: // $context.jumpTo(200); // break; // becomes: // case 100: // $context.yield("something", 200); // break; // case 101: // $context.jumpTo(102); // break; // case 102: // $context.jumpTo(200); // break; for (Case currentCase : allCases) { if (currentCase.jumpTo != null) { // Flatten jumps chains: // 1 -> 2 // 2 -> 8 // 8 -> 300 // to: // 1 -> 300 // 2 -> 300 // 8 -> 300 while (currentCase.jumpTo.jumpTo != null) { currentCase.jumpTo = currentCase.jumpTo.jumpTo; } if (currentCase.embedInto != null && currentCase.references.size() == 1) { currentCase.jumpTo.embedInto = currentCase.embedInto; } currentCase.embedInto = null; // Update references to jump to the final case in the chain for (Node reference : currentCase.references) { reference.setDouble(currentCase.jumpTo.id); } currentCase.jumpTo.references.addAll(currentCase.references); currentCase.references.clear(); } } // Merge cases without any references with the previous case: // case 100: // doSomething(); // case 101: // doSomethingElse(); // break; // case 102: // doEvenMore(); // becomes: // case 100: // doSomething(); // doSomethingElse(); // break; Iterator it = allCases.iterator(); Case prevCase = it.next(); checkState(prevCase.id == 1); while (it.hasNext()) { Case currentCase = it.next(); if (currentCase.references.isEmpty()) { // No jump references, just append the body to a previous case if needed. checkState(currentCase.embedInto == null); if (prevCase.mayFallThrough) { prevCase.caseBlock.addChildrenToBack(currentCase.caseBlock.removeChildren()); prevCase.mayFallThrough = currentCase.mayFallThrough; } it.remove(); continue; } if (currentCase.embedInto != null) { checkState(currentCase.jumpTo == null); // Cases can be embedded only if they are referenced once and don't fall through. if (currentCase.references.size() == 1 && !currentCase.mayFallThrough) { currentCase.embedInto.replaceWith(currentCase.caseBlock); it.remove(); continue; } } if (prevCase.jumpTo == currentCase) { // Merging "case 1:" with the following case. The standard merging cannot be used // as "case 1:" is an entry point and it cannot be renamed. // case 1: // case 2: // doSomethingElse(); // break; // case 102: // $context.jumpTo(2); // break; // becomes: // case 1: // doSomethingElse(); // break; // case 102: // $context.jumpTo(1); // break; checkState(prevCase.mayFallThrough); checkState(!prevCase.caseBlock.hasChildren()); checkState(currentCase.jumpTo == null); prevCase.caseBlock.addChildrenToBack(currentCase.caseBlock.removeChildren()); prevCase.mayFallThrough = currentCase.mayFallThrough; for (Node reference : currentCase.references) { reference.setDouble(prevCase.id); } prevCase.jumpTo = currentCase.jumpTo; prevCase.references.addAll(currentCase.references); it.remove(); continue; } prevCase = currentCase; } } /** * Replaces "...; break;" with "return ...;". */ void eliminateSwitchBreaks() { for (Node breakNode : switchBreaks) { Node prevStatement = breakNode.getPrevious(); checkState(prevStatement != null); checkState(prevStatement.isExprResult()); prevStatement.replaceWith(IR.returnNode(prevStatement.removeFirstChild())); breakNode.detach(); } switchBreaks.clear(); } /** Finalizes transpilation by dumping all generated "case" nodes. */ public void finalizeTransformation(Node generatorBody) { optimizeCaseIds(); // If number of cases is small we render them without using "switch" // switch ($context.nextAddress) { // case 1: a(); // case 2: b(); // case 3: c(); // } // are rendered as: // if ($context.nextAddress == 1) a(); // if ($context.nextAddress != 3) b(); // c(); if (allCases.size() == 2 || allCases.size() == 3) { generatorBody.addChildToBack( astFactory .createIf( astFactory.createEq( getContextField(generatorBody, "nextAddress"), astFactory.createNumber(1)), allCases.remove(0).caseBlock) .srcrefTreeIfMissing(generatorBody)); } // If number of cases is small we render them without using "switch" // switch ($context.nextAddress) { // case 1: a(); // case 2: b(); // } // are rendered as: // if ($context.nextAddress == 1) a(); // b(); if (allCases.size() == 2) { generatorBody.addChildToBack( astFactory .createIf( astFactory.createNe( getContextField(generatorBody, "nextAddress"), astFactory.createNumber(allCases.get(1).id)), allCases.remove(0).caseBlock) .srcrefTreeIfMissing(generatorBody)); } // If number of cases is small we render them without using "switch" // switch ($context.nextAddress) { // case 1: a(); // } // are rendered as: // a(); if (allCases.size() == 1) { generatorBody.addChildrenToBack(allCases.remove(0).caseBlock.removeChildren()); eliminateSwitchBreaks(); return; } // switch ($jscomp$generator$context.nextAddress) {} Node switchNode = IR.switchNode(getContextField(generatorBody, "nextAddress")).srcref(generatorBody); generatorBody.addChildToBack(switchNode); // Populate "switch" statement with "case"s. for (Case currentCase : allCases) { switchNode.addChildToBack(currentCase.createCaseNode()); } allCases.clear(); } /** Ensures that the context has an empty state. */ public void checkStateIsEmpty() { checkState(namedLabels.isEmpty()); checkState(breakCases.isEmpty()); checkState(continueCases.isEmpty()); checkState(catchCases.isEmpty()); checkState(finallyCases.isEmpty()); checkState(nestedFinallyBlockCount == 0); checkState(allCases.isEmpty()); } /** Adds a block of original code to the end of the current case. */ void transpileUnmarkedBlock(Node block) { if (block.hasChildren()) { NodeTraversal.traverse(compiler, block, new UnmarkedNodeTranspiler()); while (block.hasChildren()) { writeGeneratedNode(block.removeFirstChild()); } } } /** Adds a new generated node to the end of the current case. */ void writeGeneratedNode(Node n) { currentCase.addNode(n); } /** Adds a new generated node to the end of the current case and finializes it. */ void writeGeneratedNodeAndBreak(Node n) { writeGeneratedNode(n); writeGeneratedNode(createBreakNodeFor(n)); currentCase.mayFallThrough = false; } /** Creates a new detached case statement. */ Case createCase() { return new Case(); } /** Returns a passed case object or creates a new one if it's null. */ Case maybeCreateCase(@Nullable Case other) { if (other != null) { return other; } return createCase(); } /** Returns the name node of context parameter passed to the program. */ Node getJsContextNameNode(Node sourceNode) { return astFactory .createName(getScopedName(GENERATOR_CONTEXT), generatorContextType.get()) .srcref(sourceNode); } /** Returns unique name in the current context. */ String getScopedName(String name) { return name + (generatorNestingLevel == 0 ? "" : "$" + generatorNestingLevel); } /** Creates node that access a specified field of the current context. */ Node getContextField(Node sourceNode, String fieldName) { return astFactory .createGetPropWithUnknownType(getJsContextNameNode(sourceNode), fieldName) .srcref(sourceNode); } /** Creates node that make a call to a context function. */ Node callContextMethod( Node sourceNode, String methodName, AstFactory.Type type, Node... args) { Node contextField = getContextField(sourceNode, methodName); return astFactory.createCall(contextField, type, args).srcref(sourceNode); } /** Creates node that make a call to a context function. */ Node callContextMethodResult( Node sourceNode, String methodName, AstFactory.Type type, Node... args) { return astFactory .exprResult(callContextMethod(sourceNode, methodName, type, args)) .srcref(sourceNode); } /** Creates node that returns the result of a call to a context function. */ Node returnContextMethod( Node sourceNode, String methodName, AstFactory.Type type, Node... args) { return astFactory .createReturn(callContextMethod(sourceNode, methodName, type, args)) .srcref(sourceNode); } /** * Creates a "break;" statement that will follow {@code preBreak} node. * *

This is used to be able to generatate a state machine program outside of "swtich" * statement so: * *

       *   $context.jumpTo(5);
       *   break;
       * 
* * could be converted into: * *
       *   return $context.jumpTo(5);
       * 
*/ Node createBreakNodeFor(Node preBreak) { Node breakNode = IR.breakNode().srcref(preBreak); switchBreaks.add(breakNode); return breakNode; } /** * Returns a node that instructs a state machine program to jump to a selected case section. */ Node createJumpToNode(Case section, Node sourceNode) { return returnContextMethod( sourceNode, "jumpTo", type(StandardColors.NULL_OR_VOID), section.getNumber(sourceNode)); } /** Instructs a state machine program to jump to a selected case section. */ void writeJumpTo(Case section, Node sourceNode) { currentCase.jumpTo( section, createJumpToBlock(section, /** allowEmbedding=*/ false, sourceNode)); } /** * Creates a block node that contains a jump instruction. * * @param allowEmbedding Whether the code from the target section can be embedded into jump * block. */ Node createJumpToBlock(Case section, boolean allowEmbedding, Node sourceNode) { checkState(section.embedInto == null); Node jumpBlock = astFactory .createBlock( callContextMethodResult( sourceNode, "jumpTo", type(StandardColors.NULL_OR_VOID), section.getNumber(sourceNode)), createBreakNodeFor(sourceNode)) .srcref(sourceNode); if (allowEmbedding) { section.embedInto = jumpBlock; } return jumpBlock; } /** Converts "break" and "continue" statements into state machine jumps. */ void replaceBreakContinueWithJump(Node sourceNode, Case section, int breakSuppressors) { final String jumpMethod; if (finallyCases.isEmpty() || finallyCases.getFirst().id < section.id) { // There are no finally blocks that should be exectuted pior to jumping jumpMethod = "jumpTo"; } else { // There are some finally blocks that should be exectuted before we can break/continue. checkState(finallyCases.getFirst().id != section.id); jumpMethod = "jumpThroughFinallyBlocks"; } if (breakSuppressors == 0) { // continue; => $context.jumpTo(x); break; callContextMethodResult( sourceNode, jumpMethod, type(StandardColors.NULL_OR_VOID), section.getNumber(sourceNode)) .insertBefore(sourceNode); sourceNode.replaceWith(createBreakNodeFor(sourceNode)); } else { // "break;" inside a loop or swtich statement: // for (...) { // break l1; // } // becomes: // for (...) { // loop doesn't allow to use "break" to advance to the // return $context.jumpTo(x); // next address, so "return" is used instead. // } sourceNode.replaceWith( returnContextMethod( sourceNode, jumpMethod, type(StandardColors.NULL_OR_VOID), section.getNumber(sourceNode))); } } /** * Instructs a state machine program to yield a value and then jump to a selected case * section. */ void yield( @Nullable Node expression, TranspilationContext.Case jumpToSection, Node sourceNode) { ArrayList args = new ArrayList<>(); args.add( expression == null ? astFactory.createUndefinedValue().srcrefTree(sourceNode) : expression); args.add(jumpToSection.getNumber(sourceNode)); context.writeGeneratedNode( returnContextMethod( sourceNode, "yield", type(StandardColors.UNKNOWN), args.toArray(new Node[0]))); context.currentCase.mayFallThrough = false; } /** * Instructs a state machine program to yield all values and then jump to a selected case * section. */ void yieldAll(Node expression, TranspilationContext.Case jumpToSection, Node sourceNode) { writeGeneratedNode( returnContextMethod( sourceNode, "yieldAll", type(StandardColors.UNKNOWN), expression, jumpToSection.getNumber(sourceNode))); context.currentCase.mayFallThrough = false; } /** Instructs a state machine program to return a given expression. */ Node returnExpression(Node sourceNode, @Nullable Node expression) { if (expression == null) { return callContextMethod(sourceNode, "return", type(StandardColors.NULL_OR_VOID)); } return callContextMethod( sourceNode, "return", type(StandardColors.NULL_OR_VOID), expression); } /** Instructs a state machine program to consume a yield result after yielding. */ Node yieldResult(Node sourceNode) { return getContextField(sourceNode, "yieldResult"); } /** Adds references to catch and finally blocks to the transpilation context. */ private void addCatchFinallyCases(@Nullable Case catchCase, @Nullable Case finallyCase) { if (finallyCase != null) { if (!catchCases.isEmpty()) { ++catchCases.getFirst().finallyBlocks; } finallyCases.addFirst(finallyCase); } if (catchCase != null) { catchCases.addFirst(new CatchCase(catchCase)); } } /** Returns the case section of the next catch block that is not hidden by finally blocks. */ private @Nullable Case getNextCatchCase() { for (CatchCase catchCase : catchCases) { if (catchCase.finallyBlocks == 0) { return catchCase.catchCase; } break; } return null; } /** Returns the case section of the next finally block. */ private @Nullable Case getNextFinallyCase() { return finallyCases.isEmpty() ? null : finallyCases.getFirst(); } /** Removes references to catch and finally blocks from the transpilation context. */ private void removeCatchFinallyCases(@Nullable Case catchCase, @Nullable Case finallyCase) { if (catchCase != null) { CatchCase lastCatch = catchCases.removeFirst(); checkState(lastCatch.finallyBlocks == 0); checkState(lastCatch.catchCase == catchCase); } if (finallyCase != null) { if (!catchCases.isEmpty()) { int finallyBlocks = --catchCases.getFirst().finallyBlocks; checkState(finallyBlocks >= 0); } Case lastFinally = finallyCases.removeFirst(); checkState(lastFinally == finallyCase); } } /** Writes a statement Node that should be placed at the beginning of try block. */ void enterTryBlock(@Nullable Case catchCase, @Nullable Case finallyCase, Node sourceNode) { addCatchFinallyCases(catchCase, finallyCase); final String methodName; ArrayList args = new ArrayList<>(); if (catchCase == null) { methodName = "setFinallyBlock"; args.add(finallyCase.getNumber(sourceNode)); } else { methodName = "setCatchFinallyBlocks"; args.add(catchCase.getNumber(sourceNode)); if (finallyCase != null) { args.add(finallyCase.getNumber(sourceNode)); } } writeGeneratedNode( callContextMethodResult( sourceNode, methodName, type(StandardColors.NULL_OR_VOID), args.toArray(new Node[0]))); } /** * Writes a statements that should be placed at the end of try block if finally block is not * present. */ void leaveTryBlock(@Nullable Case catchCase, Case endCase, Node sourceNode) { removeCatchFinallyCases(catchCase, null); ArrayList args = new ArrayList<>(); args.add(endCase.getNumber(sourceNode)); // Find the next catch block that is not hidden by any finally blocks. Case nextCatchCase = getNextCatchCase(); if (nextCatchCase != null) { args.add(nextCatchCase.getNumber(sourceNode)); } writeGeneratedNodeAndBreak( callContextMethodResult( sourceNode, "leaveTryBlock", type(StandardColors.NULL_OR_VOID), args.toArray(new Node[0]))); } /** Writes a statement Node that should be placed at the beginning of catch block. */ void enterCatchBlock(@Nullable Case finallyCase, Node exceptionName) { checkState(exceptionName.isName()); addCatchFinallyCases(null, finallyCase); // Find the next catch block that is not hidden by any finally blocks. Case nextCatchCase = getNextCatchCase(); if (catchNames.add(exceptionName.getString())) { hoistNode(IR.var(exceptionName.cloneNode()).srcref(exceptionName)); } ArrayList args = new ArrayList<>(); if (nextCatchCase != null) { args.add(nextCatchCase.getNumber(exceptionName)); } Node enterCatchBlockCall = callContextMethod( exceptionName, "enterCatchBlock", type(StandardColors.UNKNOWN), args.toArray(new Node[0])); exceptionName.setColor(enterCatchBlockCall.getColor()); writeGeneratedNode( astFactory .exprResult( astFactory .createAssign(exceptionName, enterCatchBlockCall) .srcref(exceptionName)) .srcref(exceptionName)); } /** Writes a statement to jump to the finally block if it's present. */ void leaveCatchBlock(@Nullable Case finallyCase, Node sourceNode) { if (finallyCase != null) { removeCatchFinallyCases(null, finallyCase); writeJumpTo(finallyCase, sourceNode); } } /** Writes a Node that should be placed at the beginning of finally block. */ void enterFinallyBlock( @Nullable Case catchCase, @Nullable Case finallyCase, Node sourceNode) { removeCatchFinallyCases(catchCase, finallyCase); Case nextCatchCase = getNextCatchCase(); Case nextFinallyCase = getNextFinallyCase(); ArrayList args = new ArrayList<>(); if (nestedFinallyBlockCount == 0) { if (nextCatchCase != null || nextFinallyCase != null) { args.add( nextCatchCase == null ? astFactory.createNumber(0).srcref(sourceNode) : nextCatchCase.getNumber(sourceNode)); if (nextFinallyCase != null) { args.add(nextFinallyCase.getNumber(sourceNode)); } } } else { args.add( nextCatchCase == null ? astFactory.createNumber(0).srcref(sourceNode) : nextCatchCase.getNumber(sourceNode)); args.add( nextFinallyCase == null ? astFactory.createNumber(0).srcref(sourceNode) : nextFinallyCase.getNumber(sourceNode)); args.add(astFactory.createNumber(nestedFinallyBlockCount).srcref(sourceNode)); } writeGeneratedNode( callContextMethodResult( sourceNode, "enterFinallyBlock", type(StandardColors.NULL_OR_VOID), args.toArray(new Node[0]))); ++nestedFinallyBlockCount; } /** Writes a Node that should be placed at the end of finally block. */ void leaveFinallyBlock(Case endCase, Node sourceNode) { ArrayList args = new ArrayList<>(); args.add(endCase.getNumber(sourceNode)); if (--nestedFinallyBlockCount != 0) { args.add(astFactory.createNumber(nestedFinallyBlockCount).srcref(sourceNode)); } writeGeneratedNodeAndBreak( callContextMethodResult( sourceNode, "leaveFinallyBlock", type(StandardColors.NULL_OR_VOID), args.toArray(new Node[0]))); } /** Changes the {@link #currentCase} to a new one. */ void switchCaseTo(Case caseSection) { currentCase.willFollowBy(caseSection); allCases.add(caseSection); currentCase = caseSection; } /** Adds a named labels to the context. */ public void pushLabels( ArrayList labelNames, Case breakCase, @Nullable Case continueCase) { for (Node labelName : labelNames) { checkState(labelName.isLabelName()); namedLabels.put(labelName.getString(), new LabelCases(breakCase, continueCase)); } } /** Removes the named labels from the context. */ public void popLabels(ArrayList labelNames) { for (Node labelName : labelNames) { checkState(labelName.isLabelName()); namedLabels.remove(labelName.getString()); } } /** Adds "break" jump point to the context */ public void pushBreakContext(Case breakCase) { breakCases.push(breakCase); } /** Adds "break" and "continue" jump points to the context */ public void pushBreakContinueContext(Case breakCase, Case continueCase) { pushBreakContext(breakCase); continueCases.push(continueCase); } /** Removes "break" jump point from the context, restoring the previous one */ public void popBreakContext() { breakCases.pop(); } /** * Removes "break" and "continue" jump points from the context, restoring the previous ones. */ public void popBreakContinueContext() { popBreakContext(); continueCases.pop(); } /** A case section in a switch block of generator program. */ private class Case { final int id; final Node caseBlock; /** * Records number of times the section was referenced. * *

It's used to drop unreferenced sections. */ final ArrayList references = new ArrayList<>(); /** * Indicates that this case is a simple jump or a fall-though case. Points to the target * case. */ @Nullable Case jumpTo; /** * Indicates that the body of this case could potentially be embedded into another block * node. * *

Usually "if (a) {b();} else { c(); }" is transpiled into: *

         *   if (a) { goto labelIf; }
         *   c();
         *   goto labelEnd;
         * labelIf:
         *   b();
         * labelEnd:
         * 
* * but "labelIf: b();" can be inlined to get shorter output: * *
         *   if (a) { b(); goto labelEnd; }
         *   c();
         * labelEnd:
         * 
* * In this example "labelIf" case can be embedded into "{ goto labelIf; }" * block. */ @Nullable Node embedInto; /** Tells whether this case might fall-through. */ boolean mayFallThrough = true; /** Creates a new empty case section and assings a new id. */ Case() { this.id = caseIdCounter++; this.caseBlock = astFactory.createBlock().srcref(originalGeneratorBody); } Node createCaseNode() { return IR.caseNode(astFactory.createNumber(id).srcref(caseBlock), caseBlock) .srcref(caseBlock); } /** Returns the number node of the case section and increments a reference counter. */ Node getNumber(Node sourceNode) { if (jumpTo != null) { return jumpTo.getNumber(sourceNode); } Node node = astFactory.createNumber(id).srcref(sourceNode); references.add(node); return node; } /** * Finalizes the case section with a jump instruction. * *

{@link #addNode} cannot be invoked after this method is called. */ void jumpTo(Case other, Node jumpBlock) { checkState(jumpBlock.isBlock()); checkState(jumpTo == null); willFollowBy(other); caseBlock.addChildrenToBack(jumpBlock.removeChildren()); mayFallThrough = false; } /** * Informs which other case will be executed after this one. * *

It's used to detect and then eliminate case statements that are used as simple jump * hops: * *

         *  case 100:
         *    $context.jumpTo(200);
         *    break;
         * 
* * or * *
         *  case 300:
         * 
*/ void willFollowBy(Case other) { if (jumpTo == null && !caseBlock.hasChildren()) { checkState(other.jumpTo == null); jumpTo = other; } } /** Adds a new node to the end of the case block. */ void addNode(Node n) { checkState(jumpTo == null); checkState(IR.mayBeStatement(n)); caseBlock.addChildToBack(n); } } /** * Adjust YIELD-free nodes to run correctly inside a state machine program. * *

The following transformations are performed: * *

    *
  • moving var into hois scope; *
  • transpiling return statements; *
  • transpiling break and continue statements; *
  • transpiling references to this and arguments. *
*/ private class UnmarkedNodeTranspiler implements NodeTraversal.Callback { // Count the number of enclosing statements that a bare break could address. // A value > 0 means that a bare break statement we encounter can be left unmodified, // since it addresses a statement within the node we are transpiling. int breakSuppressors; // Same as breakSuppressors, but for bare continue statements. int continueSuppressors; @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (n.isGeneratorSafe()) { // Skip nodes that were generated by the compiler. n.setGeneratorSafe(false); return false; } checkState(!n.isGeneratorMarker()); checkState(!n.isSuper(), "Reference to SUPER is not supported"); if (NodeUtil.isLoopStructure(n)) { ++continueSuppressors; ++breakSuppressors; } else if (n.isSwitch()) { ++breakSuppressors; } if (n.isBreak() || n.isContinue()) { if (n.hasChildren()) { visitNamedBreakContinue(n); } else { visitBreakContinue(n); } return false; } return !n.isFunction(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (NodeUtil.isLoopStructure(n)) { --continueSuppressors; --breakSuppressors; } else if (n.isSwitch()) { --breakSuppressors; } else if (n.isThis()) { visitThis(n); } else if (n.isReturn()) { visitReturn(n); } else if (n.isName() && n.getString().equals("arguments")) { visitArguments(n); } else if (n.isVar()) { if (parent.isVanillaFor()) { visitVanillaForLoopVar(n); } else if (parent.isForIn()) { visitForInLoopVar(n); } else { // NOTE: for-of loops are transpiled away before this pass visitVarStatement(n); } } // else no changes need to be made } /** Adjust return statements. */ void visitReturn(Node n) { // return ...; => return $context.return(...); n.addChildToFront(returnExpression(n, n.removeFirstChild())); } /** Converts labeled break or continue statement into a jump. */ void visitNamedBreakContinue(Node n) { checkState(n.getFirstChild().isLabelName()); LabelCases cases = namedLabels.get(n.getFirstChild().getString()); if (cases != null) { Case caseSection = n.isBreak() ? cases.breakCase : cases.continueCase; context.replaceBreakContinueWithJump(n, caseSection, breakSuppressors); } } /** Converts break or continue statement into a jump. */ void visitBreakContinue(Node n) { Case caseSection = null; if (n.isBreak() && breakSuppressors == 0) { caseSection = breakCases.getFirst(); } if (n.isContinue() && continueSuppressors == 0) { caseSection = continueCases.getFirst(); } if (caseSection != null) { context.replaceBreakContinueWithJump(n, caseSection, breakSuppressors); } } /** Replaces reference to this with $jscomp$generator$this. */ void visitThis(Node n) { Node newThis = astFactory.createName(context.getScopedName(GENERATOR_THIS), type(n)); n.replaceWith(newThis); if (!thisReferenceFound) { Node var = IR.var(newThis.cloneNode().srcref(n), n).srcref(newGeneratorHoistBlock); hoistNode(var); thisReferenceFound = true; } } /** * Replaces reference to arguments with $jscomp$generator$arguments * . */ void visitArguments(Node n) { Node newArguments = astFactory.createName(context.getScopedName(GENERATOR_ARGUMENTS), type(n)).srcref(n); n.replaceWith(newArguments); if (!argumentsReferenceFound) { Node var = IR.var(newArguments.cloneNode(), n).srcref(newGeneratorHoistBlock); hoistNode(var); argumentsReferenceFound = true; } } /** * Hoists {@code var} statements into the closure containing the generator to preserve their * state across multiple invocation of state machine program. * *

* *

         * var a = "test", b = i + 5;
         * 
* * is transpiled to: * *
         * var a, b;
         * a = "test", b = i + 5;
         * 
*/ void visitVarStatement(Node varStatement) { Node commaExpression = extractAssignmentsToCommaExpression(varStatement); if (commaExpression == null) { varStatement.detach(); } else { varStatement.replaceWith(astFactory.exprResult(commaExpression)); } // Move declaration without initial values to just before the program method definition. hoistNode(varStatement); } /** * Hoists {@code var} declarations in vanilla for loops into the closure containing the * generator to preserve their state across multiple invocation of state machine program. * *

* *

         * for (var a = "test", b = i + 5; ... ; )
         * 
* * is transpiled to: * *
         * var a, b;
         * for (a = "test", b = i + 5; ...; )
         * 
*/ private void visitVanillaForLoopVar(Node varDeclaration) { Node commaExpression = extractAssignmentsToCommaExpression(varDeclaration); if (commaExpression == null) { // `for (var x; ` becomes `for (; ` varDeclaration.replaceWith(IR.empty()); } else { // `for (var i = 0, j = 0; `... becomes `for (i = 0, j = 0; `... varDeclaration.replaceWith(commaExpression); } // Move declaration without initial values to just before the program method definition. hoistNode(varDeclaration); } /** * Hoists {@code var} declarations in for-in loops into the closure containing the * generator to preserve their state across multiple invocation of state machine program. * *

* *

         * for (var a in obj)
         * 
* * is transpiled to: * *
         * var a;
         * for (a in obj))
         * 
*/ private void visitForInLoopVar(Node varDeclaration) { // `for (var varName in ` ... Node varName = varDeclaration.getOnlyChild(); checkState(!varName.hasChildren(), varName); Node clonedVarName = varName.cloneNode().setJSDocInfo(null); // becomes `for (varName in ` ... varDeclaration.replaceWith(clonedVarName); // Move declaration without initial values to just before the program method definition. hoistNode(varDeclaration); } /** * Removes all initializers from a var declaration and returns them as a single expression * of comma-separated assignments or null if there aren't any initializers. * * @param varDeclaration VAR node * @return null or expression node (e.g. `varName1 = 1, varName2 = y`) */ private @Nullable Node extractAssignmentsToCommaExpression(Node varDeclaration) { ArrayList assignments = new ArrayList<>(); for (Node varName = varDeclaration.getFirstChild(); varName != null; varName = varName.getNext()) { if (varName.hasChildren()) { Node copiedVarName = varName.cloneNode().setJSDocInfo(null); Node assign = astFactory .createAssign(copiedVarName, varName.removeFirstChild()) .srcref(varName); assignments.add(assign); } } Node commaExpression = null; for (Node assignment : assignments) { commaExpression = commaExpression == null ? assignment : astFactory.createComma(commaExpression, assignment).srcref(assignment); } return commaExpression; } } /** Reprasents a catch case that is used by try/catch transpilation */ class CatchCase { final Case catchCase; /** * Number of finally blocks that should be executed before exception can be handled by this * catch case. */ int finallyBlocks; CatchCase(Case catchCase) { this.catchCase = catchCase; } } /** Stores "break" and "continue" case sections assosiated with a label. */ class LabelCases { final Case breakCase; final @Nullable Case continueCase; LabelCases(Case breakCase, @Nullable Case continueCase) { this.breakCase = breakCase; this.continueCase = continueCase; } } } } /** Marks "yield" nodes and propagates this information up through the tree */ private static class YieldNodeMarker implements NodeTraversal.Callback { @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return !n.isFunction(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isYield()) { n.setGeneratorMarker(true); } // This class is used on a tree that is detached from the main AST, so this will not end up // marking the parent of the node used to start the traversal. if (parent != null && n.isGeneratorMarker()) { parent.setGeneratorMarker(true); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy