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;