From dee3b7b8c9e62e405f856ea7422c5e64949d7b44 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 21 Aug 2021 13:41:32 -0400 Subject: LibJS: Implement Promise.all on the Promise constructor --- Userland/Libraries/LibJS/CMakeLists.txt | 1 + .../Runtime/PromiseAllResolveElementFunction.cpp | 65 +++++++++ .../Runtime/PromiseAllResolveElementFunction.h | 61 +++++++++ .../Libraries/LibJS/Runtime/PromiseConstructor.cpp | 145 ++++++++++++++++++++- .../LibJS/Tests/builtins/Promise/Promise.all.js | 116 +++++++++++++++++ 5 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.all.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index d606f57480..5767412381 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/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 diff --git a/Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.cpp b/Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.cpp new file mode 100644 index 0000000000..7960aa9a45 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS { + +PromiseAllResolveElementFunction* PromiseAllResolveElementFunction::create(GlobalObject& global_object, size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements) +{ + return global_object.heap().allocate(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); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.h b/Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.h new file mode 100644 index 0000000000..910b434af5 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseAllResolveElementFunction.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +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 }; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp index 90531169dd..e275230af0 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp @@ -6,15 +6,131 @@ #include #include +#include #include #include #include +#include #include +#include #include #include +#include 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(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 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(); + auto* remaining_elements_count = vm.heap().allocate_without_global_object(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 diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.all.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.all.js new file mode 100644 index 0000000000..ff18a8dbfa --- /dev/null +++ b/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); + }); +}); -- cgit v1.2.3