From ce7bc2a504168c3b4fad1d88fd3ccb3eba92f2a3 Mon Sep 17 00:00:00 2001 From: yitingwang Date: Thu, 17 Aug 2017 12:21:58 -0700 Subject: [PATCH] Preload Generator skeleton that is needed for transpiling generators in EarlyEs6ToEs3Converter. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165611752 --- .../jscomp/EarlyEs6ToEs3Converter.java | 11 ++ .../jscomp/Es6RewriteGenerators.java | 155 +++++++++++++----- .../javascript/jscomp/Es6ToEs3Util.java | 5 +- .../Es6RewriteBlockScopedDeclarationTest.java | 7 + .../jscomp/Es6RewriteClassTest.java | 26 +++ .../jscomp/Es6ToEs3ConverterTest.java | 41 ----- .../jscomp/NewTypeInferenceTestBase.java | 5 + ...NewTypeInferenceWithTranspilationTest.java | 10 +- .../javascript/jscomp/TypeCheckTest.java | 3 + .../runtime_tests/computed_properties_test.js | 8 + 10 files changed, 181 insertions(+), 90 deletions(-) diff --git a/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java b/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java index a2eed920f4b..a9a7182396f 100644 --- a/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java +++ b/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java @@ -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) { @@ -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; } diff --git a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java index cdd1202ae89..d8c5837e8f7 100644 --- a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java +++ b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java @@ -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 @@ -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. @@ -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 @@ -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(); @@ -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(); + } + } + } } diff --git a/src/com/google/javascript/jscomp/Es6ToEs3Util.java b/src/com/google/javascript/jscomp/Es6ToEs3Util.java index b1f8bac3adb..903a1d1daa7 100644 --- a/src/com/google/javascript/jscomp/Es6ToEs3Util.java +++ b/src/com/google/javascript/jscomp/Es6ToEs3Util.java @@ -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); @@ -117,4 +121,3 @@ static TypeI createGenericType( return registry.instantiateGenericType(uninstantiated, ImmutableList.of(typeArg)); } } - diff --git a/test/com/google/javascript/jscomp/Es6RewriteBlockScopedDeclarationTest.java b/test/com/google/javascript/jscomp/Es6RewriteBlockScopedDeclarationTest.java index 36d2d6d021b..4332833dc1c 100644 --- a/test/com/google/javascript/jscomp/Es6RewriteBlockScopedDeclarationTest.java +++ b/test/com/google/javascript/jscomp/Es6RewriteBlockScopedDeclarationTest.java @@ -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;"); } diff --git a/test/com/google/javascript/jscomp/Es6RewriteClassTest.java b/test/com/google/javascript/jscomp/Es6RewriteClassTest.java index 632d7ded903..89706bc0ae5 100644 --- a/test/com/google/javascript/jscomp/Es6RewriteClassTest.java +++ b/test/com/google/javascript/jscomp/Es6RewriteClassTest.java @@ -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(); diff --git a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java index a719addca08..f42d3d940fe 100644 --- a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java +++ b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java @@ -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( @@ -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() { @@ -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); diff --git a/test/com/google/javascript/jscomp/NewTypeInferenceTestBase.java b/test/com/google/javascript/jscomp/NewTypeInferenceTestBase.java index 72125e7157a..8f792121706 100644 --- a/test/com/google/javascript/jscomp/NewTypeInferenceTestBase.java +++ b/test/com/google/javascript/jscomp/NewTypeInferenceTestBase.java @@ -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); diff --git a/test/com/google/javascript/jscomp/NewTypeInferenceWithTranspilationTest.java b/test/com/google/javascript/jscomp/NewTypeInferenceWithTranspilationTest.java index e63d07a1d68..9524ca53177 100644 --- a/test/com/google/javascript/jscomp/NewTypeInferenceWithTranspilationTest.java +++ b/test/com/google/javascript/jscomp/NewTypeInferenceWithTranspilationTest.java @@ -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; }", "}")); diff --git a/test/com/google/javascript/jscomp/TypeCheckTest.java b/test/com/google/javascript/jscomp/TypeCheckTest.java index 643437dff8b..19f5e47092d 100644 --- a/test/com/google/javascript/jscomp/TypeCheckTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckTest.java @@ -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()), diff --git a/test/com/google/javascript/jscomp/runtime_tests/computed_properties_test.js b/test/com/google/javascript/jscomp/runtime_tests/computed_properties_test.js index 6e806ca4fc4..55093a9ac68 100644 --- a/test/com/google/javascript/jscomp/runtime_tests/computed_properties_test.js +++ b/test/com/google/javascript/jscomp/runtime_tests/computed_properties_test.js @@ -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); +}