From 658f807348d64c857834130d6c47f136331f2e18 Mon Sep 17 00:00:00 2001 From: skill Date: Thu, 3 May 2018 07:58:10 -0700 Subject: [PATCH] Short circuiting async functions. Today: async function f() { var x = 10; return x; } Is transpiled to: function f() { return $jscomp.executeAsyncGenerator( function $jscomp$async$generator() { var x; return $jscomp.generator.createGenerator( $jscomp$async$generator, function($jscomp$generator$context) { x = 10; return $jscomp$generator$context.return(x); }); }); }; Will be transpiled to: function f() { var x; return $jscomp.asyncExecutePromiseGeneratorProgram( function ($jscomp$generator$context) { x = 10; return $jscomp$generator$context.return(x); }); }; ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=195251809 --- .../jscomp/Es6RewriteGenerators.java | 213 ++++++++++++------ .../jscomp/Es6RewriteGeneratorsTest.java | 60 +++++ .../javascript/jscomp/IntegrationTest.java | 32 ++- 3 files changed, 223 insertions(+), 82 deletions(-) diff --git a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java index 8b961af7a63..3cb5e816ea2 100644 --- a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java +++ b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java @@ -236,8 +236,23 @@ private class SingleGeneratorFunctionTranspiler { /** The body of original generator function that should be transpiled */ final Node originalGeneratorBody; - /** The body of a replacement function. */ - Node newGeneratorBody; + /** + * 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; @@ -271,67 +286,140 @@ private class SingleGeneratorFunctionTranspiler { this.context = new TranspilationContext(contextType); } - public void transpile() { - // Test if end of generator function is reachable - boolean shouldAddFinalJump = !isEndOfBlockUnreachable(originalGeneratorBody); - Node genFunc = originalGeneratorBody.getParent(); - checkState(genFunc.isGeneratorFunction()); + /** + * Hoists a node inside {@link #newGeneratorHoistBlock}. + * + * @see #newGeneratorHoistBlock + */ + private void hoistNode(Node node) { + newGeneratorHoistBlock.addChildBefore(node, newGeneratorHoistBlock.getLastChild()); + } - Node genFuncName = genFunc.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()); + /** + * 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; + } - Node generatorBody = IR.block(); + public void transpile() { + Node generatorFunction = originalGeneratorBody.getParent(); + checkState(generatorFunction.isGeneratorFunction()); + generatorFunction.putBooleanProp(Node.GENERATOR_FN, false); - // Prepare a "program" function: + // A "program" function: // function ($jscomp$generator$context) { // } - Node program = - IR.function( - IR.name(""), - IR.paramList(context.getJsContextNameNode(originalGeneratorBody)), - generatorBody); - - // Replace original generator function body with: - // return $jscomp.generator.createGenerator(, ); - Node createGenerator = - IR.getprop( - withType( - IR.getprop(withType(IR.name("$jscomp"), unknownType), "generator"), unknownType), - "createGenerator"); - newGeneratorBody = - 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(), genFunc.getTypeI()), - program), - this.originalGenReturnType)) - .useSourceInfoFromForTree(originalGeneratorBody)); + 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(); - if (shouldAddTypes) { - createGenerator.setJSType(registry.createFunctionType(originalGenReturnType)); - // function(!Context): (void|{value: YIELD_TYPE}) - program.setJSType(registry.createFunctionType( - registry.createUnionType( - registry.getNativeType(JSTypeNative.VOID_TYPE), - registry.createRecordType(ImmutableMap.of("value", yieldType))), - context.contextType)); - } + 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.isNormalBlock(), 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)); + } - // Newly introduced functions have to be reported immediately. - compiler.reportChangeToChangeScope(program); + 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()); + } - originalGeneratorBody.replaceWith(newGeneratorBody); + // 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.getTypeI()), + 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()); @@ -349,8 +437,7 @@ public void transpile() { context.finalizeTransformation(generatorBody); context.checkStateIsEmpty(); - genFunc.putBooleanProp(Node.GENERATOR_FN, false); - compiler.reportChangeToChangeScope(genFunc); + compiler.reportChangeToChangeScope(changeScopeNode); } /** @see #transpileStatement(Node, TranspilationContext.Case, TranspilationContext.Case) */ @@ -447,7 +534,7 @@ void transpileUnmarkedNode(Node n) { // "function *(...) {...}" becomes "function $jscomp$generator$function(...) {...}" as // inner generator functions are transpiled first). checkState(!functionName.isEmpty() && !functionName.startsWith(GENERATOR_FUNCTION)); - newGeneratorBody.addChildBefore(n, newGeneratorBody.getLastChild()); + hoistNode(n); return; } context.transpileUnmarkedBlock(n.isNormalBlock() || n.isAddedBlock() ? n : IR.block(n)); @@ -1659,9 +1746,7 @@ void enterCatchBlock(@Nullable Case finallyCase, Node exceptionName) { Case nextCatchCase = getNextCatchCase(); if (catchNames.add(exceptionName.getString())) { - newGeneratorBody.addChildBefore( - IR.var(exceptionName.cloneNode()).useSourceInfoFrom(exceptionName), - newGeneratorBody.getLastChild()); + hoistNode(IR.var(exceptionName.cloneNode()).useSourceInfoFrom(exceptionName)); } ArrayList args = new ArrayList<>(); @@ -2006,15 +2091,15 @@ void visitBreakContinue(Node n) { /** Replaces reference to this with $jscomp$generator$this. */ void visitThis(Node n) { - Node newThis = - withType(context.getScopedName(GENERATOR_THIS), n.getTypeI()).useSourceInfoFrom(n); + Node newThis = withType(context.getScopedName(GENERATOR_THIS), n.getTypeI()); n.replaceWith(newThis); if (!thisReferenceFound) { - Node var = IR.var(newThis.cloneNode(), n).useSourceInfoFrom(newGeneratorBody); + Node var = IR.var(newThis.cloneNode().useSourceInfoFrom(n), n) + .useSourceInfoFrom(newGeneratorHoistBlock); JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false); jsDoc.recordConstancy(); var.setJSDocInfo(jsDoc.build()); - newGeneratorBody.addChildBefore(var, newGeneratorBody.getLastChild()); + hoistNode(var); thisReferenceFound = true; } } @@ -2028,11 +2113,11 @@ void visitArguments(Node n) { n.replaceWith(newArguments); if (!argumentsReferenceFound) { Node var = - IR.var(newArguments.cloneNode(), n).useSourceInfoFrom(newGeneratorBody); + IR.var(newArguments.cloneNode(), n).useSourceInfoFrom(newGeneratorHoistBlock); JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false); jsDoc.recordConstancy(); var.setJSDocInfo(jsDoc.build()); - newGeneratorBody.addChildBefore(var, newGeneratorBody.getLastChild()); + hoistNode(var); argumentsReferenceFound = true; } } @@ -2105,7 +2190,7 @@ void visitVar(Node varStatement) { } // Place original var statement with initial values removed to just before // the program method definition. - newGeneratorBody.addChildBefore(varStatement, newGeneratorBody.getLastChild()); + hoistNode(varStatement); } } diff --git a/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java b/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java index 1786d70cc1c..96838a269c1 100644 --- a/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java +++ b/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java @@ -103,6 +103,66 @@ private void rewriteGeneratorSwitchBodyWithVars( "}")); } + public void testGeneratorForAsyncFunction() { + ensureLibraryInjected("es6/execute_async_generator"); + + test( + lines( + "f = function() {", + " return $jscomp.asyncExecutePromiseGeneratorFunction(", + " function *() {", + " var x = 6;", + " yield x;", + " });", + "}"), + lines( + "f = function () {", + " var x;", + " return $jscomp.asyncExecutePromiseGeneratorProgram(", + " function ($jscomp$generator$context) {", + " x = 6;", + " return $jscomp$generator$context.yield(x, 0);", + " });", + "}")); + + test( + lines( + "f = function(a) {", + " var $jscomp$restParams = [];", + " for (var $jscomp$restIndex = 0;", + " $jscomp$restIndex < arguments.length;", + " ++$jscomp$restIndex) {", + " $jscomp$restParams[$jscomp$restIndex - 0] = arguments[$jscomp$restIndex];", + " }", + " {", + " var bla$0 = $jscomp$restParams;", + " return $jscomp.asyncExecutePromiseGeneratorFunction(", + " function *() {", + " var x = bla$0[0];", + " yield x;", + " });", + " }", + "}"), + lines( + "f = function (a) {", + " var $jscomp$restParams = [];", + " for (var $jscomp$restIndex = 0;", + " $jscomp$restIndex < arguments.length;", + " ++$jscomp$restIndex) {", + " $jscomp$restParams[$jscomp$restIndex - 0] = arguments[$jscomp$restIndex];", + " }", + " {", + " var bla$0 = $jscomp$restParams;", + " var x;", + " return $jscomp.asyncExecutePromiseGeneratorProgram(", + " function ($jscomp$generator$context) {", + " x = bla$0[0];", + " return $jscomp$generator$context.yield(x, 0);", + " });", + " }", + "}")); + } + public void testUnnamed() { test( lines("f = function *() {};"), diff --git a/test/com/google/javascript/jscomp/IntegrationTest.java b/test/com/google/javascript/jscomp/IntegrationTest.java index ff15aac4af8..c69bd7f7110 100644 --- a/test/com/google/javascript/jscomp/IntegrationTest.java +++ b/test/com/google/javascript/jscomp/IntegrationTest.java @@ -5071,24 +5071,20 @@ public void testMethodDestructuringInTranspiledAsyncFunction() { " alert(i);", "};", "function foo() {", - " return $jscomp.asyncExecutePromiseGeneratorFunction(", - " function $jscomp$generator$function() {", - " var JSCompiler_temp_const;", - " var JSCompiler_temp_const$jscomp$1;", - " return $jscomp.generator.createGenerator(", - " $jscomp$generator$function,", - " function ($jscomp$generator$context) {", - " if ($jscomp$generator$context.nextAddress == 1) {", - " JSCompiler_temp_const = A;", - " JSCompiler_temp_const$jscomp$1 = A$doSomething;", - " return $jscomp$generator$context.yield(3, 2);", - " }", - " JSCompiler_temp_const$jscomp$1.call(", - " JSCompiler_temp_const,", - " $jscomp$generator$context.yieldResult);", - " $jscomp$generator$context.jumpToEnd();", - " });", - " })", + " var JSCompiler_temp_const;", + " var JSCompiler_temp_const$jscomp$1;", + " return $jscomp.asyncExecutePromiseGeneratorProgram(", + " function ($jscomp$generator$context) {", + " if ($jscomp$generator$context.nextAddress == 1) {", + " JSCompiler_temp_const = A;", + " JSCompiler_temp_const$jscomp$1 = A$doSomething;", + " return $jscomp$generator$context.yield(3, 2);", + " }", + " JSCompiler_temp_const$jscomp$1.call(", + " JSCompiler_temp_const,", + " $jscomp$generator$context.yieldResult);", + " $jscomp$generator$context.jumpToEnd();", + " });", "}", "foo();")); }