Переглянути джерело

Implement Promise.allSettled (#1316)

Co-authored-by: Marko Lahma <[email protected]>
Gökhan Kurt 2 роки тому
батько
коміт
e9f3968085

+ 4 - 6
Jint.Tests.Test262/Test262Harness.settings.json

@@ -19,7 +19,6 @@
     "FinalizationRegistry",
     "generators",
     "import-assertions",
-    "Promise.allSettled",
     "regexp-duplicate-named-groups",
     "regexp-lookbehind",
     "regexp-unicode-property-escapes",
@@ -93,12 +92,11 @@
     "built-ins/RegExp/prototype/exec/S15.10.6.2_A1_T6.js",
     "built-ins/RegExp/S15.10.2.5_A1_T4.js",
 
-    "built-ins/String/raw/special-characters.js", // Windows line ending differences
-    "language/expressions/object/method-definition/object-method-returns-promise.js", // Promise not implemented
-    "language/statements/class/definition/class-method-returns-promise.js", // Promise not implemented
+    // Windows line ending differences
+    "built-ins/String/raw/special-characters.js",
 
-    // there is bug in suite and bug in Jint, refer to https://github.com/sebastienros/jint/issues/888 and https://github.com/tc39/test262/issues/2985
-    "built-ins/Promise/race/resolve-element-function-name.js",
+    // Async flag is missing from test
+    "language/expressions/object/method-definition/object-method-returns-promise.js",
 
     // parsing of large/small years not implemented in .NET (-271821, +271821)
     "built-ins/Date/parse/time-value-maximum-range.js",

+ 150 - 113
Jint/Native/Promise/PromiseConstructor.cs

@@ -40,28 +40,21 @@ namespace Jint.Native.Promise
         {
             const PropertyFlag propertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
             const PropertyFlag lengthFlags = PropertyFlag.Configurable;
-            var properties = new PropertyDictionary(5, checkExistingKeys: false)
-            {
-                ["resolve"] =
-                    new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "resolve", Resolve, 1, lengthFlags),
-                        propertyFlags)),
-                ["reject"] =
-                    new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "reject", Reject, 1, lengthFlags),
-                        propertyFlags)),
-                ["all"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "all", All, 1, lengthFlags),
-                    propertyFlags)),
-                ["any"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "any", Any, 1, lengthFlags),
-                    propertyFlags)),
-                ["race"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "race", Race, 1, lengthFlags),
-                    propertyFlags)),
+            var properties = new PropertyDictionary(6, checkExistingKeys: false)
+            {
+                ["resolve"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "resolve", Resolve, 1, lengthFlags), propertyFlags)),
+                ["reject"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "reject", Reject, 1, lengthFlags), propertyFlags)),
+                ["all"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "all", All, 1, lengthFlags), propertyFlags)),
+                ["allSettled"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "allSettled", AllSettled, 1, lengthFlags), propertyFlags)),
+                ["any"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "any", Any, 1, lengthFlags), propertyFlags)),
+                ["race"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "race", Race, 1, lengthFlags), propertyFlags)),
             };
             SetProperties(properties);
 
             var symbols = new SymbolDictionary(1)
             {
                 [GlobalSymbolRegistry.Species] = new GetSetPropertyDescriptor(
-                    get: new ClrFunctionInstance(_engine, "get [Symbol.species]", (thisObj, _) => thisObj, 0,
-                        PropertyFlag.Configurable),
+                    get: new ClrFunctionInstance(_engine, "get [Symbol.species]", (thisObj, _) => thisObj, 0, PropertyFlag.Configurable),
                     set: Undefined, PropertyFlag.Configurable)
             };
             SetSymbols(symbols);
@@ -157,34 +150,22 @@ namespace Jint.Native.Promise
             return instance;
         }
 
-        // https://tc39.es/ecma262/#sec-promise.all
-        // The all function returns a new promise which is fulfilled with an array of fulfillment values for the passed promises,
-        // or rejects with the reason of the first passed promise that rejects. It resolves all elements of the passed iterable to promises as it runs this algorithm.
-        //
-        // 1. Let C be the this value.
-        // 2. Let promiseCapability be ? NewPromiseCapability(C).
-        // 3. Let promiseResolve be GetPromiseResolve(C).
-        // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
-        // 5. Let iteratorRecord be GetIterator(iterable).
-        // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
-        // 7. Let result be PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve).
-        // 8. If result is an abrupt completion, then
-        //     a. If iteratorRecord.[[Done]] is false, set result to IteratorClose(iteratorRecord, result).
-        //     b. IfAbruptRejectPromise(result, promiseCapability).
-        // 9. Return Completion(result)
-        private JsValue All(JsValue thisObj, JsValue[] arguments)
+        // This helper methods executes the first 6 steps in the specs belonging to static Promise methods like all, any etc.
+        // If it returns false, that means it has an error and it is already rejected
+        // If it returns true, the logic specific to the calling function should continue executing
+        private bool TryGetPromiseCapabilityAndIterator(JsValue thisObj, JsValue[] arguments, string callerName, out PromiseCapability capability, out ICallable promiseResolve, out IteratorInstance iterator)
         {
             if (!thisObj.IsObject())
             {
-                ExceptionHelper.ThrowTypeError(_realm, "Promise.all called on non-object");
+                ExceptionHelper.ThrowTypeError(_realm, $"{callerName} called on non-object");
             }
 
             //2. Let promiseCapability be ? NewPromiseCapability(C).
-            var (resultingPromise, resolve, reject, _, rejectObj) = NewPromiseCapability(_engine, thisObj);
+            capability = NewPromiseCapability(_engine, thisObj);
+            var reject = capability.Reject;
 
             //3. Let promiseResolve be GetPromiseResolve(C).
             // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
-            ICallable promiseResolve;
             try
             {
                 promiseResolve = GetPromiseResolve(thisObj);
@@ -192,11 +173,12 @@ namespace Jint.Native.Promise
             catch (JavaScriptException e)
             {
                 reject.Call(Undefined, new[] { e.Error });
-                return resultingPromise;
+                promiseResolve = null!;
+                iterator = null!;
+                return false;
             }
 
 
-            IteratorInstance iterator;
             // 5. Let iteratorRecord be GetIterator(iterable).
             // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
 
@@ -204,7 +186,7 @@ namespace Jint.Native.Promise
             {
                 if (arguments.Length == 0)
                 {
-                    ExceptionHelper.ThrowTypeError(_realm, "no arguments were passed to Promise.all");
+                    ExceptionHelper.ThrowTypeError(_realm, $"no arguments were passed to {callerName}");
                 }
 
                 var iterable = arguments.At(0);
@@ -214,9 +196,21 @@ namespace Jint.Native.Promise
             catch (JavaScriptException e)
             {
                 reject.Call(Undefined, new[] { e.Error });
-                return resultingPromise;
+                iterator = null!;
+                return false;
             }
 
+            return true;
+        }
+
+        // https://tc39.es/ecma262/#sec-promise.all
+        private JsValue All(JsValue thisObj, JsValue[] arguments)
+        {
+            if (!TryGetPromiseCapabilityAndIterator(thisObj, arguments, "Promise.all", out var capability, out var promiseResolve, out var iterator))
+                return capability.PromiseInstance;
+
+            var (resultingPromise, resolve, reject, _, rejectObj) = capability;
+
             var results = new List<JsValue>();
             bool doneIterating = false;
 
@@ -271,13 +265,13 @@ namespace Jint.Native.Promise
                     {
                         var capturedIndex = index;
 
-                        var fulfilled = false;
+                        var alreadyCalled = false;
                         var onSuccess =
                             new ClrFunctionInstance(_engine, "", (_, args) =>
                             {
-                                if (!fulfilled)
+                                if (!alreadyCalled)
                                 {
-                                    fulfilled = true;
+                                    alreadyCalled = true;
                                     results[capturedIndex] = args.At(0);
                                     ResolveIfFinished();
                                 }
@@ -305,52 +299,132 @@ namespace Jint.Native.Promise
             return resultingPromise;
         }
 
-        // https://tc39.es/ecma262/#sec-promise.any
-        private JsValue Any(JsValue thisObj, JsValue[] arguments)
+        // https://tc39.es/ecma262/#sec-promise.allsettled
+        private JsValue AllSettled(JsValue thisObj, JsValue[] arguments)
         {
-            if (!thisObj.IsObject())
-            {
-                ExceptionHelper.ThrowTypeError(_realm, "Promise.any called on non-object");
-            }
+            if (!TryGetPromiseCapabilityAndIterator(thisObj, arguments, "Promise.allSettled", out var capability, out var promiseResolve, out var iterator))
+                return capability.PromiseInstance;
 
-            //2. Let promiseCapability be ? NewPromiseCapability(C).
-            var (resultingPromise, resolve, reject, resolveObj, _) = NewPromiseCapability(_engine, thisObj);
+            var (resultingPromise, resolve, reject, _, rejectObj) = capability;
 
-            //3. Let promiseResolve be GetPromiseResolve(C).
-            // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
-            ICallable promiseResolve;
-            try
-            {
-                promiseResolve = GetPromiseResolve(thisObj);
-            }
-            catch (JavaScriptException e)
+            var results = new List<JsValue>();
+            bool doneIterating = false;
+
+            void ResolveIfFinished()
             {
-                reject.Call(Undefined, new[] { e.Error });
-                return resultingPromise;
+                // that means all of them were resolved
+                // Note that "Undefined" is not null, thus the logic is sound, even though awkward
+                // also note that it is important to check if we are done iterating.
+                // if "then" method is sync then it will be resolved BEFORE the next iteration cycle
+                if (results.TrueForAll(static x => x != null) && doneIterating)
+                {
+                    var array = _realm.Intrinsics.Array.ConstructFast(results);
+                    resolve.Call(Undefined, new JsValue[] { array });
+                }
             }
 
-
-            IteratorInstance iterator;
-            // 5. Let iteratorRecord be GetIterator(iterable).
-            // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
-
+            // 27.2.4.1.2 PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )
+            // https://tc39.es/ecma262/#sec-performpromiseall
             try
             {
-                if (arguments.Length == 0)
+                int index = 0;
+
+                do
                 {
-                    ExceptionHelper.ThrowTypeError(_realm, "no arguments were passed to Promise.all");
-                }
+                    JsValue value;
+                    try
+                    {
+                        if (!iterator.TryIteratorStep(out var nextItem))
+                        {
+                            doneIterating = true;
 
-                var iterable = arguments.At(0);
+                            ResolveIfFinished();
+                            break;
+                        }
 
-                iterator = iterable.GetIterator(_realm);
+                        value = nextItem.Get(CommonProperties.Value);
+                    }
+                    catch (JavaScriptException e)
+                    {
+                        reject.Call(Undefined, new[] { e.Error });
+                        return resultingPromise;
+                    }
+
+                    // note that null here is important
+                    // it will help to detect if all inner promises were resolved
+                    // In F# it would be Option<JsValue>
+                    results.Add(null!);
+
+                    var item = promiseResolve.Call(thisObj, new JsValue[] { value });
+                    var thenProps = item.Get("then");
+                    if (thenProps is ICallable thenFunc)
+                    {
+                        var capturedIndex = index;
+
+                        var alreadyCalled = false;
+                        var onSuccess =
+                            new ClrFunctionInstance(_engine, "", (_, args) =>
+                            {
+                                if (!alreadyCalled)
+                                {
+                                    alreadyCalled = true;
+
+                                    var res = Engine.Realm.Intrinsics.Object.Construct(2);
+                                    res.FastAddProperty("status", "fulfilled", true, true, true);
+                                    res.FastAddProperty("value", args.At(0), true, true, true);
+                                    results[capturedIndex] = res;
+
+                                    ResolveIfFinished();
+                                }
+
+                                return Undefined;
+                            }, 1, PropertyFlag.Configurable);
+                        var onFailure =
+                            new ClrFunctionInstance(_engine, "", (_, args) =>
+                            {
+                                if (!alreadyCalled)
+                                {
+                                    alreadyCalled = true;
+
+                                    var res = Engine.Realm.Intrinsics.Object.Construct(2);
+                                    res.FastAddProperty("status", "rejected", true, true, true);
+                                    res.FastAddProperty("reason", args.At(0), true, true, true);
+                                    results[capturedIndex] = res;
+
+                                    ResolveIfFinished();
+                                }
+
+                                return Undefined;
+                            }, 1, PropertyFlag.Configurable);
+
+                        thenFunc.Call(item, new JsValue[] { onSuccess, onFailure });
+                    }
+                    else
+                    {
+                        ExceptionHelper.ThrowTypeError(_realm, "Passed non Promise-like value");
+                    }
+
+                    index += 1;
+                } while (true);
             }
             catch (JavaScriptException e)
             {
+                iterator.Close(CompletionType.Throw);
                 reject.Call(Undefined, new[] { e.Error });
                 return resultingPromise;
             }
 
+            return resultingPromise;
+        }
+
+        // https://tc39.es/ecma262/#sec-promise.any
+        private JsValue Any(JsValue thisObj, JsValue[] arguments)
+        {
+            if (!TryGetPromiseCapabilityAndIterator(thisObj, arguments, "Promise.any", out var capability, out var promiseResolve, out var iterator))
+                return capability.PromiseInstance;
+
+            var (resultingPromise, resolve, reject, resolveObj, _) = capability;
+
             var errors = new List<JsValue>();
             bool doneIterating = false;
 
@@ -405,14 +479,14 @@ namespace Jint.Native.Promise
                     {
                         var capturedIndex = index;
 
-                        var fulfilled = false;
+                        var alreadyCalled = false;
 
                         var onError =
                             new ClrFunctionInstance(_engine, "", (_, args) =>
                             {
-                                if (!fulfilled)
+                                if (!alreadyCalled)
                                 {
-                                    fulfilled = true;
+                                    alreadyCalled = true;
                                     errors[capturedIndex] = args.At(0);
                                     RejectIfAllRejected();
                                 }
@@ -443,48 +517,11 @@ namespace Jint.Native.Promise
         // https://tc39.es/ecma262/#sec-promise.race
         private JsValue Race(JsValue thisObj, JsValue[] arguments)
         {
-            if (!thisObj.IsObject())
-            {
-                ExceptionHelper.ThrowTypeError(_realm, "Promise.all called on non-object");
-            }
-
-            // 2. Let promiseCapability be ? NewPromiseCapability(C).
-            var (resultingPromise, resolve, reject, _, rejectObj) = NewPromiseCapability(_engine, thisObj);
-
-            // 3. Let promiseResolve be GetPromiseResolve(C).
-            // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
-            ICallable promiseResolve;
-            try
-            {
-                promiseResolve = GetPromiseResolve(thisObj);
-            }
-            catch (JavaScriptException e)
-            {
-                reject.Call(Undefined, new[] { e.Error });
-                return resultingPromise;
-            }
-
-
-            IteratorInstance iterator;
-            // 5. Let iteratorRecord be GetIterator(iterable).
-            // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
+            if (!TryGetPromiseCapabilityAndIterator(thisObj, arguments, "Promise.race", out var capability, out var promiseResolve, out var iterator))
+                return capability.PromiseInstance;
 
-            try
-            {
-                if (arguments.Length == 0)
-                {
-                    ExceptionHelper.ThrowTypeError(_realm, "no arguments were passed to Promise.all");
-                }
+            var (resultingPromise, resolve, reject, _, rejectObj) = capability;
 
-                var iterable = arguments.At(0);
-
-                iterator = iterable.GetIterator(_realm);
-            }
-            catch (JavaScriptException e)
-            {
-                reject.Call(Undefined, new[] { e.Error });
-                return resultingPromise;
-            }
 
             // 7. Let result be PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve).
             // https://tc39.es/ecma262/#sec-performpromiserace

+ 1 - 1
README.md

@@ -82,7 +82,7 @@ The entire execution engine was rebuild with performance in mind, in many cases
 - ✔ `import.meta`
 - ✔ Nullish coalescing operator (`??`)
 - ✔ Optional chaining
--  `Promise.allSettled`
+-  `Promise.allSettled`
 - ✔ `String.prototype.matchAll`
 
 #### ECMAScript 2021