Skip to content

Commit

Permalink
Preload Generator skeleton that is needed for transpiling generators …
Browse files Browse the repository at this point in the history
…in EarlyEs6ToEs3Converter.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=165611752
  • Loading branch information
EatingW authored and Tyler Breisacher committed Aug 18, 2017
1 parent 60a022f commit ce7bc2a
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 90 deletions.
11 changes: 11 additions & 0 deletions src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java
Expand Up @@ -85,6 +85,12 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
// but we want the runtime functions to be have TypeI applied to it by the type checker.
Es6ToEs3Util.preloadEs6RuntimeFunction(compiler, "makeIterator");
break;
case YIELD:
if (n.isYieldAll()) {
Es6ToEs3Util.preloadEs6RuntimeFunction(compiler, "makeIterator");
}
Es6ToEs3Util.preloadEs6Symbol(compiler);
break;
case GETTER_DEF:
case SETTER_DEF:
if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) {
Expand Down Expand Up @@ -127,6 +133,11 @@ public void visit(NodeTraversal t, Node n, Node parent) {
}
}
break;
case FUNCTION:
if (n.isGeneratorFunction()) {
Es6RewriteGenerators.preloadGeneratorSkeletonAndReportChange(compiler);
}
break;
default:
break;
}
Expand Down
155 changes: 112 additions & 43 deletions src/com/google/javascript/jscomp/Es6RewriteGenerators.java
Expand Up @@ -31,6 +31,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

/**
* Converts ES6 generator functions to valid ES3 code. This pass runs after all ES6 features
Expand All @@ -41,6 +42,8 @@
public final class Es6RewriteGenerators
extends NodeTraversal.AbstractPostOrderCallback implements HotSwapCompilerPass {

static final String GENERATOR_PRELOAD_FUNCTION_NAME = "$jscomp$generator$function$name";

// Name of the variable that holds the state at which the generator
// should resume execution after a call to yield or return.
// The beginning state is 0 and the end state is -1.
Expand Down Expand Up @@ -101,7 +104,10 @@ public Es6RewriteGenerators(AbstractCompiler compiler) {

@Override
public void process(Node externs, Node root) {
// Report change only if the generator function is preloaded. See #cleanUpGeneratorSkeleton.
boolean reportChange = getPreloadedGeneratorFunc(compiler.getJsRoot()) != null;
TranspilationPasses.processTranspile(compiler, root, new DecomposeYields(compiler), this);
cleanUpGeneratorSkeleton(reportChange);
}

@Override
Expand Down Expand Up @@ -222,50 +228,9 @@ private void visitYieldExpr(NodeTraversal t, Node n, Node parent) {
}

private void visitGenerator(Node n, Node parent) {
compiler.ensureLibraryInjected("es6/symbol", false);
Es6ToEs3Util.preloadEs6Symbol(compiler);
hasTranslatedTry = false;
Node genBlock =
compiler
.parseSyntheticCode(
Joiner.on('\n')
.join(
"function generatorBody() {",
" var " + GENERATOR_STATE + " = " + generatorCaseCount + ";",
" function $jscomp$generator$impl(",
" " + GENERATOR_ACTION_ARG + ",",
" " + GENERATOR_NEXT_ARG + ",",
" " + GENERATOR_THROW_ARG + ") {",
" while (1) switch (" + GENERATOR_STATE + ") {",
" case " + generatorCaseCount + ":",
" default:",
" return {value: undefined, done: true};",
" }",
" }",
// TODO(tbreisacher): Remove this cast if we start returning an actual
// Generator object.
" var iterator = /** @type {!Generator<?>} */ ({",
" next: function(arg) {",
" return $jscomp$generator$impl("
+ GENERATOR_ACTION_NEXT
+ ", arg, undefined);",
" },",
" throw: function(arg) {",
" return $jscomp$generator$impl("
+ GENERATOR_ACTION_THROW
+ ", undefined, arg);",
" },",
// TODO(tbreisacher): Implement Generator.return:
// http://www.ecma-international.org/ecma-262/6.0/#sec-generator.prototype.return
" return: function(arg) { throw Error('Not yet implemented'); },",
" });",
" $jscomp.initSymbolIterator();",
" /** @this {!Generator<?>} */",
" iterator[Symbol.iterator] = function() { return this; };",
" return iterator;",
"}"))
.getFirstChild() // function
.getLastChild()
.detach();
Node genBlock = preloadGeneratorSkeleton(compiler, false).getLastChild().cloneTree();
generatorCaseCount++;

originalGeneratorBody = n.getLastChild();
Expand Down Expand Up @@ -1234,4 +1199,108 @@ private static final class ExceptionContext {
this.catchBlock = catchBlock;
}
}

/**
* Preloads the skeleton AST function that is needed for generators,
* reports change to enclosing scope, and returns it.
* If the skeleton is already preloaded, does not do anything, just returns the node.
*/
static Node preloadGeneratorSkeletonAndReportChange(AbstractCompiler compiler) {
return preloadGeneratorSkeleton(compiler, true);
}

/**
* Preloads the skeleton AST function that is needed for generators and returns it.
* If the skeleton is already preloaded, does not do anything, just returns the node.
* reportChange tells the function whether to report a code change in the enclosing scope.
*
* Because sanity checks happen between passes, we need to report the change if the generator
* was preloaded in the {@link EarlyEs6ToEs3Converter} class.
* However, if the generator was preloaded in this {@link Es6RewriteGenerators} class, we do not
* want to report the change since it will be removed by {@link #cleanUpGeneratorSkeleton}
*/
private static Node preloadGeneratorSkeleton(AbstractCompiler compiler, boolean reportChange) {
Node root = compiler.getJsRoot();
Node generatorFunc = getPreloadedGeneratorFunc(root);
if (generatorFunc != null) {
return generatorFunc;
}
Node genFunc = compiler.parseSyntheticCode(Joiner.on('\n').join(
"function " + GENERATOR_PRELOAD_FUNCTION_NAME + "() {",
" var " + GENERATOR_STATE + " = 0;",
" function $jscomp$generator$impl(",
" " + GENERATOR_ACTION_ARG + ",",
" " + GENERATOR_NEXT_ARG + ",",
" " + GENERATOR_THROW_ARG + ") {",
" while (1) switch (" + GENERATOR_STATE + ") {",
" case 0:",
" default:",
" return {value: undefined, done: true};",
" }",
" }",
// TODO(tbreisacher): Remove this cast if we start returning an actual
// Generator object.
" var iterator = /** @type {!Generator<?>} */ ({",
" next: function(arg) {",
" return $jscomp$generator$impl("
+ GENERATOR_ACTION_NEXT
+ ", arg, undefined);",
" },",
" throw: function(arg) {",
" return $jscomp$generator$impl("
+ GENERATOR_ACTION_THROW
+ ", undefined, arg);",
" },",
// TODO(tbreisacher): Implement Generator.return:
// http://www.ecma-international.org/ecma-262/6.0/#sec-generator.prototype.return
" return: function(arg) { throw Error('Not yet implemented'); },",
" });",
" $jscomp.initSymbolIterator();",
" /** @this {!Generator<?>} */",
" iterator[Symbol.iterator] = function() { return this; };",
" return iterator;",
"}"))
.getFirstChild() // function
.detach();
root.getFirstChild().addChildToFront(genFunc);
if (reportChange) {
NodeUtil.markNewScopesChanged(genFunc, compiler);
compiler.reportChangeToEnclosingScope(genFunc);
}
return genFunc;
}

/** Returns the generator function that was preloaded, or null if not found. */
@Nullable
private static Node getPreloadedGeneratorFunc(Node root) {
if (root.getFirstChild() == null) {
return null;
}
for (Node c = root.getFirstFirstChild(); c != null; c = c.getNext()) {
if (c.isFunction() && GENERATOR_PRELOAD_FUNCTION_NAME.equals(c.getFirstChild().getString())) {
return c;
}
}
return null;
}

/**
* Delete the preloaded generator function, and report code change if reportChange is true.
*
* We only want to reportChange if the generator function was preloaded in the
* {@link EarlyEs6ToEs3Converter} class, since a change was reported there.
* If we preload the generator function in this class, it will be an addition and deletion of the
* same node, which means we do not have to report code change in either case since the code was
* ultimately not changed.
*/
private void cleanUpGeneratorSkeleton(boolean reportChange) {
Node genFunc = getPreloadedGeneratorFunc(compiler.getJsRoot());
if (genFunc != null) {
if (reportChange) {
NodeUtil.deleteNode(genFunc, compiler);
} else {
genFunc.detach();
}
}
}
}
5 changes: 4 additions & 1 deletion src/com/google/javascript/jscomp/Es6ToEs3Util.java
Expand Up @@ -76,6 +76,10 @@ static void preloadEs6RuntimeFunction(AbstractCompiler compiler, String function
compiler.ensureLibraryInjected("es6/util/" + function.toLowerCase(Locale.US), false);
}

static void preloadEs6Symbol(AbstractCompiler compiler) {
compiler.ensureLibraryInjected("es6/symbol", false);
}

static Node callEs6RuntimeFunction(
AbstractCompiler compiler, Node iterable, String function) {
preloadEs6RuntimeFunction(compiler, function);
Expand Down Expand Up @@ -117,4 +121,3 @@ static TypeI createGenericType(
return registry.instantiateGenericType(uninstantiated, ImmutableList.of(typeArg));
}
}

Expand Up @@ -1379,6 +1379,13 @@ public void testCatch() {
"}"));
}

public void testBlockScopedGeneratorFunction() {
// Functions defined in a block get translated to a var
test(
"{ function *f() {yield 1;} }",
"{ var f = function*() { yield 1; }; }");
}

public void testExterns() {
testExternChanges("let x;", "", "var x;");
}
Expand Down
26 changes: 26 additions & 0 deletions test/com/google/javascript/jscomp/Es6RewriteClassTest.java
Expand Up @@ -1974,6 +1974,32 @@ public void testComputedPropClass() {
"C[foo] = function() { alert(2); };"));
}

public void testComputedPropGeneratorMethods() {
test(
"class C { *[foo]() { yield 1; } }",
LINE_JOINER.join(
"/** @constructor @struct */",
"let C = function() {};",
"C.prototype[foo] = function*() { yield 1; };"));

test(
"class C { static *[foo]() { yield 2; } }",
LINE_JOINER.join(
"/** @constructor @struct */",
"let C = function() {};",
"C[foo] = function*() { yield 2; };"));
}

public void testClassGenerator() {
test(
"class C { *foo() { yield 1; } }",
LINE_JOINER.join(
"/** @constructor @struct */",
"let C = function() {};",
"C.prototype.foo = function*() { yield 1;};"));
assertThat(getLastCompiler().injected).isEmpty();
}

@Override
protected Compiler createCompiler() {
return new NoninjectingCompiler();
Expand Down
41 changes: 0 additions & 41 deletions test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
Expand Up @@ -160,16 +160,6 @@ public void testObjectLiteralMemberFunctionDef() {
assertThat(getLastCompiler().injected).isEmpty();
}

public void testClassGenerator() {
test(
"class C { *foo() { yield 1; } }",
LINE_JOINER.join(
"/** @constructor @struct */",
"var C = function() {};",
"C.prototype.foo = function*() { yield 1;};"));
assertThat(getLastCompiler().injected).isEmpty();
}

public void testClassStatement() {
test("class C { }", "/** @constructor @struct */ var C = function() {};");
test(
Expand Down Expand Up @@ -2654,14 +2644,6 @@ public void testComputedProperties() {
LINE_JOINER.join(
"var $jscomp$compprop0 = {};",
"var obj = ($jscomp$compprop0[foo] = function(){}, $jscomp$compprop0)"));

test(
"var obj = { *[foo]() {}}",
LINE_JOINER.join(
"var $jscomp$compprop0 = {};",
"var obj = (",
" $jscomp$compprop0[foo] = function*(){},",
" $jscomp$compprop0)"));
}

public void testComputedPropGetterSetter() {
Expand Down Expand Up @@ -2698,29 +2680,6 @@ public void testComputedPropClass() {
"C[foo] = function() { alert(2); };"));
}

public void testComputedPropGeneratorMethods() {
test(
"class C { *[foo]() { yield 1; } }",
LINE_JOINER.join(
"/** @constructor @struct */",
"var C = function() {};",
"C.prototype[foo] = function*() { yield 1; };"));

test(
"class C { static *[foo]() { yield 2; } }",
LINE_JOINER.join(
"/** @constructor @struct */",
"var C = function() {};",
"C[foo] = function*() { yield 2; };"));
}

public void testBlockScopedGeneratorFunction() {
// Functions defined in a block get translated to a var
test(
"{ function *f() {yield 1;} }",
"{ var f = function*() { yield 1; }; }");
}

public void testComputedPropCannotConvert() {
testError("var o = { get [foo]() {}}", CANNOT_CONVERT_YET);
testError("var o = { set [foo](val) {}}", CANNOT_CONVERT_YET);
Expand Down
Expand Up @@ -233,6 +233,11 @@ private final void parseAndTypeCheck(String externs, String js) {
// Create common parent of externs and ast; needed by Es6RewriteBlockScopedDeclaration.
Node block = IR.root(externsRoot, astRoot);

// TODO(dimvar): clean this up and use parseInputs instead of setting the jsRoot directly.
compiler.jsRoot = astRoot;
compiler.externsRoot = externsRoot;
compiler.externAndJsRoot = block;

// Run ASTValidator
(new AstValidator(compiler)).validateRoot(block);

Expand Down
Expand Up @@ -632,11 +632,11 @@ public void testForOf() {

typeCheck(
LINE_JOINER.join(
"function* foo(){",
" yield 1; ",
" yield 2; ",
" yield 3; ",
"}; ",
"function* foo() {",
" yield 1;",
" yield 2;",
" yield 3;",
"}",
"function f(/** number */ y) {",
" for (var x of foo()) { y = x; }",
"}"));
Expand Down
3 changes: 3 additions & 0 deletions test/com/google/javascript/jscomp/TypeCheckTest.java
Expand Up @@ -17978,6 +17978,9 @@ private TypeCheckResult parseAndTypeCheckWithScope(String externs, String js) {
Node externsNode = compiler.getInput(new InputId("[externs]"))
.getAstRoot(compiler);
Node externAndJsRoot = IR.root(externsNode, n);
compiler.jsRoot = n;
compiler.externsRoot = externsNode;
compiler.externAndJsRoot = externAndJsRoot;

assertEquals("parsing error: " +
Joiner.on(", ").join(compiler.getErrors()),
Expand Down
Expand Up @@ -34,3 +34,11 @@ function testWithMethod() {
};
assertEquals(3, obj['f1'] + obj.m());
}

function testWithGenerator() {
var obj = {
*["foo" + "bar"]() { yield 1; }
};
var gen = obj["foobar"]();
assertEquals(1, gen.next().value);
}

0 comments on commit ce7bc2a

Please sign in to comment.