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

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.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.Es6ToEs3Util.withType;

import com.google.common.collect.ImmutableMap;
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.Token;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import javax.annotation.Nullable;

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

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 HotSwapCompilerPass { 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 FeatureSet transpiledFeatures = FeatureSet.BARE_MINIMUM.with(Feature.GENERATORS); private final AbstractCompiler compiler; private final JSTypeRegistry registry; private final boolean shouldAddTypes; private final JSType unknownType; private final JSType numberType; private final JSType booleanType; private final JSType nullType; private final JSType nullableStringType; private final JSType voidType; Es6RewriteGenerators(AbstractCompiler compiler) { checkNotNull(compiler); this.compiler = compiler; registry = compiler.getTypeRegistry(); if (compiler.hasTypeCheckingRun()) { // typechecking has run, so we must preserve and propagate type information shouldAddTypes = true; unknownType = registry.getNativeType(JSTypeNative.UNKNOWN_TYPE); numberType = registry.getNativeType(JSTypeNative.NUMBER_TYPE); booleanType = registry.getNativeType(JSTypeNative.BOOLEAN_TYPE); nullType = registry.getNativeType(JSTypeNative.NULL_TYPE); nullableStringType = registry.createNullableType(registry.getNativeType(JSTypeNative.STRING_TYPE)); voidType = registry.getNativeType(JSTypeNative.VOID_TYPE); } else { shouldAddTypes = false; unknownType = null; numberType = null; booleanType = null; nullType = null; nullableStringType = null; voidType = null; } } @Override public void process(Node externs, Node root) { TranspilationPasses.processTranspile( compiler, root, transpiledFeatures, new GeneratorFunctionsTranspiler()); TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { TranspilationPasses.hotSwapTranspile( compiler, scriptRoot, 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 = new ExpressionDecomposer( compiler, compiler.getUniqueNameIdSupplier(), new HashSet<>(), Scope.createGlobalScope(new Node(Token.SCRIPT)), /* allowMethodCallDecomposing = */ true); } @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, Es6ToEs3Util.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; /** The original inferred return type of the Generator */ JSType originalGenReturnType; JSType yieldType; SingleGeneratorFunctionTranspiler(Node genFunc, int genaratorNestingLevel) { this.generatorNestingLevel = genaratorNestingLevel; this.originalGeneratorBody = genFunc.getLastChild(); ObjectType contextType = null; if (shouldAddTypes) { // Find the yield type of the generator. // e.g. given @return {!Generator}, we want this.yieldType to be number. yieldType = unknownType; if (genFunc.getJSType() != null && genFunc.getJSType().isFunctionType()) { FunctionType fnType = genFunc.getJSType().toMaybeFunctionType(); this.originalGenReturnType = fnType.getReturnType(); yieldType = JsIterables.getElementType(originalGenReturnType, registry); } JSType globalContextType = registry.getGlobalType("$jscomp.generator.Context"); if (globalContextType == null) { // We don't have the es6/generator polyfill, which can happen in tests using a // NonInjectingCompiler or if someone sets --inject_libraries=false. Don't crash, just // back off on giving some type information. contextType = registry.getNativeObjectType(JSTypeNative.OBJECT_TYPE); } else { contextType = registry.createTemplatizedType(globalContextType.toMaybeObjectType(), yieldType); } } this.context = new TranspilationContext(contextType); } /** * Hoists a node inside {@link #newGeneratorHoistBlock}. * * @see #newGeneratorHoistBlock */ private void hoistNode(Node node) { newGeneratorHoistBlock.addChildBefore(node, 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 && callTarget.matchesQualifiedName("$jscomp.asyncExecutePromiseGeneratorFunction")) { 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; JSType programType = shouldAddTypes // function(!Context): (void|{value: YIELD_TYPE}) ? registry.createFunctionType( registry.createUnionType( voidType, registry.createRecordType(ImmutableMap.of("value", yieldType))), context.contextType) : null; Node generatorBody = IR.block(); 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.getSecondChild().setString("asyncExecutePromiseGeneratorProgram"); JSType oldType = callTarget.getJSType(); if (oldType != null && oldType.isFunctionType()) { callTarget.setJSType( registry.createFunctionType( oldType.toMaybeFunctionType().getReturnType(), programType)); } 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).getString()); } // Prepare a "program" function: // function ($jscomp$generator$context) { // } program = IR.function( IR.name(""), IR.paramList(context.getJsContextNameNode(originalGeneratorBody)), generatorBody); // $jscomp.generator.createGenerator Node createGenerator = IR.getprop( withType( IR.getprop(withType(IR.name("$jscomp"), unknownType), "generator"), unknownType), "createGenerator"); if (shouldAddTypes) { createGenerator.setJSType( registry.createFunctionType(originalGenReturnType, programType)); } // Replace original generator function body with: // return $jscomp.generator.createGenerator(, ); newGeneratorHoistBlock = IR.block( IR.returnNode( withType( IR.call( createGenerator, // function name passed as parameter must have the type of the // generator function itself withType(genFuncName.cloneNode(), generatorFunction.getJSType()), program), originalGenReturnType)) .useSourceInfoFromForTree(originalGeneratorBody)); originalGeneratorBody.replaceWith(newGeneratorHoistBlock); } program.setJSType(programType); // 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 = IR.block(); if (shouldAddFinalJump) { finalBlock.addChildToBack( context.callContextMethodResult(originalGeneratorBody, "jumpToEnd")); } 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 * @param breakCase * @param continueCase */ void transpileStatement( Node statement, @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.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 = IR.block(isExpression ? IR.returnNode(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. */ 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) ? IR.block(n) : IR.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, @Nullable TranspilationContext.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 = IR.block().useSourceInfoFrom(n); } else { elseBlock.detach(); } // Only "else" block is unmarked, swap "if" and "else" blocks and negate the condition. if (ifBlock.isGeneratorMarker() && !elseBlock.isGeneratorMarker()) { condition = withType(IR.not(condition), booleanType).useSourceInfoFrom(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(IR.ifNode(condition, ifBlock).useSourceInfoFrom(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( IR.ifNode(prepareNodeForWrite(condition), newIfBlock).useSourceInfoFrom(n)); transpileStatement(elseBlock); context.writeJumpTo(endCase, elseBlock); context.switchCaseTo(ifCase); transpileStatement(ifBlock); context.switchCaseTo(endCase); } /** Transpiles marked "for" statement. */ void transpileFor( Node n, @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.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 = IR.exprResult(init).useSourceInfoFrom(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( IR.ifNode( withType(IR.not(condition), booleanType).useSourceInfoFrom(condition), context.createJumpToBlock( endCase, /** allowEmbedding= */ true, n)) .useSourceInfoFrom(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(IR.exprResult(increment).useSourceInfoFrom(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, @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.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()) { n.addChildAfter(detachedExpr, 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).useSourceInfoFrom(target); } // "$for$in" Node forIn = context .getScopedName(GENERATOR_FORIN_PREFIX + compiler.getUniqueNameIdSupplier().get()) .useSourceInfoFrom(target); // "$context.forIn(x)" forIn.addChildToFront(context.callContextMethod(target, "forIn", detachedExpr)); ObjectType propertyIteratorType = shouldAddTypes ? forIn.getFirstChild().getJSType().toMaybeObjectType() : null; forIn.setJSType(propertyIteratorType); // "var ..., $for$in = $context.forIn(expr)" init.addChildToBack(forIn); // "$for$in.getNext()" Node forInGetNext = withType( IR.getprop( forIn.cloneNode(), IR.string("getNext").useSourceInfoFrom(detachedExpr)), shouldAddTypes ? propertyIteratorType.getPropertyType("getNext") : null) .useSourceInfoFrom(detachedExpr); // "(i = $for$in.getNext()) != null" Node forCond = withType(IR.ne( withType(IR.assign( withType(target, nullableStringType), withType(IR.call(forInGetNext), nullableStringType) .useSourceInfoFrom(detachedExpr)), nullableStringType) .useSourceInfoFrom(detachedExpr), withType(IR.nullNode(), nullType).useSourceInfoFrom(forIn)), booleanType) .useSourceInfoFrom(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().useSourceInfoFrom(n), body.detach()) .useSourceInfoFrom(n); // Transpile "for" instead of "for in". transpileFor(forNode, breakCase, continueCase); } /** Transpiles "while" statement. */ void transpileWhile( Node n, @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.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( IR.ifNode( withType(IR.not(condition), booleanType).useSourceInfoFrom(condition), context.createJumpToBlock( endCase, /** allowEmbedding= */ true, n)) .useSourceInfoFrom(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, @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.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( IR.ifNode(condition, context.createJumpToBlock(startCase, /** allowEmbedding=*/ false, n)) .useSourceInfoFrom(n)); context.switchCaseTo(breakCase); } /** Transpiles "try" statement */ void transpileTry(Node n, @Nullable TranspilationContext.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, @Nullable TranspilationContext.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, Es6ToEs3Util.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.useSourceInfoFrom(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; /** The JSType for this context. May be null. */ final ObjectType contextType; TranspilationContext(ObjectType contextType) { programEndCase = new Case(); checkState(programEndCase.id == 0); currentCase = new Case(); checkState(currentCase.id == 1); allCases.add(currentCase); this.contextType = contextType; } /** * 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( IR.ifNode( withType( IR.eq(getContextField(generatorBody, "nextAddress"), withType(IR.number(1), numberType)), booleanType), allCases.remove(0).caseBlock).useSourceInfoIfMissingFromForTree(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( IR.ifNode( withType( IR.ne(getContextField(generatorBody, "nextAddress"), withType(IR.number(allCases.get(1).id), numberType)), booleanType), allCases.remove(0).caseBlock).useSourceInfoIfMissingFromForTree(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")) .useSourceInfoFrom(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 withType(getScopedName(GENERATOR_CONTEXT), this.contextType) .useSourceInfoFrom(sourceNode); } /** Returns unique name in the current context. */ Node getScopedName(String name) { return IR.name(name + (generatorNestingLevel == 0 ? "" : "$" + generatorNestingLevel)); } /** Creates node that access a specified field of the current context. */ Node getContextField(Node sourceNode, String fieldName) { return withType( IR.getprop( getJsContextNameNode(sourceNode), IR.string(fieldName).useSourceInfoFrom(sourceNode)), shouldAddTypes ? contextType.getPropertyType(fieldName) : null) .useSourceInfoFrom(sourceNode); } /** Creates node that make a call to a context function. */ Node callContextMethod(Node sourceNode, String methodName, Node... args) { Node contextField = getContextField(sourceNode, methodName); Node callNode = IR.call(contextField, args).useSourceInfoFrom(sourceNode); if (shouldAddTypes) { callNode.setJSType( contextField.getJSType().isFunctionType() ? contextField.getJSType().toMaybeFunctionType().getReturnType() : unknownType); } return callNode; } /** Creates node that make a call to a context function. */ Node callContextMethodResult(Node sourceNode, String methodName, Node... args) { return IR.exprResult(callContextMethod(sourceNode, methodName, args)) .useSourceInfoFrom(sourceNode); } /** Creates node that returns the result of a call to a context function. */ Node returnContextMethod(Node sourceNode, String methodName, Node... args) { return IR.returnNode(callContextMethod(sourceNode, methodName, args)) .useSourceInfoFrom(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().useSourceInfoFrom(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", 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 = IR.block( callContextMethodResult(sourceNode, "jumpTo", section.getNumber(sourceNode)), createBreakNodeFor(sourceNode)) .useSourceInfoFrom(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; sourceNode.getParent().addChildBefore( callContextMethodResult(sourceNode, jumpMethod, section.getNumber(sourceNode)), 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, 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 ? withType(IR.name("undefined"), voidType).useSourceInfoFrom(sourceNode) : expression); args.add(jumpToSection.getNumber(sourceNode)); context.writeGeneratedNode( returnContextMethod(sourceNode, "yield", 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", 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"); } return callContextMethod(sourceNode, "return", 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. */ @Nullable private 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. */ @Nullable private 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, 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", 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()).useSourceInfoFrom(exceptionName)); } ArrayList args = new ArrayList<>(); if (nextCatchCase != null) { args.add(nextCatchCase.getNumber(exceptionName)); } Node enterCatchBlockCall = callContextMethod(exceptionName, "enterCatchBlock", args.toArray(new Node[0])); exceptionName.setJSType(enterCatchBlockCall.getJSType()); writeGeneratedNode( IR.exprResult( withType( IR.assign(exceptionName, enterCatchBlockCall), enterCatchBlockCall.getJSType()) .useSourceInfoFrom(exceptionName)) .useSourceInfoFrom(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 ? withType(IR.number(0), numberType).useSourceInfoFrom(sourceNode) : nextCatchCase.getNumber(sourceNode)); if (nextFinallyCase != null) { args.add(nextFinallyCase.getNumber(sourceNode)); } } } else { args.add( nextCatchCase == null ? withType(IR.number(0), numberType).useSourceInfoFrom(sourceNode) : nextCatchCase.getNumber(sourceNode)); args.add( nextFinallyCase == null ? withType(IR.number(0), numberType).useSourceInfoFrom(sourceNode) : nextFinallyCase.getNumber(sourceNode)); args.add(IR.number(nestedFinallyBlockCount).useSourceInfoFrom(sourceNode)); } writeGeneratedNode( callContextMethodResult(sourceNode, "enterFinallyBlock", 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(IR.number(nestedFinallyBlockCount).useSourceInfoFrom(sourceNode)); } writeGeneratedNodeAndBreak( callContextMethodResult(sourceNode, "leaveFinallyBlock", 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 = IR.block().useSourceInfoFrom(originalGeneratorBody); } Node createCaseNode() { return IR.caseNode( withType(IR.number(id), numberType).useSourceInfoFrom(caseBlock), caseBlock) .useSourceInfoFrom(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 = withType(IR.number(id), numberType).useSourceInfoFrom(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 = withType(context.getScopedName(GENERATOR_THIS), n.getJSType()); n.replaceWith(newThis); if (!thisReferenceFound) { Node var = IR.var(newThis.cloneNode().useSourceInfoFrom(n), n) .useSourceInfoFrom(newGeneratorHoistBlock); hoistNode(var); thisReferenceFound = true; } } /** * Replaces reference to arguments with $jscomp$generator$arguments * . */ void visitArguments(Node n) { Node newArguments = context.getScopedName(GENERATOR_ARGUMENTS).useSourceInfoFrom(n); n.replaceWith(newArguments); if (!argumentsReferenceFound) { Node var = IR.var(newArguments.cloneNode(), n).useSourceInfoFrom(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(IR.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`) */ @Nullable private Node extractAssignmentsToCommaExpression(Node varDeclaration) { ArrayList assignments = new ArrayList<>(); for (Node varName : varDeclaration.children()) { if (varName.hasChildren()) { Node copiedVarName = varName.cloneNode().setJSDocInfo(null); Node assign = withType( IR.assign(copiedVarName, varName.removeFirstChild()), varName.getJSType()) .useSourceInfoFrom(varName); assignments.add(assign); } } Node commaExpression = null; for (Node assignment : assignments) { commaExpression = commaExpression == null ? assignment : withType(IR.comma(commaExpression, assignment), assignment.getJSType()) .useSourceInfoFrom(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; @Nullable final 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