diff --git a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java index 1ab365b6eda..a0ea5a945c6 100644 --- a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java +++ b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; @@ -192,7 +193,7 @@ private class SingleGeneratorFunctionTranspiler { final int generatorNestingLevel; /** The transpilation context for the state machine program. */ - final TranspilationContext context = new TranspilationContext(); + final TranspilationContext context; /** The body of original generator function that should be transpiled */ final Node originalGeneratorBody; @@ -206,19 +207,17 @@ private class SingleGeneratorFunctionTranspiler { SingleGeneratorFunctionTranspiler(Node genFunc, int genaratorNestingLevel) { this.generatorNestingLevel = genaratorNestingLevel; this.originalGeneratorBody = genFunc.getLastChild(); + this.context = new TranspilationContext(originalGeneratorBody); } public void transpile() { - // Ensure that the state machine program ends - Node jumpToEnd = context.callContextMethodResult(originalGeneratorBody, "jumpToEnd"); - jumpToEnd.setGeneratorSafe(true); - originalGeneratorBody.addChildToBack(jumpToEnd); - - // Insert $context.jumpToEnd only if it's reachable + // Test if end of generator function is reachable + Node returnNode = IR.returnNode(); + originalGeneratorBody.addChildToBack(returnNode); + // TODO(skill): figure out a better solution as CGF is not the best option due to b/73762053 controlFlowGraph = ControlFlowAnalysis.getCfg(compiler, originalGeneratorBody.getParent()); - if (controlFlowGraph.getInEdges(jumpToEnd).isEmpty()) { - jumpToEnd.detach(); - } + boolean shouldAddFinalJump = !controlFlowGraph.getInEdges(returnNode).isEmpty(); + returnNode.detach(); Node genFunc = originalGeneratorBody.getParent(); checkState(genFunc.isGeneratorFunction()); @@ -230,11 +229,15 @@ public void transpile() { genFuncName.setString(context.getScopedName(GENERATOR_FUNCTION).getString()); } + // switch ($jscomp$generator$context.nextAddress) { + // } + Node switchNode = + IR.switchNode(IR.getprop(context.getJsContextNameNode(genFunc), "nextAddress")); + // Prepare a "program" function: // function($jscomp$generator$context) { // while ($jscomp$generator$context.nextAddress) { // switch ($jscomp$generator$context.nextAddress) { - // case 0: // } // }); // } @@ -249,10 +252,7 @@ public void transpile() { IR.whileNode( // TODO(skill): Remove while loop when this pass moves after // type checking or when OTI is fixed to handle this correctly. IR.getprop(context.getJsContextNameNode(genFunc), "nextAddress"), - IR.block( - IR.switchNode( - IR.getprop(context.getJsContextNameNode(genFunc), "nextAddress"), - context.currentCase.caseNode))))); + IR.block(switchNode)))); // Propagate all suppressions from original generator function to a new "program" function. JSDocInfoBuilder jsDocBuilder = new JSDocInfoBuilder(false); @@ -291,6 +291,15 @@ public void transpile() { transpileStatement(statement); } + // 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.finalizeTransformation(switchNode); context.checkStateIsEmpty(); genFunc.putBooleanProp(Node.GENERATOR_FN, false); @@ -478,12 +487,14 @@ void transpileReturn(Node n) { context.returnExpression( 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. */ @@ -626,14 +637,22 @@ void transpileIf(Node n, @Nullable TranspilationContext.Case breakCase) { TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); // "if" and "else" blocks marked + condition = prepareNodeForWrite(condition); + Node newIfBlock = context.createJumpToBlock(ifCase, n); context.writeGeneratedNode( - IR.ifNode(prepareNodeForWrite(condition), context.createJumpToBlock(ifCase, n)) - .useSourceInfoFrom(n)); + IR.ifNode(prepareNodeForWrite(condition), newIfBlock).useSourceInfoFrom(n)); transpileStatement(elseBlock); context.writeJumpTo(endCase, elseBlock); context.switchCaseTo(ifCase); transpileStatement(ifBlock); context.switchCaseTo(endCase); + + // Inline the beginning of ifBlock if possible + if (ifCase.references.size() == 1 && !ifCase.mayFallThrough) { + checkState(ifCase.jumpTo == null); + checkState(context.allCases.remove(ifCase)); + newIfBlock.replaceWith(ifCase.caseBlock.detach()); + } } /** Transpiles marked "for" statement. */ @@ -667,8 +686,6 @@ void transpileFor( } TranspilationContext.Case startCase = context.createCase(); - startCase.markUsed(); // start of loop is referenced at the end, prevent case from collapsing. - TranspilationContext.Case incrementCase = context.maybeCreateCase(continueCase); TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); @@ -778,7 +795,6 @@ void transpileWhile( @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.Case continueCase) { TranspilationContext.Case startCase = context.maybeCreateCase(continueCase); - startCase.markUsed(); // start of loop is referenced at the end, prevent case from collapsing. TranspilationContext.Case endCase = context.maybeCreateCase(breakCase); context.switchCaseTo(startCase); @@ -807,7 +823,6 @@ void transpileDo( @Nullable TranspilationContext.Case breakCase, @Nullable TranspilationContext.Case continueCase) { TranspilationContext.Case startCase = context.createCase(); - startCase.markUsed(); // start of loop is referenced at the end, prevent case from collapsing. breakCase = context.maybeCreateCase(breakCase); continueCase = context.maybeCreateCase(continueCase); @@ -1010,6 +1025,19 @@ Node getYieldNode() { /** 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<>(); + + /** A virtual case that indicates end of program */ + final Case programEndCase; /** Most recently assigned id. */ int caseIdCounter; @@ -1018,22 +1046,143 @@ private class TranspilationContext { * Points to the switch case that is being populated with transpiled instructions from the * original generator function that is being transpiled. */ - private Case currentCase; - - private final HashMap namedLabels = new HashMap<>(); - private final ArrayDeque breakCases = new ArrayDeque<>(); - private final ArrayDeque continueCases = new ArrayDeque<>(); + Case currentCase; - private final ArrayDeque catchCases = new ArrayDeque<>(); - private final ArrayDeque finallyCases = new ArrayDeque<>(); - private final HashSet catchNames = new HashSet<>(); int nestedFinallyBlockCount = 0; boolean thisReferenceFound; boolean argumentsReferenceFound; - TranspilationContext() { - currentCase = new Case(IR.caseNode(IR.number(++caseIdCounter), IR.block())); + TranspilationContext(Node sourceNode) { + programEndCase = new Case(IR.caseNode(IR.number(0), IR.block())); + + currentCase = + new Case(IR.caseNode(IR.number(1), IR.block()).useSourceInfoFromForTree(sourceNode)); + allCases.add(currentCase); + caseIdCounter = 1; + } + + /** + * 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; + } + // 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; + Case prevCase = null; + for (Iterator it = allCases.iterator(); it.hasNext(); ) { + Case currentCase = it.next(); + if (prevCase != null) { + if (currentCase.references.isEmpty()) { + // No jump references, just append the body to a previous case if needed. + if (prevCase.mayFallThrough) { + prevCase.caseBlock.addChildrenToBack(currentCase.caseBlock.removeChildren()); + prevCase.mayFallThrough = currentCase.mayFallThrough; + } + 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 etnry 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; + } + } + + /** Finalizes transpilation by dumping all generated "case" nodes. */ + public void finalizeTransformation(Node switchNode) { + optimizeCaseIds(); + // Write all state machine cases + for (Case currentCase : allCases) { + switchNode.addChildToBack(currentCase.caseNode); + } + allCases.clear(); } /** Ensures that the context has an empty state. */ @@ -1118,9 +1267,11 @@ Node createJumpToNode(Case section, Node sourceNode) { /** Instructs a state machine program to jump to a selected case section. */ void writeJumpTo(Case section, Node sourceNode) { - writeGeneratedNode( - callContextMethodResult(sourceNode, "jumpTo", section.getNumber(sourceNode))); - writeGeneratedNode(IR.breakNode().useSourceInfoFrom(sourceNode)); + currentCase.jumpTo( + section, + IR.block( + callContextMethodResult(sourceNode, "jumpTo", section.getNumber(sourceNode)), + IR.breakNode().useSourceInfoFrom(sourceNode))); } /** Creates a block node that contains a jump instruction. */ @@ -1176,6 +1327,7 @@ void yield( args.add(jumpToSection.getNumber(sourceNode)); context.writeGeneratedNode( returnContextMethod(sourceNode, "yield", args.toArray(new Node[0]))); + context.currentCase.mayFallThrough = false; } /** @@ -1186,6 +1338,7 @@ void yieldAll(Node expression, TranspilationContext.Case jumpToSection, Node sou writeGeneratedNode( returnContextMethod( sourceNode, "yieldAll", expression, jumpToSection.getNumber(sourceNode))); + context.currentCase.mayFallThrough = false; } /** Instructs a state machine program to return a given expression. */ @@ -1285,6 +1438,7 @@ void leaveTryBlock(@Nullable Case catchCase, Case endCase, Node sourceNode) { writeGeneratedNode( callContextMethodResult(sourceNode, "leaveTryBlock", args.toArray(new Node[0]))); writeGeneratedNode(IR.breakNode().useSourceInfoFrom(sourceNode)); + currentCase.mayFallThrough = false; } /** Writes a statement Node that should be placed at the beginning of catch block. */ @@ -1372,13 +1526,14 @@ void leaveFinallyBlock(Case endCase, Node sourceNode) { writeGeneratedNode( callContextMethodResult(sourceNode, "leaveFinallyBlock", args.toArray(new Node[0]))); writeGeneratedNode(IR.breakNode().useSourceInfoFrom(sourceNode)); + currentCase.mayFallThrough = false; } /** Changes the {@link #currentCase} to a new one. */ - void switchCaseTo(@Nullable Case caseSection) { - if (caseSection != null) { - currentCase = caseSection.insertAfter(currentCase); - } + void switchCaseTo(Case caseSection) { + currentCase.willFollowBy(caseSection); + allCases.add(caseSection); + currentCase = caseSection; } /** Adds a named labels to the context. */ @@ -1427,13 +1582,22 @@ private class Case { final int id; final Node caseNode; final Node caseBlock; + 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; + + /** Tells whether this case might fall-through. */ + boolean mayFallThrough = true; /** * Records number of times the section was referenced. * *

It's used to drop unreferenced sections. */ - int referenceCount; /** Creates a Case object for an already created case node. */ Case(Node caseNode) { @@ -1441,7 +1605,6 @@ private class Case { this.id = NodeUtil.getNumberValue(caseNode.getFirstChild()).intValue(); this.caseNode = caseNode; this.caseBlock = caseNode.getLastChild(); - referenceCount = 1; } /** Creates a new empty case section and assings a new id. */ @@ -1450,43 +1613,62 @@ private class Case { caseNode = IR.caseNode(IR.number(id), caseBlock = IR.block()) .useSourceInfoFromForTree(originalGeneratorBody); - referenceCount = 0; } /** Returns the number node of the case section and increments a reference counter. */ Node getNumber(Node sourceNode) { - markUsed(); - return IR.number(id).useSourceInfoFrom(sourceNode); - } - - /** Increases a reference counter. */ - void markUsed() { - ++referenceCount; + if (jumpTo != null) { + return jumpTo.getNumber(sourceNode); + } + Node node = IR.number(id).useSourceInfoFrom(sourceNode); + references.add(node); + return node; } - /** Adds a new node to the end of the case block. */ - void addNode(Node n) { - checkState(IR.mayBeStatement(n)); - caseBlock.addChildToBack(n); + /** + * 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.isNormalBlock()); + checkState(jumpTo == null); + willFollowBy(other); + caseBlock.addChildrenToBack(jumpBlock.removeChildren()); + mayFallThrough = false; } /** - * Chains two case sections togeter. + * 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;
+         * 
* - * @return A combined section. + * or + * + *
+         *  case 300:
+         * 
*/ - Case insertAfter(Case other) { - checkState(caseNode.getParent() == null); - checkState(other.caseNode.getParent() != null); - if (referenceCount == 0) { - // No references, just merge with the previous case - other.caseBlock.addChildrenToBack(caseBlock.removeChildren()); - return other; - } else { - other.caseNode.getParent().addChildAfter(caseNode, other.caseNode); - return this; + 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); + } } /** @@ -1668,9 +1850,7 @@ void moveJsDocInfo(Node from, Node to) { void visitVar(Node varStatement) { maybeRemoveConstAnnotation(varStatement); ArrayList assignments = new ArrayList<>(); - for (Node varName = varStatement.getFirstChild(); - varName != null; - varName = varName.getNext()) { + for (Node varName : varStatement.children()) { if (varName.hasChildren()) { Node copiedVarName = varName.cloneNode(); Node assign = diff --git a/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java b/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java index fe85de40bd7..703c0bff910 100644 --- a/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java +++ b/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java @@ -93,16 +93,12 @@ public void testSimpleGenerator() { rewriteGeneratorBody( "yield;", lines( - " return $jscomp$generator$context.yield(undefined, 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(undefined, 0);")); rewriteGeneratorBody( "yield 1;", lines( - " return $jscomp$generator$context.yield(1, 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(1, 0);")); test( "/** @param {*} a */ function *f(a, b) {}", @@ -139,9 +135,7 @@ public void testSimpleGenerator() { " return $jscomp$generator$context.yield(i, 3);", "case 3:", " i = i + 1;", - " return $jscomp$generator$context.yield(i, 4);", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(i, 0);")); } public void testReturnGenerator() { @@ -157,9 +151,7 @@ public void testReturnGenerator() { " while ($jscomp$generator$context.nextAddress)", " switch ($jscomp$generator$context.nextAddress) {", " case 1:", - " return $jscomp$generator$context.yield(1, 2);", - " case 2:", - " $jscomp$generator$context.jumpToEnd();", + " return $jscomp$generator$context.yield(1, 0);", " }", " });", " }", @@ -179,9 +171,7 @@ public void testNestedGenerator() { " while ($jscomp$generator$context$1.nextAddress)", " switch ($jscomp$generator$context$1.nextAddress) {", " case 1:", - " return $jscomp$generator$context$1.yield(2, 2);", - " case 2:", - " $jscomp$generator$context$1.jumpToEnd();", + " return $jscomp$generator$context$1.yield(2, 0);", " }", " });", " }", @@ -192,9 +182,7 @@ public void testNestedGenerator() { " while ($jscomp$generator$context.nextAddress)", " switch ($jscomp$generator$context.nextAddress) {", " case 1:", - " return $jscomp$generator$context.yield(1, 2);", - " case 2:", - " $jscomp$generator$context.jumpToEnd();", + " return $jscomp$generator$context.yield(1, 0);", " }", " });", "}")); @@ -202,7 +190,6 @@ public void testNestedGenerator() { public void testForLoops() { - rewriteGeneratorBodyWithVars( "var i = 0; for (var j = 0; j < 10; j++) { i += j; }", "var i;", @@ -224,11 +211,7 @@ public void testForLoops() { rewriteGeneratorBody( "for (;;) { yield 1; }", lines( - "case 2:", - " return $jscomp$generator$context.yield(1, 5);", - "case 5:", - " $jscomp$generator$context.jumpTo(2);", - " break;")); + " return $jscomp$generator$context.yield(1, 1);")); rewriteGeneratorBodyWithVars( "for (var j = 0; j < 10; j++) { yield j; }", @@ -237,16 +220,14 @@ public void testForLoops() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - " return $jscomp$generator$context.yield(j, 5);", - "case 5:", + " return $jscomp$generator$context.yield(j, 3);", + "case 3:", " j++;", " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); rewriteGeneratorBodyWithVars( "var i = 0; for (var j = 0; j < 10; j++) { i += j; yield 5; }", @@ -256,17 +237,15 @@ public void testForLoops() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " i += j;", - " return $jscomp$generator$context.yield(5, 5);", - "case 5:", + " return $jscomp$generator$context.yield(5, 3);", + "case 3:", " j++;", " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); } public void testWhileLoops() { @@ -276,9 +255,7 @@ public void testWhileLoops() { lines( " i = 0;", " while (i < 10) { i ++; i++; i++; }", - " return $jscomp$generator$context.yield(i, 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(i, 0);")); rewriteGeneratorBodyWithVars( "var j = 0; while (j < 10) { yield j; j++; }", @@ -287,16 +264,14 @@ public void testWhileLoops() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " return $jscomp$generator$context.yield(j, 4)", "case 4:", " j++;", " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 3:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); rewriteGeneratorBodyWithVars( "var j = 0; while (yield) { j++; }", @@ -307,14 +282,12 @@ public void testWhileLoops() { " return $jscomp$generator$context.yield(undefined, 4);", "case 4:", " if (!($jscomp$generator$context.yieldResult)) {", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " j++;", " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 3:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); } public void testUndecomposableExpression() { @@ -384,10 +357,9 @@ public void testLabels() { " return $jscomp$generator$context.yield(undefined, 3);", "case 3:", " if ($jscomp$generator$context.yieldResult) {", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - "case 2:", " $jscomp$generator$context.jumpToEnd();")); rewriteGeneratorBody( @@ -397,22 +369,19 @@ public void testLabels() { "case 3:", " if ($jscomp$generator$context.yieldResult) {", " while (1) {", - " return $jscomp$generator$context.jumpTo(2);", + " return $jscomp$generator$context.jumpTo(0);", " }", " }", - "case 2:", " $jscomp$generator$context.jumpToEnd();")); rewriteGeneratorBody( "l: for (;;) { yield i; continue l; }", lines( - "case 4:", " return $jscomp$generator$context.yield(i, 5);", "case 5:", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(1);", " break;", - "case 2:", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(1);", " break;")); rewriteGeneratorBody( @@ -421,13 +390,12 @@ public void testLabels() { " return $jscomp$generator$context.yield(undefined, 3);", "case 3:", " if($jscomp$generator$context.yieldResult) {", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(0);", " break;", " } else {", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - "case 2:", " $jscomp$generator$context.jumpToEnd();")); } @@ -436,18 +404,76 @@ public void testUnreachable() { rewriteGeneratorBody( "while (true) {yield; break;}", lines( - "case 2:", " if (!true) {", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " return $jscomp$generator$context.yield(undefined, 4);", "case 4:", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(1);", + " break;")); + } + + public void testCaseNumberOptimization() { + rewriteGeneratorBodyWithVars( + lines( + "while (true) {", + " var gen = generatorSrc();", + " var gotResponse = false;", + " for (var response in gen) {", + " yield response;", + " gotResponse = true;", + " }", + " if (!gotResponse) {", + " return;", + " }", + "}"), + lines( + "var gen;", + "var gotResponse;", + "var response, $jscomp$generator$forin$0;"), + lines( + " if (!true) {", + " $jscomp$generator$context.jumpTo(0);", + " break;", + " }", + " gen = generatorSrc();", + " gotResponse = false;", + " $jscomp$generator$forin$0 = $jscomp$generator$context.forIn(gen);", + "case 4:", + " if (!((response=$jscomp$generator$forin$0.getNext()) != null)) {", + " $jscomp$generator$context.jumpTo(6);", + " break;", + " }", + " return $jscomp$generator$context.yield(response, 7);", + "case 7:", + " gotResponse = true;", + " $jscomp$generator$context.jumpTo(4);", " break;", - "case 3:", + "case 6:", + " if (!gotResponse) return $jscomp$generator$context.return(undefined);", + " $jscomp$generator$context.jumpTo(1);", + " break;")); + + rewriteGeneratorBody( + "do { do { do { yield; } while (3) } while (2) } while (1)", + lines( + " return $jscomp$generator$context.yield(undefined, 10);", + "case 10:", + " if (3) {", + " $jscomp$generator$context.jumpTo(1);", + " break;", + " }", + " if (2) {", + " $jscomp$generator$context.jumpTo(1);", + " break;", + " }", + " if (1) {", + " $jscomp$generator$context.jumpTo(1);", + " break;", + " }", " $jscomp$generator$context.jumpToEnd();")); } @@ -469,13 +495,10 @@ public void testIf() { " j = 0;", " if (j < 1) {", " j = 5;", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - " return $jscomp$generator$context.yield(j, 3);", - "case 3:", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(j, 0);")); // When "else" doesn't contain yields, it's more optimal to swap "if" and else "blocks" and // negate the condition. @@ -486,13 +509,10 @@ public void testIf() { " j = 0;", " if (!(j < 1)) {", " j = 5;", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - " return $jscomp$generator$context.yield(j, 3);", - "case 3:", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(j, 0);")); // No "else" block, pretend as it's empty rewriteGeneratorBodyWithVars( @@ -501,30 +521,80 @@ public void testIf() { lines( " j = 0;", " if (!(j < 1)) {", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - " return $jscomp$generator$context.yield(j, 3);", - "case 3:", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(j, 0);")); rewriteGeneratorBody( "if (i < 1) { yield i; } else { yield 1; }", lines( " if (i < 1) {", - " $jscomp$generator$context.jumpTo(2);", - " break;", + " return $jscomp$generator$context.yield(i, 0);", + " }", + " return $jscomp$generator$context.yield(1, 0);")); + + rewriteGeneratorBody( + "if (i < 1) { yield i; yield i + 1; i = 10; } else { yield 1; yield 2; i = 5;}", + lines( + " if (i < 1) {", + " return $jscomp$generator$context.yield(i, 6);", " }", " return $jscomp$generator$context.yield(1, 4);", "case 4:", - " $jscomp$generator$context.jumpTo(3);", - " break;", - "case 2:", - " return $jscomp$generator$context.yield(i, 5);", + " return $jscomp$generator$context.yield(2, 5);", "case 5:", - "case 3:", + " i = 5;", + " $jscomp$generator$context.jumpTo(0);", + " break;", + "case 6:", + " return $jscomp$generator$context.yield(i + 1, 7)", + "case 7:", + " i = 10;", " $jscomp$generator$context.jumpToEnd();")); + + rewriteGeneratorBody( + "if (i < 1) { while (true) { yield 1;} } else { while (false) { yield 2;}}", + lines( + " if (i < 1) {", + " $jscomp$generator$context.jumpTo(7);", + " break;", + " }", + "case 4:", + " if (!false) {", + " $jscomp$generator$context.jumpTo(0);", + " break;", + " }", + " return $jscomp$generator$context.yield(2, 4);", + "case 7:", + " if (!true) {", + " $jscomp$generator$context.jumpTo(0);", + " break;", + " }", + " return $jscomp$generator$context.yield(1, 7);")); + + rewriteGeneratorBody( + "if (i < 1) { if (i < 2) {yield 2; } } else { yield 1; }", + lines( + " if (i < 1) {", + " if (!(i < 2)) {", + " $jscomp$generator$context.jumpTo(0);", + " break;", + " }", + " return $jscomp$generator$context.yield(2, 0);", + " }", + " return $jscomp$generator$context.yield(1, 0)")); + + rewriteGeneratorBody( + "if (i < 1) { if (i < 2) {yield 2; } else { yield 3; } } else { yield 1; }", + lines( + " if (i < 1) {", + " if (i < 2) {", + " return $jscomp$generator$context.yield(2, 0);", + " }", + " return $jscomp$generator$context.yield(3, 0);", + " }", + " return $jscomp$generator$context.yield(1, 0)")); } public void testReturn() { @@ -564,17 +634,15 @@ public void testBreakContinue() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " return $jscomp$generator$context.yield(j, 4);", "case 4:", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " $jscomp$generator$context.jumpTo(2)", - " break;", - "case 3:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); rewriteGeneratorBodyWithVars( "var j = 0; while (j < 10) { yield j; continue; }", @@ -583,7 +651,7 @@ public void testBreakContinue() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " return $jscomp$generator$context.yield(j, 4);", @@ -591,9 +659,7 @@ public void testBreakContinue() { " $jscomp$generator$context.jumpTo(2);", " break;", " $jscomp$generator$context.jumpTo(2)", - " break;", - "case 3:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); rewriteGeneratorBodyWithVars( "for (var j = 0; j < 10; j++) { yield j; break; }", @@ -602,18 +668,16 @@ public void testBreakContinue() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " return $jscomp$generator$context.yield(j, 5);", "case 5:", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " j++;", " $jscomp$generator$context.jumpTo(2)", - " break;", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); rewriteGeneratorBodyWithVars( "for (var j = 0; j < 10; j++) { yield j; continue; }", @@ -622,7 +686,7 @@ public void testBreakContinue() { " j = 0;", "case 2:", " if (!(j < 10)) {", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " return $jscomp$generator$context.yield(j, 5);", @@ -632,20 +696,28 @@ public void testBreakContinue() { "case 3:", " j++;", " $jscomp$generator$context.jumpTo(2)", - " break;", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); } public void testDoWhileLoops() { rewriteGeneratorBody( "do { yield j; } while (j < 10);", lines( - "case 2:", - " return $jscomp$generator$context.yield(j, 5);", - "case 5:", + " return $jscomp$generator$context.yield(j, 4);", + "case 4:", " if (j<10) {", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(1);", + " break;", + " }", + " $jscomp$generator$context.jumpToEnd();")); + + rewriteGeneratorBody( + "do {} while (yield 1);", + lines( + " return $jscomp$generator$context.yield(1, 5);", + "case 5:", + " if ($jscomp$generator$context.yieldResult) {", + " $jscomp$generator$context.jumpTo(1);", " break;", " }", " $jscomp$generator$context.jumpToEnd();")); @@ -655,9 +727,7 @@ public void testYieldNoValue() { rewriteGeneratorBody( "yield;", lines( - " return $jscomp$generator$context.yield(undefined, 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(undefined, 0);")); } public void testReturnNoValue() { @@ -686,9 +756,7 @@ public void testYieldAll() { rewriteGeneratorBody( "yield * n;", lines( - " return $jscomp$generator$context.yieldAll(n, 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yieldAll(n, 0);")); rewriteGeneratorBodyWithVars( "var i = yield * n;", @@ -705,9 +773,7 @@ public void testYieldArguments() { "yield arguments[0];", "/** @const */ var $jscomp$generator$arguments = arguments;", lines( - " return $jscomp$generator$context.yield($jscomp$generator$arguments[0], 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield($jscomp$generator$arguments[0], 0);")); } public void testYieldThis() { @@ -715,9 +781,7 @@ public void testYieldThis() { "yield this;", "/** @const */ var $jscomp$generator$this = this;", lines( - " return $jscomp$generator$context.yield($jscomp$generator$this, 2);", - "case 2:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield($jscomp$generator$this, 0);")); } public void testGeneratorShortCircuit() { @@ -824,9 +888,8 @@ public void testYieldSwitch() { " }", "}"), lines( - "case 2:", " if (!1) {", - " $jscomp$generator$context.jumpTo(3);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " switch (i) {", @@ -838,22 +901,15 @@ public void testYieldSwitch() { " case 5: return $jscomp$generator$context.return(2);", " default: return $jscomp$generator$context.jumpTo(7);", " }", - " $jscomp$generator$context.jumpTo(8);", + " $jscomp$generator$context.jumpTo(1);", " break;", "case 4: return $jscomp$generator$context.yield(3, 9);", "case 9:", - " $jscomp$generator$context.jumpTo(2);", + " $jscomp$generator$context.jumpTo(1);", " break;", - "case 5: return $jscomp$generator$context.yield(4, 10);", - "case 10:", + "case 5: return $jscomp$generator$context.yield(4, 6);", "case 6: return $jscomp$generator$context.return(1);", - "case 7: return $jscomp$generator$context.yield(5, 11);", - "case 11:", - "case 8:", - " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 3:", - " $jscomp$generator$context.jumpToEnd();")); + "case 7: return $jscomp$generator$context.yield(5, 1);")); rewriteGeneratorBody( lines( @@ -869,12 +925,9 @@ public void testYieldSwitch() { " case 1:", " return $jscomp$generator$context.jumpTo(3)", " }", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", - "case 3: return $jscomp$generator$context.yield(1, 5);", - "case 5:", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + "case 3: return $jscomp$generator$context.yield(1, 0);")); } public void testNoTranslate() { @@ -902,15 +955,10 @@ public void testForIn() { " $jscomp$generator$forin$0 = $jscomp$generator$context.forIn(j);", "case 2:", " if (!((i = $jscomp$generator$forin$0.getNext()) != null)) {", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - " return $jscomp$generator$context.yield(i, 5);", - "case 5:", - " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(i, 2);")); rewriteGeneratorBodyWithVars( "for (var i in yield) { yield i; }", @@ -922,15 +970,10 @@ public void testForIn() { " $jscomp$generator$context.forIn($jscomp$generator$context.yieldResult);", "case 3:", " if (!((i = $jscomp$generator$forin$0.getNext()) != null)) {", - " $jscomp$generator$context.jumpTo(5);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", - " return $jscomp$generator$context.yield(i, 6);", - "case 6:", - " $jscomp$generator$context.jumpTo(3);", - " break;", - "case 5:", - " $jscomp$generator$context.jumpToEnd();")); + " return $jscomp$generator$context.yield(i, 3);")); rewriteGeneratorBodyWithVars( "for (i[yield] in j) {}", @@ -943,13 +986,11 @@ public void testForIn() { "case 5:", " if (!((JSCompiler_temp_const$jscomp$1[$jscomp$generator$context.yieldResult] =", " $jscomp$generator$forin$0.getNext()) != null)) {", - " $jscomp$generator$context.jumpTo(4);", + " $jscomp$generator$context.jumpTo(0);", " break;", " }", " $jscomp$generator$context.jumpTo(2);", - " break;", - "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " break;")); } public void testTryCatch() { @@ -960,11 +1001,10 @@ public void testTryCatch() { " $jscomp$generator$context.setCatchFinallyBlocks(2);", " return $jscomp$generator$context.yield(1, 4);", "case 4:", - " $jscomp$generator$context.leaveTryBlock(3)", + " $jscomp$generator$context.leaveTryBlock(0)", " break;", "case 2:", " e=$jscomp$generator$context.enterCatchBlock();", - "case 3:", " $jscomp$generator$context.jumpToEnd();")); rewriteGeneratorBodyWithVars( @@ -984,12 +1024,26 @@ public void testTryCatch() { " $jscomp$generator$context.setCatchFinallyBlocks(5);", " return $jscomp$generator$context.yield(1, 7);", "case 7:", - " $jscomp$generator$context.leaveTryBlock(6)", + " $jscomp$generator$context.leaveTryBlock(0)", " break;", "case 5:", " e = $jscomp$generator$context.enterCatchBlock();", - "case 6:", " $jscomp$generator$context.jumpToEnd();")); + + rewriteGeneratorBodyWithVars( + "l1: try { break l1; } catch (e) { yield; } finally {}", + "var e;", + lines( + " $jscomp$generator$context.setCatchFinallyBlocks(3, 4);", + " $jscomp$generator$context.jumpThroughFinallyBlocks(0);", + " break;", + "case 4:", + " $jscomp$generator$context.enterFinallyBlock();", + " $jscomp$generator$context.leaveFinallyBlock(0);", + " break;", + "case 3:", + " e = $jscomp$generator$context.enterCatchBlock();", + " return $jscomp$generator$context.yield(undefined, 4)")); } public void testFinally() { @@ -998,19 +1052,57 @@ public void testFinally() { "var e;", lines( " $jscomp$generator$context.setCatchFinallyBlocks(2, 3);", - " return $jscomp$generator$context.yield(1, 5);", - "case 5:", + " return $jscomp$generator$context.yield(1, 3);", "case 3:", " $jscomp$generator$context.enterFinallyBlock();", " b();", - " $jscomp$generator$context.leaveFinallyBlock(4);", + " $jscomp$generator$context.leaveFinallyBlock(0);", " break;", "case 2:", " e = $jscomp$generator$context.enterCatchBlock();", " $jscomp$generator$context.jumpTo(3);", + " break;")); + + rewriteGeneratorBodyWithVars( + lines( + "try {", + " try {", + " yield 1;", + " throw 2;", + " } catch (x) {", + " throw yield x;", + " } finally {", + " yield 5;", + " }", + "} catch (thrown) {", + " yield thrown;", + "}"), + "var x; var thrown;", + lines( + " $jscomp$generator$context.setCatchFinallyBlocks(2);", + " $jscomp$generator$context.setCatchFinallyBlocks(4, 5);", + " return $jscomp$generator$context.yield(1, 7);", + "case 7:", + " throw 2;", + "case 5:", + " $jscomp$generator$context.enterFinallyBlock(2);", + " return $jscomp$generator$context.yield(5, 8);", + "case 8:", + " $jscomp$generator$context.leaveFinallyBlock(6);", " break;", "case 4:", - " $jscomp$generator$context.jumpToEnd();")); + " x = $jscomp$generator$context.enterCatchBlock();", + " return $jscomp$generator$context.yield(x, 9);", + "case 9:", + " throw $jscomp$generator$context.yieldResult;", + " $jscomp$generator$context.jumpTo(5);", + " break;", + "case 6:", + " $jscomp$generator$context.leaveTryBlock(0);", + " break;", + "case 2:", + " thrown = $jscomp$generator$context.enterCatchBlock();", + " return $jscomp$generator$context.yield(thrown,0)")); } @Override