From 04b432a092f13e63a50af2bb4ac0cfd3a935be69 Mon Sep 17 00:00:00 2001 From: davidot Date: Wed, 21 Dec 2022 11:48:14 +0100 Subject: [PATCH] LibJS: Add DisposableStack{, Prototype, Constructor} Since the async parts of the spec are not stage 3 at this point we don't add AsyncDisposableStack. --- .prettierignore | 1 + Userland/Libraries/LibJS/CMakeLists.txt | 3 + Userland/Libraries/LibJS/Forward.h | 1 + .../LibJS/Runtime/CommonPropertyNames.h | 5 + .../LibJS/Runtime/DisposableStack.cpp | 26 +++ .../Libraries/LibJS/Runtime/DisposableStack.h | 40 ++++ .../Runtime/DisposableStackConstructor.cpp | 50 +++++ .../Runtime/DisposableStackConstructor.h | 29 +++ .../Runtime/DisposableStackPrototype.cpp | 206 ++++++++++++++++++ .../LibJS/Runtime/DisposableStackPrototype.h | 32 +++ Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 + .../Libraries/LibJS/Runtime/GlobalObject.cpp | 2 + .../Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../DisposableStack/DisposableStack.js | 18 ++ .../DisposableStack.prototype.@@dispose.js | 19 ++ ...DisposableStack.prototype.@@toStringTag.js | 3 + .../DisposableStack.prototype.adopt.js | 95 ++++++++ .../DisposableStack.prototype.defer.js | 70 ++++++ .../DisposableStack.prototype.dispose.js | 83 +++++++ .../DisposableStack.prototype.disposed.js | 24 ++ .../DisposableStack.prototype.move.js | 62 ++++++ .../DisposableStack.prototype.use.js | 96 ++++++++ 22 files changed, 868 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/DisposableStack.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/DisposableStack.h create mode 100644 Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.h create mode 100644 Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@toStringTag.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.adopt.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.defer.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.dispose.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.disposed.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.move.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.use.js diff --git a/.prettierignore b/.prettierignore index 3f5155996bb6fe..0d31829d6e4636 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ Userland/Libraries/LibJS/Tests/unicode-identifier-escape.js Userland/Libraries/LibJS/Tests/modules/failing.mjs # FIXME: Remove once prettier is updated to support using declarations. +Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs Userland/Libraries/LibJS/Tests/using-declaration.js Userland/Libraries/LibJS/Tests/using-for-loops.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 9650dc20826df5..e7b0ccdceb0577 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -75,6 +75,9 @@ set(SOURCES Runtime/DateConstructor.cpp Runtime/DatePrototype.cpp Runtime/DeclarativeEnvironment.cpp + Runtime/DisposableStack.cpp + Runtime/DisposableStackConstructor.cpp + Runtime/DisposableStackPrototype.cpp Runtime/ECMAScriptFunctionObject.cpp Runtime/Environment.cpp Runtime/Error.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index c5938e98fc2db4..2df9320ee4a787 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -27,6 +27,7 @@ __JS_ENUMERATE(BooleanObject, boolean, BooleanPrototype, BooleanConstructor, void) \ __JS_ENUMERATE(DataView, data_view, DataViewPrototype, DataViewConstructor, void) \ __JS_ENUMERATE(Date, date, DatePrototype, DateConstructor, void) \ + __JS_ENUMERATE(DisposableStack, disposable_stack, DisposableStackPrototype, DisposableStackConstructor, void) \ __JS_ENUMERATE(Error, error, ErrorPrototype, ErrorConstructor, void) \ __JS_ENUMERATE(FinalizationRegistry, finalization_registry, FinalizationRegistryPrototype, FinalizationRegistryConstructor, void) \ __JS_ENUMERATE(FunctionObject, function, FunctionPrototype, FunctionConstructor, void) \ diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index ed742088a0f504..90c2449a98a4c7 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -65,6 +65,7 @@ namespace JS { P(acos) \ P(acosh) \ P(add) \ + P(adopt) \ P(all) \ P(allSettled) \ P(anchor) \ @@ -143,6 +144,7 @@ namespace JS { P(debug) \ P(decodeURI) \ P(decodeURIComponent) \ + P(defer) \ P(defineProperties) \ P(defineProperty) \ P(deleteProperty) \ @@ -151,6 +153,7 @@ namespace JS { P(difference) \ P(direction) \ P(disambiguation) \ + P(disposed) \ P(done) \ P(dotAll) \ P(encodeURI) \ @@ -365,6 +368,7 @@ namespace JS { P(months) \ P(monthsDisplay) \ P(monthsInYear) \ + P(move) \ P(multiline) \ P(name) \ P(nanosecond) \ @@ -555,6 +559,7 @@ namespace JS { P(unshift) \ P(until) \ P(usage) \ + P(use) \ P(useGrouping) \ P(value) \ P(valueOf) \ diff --git a/Userland/Libraries/LibJS/Runtime/DisposableStack.cpp b/Userland/Libraries/LibJS/Runtime/DisposableStack.cpp new file mode 100644 index 00000000000000..e54ad354b00b80 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/DisposableStack.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace JS { + +DisposableStack::DisposableStack(Vector stack, Object& prototype) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_disposable_resource_stack(move(stack)) +{ +} + +void DisposableStack::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + for (auto& resource : m_disposable_resource_stack) { + visitor.visit(resource.resource_value); + visitor.visit(resource.dispose_method); + } +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/DisposableStack.h b/Userland/Libraries/LibJS/Runtime/DisposableStack.h new file mode 100644 index 00000000000000..77b846f5595453 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/DisposableStack.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class DisposableStack final : public Object { + JS_OBJECT(DisposableStack, Object); + +public: + virtual ~DisposableStack() override = default; + + enum class DisposableState { + Pending, + Disposed + }; + + [[nodiscard]] DisposableState disposable_state() const { return m_state; } + [[nodiscard]] Vector const& disposable_resource_stack() const { return m_disposable_resource_stack; } + [[nodiscard]] Vector& disposable_resource_stack() { return m_disposable_resource_stack; } + + void set_disposed() { m_state = DisposableState::Disposed; } + +private: + DisposableStack(Vector stack, Object& prototype); + + virtual void visit_edges(Visitor& visitor) override; + + Vector m_disposable_resource_stack; + DisposableState m_state { DisposableState::Pending }; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.cpp b/Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.cpp new file mode 100644 index 00000000000000..d70786d5a61b37 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS { + +DisposableStackConstructor::DisposableStackConstructor(Realm& realm) + : NativeFunction(realm.vm().names.DisposableStack.as_string(), *realm.intrinsics().function_prototype()) +{ +} + +void DisposableStackConstructor::initialize(Realm& realm) +{ + auto& vm = this->vm(); + NativeFunction::initialize(realm); + + // 26.2.2.1 DisposableStack.prototype, https://tc39.es/ecma262/#sec-finalization-registry.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().disposable_stack_prototype(), 0); + + define_direct_property(vm.names.length, Value(0), Attribute::Configurable); +} + +// 11.3.1.1 DisposableStack ( ), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack +ThrowCompletionOr DisposableStackConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, vm.names.DisposableStack); +} + +// 11.3.1.1 DisposableStack ( ), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack +ThrowCompletionOr> DisposableStackConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + // 2. Let disposableStack be ? OrdinaryCreateFromConstructor(NewTarget, "%DisposableStack.prototype%", « [[DisposableState]], [[DisposableResourceStack]] »). + // 3. Set disposableStack.[[DisposableState]] to pending. + // 4. Set disposableStack.[[DisposableResourceStack]] to a new empty List. + // 5. Return disposableStack. + return TRY(ordinary_create_from_constructor(vm, new_target, &Intrinsics::disposable_stack_prototype, Vector {})); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.h b/Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.h new file mode 100644 index 00000000000000..b512657a368475 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/DisposableStackConstructor.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class DisposableStackConstructor final : public NativeFunction { + JS_OBJECT(DisposableStackConstructor, NativeFunction); + +public: + virtual void initialize(Realm&) override; + virtual ~DisposableStackConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject&) override; + +private: + explicit DisposableStackConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.cpp b/Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.cpp new file mode 100644 index 00000000000000..9ba270ce15d655 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS { + +DisposableStackPrototype::DisposableStackPrototype(Realm& realm) + : PrototypeObject(*realm.intrinsics().object_prototype()) +{ +} + +void DisposableStackPrototype::initialize(Realm& realm) +{ + auto& vm = this->vm(); + Object::initialize(realm); + u8 attr = Attribute::Writable | Attribute::Configurable; + + define_native_accessor(realm, vm.names.disposed, disposed_getter, {}, attr); + define_native_function(realm, vm.names.dispose, dispose, 0, attr); + define_native_function(realm, vm.names.use, use, 1, attr); + 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.move, move_, 0, attr); + + // 11.3.3.7 DisposableStack.prototype [ @@dispose ] (), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype-@@dispose + define_direct_property(*vm.well_known_symbol_dispose(), get_without_side_effects(vm.names.dispose), attr); + + // 11.3.3.8 DisposableStack.prototype [ @@toStringTag ], https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype-@@toStringTag + define_direct_property(*vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, vm.names.DisposableStack.as_string()), Attribute::Configurable); +} + +// 11.3.3.1 get DisposableStack.prototype.disposed, https://tc39.es/proposal-explicit-resource-management/#sec-get-disposablestack.prototype.disposed +JS_DEFINE_NATIVE_FUNCTION(DisposableStackPrototype::disposed_getter) +{ + // 1. Let disposableStack be the this value. + // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]). + auto* disposable_stack = TRY(typed_this_object(vm)); + + // 3. If disposableStack.[[DisposableState]] is disposed, return true. + if (disposable_stack->disposable_state() == DisposableStack::DisposableState::Disposed) + return Value(true); + + // 4. Otherwise, return false. + return Value(false); +} + +// 11.3.3.2 DisposableStack.prototype.dispose (), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype.dispose +JS_DEFINE_NATIVE_FUNCTION(DisposableStackPrototype::dispose) +{ + // 1. Let disposableStack be the this value. + // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]). + auto* disposable_stack = TRY(typed_this_object(vm)); + + // 3. If disposableStack.[[DisposableState]] is disposed, return undefined. + if (disposable_stack->disposable_state() == DisposableStack::DisposableState::Disposed) + return js_undefined(); + + // 4. Set disposableStack.[[DisposableState]] to disposed. + disposable_stack->set_disposed(); + + // 5. Return DisposeResources(disposableStack, NormalCompletion(undefined)). + return TRY(dispose_resources(vm, disposable_stack->disposable_resource_stack(), Completion { js_undefined() })); +} + +// 11.3.3.3 DisposableStack.prototype.use( value ), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype.use +JS_DEFINE_NATIVE_FUNCTION(DisposableStackPrototype::use) +{ + auto value = vm.argument(0); + + // 1. Let disposableStack be the this value. + // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]). + auto* disposable_stack = TRY(typed_this_object(vm)); + + // 3. If disposableStack.[[DisposableState]] is disposed, throw a ReferenceError exception. + if (disposable_stack->disposable_state() == DisposableStack::DisposableState::Disposed) + return vm.throw_completion(ErrorType::DisposableStackAlreadyDisposed); + + // 4. If value is neither null nor undefined, then + if (!value.is_nullish()) { + // a. If Type(value) is not Object, throw a TypeError exception. + if (!value.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, value.to_string_without_side_effects()); + + // FIXME: This should be TRY in the spec + // b. Let method be GetDisposeMethod(value, sync-dispose). + auto method = TRY(get_dispose_method(vm, value, Environment::InitializeBindingHint::SyncDispose)); + + // c. If method is undefined, then + if (!method.ptr()) { + // i. Throw a TypeError exception. + return vm.throw_completion(ErrorType::NoDisposeMethod, value.to_string_without_side_effects()); + } + // d. Else, + // i. Perform ? AddDisposableResource(disposableStack, value, sync-dispose, method). + add_disposable_resource(vm, disposable_stack->disposable_resource_stack(), value, Environment::InitializeBindingHint::SyncDispose, method); + } + + // 5. Return value. + return value; +} + +// 11.3.3.4 DisposableStack.prototype.adopt( value, onDispose ), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype.adopt +JS_DEFINE_NATIVE_FUNCTION(DisposableStackPrototype::adopt) +{ + auto& realm = *vm.current_realm(); + + auto value = vm.argument(0); + auto on_dispose = vm.argument(1); + + // 1. Let disposableStack be the this value. + // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]). + auto* disposable_stack = TRY(typed_this_object(vm)); + + // 3. If disposableStack.[[DisposableState]] is disposed, throw a ReferenceError exception. + if (disposable_stack->disposable_state() == DisposableStack::DisposableState::Disposed) + return vm.throw_completion(ErrorType::DisposableStackAlreadyDisposed); + + // 4. If IsCallable(onDispose) is false, throw a TypeError exception. + if (!on_dispose.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, on_dispose.to_string_without_side_effects()); + + // 5. Let F be a new built-in function object as defined in 11.3.3.4.1. + // 6. Set F.[[Argument]] to value. + // 7. Set F.[[OnDisposeCallback]] to onDispose. + // 11.3.3.4.1 DisposableStack Adopt Callback Functions, https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack-adopt-callback-functions + // A DisposableStack adopt callback function is an anonymous built-in function object that has [[Argument]] and [[OnDisposeCallback]] internal slots. + auto function = NativeFunction::create( + realm, [argument = make_handle(value), callback = make_handle(on_dispose)](VM& vm) { + // When a DisposableStack adopt callback function is called, the following steps are taken: + // 1. Let F be the active function object. + // 2. Assert: IsCallable(F.[[OnDisposeCallback]]) is true. + VERIFY(callback.value().is_function()); + + // 3. Return Call(F.[[OnDisposeCallback]], undefined, « F.[[Argument]] »). + return call(vm, callback.value(), js_undefined(), argument.value()); + }, + 0, ""); + + // 8. Perform ? AddDisposableResource(disposableStack, undefined, sync-dispose, F). + TRY(add_disposable_resource(vm, disposable_stack->disposable_resource_stack(), js_undefined(), Environment::InitializeBindingHint::SyncDispose, function)); + + // 9. Return value. + return value; +} + +// 11.3.3.5 DisposableStack.prototype.defer( onDispose ), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype.defer +JS_DEFINE_NATIVE_FUNCTION(DisposableStackPrototype::defer) +{ + auto on_dispose = vm.argument(0); + + // 1. Let disposableStack be the this value. + // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]). + auto* disposable_stack = TRY(typed_this_object(vm)); + + // 3. If disposableStack.[[DisposableState]] is disposed, throw a ReferenceError exception. + if (disposable_stack->disposable_state() == DisposableStack::DisposableState::Disposed) + return vm.throw_completion(ErrorType::DisposableStackAlreadyDisposed); + + // 4. If IsCallable(onDispose) is false, throw a TypeError exception. + if (!on_dispose.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, on_dispose.to_string_without_side_effects()); + + // 5. Perform ? AddDisposableResource(disposableStack, undefined, sync-dispose, onDispose). + TRY(add_disposable_resource(vm, disposable_stack->disposable_resource_stack(), js_undefined(), Environment::InitializeBindingHint::SyncDispose, &on_dispose.as_function())); + + // 6. Return undefined. + return js_undefined(); +} + +// 11.3.3.6 DisposableStack.prototype.move(), https://tc39.es/proposal-explicit-resource-management/#sec-disposablestack.prototype.move +JS_DEFINE_NATIVE_FUNCTION(DisposableStackPrototype::move_) +{ + // 1. Let disposableStack be the this value. + // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]). + auto* disposable_stack = TRY(typed_this_object(vm)); + + // 3. If disposableStack.[[DisposableState]] is disposed, throw a ReferenceError exception. + if (disposable_stack->disposable_state() == DisposableStack::DisposableState::Disposed) + return vm.throw_completion(ErrorType::DisposableStackAlreadyDisposed); + + // 4. Let newDisposableStack be ? OrdinaryCreateFromConstructor(%DisposableStack%, "%DisposableStack.prototype%", « [[DisposableState]], [[DisposableResourceStack]] »). + auto new_disposable_stack = TRY(ordinary_create_from_constructor(vm, *vm.current_realm()->intrinsics().disposable_stack_constructor(), &Intrinsics::disposable_stack_prototype, disposable_stack->disposable_resource_stack())); + + // 5. Set newDisposableStack.[[DisposableState]] to pending. + // 6. Set newDisposableStack.[[DisposableResourceStack]] to disposableStack.[[DisposableResourceStack]]. + // NOTE: Already done in the constructor + + // 7. Set disposableStack.[[DisposableResourceStack]] to a new empty List. + disposable_stack->disposable_resource_stack().clear(); + + // 8. Set disposableStack.[[DisposableState]] to disposed. + disposable_stack->set_disposed(); + + // 9. Return newDisposableStack. + return new_disposable_stack; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.h b/Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.h new file mode 100644 index 00000000000000..6b093a12aab5d1 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/DisposableStackPrototype.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class DisposableStackPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(DisposableStackPrototype, DisposableStack, DisposableStack); + +public: + virtual void initialize(Realm&) override; + virtual ~DisposableStackPrototype() override = default; + +private: + explicit DisposableStackPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(disposed_getter); + JS_DECLARE_NATIVE_FUNCTION(dispose); + JS_DECLARE_NATIVE_FUNCTION(use); + JS_DECLARE_NATIVE_FUNCTION(adopt); + JS_DECLARE_NATIVE_FUNCTION(defer); + JS_DECLARE_NATIVE_FUNCTION(move_); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 6e2e07d2e0a661..3c169ccca424b6 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -32,6 +32,7 @@ M(DescWriteNonWritable, "Cannot write to non-writable property '{}'") \ M(DetachedArrayBuffer, "ArrayBuffer is detached") \ M(DetachKeyMismatch, "Provided detach key {} does not match the ArrayBuffer's detach key {}") \ + M(DisposableStackAlreadyDisposed, "DisposableStack already disposed values") \ M(DivisionByZero, "Division by zero") \ M(DynamicImportNotAllowed, "Dynamic Imports are not allowed") \ M(FinalizationRegistrySameTargetAndValue, "Target and held value must not be the same") \ diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index 73a925d3f168b8..42b47836f74216 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -135,6 +136,7 @@ Object& set_default_global_bindings(Realm& realm) global.define_intrinsic_accessor(vm.names.Boolean, attr, [](auto& realm) -> Value { return realm.intrinsics().boolean_constructor(); }); global.define_intrinsic_accessor(vm.names.DataView, attr, [](auto& realm) -> Value { return realm.intrinsics().data_view_constructor(); }); global.define_intrinsic_accessor(vm.names.Date, attr, [](auto& realm) -> Value { return realm.intrinsics().date_constructor(); }); + global.define_intrinsic_accessor(vm.names.DisposableStack, attr, [](auto& realm) -> Value { return realm.intrinsics().disposable_stack_constructor(); }); global.define_intrinsic_accessor(vm.names.Error, attr, [](auto& realm) -> Value { return realm.intrinsics().error_constructor(); }); global.define_intrinsic_accessor(vm.names.EvalError, attr, [](auto& realm) -> Value { return realm.intrinsics().eval_error_constructor(); }); global.define_intrinsic_accessor(vm.names.FinalizationRegistry, attr, [](auto& realm) -> Value { return realm.intrinsics().finalization_registry_constructor(); }); diff --git a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp index 3d229eaa5e36c9..feac0524d46381 100644 --- a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.js new file mode 100644 index 00000000000000..652ffab02f4f69 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.js @@ -0,0 +1,18 @@ +test("constructor properties", () => { + expect(DisposableStack).toHaveLength(0); + expect(DisposableStack.name).toBe("DisposableStack"); +}); + +describe("errors", () => { + test("called without new", () => { + expect(() => { + DisposableStack(); + }).toThrowWithMessage(TypeError, "DisposableStack constructor must be called with 'new'"); + }); +}); + +describe("normal behavior", () => { + test("typeof", () => { + expect(typeof new DisposableStack()).toBe("object"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js new file mode 100644 index 00000000000000..62c76f8e7702ee --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js @@ -0,0 +1,19 @@ +test("length is 0", () => { + expect(DisposableStack.prototype[Symbol.dispose]).toHaveLength(0); +}); + +test("is the same as dispose", () => { + expect(DisposableStack.prototype[Symbol.dispose]).toBe(DisposableStack.prototype.dispose); +}); + +describe("used in using functionality", () => { + test("make the stack marked as disposed", () => { + let innerStack; + { + using stack = new DisposableStack(); + innerStack = stack; + expect(stack.disposed).toBeFalse(); + } + expect(innerStack.disposed).toBeTrue(); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@toStringTag.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@toStringTag.js new file mode 100644 index 00000000000000..1578a6f556320e --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@toStringTag.js @@ -0,0 +1,3 @@ +test("basic functionality", () => { + expect(DisposableStack.prototype[Symbol.toStringTag]).toBe("DisposableStack"); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.adopt.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.adopt.js new file mode 100644 index 00000000000000..e881daea9a94c5 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.adopt.js @@ -0,0 +1,95 @@ +test("length is 2", () => { + expect(DisposableStack.prototype.adopt).toHaveLength(2); +}); + +describe("basic functionality", () => { + test("adopted dispose method gets called when stack is disposed", () => { + const stack = new DisposableStack(); + 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); + stack.dispose(); + expect(disposedCalled).toBe(1); + expect(disposeArgument).toBeNull(); + stack.dispose(); + expect(disposedCalled).toBe(1); + }); + + test("can adopt any value", () => { + const stack = new DisposableStack(); + 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); + }); + + stack.dispose(); + + expect(disposed).toEqual(values.reverse()); + }); + + test("adopted stack is already disposed", () => { + const stack = new DisposableStack(); + stack.adopt(stack, value => { + expect(stack).toBe(value); + expect(stack.disposed).toBeTrue(); + }); + stack.dispose(); + }); +}); + +describe("throws errors", () => { + test("if call back is not a function throws type error", () => { + const stack = new DisposableStack(); + [ + 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)", () => { + const stack = new DisposableStack(); + stack.dispose(); + expect(stack.disposed).toBeTrue(); + + [{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => { + expect(() => stack.adopt(value, () => {})).toThrowWithMessage( + ReferenceError, + "DisposableStack already disposed values" + ); + expect(() => stack.adopt(null, value)).toThrowWithMessage( + ReferenceError, + "DisposableStack already disposed values" + ); + }); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.defer.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.defer.js new file mode 100644 index 00000000000000..452b66c296bf89 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.defer.js @@ -0,0 +1,70 @@ +test("length is 1", () => { + expect(DisposableStack.prototype.defer).toHaveLength(1); +}); + +describe("basic functionality", () => { + test("deferred function gets called when stack is disposed", () => { + const stack = new DisposableStack(); + 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); + stack.dispose(); + expect(disposedCalled).toBe(1); + stack.dispose(); + expect(disposedCalled).toBe(1); + }); + + test("deferred stack is already disposed", () => { + const stack = new DisposableStack(); + stack.defer(() => { + expect(stack.disposed).toBeTrue(); + }); + stack.dispose(); + }); +}); + +describe("throws errors", () => { + test("if call back is not a function throws type error", () => { + const stack = new DisposableStack(); + [ + 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)", () => { + const stack = new DisposableStack(); + stack.dispose(); + expect(stack.disposed).toBeTrue(); + + [{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => { + expect(() => stack.defer(value)).toThrowWithMessage( + ReferenceError, + "DisposableStack already disposed values" + ); + }); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.dispose.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.dispose.js new file mode 100644 index 00000000000000..5229caa47405e6 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.dispose.js @@ -0,0 +1,83 @@ +test("length is 0", () => { + expect(DisposableStack.prototype.dispose).toHaveLength(0); +}); + +describe("basic functionality", () => { + test("make the stack marked as disposed", () => { + const stack = new DisposableStack(); + const result = stack.dispose(); + expect(stack.disposed).toBeTrue(); + expect(result).toBeUndefined(); + }); + + test("call dispose on objects in stack when called", () => { + const stack = new DisposableStack(); + let disposedCalled = false; + stack.use({ + [Symbol.dispose]() { + disposedCalled = true; + }, + }); + + expect(disposedCalled).toBeFalse(); + const result = stack.dispose(); + expect(disposedCalled).toBeTrue(); + expect(result).toBeUndefined(); + }); + + test("disposed the objects added to the stack in reverse order", () => { + const disposed = []; + const stack = new DisposableStack(); + stack.use({ + [Symbol.dispose]() { + disposed.push("a"); + }, + }); + stack.use({ + [Symbol.dispose]() { + disposed.push("b"); + }, + }); + + expect(disposed).toEqual([]); + const result = stack.dispose(); + expect(disposed).toEqual(["b", "a"]); + expect(result).toBeUndefined(); + }); + + test("does not dispose anything if already disposed", () => { + const disposed = []; + const stack = new DisposableStack(); + stack.use({ + [Symbol.dispose]() { + disposed.push("a"); + }, + }); + + expect(stack.disposed).toBeFalse(); + expect(disposed).toEqual([]); + + expect(stack.dispose()).toBeUndefined(); + + expect(stack.disposed).toBeTrue(); + expect(disposed).toEqual(["a"]); + + expect(stack.dispose()).toBeUndefined(); + + expect(stack.disposed).toBeTrue(); + expect(disposed).toEqual(["a"]); + }); + + test("throws if dispose method throws", () => { + const stack = new DisposableStack(); + let disposedCalled = false; + stack.use({ + [Symbol.dispose]() { + disposedCalled = true; + expect().fail("fail in dispose"); + }, + }); + + expect(() => stack.dispose()).toThrowWithMessage(ExpectationError, "fail in dispose"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.disposed.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.disposed.js new file mode 100644 index 00000000000000..e1232749153023 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.disposed.js @@ -0,0 +1,24 @@ +test("is getter without setter", () => { + const property = Object.getOwnPropertyDescriptor(DisposableStack.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 DisposableStack(); + expect(Object.hasOwn(stack, "disposed")).toBeFalse(); + }); + + test("starts off as false", () => { + const stack = new DisposableStack(); + expect(stack.disposed).toBeFalse(); + }); + + test("becomes true after being disposed", () => { + const stack = new DisposableStack(); + stack.dispose(); + expect(stack.disposed).toBeTrue(); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.move.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.move.js new file mode 100644 index 00000000000000..9ed2a1f140ae69 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.move.js @@ -0,0 +1,62 @@ +test("length is 0", () => { + expect(DisposableStack.prototype.move).toHaveLength(0); +}); + +describe("basic functionality", () => { + test("stack is disposed after moving", () => { + const stack = new DisposableStack(); + + const newStack = stack.move(); + + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeFalse(); + }); + + test("move does not dispose resource but only move them", () => { + const stack = new DisposableStack(); + 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(); + + stack.dispose(); + + expect(disposeCalled).toBeFalse(); + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeFalse(); + + newStack.dispose(); + + expect(disposeCalled).toBeTrue(); + expect(stack.disposed).toBeTrue(); + expect(newStack.disposed).toBeTrue(); + }); + + test("can add stack to itself", () => { + const stack = new DisposableStack(); + stack.move(stack); + stack.dispose(); + }); +}); + +describe("throws errors", () => { + test("move throws if stack is already disposed (over type errors)", () => { + const stack = new DisposableStack(); + stack.dispose(); + expect(stack.disposed).toBeTrue(); + + expect(() => stack.move()).toThrowWithMessage( + ReferenceError, + "DisposableStack already disposed values" + ); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.use.js b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.use.js new file mode 100644 index 00000000000000..77224b130c5316 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.use.js @@ -0,0 +1,96 @@ +test("length is 1", () => { + expect(DisposableStack.prototype.use).toHaveLength(1); +}); + +describe("basic functionality", () => { + test("added objects dispose method gets when stack is disposed", () => { + const stack = new DisposableStack(); + 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); + stack.dispose(); + expect(disposedCalled).toBe(1); + stack.dispose(); + expect(disposedCalled).toBe(1); + }); + + test("can add null and undefined", () => { + const stack = new DisposableStack(); + + expect(stack.use(null)).toBeNull(); + expect(stack.use(undefined)).toBeUndefined(); + + expect(stack.disposed).toBeFalse(); + stack.dispose(); + expect(stack.disposed).toBeTrue(); + }); + + test("can add stack to itself", () => { + const stack = new DisposableStack(); + stack.use(stack); + stack.dispose(); + }); +}); + +describe("throws errors", () => { + test("if added value is not an object or null or undefined throws type error", () => { + const stack = new DisposableStack(); + [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 DisposableStack(); + [{}, [], { 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 DisposableStack(); + 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)", () => { + const stack = new DisposableStack(); + stack.dispose(); + expect(stack.disposed).toBeTrue(); + + [{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => { + expect(() => stack.use(value)).toThrowWithMessage( + ReferenceError, + "DisposableStack already disposed values" + ); + }); + }); +});