From 4c1c6ef91c5339cced845ea63da4dc937e7ffb0e Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Sun, 6 Feb 2022 03:46:45 +0000 Subject: [PATCH] LibJS: Setup host hooks and have promise jobs work out the realm This allows the host of LibJS (notably LibWeb in this case) to override certain functions such as HostEnqueuePromiseJob, so it can do it's own thing in certain situations. Notably, LibWeb will override HostEnqueuePromiseJob to put promise jobs on the microtask queue. This also makes promise jobs use AK::Function instead of JS::NativeFunction. This removes the need to go through a JavaScript function and it more closely matches the spec's idea of "abstract closures" --- .../LibJS/Runtime/FinalizationRegistry.cpp | 25 ++-- .../LibJS/Runtime/FinalizationRegistry.h | 16 +- .../FinalizationRegistryConstructor.cpp | 12 +- .../Runtime/FinalizationRegistryPrototype.cpp | 4 +- .../Libraries/LibJS/Runtime/JobCallback.h | 16 +- Userland/Libraries/LibJS/Runtime/Promise.cpp | 38 ++--- .../Libraries/LibJS/Runtime/PromiseJobs.cpp | 138 +++++++++++------- .../Libraries/LibJS/Runtime/PromiseJobs.h | 41 +----- .../LibJS/Runtime/PromiseReaction.cpp | 4 - .../Libraries/LibJS/Runtime/PromiseReaction.h | 4 +- Userland/Libraries/LibJS/Runtime/VM.cpp | 60 ++++---- Userland/Libraries/LibJS/Runtime/VM.h | 16 +- 12 files changed, 202 insertions(+), 172 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp index 42ddfe47d3..40bdbcb214 100644 --- a/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp @@ -9,15 +9,11 @@ namespace JS { -FinalizationRegistry* FinalizationRegistry::create(GlobalObject& global_object, FunctionObject& cleanup_callback) -{ - return global_object.heap().allocate(global_object, cleanup_callback, *global_object.finalization_registry_prototype()); -} - -FinalizationRegistry::FinalizationRegistry(FunctionObject& cleanup_callback, Object& prototype) +FinalizationRegistry::FinalizationRegistry(Realm& realm, JS::JobCallback cleanup_callback, Object& prototype) : Object(prototype) , WeakContainer(heap()) - , m_cleanup_callback(&cleanup_callback) + , m_realm(JS::make_handle(realm)) + , m_cleanup_callback(move(cleanup_callback)) { } @@ -54,30 +50,34 @@ void FinalizationRegistry::remove_dead_cells(Badge) break; } if (any_cells_were_removed) - vm().enqueue_finalization_registry_cleanup_job(*this); + vm().host_enqueue_finalization_registry_cleanup_job(*this); } // 9.13 CleanupFinalizationRegistry ( finalizationRegistry ), https://tc39.es/ecma262/#sec-cleanup-finalization-registry -ThrowCompletionOr FinalizationRegistry::cleanup(FunctionObject* callback) +ThrowCompletionOr FinalizationRegistry::cleanup(Optional callback) { + auto& vm = this->vm(); + auto& global_object = this->global_object(); + // 1. Assert: finalizationRegistry has [[Cells]] and [[CleanupCallback]] internal slots. // Note: Ensured by type. // 2. Let callback be finalizationRegistry.[[CleanupCallback]]. - auto cleanup_callback = callback ?: m_cleanup_callback; + auto& cleanup_callback = callback.has_value() ? callback.value() : m_cleanup_callback; // 3. While finalizationRegistry.[[Cells]] contains a Record cell such that cell.[[WeakRefTarget]] is empty, an implementation may perform the following steps: for (auto it = m_records.begin(); it != m_records.end(); ++it) { // a. Choose any such cell. if (it->target != nullptr) continue; - auto cell = *it; // b. Remove cell from finalizationRegistry.[[Cells]]. + MarkedValueList arguments(vm.heap()); + arguments.append(it->held_value); it.remove(m_records); // c. Perform ? HostCallJobCallback(callback, undefined, « cell.[[HeldValue]] »). - (void)TRY(call(global_object(), *cleanup_callback, js_undefined(), cell.held_value)); + TRY(vm.host_call_job_callback(global_object, cleanup_callback, js_undefined(), move(arguments))); } // 4. Return NormalCompletion(empty). @@ -87,7 +87,6 @@ ThrowCompletionOr FinalizationRegistry::cleanup(FunctionObject* callback) void FinalizationRegistry::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); - visitor.visit(m_cleanup_callback); for (auto& record : m_records) { visitor.visit(record.held_value); visitor.visit(record.unregister_token); diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h index a4b9a9696f..1956bd248c 100644 --- a/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,21 +22,26 @@ class FinalizationRegistry final JS_OBJECT(FinalizationRegistry, Object); public: - static FinalizationRegistry* create(GlobalObject&, FunctionObject&); - - explicit FinalizationRegistry(FunctionObject&, Object& prototype); + explicit FinalizationRegistry(Realm&, JS::JobCallback, Object& prototype); virtual ~FinalizationRegistry() override; void add_finalization_record(Cell& target, Value held_value, Object* unregister_token); bool remove_by_token(Object& unregister_token); - ThrowCompletionOr cleanup(FunctionObject* callback = nullptr); + ThrowCompletionOr cleanup(Optional = {}); virtual void remove_dead_cells(Badge) override; + Realm& realm() { return *m_realm.cell(); } + Realm const& realm() const { return *m_realm.cell(); } + + JobCallback& cleanup_callback() { return m_cleanup_callback; } + JobCallback const& cleanup_callback() const { return m_cleanup_callback; } + private: virtual void visit_edges(Visitor& visitor) override; - FunctionObject* m_cleanup_callback { nullptr }; + Handle m_realm; + JS::JobCallback m_cleanup_callback; struct FinalizationRecord { Cell* target { nullptr }; diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp index 8e01326888..19e8c8771f 100644 --- a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace JS { @@ -45,11 +46,20 @@ ThrowCompletionOr FinalizationRegistryConstructor::construct(FunctionOb auto& vm = this->vm(); auto& global_object = this->global_object(); + // NOTE: Step 1 is implemented in FinalizationRegistryConstructor::call() + + // 2. If IsCallable(cleanupCallback) is false, throw a TypeError exception. auto cleanup_callback = vm.argument(0); if (!cleanup_callback.is_function()) return vm.throw_completion(global_object, ErrorType::NotAFunction, cleanup_callback.to_string_without_side_effects()); - return TRY(ordinary_create_from_constructor(global_object, new_target, &GlobalObject::finalization_registry_prototype, cleanup_callback.as_function())); + // 3. Let finalizationRegistry be ? OrdinaryCreateFromConstructor(NewTarget, "%FinalizationRegistry.prototype%", « [[Realm]], [[CleanupCallback]], [[Cells]] »). + // 4. Let fn be the active function object. (NOTE: Not necessary. The active function object is `this`) + // 5. Set finalizationRegistry.[[Realm]] to fn.[[Realm]]. + // 6. Set finalizationRegistry.[[CleanupCallback]] to HostMakeJobCallback(cleanupCallback). + // 7. Set finalizationRegistry.[[Cells]] to a new empty List. (NOTE: This is done inside FinalizationRegistry instead of here) + // 8. Return finalizationRegistry. + return TRY(ordinary_create_from_constructor(global_object, new_target, &GlobalObject::finalization_registry_prototype, *realm(), vm.host_make_job_callback(cleanup_callback.as_function()))); } } diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp index 7d90b8c797..f617ff67cb 100644 --- a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp @@ -41,7 +41,9 @@ JS_DEFINE_NATIVE_FUNCTION(FinalizationRegistryPrototype::cleanup_some) if (vm.argument_count() > 0 && !callback.is_function()) return vm.throw_completion(global_object, ErrorType::NotAFunction, callback.to_string_without_side_effects()); - TRY(finalization_registry->cleanup(callback.is_undefined() ? nullptr : &callback.as_function())); + // IMPLEMENTATION DEFINED: The specification for this function hasn't been updated to accomodate for JobCallback records. + // This just follows how the constructor immediately converts the callback to a JobCallback using HostMakeJobCallback. + TRY(finalization_registry->cleanup(callback.is_undefined() ? Optional {} : vm.host_make_job_callback(callback.as_function()))); return js_undefined(); } diff --git a/Userland/Libraries/LibJS/Runtime/JobCallback.h b/Userland/Libraries/LibJS/Runtime/JobCallback.h index 1b596455bd..90c84b7a7e 100644 --- a/Userland/Libraries/LibJS/Runtime/JobCallback.h +++ b/Userland/Libraries/LibJS/Runtime/JobCallback.h @@ -14,25 +14,29 @@ namespace JS { // 9.5.1 JobCallback Records, https://tc39.es/ecma262/#sec-jobcallback-records struct JobCallback { - FunctionObject* callback { nullptr }; + struct CustomData { + virtual ~CustomData() = default; + }; + + Handle callback; + OwnPtr custom_data { nullptr }; }; // 9.5.2 HostMakeJobCallback ( callback ), https://tc39.es/ecma262/#sec-hostmakejobcallback inline JobCallback make_job_callback(FunctionObject& callback) { // 1. Return the JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }. - return { &callback }; + return { make_handle(&callback) }; } // 9.5.3 HostCallJobCallback ( jobCallback, V, argumentsList ), https://tc39.es/ecma262/#sec-hostcalljobcallback -template -inline ThrowCompletionOr call_job_callback(GlobalObject& global_object, JobCallback& job_callback, Value this_value, Args... args) +inline ThrowCompletionOr call_job_callback(GlobalObject& global_object, JobCallback& job_callback, Value this_value, MarkedValueList arguments_list) { // 1. Assert: IsCallable(jobCallback.[[Callback]]) is true. - VERIFY(job_callback.callback); + VERIFY(!job_callback.callback.is_null()); // 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList). - return call(global_object, job_callback.callback, this_value, args...); + return call(global_object, job_callback.callback.cell(), this_value, move(arguments_list)); } } diff --git a/Userland/Libraries/LibJS/Runtime/Promise.cpp b/Userland/Libraries/LibJS/Runtime/Promise.cpp index d5181f74fd..6770a95170 100644 --- a/Userland/Libraries/LibJS/Runtime/Promise.cpp +++ b/Userland/Libraries/LibJS/Runtime/Promise.cpp @@ -129,15 +129,15 @@ Promise::ResolvingFunctions Promise::create_resolving_functions() // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). 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()); + auto then_job_callback = vm.host_make_job_callback(then_action.as_function()); // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Creating PromiseResolveThenableJob for thenable {}", &promise, resolution); - auto* job = PromiseResolveThenableJob::create(global_object, promise, resolution, then_job_callback); + auto [job, realm] = create_promise_resolve_thenable_job(global_object, promise, resolution, move(then_job_callback)); // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Enqueuing job @ {}", &promise, job); - vm.enqueue_promise_job(*job); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / PromiseResolvingFunction]: Enqueuing job @ {} in realm {}", &promise, &job, realm); + vm.host_enqueue_promise_job(move(job), realm); // 16. Return undefined. return js_undefined(); @@ -230,7 +230,7 @@ Value Promise::reject(Value reason) // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). if (!m_is_handled) - vm.promise_rejection_tracker(*this, RejectionOperation::Reject); + vm.host_promise_rejection_tracker(*this, RejectionOperation::Reject); // 8. Return TriggerPromiseReactions(reactions, reason). trigger_reactions(); @@ -252,11 +252,11 @@ void Promise::trigger_reactions() const for (auto& reaction : reactions) { // a. Let job be NewPromiseReactionJob(reaction, argument). 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); + auto [job, realm] = create_promise_reaction_job(global_object(), *reaction, m_result); // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - dbgln_if(PROMISE_DEBUG, "[Promise @ {} / trigger_reactions()]: Enqueuing job @ {}", this, job); - vm.enqueue_promise_job(*job); + dbgln_if(PROMISE_DEBUG, "[Promise @ {} / trigger_reactions()]: Enqueuing job @ {} in realm {}", this, &job, realm); + vm.host_enqueue_promise_job(move(job), realm); } if constexpr (PROMISE_DEBUG) { @@ -284,7 +284,7 @@ Value Promise::perform_then(Value on_fulfilled, Value on_rejected, Optional + * Copyright (c) 2021, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include #include #include #include @@ -13,42 +15,30 @@ 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 ( reaction, argument ), https://tc39.es/ecma262/#sec-newpromisereactionjob -ThrowCompletionOr PromiseReactionJob::call() +static ThrowCompletionOr run_reaction_job(GlobalObject& global_object, PromiseReaction& reaction, Value argument) { - auto& global_object = this->global_object(); + auto& vm = global_object.vm(); // a. Let promiseCapability be reaction.[[Capability]]. - auto& promise_capability = m_reaction.capability(); + auto& promise_capability = reaction.capability(); // b. Let type be reaction.[[Type]]. - auto type = m_reaction.type(); + auto type = reaction.type(); // c. Let handler be reaction.[[Handler]]. - auto handler = m_reaction.handler(); + auto& handler = reaction.handler(); Completion handler_result; // d. If handler is empty, then if (!handler.has_value()) { - dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Handler is empty", this); + dbgln_if(PROMISE_DEBUG, "run_reaction_job: Handler is empty"); // i. If type is Fulfill, let handlerResult be NormalCompletion(argument). if (type == PromiseReaction::Type::Fulfill) { - dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction type is Type::Fulfill, setting handler result to {}", this, m_argument); - handler_result = normal_completion(m_argument); + dbgln_if(PROMISE_DEBUG, "run_reaction_job: Reaction type is Type::Fulfill, setting handler result to {}", argument); + handler_result = normal_completion(argument); } // ii. Else, else { @@ -56,14 +46,16 @@ ThrowCompletionOr PromiseReactionJob::call() VERIFY(type == PromiseReaction::Type::Reject); // 2. Let handlerResult be ThrowCompletion(argument). - dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction type is Type::Reject, throwing exception with argument {}", this, m_argument); - handler_result = throw_completion(m_argument); + dbgln_if(PROMISE_DEBUG, "run_reaction_job: Reaction type is Type::Reject, throwing exception with argument {}", argument); + handler_result = throw_completion(argument); } } // e. Else, let handlerResult be HostCallJobCallback(handler, undefined, « argument »). 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(global_object, handler.value(), js_undefined(), m_argument); + dbgln_if(PROMISE_DEBUG, "run_reaction_job: Calling handler callback {} @ {} with argument {}", handler.value().callback.cell()->class_name(), handler.value().callback.cell(), argument); + MarkedValueList arguments(vm.heap()); + arguments.append(argument); + handler_result = vm.host_call_job_callback(global_object, handler.value(), js_undefined(), move(arguments)); } // f. If promiseCapability is undefined, then @@ -72,7 +64,7 @@ ThrowCompletionOr PromiseReactionJob::call() VERIFY(!handler_result.is_abrupt()); // ii. Return NormalCompletion(empty). - dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction has no PromiseCapability, returning empty value", this); + dbgln_if(PROMISE_DEBUG, "run_reaction_job: Reaction has no PromiseCapability, returning empty value"); // TODO: This can't return an empty value at the moment, because the implicit conversion to Completion would fail. // Change it back when this is using completions (`return normal_completion({})`) return js_undefined(); @@ -84,57 +76,72 @@ ThrowCompletionOr PromiseReactionJob::call() if (handler_result.is_abrupt()) { // i. Let status be Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »). auto* reject_function = promise_capability.value().reject; - dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling PromiseCapability's reject function @ {}", this, reject_function); + dbgln_if(PROMISE_DEBUG, "run_reaction_job: Calling PromiseCapability's reject function @ {}", reject_function); return JS::call(global_object, *reject_function, js_undefined(), *handler_result.value()); } // i. Else, else { // i. Let status be Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »). auto* resolve_function = promise_capability.value().resolve; - dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling PromiseCapability's resolve function @ {}", this, resolve_function); + dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob]: Calling PromiseCapability's resolve function @ {}", resolve_function); return JS::call(global_object, *resolve_function, js_undefined(), *handler_result.value()); } // j. Return Completion(status). } -void PromiseReactionJob::visit_edges(Visitor& visitor) +// 27.2.2.1 NewPromiseReactionJob ( reaction, argument ), https://tc39.es/ecma262/#sec-newpromisereactionjob +PromiseJob create_promise_reaction_job(GlobalObject& global_object, PromiseReaction& reaction, Value argument) { - Base::visit_edges(visitor); - visitor.visit(&m_reaction); - visitor.visit(m_argument); -} + // 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called: + // See run_reaction_job for "the following steps". + auto job = [global_object = JS::make_handle(&global_object), reaction = JS::make_handle(&reaction), argument = JS::make_handle(argument)]() mutable { + return run_reaction_job(*global_object.cell(), *reaction.cell(), argument.value()); + }; -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()); -} + // 2. Let handlerRealm be null. + Realm* handler_realm { nullptr }; -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) -{ + // 3. If reaction.[[Handler]] is not empty, then + auto& handler = reaction.handler(); + if (handler.has_value()) { + // a. Let getHandlerRealmResult be GetFunctionRealm(reaction.[[Handler]].[[Callback]]). + auto get_handler_realm_result = get_function_realm(global_object, *handler->callback.cell()); + + // b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]]. + if (!get_handler_realm_result.is_throw_completion()) { + handler_realm = get_handler_realm_result.release_value(); + } else { + // c. Else, set handlerRealm to the current Realm Record. + handler_realm = global_object.vm().current_realm(); + } + + // d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects. + } + + // 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }. + return { move(job), handler_realm }; } // 27.2.2.2 NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ), https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob -ThrowCompletionOr PromiseResolveThenableJob::call() +static ThrowCompletionOr run_resolve_thenable_job(GlobalObject& global_object, Promise& promise_to_resolve, Value thenable, JobCallback& then) { - auto& global_object = this->global_object(); + auto& vm = global_object.vm(); // a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve). - auto [resolve_function, reject_function] = m_promise_to_resolve.create_resolving_functions(); + auto [resolve_function, reject_function] = promise_to_resolve.create_resolving_functions(); // b. Let thenCallResult be HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »). - dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: Calling then job callback for thenable {}", this, &m_thenable); - auto then_call_result = call_job_callback(global_object, m_then, m_thenable, &resolve_function, &reject_function); + dbgln_if(PROMISE_DEBUG, "run_resolve_thenable_job: Calling then job callback for thenable {}", &thenable); + MarkedValueList arguments(vm.heap()); + arguments.append(Value(&resolve_function)); + arguments.append(Value(&reject_function)); + auto then_call_result = vm.host_call_job_callback(global_object, then, thenable, move(arguments)); // c. If thenCallResult is an abrupt completion, then if (then_call_result.is_error()) { // i. Let status be Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »). - dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: then_call_result is an abrupt completion, calling reject function with value {}", this, *then_call_result.throw_completion().value()); + dbgln_if(PROMISE_DEBUG, "run_resolve_thenable_job: then_call_result is an abrupt completion, calling reject function with value {}", *then_call_result.throw_completion().value()); auto status = JS::call(global_object, &reject_function, js_undefined(), *then_call_result.throw_completion().value()); // ii. Return Completion(status). @@ -142,16 +149,37 @@ ThrowCompletionOr PromiseResolveThenableJob::call() } // d. Return Completion(thenCallResult). - dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: Returning then call result {}", this, then_call_result.value()); + dbgln_if(PROMISE_DEBUG, "run_resolve_thenable_job: Returning then call result {}", then_call_result.value()); return then_call_result; } -void PromiseResolveThenableJob::visit_edges(Visitor& visitor) +// 27.2.2.2 NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ), https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob +PromiseJob create_promise_resolve_thenable_job(GlobalObject& global_object, Promise& promise_to_resolve, Value thenable, JobCallback then) { - Base::visit_edges(visitor); - visitor.visit(&m_promise_to_resolve); - visitor.visit(m_thenable); - visitor.visit(m_then.callback); + // 2. Let getThenRealmResult be GetFunctionRealm(then.[[Callback]]). + Realm* then_realm { nullptr }; + auto get_then_realm_result = get_function_realm(global_object, *then.callback.cell()); + + // 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]]. + if (!get_then_realm_result.is_throw_completion()) { + then_realm = get_then_realm_result.release_value(); + } else { + // 4. Else, let thenRealm be the current Realm Record. + then_realm = global_object.vm().current_realm(); + } + + // 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects. + VERIFY(then_realm); + + // 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called: + // See PromiseResolveThenableJob::call() for "the following steps". + // NOTE: This is done out of order, since `then` is moved into the lambda and `then` would be invalid if it was done at the start. + auto job = [global_object = JS::make_handle(&global_object), promise_to_resolve = JS::make_handle(&promise_to_resolve), thenable = JS::make_handle(thenable), then = move(then)]() mutable { + return run_resolve_thenable_job(*global_object.cell(), *promise_to_resolve.cell(), thenable.value(), then); + }; + + // 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }. + return { move(job), then_realm }; } } diff --git a/Userland/Libraries/LibJS/Runtime/PromiseJobs.h b/Userland/Libraries/LibJS/Runtime/PromiseJobs.h index 8405e56326..c46685bb9e 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseJobs.h +++ b/Userland/Libraries/LibJS/Runtime/PromiseJobs.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Linus Groh + * Copyright (c) 2021, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -13,41 +14,13 @@ 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 ThrowCompletionOr call() override; - -private: - virtual void visit_edges(Visitor&) override; - - PromiseReaction& m_reaction; - Value m_argument; +struct PromiseJob { + Function()> job; + Realm* realm { nullptr }; }; -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 ThrowCompletionOr call() override; - -private: - virtual void visit_edges(Visitor&) override; - - Promise& m_promise_to_resolve; - Value m_thenable; - JobCallback m_then; -}; +// NOTE: These return a PromiseJob to prevent awkward casting at call sites. +PromiseJob create_promise_reaction_job(GlobalObject&, PromiseReaction&, Value argument); +PromiseJob create_promise_resolve_thenable_job(GlobalObject&, Promise&, Value thenable, JobCallback then); } diff --git a/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp b/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp index 42858cc16b..0905142740 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp +++ b/Userland/Libraries/LibJS/Runtime/PromiseReaction.cpp @@ -93,10 +93,6 @@ void PromiseReaction::visit_edges(Cell::Visitor& visitor) 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 index 9300096a75..5640843513 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseReaction.h +++ b/Userland/Libraries/LibJS/Runtime/PromiseReaction.h @@ -66,7 +66,7 @@ public: static PromiseReaction* create(VM& vm, Type type, Optional capability, Optional handler) { - return vm.heap().allocate_without_global_object(type, capability, handler); + return vm.heap().allocate_without_global_object(type, capability, move(handler)); } PromiseReaction(Type type, Optional capability, Optional handler); @@ -74,6 +74,8 @@ public: Type type() const { return m_type; } const Optional& capability() const { return m_capability; } + + Optional& handler() { return m_handler; } const Optional& handler() const { return m_handler; } private: diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 9d849c1f3a..e42caa49b4 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -46,6 +46,27 @@ VM::VM(OwnPtr custom_data) m_single_ascii_character_strings[i] = m_heap.allocate_without_global_object(String::formatted("{:c}", i)); } + // Default hook implementations. These can be overridden by the host, for example, LibWeb overrides the default hooks to place promise jobs on the microtask queue. + host_promise_rejection_tracker = [this](Promise& promise, Promise::RejectionOperation operation) { + promise_rejection_tracker(promise, operation); + }; + + host_call_job_callback = [](GlobalObject& global_object, JobCallback& job_callback, Value this_value, MarkedValueList arguments) { + return call_job_callback(global_object, job_callback, this_value, move(arguments)); + }; + + host_enqueue_finalization_registry_cleanup_job = [this](FinalizationRegistry& finalization_registry) { + enqueue_finalization_registry_cleanup_job(finalization_registry); + }; + + host_enqueue_promise_job = [this](Function()> job, Realm* realm) { + enqueue_promise_job(move(job), realm); + }; + + host_make_job_callback = [](FunctionObject& function_object) { + return make_job_callback(function_object); + }; + host_resolve_imported_module = [&](ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier) { return resolve_imported_module(move(referencing_script_or_module), specifier); }; @@ -177,9 +198,6 @@ 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); - for (auto* finalization_registry : m_finalization_registry_cleanup_jobs) roots.set(finalization_registry); } @@ -604,36 +622,22 @@ void VM::run_queued_promise_jobs() dbgln_if(PROMISE_DEBUG, "Running queued promise jobs"); while (!m_promise_jobs.is_empty()) { - auto* job = m_promise_jobs.take_first(); - dbgln_if(PROMISE_DEBUG, "Calling promise job function @ {}", job); + auto job = m_promise_jobs.take_first(); + dbgln_if(PROMISE_DEBUG, "Calling promise job function"); - // NOTE: If the execution context stack is empty, we make and push a temporary context. - ExecutionContext execution_context(heap()); - bool pushed_execution_context = false; - if (m_execution_context_stack.is_empty()) { - static FlyString promise_execution_context_name = "(promise execution context)"; - execution_context.function_name = promise_execution_context_name; - // FIXME: Propagate potential failure - MUST(push_execution_context(execution_context, job->global_object())); - pushed_execution_context = true; - } - - [[maybe_unused]] auto result = call(job->global_object(), *job, js_undefined()); - - // This doesn't match the spec, it actually defines that Job Abstract Closures must return - // a normal completion. In reality that's not the case however, and all major engines clear - // exceptions when running Promise jobs. See the commit where these two lines were initially - // added for a much more detailed explanation. (Hash: a53542e0a3fbd7bf22b685d87f0473e489e1cf42) - - if (pushed_execution_context) - pop_execution_context(); + [[maybe_unused]] auto result = job(); } } // 9.5.4 HostEnqueuePromiseJob ( job, realm ), https://tc39.es/ecma262/#sec-hostenqueuepromisejob -void VM::enqueue_promise_job(NativeFunction& job) +void VM::enqueue_promise_job(Function()> job, Realm*) { - m_promise_jobs.append(&job); + // An implementation of HostEnqueuePromiseJob must conform to the requirements in 9.5 as well as the following: + // - FIXME: If realm is not null, each time job is invoked the implementation must perform implementation-defined steps such that execution is prepared to evaluate ECMAScript code at the time of job's invocation. + // - FIXME: Let scriptOrModule be GetActiveScriptOrModule() at the time HostEnqueuePromiseJob is invoked. If realm is not null, each time job is invoked the implementation must perform implementation-defined steps + // such that scriptOrModule is the active script or module at the time of job's invocation. + // - Jobs must run in the same order as the HostEnqueuePromiseJob invocations that scheduled them. + m_promise_jobs.append(move(job)); } void VM::run_queued_finalization_registry_cleanup_jobs() @@ -652,7 +656,7 @@ void VM::enqueue_finalization_registry_cleanup_job(FinalizationRegistry& registr } // 27.2.1.9 HostPromiseRejectionTracker ( promise, operation ), https://tc39.es/ecma262/#sec-host-promise-rejection-tracker -void VM::promise_rejection_tracker(const Promise& promise, Promise::RejectionOperation operation) const +void VM::promise_rejection_tracker(Promise& promise, Promise::RejectionOperation operation) const { switch (operation) { case Promise::RejectionOperation::Reject: diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index 126c20c30e..5b9ee5bbcc 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -176,16 +176,16 @@ public: CommonPropertyNames names; void run_queued_promise_jobs(); - void enqueue_promise_job(NativeFunction&); + void enqueue_promise_job(Function()> job, Realm*); void run_queued_finalization_registry_cleanup_jobs(); void enqueue_finalization_registry_cleanup_job(FinalizationRegistry&); - void promise_rejection_tracker(const Promise&, Promise::RejectionOperation) const; + void promise_rejection_tracker(Promise&, Promise::RejectionOperation) const; Function on_call_stack_emptied; - Function on_promise_unhandled_rejection; - Function on_promise_rejection_handled; + Function on_promise_unhandled_rejection; + Function on_promise_rejection_handled; ThrowCompletionOr initialize_instance_elements(Object& object, ECMAScriptFunctionObject& constructor); @@ -216,6 +216,12 @@ public: void enable_default_host_import_module_dynamically_hook(); + Function host_promise_rejection_tracker; + Function(GlobalObject&, JobCallback&, Value, MarkedValueList)> host_call_job_callback; + Function host_enqueue_finalization_registry_cleanup_job; + Function()>, Realm*)> host_enqueue_promise_job; + Function host_make_job_callback; + private: explicit VM(OwnPtr); @@ -241,7 +247,7 @@ private: HashMap m_global_symbol_map; - Vector m_promise_jobs; + Vector()>> m_promise_jobs; Vector m_finalization_registry_cleanup_jobs;