Skip to content

Commit

Permalink
Short circuiting async functions.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
SergejSalnikov authored and lauraharker committed May 3, 2018
1 parent 4daf56c commit 658f807
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 82 deletions.
213 changes: 149 additions & 64 deletions src/com/google/javascript/jscomp/Es6RewriteGenerators.java
Expand Up @@ -236,8 +236,23 @@ private class SingleGeneratorFunctionTranspiler {
/** The body of original generator function that should be transpiled */ /** The body of original generator function that should be transpiled */
final Node originalGeneratorBody; final Node originalGeneratorBody;


/** The body of a replacement function. */ /**
Node newGeneratorBody; * The body of a replacement function.
*
* <p>It's a block node that hoists local variables of a generator program and returns an
* actual generator object created from that program:
* <pre>
* {
* var a;
* var b;
* ...
* return createGenerator(function ($jscomp$generator$context) { ... });
* }
* </pre>
* 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 */ /** The original inferred return type of the Generator */
JSType originalGenReturnType; JSType originalGenReturnType;
Expand Down Expand Up @@ -271,67 +286,140 @@ private class SingleGeneratorFunctionTranspiler {
this.context = new TranspilationContext(contextType); this.context = new TranspilationContext(contextType);
} }


public void transpile() { /**
// Test if end of generator function is reachable * Hoists a node inside {@link #newGeneratorHoistBlock}.
boolean shouldAddFinalJump = !isEndOfBlockUnreachable(originalGeneratorBody); *
Node genFunc = originalGeneratorBody.getParent(); * @see #newGeneratorHoistBlock
checkState(genFunc.isGeneratorFunction()); */
private void hoistNode(Node node) {
newGeneratorHoistBlock.addChildBefore(node, newGeneratorHoistBlock.getLastChild());
}


Node genFuncName = genFunc.getFirstChild(); /**
checkState(genFuncName.isName()); * Detects whether the generator function was generated by async function transpilation:
// The transpiled function needs to be able to refer to itself, so make sure it has a name. * <pre>
if (genFuncName.getString().isEmpty()) { * function() {
genFuncName.setString(context.getScopedName(GENERATOR_FUNCTION).getString()); * ...
* return $jscomp.asyncExecutePromiseGeneratorFunction(function* genFunc() {...});
* }
* </pre>
*/
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) { // function ($jscomp$generator$context) {
// } // }
Node program = final Node program;
IR.function( JSType programType = shouldAddTypes
IR.name(""), // function(!Context<YIELD_TYPE>): (void|{value: YIELD_TYPE})
IR.paramList(context.getJsContextNameNode(originalGeneratorBody)), ? registry.createFunctionType(
generatorBody); registry.createUnionType(

voidType,
// Replace original generator function body with: registry.createRecordType(ImmutableMap.of("value", yieldType))),
// return $jscomp.generator.createGenerator(<origGenerator>, <program function>); context.contextType)
Node createGenerator = : null;
IR.getprop( Node generatorBody = IR.block();
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));


if (shouldAddTypes) { final Node changeScopeNode;
createGenerator.setJSType(registry.createFunctionType(originalGenReturnType)); if (isTranspiledAsyncFunction(generatorFunction)) {
// function(!Context<YIELD_TYPE>): (void|{value: YIELD_TYPE}) // Our generatorFunction is a transpiled async function
program.setJSType(registry.createFunctionType(
registry.createUnionType( // $jscomp.asyncExecutePromiseGeneratorFunction
registry.getNativeType(JSTypeNative.VOID_TYPE), Node callTarget = generatorFunction.getPrevious();
registry.createRecordType(ImmutableMap.of("value", yieldType))), checkState(callTarget.isGetProp());
context.contextType));
} // 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. program = originalGeneratorBody.getParent();
compiler.reportChangeToChangeScope(program); // 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(<origGenerator>, <program function>);
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()); NodeTraversal.traverse(compiler, originalGeneratorBody, new YieldNodeMarker());


// Test if end of generator function is reachable
boolean shouldAddFinalJump = !isEndOfBlockUnreachable(originalGeneratorBody);

// Transpile statements from original generator function // Transpile statements from original generator function
while (originalGeneratorBody.hasChildren()) { while (originalGeneratorBody.hasChildren()) {
transpileStatement(originalGeneratorBody.removeFirstChild()); transpileStatement(originalGeneratorBody.removeFirstChild());
Expand All @@ -349,8 +437,7 @@ public void transpile() {
context.finalizeTransformation(generatorBody); context.finalizeTransformation(generatorBody);
context.checkStateIsEmpty(); context.checkStateIsEmpty();


genFunc.putBooleanProp(Node.GENERATOR_FN, false); compiler.reportChangeToChangeScope(changeScopeNode);
compiler.reportChangeToChangeScope(genFunc);
} }


/** @see #transpileStatement(Node, TranspilationContext.Case, TranspilationContext.Case) */ /** @see #transpileStatement(Node, TranspilationContext.Case, TranspilationContext.Case) */
Expand Down Expand Up @@ -447,7 +534,7 @@ void transpileUnmarkedNode(Node n) {
// "function *(...) {...}" becomes "function $jscomp$generator$function(...) {...}" as // "function *(...) {...}" becomes "function $jscomp$generator$function(...) {...}" as
// inner generator functions are transpiled first). // inner generator functions are transpiled first).
checkState(!functionName.isEmpty() && !functionName.startsWith(GENERATOR_FUNCTION)); checkState(!functionName.isEmpty() && !functionName.startsWith(GENERATOR_FUNCTION));
newGeneratorBody.addChildBefore(n, newGeneratorBody.getLastChild()); hoistNode(n);
return; return;
} }
context.transpileUnmarkedBlock(n.isNormalBlock() || n.isAddedBlock() ? n : IR.block(n)); context.transpileUnmarkedBlock(n.isNormalBlock() || n.isAddedBlock() ? n : IR.block(n));
Expand Down Expand Up @@ -1659,9 +1746,7 @@ void enterCatchBlock(@Nullable Case finallyCase, Node exceptionName) {
Case nextCatchCase = getNextCatchCase(); Case nextCatchCase = getNextCatchCase();


if (catchNames.add(exceptionName.getString())) { if (catchNames.add(exceptionName.getString())) {
newGeneratorBody.addChildBefore( hoistNode(IR.var(exceptionName.cloneNode()).useSourceInfoFrom(exceptionName));
IR.var(exceptionName.cloneNode()).useSourceInfoFrom(exceptionName),
newGeneratorBody.getLastChild());
} }


ArrayList<Node> args = new ArrayList<>(); ArrayList<Node> args = new ArrayList<>();
Expand Down Expand Up @@ -2006,15 +2091,15 @@ void visitBreakContinue(Node n) {


/** Replaces reference to <code>this</code> with <code>$jscomp$generator$this</code>. */ /** Replaces reference to <code>this</code> with <code>$jscomp$generator$this</code>. */
void visitThis(Node n) { void visitThis(Node n) {
Node newThis = Node newThis = withType(context.getScopedName(GENERATOR_THIS), n.getTypeI());
withType(context.getScopedName(GENERATOR_THIS), n.getTypeI()).useSourceInfoFrom(n);
n.replaceWith(newThis); n.replaceWith(newThis);
if (!thisReferenceFound) { 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); JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false);
jsDoc.recordConstancy(); jsDoc.recordConstancy();
var.setJSDocInfo(jsDoc.build()); var.setJSDocInfo(jsDoc.build());
newGeneratorBody.addChildBefore(var, newGeneratorBody.getLastChild()); hoistNode(var);
thisReferenceFound = true; thisReferenceFound = true;
} }
} }
Expand All @@ -2028,11 +2113,11 @@ void visitArguments(Node n) {
n.replaceWith(newArguments); n.replaceWith(newArguments);
if (!argumentsReferenceFound) { if (!argumentsReferenceFound) {
Node var = Node var =
IR.var(newArguments.cloneNode(), n).useSourceInfoFrom(newGeneratorBody); IR.var(newArguments.cloneNode(), n).useSourceInfoFrom(newGeneratorHoistBlock);
JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false); JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false);
jsDoc.recordConstancy(); jsDoc.recordConstancy();
var.setJSDocInfo(jsDoc.build()); var.setJSDocInfo(jsDoc.build());
newGeneratorBody.addChildBefore(var, newGeneratorBody.getLastChild()); hoistNode(var);
argumentsReferenceFound = true; argumentsReferenceFound = true;
} }
} }
Expand Down Expand Up @@ -2105,7 +2190,7 @@ void visitVar(Node varStatement) {
} }
// Place original var statement with initial values removed to just before // Place original var statement with initial values removed to just before
// the program method definition. // the program method definition.
newGeneratorBody.addChildBefore(varStatement, newGeneratorBody.getLastChild()); hoistNode(varStatement);
} }
} }


Expand Down
60 changes: 60 additions & 0 deletions test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java
Expand Up @@ -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() { public void testUnnamed() {
test( test(
lines("f = function *() {};"), lines("f = function *() {};"),
Expand Down
32 changes: 14 additions & 18 deletions test/com/google/javascript/jscomp/IntegrationTest.java
Expand Up @@ -5071,24 +5071,20 @@ public void testMethodDestructuringInTranspiledAsyncFunction() {
" alert(i);", " alert(i);",
"};", "};",
"function foo() {", "function foo() {",
" return $jscomp.asyncExecutePromiseGeneratorFunction(", " var JSCompiler_temp_const;",
" function $jscomp$generator$function() {", " var JSCompiler_temp_const$jscomp$1;",
" var JSCompiler_temp_const;", " return $jscomp.asyncExecutePromiseGeneratorProgram(",
" var JSCompiler_temp_const$jscomp$1;", " function ($jscomp$generator$context) {",
" return $jscomp.generator.createGenerator(", " if ($jscomp$generator$context.nextAddress == 1) {",
" $jscomp$generator$function,", " JSCompiler_temp_const = A;",
" function ($jscomp$generator$context) {", " JSCompiler_temp_const$jscomp$1 = A$doSomething;",
" if ($jscomp$generator$context.nextAddress == 1) {", " return $jscomp$generator$context.yield(3, 2);",
" JSCompiler_temp_const = A;", " }",
" JSCompiler_temp_const$jscomp$1 = A$doSomething;", " JSCompiler_temp_const$jscomp$1.call(",
" return $jscomp$generator$context.yield(3, 2);", " JSCompiler_temp_const,",
" }", " $jscomp$generator$context.yieldResult);",
" JSCompiler_temp_const$jscomp$1.call(", " $jscomp$generator$context.jumpToEnd();",
" JSCompiler_temp_const,", " });",
" $jscomp$generator$context.yieldResult);",
" $jscomp$generator$context.jumpToEnd();",
" });",
" })",
"}", "}",
"foo();")); "foo();"));
} }
Expand Down

0 comments on commit 658f807

Please sign in to comment.