diff --git a/AK/Debug.h.in b/AK/Debug.h.in index 0ebc7d29de..d89845cea4 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -310,6 +310,10 @@ #cmakedefine01 PORTABLE_IMAGE_LOADER_DEBUG #endif +#ifndef PROMISE_DEBUG +#cmakedefine01 PROMISE_DEBUG +#endif + #ifndef PTHREAD_DEBUG #cmakedefine01 PTHREAD_DEBUG #endif diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index bcb8559644..f533be05a9 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -144,6 +144,7 @@ set(MENU_DEBUG ON) set(NETWORK_TASK_DEBUG ON) set(OBJECT_DEBUG ON) set(OFFD_DEBUG ON) +set(PROMISE_DEBUG ON) set(PTHREAD_DEBUG ON) set(REACHABLE_DEBUG ON) set(ROUTING_DEBUG ON) diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 5f5329cb5d..8098e2b9bc 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -55,6 +55,12 @@ set(SOURCES Runtime/Object.cpp Runtime/ObjectPrototype.cpp Runtime/PrimitiveString.cpp + Runtime/Promise.cpp + Runtime/PromiseConstructor.cpp + Runtime/PromiseJobs.cpp + Runtime/PromisePrototype.cpp + Runtime/PromiseReaction.cpp + Runtime/PromiseResolvingFunction.cpp Runtime/ProxyConstructor.cpp Runtime/ProxyObject.cpp Runtime/Reference.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 0eb6ce6efd..0e2a4d7d44 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -55,6 +55,7 @@ __JS_ENUMERATE(Function, function, FunctionPrototype, FunctionConstructor, void) \ __JS_ENUMERATE(NumberObject, number, NumberPrototype, NumberConstructor, void) \ __JS_ENUMERATE(Object, object, ObjectPrototype, ObjectConstructor, void) \ + __JS_ENUMERATE(Promise, promise, PromisePrototype, PromiseConstructor, void) \ __JS_ENUMERATE(RegExpObject, regexp, RegExpPrototype, RegExpConstructor, void) \ __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor, void) \ __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor, void) @@ -139,6 +140,9 @@ class MarkedValueList; class NativeFunction; class NativeProperty; class PrimitiveString; +class PromiseReaction; +class PromiseReactionJob; +class PromiseResolveThenableJob; class PropertyName; class Reference; class ScopeNode; @@ -151,6 +155,9 @@ class Uint8ClampedArray; class VM; class Value; enum class DeclarationKind; +struct AlreadyResolved; +struct JobCallback; +struct PromiseCapability; // Not included in JS_ENUMERATE_NATIVE_OBJECTS due to missing distinct prototype class ProxyObject; diff --git a/Userland/Libraries/LibJS/Interpreter.cpp b/Userland/Libraries/LibJS/Interpreter.cpp index 4518a5b41a..cbe28ce249 100644 --- a/Userland/Libraries/LibJS/Interpreter.cpp +++ b/Userland/Libraries/LibJS/Interpreter.cpp @@ -76,8 +76,10 @@ void Interpreter::run(GlobalObject& global_object, const Program& program) program.execute(*this, global_object); vm.pop_call_frame(); - if (vm.last_value().is_empty()) - vm.set_last_value({}, js_undefined()); + // Whatever the promise jobs do should not affect the effective 'last value'. + auto last_value = vm.last_value(); + vm.run_queued_promise_jobs(); + vm.set_last_value({}, last_value.value_or(js_undefined())); } GlobalObject& Interpreter::global_object() diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index e98f66ff04..21ad39f0b4 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -62,6 +62,9 @@ namespace JS { P(abs) \ P(acos) \ P(acosh) \ + P(all) \ + P(allSettled) \ + P(any) \ P(apply) \ P(arguments) \ P(asIntN) \ @@ -108,6 +111,7 @@ namespace JS { P(expm1) \ P(fill) \ P(filter) \ + P(finally) \ P(find) \ P(findIndex) \ P(flags) \ @@ -192,11 +196,14 @@ namespace JS { P(propertyIsEnumerable) \ P(prototype) \ P(push) \ + P(race) \ P(random) \ P(raw) \ P(reduce) \ P(reduceRight) \ + P(reject) \ P(repeat) \ + P(resolve) \ P(reverse) \ P(round) \ P(set) \ @@ -224,6 +231,7 @@ namespace JS { P(tan) \ P(tanh) \ P(test) \ + P(then) \ P(toDateString) \ P(toGMTString) \ P(toISOString) \ @@ -251,6 +259,7 @@ namespace JS { P(writable) struct CommonPropertyNames { + FlyString catch_ { "catch" }; FlyString for_ { "for" }; #define __ENUMERATE(x) FlyString x { #x }; ENUMERATE_STANDARD_PROPERTY_NAMES(__ENUMERATE) diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index cadc118d16..8491890f75 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -45,6 +45,7 @@ M(DescChangeNonConfigurable, "Cannot change attributes of non-configurable property '{}'") \ M(DivisionByZero, "Division by zero") \ M(FunctionArgsNotObject, "Argument array must be an object") \ + M(GetCapabilitiesExecutorCalledMultipleTimes, "GetCapabilitiesExecutor was called multiple times") \ M(InOperatorWithObject, "'in' operator must be used on an object") \ M(InstanceOfOperatorBadPrototype, "'prototype' property of {} is not an object") \ M(InvalidAssignToConst, "Invalid assignment to const variable") \ @@ -79,6 +80,7 @@ M(ObjectPrototypeNullOrUndefinedOnSuperPropertyAccess, \ "Object prototype must not be {} on a super property access") \ M(ObjectPrototypeWrongType, "Prototype must be an object or null") \ + M(PromiseExecutorNotAFunction, "Promise executor must be a function") \ M(ProxyConstructBadReturnType, "Proxy handler's construct trap violates invariant: must return " \ "an object") \ M(ProxyConstructorBadType, "Expected {} argument of Proxy constructor to be object, got {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index 6c2bc8d31a..25a3509716 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -58,6 +58,8 @@ #include #include #include +#include +#include #include #include #include @@ -144,6 +146,7 @@ void GlobalObject::initialize_global_object() add_constructor(vm.names.Function, m_function_constructor, m_function_prototype); add_constructor(vm.names.Number, m_number_constructor, m_number_prototype); add_constructor(vm.names.Object, m_object_constructor, m_object_prototype); + add_constructor(vm.names.Promise, m_promise_constructor, m_promise_prototype); add_constructor(vm.names.Proxy, m_proxy_constructor, nullptr); add_constructor(vm.names.RegExp, m_regexp_constructor, m_regexp_prototype); add_constructor(vm.names.String, m_string_constructor, m_string_prototype); diff --git a/Userland/Libraries/LibJS/Runtime/JobCallback.h b/Userland/Libraries/LibJS/Runtime/JobCallback.h new file mode 100644 index 0000000000..6da857a76a --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/JobCallback.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace JS { + +// 9.4.1 JobCallback Records, https://tc39.es/ecma262/#sec-jobcallback-records +struct JobCallback { + Function* callback; +}; + +// 9.4.2 HostMakeJobCallback, https://tc39.es/ecma262/#sec-hostmakejobcallback +inline JobCallback make_job_callback(Function& callback) +{ + return { &callback }; +} + +// 9.4.3 HostCallJobCallback, https://tc39.es/ecma262/#sec-hostcalljobcallback +template +[[nodiscard]] inline Value call_job_callback(VM& vm, JobCallback& job_callback, Value this_value, Args... args) +{ + VERIFY(job_callback.callback); + auto& callback = *job_callback.callback; + return vm.call(callback, this_value, args...); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Promise.cpp b/Userland/Libraries/LibJS/Runtime/Promise.cpp new file mode 100644 index 0000000000..38fe95b252 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Promise.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +// 27.2.4.7.1 PromiseResolve, https://tc39.es/ecma262/#sec-promise-resolve +Object* promise_resolve(GlobalObject& global_object, Object& constructor, Value value) +{ + auto& vm = global_object.vm(); + if (value.is_object() && is(value.as_object())) { + auto value_constructor = value.as_object().get(vm.names.constructor).value_or(js_undefined()); + if (vm.exception()) + return nullptr; + if (same_value(value_constructor, &constructor)) + return &static_cast(value.as_object()); + } + auto promise_capability = new_promise_capability(global_object, &constructor); + if (vm.exception()) + return nullptr; + [[maybe_unused]] auto result = vm.call(*promise_capability.resolve, js_undefined(), value); + if (vm.exception()) + return nullptr; + return promise_capability.promise; +} + +Promise* Promise::create(GlobalObject& global_object) +{ + return global_object.heap().allocate(global_object, *global_object.promise_prototype()); +} + +Promise::Promise(Object& prototype) + : Object(prototype) +{ +} + +// 27.2.1.3 CreateResolvingFunctions, https://tc39.es/ecma262/#sec-createresolvingfunctions +Promise::ResolvingFunctions Promise::create_resolving_functions() +{ + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / create_resolving_functions()]", this); + auto& vm = this->vm(); + + auto* already_resolved = vm.heap().allocate_without_global_object(); + + // 27.2.1.3.2 Promise Resolve Functions, https://tc39.es/ecma262/#sec-promise-resolve-functions + auto* resolve_function = PromiseResolvingFunction::create(global_object(), *this, *already_resolved, [](auto& vm, auto& global_object, auto& promise, auto& already_resolved) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Resolve function was called", &promise); + if (already_resolved.value) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Promise is already resolved, returning undefined", &promise); + return js_undefined(); + } + already_resolved.value = true; + auto resolution = vm.argument(0).value_or(js_undefined()); + if (resolution.is_object() && &resolution.as_object() == &promise) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Promise can't be resolved with itself, rejecting with error", &promise); + auto* self_resolution_error = TypeError::create(global_object, "Cannot resolve promise with itself"); + return promise.reject(self_resolution_error); + } + if (!resolution.is_object()) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Resolution is not an object, fulfilling with {}", &promise, resolution); + return promise.fulfill(resolution); + } + auto then_action = resolution.as_object().get(vm.names.then); + if (vm.exception()) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Exception while getting 'then' property, rejecting with error", &promise); + auto error = vm.exception()->value(); + vm.clear_exception(); + vm.stop_unwind(); + return promise.reject(error); + } + if (!then_action.is_function()) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Then action is not a function, fulfilling with {}", &promise, resolution); + return promise.fulfill(resolution); + } + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Creating JobCallback for then action @ {}", &promise, &then_action.as_function()); + auto then_job_callback = make_job_callback(then_action.as_function()); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Creating PromiseResolveThenableJob for thenable {}", &promise, resolution); + auto* job = PromiseResolveThenableJob::create(global_object, promise, resolution, then_job_callback); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Enqueuing job @ {}", &promise, job); + vm.enqueue_promise_job(*job); + return js_undefined(); + }); + + // 27.2.1.3.1 Promise Reject Functions, https://tc39.es/ecma262/#sec-promise-reject-functions + auto* reject_function = PromiseResolvingFunction::create(global_object(), *this, *already_resolved, [](auto& vm, auto&, auto& promise, auto& already_resolved) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Reject function was called", &promise); + if (already_resolved.value) + return js_undefined(); + already_resolved.value = true; + auto reason = vm.argument(0).value_or(js_undefined()); + return promise.reject(reason); + }); + + return { *resolve_function, *reject_function }; +} + +// 27.2.1.4 FulfillPromise, https://tc39.es/ecma262/#sec-fulfillpromise +Value Promise::fulfill(Value value) +{ + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / fulfill()]: Fulfilling promise with value {}", this, value); + VERIFY(m_state == State::Pending); + VERIFY(!value.is_empty()); + m_state = State::Fulfilled; + m_result = value; + trigger_reactions(); + m_fulfill_reactions.clear(); + m_reject_reactions.clear(); + return js_undefined(); +} + +// 27.2.1.7 RejectPromise, https://tc39.es/ecma262/#sec-rejectpromise +Value Promise::reject(Value reason) +{ + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / reject()]: Rejecting promise with reason {}", this, reason); + VERIFY(m_state == State::Pending); + VERIFY(!reason.is_empty()); + auto& vm = this->vm(); + m_state = State::Rejected; + m_result = reason; + if (!m_is_handled) + vm.promise_rejection_tracker(*this, RejectionOperation::Reject); + trigger_reactions(); + m_fulfill_reactions.clear(); + m_reject_reactions.clear(); + return js_undefined(); +} + +// 27.2.1.8 TriggerPromiseReactions, https://tc39.es/ecma262/#sec-triggerpromisereactions +void Promise::trigger_reactions() const +{ + VERIFY(is_settled()); + auto& vm = this->vm(); + auto& reactions = m_state == State::Fulfilled + ? m_fulfill_reactions + : m_reject_reactions; + for (auto& reaction : reactions) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / trigger_reactions()]: Creating PromiseReactionJob for PromiseReaction @ {} with argument {}", this, &reaction, m_result); + auto* job = PromiseReactionJob::create(global_object(), *reaction, m_result); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / trigger_reactions()]: Enqueuing job @ {}", this, job); + vm.enqueue_promise_job(*job); + } + if constexpr (PROMISE_DEBUG) { + if (reactions.is_empty()) + dbgln("[Promise @ {} / trigger_reactions()]: No reactions!", this); + } +} + +// 27.2.5.4.1 PerformPromiseThen, https://tc39.es/ecma262/#sec-performpromisethen +Value Promise::perform_then(Value on_fulfilled, Value on_rejected, Optional result_capability) +{ + auto& vm = this->vm(); + + Optional on_fulfilled_job_callback; + if (on_fulfilled.is_function()) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Creating JobCallback for on_fulfilled function @ {}", this, &on_fulfilled.as_function()); + on_fulfilled_job_callback = make_job_callback(on_fulfilled.as_function()); + } + + Optional on_rejected_job_callback; + if (on_rejected.is_function()) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Creating JobCallback for on_rejected function @ {}", this, &on_rejected.as_function()); + on_rejected_job_callback = make_job_callback(on_rejected.as_function()); + } + + auto* fulfill_reaction = PromiseReaction::create(vm, PromiseReaction::Type::Fulfill, result_capability, on_fulfilled_job_callback); + auto* reject_reaction = PromiseReaction::create(vm, PromiseReaction::Type::Reject, result_capability, on_rejected_job_callback); + + switch (m_state) { + case Promise::State::Pending: + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: state is State::Pending, adding fulfill/reject reactions", this); + m_fulfill_reactions.append(fulfill_reaction); + m_reject_reactions.append(reject_reaction); + break; + case Promise::State::Fulfilled: { + auto value = m_result; + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: State is State::Fulfilled, creating PromiseReactionJob for PromiseReaction @ {} with argument {}", this, fulfill_reaction, value); + auto* fulfill_job = PromiseReactionJob::create(global_object(), *fulfill_reaction, value); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Enqueuing job @ {}", this, fulfill_job); + vm.enqueue_promise_job(*fulfill_job); + break; + } + case Promise::State::Rejected: { + auto reason = m_result; + if (!m_is_handled) + vm.promise_rejection_tracker(*this, RejectionOperation::Handle); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: State is State::Rejected, creating PromiseReactionJob for PromiseReaction @ {} with argument {}", this, reject_reaction, reason); + auto* reject_job = PromiseReactionJob::create(global_object(), *reject_reaction, reason); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Enqueuing job @ {}", this, reject_job); + vm.enqueue_promise_job(*reject_job); + break; + } + default: + VERIFY_NOT_REACHED(); + } + + m_is_handled = true; + + if (!result_capability.has_value()) { + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: No ResultCapability, returning undefined", this); + return js_undefined(); + } + auto* promise = result_capability.value().promise; + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Returning Promise @ {} from ResultCapability @ {}", this, promise, &result_capability.value()); + return promise; +} + +void Promise::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_result); + for (auto& reaction : m_fulfill_reactions) + visitor.visit(reaction); + for (auto& reaction : m_reject_reactions) + visitor.visit(reaction); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Promise.h b/Userland/Libraries/LibJS/Runtime/Promise.h new file mode 100644 index 0000000000..a38e2aa4ef --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Promise.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace JS { + +Object* promise_resolve(GlobalObject&, Object& constructor, Value); + +class Promise final : public Object { + JS_OBJECT(Promise, Object); + +public: + enum class State { + Pending, + Fulfilled, + Rejected, + }; + enum class RejectionOperation { + Reject, + Handle, + }; + + static Promise* create(GlobalObject&); + + explicit Promise(Object& prototype); + virtual ~Promise() = default; + + State state() const { return m_state; } + Value result() const { return m_result; } + + struct ResolvingFunctions { + Function& resolve; + Function& reject; + }; + ResolvingFunctions create_resolving_functions(); + + Value fulfill(Value value); + Value reject(Value reason); + Value perform_then(Value on_fulfilled, Value on_rejected, Optional result_capability); + +private: + virtual void visit_edges(Visitor&) override; + + bool is_settled() const { return m_state == State::Fulfilled || m_state == State::Rejected; } + + void trigger_reactions() const; + + State m_state { State::Pending }; + Value m_result; + Vector m_fulfill_reactions; + Vector m_reject_reactions; + bool m_is_handled { false }; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp new file mode 100644 index 0000000000..c48ccf9bba --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +PromiseConstructor::PromiseConstructor(GlobalObject& global_object) + : NativeFunction(vm().names.Promise, *global_object.function_prototype()) +{ +} + +void PromiseConstructor::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + NativeFunction::initialize(global_object); + + define_property(vm.names.prototype, global_object.promise_prototype()); + define_property(vm.names.length, Value(1)); + + u8 attr = Attribute::Writable | Attribute::Configurable; + 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); + define_native_function(vm.names.reject, reject, 1, attr); + define_native_function(vm.names.resolve, resolve, 1, attr); +} + +Value PromiseConstructor::call() +{ + auto& vm = this->vm(); + vm.throw_exception(global_object(), ErrorType::ConstructorWithoutNew, vm.names.Promise); + return {}; +} + +// 27.2.3.1 Promise, https://tc39.es/ecma262/#sec-promise-executor +Value PromiseConstructor::construct(Function&) +{ + auto& vm = this->vm(); + auto executor = vm.argument(0); + if (!executor.is_function()) { + vm.throw_exception(global_object(), ErrorType::PromiseExecutorNotAFunction); + return {}; + } + auto* promise = Promise::create(global_object()); + auto [resolve_function, reject_function] = promise->create_resolving_functions(); + + auto completion_value = vm.call(executor.as_function(), js_undefined(), &resolve_function, &reject_function); + if (vm.exception()) { + vm.clear_exception(); + vm.stop_unwind(); + [[maybe_unused]] auto result = vm.call(reject_function, js_undefined(), completion_value); + } + return promise; +} + +// 27.2.4.1 Promise.all, https://tc39.es/ecma262/#sec-promise.all +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::all) +{ + TODO(); +} + +// 27.2.4.2 Promise.allSettled, https://tc39.es/ecma262/#sec-promise.allsettled +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::all_settled) +{ + TODO(); +} + +// 27.2.4.3 Promise.any, https://tc39.es/ecma262/#sec-promise.any +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::any) +{ + TODO(); +} + +// 27.2.4.5 Promise.race, https://tc39.es/ecma262/#sec-promise.race +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::race) +{ + TODO(); +} + +// 27.2.4.6 Promise.reject, https://tc39.es/ecma262/#sec-promise.reject +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::reject) +{ + 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 reason = vm.argument(0); + [[maybe_unused]] auto result = vm.call(*promise_capability.reject, js_undefined(), reason); + return promise_capability.promise; +} + +// 27.2.4.7 Promise.resolve, https://tc39.es/ecma262/#sec-promise.resolve +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::resolve) +{ + auto* constructor = vm.this_value(global_object).to_object(global_object); + if (!constructor) + return {}; + auto value = vm.argument(0); + return promise_resolve(global_object, *constructor, value); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h new file mode 100644 index 0000000000..fe4851804a --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace JS { + +class PromiseConstructor final : public NativeFunction { + JS_OBJECT(PromiseConstructor, NativeFunction); + +public: + explicit PromiseConstructor(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~PromiseConstructor() override = default; + + virtual Value call() override; + virtual Value construct(Function& new_target) override; + +private: + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(all); + JS_DECLARE_NATIVE_FUNCTION(all_settled); + JS_DECLARE_NATIVE_FUNCTION(any); + JS_DECLARE_NATIVE_FUNCTION(race); + JS_DECLARE_NATIVE_FUNCTION(reject); + JS_DECLARE_NATIVE_FUNCTION(resolve); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseJobs.cpp b/Userland/Libraries/LibJS/Runtime/PromiseJobs.cpp new file mode 100644 index 0000000000..c0216e230f --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseJobs.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include + +namespace JS { + +PromiseReactionJob* PromiseReactionJob::create(GlobalObject& global_object, PromiseReaction& reaction, Value argument) +{ + return global_object.heap().allocate(global_object, reaction, argument, *global_object.function_prototype()); +} + +PromiseReactionJob::PromiseReactionJob(PromiseReaction& reaction, Value argument, Object& prototype) + : NativeFunction(prototype) + , m_reaction(reaction) + , m_argument(argument) +{ +} + +// 27.2.2.1 NewPromiseReactionJob, https://tc39.es/ecma262/#sec-newpromisereactionjob +Value PromiseReactionJob::call() +{ + auto& vm = this->vm(); + auto& promise_capability = m_reaction.capability(); + auto type = m_reaction.type(); + auto handler = m_reaction.handler(); + Value handler_result; + if (!handler.has_value()) { + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Handler is empty", this); + switch (type) { + case PromiseReaction::Type::Fulfill: + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction type is Type::Fulfill, setting handler result to {}", this, m_argument); + handler_result = m_argument; + break; + case PromiseReaction::Type::Reject: + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction type is Type::Reject, throwing exception with argument {}", this, m_argument); + vm.throw_exception(global_object(), m_argument); + // handler_result is set to exception value further below + break; + } + } else { + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling handler callback {} @ {} with argument {}", this, handler.value().callback->class_name(), handler.value().callback, m_argument); + handler_result = call_job_callback(vm, handler.value(), js_undefined(), m_argument); + } + + if (!promise_capability.has_value()) { + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction has no PromiseCapability, returning empty value", this); + return {}; + } + + if (vm.exception()) { + handler_result = vm.exception()->value(); + vm.clear_exception(); + vm.stop_unwind(); + auto* reject_function = promise_capability.value().reject; + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling PromiseCapability's reject function @ {}", this, reject_function); + return vm.call(*reject_function, js_undefined(), handler_result); + } else { + auto* resolve_function = promise_capability.value().resolve; + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling PromiseCapability's resolve function @ {}", this, resolve_function); + return vm.call(*resolve_function, js_undefined(), handler_result); + } +} + +void PromiseReactionJob::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(&m_reaction); + visitor.visit(m_argument); +} + +PromiseResolveThenableJob* PromiseResolveThenableJob::create(GlobalObject& global_object, Promise& promise_to_resolve, Value thenable, JobCallback then) +{ + // FIXME: A bunch of stuff regarding realms, see step 2-5 in the spec linked below + return global_object.heap().allocate(global_object, promise_to_resolve, thenable, then, *global_object.function_prototype()); +} + +PromiseResolveThenableJob::PromiseResolveThenableJob(Promise& promise_to_resolve, Value thenable, JobCallback then, Object& prototype) + : NativeFunction(prototype) + , m_promise_to_resolve(promise_to_resolve) + , m_thenable(thenable) + , m_then(then) +{ +} + +// 27.2.2.2 NewPromiseResolveThenableJob, https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob +Value PromiseResolveThenableJob::call() +{ + auto& vm = this->vm(); + auto [resolve_function, reject_function] = m_promise_to_resolve.create_resolving_functions(); + dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: Calling then job callback for thenable {}", this, &m_thenable); + auto then_call_result = call_job_callback(vm, m_then, m_thenable, &resolve_function, &reject_function); + if (vm.exception()) { + auto error = vm.exception()->value(); + vm.clear_exception(); + vm.stop_unwind(); + dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: An exception was thrown, returning error {}", this, error); + return error; + } + dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: Returning then call result {}", this, then_call_result); + return then_call_result; +} + +void PromiseResolveThenableJob::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(&m_promise_to_resolve); + visitor.visit(m_thenable); + visitor.visit(m_then.callback); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseJobs.h b/Userland/Libraries/LibJS/Runtime/PromiseJobs.h new file mode 100644 index 0000000000..b71008ea60 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseJobs.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS { + +class PromiseReactionJob final : public NativeFunction { + JS_OBJECT(PromiseReactionJob, NativeFunction); + +public: + static PromiseReactionJob* create(GlobalObject&, PromiseReaction&, Value argument); + + explicit PromiseReactionJob(PromiseReaction&, Value argument, Object& prototype); + virtual ~PromiseReactionJob() override = default; + + virtual Value call() override; + +private: + virtual void visit_edges(Visitor&) override; + + PromiseReaction& m_reaction; + Value m_argument; +}; + +class PromiseResolveThenableJob final : public NativeFunction { + JS_OBJECT(PromiseReactionJob, NativeFunction); + +public: + static PromiseResolveThenableJob* create(GlobalObject&, Promise&, Value thenable, JobCallback then); + + explicit PromiseResolveThenableJob(Promise&, Value thenable, JobCallback then, Object& prototype); + virtual ~PromiseResolveThenableJob() override = default; + + virtual Value call() override; + +private: + virtual void visit_edges(Visitor&) override; + + Promise& m_promise_to_resolve; + Value m_thenable; + JobCallback m_then; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromisePrototype.cpp b/Userland/Libraries/LibJS/Runtime/PromisePrototype.cpp new file mode 100644 index 0000000000..f83ec05015 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromisePrototype.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +PromisePrototype::PromisePrototype(GlobalObject& global_object) + : Object(*global_object.object_prototype()) +{ +} + +void PromisePrototype::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + Object::initialize(global_object); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.then, then, 2, attr); + define_native_function(vm.names.catch_, catch_, 1, attr); + define_native_function(vm.names.finally, finally, 1, attr); +} + +static Promise* promise_from(VM& vm, GlobalObject& global_object) +{ + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) + return nullptr; + if (!is(this_object)) { + vm.throw_exception(global_object, ErrorType::NotA, vm.names.Promise); + return nullptr; + } + return static_cast(this_object); +} + +// 27.2.5.4 Promise.prototype.then, https://tc39.es/ecma262/#sec-promise.prototype.then +JS_DEFINE_NATIVE_FUNCTION(PromisePrototype::then) +{ + auto* promise = promise_from(vm, global_object); + if (!promise) + return {}; + auto on_fulfilled = vm.argument(0); + auto on_rejected = vm.argument(1); + auto* constructor = species_constructor(global_object, *promise, *global_object.promise_constructor()); + if (vm.exception()) + return {}; + auto result_capability = new_promise_capability(global_object, constructor); + if (vm.exception()) + return {}; + return promise->perform_then(on_fulfilled, on_rejected, result_capability); +} + +// 27.2.5.1 Promise.prototype.catch, https://tc39.es/ecma262/#sec-promise.prototype.catch +JS_DEFINE_NATIVE_FUNCTION(PromisePrototype::catch_) +{ + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) + return {}; + auto on_rejected = vm.argument(0); + return this_object->invoke(vm.names.then, js_undefined(), on_rejected); +} + +// 27.2.5.3 Promise.prototype.finally, https://tc39.es/ecma262/#sec-promise.prototype.finally +JS_DEFINE_NATIVE_FUNCTION(PromisePrototype::finally) +{ + auto* promise = vm.this_value(global_object).to_object(global_object); + if (!promise) + return {}; + auto* constructor = species_constructor(global_object, *promise, *global_object.promise_constructor()); + if (vm.exception()) + return {}; + Value then_finally; + Value catch_finally; + auto on_finally = vm.argument(0); + if (!on_finally.is_function()) { + then_finally = on_finally; + catch_finally = on_finally; + } else { + // 27.2.5.3.1 Then Finally Functions, https://tc39.es/ecma262/#sec-thenfinallyfunctions + auto* then_finally_function = NativeFunction::create(global_object, "", [constructor_handle = make_handle(constructor), on_finally_handle = make_handle(&on_finally.as_function())](auto& vm, auto& global_object) -> Value { + auto& constructor = const_cast(*constructor_handle.cell()); + auto& on_finally = const_cast(*on_finally_handle.cell()); + auto value = vm.argument(0); + auto result = vm.call(on_finally, js_undefined()); + if (vm.exception()) + return {}; + auto* promise = promise_resolve(global_object, constructor, result); + if (vm.exception()) + return {}; + auto* value_thunk = NativeFunction::create(global_object, "", [value](auto&, auto&) -> Value { + return value; + }); + return promise->invoke(vm.names.then, value_thunk); + }); + then_finally_function->define_property(vm.names.length, Value(1)); + + // 27.2.5.3.2 Catch Finally Functions, https://tc39.es/ecma262/#sec-catchfinallyfunctions + auto* catch_finally_function = NativeFunction::create(global_object, "", [constructor_handle = make_handle(constructor), on_finally_handle = make_handle(&on_finally.as_function())](auto& vm, auto& global_object) -> Value { + auto& constructor = const_cast(*constructor_handle.cell()); + auto& on_finally = const_cast(*on_finally_handle.cell()); + auto reason = vm.argument(0); + auto result = vm.call(on_finally, js_undefined()); + if (vm.exception()) + return {}; + auto* promise = promise_resolve(global_object, constructor, result); + if (vm.exception()) + return {}; + auto* thrower = NativeFunction::create(global_object, "", [reason](auto& vm, auto& global_object) -> Value { + vm.throw_exception(global_object, reason); + return {}; + }); + return promise->invoke(vm.names.then, thrower); + }); + catch_finally_function->define_property(vm.names.length, Value(1)); + + then_finally = Value(then_finally_function); + catch_finally = Value(catch_finally_function); + } + return promise->invoke(vm.names.then, then_finally, catch_finally); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromisePrototype.h b/Userland/Libraries/LibJS/Runtime/PromisePrototype.h new file mode 100644 index 0000000000..6a09c28e9b --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromisePrototype.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace JS { + +class PromisePrototype final : public Object { + JS_OBJECT(PromisePrototype, Object); + +public: + PromisePrototype(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~PromisePrototype() override = default; + +private: + JS_DECLARE_NATIVE_FUNCTION(then); + JS_DECLARE_NATIVE_FUNCTION(catch_); + JS_DECLARE_NATIVE_FUNCTION(finally); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp b/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp new file mode 100644 index 0000000000..53e61432e7 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +namespace JS { + +// 27.2.1.5 NewPromiseCapability, https://tc39.es/ecma262/#sec-newpromisecapability +PromiseCapability new_promise_capability(GlobalObject& global_object, Value constructor) +{ + auto& vm = global_object.vm(); + if (!constructor.is_constructor()) { + vm.throw_exception(global_object, ErrorType::NotAConstructor, constructor.to_string_without_side_effects()); + return {}; + } + + struct { + Value resolve { js_undefined() }; + Value reject { js_undefined() }; + } promise_capability_functions; + + auto* executor = NativeFunction::create(global_object, "", [&promise_capability_functions](auto& vm, auto& global_object) -> Value { + auto resolve = vm.argument(0); + auto reject = vm.argument(1); + // No idea what other engines say here. + if (!promise_capability_functions.resolve.is_undefined()) { + vm.template throw_exception(global_object, ErrorType::GetCapabilitiesExecutorCalledMultipleTimes); + return {}; + } + if (!promise_capability_functions.resolve.is_undefined()) { + vm.template throw_exception(global_object, ErrorType::GetCapabilitiesExecutorCalledMultipleTimes); + return {}; + } + promise_capability_functions.resolve = resolve; + promise_capability_functions.reject = reject; + return js_undefined(); + }); + executor->define_property(vm.names.length, Value(2)); + + MarkedValueList arguments(vm.heap()); + arguments.append(executor); + auto promise = vm.construct(constructor.as_function(), constructor.as_function(), move(arguments), global_object); + if (vm.exception()) + return {}; + + // I'm not sure if we could VERIFY(promise.is_object()) instead - the spec doesn't have this check... + if (!promise.is_object()) { + vm.throw_exception(global_object, ErrorType::NotAnObject, promise.to_string_without_side_effects()); + return {}; + } + + if (!promise_capability_functions.resolve.is_function()) { + vm.throw_exception(global_object, ErrorType::NotAFunction, promise_capability_functions.resolve.to_string_without_side_effects()); + return {}; + } + if (!promise_capability_functions.reject.is_function()) { + vm.throw_exception(global_object, ErrorType::NotAFunction, promise_capability_functions.reject.to_string_without_side_effects()); + return {}; + } + + return { + &promise.as_object(), + &promise_capability_functions.resolve.as_function(), + &promise_capability_functions.reject.as_function(), + }; +} + +PromiseReaction::PromiseReaction(Type type, Optional capability, Optional handler) + : m_type(type) + , m_capability(move(capability)) + , m_handler(move(handler)) +{ +} + +void PromiseReaction::visit_edges(Cell::Visitor& visitor) +{ + Cell::visit_edges(visitor); + if (m_capability.has_value()) { + auto& capability = m_capability.value(); + visitor.visit(capability.promise); + visitor.visit(capability.resolve); + visitor.visit(capability.reject); + } + if (m_handler.has_value()) { + auto& handler = m_handler.value(); + visitor.visit(handler.callback); + } +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseReaction.h b/Userland/Libraries/LibJS/Runtime/PromiseReaction.h new file mode 100644 index 0000000000..c2d9d79d98 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseReaction.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include + +namespace JS { + +// 27.2.1.1 PromiseCapability Records, https://tc39.es/ecma262/#sec-promisecapability-records +struct PromiseCapability { + Object* promise; + Function* resolve; + Function* reject; +}; + +// 27.2.1.5 NewPromiseCapability, https://tc39.es/ecma262/#sec-newpromisecapability +PromiseCapability new_promise_capability(GlobalObject& global_object, Value constructor); + +// https://tc39.es/ecma262/#sec-promisereaction-records +class PromiseReaction final : public Cell { +public: + enum class Type { + Fulfill, + Reject, + }; + + static PromiseReaction* create(VM& vm, Type type, Optional capability, Optional handler) + { + return vm.heap().allocate_without_global_object(type, capability, handler); + } + + PromiseReaction(Type type, Optional capability, Optional handler); + virtual ~PromiseReaction() = default; + + Type type() const { return m_type; } + const Optional& capability() const { return m_capability; } + const Optional& handler() const { return m_handler; } + +private: + virtual const char* class_name() const override { return "PromiseReaction"; } + virtual void visit_edges(Visitor&) override; + + Type m_type; + Optional m_capability; + Optional m_handler; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseResolvingFunction.cpp b/Userland/Libraries/LibJS/Runtime/PromiseResolvingFunction.cpp new file mode 100644 index 0000000000..a3a442b583 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseResolvingFunction.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +namespace JS { + +PromiseResolvingFunction* PromiseResolvingFunction::create(GlobalObject& global_object, Promise& promise, AlreadyResolved& already_resolved, FunctionType function) +{ + return global_object.heap().allocate(global_object, promise, already_resolved, move(function), *global_object.function_prototype()); +} + +PromiseResolvingFunction::PromiseResolvingFunction(Promise& promise, AlreadyResolved& already_resolved, FunctionType native_function, Object& prototype) + : NativeFunction(prototype) + , m_promise(promise) + , m_already_resolved(already_resolved) + , m_native_function(move(native_function)) +{ +} + +void PromiseResolvingFunction::initialize(GlobalObject& global_object) +{ + Base::initialize(global_object); + define_property(vm().names.length, Value(1)); +} + +Value PromiseResolvingFunction::call() +{ + return m_native_function(vm(), global_object(), m_promise, m_already_resolved); +} + +void PromiseResolvingFunction::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(&m_promise); + visitor.visit(&m_already_resolved); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/PromiseResolvingFunction.h b/Userland/Libraries/LibJS/Runtime/PromiseResolvingFunction.h new file mode 100644 index 0000000000..1e82015f67 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/PromiseResolvingFunction.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace JS { + +struct AlreadyResolved final : public Cell { + bool value { false }; + + virtual const char* class_name() const override { return "AlreadyResolved"; } + +protected: + // Allocated cells must be >= sizeof(FreelistEntry), which is 24 bytes - + // but AlreadyResolved is only 16 bytes without this. + u8 dummy[8]; +}; + +class PromiseResolvingFunction final : public NativeFunction { + JS_OBJECT(PromiseResolvingFunction, NativeFunction); + +public: + using FunctionType = AK::Function; + + static PromiseResolvingFunction* create(GlobalObject&, Promise&, AlreadyResolved&, FunctionType); + + explicit PromiseResolvingFunction(Promise&, AlreadyResolved&, FunctionType, Object& prototype); + virtual void initialize(GlobalObject&) override; + virtual ~PromiseResolvingFunction() override = default; + + virtual Value call() override; + +private: + virtual void visit_edges(Visitor&) override; + + Promise& m_promise; + AlreadyResolved& m_already_resolved; + FunctionType m_native_function; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index dc7a43ab75..6d52861bc9 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2020-2021, Linus Groh * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,12 +25,16 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -128,6 +133,9 @@ void VM::gather_roots(HashTable& roots) for (auto& symbol : m_global_symbol_map) roots.set(symbol.value); + + for (auto* job : m_promise_jobs) + roots.set(job); } Symbol* VM::get_global_symbol(const String& description) @@ -371,4 +379,44 @@ bool VM::in_strict_mode() const return call_frame().is_strict_mode; } +void VM::run_queued_promise_jobs() +{ + dbgln_if(PROMISE_DEBUG, "Running queued promise jobs"); + // Temporarily get rid of the exception, if any - job functions must be called + // either way, and that can't happen if we already have an exception stored. + TemporaryChange change(m_exception, static_cast(nullptr)); + while (!m_promise_jobs.is_empty()) { + auto* job = m_promise_jobs.take_first(); + dbgln_if(PROMISE_DEBUG, "Calling promise job function @ {}", job); + [[maybe_unused]] auto result = call(*job, js_undefined()); + } + // Ensure no job has created a new exception, they must clean up after themselves. + VERIFY(!m_exception); +} + +// 9.4.4 HostEnqueuePromiseJob, https://tc39.es/ecma262/#sec-hostenqueuepromisejob +void VM::enqueue_promise_job(NativeFunction& job) +{ + m_promise_jobs.append(&job); +} + +// 27.2.1.9 HostPromiseRejectionTracker, https://tc39.es/ecma262/#sec-host-promise-rejection-tracker +void VM::promise_rejection_tracker(const Promise& promise, Promise::RejectionOperation operation) const +{ + switch (operation) { + case Promise::RejectionOperation::Reject: + // A promise was rejected without any handlers + if (on_promise_unhandled_rejection) + on_promise_unhandled_rejection(promise); + break; + case Promise::RejectionOperation::Handle: + // A handler was added to an already rejected promise + if (on_promise_rejection_handled) + on_promise_rejection_handled(promise); + break; + default: + VERIFY_NOT_REACHED(); + } +} + } diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index ad8fc88db9..1988e20afa 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2020-2021, Linus Groh * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,6 +28,7 @@ #pragma once #include +#include #include #include #include @@ -36,6 +38,7 @@ #include #include #include +#include #include namespace JS { @@ -240,6 +243,14 @@ public: Shape& scope_object_shape() { return *m_scope_object_shape; } + void run_queued_promise_jobs(); + void enqueue_promise_job(NativeFunction&); + + void promise_rejection_tracker(const Promise&, Promise::RejectionOperation) const; + + AK::Function on_promise_unhandled_rejection; + AK::Function on_promise_rejection_handled; + private: VM(); @@ -258,10 +269,10 @@ private: StackInfo m_stack_info; - bool m_underscore_is_last_value { false }; - HashMap m_global_symbol_map; + Vector m_promise_jobs; + PrimitiveString* m_empty_string { nullptr }; PrimitiveString* m_single_ascii_character_strings[128] {}; @@ -272,6 +283,7 @@ private: Shape* m_scope_object_shape { nullptr }; + bool m_underscore_is_last_value { false }; bool m_should_log_exceptions { false }; }; diff --git a/Userland/Libraries/LibJS/Runtime/Value.cpp b/Userland/Libraries/LibJS/Runtime/Value.cpp index 873a456385..be8375115b 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.cpp +++ b/Userland/Libraries/LibJS/Runtime/Value.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -237,10 +238,20 @@ Function& Value::as_function() return static_cast(as_object()); } +// 7.2.4 IsConstructor, https://tc39.es/ecma262/#sec-isconstructor +bool Value::is_constructor() const +{ + if (!is_function()) + return false; + if (is(as_object())) + return static_cast(as_object()).has_constructor(); + // ScriptFunction or BoundFunction + return true; +} + +// 7.2.8 IsRegExp, https://tc39.es/ecma262/#sec-isregexp bool Value::is_regexp(GlobalObject& global_object) const { - // 7.2.8 IsRegExp, https://tc39.es/ecma262/#sec-isregexp - if (!is_object()) return false; @@ -1303,10 +1314,9 @@ TriState abstract_relation(GlobalObject& global_object, bool left_first, Value l return TriState::False; } +// 7.3.10 GetMethod, https://tc39.es/ecma262/#sec-getmethod Function* get_method(GlobalObject& global_object, Value value, const PropertyName& property_name) { - // 7.3.10 GetMethod, https://tc39.es/ecma262/#sec-getmethod - auto& vm = global_object.vm(); auto* object = value.to_object(global_object); if (vm.exception()) @@ -1323,10 +1333,9 @@ Function* get_method(GlobalObject& global_object, Value value, const PropertyNam return &property_value.as_function(); } +// 7.3.18 LengthOfArrayLike, https://tc39.es/ecma262/#sec-lengthofarraylike size_t length_of_array_like(GlobalObject& global_object, const Object& object) { - // 7.3.18 LengthOfArrayLike, https://tc39.es/ecma262/#sec-lengthofarraylike - auto& vm = global_object.vm(); auto result = object.get(vm.names.length).value_or(js_undefined()); if (vm.exception()) @@ -1334,4 +1343,26 @@ size_t length_of_array_like(GlobalObject& global_object, const Object& object) return result.to_length(global_object); } +// 7.3.22 SpeciesConstructor, https://tc39.es/ecma262/#sec-speciesconstructor +Object* species_constructor(GlobalObject& global_object, const Object& object, Object& default_constructor) +{ + auto& vm = global_object.vm(); + auto constructor = object.get(vm.names.constructor).value_or(js_undefined()); + if (vm.exception()) + return nullptr; + if (constructor.is_undefined()) + return &default_constructor; + if (!constructor.is_object()) { + vm.throw_exception(global_object, ErrorType::NotAConstructor, constructor.to_string_without_side_effects()); + return nullptr; + } + auto species = constructor.as_object().get(vm.well_known_symbol_species()).value_or(js_undefined()); + if (species.is_nullish()) + return &default_constructor; + if (species.is_constructor()) + return &species.as_object(); + vm.throw_exception(global_object, ErrorType::NotAConstructor, species.to_string_without_side_effects()); + return nullptr; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Value.h b/Userland/Libraries/LibJS/Runtime/Value.h index e763cfa0bf..4b8d42431e 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.h +++ b/Userland/Libraries/LibJS/Runtime/Value.h @@ -82,6 +82,7 @@ public: bool is_cell() const { return is_string() || is_accessor() || is_object() || is_bigint() || is_symbol() || is_native_property(); } bool is_array() const; bool is_function() const; + bool is_constructor() const; bool is_regexp(GlobalObject& global_object) const; bool is_nan() const { return is_number() && __builtin_isnan(as_double()); } @@ -377,6 +378,7 @@ bool same_value_non_numeric(Value lhs, Value rhs); TriState abstract_relation(GlobalObject&, bool left_first, Value lhs, Value rhs); Function* get_method(GlobalObject& global_object, Value, const PropertyName&); size_t length_of_array_like(GlobalObject&, const Object&); +Object* species_constructor(GlobalObject&, const Object&, Object& default_constructor); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.js new file mode 100644 index 0000000000..7aa0bed748 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.js @@ -0,0 +1,37 @@ +test("length is 1", () => { + expect(Promise).toHaveLength(1); +}); + +describe("errors", () => { + test("must be called as constructor", () => { + expect(() => { + Promise(); + }).toThrowWithMessage(TypeError, "Promise constructor must be called with 'new'"); + }); + + test("executor must be a function", () => { + expect(() => { + new Promise(); + }).toThrowWithMessage(TypeError, "Promise executor must be a function"); + }); +}); + +describe("normal behavior", () => { + test("returns a Promise object", () => { + const promise = new Promise(() => {}); + expect(promise).toBeInstanceOf(Promise); + expect(typeof promise).toBe("object"); + }); + + test("executor is called with resolve and reject functions", () => { + let resolveFunction = null; + let rejectFunction = null; + new Promise((resolve, reject) => { + resolveFunction = resolve; + rejectFunction = reject; + }); + expect(typeof resolveFunction).toBe("function"); + expect(typeof rejectFunction).toBe("function"); + expect(resolveFunction).not.toBe(rejectFunction); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.catch.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.catch.js new file mode 100644 index 0000000000..02a12b26e1 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.catch.js @@ -0,0 +1,55 @@ +test("length is 1", () => { + expect(Promise.prototype.catch).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns a Promise object different from the initial Promise", () => { + const initialPromise = new Promise(() => {}); + const catchPromise = initialPromise.catch(); + expect(catchPromise).toBeInstanceOf(Promise); + expect(initialPromise).not.toBe(catchPromise); + }); + + test("catch() onRejected handler is called when Promise is rejected", () => { + let rejectPromise = null; + let rejectionReason = null; + new Promise((_, reject) => { + rejectPromise = reject; + }).catch(reason => { + rejectionReason = reason; + }); + rejectPromise("Some reason"); + runQueuedPromiseJobs(); + expect(rejectionReason).toBe("Some reason"); + }); + + test("returned Promise is rejected with undefined if handler is missing", () => { + let rejectPromise = null; + let rejectionReason = null; + new Promise((_, reject) => { + rejectPromise = reject; + }) + .catch() + .catch(reason => { + rejectionReason = reason; + }); + rejectPromise(); + runQueuedPromiseJobs(); + expect(rejectionReason).toBeUndefined(); + }); + + test("works with any object", () => { + let onFulfilledArg = null; + let onRejectedArg = null; + const onRejected = () => {}; + const thenable = { + then: (onFulfilled, onRejected) => { + onFulfilledArg = onFulfilled; + onRejectedArg = onRejected; + }, + }; + Promise.prototype.catch.call(thenable, onRejected); + expect(onFulfilledArg).toBeUndefined(); + expect(onRejectedArg).toBe(onRejected); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.finally.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.finally.js new file mode 100644 index 0000000000..cdb7a09029 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.finally.js @@ -0,0 +1,54 @@ +test("length is 1", () => { + expect(Promise.prototype.finally).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns a Promise object different from the initial Promise", () => { + const initialPromise = new Promise(() => {}); + const finallyPromise = initialPromise.finally(); + expect(finallyPromise).toBeInstanceOf(Promise); + expect(initialPromise).not.toBe(finallyPromise); + }); + + test("finally() onFinally handler is called when Promise is resolved", () => { + let resolvePromise = null; + let finallyWasCalled = false; + new Promise(resolve => { + resolvePromise = resolve; + }).finally(() => { + finallyWasCalled = true; + }); + resolvePromise(); + runQueuedPromiseJobs(); + expect(finallyWasCalled).toBeTrue(); + }); + + test("finally() onFinally handler is called when Promise is rejected", () => { + let rejectPromise = null; + let finallyWasCalled = false; + new Promise((_, reject) => { + rejectPromise = reject; + }).finally(() => { + finallyWasCalled = true; + }); + rejectPromise(); + runQueuedPromiseJobs(); + expect(finallyWasCalled).toBeTrue(); + }); + + test("works with any object", () => { + let thenFinallyArg = null; + let catchFinallyArg = null; + const onFinally = () => {}; + const thenable = { + then: (thenFinally, catchFinally) => { + thenFinallyArg = thenFinally; + catchFinallyArg = catchFinally; + }, + }; + Promise.prototype.finally.call(thenable, onFinally); + expect(typeof thenFinallyArg).toBe("function"); + expect(typeof catchFinallyArg).toBe("function"); + expect(thenFinallyArg).not.toBe(catchFinallyArg); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js new file mode 100644 index 0000000000..939981ece5 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js @@ -0,0 +1,143 @@ +test("length is 2", () => { + expect(Promise.prototype.then).toHaveLength(2); +}); + +describe("errors", () => { + test("this value must be a Promise", () => { + expect(() => { + Promise.prototype.then.call({}); + }).toThrowWithMessage(TypeError, "Not a Promise"); + }); +}); + +describe("normal behavior", () => { + test("returns a Promise object different from the initial Promise", () => { + const initialPromise = new Promise(() => {}); + const thenPromise = initialPromise.then(); + expect(thenPromise).toBeInstanceOf(Promise); + expect(initialPromise).not.toBe(thenPromise); + }); + + test("then() onFulfilled handler is called when Promise is fulfilled", () => { + let resolvePromise = null; + let fulfillmentValue = null; + new Promise(resolve => { + resolvePromise = resolve; + }).then( + value => { + fulfillmentValue = value; + }, + () => { + expect().fail(); + } + ); + resolvePromise("Some value"); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBe("Some value"); + }); + + test("then() onRejected handler is called when Promise is rejected", () => { + let rejectPromise = null; + let rejectionReason = null; + new Promise((_, reject) => { + rejectPromise = reject; + }).then( + () => { + expect().fail(); + }, + reason => { + rejectionReason = reason; + } + ); + rejectPromise("Some reason"); + runQueuedPromiseJobs(); + expect(rejectionReason).toBe("Some reason"); + }); + + test("returned Promise is resolved with undefined if handler is missing", () => { + let resolvePromise = null; + let fulfillmentValue = null; + new Promise(resolve => { + resolvePromise = resolve; + }) + .then() + .then(value => { + fulfillmentValue = value; + }); + resolvePromise(); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBeUndefined(); + }); + + test("returned Promise is resolved with return value if handler returns value", () => { + let resolvePromise = null; + let fulfillmentValue = null; + new Promise(resolve => { + resolvePromise = resolve; + }) + .then(() => "Some value") + .then(value => { + fulfillmentValue = value; + }); + resolvePromise(); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBe("Some value"); + }); + + test("returned Promise is rejected with error if handler throws error", () => { + let resolvePromise = null; + let rejectionReason = null; + const error = new Error(); + new Promise(resolve => { + resolvePromise = resolve; + }) + .then(() => { + throw error; + }) + .catch(reason => { + rejectionReason = reason; + }); + resolvePromise(); + runQueuedPromiseJobs(); + expect(rejectionReason).toBe(error); + }); + + test("returned Promise is resolved with Promise result if handler returns fulfilled Promise", () => { + let resolvePromise = null; + let fulfillmentValue = null; + new Promise(resolve => { + resolvePromise = resolve; + }) + .then(() => { + return Promise.resolve("Some value"); + }) + .then(value => { + fulfillmentValue = value; + }); + resolvePromise(); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBe("Some value"); + }); + + test("returned Promise is resolved with thenable result if handler returns thenable", () => { + let resolvePromise = null; + let fulfillmentValue = null; + const thenable = { + then: onFulfilled => { + onFulfilled("Some value"); + }, + }; + new Promise(resolve => { + resolvePromise = resolve; + }) + .then(() => { + return thenable; + }) + .then(value => { + fulfillmentValue = value; + }); + resolvePromise(); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBe("Some value"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.reject.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.reject.js new file mode 100644 index 0000000000..623245471d --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.reject.js @@ -0,0 +1,33 @@ +test("length is 1", () => { + expect(Promise.reject).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns a Promise", () => { + const rejectedPromise = Promise.reject(); + expect(rejectedPromise).toBeInstanceOf(Promise); + }); + + test("returned Promise is rejected with given argument", () => { + let rejectionReason = null; + Promise.reject("Some value").catch(reason => { + rejectionReason = reason; + }); + runQueuedPromiseJobs(); + expect(rejectionReason).toBe("Some value"); + }); + + test("works with subclasses", () => { + class CustomPromise extends Promise {} + + const rejectedPromise = CustomPromise.reject("Some value"); + expect(rejectedPromise).toBeInstanceOf(CustomPromise); + + let rejectionReason = null; + rejectedPromise.catch(reason => { + rejectionReason = reason; + }); + runQueuedPromiseJobs(); + expect(rejectionReason).toBe("Some value"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.resolve.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.resolve.js new file mode 100644 index 0000000000..389351f8be --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.resolve.js @@ -0,0 +1,33 @@ +test("length is 1", () => { + expect(Promise.resolve).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns a Promise", () => { + const resolvedPromise = Promise.resolve(); + expect(resolvedPromise).toBeInstanceOf(Promise); + }); + + test("returned Promise is resolved with given argument", () => { + let fulfillmentValue = null; + Promise.resolve("Some value").then(value => { + fulfillmentValue = value; + }); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBe("Some value"); + }); + + test("works with subclasses", () => { + class CustomPromise extends Promise {} + + const resolvedPromise = CustomPromise.resolve("Some value"); + expect(resolvedPromise).toBeInstanceOf(CustomPromise); + + let fulfillmentValue = null; + resolvedPromise.then(value => { + fulfillmentValue = value; + }); + runQueuedPromiseJobs(); + expect(fulfillmentValue).toBe("Some value"); + }); +}); diff --git a/Userland/Utilities/test-js.cpp b/Userland/Utilities/test-js.cpp index 93a8e8c2a2..665f9be454 100644 --- a/Userland/Utilities/test-js.cpp +++ b/Userland/Utilities/test-js.cpp @@ -80,6 +80,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(is_strict_mode); JS_DECLARE_NATIVE_FUNCTION(can_parse_source); + JS_DECLARE_NATIVE_FUNCTION(run_queued_promise_jobs); }; class TestRunner { @@ -138,9 +139,11 @@ void TestRunnerGlobalObject::initialize_global_object() static FlyString global_property_name { "global" }; static FlyString is_strict_mode_property_name { "isStrictMode" }; static FlyString can_parse_source_property_name { "canParseSource" }; + static FlyString run_queued_promise_jobs_property_name { "runQueuedPromiseJobs" }; define_property(global_property_name, this, JS::Attribute::Enumerable); define_native_function(is_strict_mode_property_name, is_strict_mode); define_native_function(can_parse_source_property_name, can_parse_source); + define_native_function(run_queued_promise_jobs_property_name, run_queued_promise_jobs); } JS_DEFINE_NATIVE_FUNCTION(TestRunnerGlobalObject::is_strict_mode) @@ -158,6 +161,12 @@ JS_DEFINE_NATIVE_FUNCTION(TestRunnerGlobalObject::can_parse_source) return JS::Value(!parser.has_errors()); } +JS_DEFINE_NATIVE_FUNCTION(TestRunnerGlobalObject::run_queued_promise_jobs) +{ + vm.run_queued_promise_jobs(); + return JS::js_undefined(); +} + static void cleanup_and_exit() { // Clear the taskbar progress.