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 */
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 */
JSType originalGenReturnType;
Expand Down Expand Up @@ -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:
* <pre>
* function() {
* ...
* 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) {
// }
Node program =
IR.function(
IR.name(""),
IR.paramList(context.getJsContextNameNode(originalGeneratorBody)),
generatorBody);

// Replace original generator function body with:
// return $jscomp.generator.createGenerator(<origGenerator>, <program function>);
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<YIELD_TYPE>): (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<YIELD_TYPE>): (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(<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());

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

// Transpile statements from original generator function
while (originalGeneratorBody.hasChildren()) {
transpileStatement(originalGeneratorBody.removeFirstChild());
Expand All @@ -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) */
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<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>. */
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;
}
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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);
}
}

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() {
test(
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);",
"};",
"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();"));
}
Expand Down

0 comments on commit 658f807

Please sign in to comment.