LibIDL+LibWeb: Begin support for async iterator in IDL

This adds support for async iterators of the form:

    async iterable<value_type>;
    async iterable<value_type>(/* arguments... */);

It does not yet support the value pairs of the form:

    async iterable<key_type, value_type>;
    async iterable<key_type, value_type>(/* arguments... */);

Async iterators have an optional `return` data property. There's not a
particularly good way to know what interfaces implement this property.
So this adds a new extended attribute, DefinesAsyncIteratorReturn, which
interfaces can use to declare their support.
This commit is contained in:
Timothy Flynn 2025-04-12 13:18:02 -04:00 committed by Tim Flynn
commit c0ead1b01a
Notes: github-actions[bot] 2025-04-14 21:44:24 +00:00
10 changed files with 593 additions and 7 deletions

View file

@ -0,0 +1,217 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibWeb/WebIDL/AsyncIterator.h>
namespace Web::WebIDL {
GC_DEFINE_ALLOCATOR(AsyncIterator);
AsyncIterator::AsyncIterator(JS::Realm& realm, JS::Object::PropertyKind iteration_kind)
: PlatformObject(realm)
, m_kind(iteration_kind)
{
}
AsyncIterator::~AsyncIterator() = default;
void AsyncIterator::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_ongoing_promise);
}
// https://webidl.spec.whatwg.org/#ref-for-dfn-asynchronous-iterator-prototype-object%E2%91%A2
JS::ThrowCompletionOr<GC::Ptr<JS::Object>> AsyncIterator::iterator_next_impl()
{
auto& realm = this->realm();
auto& vm = this->vm();
// 8. Let nextSteps be the following steps:
auto next_steps = [this](JS::VM& vm) {
auto& realm = this->realm();
// 1. Let nextPromiseCapability be ! NewPromiseCapability(%Promise%).
auto next_promise_capability = WebIDL::create_promise(realm);
// 2. If objects is finished is true, then:
if (m_is_finished) {
// 1. Let result be CreateIteratorResultObject(undefined, true).
auto result = JS::create_iterator_result_object(vm, JS::js_undefined(), true);
// 2. Perform ! Call(nextPromiseCapability.[[Resolve]], undefined, « result »).
MUST(JS::call(vm, *next_promise_capability->resolve(), JS::js_undefined(), result));
// 3. Return nextPromiseCapability.[[Promise]].
return next_promise_capability->promise();
}
// 3. Let kind be objects kind.
// 4. Let nextPromise be the result of getting the next iteration result with objects target and object.
auto& next_promise = as<JS::Promise>(*next_iteration_result(realm)->promise());
// 5. Let fulfillSteps be the following steps, given next:
auto fulfill_steps = [this](JS::VM& vm) {
auto next = vm.argument(0);
// 1. Set objects ongoing promise to null.
m_ongoing_promise = nullptr;
// 2. If next is end of iteration, then:
if (next.is_special_empty_value()) {
// 1. Set objects is finished to true.
m_is_finished = true;
// 2. Return CreateIteratorResultObject(undefined, true).
return JS::create_iterator_result_object(vm, JS::js_undefined(), true);
}
// FIXME: 2. Otherwise, if interface has a pair asynchronously iterable declaration:
else if (false) {
// 1. Assert: next is a value pair.
// 2. Return the iterator result for next and kind.
}
// Otherwise:
else {
// 1. Assert: interface has a value asynchronously iterable declaration.
// 2. Assert: next is a value of the type that appears in the declaration.
// 3. Let value be next, converted to a JavaScript value.
// 4. Return CreateIteratorResultObject(value, false).
return JS::create_iterator_result_object(vm, next, false);
}
};
// 6. Let onFulfilled be CreateBuiltinFunction(fulfillSteps, « »).
auto on_fulfilled = JS::NativeFunction::create(realm, move(fulfill_steps), 0);
// 7. Let rejectSteps be the following steps, given reason:
auto reject_steps = [this](JS::VM& vm) {
auto reason = vm.argument(0);
// 1. Set objects ongoing promise to null.
m_ongoing_promise = nullptr;
// 2. Set objects is finished to true.
m_is_finished = true;
// 3. Throw reason.
return JS::throw_completion(reason);
};
// 8. Let onRejected be CreateBuiltinFunction(rejectSteps, « »).
auto on_rejected = JS::NativeFunction::create(realm, move(reject_steps), 0);
// 9. Perform PerformPromiseThen(nextPromise, onFulfilled, onRejected, nextPromiseCapability).
next_promise.perform_then(on_fulfilled, on_rejected, next_promise_capability);
// 10. Return nextPromiseCapability.[[Promise]].
return next_promise_capability->promise();
};
// 9. Let ongoingPromise be objects ongoing promise.
// 10. If ongoingPromise is not null, then:
if (m_ongoing_promise) {
// 1. Let afterOngoingPromiseCapability be ! NewPromiseCapability(%Promise%).
auto after_ongoing_promise_capability = WebIDL::create_promise(realm);
// 2. Let onSettled be CreateBuiltinFunction(nextSteps, « »).
auto on_settled = JS::NativeFunction::create(realm, move(next_steps), 0);
// 3. Perform PerformPromiseThen(ongoingPromise, onSettled, onSettled, afterOngoingPromiseCapability).
m_ongoing_promise->perform_then(on_settled, on_settled, after_ongoing_promise_capability);
// 4. Set objects ongoing promise to afterOngoingPromiseCapability.[[Promise]].
m_ongoing_promise = as<JS::Promise>(*after_ongoing_promise_capability->promise());
}
// 11. Otherwise:
else {
// 1. Set objects ongoing promise to the result of running nextSteps.
m_ongoing_promise = as<JS::Promise>(*next_steps(vm));
}
// 12. Return objects ongoing promise.
return m_ongoing_promise;
}
// https://webidl.spec.whatwg.org/#ref-for-asynchronous-iterator-return
JS::ThrowCompletionOr<GC::Ptr<JS::Object>> AsyncIterator::iterator_return_impl(GC::Ref<WebIDL::Promise> return_promise_capability, JS::Value value)
{
auto& realm = this->realm();
auto& vm = this->vm();
// 8. Let returnSteps be the following steps:
auto return_steps = [this, value](JS::VM& vm) {
auto& realm = this->realm();
// 1. Let returnPromiseCapability be ! NewPromiseCapability(%Promise%).
auto return_promise_capability = WebIDL::create_promise(realm);
// 2. If objects is finished is true, then:
if (m_is_finished) {
// 1. Let result be CreateIteratorResultObject(value, true).
auto result = JS::create_iterator_result_object(vm, value, true);
// 2. Perform ! Call(returnPromiseCapability.[[Resolve]], undefined, « result »).
MUST(JS::call(vm, *return_promise_capability->resolve(), JS::js_undefined(), result));
// 3. Return returnPromiseCapability.[[Promise]].
return return_promise_capability->promise();
}
// 3. Set objects is finished to true.
m_is_finished = true;
// 4. Return the result of running the asynchronous iterator return algorithm for interface, given objects target, object, and value.
return iterator_return(realm, value)->promise();
};
// 9. Let ongoingPromise be objects ongoing promise.
// 10. If ongoingPromise is not null, then:
if (m_ongoing_promise) {
// 1. Let afterOngoingPromiseCapability be ! NewPromiseCapability(%Promise%).
auto after_ongoing_promise_capability = WebIDL::create_promise(realm);
// 2. Let onSettled be CreateBuiltinFunction(returnSteps, « »).
auto on_settled = JS::NativeFunction::create(realm, move(return_steps), 0);
// 3. Perform PerformPromiseThen(ongoingPromise, onSettled, onSettled, afterOngoingPromiseCapability).
m_ongoing_promise->perform_then(on_settled, on_settled, after_ongoing_promise_capability);
// 4. Set objects ongoing promise to afterOngoingPromiseCapability.[[Promise]].
m_ongoing_promise = as<JS::Promise>(*after_ongoing_promise_capability->promise());
}
// 11. Otherwise:
else {
// 1. Set objects ongoing promise to the result of running returnSteps.
m_ongoing_promise = as<JS::Promise>(*return_steps(vm));
}
// 12. Let fulfillSteps be the following steps:
auto fulfill_steps = [value](JS::VM& vm) {
// 1. Return CreateIteratorResultObject(value, true).
return JS::create_iterator_result_object(vm, value, true);
};
// 13. Let onFulfilled be CreateBuiltinFunction(fulfillSteps, « »).
auto on_fulfilled = JS::NativeFunction::create(realm, move(fulfill_steps), 0);
// 14. Perform PerformPromiseThen(objects ongoing promise, onFulfilled, undefined, returnPromiseCapability).
m_ongoing_promise->perform_then(on_fulfilled, JS::js_undefined(), return_promise_capability);
// 15. Return returnPromiseCapability.[[Promise]].
return return_promise_capability->promise();
}
GC::Ref<WebIDL::Promise> AsyncIterator::iterator_return(JS::Realm&, JS::Value)
{
// If this is reached, a `return` data property was generated for your async iterator, but you neglected to override this method.
VERIFY_NOT_REACHED();
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::WebIDL {
class AsyncIterator : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(AsyncIterator, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(AsyncIterator);
public:
virtual ~AsyncIterator() override;
// https://webidl.spec.whatwg.org/#ref-for-dfn-asynchronous-iterator-prototype-object%E2%91%A2
template<typename AsyncIteratorInterface>
static JS::ThrowCompletionOr<GC::Ptr<JS::Object>> next(JS::Realm& realm, StringView interface_name)
{
auto validation_result = validate_this<AsyncIteratorInterface>(realm, interface_name);
return validation_result.visit(
[](GC::Ref<AsyncIteratorInterface> iterator) {
return iterator->iterator_next_impl();
},
[](JS::ThrowCompletionOr<GC::Ptr<JS::Object>> result) {
return result;
});
}
// https://webidl.spec.whatwg.org/#ref-for-asynchronous-iterator-return
template<typename AsyncIteratorInterface>
static JS::ThrowCompletionOr<GC::Ptr<JS::Object>> return_(JS::Realm& realm, StringView interface_name, JS::Value value)
{
auto return_promise_capability = WebIDL::create_promise(realm);
auto validation_result = validate_this<AsyncIteratorInterface>(realm, interface_name, return_promise_capability);
return validation_result.visit(
[&](GC::Ref<AsyncIteratorInterface> iterator) {
return iterator->iterator_return_impl(return_promise_capability, value);
},
[](JS::ThrowCompletionOr<GC::Ptr<JS::Object>> result) {
return result;
});
}
protected:
AsyncIterator(JS::Realm&, JS::Object::PropertyKind);
virtual void visit_edges(Cell::Visitor&) override;
virtual GC::Ref<WebIDL::Promise> next_iteration_result(JS::Realm&) = 0;
virtual GC::Ref<WebIDL::Promise> iterator_return(JS::Realm&, JS::Value);
private:
template<typename AsyncIteratorInterface>
static Variant<JS::Completion, GC::Ref<JS::Object>, GC::Ref<AsyncIteratorInterface>> validate_this(JS::Realm& realm, StringView interface_name, GC::Ptr<WebIDL::Promise> this_validation_promise_capability = {})
{
// NOTE: This defines the steps to validate `this` that are common between "next" and "return".
auto& vm = realm.vm();
// 1. Let interface be the interface for which the asynchronous iterator prototype object exists.
// 2. Let thisValidationPromiseCapability be ! NewPromiseCapability(%Promise%).
if (!this_validation_promise_capability)
this_validation_promise_capability = WebIDL::create_promise(realm);
// 3. Let thisValue be the this value.
auto this_value = vm.this_value();
// 4. Let object be Completion(ToObject(thisValue)).
// 5. IfAbruptRejectPromise(object, thisValidationPromiseCapability).
auto object = TRY_OR_REJECT(vm, this_validation_promise_capability, this_value.to_object(vm));
// FIXME: 6. If object is a platform object, then perform a security check, passing:
// * the platform object object,
// * the identifier "next", and
// * the type "method".
//
// If this threw an exception e, then:
// Perform ! Call(thisValidationPromiseCapability.[[Reject]], undefined, « e »).
// Return thisValidationPromiseCapability.[[Promise]].
// 7. If object is not a default asynchronous iterator object for interface, then:
auto* iterator = as_if<AsyncIteratorInterface>(*object);
if (!iterator) {
// 1. Let error be a new TypeError.
auto error = vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, interface_name);
// 2. Perform ! Call(thisValidationPromiseCapability.[[Reject]], undefined, « error »).
// 3. Return thisValidationPromiseCapability.[[Promise]].
TRY_OR_MUST_REJECT(vm, this_validation_promise_capability, error);
VERIFY_NOT_REACHED();
}
return GC::Ref { *iterator };
}
JS::ThrowCompletionOr<GC::Ptr<JS::Object>> iterator_next_impl();
JS::ThrowCompletionOr<GC::Ptr<JS::Object>> iterator_return_impl(GC::Ref<WebIDL::Promise> return_promise_capability, JS::Value);
JS::Object::PropertyKind m_kind { JS::Object::PropertyKind::Value };
GC::Ptr<JS::Promise> m_ongoing_promise;
bool m_is_finished { false };
};
}