Procházet zdrojové kódy

Fixing the evaulation order of default parameters in generator functions

Currently the default parameter expressions are evaluated as part of the body. So when generator function is called (which creates the generator object) default parameter expressions are not executed. This changelist is to fix this behavior. A yield opcode is inserted at the beginning of the function body and after creating generator object a next call is made.
Aneesh Divakarakurup před 8 roky
rodič
revize
42541f9bcb

+ 14 - 2
lib/Runtime/ByteCode/ByteCodeEmitter.cpp

@@ -11,6 +11,7 @@ void EmitAssignment(ParseNode *asgnNode, ParseNode *lhs, Js::RegSlot rhsLocation
 void EmitLoad(ParseNode *rhs, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo);
 void EmitCall(ParseNode* pnode, Js::RegSlot rhsLocation, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, BOOL fReturnValue, BOOL fEvaluateComponents, BOOL fHasNewTarget, Js::RegSlot overrideThisLocation = Js::Constants::NoRegister);
 void EmitSuperFieldPatch(FuncInfo* funcInfo, ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator);
+void EmitYield(Js::RegSlot inputLocation, Js::RegSlot resultLocation, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot yieldStarIterator = Js::Constants::NoRegister);
 
 void EmitUseBeforeDeclaration(Symbol *sym, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo);
 void EmitUseBeforeDeclarationRuntimeError(ByteCodeGenerator *byteCodeGenerator, Js::RegSlot location);
@@ -3359,6 +3360,18 @@ void ByteCodeGenerator::EmitOneFunction(ParseNode *pnode)
             }
         }
 
+        // If the function has non simple parameter list, the params needs to be evaluated when the generator object is created
+        // (that is when the function is called). This yield opcode is to mark the  begining of the function body.
+        // TODO: Inserting a yield should have almost no impact on perf as it is a direct return from the function. But this needs
+        // to be verified. Ideally if the function has simple parameter list then we can avoid inserting the opcode and the additional call.
+        if (pnode->sxFnc.IsGenerator())
+        {
+            Js::RegSlot tempReg = funcInfo->AcquireTmpRegister();
+            EmitYield(funcInfo->AssignUndefinedConstRegister(), tempReg, this, funcInfo);
+            m_writer.Reg1(Js::OpCode::Unused, tempReg);
+            funcInfo->ReleaseTmpRegister(tempReg);
+        }
+
         // Emit all scope-wide function definitions before emitting function bodies
         // so that calls may reference functions they precede lexically.
         // Note, global eval scope is a fake local scope and is handled as if it were
@@ -10154,8 +10167,7 @@ void ByteCodeGenerator::EmitTryBlockHeadersAfterYield()
     }
 }
 
-void EmitYield(Js::RegSlot inputLocation, Js::RegSlot resultLocation, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo,
-    Js::RegSlot yieldStarIterator = Js::Constants::NoRegister)
+void EmitYield(Js::RegSlot inputLocation, Js::RegSlot resultLocation, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot yieldStarIterator)
 {
     // If the bytecode emitted by this function is part of 'yield*', inputLocation is the object
     // returned by the iterable's next/return/throw method. Otherwise, it is the yielded value.

+ 5 - 0
lib/Runtime/ByteCode/ByteCodeGenerator.cpp

@@ -3055,6 +3055,11 @@ FuncInfo* PostVisitFunction(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerat
         top->AssignSuperCtorRegister();
     }
 
+    if (pnode->sxFnc.IsGenerator())
+    {
+        top->AssignUndefinedConstRegister();
+    }
+
     if ((top->root->sxFnc.IsConstructor() && (top->isNewTargetLexicallyCaptured || top->GetCallsEval() || top->GetChildCallsEval())) || top->IsClassConstructor())
     {
         if (top->IsBaseClassConstructor())

+ 4 - 1
lib/Runtime/Library/JavascriptGeneratorFunction.cpp

@@ -114,6 +114,8 @@ namespace Js
         PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
         ARGUMENTS(stackArgs, callInfo);
 
+        Assert(!(callInfo.Flags & CallFlags_New));
+
         ScriptContext* scriptContext = function->GetScriptContext();
         JavascriptGeneratorFunction* generatorFunction = JavascriptGeneratorFunction::FromVar(function);
 
@@ -128,7 +130,8 @@ namespace Js
         // Set the prototype from constructor
         JavascriptOperators::OrdinaryCreateFromConstructor(function, generator, prototype, scriptContext);
 
-        Assert(!(callInfo.Flags & CallFlags_New));
+        // Call a next on the generator to execute till the beginning of the body
+        CALL_ENTRYPOINT(scriptContext->GetThreadContext(), generator->EntryNext, function, CallInfo(CallFlags_Value, 1), generator);
 
         return generator;
     }

+ 62 - 0
test/es6/generators-functionality.js

@@ -1782,6 +1782,68 @@ var tests = [
             assert.areEqual(gf, callergf, "Generator function returned through the caller property should be the same as the original generator function");
             assert.areEqual(100, callergf(true, 10).next().value, "Generator returned through the caller property should behave the same as the original generator function");
         }
+    },
+    {
+        name: "generator function with default parameters",
+        body: function () {
+            var yGf1 = 1;
+            function *gf1(x = yGf1 = 100) {
+                assert.areEqual(100, x, "Default parameter value is updated insided the body");
+            }
+            gf1();
+            assert.areEqual(100, yGf1, "Default parameter expression should be evaluated when the generator function is called");
+
+            var yGf2 = 1;
+            function *gf2(x, y = yGf2 = 100) {
+                yGf2 = 101;
+                yield yGf2;
+                yGf2++;
+                return yGf2;
+            }
+            var g2 = gf2();
+            assert.areEqual(100, yGf2, "Default parameter expression should be evaluated when the generator function is called even if the generator function has yield in the body");
+            var result = g2.next();
+            assert.areEqual(101, result.value, "First next call start the execution of body");
+            assert.areEqual(false, result.done, "Generator is not done yet")
+            result = g2.next();
+            assert.areEqual(102, result.value, "Second next call increments the outside var");
+            assert.areEqual(true, result.done, "Generator is done");
+
+            var yGf3 = 1;
+            function *gf3(x = yGf3 = 100, y = () => x) {
+                assert.areEqual(100, x, "Default parameter value is updated inside the body");
+            }
+            gf3();
+            assert.areEqual(100, yGf3, "Default parameter expression should be evaluated when the generator function with split scope is called");
+
+            var yGf4 = 1;
+            function *gf4(x = eval("yGf4 = 100")) {
+                assert.areEqual(100, x, "Default parameter value is updated inside the body");
+            }
+            gf4();
+            assert.areEqual(100, yGf4, "Default parameter expression should be evaluated when the generator function with eval is called");
+
+            var yGf5 = 1;
+            function *gf5(x = yGf5 = 100) {
+                assert.fail("Body is not expected to be executed!");
+            }
+            gf5.prototype.next = function () {
+                yGf5++;
+            }
+            var g5 = gf5();
+            assert.areEqual(100, yGf5, "Default parameter expression should be evaluated when the generator function is called even if the next function is overwritten");
+            g5.next();
+            assert.areEqual(101, yGf5, "Call to next method executes the new method instead of the built-in one");
+
+            var yGf6 = 1;
+            var yield = 200;
+            function *gf6(x = yGf6, y = yGf6 = 100, z = () => yield) {
+                assert.areEqual(1, x, "Default parameter value is updated inside the body");
+                assert.areEqual(200, z(), "Function defined in the parameter captures the yield variable from outside");
+            }
+            gf6();
+            assert.areEqual(100, yGf6, "Default parameter expression should be evaluated when the generator function is called");
+        }
     }
     // TODO: Test yield in expression positions of control flow constructs, e.g. initializer, condition, and increment of a for loop
 ];

+ 8 - 8
test/es6/rlexe.xml

@@ -837,21 +837,21 @@
   <test>
     <default>
       <files>generators-syntax.js</files>
-      <compile-flags>-ES6Generators -JitES6Generators -ES6Classes -ES6DefaultArgs -args summary -endargs</compile-flags>
+      <compile-flags>-ES6Generators -ES6Classes -ES6DefaultArgs -args summary -endargs</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>
   <test>
     <default>
       <files>generators-deferparse.js</files>
-      <compile-flags>-force:deferparse -ES6Generators -JitES6Generators -ES6Classes -ES6DefaultArgs</compile-flags>
+      <compile-flags>-force:deferparse -ES6Generators -ES6Classes -ES6DefaultArgs</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>
   <test>
     <default>
       <files>generators-apis.js</files>
-      <compile-flags>-ES6Generators -JitES6Generators -args summary -endargs</compile-flags>
+      <compile-flags>-ES6Generators -args summary -endargs</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>
@@ -866,14 +866,14 @@
   <test>
     <default>
       <files>generators-deferred.js</files>
-      <compile-flags>-ES6Generators -JitES6Generators -ES6Classes -force:deferparse</compile-flags>
+      <compile-flags>-ES6Generators -ES6Classes -force:deferparse</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>
   <test>
     <default>
       <files>generators-deferred.js</files>
-      <compile-flags>-ES6Generators -JitES6Generators -ES6Classes -serialized</compile-flags>
+      <compile-flags>-ES6Generators -ES6Classes -serialized</compile-flags>
       <tags>exclude_arm,exclude_forceserialized</tags>
     </default>
   </test>
@@ -887,7 +887,7 @@
   <test>
     <default>
       <files>generators-cachedscope.js</files>
-      <compile-flags>-ES6Generators -JitES6Generators</compile-flags>
+      <compile-flags>-ES6Generators</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>
@@ -895,14 +895,14 @@
     <default>
       <files>generators-applyargs.js</files>
       <!-- -off:inlineapply triggers the use of the ApplyArgs bytecode -->
-      <compile-flags>-ES6Generators -JitES6Generators -off:inlineapply</compile-flags>
+      <compile-flags>-ES6Generators -off:inlineapply</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>
   <test>
     <default>
       <files>generators-applyargs.js</files>
-      <compile-flags>-ES6Generators -JitES6Generators</compile-flags>
+      <compile-flags>-ES6Generators</compile-flags>
       <tags>exclude_arm</tags>
     </default>
   </test>