Ver código fonte

LibJS: Implement Promise.all on the Promise constructor

Timothy Flynn 4 anos atrás
pai
commit
dee3b7b8c9

+ 1 - 0
Userland/Libraries/LibJS/CMakeLists.txt

@@ -94,6 +94,7 @@ set(SOURCES
     Runtime/OrdinaryFunctionObject.cpp
     Runtime/PrimitiveString.cpp
     Runtime/Promise.cpp
+    Runtime/PromiseAllResolveElementFunction.cpp
     Runtime/PromiseConstructor.cpp
     Runtime/PromiseJobs.cpp
     Runtime/PromisePrototype.cpp

+ 65 - 0
Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.cpp

@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2021, Tim Flynn <trflynn89@pm.me>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/AbstractOperations.h>
+#include <LibJS/Runtime/Array.h>
+#include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/PromiseAllResolveElementFunction.h>
+#include <LibJS/Runtime/PromiseReaction.h>
+
+namespace JS {
+
+PromiseAllResolveElementFunction* PromiseAllResolveElementFunction::create(GlobalObject& global_object, size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements)
+{
+    return global_object.heap().allocate<PromiseAllResolveElementFunction>(global_object, index, values, capability, remaining_elements, *global_object.function_prototype());
+}
+
+PromiseAllResolveElementFunction::PromiseAllResolveElementFunction(size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements, Object& prototype)
+    : NativeFunction(prototype)
+    , m_index(index)
+    , m_values(values)
+    , m_capability(move(capability))
+    , m_remaining_elements(remaining_elements)
+{
+}
+
+void PromiseAllResolveElementFunction::initialize(GlobalObject& global_object)
+{
+    Base::initialize(global_object);
+    define_direct_property(vm().names.length, Value(1), Attribute::Configurable);
+}
+
+Value PromiseAllResolveElementFunction::call()
+{
+    auto& vm = this->vm();
+    auto& global_object = this->global_object();
+
+    if (m_already_called)
+        return js_undefined();
+    m_already_called = true;
+
+    m_values.values[m_index] = vm.argument(0);
+
+    if (--m_remaining_elements.value == 0) {
+        auto values_array = Array::create_from(global_object, m_values.values);
+        return vm.call(*m_capability.resolve, js_undefined(), values_array);
+    }
+
+    return js_undefined();
+}
+
+void PromiseAllResolveElementFunction::visit_edges(Cell::Visitor& visitor)
+{
+    Base::visit_edges(visitor);
+
+    visitor.visit(&m_values);
+    visitor.visit(m_capability.promise);
+    visitor.visit(m_capability.resolve);
+    visitor.visit(m_capability.reject);
+    visitor.visit(&m_remaining_elements);
+}
+
+}

+ 61 - 0
Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.h

@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2021, Tim Flynn <trflynn89@pm.me>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibJS/Runtime/NativeFunction.h>
+#include <LibJS/Runtime/PromiseReaction.h>
+
+namespace JS {
+
+struct RemainingElements final : public Cell {
+    RemainingElements() = default;
+
+    explicit RemainingElements(u64 initial_value)
+        : value(initial_value)
+    {
+    }
+
+    virtual const char* class_name() const override { return "RemainingElements"; }
+
+    u64 value { 0 };
+};
+
+struct PromiseValueList final : public Cell {
+    PromiseValueList()
+        : values(heap())
+    {
+    }
+
+    virtual const char* class_name() const override { return "PromiseValueList"; }
+
+    MarkedValueList values;
+};
+
+// 27.2.4.1.3 Promise.all Resolve Element Functions, https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions
+class PromiseAllResolveElementFunction final : public NativeFunction {
+    JS_OBJECT(PromiseResolvingFunction, NativeFunction);
+
+public:
+    static PromiseAllResolveElementFunction* create(GlobalObject&, size_t, PromiseValueList&, PromiseCapability, RemainingElements&);
+
+    explicit PromiseAllResolveElementFunction(size_t, PromiseValueList&, PromiseCapability, RemainingElements&, Object& prototype);
+    virtual void initialize(GlobalObject&) override;
+    virtual ~PromiseAllResolveElementFunction() override = default;
+
+    virtual Value call() override;
+
+private:
+    virtual void visit_edges(Visitor&) override;
+
+    size_t m_index { 0 };
+    PromiseValueList& m_values;
+    PromiseCapability m_capability;
+    RemainingElements& m_remaining_elements;
+    bool m_already_called { false };
+};
+
+}

+ 143 - 2
Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp

@@ -6,15 +6,131 @@
 
 #include <LibJS/Interpreter.h>
 #include <LibJS/Runtime/AbstractOperations.h>
+#include <LibJS/Runtime/Array.h>
 #include <LibJS/Runtime/Error.h>
 #include <LibJS/Runtime/FunctionObject.h>
 #include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/IteratorOperations.h>
 #include <LibJS/Runtime/Promise.h>
+#include <LibJS/Runtime/PromiseAllResolveElementFunction.h>
 #include <LibJS/Runtime/PromiseConstructor.h>
 #include <LibJS/Runtime/PromiseReaction.h>
+#include <LibJS/Runtime/TemporaryClearException.h>
 
 namespace JS {
 
+// 27.2.4.1.1 GetPromiseResolve ( promiseConstructor ), https://tc39.es/ecma262/#sec-getpromiseresolve
+static Value get_promise_resolve(GlobalObject& global_object, Value constructor)
+{
+    VERIFY(constructor.is_constructor());
+    auto& vm = global_object.vm();
+
+    auto promise_resolve = constructor.get(global_object, vm.names.resolve);
+    if (vm.exception())
+        return {};
+    if (!promise_resolve.is_function()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotAFunction, promise_resolve.to_string_without_side_effects());
+        return {};
+    }
+
+    return promise_resolve;
+}
+
+// 27.2.1.1.1 IfAbruptRejectPromise ( value, capability ), https://tc39.es/ecma262/#sec-ifabruptrejectpromise
+static Optional<Value> if_abrupt_reject_promise(GlobalObject& global_object, Value, PromiseCapability capability)
+{
+    auto& vm = global_object.vm();
+
+    if (auto* exception = vm.exception()) {
+        vm.clear_exception();
+        vm.stop_unwind();
+
+        (void)vm.call(*capability.reject, js_undefined(), exception->value());
+        return capability.promise;
+    }
+
+    return {};
+}
+
+static bool iterator_record_is_complete(GlobalObject& global_object, Object& iterator_record)
+{
+    auto& vm = global_object.vm();
+
+    // FIXME: Create a native iterator structure with the [[Done]] internal slot. For now, temporarily clear
+    //        the exception so we can access the "done" property on the iterator object.
+    TemporaryClearException clear_exception(vm);
+    return iterator_complete(global_object, iterator_record);
+}
+
+static void set_iterator_record_complete(GlobalObject& global_object, Object& iterator_record)
+{
+    auto& vm = global_object.vm();
+
+    // FIXME: Create a native iterator structure with the [[Done]] internal slot. For now, temporarily clear
+    //        the exception so we can access the "done" property on the iterator object.
+    TemporaryClearException clear_exception(vm);
+    iterator_record.set(vm.names.done, Value(true), Object::ShouldThrowExceptions::No);
+}
+
+// 27.2.4.1.2 PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve ), https://tc39.es/ecma262/#sec-performpromiseall
+static Value perform_promise_all(GlobalObject& global_object, Object& iterator_record, Value constructor, PromiseCapability result_capability, Value promise_resolve)
+{
+    auto& vm = global_object.vm();
+
+    VERIFY(constructor.is_constructor());
+    VERIFY(promise_resolve.is_function());
+
+    auto* values = vm.heap().allocate_without_global_object<PromiseValueList>();
+    auto* remaining_elements_count = vm.heap().allocate_without_global_object<RemainingElements>(1);
+    size_t index = 0;
+
+    while (true) {
+        auto* next = iterator_step(global_object, iterator_record);
+        if (vm.exception()) {
+            set_iterator_record_complete(global_object, iterator_record);
+            return {};
+        }
+
+        if (!next) {
+            set_iterator_record_complete(global_object, iterator_record);
+            if (vm.exception())
+                return {};
+
+            if (--remaining_elements_count->value == 0) {
+                auto values_array = Array::create_from(global_object, values->values);
+                (void)vm.call(*result_capability.resolve, js_undefined(), values_array);
+                if (vm.exception())
+                    return {};
+            }
+
+            return result_capability.promise;
+        }
+
+        auto next_value = iterator_value(global_object, *next);
+        if (vm.exception()) {
+            set_iterator_record_complete(global_object, iterator_record);
+            return {};
+        }
+
+        values->values.append(js_undefined());
+
+        auto next_promise = vm.call(promise_resolve.as_function(), constructor, next_value);
+        if (vm.exception())
+            return {};
+
+        auto* on_fulfilled = PromiseAllResolveElementFunction::create(global_object, index, *values, result_capability, *remaining_elements_count);
+        on_fulfilled->define_direct_property(vm.names.name, js_string(vm, String::empty()), Attribute::Configurable);
+
+        ++remaining_elements_count->value;
+
+        (void)next_promise.invoke(global_object, vm.names.then, on_fulfilled, result_capability.reject);
+        if (vm.exception())
+            return {};
+
+        ++index;
+    }
+}
+
 PromiseConstructor::PromiseConstructor(GlobalObject& global_object)
     : NativeFunction(vm().names.Promise.as_string(), *global_object.function_prototype())
 {
@@ -29,8 +145,8 @@ void PromiseConstructor::initialize(GlobalObject& global_object)
     define_direct_property(vm.names.prototype, global_object.promise_prototype(), 0);
 
     u8 attr = Attribute::Writable | Attribute::Configurable;
+    define_native_function(vm.names.all, all, 1, attr);
     // TODO: Implement these functions below and uncomment this.
-    // define_native_function(vm.names.all, all, 1, attr);
     // define_native_function(vm.names.allSettled, all_settled, 1, attr);
     // define_native_function(vm.names.any, any, 1, attr);
     // define_native_function(vm.names.race, race, 1, attr);
@@ -80,7 +196,32 @@ Value PromiseConstructor::construct(FunctionObject& new_target)
 // 27.2.4.1 Promise.all ( iterable ), https://tc39.es/ecma262/#sec-promise.all
 JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::all)
 {
-    TODO();
+    auto* constructor = vm.this_value(global_object).to_object(global_object);
+    if (!constructor)
+        return {};
+
+    auto promise_capability = new_promise_capability(global_object, constructor);
+    if (vm.exception())
+        return {};
+
+    auto promise_resolve = get_promise_resolve(global_object, constructor);
+    if (auto abrupt = if_abrupt_reject_promise(global_object, promise_resolve, promise_capability); abrupt.has_value())
+        return abrupt.value();
+
+    auto iterator_record = get_iterator(global_object, vm.argument(0));
+    if (auto abrupt = if_abrupt_reject_promise(global_object, iterator_record, promise_capability); abrupt.has_value())
+        return abrupt.value();
+
+    auto result = perform_promise_all(global_object, *iterator_record, constructor, promise_capability, promise_resolve);
+    if (vm.exception()) {
+        if (!iterator_record_is_complete(global_object, *iterator_record))
+            iterator_close(*iterator_record);
+
+        auto abrupt = if_abrupt_reject_promise(global_object, result, promise_capability);
+        return abrupt.value();
+    }
+
+    return result;
 }
 
 // 27.2.4.2 Promise.allSettled ( iterable ), https://tc39.es/ecma262/#sec-promise.allsettled

+ 116 - 0
Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.all.js

@@ -0,0 +1,116 @@
+test("length is 1", () => {
+    expect(Promise.all).toHaveLength(1);
+});
+
+describe("normal behavior", () => {
+    test("returns a Promise", () => {
+        const promise = Promise.all();
+        expect(promise).toBeInstanceOf(Promise);
+    });
+
+    test("resolve", () => {
+        const promise1 = Promise.resolve(3);
+        const promise2 = 42;
+        const promise3 = new Promise((resolve, reject) => {
+            resolve("foo");
+        });
+
+        let resolvedValues = null;
+        let wasRejected = false;
+
+        Promise.all([promise1, promise2, promise3]).then(
+            values => {
+                resolvedValues = values;
+            },
+            () => {
+                wasRejected = true;
+            }
+        );
+
+        runQueuedPromiseJobs();
+        expect(resolvedValues).toEqual([3, 42, "foo"]);
+        expect(wasRejected).toBeFalse();
+    });
+
+    test("reject", () => {
+        const promise1 = Promise.resolve(3);
+        const promise2 = 42;
+        const promise3 = new Promise((resolve, reject) => {
+            reject("foo");
+        });
+
+        let rejectionReason = null;
+        let wasResolved = false;
+
+        Promise.all([promise1, promise2, promise3]).then(
+            () => {
+                wasResolved = true;
+            },
+            reason => {
+                rejectionReason = reason;
+            }
+        );
+
+        runQueuedPromiseJobs();
+        expect(rejectionReason).toBe("foo");
+        expect(wasResolved).toBeFalse();
+    });
+});
+
+describe("exceptional behavior", () => {
+    test("cannot invoke capabilities executor twice", () => {
+        function fn() {}
+
+        expect(() => {
+            function promise(executor) {
+                executor(fn, fn);
+                executor(fn, fn);
+            }
+
+            Promise.all.call(promise, []);
+        }).toThrow(TypeError);
+
+        expect(() => {
+            function promise(executor) {
+                executor(fn, undefined);
+                executor(fn, fn);
+            }
+
+            Promise.all.call(promise, []);
+        }).toThrow(TypeError);
+
+        expect(() => {
+            function promise(executor) {
+                executor(undefined, fn);
+                executor(fn, fn);
+            }
+
+            Promise.all.call(promise, []);
+        }).toThrow(TypeError);
+    });
+
+    test("promise without resolve method", () => {
+        expect(() => {
+            function promise(executor) {}
+            Promise.all.call(promise, []);
+        }).toThrow(TypeError);
+    });
+
+    test("no parameters", () => {
+        let rejectionReason = null;
+        Promise.all().catch(reason => {
+            rejectionReason = reason;
+        });
+        runQueuedPromiseJobs();
+        expect(rejectionReason).toBeInstanceOf(TypeError);
+    });
+
+    test("non-iterable", () => {
+        let rejectionReason = null;
+        Promise.all(1).catch(reason => {
+            rejectionReason = reason;
+        });
+        runQueuedPromiseJobs();
+        expect(rejectionReason).toBeInstanceOf(TypeError);
+    });
+});