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"
This commit is contained in:
Luke Wilde 2022-02-06 03:46:45 +00:00 committed by Linus Groh
parent 5aacec65ab
commit 4c1c6ef91c
12 changed files with 202 additions and 172 deletions

View file

@ -9,15 +9,11 @@
namespace JS {
FinalizationRegistry* FinalizationRegistry::create(GlobalObject& global_object, FunctionObject& cleanup_callback)
{
return global_object.heap().allocate<FinalizationRegistry>(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<Heap>)
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<void> FinalizationRegistry::cleanup(FunctionObject* callback)
ThrowCompletionOr<void> FinalizationRegistry::cleanup(Optional<JobCallback> 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<void> 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);

View file

@ -9,6 +9,7 @@
#include <AK/SinglyLinkedList.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/JobCallback.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/WeakContainer.h>
@ -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<void> cleanup(FunctionObject* callback = nullptr);
ThrowCompletionOr<void> cleanup(Optional<JobCallback> = {});
virtual void remove_dead_cells(Badge<Heap>) 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<Realm> m_realm;
JS::JobCallback m_cleanup_callback;
struct FinalizationRecord {
Cell* target { nullptr };

View file

@ -9,6 +9,7 @@
#include <LibJS/Runtime/FinalizationRegistry.h>
#include <LibJS/Runtime/FinalizationRegistryConstructor.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/JobCallback.h>
namespace JS {
@ -45,11 +46,20 @@ ThrowCompletionOr<Object*> 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<TypeError>(global_object, ErrorType::NotAFunction, cleanup_callback.to_string_without_side_effects());
return TRY(ordinary_create_from_constructor<FinalizationRegistry>(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<FinalizationRegistry>(global_object, new_target, &GlobalObject::finalization_registry_prototype, *realm(), vm.host_make_job_callback(cleanup_callback.as_function())));
}
}

View file

@ -41,7 +41,9 @@ JS_DEFINE_NATIVE_FUNCTION(FinalizationRegistryPrototype::cleanup_some)
if (vm.argument_count() > 0 && !callback.is_function())
return vm.throw_completion<TypeError>(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<JobCallback> {} : vm.host_make_job_callback(callback.as_function())));
return js_undefined();
}

View file

@ -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<FunctionObject> callback;
OwnPtr<CustomData> 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<typename... Args>
inline ThrowCompletionOr<Value> call_job_callback(GlobalObject& global_object, JobCallback& job_callback, Value this_value, Args... args)
inline ThrowCompletionOr<Value> 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));
}
}

View file

@ -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<Prom
if (on_fulfilled.is_function()) {
// a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled).
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());
on_fulfilled_job_callback = vm.host_make_job_callback(on_fulfilled.as_function());
}
// 5. If IsCallable(onRejected) is false, then
@ -295,14 +295,14 @@ Value Promise::perform_then(Value on_fulfilled, Value on_rejected, Optional<Prom
if (on_rejected.is_function()) {
// a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected).
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());
on_rejected_job_callback = vm.host_make_job_callback(on_rejected.as_function());
}
// 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }.
auto* fulfill_reaction = PromiseReaction::create(vm, PromiseReaction::Type::Fulfill, result_capability, on_fulfilled_job_callback);
auto* fulfill_reaction = PromiseReaction::create(vm, PromiseReaction::Type::Fulfill, result_capability, move(on_fulfilled_job_callback));
// 8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }.
auto* reject_reaction = PromiseReaction::create(vm, PromiseReaction::Type::Reject, result_capability, on_rejected_job_callback);
auto* reject_reaction = PromiseReaction::create(vm, PromiseReaction::Type::Reject, result_capability, move(on_rejected_job_callback));
switch (m_state) {
// 9. If promise.[[PromiseState]] is pending, then
@ -322,11 +322,11 @@ Value Promise::perform_then(Value on_fulfilled, Value on_rejected, Optional<Prom
// b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value).
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);
auto [fulfill_job, realm] = create_promise_reaction_job(global_object(), *fulfill_reaction, value);
// c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).
dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Enqueuing job @ {}", this, fulfill_job);
vm.enqueue_promise_job(*fulfill_job);
dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Enqueuing job @ {} in realm {}", this, &fulfill_job, realm);
vm.host_enqueue_promise_job(move(fulfill_job), realm);
break;
}
// 11. Else,
@ -338,15 +338,15 @@ Value Promise::perform_then(Value on_fulfilled, Value on_rejected, Optional<Prom
// c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
if (!m_is_handled)
vm.promise_rejection_tracker(*this, RejectionOperation::Handle);
vm.host_promise_rejection_tracker(*this, RejectionOperation::Handle);
// d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason).
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);
auto [reject_job, realm] = create_promise_reaction_job(global_object(), *reject_reaction, reason);
// e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]).
dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Enqueuing job @ {}", this, reject_job);
vm.enqueue_promise_job(*reject_job);
dbgln_if(PROMISE_DEBUG, "[Promise @ {} / perform_then()]: Enqueuing job @ {} in realm {}", this, &reject_job, realm);
vm.host_enqueue_promise_job(move(reject_job), realm);
break;
}
default:

View file

@ -1,10 +1,12 @@
/*
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/JobCallback.h>
#include <LibJS/Runtime/Promise.h>
@ -13,42 +15,30 @@
namespace JS {
PromiseReactionJob* PromiseReactionJob::create(GlobalObject& global_object, PromiseReaction& reaction, Value argument)
{
return global_object.heap().allocate<PromiseReactionJob>(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<Value> PromiseReactionJob::call()
static ThrowCompletionOr<Value> 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<Value> 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<Value> 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<Value> 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<PromiseResolveThenableJob>(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<Value> PromiseResolveThenableJob::call()
static ThrowCompletionOr<Value> 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<Value> 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 };
}
}

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
*
* 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<Value> call() override;
private:
virtual void visit_edges(Visitor&) override;
PromiseReaction& m_reaction;
Value m_argument;
struct PromiseJob {
Function<ThrowCompletionOr<Value>()> 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<Value> 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);
}

View file

@ -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);
}
}
}

View file

@ -66,7 +66,7 @@ public:
static PromiseReaction* create(VM& vm, Type type, Optional<PromiseCapability> capability, Optional<JobCallback> handler)
{
return vm.heap().allocate_without_global_object<PromiseReaction>(type, capability, handler);
return vm.heap().allocate_without_global_object<PromiseReaction>(type, capability, move(handler));
}
PromiseReaction(Type type, Optional<PromiseCapability> capability, Optional<JobCallback> handler);
@ -74,6 +74,8 @@ public:
Type type() const { return m_type; }
const Optional<PromiseCapability>& capability() const { return m_capability; }
Optional<JobCallback>& handler() { return m_handler; }
const Optional<JobCallback>& handler() const { return m_handler; }
private:

View file

@ -46,6 +46,27 @@ VM::VM(OwnPtr<CustomData> custom_data)
m_single_ascii_character_strings[i] = m_heap.allocate_without_global_object<PrimitiveString>(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<ThrowCompletionOr<Value>()> 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<Cell*>& 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<ThrowCompletionOr<Value>()> 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:

View file

@ -176,16 +176,16 @@ public:
CommonPropertyNames names;
void run_queued_promise_jobs();
void enqueue_promise_job(NativeFunction&);
void enqueue_promise_job(Function<ThrowCompletionOr<Value>()> 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<void()> on_call_stack_emptied;
Function<void(const Promise&)> on_promise_unhandled_rejection;
Function<void(const Promise&)> on_promise_rejection_handled;
Function<void(Promise&)> on_promise_unhandled_rejection;
Function<void(Promise&)> on_promise_rejection_handled;
ThrowCompletionOr<void> initialize_instance_elements(Object& object, ECMAScriptFunctionObject& constructor);
@ -216,6 +216,12 @@ public:
void enable_default_host_import_module_dynamically_hook();
Function<void(Promise&, Promise::RejectionOperation)> host_promise_rejection_tracker;
Function<ThrowCompletionOr<Value>(GlobalObject&, JobCallback&, Value, MarkedValueList)> host_call_job_callback;
Function<void(FinalizationRegistry&)> host_enqueue_finalization_registry_cleanup_job;
Function<void(Function<ThrowCompletionOr<Value>()>, Realm*)> host_enqueue_promise_job;
Function<JobCallback(FunctionObject&)> host_make_job_callback;
private:
explicit VM(OwnPtr<CustomData>);
@ -241,7 +247,7 @@ private:
HashMap<String, Symbol*> m_global_symbol_map;
Vector<NativeFunction*> m_promise_jobs;
Vector<Function<ThrowCompletionOr<Value>()>> m_promise_jobs;
Vector<FinalizationRegistry*> m_finalization_registry_cleanup_jobs;