diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index 99c89cd4d20..fa662b37338 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -41,6 +41,9 @@ set(SOURCES Runtime/ArrayIterator.cpp Runtime/ArrayIteratorPrototype.cpp Runtime/ArrayPrototype.cpp + Runtime/AsyncDisposableStack.cpp + Runtime/AsyncDisposableStackConstructor.cpp + Runtime/AsyncDisposableStackPrototype.cpp Runtime/AsyncFromSyncIterator.cpp Runtime/AsyncFromSyncIteratorPrototype.cpp Runtime/AsyncFunctionConstructor.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index c60b526cdd8..15fffcdb700 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -21,6 +21,7 @@ __JS_ENUMERATE(AggregateError, aggregate_error, AggregateErrorPrototype, AggregateErrorConstructor, void) \ __JS_ENUMERATE(Array, array, ArrayPrototype, ArrayConstructor, void) \ __JS_ENUMERATE(ArrayBuffer, array_buffer, ArrayBufferPrototype, ArrayBufferConstructor, void) \ + __JS_ENUMERATE(AsyncDisposableStack, async_disposable_stack, AsyncDisposableStackPrototype, AsyncDisposableStackConstructor, void) \ __JS_ENUMERATE(AsyncFunction, async_function, AsyncFunctionPrototype, AsyncFunctionConstructor, void) \ __JS_ENUMERATE(AsyncGeneratorFunction, async_generator_function, AsyncGeneratorFunctionPrototype, AsyncGeneratorFunctionConstructor, void) \ __JS_ENUMERATE(BigIntObject, bigint, BigIntPrototype, BigIntConstructor, void) \ diff --git a/Libraries/LibJS/Runtime/AsyncDisposableStack.cpp b/Libraries/LibJS/Runtime/AsyncDisposableStack.cpp new file mode 100644 index 00000000000..f89028d460b --- /dev/null +++ b/Libraries/LibJS/Runtime/AsyncDisposableStack.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace JS { + +GC_DEFINE_ALLOCATOR(AsyncDisposableStack); + +AsyncDisposableStack::AsyncDisposableStack(DisposeCapability dispose_capability, Object& prototype) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_dispose_capability(move(dispose_capability)) +{ +} + +void AsyncDisposableStack::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + m_dispose_capability.visit_edges(visitor); +} + +} diff --git a/Libraries/LibJS/Runtime/AsyncDisposableStack.h b/Libraries/LibJS/Runtime/AsyncDisposableStack.h new file mode 100644 index 00000000000..faa4cba73af --- /dev/null +++ b/Libraries/LibJS/Runtime/AsyncDisposableStack.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class AsyncDisposableStack final : public Object { + JS_OBJECT(AsyncDisposableStack, Object); + GC_DECLARE_ALLOCATOR(AsyncDisposableStack); + +public: + virtual ~AsyncDisposableStack() override = default; + + enum class AsyncDisposableState { + Pending, + Disposed + }; + + [[nodiscard]] AsyncDisposableState async_disposable_state() const { return m_async_disposable_state; } + void set_disposed() { m_async_disposable_state = AsyncDisposableState::Disposed; } + + [[nodiscard]] DisposeCapability const& dispose_capability() const { return m_dispose_capability; } + [[nodiscard]] DisposeCapability& dispose_capability() { return m_dispose_capability; } + +private: + AsyncDisposableStack(DisposeCapability, Object& prototype); + + virtual void visit_edges(Visitor& visitor) override; + + DisposeCapability m_dispose_capability; + AsyncDisposableState m_async_disposable_state { AsyncDisposableState::Pending }; +}; + +} diff --git a/Libraries/LibJS/Runtime/AsyncDisposableStackConstructor.cpp b/Libraries/LibJS/Runtime/AsyncDisposableStackConstructor.cpp new file mode 100644 index 00000000000..17a184e1976 --- /dev/null +++ b/Libraries/LibJS/Runtime/AsyncDisposableStackConstructor.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS { + +GC_DEFINE_ALLOCATOR(AsyncDisposableStackConstructor); + +AsyncDisposableStackConstructor::AsyncDisposableStackConstructor(Realm& realm) + : NativeFunction(realm.vm().names.AsyncDisposableStack.as_string(), realm.intrinsics().function_prototype()) +{ +} + +void AsyncDisposableStackConstructor::initialize(Realm& realm) +{ + auto& vm = this->vm(); + Base::initialize(realm); + + // 12.4.2.1 AsyncDisposableStack.prototype, https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().async_disposable_stack_prototype(), 0); + + define_direct_property(vm.names.length, Value(0), Attribute::Configurable); +} + +// 12.4.1.1 AsyncDisposableStack ( ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack +ThrowCompletionOr AsyncDisposableStackConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, vm.names.AsyncDisposableStack); +} + +// 12.4.1.1 AsyncDisposableStack ( ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack +ThrowCompletionOr> AsyncDisposableStackConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + // 2. Let asyncDisposableStack be ? OrdinaryCreateFromConstructor(NewTarget, "%AsyncDisposableStack.prototype%", « [[AsyncDisposableState]], [[DisposeCapability]] »). + // 3. Set asyncDisposableStack.[[AsyncDisposableState]] to pending. + // 4. Set asyncDisposableStack.[[DisposeCapability]] to NewDisposeCapability(). + // 5. Return asyncDisposableStack. + return TRY(ordinary_create_from_constructor(vm, new_target, &Intrinsics::async_disposable_stack_prototype, new_dispose_capability())); +} + +} diff --git a/Libraries/LibJS/Runtime/AsyncDisposableStackConstructor.h b/Libraries/LibJS/Runtime/AsyncDisposableStackConstructor.h new file mode 100644 index 00000000000..59854563ee9 --- /dev/null +++ b/Libraries/LibJS/Runtime/AsyncDisposableStackConstructor.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class AsyncDisposableStackConstructor final : public NativeFunction { + JS_OBJECT(AsyncDisposableStackConstructor, NativeFunction); + GC_DECLARE_ALLOCATOR(AsyncDisposableStackConstructor); + +public: + virtual void initialize(Realm&) override; + virtual ~AsyncDisposableStackConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject&) override; + +private: + explicit AsyncDisposableStackConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } +}; + +} diff --git a/Libraries/LibJS/Runtime/AsyncDisposableStackPrototype.cpp b/Libraries/LibJS/Runtime/AsyncDisposableStackPrototype.cpp new file mode 100644 index 00000000000..b206236e56e --- /dev/null +++ b/Libraries/LibJS/Runtime/AsyncDisposableStackPrototype.cpp @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace JS { + +GC_DEFINE_ALLOCATOR(AsyncDisposableStackPrototype); + +AsyncDisposableStackPrototype::AsyncDisposableStackPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().object_prototype()) +{ +} + +void AsyncDisposableStackPrototype::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.adopt, adopt, 2, attr); + define_native_function(realm, vm.names.defer, defer, 1, attr); + define_native_function(realm, vm.names.disposeAsync, dispose_async, 0, attr); + define_native_accessor(realm, vm.names.disposed, disposed_getter, {}, attr); + define_native_function(realm, vm.names.move, move_, 0, attr); + define_native_function(realm, vm.names.use, use, 1, attr); + + // 12.4.3.7 AsyncDisposableStack.prototype [ @@asyncDispose ] (), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype-@@asyncDispose + define_direct_property(vm.well_known_symbol_async_dispose(), get_without_side_effects(vm.names.disposeAsync), attr); + + // 12.4.3.8 AsyncDisposableStack.prototype [ @@toStringTag ], https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype-@@toStringTag + define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, vm.names.AsyncDisposableStack.as_string()), Attribute::Configurable); +} + +// 12.4.3.1 AsyncDisposableStack.prototype.adopt( value, onDisposeAsync ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.adopt +JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::adopt) +{ + auto& realm = *vm.current_realm(); + + auto value = vm.argument(0); + auto on_dispose_async = vm.argument(1); + + // 1. Let asyncDisposableStack be the this value. + // 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]). + auto async_disposable_stack = TRY(typed_this_object(vm)); + + // 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception. + if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) + return vm.throw_completion(ErrorType::AsyncDisposableStackAlreadyDisposed); + + // 4. If IsCallable(onDisposeAsync) is false, throw a TypeError exception. + if (!on_dispose_async.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, on_dispose_async); + + // 5. Let closure be a new Abstract Closure with no parameters that captures value and onDisposeAsync and performs the following steps when called: + auto closure = [value, on_dispose_async](VM& vm) mutable -> ThrowCompletionOr { + // a. Return ? Call(onDisposeAsync, undefined, « value »). + return TRY(call(vm, on_dispose_async.as_function(), js_undefined(), value)); + }; + + // 6. Let F be CreateBuiltinFunction(closure, 0, "", « »). + auto function = NativeFunction::create(realm, move(closure), 0, ""); + + // 7. Perform ? AddDisposableResource(asyncDisposableStack.[[DisposeCapability]], undefined, async-dispose, F). + TRY(add_disposable_resource(vm, async_disposable_stack->dispose_capability(), js_undefined(), Environment::InitializeBindingHint::AsyncDispose, function)); + + // 8. Return value. + return value; +} + +// 12.4.3.2 AsyncDisposableStack.prototype.defer( onDisposeAsync ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.defer +JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::defer) +{ + auto on_dispose_async = vm.argument(0); + + // 1. Let asyncDisposableStack be the this value. + // 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]). + auto async_disposable_stack = TRY(typed_this_object(vm)); + + // 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception. + if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) + return vm.throw_completion(ErrorType::AsyncDisposableStackAlreadyDisposed); + + // 4. If IsCallable(onDisposeAsync) is false, throw a TypeError exception. + if (!on_dispose_async.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, on_dispose_async); + + // 5. Perform ? AddDisposableResource(asyncDisposableStack.[[DisposeCapability]], undefined, async-dispose, onDisposeAsync). + TRY(add_disposable_resource(vm, async_disposable_stack->dispose_capability(), js_undefined(), Environment::InitializeBindingHint::AsyncDispose, on_dispose_async.as_function())); + + // 6. Return undefined. + return js_undefined(); +} + +// 12.4.3.3 AsyncDisposableStack.prototype.disposeAsync(), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.disposeAsync +JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::dispose_async) +{ + auto& realm = *vm.current_realm(); + + // 1. Let asyncDisposableStack be the this value. + auto async_disposable_stack_value = vm.this_value(); + + // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). + auto promise_capability = MUST(new_promise_capability(vm, realm.intrinsics().promise_constructor())); + + // 3. If asyncDisposableStack does not have an [[AsyncDisposableState]] internal slot, then + if (!async_disposable_stack_value.is_object() || !is(async_disposable_stack_value.as_object())) { + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + auto error = TypeError::create(realm, MUST(String::formatted(ErrorType::NotAnObjectOfType.message(), display_name()))); + MUST(call(vm, *promise_capability->reject(), js_undefined(), error)); + + // b. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); + } + + auto& async_disposable_stack = static_cast(async_disposable_stack_value.as_object()); + + // 4. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, then + if (async_disposable_stack.async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) { + // a. Perform ! Call(promiseCapability.[[Resolve]], undefined, « undefined »). + MUST(call(vm, *promise_capability->resolve(), js_undefined(), js_undefined())); + + // b. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); + } + + // 5. Set asyncDisposableStack.[[AsyncDisposableState]] to disposed. + async_disposable_stack.set_disposed(); + + // 6. Let result be DisposeResources(asyncDisposableStack.[[DisposeCapability]], NormalCompletion(undefined)). + // 7. IfAbruptRejectPromise(result, promiseCapability). + auto result = TRY_OR_REJECT(vm, promise_capability, dispose_resources(vm, async_disposable_stack.dispose_capability(), normal_completion(js_undefined()))); + + // 8. Perform ! Call(promiseCapability.[[Resolve]], undefined, « result »). + MUST(call(vm, *promise_capability->resolve(), js_undefined(), *result)); + + // 9. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); +} + +// 12.4.3.4 get AsyncDisposableStack.prototype.disposed, https://tc39.es/proposal-explicit-resource-management/#sec-get-asyncdisposablestack.prototype.disposed +JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::disposed_getter) +{ + // 1. Let asyncDisposableStack be the this value. + // 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]). + auto async_disposable_stack = TRY(typed_this_object(vm)); + + // 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, return true. + if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) + return true; + + // 4. Otherwise, return false. + return false; +} + +// 12.4.3.5 AsyncDisposableStack.prototype.move(), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.move +JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::move_) +{ + auto& realm = *vm.current_realm(); + + // 1. Let asyncDisposableStack be the this value. + // 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]). + auto async_disposable_stack = TRY(typed_this_object(vm)); + + // 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception. + if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) + return vm.throw_completion(ErrorType::AsyncDisposableStackAlreadyDisposed); + + // 4. Let newAsyncDisposableStack be ? OrdinaryCreateFromConstructor(%AsyncDisposableStack%, "%AsyncDisposableStack.prototype%", « [[AsyncDisposableState]], [[DisposeCapability]] »). + // 5. Set newAsyncDisposableStack.[[AsyncDisposableState]] to pending. + // 6. Set newAsyncDisposableStack.[[DisposeCapability]] to asyncDisposableStack.[[DisposeCapability]]. + auto new_async_disposable_stack = TRY(ordinary_create_from_constructor(vm, realm.intrinsics().async_disposable_stack_constructor(), &Intrinsics::async_disposable_stack_prototype, move(async_disposable_stack->dispose_capability()))); + + // 7. Set asyncDisposableStack.[[DisposeCapability]] to NewDisposeCapability(). + async_disposable_stack->dispose_capability() = new_dispose_capability(); + + // 8. Set asyncDisposableStack.[[AsyncDisposableState]] to disposed. + async_disposable_stack->set_disposed(); + + // 9. Return newAsyncDisposableStack. + return new_async_disposable_stack; +} + +// 12.4.3.6 AsyncDisposableStack.prototype.use( value ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.use +JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::use) +{ + auto value = vm.argument(0); + + // 1. Let asyncDisposableStack be the this value. + // 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]). + auto async_disposable_stack = TRY(typed_this_object(vm)); + + // 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception. + if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) + return vm.throw_completion(ErrorType::AsyncDisposableStackAlreadyDisposed); + + // 4. Perform ? AddDisposableResource(asyncDisposableStack.[[DisposeCapability]], value, async-dispose). + TRY(add_disposable_resource(vm, async_disposable_stack->dispose_capability(), value, Environment::InitializeBindingHint::AsyncDispose)); + + // 5. Return value. + return value; +} + +} diff --git a/Libraries/LibJS/Runtime/AsyncDisposableStackPrototype.h b/Libraries/LibJS/Runtime/AsyncDisposableStackPrototype.h new file mode 100644 index 00000000000..712bba568c6 --- /dev/null +++ b/Libraries/LibJS/Runtime/AsyncDisposableStackPrototype.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class AsyncDisposableStackPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(AsyncDisposableStackPrototype, AsyncDisposableStack, AsyncDisposableStack); + GC_DECLARE_ALLOCATOR(AsyncDisposableStackPrototype); + +public: + virtual void initialize(Realm&) override; + virtual ~AsyncDisposableStackPrototype() override = default; + +private: + explicit AsyncDisposableStackPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(adopt); + JS_DECLARE_NATIVE_FUNCTION(defer); + JS_DECLARE_NATIVE_FUNCTION(dispose_async); + JS_DECLARE_NATIVE_FUNCTION(disposed_getter); + JS_DECLARE_NATIVE_FUNCTION(move_); + JS_DECLARE_NATIVE_FUNCTION(use); +}; + +} diff --git a/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Libraries/LibJS/Runtime/CommonPropertyNames.h index 37f0db7f739..9962d778a4e 100644 --- a/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -127,6 +127,7 @@ namespace JS { P(direction) \ P(disabledFeatures) \ P(disambiguation) \ + P(disposeAsync) \ P(disposed) \ P(done) \ P(dotAll) \ diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 510b45f1cb4..be99309b761 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -13,6 +13,7 @@ M(AccessorValueOrWritable, "Accessor property descriptor cannot specify a value or writable key") \ M(AgentCannotSuspend, "Agent is not allowed to suspend") \ M(ArrayMaxSize, "Maximum array size exceeded") \ + M(AsyncDisposableStackAlreadyDisposed, "AsyncDisposableStack is already disposed") \ M(BadArgCountMany, "{}() needs {} arguments") \ M(BadArgCountOne, "{}() needs one argument") \ M(BigIntBadOperator, "Cannot use {} operator with BigInt") \ diff --git a/Libraries/LibJS/Runtime/GlobalObject.cpp b/Libraries/LibJS/Runtime/GlobalObject.cpp index 8d0426eaf8b..6ca80cf8a5b 100644 --- a/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -123,6 +124,7 @@ void set_default_global_bindings(Realm& realm) global.define_intrinsic_accessor(vm.names.AggregateError, attr, [](auto& realm) -> Value { return realm.intrinsics().aggregate_error_constructor(); }); global.define_intrinsic_accessor(vm.names.Array, attr, [](auto& realm) -> Value { return realm.intrinsics().array_constructor(); }); global.define_intrinsic_accessor(vm.names.ArrayBuffer, attr, [](auto& realm) -> Value { return realm.intrinsics().array_buffer_constructor(); }); + global.define_intrinsic_accessor(vm.names.AsyncDisposableStack, attr, [](auto& realm) -> Value { return realm.intrinsics().async_disposable_stack_constructor(); }); global.define_intrinsic_accessor(vm.names.BigInt, attr, [](auto& realm) -> Value { return realm.intrinsics().bigint_constructor(); }); global.define_intrinsic_accessor(vm.names.BigInt64Array, attr, [](auto& realm) -> Value { return realm.intrinsics().big_int64_array_constructor(); }); global.define_intrinsic_accessor(vm.names.BigUint64Array, attr, [](auto& realm) -> Value { return realm.intrinsics().big_uint64_array_constructor(); }); diff --git a/Libraries/LibJS/Runtime/Intrinsics.cpp b/Libraries/LibJS/Runtime/Intrinsics.cpp index 613646c02ae..3058b073e6b 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.js new file mode 100644 index 00000000000..17012cf2aca --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.js @@ -0,0 +1,21 @@ +test("constructor properties", () => { + expect(AsyncDisposableStack).toHaveLength(0); + expect(AsyncDisposableStack.name).toBe("AsyncDisposableStack"); +}); + +describe("errors", () => { + test("called without new", () => { + expect(() => { + AsyncDisposableStack(); + }).toThrowWithMessage( + TypeError, + "AsyncDisposableStack constructor must be called with 'new'" + ); + }); +}); + +describe("normal behavior", () => { + test("typeof", () => { + expect(typeof new AsyncDisposableStack()).toBe("object"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.@@asyncDispose.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.@@asyncDispose.js new file mode 100644 index 00000000000..91212c1c80a --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.@@asyncDispose.js @@ -0,0 +1,9 @@ +test("length is 0", () => { + expect(AsyncDisposableStack.prototype[Symbol.asyncDispose]).toHaveLength(0); +}); + +test("is the same as disposeAsync", () => { + expect(AsyncDisposableStack.prototype[Symbol.asyncDispose]).toBe( + AsyncDisposableStack.prototype.disposeAsync + ); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.@@toStringTag.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.@@toStringTag.js new file mode 100644 index 00000000000..de49915c833 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.@@toStringTag.js @@ -0,0 +1,3 @@ +test("basic functionality", () => { + expect(AsyncDisposableStack.prototype[Symbol.toStringTag]).toBe("AsyncDisposableStack"); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.adopt.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.adopt.js new file mode 100644 index 00000000000..3c5e7ef1451 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.adopt.js @@ -0,0 +1,95 @@ +test("length is 2", () => { + expect(AsyncDisposableStack.prototype.adopt).toHaveLength(2); +}); + +describe("basic functionality", () => { + test("adopted dispose method gets called when stack is disposed", async () => { + const stack = new AsyncDisposableStack(); + let disposedCalled = 0; + let disposeArgument = undefined; + expect(disposedCalled).toBe(0); + const result = stack.adopt(null, arg => { + disposeArgument = arg; + ++disposedCalled; + }); + expect(result).toBeNull(); + + expect(disposedCalled).toBe(0); + await stack.disposeAsync(); + expect(disposedCalled).toBe(1); + expect(disposeArgument).toBeNull(); + await stack.disposeAsync(); + expect(disposedCalled).toBe(1); + }); + + test("can adopt any value", async () => { + const stack = new AsyncDisposableStack(); + const disposed = []; + function dispose(value) { + disposed.push(value); + } + + const values = [null, undefined, 1, "a", Symbol.dispose, () => {}, new WeakMap(), [], {}]; + + values.forEach(value => { + stack.adopt(value, dispose); + }); + + await stack.disposeAsync(); + + expect(disposed).toEqual(values.reverse()); + }); + + test("adopted stack is already disposed", async () => { + const stack = new AsyncDisposableStack(); + stack.adopt(stack, value => { + expect(stack).toBe(value); + expect(stack.disposed).toBeTrue(); + }); + await stack.disposeAsync(); + }); +}); + +describe("throws errors", () => { + test("if call back is not a function throws type error", () => { + const stack = new AsyncDisposableStack(); + [ + 1, + 1n, + "a", + Symbol.dispose, + NaN, + 0, + {}, + [], + { f() {} }, + { [Symbol.dispose]() {} }, + { + get [Symbol.dispose]() { + return () => {}; + }, + }, + ].forEach(value => { + expect(() => stack.adopt(null, value)).toThrowWithMessage(TypeError, "not a function"); + }); + + expect(stack.disposed).toBeFalse(); + }); + + test("adopt throws if stack is already disposed (over type errors)", async () => { + const stack = new AsyncDisposableStack(); + await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + + [{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => { + expect(() => stack.adopt(value, () => {})).toThrowWithMessage( + ReferenceError, + "AsyncDisposableStack already disposed values" + ); + expect(() => stack.adopt(null, value)).toThrowWithMessage( + ReferenceError, + "AsyncDisposableStack already disposed values" + ); + }); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.defer.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.defer.js new file mode 100644 index 00000000000..6c4e78b28ac --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.defer.js @@ -0,0 +1,70 @@ +test("length is 1", () => { + expect(AsyncDisposableStack.prototype.defer).toHaveLength(1); +}); + +describe("basic functionality", () => { + test("deferred function gets called when stack is disposed", async () => { + const stack = new AsyncDisposableStack(); + let disposedCalled = 0; + expect(disposedCalled).toBe(0); + const result = stack.defer((...args) => { + expect(args.length).toBe(0); + ++disposedCalled; + }); + expect(result).toBeUndefined(); + + expect(disposedCalled).toBe(0); + await stack.disposeAsync(); + expect(disposedCalled).toBe(1); + await stack.disposeAsync(); + expect(disposedCalled).toBe(1); + }); + + test("deferred stack is already disposed", async () => { + const stack = new AsyncDisposableStack(); + stack.defer(() => { + expect(stack.disposed).toBeTrue(); + }); + await stack.disposeAsync(); + }); +}); + +describe("throws errors", () => { + test("if call back is not a function throws type error", () => { + const stack = new AsyncDisposableStack(); + [ + 1, + 1n, + "a", + Symbol.dispose, + NaN, + 0, + {}, + [], + { f() {} }, + { [Symbol.dispose]() {} }, + { + get [Symbol.dispose]() { + return () => {}; + }, + }, + ].forEach(value => { + expect(() => stack.defer(value)).toThrowWithMessage(TypeError, "not a function"); + }); + + expect(stack.disposed).toBeFalse(); + }); + + test("defer throws if stack is already disposed (over type errors)", async () => { + const stack = new AsyncDisposableStack(); + await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + + [{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => { + expect(() => stack.defer(value)).toThrowWithMessage( + ReferenceError, + "AsyncDisposableStack already disposed values" + ); + }); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.disposeAsync.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.disposeAsync.js new file mode 100644 index 00000000000..e1ebee619a3 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.disposeAsync.js @@ -0,0 +1,88 @@ +test("length is 0", () => { + expect(AsyncDisposableStack.prototype.disposeAsync).toHaveLength(0); +}); + +describe("basic functionality", () => { + test("make the stack marked as disposed", async () => { + const stack = new AsyncDisposableStack(); + const result = await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + expect(result).toBeUndefined(); + }); + + test("call dispose on objects in stack when called", async () => { + const stack = new AsyncDisposableStack(); + let disposedCalled = false; + stack.use({ + [Symbol.asyncDispose]() { + disposedCalled = true; + }, + }); + + expect(disposedCalled).toBeFalse(); + const result = await stack.disposeAsync(); + expect(disposedCalled).toBeTrue(); + expect(result).toBeUndefined(); + }); + + test("disposed the objects added to the stack in reverse order", async () => { + const disposed = []; + const stack = new AsyncDisposableStack(); + stack.use({ + [Symbol.asyncDispose]() { + disposed.push("a"); + }, + }); + stack.use({ + [Symbol.asyncDispose]() { + disposed.push("b"); + }, + }); + + expect(disposed).toEqual([]); + const result = await stack.disposeAsync(); + expect(disposed).toEqual(["b", "a"]); + expect(result).toBeUndefined(); + }); + + test("does not dispose anything if already disposed", async () => { + const disposed = []; + const stack = new AsyncDisposableStack(); + stack.use({ + [Symbol.asyncDispose]() { + disposed.push("a"); + }, + }); + + expect(stack.disposed).toBeFalse(); + expect(disposed).toEqual([]); + + let result = await stack.disposeAsync(); + expect(result).toBeUndefined(); + + expect(stack.disposed).toBeTrue(); + expect(disposed).toEqual(["a"]); + + result = await stack.disposeAsync(); + expect(result).toBeUndefined(); + + expect(stack.disposed).toBeTrue(); + expect(disposed).toEqual(["a"]); + }); + + test("throws if dispose method throws", async () => { + const stack = new AsyncDisposableStack(); + let disposedCalled = false; + stack.use({ + [Symbol.asyncDispose]() { + disposedCalled = true; + expect().fail("fail in dispose"); + }, + }); + + expect(async () => await stack.disposeAsync()).toThrowWithMessage( + ExpectationError, + "fail in dispose" + ); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.disposed.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.disposed.js new file mode 100644 index 00000000000..8811ce1d662 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.disposed.js @@ -0,0 +1,24 @@ +test("is getter without setter", () => { + const property = Object.getOwnPropertyDescriptor(AsyncDisposableStack.prototype, "disposed"); + expect(property.get).not.toBeUndefined(); + expect(property.set).toBeUndefined(); + expect(property.value).toBeUndefined(); +}); + +describe("basic functionality", () => { + test("is not a property on the object itself", () => { + const stack = new AsyncDisposableStack(); + expect(Object.hasOwn(stack, "disposed")).toBeFalse(); + }); + + test("starts off as false", () => { + const stack = new AsyncDisposableStack(); + expect(stack.disposed).toBeFalse(); + }); + + test("becomes true after being disposed", async () => { + const stack = new AsyncDisposableStack(); + await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.move.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.move.js new file mode 100644 index 00000000000..6ef47f91f9c --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.move.js @@ -0,0 +1,62 @@ +test("length is 0", () => { + expect(AsyncDisposableStack.prototype.move).toHaveLength(0); +}); + +describe("basic functionality", () => { + test("stack is disposed after moving", () => { + const stack = new AsyncDisposableStack(); + + const newStack = stack.move(); + + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeFalse(); + }); + + test("move does not dispose resource but only move them", async () => { + const stack = new AsyncDisposableStack(); + let disposeCalled = false; + stack.defer(() => { + disposeCalled = true; + }); + + expect(disposeCalled).toBeFalse(); + expect(stack.disposed).toBeFalse(); + + const newStack = stack.move(); + + expect(disposeCalled).toBeFalse(); + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeFalse(); + + await stack.disposeAsync(); + + expect(disposeCalled).toBeFalse(); + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeFalse(); + + await newStack.disposeAsync(); + + expect(disposeCalled).toBeTrue(); + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeTrue(); + }); + + test("can add stack to itself", async () => { + const stack = new AsyncDisposableStack(); + stack.move(stack); + await stack.disposeAsync(); + }); +}); + +describe("throws errors", () => { + test("move throws if stack is already disposed (over type errors)", async () => { + const stack = new AsyncDisposableStack(); + await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + + expect(() => stack.move()).toThrowWithMessage( + ReferenceError, + "AsyncDisposableStack already disposed values" + ); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.use.js b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.use.js new file mode 100644 index 00000000000..d800b631650 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/AsyncDisposableStack/AsyncDisposableStack.prototype.use.js @@ -0,0 +1,96 @@ +test("length is 1", () => { + expect(AsyncDisposableStack.prototype.use).toHaveLength(1); +}); + +describe("basic functionality", () => { + test("added objects dispose method gets when stack is disposed", async () => { + const stack = new AsyncDisposableStack(); + let disposedCalled = 0; + const obj = { + [Symbol.dispose]() { + ++disposedCalled; + }, + }; + expect(disposedCalled).toBe(0); + const result = stack.use(obj); + expect(result).toBe(obj); + + expect(disposedCalled).toBe(0); + await stack.disposeAsync(); + expect(disposedCalled).toBe(1); + await stack.disposeAsync(); + expect(disposedCalled).toBe(1); + }); + + test("can add null and undefined", async () => { + const stack = new AsyncDisposableStack(); + + expect(stack.use(null)).toBeNull(); + expect(stack.use(undefined)).toBeUndefined(); + + expect(stack.disposed).toBeFalse(); + await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + }); + + test("can add stack to itself", async () => { + const stack = new AsyncDisposableStack(); + stack.use(stack); + await stack.disposeAsync(); + }); +}); + +describe("throws errors", () => { + test("if added value is not an object or null or undefined throws type error", () => { + const stack = new AsyncDisposableStack(); + [1, 1n, "a", Symbol.dispose, NaN, 0].forEach(value => { + expect(() => stack.use(value)).toThrowWithMessage(TypeError, "not an object"); + }); + + expect(stack.disposed).toBeFalse(); + }); + + test("if added object does not have a dispose method throws type error", () => { + const stack = new AsyncDisposableStack(); + [{}, [], { f() {} }].forEach(value => { + expect(() => stack.use(value)).toThrowWithMessage( + TypeError, + "does not have dispose method" + ); + }); + + expect(stack.disposed).toBeFalse(); + }); + + test("if added object has non function dispose method it throws type error", () => { + const stack = new AsyncDisposableStack(); + let calledGetter = false; + [ + { [Symbol.dispose]: 1 }, + { + get [Symbol.dispose]() { + calledGetter = true; + return 1; + }, + }, + ].forEach(value => { + expect(() => stack.use(value)).toThrowWithMessage(TypeError, "is not a function"); + }); + + expect(stack.disposed).toBeFalse(); + expect(calledGetter).toBeTrue(); + }); + + test("use throws if stack is already disposed (over type errors)", async () => { + const stack = new AsyncDisposableStack(); + await stack.disposeAsync(); + expect(stack.disposed).toBeTrue(); + + [{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => { + expect(() => stack.use(value)).toThrowWithMessage( + ReferenceError, + "AsyncDisposableStack already disposed values" + ); + }); + }); +}); diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index e24b4682d2d..3ec6a049a16 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -10,6 +10,7 @@ AnimationPlaybackEvent AnimationTimeline Array ArrayBuffer +AsyncDisposableStack Attr Audio AudioBuffer