From 87bfbc92c45ac0146344cb2abc05265427b1cb1d Mon Sep 17 00:00:00 2001 From: davidot Date: Tue, 20 Dec 2022 19:38:42 +0100 Subject: [PATCH 1/8] LibJS: Clarify more errors in test-common Without a message these just show 'ExpectationError' even if the check has multiple steps. --- Userland/Libraries/LibJS/Tests/test-common.js | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Userland/Libraries/LibJS/Tests/test-common.js b/Userland/Libraries/LibJS/Tests/test-common.js index 0c177e72808595..5251cf79af81d1 100644 --- a/Userland/Libraries/LibJS/Tests/test-common.js +++ b/Userland/Libraries/LibJS/Tests/test-common.js @@ -145,15 +145,24 @@ class ExpectationError extends Error { if (Array.isArray(property)) { for (let key of property) { - this.__expect(object !== undefined && object !== null); + this.__expect( + object !== undefined && object !== null, + "got undefined or null as array key" + ); object = object[key]; } } else { object = object[property]; } - this.__expect(object !== undefined); - if (value !== undefined) this.__expect(deepEquals(object, value)); + this.__expect(object !== undefined, "should not be undefined"); + if (value !== undefined) + this.__expect( + deepEquals(object, value), + `value does not equal property ${valueToString(object)} vs ${valueToString( + value + )}` + ); }); } @@ -168,13 +177,19 @@ class ExpectationError extends Error { toBeInstanceOf(class_) { this.__doMatcher(() => { - this.__expect(this.target instanceof class_); + this.__expect( + this.target instanceof class_, + `Expected ${valueToString(this.target)} to be instance of ${class_.name}` + ); }); } toBeNull() { this.__doMatcher(() => { - this.__expect(this.target === null); + this.__expect( + this.target === null, + `Expected target to be null got ${valueToString(this.target)}` + ); }); } From 840272649c7de5400158862eb41e47efceb97974 Mon Sep 17 00:00:00 2001 From: davidot Date: Tue, 20 Dec 2022 19:41:23 +0100 Subject: [PATCH 2/8] LibJS: Add custom details to toBe{True, False} shown on failure Any test with multiple expect(...).toBe{True, False} checks is very hard to debug. --- Userland/Libraries/LibJS/Tests/test-common.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Userland/Libraries/LibJS/Tests/test-common.js b/Userland/Libraries/LibJS/Tests/test-common.js index 5251cf79af81d1..1ecfa8c0b673d4 100644 --- a/Userland/Libraries/LibJS/Tests/test-common.js +++ b/Userland/Libraries/LibJS/Tests/test-common.js @@ -214,24 +214,26 @@ class ExpectationError extends Error { }); } - toBeTrue() { + toBeTrue(customDetails = undefined) { this.__doMatcher(() => { this.__expect( this.target === true, () => - `toBeTrue: expected target to be true, got _${valueToString(this.target)}_` + `toBeTrue: expected target to be true, got _${valueToString(this.target)}_${ + customDetails ? ` (${customDetails})` : "" + }` ); }); } - toBeFalse() { + toBeFalse(customDetails = undefined) { this.__doMatcher(() => { this.__expect( this.target === false, () => `toBeFalse: expected target to be false, got _${valueToString( this.target - )}_` + )}_${customDetails ?? ""}` ); }); } From 92e69a69f927f51a77fae6880900c0215c9d98cb Mon Sep 17 00:00:00 2001 From: davidot Date: Tue, 6 Dec 2022 02:10:01 +0100 Subject: [PATCH 3/8] LibJS: Add SuppressedError{, Prototype, Constructor} --- Userland/Libraries/LibJS/CMakeLists.txt | 3 + Userland/Libraries/LibJS/Forward.h | 1 + .../LibJS/Runtime/CommonPropertyNames.h | 1 + .../Libraries/LibJS/Runtime/GlobalObject.cpp | 2 + .../Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../LibJS/Runtime/SuppressedError.cpp | 23 ++++++ .../Libraries/LibJS/Runtime/SuppressedError.h | 24 ++++++ .../Runtime/SuppressedErrorConstructor.cpp | 74 +++++++++++++++++++ .../Runtime/SuppressedErrorConstructor.h | 28 +++++++ .../Runtime/SuppressedErrorPrototype.cpp | 27 +++++++ .../LibJS/Runtime/SuppressedErrorPrototype.h | 24 ++++++ .../SuppressedError/SuppressedError.js | 57 ++++++++++++++ .../SuppressedError.prototype.message.js | 21 ++++++ .../SuppressedError.prototype.name.js | 13 ++++ 14 files changed, 300 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/SuppressedError.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/SuppressedError.h create mode 100644 Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.h create mode 100644 Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.message.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.name.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index a9f5381144fe0c..9650dc20826df5 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -193,6 +193,9 @@ set(SOURCES Runtime/StringIteratorPrototype.cpp Runtime/StringObject.cpp Runtime/StringPrototype.cpp + Runtime/SuppressedError.cpp + Runtime/SuppressedErrorConstructor.cpp + Runtime/SuppressedErrorPrototype.cpp Runtime/Symbol.cpp Runtime/SymbolConstructor.cpp Runtime/SymbolObject.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 6e656ccfc4e4de..626936eb1988a0 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -39,6 +39,7 @@ __JS_ENUMERATE(Set, set, SetPrototype, SetConstructor, void) \ __JS_ENUMERATE(ShadowRealm, shadow_realm, ShadowRealmPrototype, ShadowRealmConstructor, void) \ __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor, void) \ + __JS_ENUMERATE(SuppressedError, suppressed_error, SuppressedErrorPrototype, SuppressedErrorConstructor, void) \ __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor, void) \ __JS_ENUMERATE(WeakMap, weak_map, WeakMapPrototype, WeakMapConstructor, void) \ __JS_ENUMERATE(WeakRef, weak_ref, WeakRefPrototype, WeakRefConstructor, void) \ diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 34f7dfed23f5e8..ed742088a0f504 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -489,6 +489,7 @@ namespace JS { P(substring) \ P(subtract) \ P(sup) \ + P(suppressed) \ P(supportedLocalesOf) \ P(supportedValuesOf) \ P(symmetricDifference) \ diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index 83f29af207999d..73a925d3f168b8 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -154,6 +155,7 @@ Object& set_default_global_bindings(Realm& realm) global.define_intrinsic_accessor(vm.names.Set, attr, [](auto& realm) -> Value { return realm.intrinsics().set_constructor(); }); global.define_intrinsic_accessor(vm.names.ShadowRealm, attr, [](auto& realm) -> Value { return realm.intrinsics().shadow_realm_constructor(); }); global.define_intrinsic_accessor(vm.names.String, attr, [](auto& realm) -> Value { return realm.intrinsics().string_constructor(); }); + global.define_intrinsic_accessor(vm.names.SuppressedError, attr, [](auto& realm) -> Value { return realm.intrinsics().suppressed_error_constructor(); }); global.define_intrinsic_accessor(vm.names.Symbol, attr, [](auto& realm) -> Value { return realm.intrinsics().symbol_constructor(); }); global.define_intrinsic_accessor(vm.names.SyntaxError, attr, [](auto& realm) -> Value { return realm.intrinsics().syntax_error_constructor(); }); global.define_intrinsic_accessor(vm.names.TypeError, attr, [](auto& realm) -> Value { return realm.intrinsics().type_error_constructor(); }); diff --git a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp index 70b82a470f9f83..3d229eaa5e36c9 100644 --- a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -89,6 +89,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Userland/Libraries/LibJS/Runtime/SuppressedError.cpp b/Userland/Libraries/LibJS/Runtime/SuppressedError.cpp new file mode 100644 index 00000000000000..b067f5b985cd96 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SuppressedError.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS { + +NonnullGCPtr SuppressedError::create(Realm& realm) +{ + return *realm.heap().allocate(realm, *realm.intrinsics().suppressed_error_prototype()); +} + +SuppressedError::SuppressedError(Object& prototype) + : Error(prototype) +{ +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SuppressedError.h b/Userland/Libraries/LibJS/Runtime/SuppressedError.h new file mode 100644 index 00000000000000..9839cd2a9fa101 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SuppressedError.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class SuppressedError : public Error { + JS_OBJECT(SuppressedError, Error); + +public: + static NonnullGCPtr create(Realm&); + virtual ~SuppressedError() override = default; + +private: + explicit SuppressedError(Object& prototype); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.cpp b/Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.cpp new file mode 100644 index 00000000000000..74ac04741ff580 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +SuppressedErrorConstructor::SuppressedErrorConstructor(Realm& realm) + : NativeFunction(static_cast(*realm.intrinsics().error_constructor())) +{ +} + +void SuppressedErrorConstructor::initialize(Realm& realm) +{ + auto& vm = this->vm(); + NativeFunction::initialize(realm); + + // 10.1.4.2.1 SuppressedError.prototype, https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().suppressed_error_prototype(), 0); + + define_direct_property(vm.names.length, Value(3), Attribute::Configurable); +} + +// 10.1.4.1.1 SuppressedError ( error, suppressed, message [ , options ] ), https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror +ThrowCompletionOr SuppressedErrorConstructor::call() +{ + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + return TRY(construct(*this)); +} + +// 10.1.4.1.1 SuppressedError ( error, suppressed, message [ , options ] ), https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror +ThrowCompletionOr> SuppressedErrorConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + auto error = vm.argument(0); + auto suppressed = vm.argument(1); + auto message = vm.argument(2); + auto options = vm.argument(3); + + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%SuppressedError.prototype%", « [[ErrorData]] »). + auto suppressed_error = TRY(ordinary_create_from_constructor(vm, new_target, &Intrinsics::suppressed_error_prototype)); + + // 3. If message is not undefined, then + if (!message.is_undefined()) { + // a. Let msg be ? ToString(message). + auto msg = TRY(message.to_string(vm)); + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + suppressed_error->create_non_enumerable_data_property_or_throw(vm.names.message, PrimitiveString::create(vm, move(msg))); + } + + // 4. Perform ? InstallErrorCause(O, options). + TRY(suppressed_error->install_error_cause(options)); + + // 5. Perform ! DefinePropertyOrThrow(O, "error", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: error }). + MUST(suppressed_error->define_property_or_throw(vm.names.error, { .value = error, .writable = true, .enumerable = false, .configurable = true })); + + // 6. Perform ! DefinePropertyOrThrow(O, "suppressed", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: suppressed }). + MUST(suppressed_error->define_property_or_throw(vm.names.suppressed, { .value = suppressed, .writable = true, .enumerable = false, .configurable = true })); + + // 7. Return O. + return suppressed_error; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.h b/Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.h new file mode 100644 index 00000000000000..f27672ed208088 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SuppressedErrorConstructor.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class SuppressedErrorConstructor final : public NativeFunction { + JS_OBJECT(SuppressedErrorConstructor, NativeFunction); + +public: + virtual void initialize(Realm&) override; + virtual ~SuppressedErrorConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject& new_target) override; + +private: + explicit SuppressedErrorConstructor(Realm&); + virtual bool has_constructor() const override { return true; } +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.cpp new file mode 100644 index 00000000000000..224226cd640279 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS { + +SuppressedErrorPrototype::SuppressedErrorPrototype(Realm& realm) + : Object(ConstructWithPrototypeTag::Tag, *realm.intrinsics().error_prototype()) +{ +} + +void SuppressedErrorPrototype::initialize(Realm& realm) +{ + auto& vm = this->vm(); + Object::initialize(realm); + u8 attr = Attribute::Writable | Attribute::Configurable; + define_direct_property(vm.names.name, PrimitiveString::create(vm, "SuppressedError"), attr); + define_direct_property(vm.names.message, PrimitiveString::create(vm, ""), attr); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.h b/Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.h new file mode 100644 index 00000000000000..27791d6d7da45a --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SuppressedErrorPrototype.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class SuppressedErrorPrototype final : public Object { + JS_OBJECT(SuppressedErrorPrototype, Object); + +public: + virtual void initialize(Realm&) override; + virtual ~SuppressedErrorPrototype() override = default; + +private: + explicit SuppressedErrorPrototype(Realm&); +}; + +} diff --git a/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.js b/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.js new file mode 100644 index 00000000000000..ade5c1f26bc635 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.js @@ -0,0 +1,57 @@ +describe("normal behavior", () => { + test("length is 2", () => { + expect(SuppressedError).toHaveLength(3); + }); + + test("name is SuppressedError", () => { + expect(SuppressedError.name).toBe("SuppressedError"); + }); + + test("Prototype of the SuppressedError constructor is the Error constructor", () => { + expect(Object.getPrototypeOf(SuppressedError)).toBe(Error); + }); + + test("Prototype of SuppressedError.prototype is Error.prototype", () => { + expect(Object.getPrototypeOf(SuppressedError.prototype)).toBe(Error.prototype); + }); + + test("construction", () => { + expect(SuppressedError()).toBeInstanceOf(SuppressedError); + expect(SuppressedError(1)).toBeInstanceOf(SuppressedError); + expect(SuppressedError(1, 1)).toBeInstanceOf(SuppressedError); + expect(new SuppressedError()).toBeInstanceOf(SuppressedError); + expect(new SuppressedError(1)).toBeInstanceOf(SuppressedError); + expect(new SuppressedError(1, 1)).toBeInstanceOf(SuppressedError); + expect(Object.hasOwn(new SuppressedError(1, 1), "message")).toBeFalse(); + expect(new SuppressedError().toString()).toBe("SuppressedError"); + expect(new SuppressedError(1).toString()).toBe("SuppressedError"); + expect(new SuppressedError(1, 1).toString()).toBe("SuppressedError"); + expect(new SuppressedError(undefined, undefined, "Foo").toString()).toBe( + "SuppressedError: Foo" + ); + expect(new SuppressedError(1, 1, "Foo").toString()).toBe("SuppressedError: Foo"); + expect(Object.hasOwn(new SuppressedError(), "error")).toBeTrue(); + expect(Object.hasOwn(new SuppressedError(), "suppressed")).toBeTrue(); + const obj = {}; + expect(new SuppressedError(obj).error).toBe(obj); + expect(new SuppressedError(null, obj).suppressed).toBe(obj); + }); + + test("converts message to string", () => { + expect(new SuppressedError(undefined, undefined, 1)).toHaveProperty("message", "1"); + expect(new SuppressedError(undefined, undefined, {})).toHaveProperty( + "message", + "[object Object]" + ); + }); + + test("supports options object with cause", () => { + const cause = new Error(); + const error = new SuppressedError(1, 2, "test", { cause }); + expect(error.hasOwnProperty("cause")).toBeTrue(); + expect(error.cause).toBe(cause); + + const errorWithoutCase = new SuppressedError(1, 2, "test"); + expect(errorWithoutCase.hasOwnProperty("cause")).toBeFalse(); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.message.js b/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.message.js new file mode 100644 index 00000000000000..ba743ddd9cecc4 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.message.js @@ -0,0 +1,21 @@ +describe("normal behavior", () => { + test("initial message value is empty string", () => { + expect(SuppressedError.prototype.message).toBe(""); + }); + + test("Error gets message via prototype by default", () => { + const error = new SuppressedError(); + expect(error.hasOwnProperty("message")).toBeFalse(); + expect(error.message).toBe(""); + SuppressedError.prototype.message = "Well hello friends"; + expect(error.message).toBe("Well hello friends"); + }); + + test("Error gets message via object if given to constructor", () => { + const error = new SuppressedError(undefined, undefined, "Custom error message"); + expect(error.hasOwnProperty("message")).toBeTrue(); + expect(error.message).toBe("Custom error message"); + SuppressedError.prototype.message = "Well hello friends"; + expect(error.message).toBe("Custom error message"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.name.js b/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.name.js new file mode 100644 index 00000000000000..b30b27d2576a8d --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/SuppressedError/SuppressedError.prototype.name.js @@ -0,0 +1,13 @@ +describe("normal behavior", () => { + test("initial name value is type name", () => { + expect(SuppressedError.prototype.name).toBe("SuppressedError"); + }); + + test("Error gets name via prototype", () => { + const error = new SuppressedError([]); + expect(error.hasOwnProperty("name")).toBeFalse(); + expect(error.name).toBe("SuppressedError"); + SuppressedError.prototype.name = "Foo"; + expect(error.name).toBe("Foo"); + }); +}); From 6d359bf263f103f46565d018094d95d1370e056a Mon Sep 17 00:00:00 2001 From: davidot Date: Wed, 14 Dec 2022 13:26:10 +0100 Subject: [PATCH 4/8] LibJS: Add an initialize binding hint to all initialize_binding methods This will allow us to specify things like SyncDispose and perhaps AsyncDispose in the future. --- Userland/Libraries/LibJS/AST.cpp | 6 ++--- .../LibJS/Runtime/AbstractOperations.cpp | 18 ++++++++------- .../LibJS/Runtime/DeclarativeEnvironment.cpp | 23 +++++++++---------- .../LibJS/Runtime/DeclarativeEnvironment.h | 3 +-- .../Runtime/ECMAScriptFunctionObject.cpp | 10 ++++---- .../Libraries/LibJS/Runtime/Environment.h | 7 +++++- .../LibJS/Runtime/GlobalEnvironment.cpp | 20 ++++++++-------- .../LibJS/Runtime/GlobalEnvironment.h | 2 +- .../LibJS/Runtime/ObjectEnvironment.cpp | 7 ++++-- .../LibJS/Runtime/ObjectEnvironment.h | 2 +- .../Libraries/LibJS/Runtime/Reference.cpp | 5 ++-- Userland/Libraries/LibJS/Runtime/Reference.h | 2 +- Userland/Libraries/LibJS/SourceTextModule.cpp | 16 ++++++------- Userland/Libraries/LibJS/SyntheticModule.cpp | 4 ++-- 14 files changed, 68 insertions(+), 57 deletions(-) diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index dc4b5b0a8ed9ca..5c67c1ea443052 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -323,7 +323,7 @@ Value FunctionExpression::instantiate_ordinary_function_expression(Interpreter& // FIXME: 7. Perform MakeConstructor(closure). if (has_own_name) - MUST(environment->initialize_binding(vm, name(), closure)); + MUST(environment->initialize_binding(vm, name(), closure, Environment::InitializeBindingHint::Normal)); return closure; } @@ -1973,7 +1973,7 @@ ThrowCompletionOr ClassExpression::class_definition_e restore_environment.disarm(); if (!binding_name.is_null()) - MUST(class_environment->initialize_binding(vm, binding_name, class_constructor)); + MUST(class_environment->initialize_binding(vm, binding_name, class_constructor, Environment::InitializeBindingHint::Normal)); for (auto& field : instance_fields) class_constructor->add_field(field); @@ -3864,7 +3864,7 @@ Completion TryStatement::execute(Interpreter& interpreter) const // 5. Let status be Completion(BindingInitialization of CatchParameter with arguments thrownValue and catchEnv). auto status = m_handler->parameter().visit( [&](DeprecatedFlyString const& parameter) { - return catch_environment->initialize_binding(vm, parameter, thrown_value); + return catch_environment->initialize_binding(vm, parameter, thrown_value, Environment::InitializeBindingHint::Normal); }, [&](NonnullRefPtr const& pattern) { return vm.binding_initialization(pattern, thrown_value, catch_environment); diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp index b904fec61267c3..dcfe8bec7d875b 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -205,8 +205,9 @@ ThrowCompletionOr initialize_bound_name(VM& vm, DeprecatedFlyString const& { // 1. If environment is not undefined, then if (environment) { - // a. Perform ! environment.InitializeBinding(name, value). - MUST(environment->initialize_binding(vm, name, value)); + // FIXME: The normal is not included in the explicit resource management spec yet, so there is no spec link for it. + // a. Perform ! environment.InitializeBinding(name, value, normal). + MUST(environment->initialize_binding(vm, name, value, Environment::InitializeBindingHint::Normal)); // b. Return unused. return {}; @@ -729,6 +730,7 @@ ThrowCompletionOr perform_eval(VM& vm, Value x, CallerMode strict_caller, } // 19.2.1.3 EvalDeclarationInstantiation ( body, varEnv, lexEnv, privateEnv, strict ), https://tc39.es/ecma262/#sec-evaldeclarationinstantiation +// 9.1.1.1 EvalDeclarationInstantiation ( body, varEnv, lexEnv, privateEnv, strict ), https://tc39.es/proposal-explicit-resource-management/#sec-evaldeclarationinstantiation ThrowCompletionOr eval_declaration_instantiation(VM& vm, Program const& program, Environment* variable_environment, Environment* lexical_environment, PrivateEnvironment* private_environment, bool strict) { auto& realm = *vm.current_realm(); @@ -903,9 +905,9 @@ ThrowCompletionOr eval_declaration_instantiation(VM& vm, Program const& pr // ii. If bindingExists is false, then if (!MUST(variable_environment->has_binding(function_name))) { // i. Perform ! varEnv.CreateMutableBinding(F, true). - // ii. Perform ! varEnv.InitializeBinding(F, undefined). MUST(variable_environment->create_mutable_binding(vm, function_name, true)); - MUST(variable_environment->initialize_binding(vm, function_name, js_undefined())); + // ii. Perform ! varEnv.InitializeBinding(F, undefined, normal). + MUST(variable_environment->initialize_binding(vm, function_name, js_undefined(), Environment::InitializeBindingHint::Normal)); } } } @@ -1003,8 +1005,8 @@ ThrowCompletionOr eval_declaration_instantiation(VM& vm, Program const& pr // 2. Perform ! varEnv.CreateMutableBinding(fn, true). MUST(variable_environment->create_mutable_binding(vm, declaration.name(), true)); - // 3. Perform ! varEnv.InitializeBinding(fn, fo). - MUST(variable_environment->initialize_binding(vm, declaration.name(), function)); + // 3. Perform ! varEnv.InitializeBinding(fn, fo, normal). + MUST(variable_environment->initialize_binding(vm, declaration.name(), function, Environment::InitializeBindingHint::Normal)); } // iii. Else, else { @@ -1033,8 +1035,8 @@ ThrowCompletionOr eval_declaration_instantiation(VM& vm, Program const& pr // 2. Perform ! varEnv.CreateMutableBinding(vn, true). MUST(variable_environment->create_mutable_binding(vm, var_name, true)); - // 3. Perform ! varEnv.InitializeBinding(vn, undefined). - MUST(variable_environment->initialize_binding(vm, var_name, js_undefined())); + // 3. Perform ! varEnv.InitializeBinding(vn, undefined, normal). + MUST(variable_environment->initialize_binding(vm, var_name, js_undefined(), Environment::InitializeBindingHint::Normal)); } } } diff --git a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp index d427780ed9997e..08904e5ad4d068 100644 --- a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp +++ b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp @@ -96,26 +96,25 @@ ThrowCompletionOr DeclarativeEnvironment::create_immutable_binding(VM&, De } // 9.1.1.1.4 InitializeBinding ( N, V ), https://tc39.es/ecma262/#sec-declarative-environment-records-initializebinding-n-v -ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value) +// 4.1.1.1.1 InitializeBinding ( N, V, hint ), https://tc39.es/proposal-explicit-resource-management/#sec-declarative-environment-records +ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM&, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint) { auto binding_and_index = find_binding_and_index(name); VERIFY(binding_and_index.has_value()); + auto& binding = binding_and_index->binding(); - return initialize_binding_direct(vm, binding_and_index->binding(), value); -} - -ThrowCompletionOr DeclarativeEnvironment::initialize_binding_direct(VM&, Binding& binding, Value value) -{ // 1. Assert: envRec must have an uninitialized binding for N. VERIFY(binding.initialized == false); - // 2. Set the bound value for N in envRec to V. + // FIXME: 2. If hint is not normal, perform ? AddDisposableResource(envRec, V, hint). + + // 3. Set the bound value for N in envRec to V. binding.value = value; - // 3. Record that the binding for N in envRec has been initialized. + // 4. Record that the binding for N in envRec has been initialized. binding.initialized = true; - // 4. Return unused. + // 5. Return unused. return {}; } @@ -132,8 +131,8 @@ ThrowCompletionOr DeclarativeEnvironment::set_mutable_binding(VM& vm, Depr // b. Perform ! envRec.CreateMutableBinding(N, true). MUST(create_mutable_binding(vm, name, true)); - // c. Perform ! envRec.InitializeBinding(N, V). - MUST(initialize_binding(vm, name, value)); + // c. Perform ! envRec.InitializeBinding(N, V, normal). + MUST(initialize_binding(vm, name, value, Environment::InitializeBindingHint::Normal)); // d. Return unused. return {}; @@ -220,7 +219,7 @@ ThrowCompletionOr DeclarativeEnvironment::initialize_or_set_mutable_bindin VERIFY(binding_and_index.has_value()); if (!binding_and_index->binding().initialized) - TRY(initialize_binding(vm, name, value)); + TRY(initialize_binding(vm, name, value, Environment::InitializeBindingHint::Normal)); else TRY(set_mutable_binding(vm, name, value, false)); return {}; diff --git a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h index c4c844e0a8df24..5d792edfed52c7 100644 --- a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h +++ b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h @@ -34,7 +34,7 @@ class DeclarativeEnvironment : public Environment { virtual ThrowCompletionOr has_binding(DeprecatedFlyString const& name, Optional* = nullptr) const override; virtual ThrowCompletionOr create_mutable_binding(VM&, DeprecatedFlyString const& name, bool can_be_deleted) override; virtual ThrowCompletionOr create_immutable_binding(VM&, DeprecatedFlyString const& name, bool strict) override; - virtual ThrowCompletionOr initialize_binding(VM&, DeprecatedFlyString const& name, Value) override; + virtual ThrowCompletionOr initialize_binding(VM&, DeprecatedFlyString const& name, Value, InitializeBindingHint) override; virtual ThrowCompletionOr set_mutable_binding(VM&, DeprecatedFlyString const& name, Value, bool strict) override; virtual ThrowCompletionOr get_binding_value(VM&, DeprecatedFlyString const& name, bool strict) override; virtual ThrowCompletionOr delete_binding(VM&, DeprecatedFlyString const& name) override; @@ -60,7 +60,6 @@ class DeclarativeEnvironment : public Environment { void shrink_to_fit(); private: - ThrowCompletionOr initialize_binding_direct(VM&, Binding&, Value); ThrowCompletionOr get_binding_value_direct(VM&, Binding&, bool strict); ThrowCompletionOr set_mutable_binding_direct(VM&, Binding&, Value, bool strict); diff --git a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index 2c7c270886580d..ab5b4bff951466 100644 --- a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -407,7 +407,7 @@ ThrowCompletionOr ECMAScriptFunctionObject::function_declaration_instantia MUST(environment->create_mutable_binding(vm, parameter_name, false)); if (has_duplicates) - MUST(environment->initialize_binding(vm, parameter_name, js_undefined())); + MUST(environment->initialize_binding(vm, parameter_name, js_undefined(), Environment::InitializeBindingHint::Normal)); } if (arguments_object_needed) { @@ -422,7 +422,7 @@ ThrowCompletionOr ECMAScriptFunctionObject::function_declaration_instantia else MUST(environment->create_mutable_binding(vm, vm.names.arguments.as_string(), false)); - MUST(environment->initialize_binding(vm, vm.names.arguments.as_string(), arguments_object)); + MUST(environment->initialize_binding(vm, vm.names.arguments.as_string(), arguments_object, Environment::InitializeBindingHint::Normal)); parameter_names.set(vm.names.arguments.as_string()); } @@ -488,7 +488,7 @@ ThrowCompletionOr ECMAScriptFunctionObject::function_declaration_instantia scope_body->for_each_var_declared_name([&](auto const& name) { if (!parameter_names.contains(name) && instantiated_var_names.set(name) == AK::HashSetResult::InsertedNewEntry) { MUST(environment->create_mutable_binding(vm, name, false)); - MUST(environment->initialize_binding(vm, name, js_undefined())); + MUST(environment->initialize_binding(vm, name, js_undefined(), Environment::InitializeBindingHint::Normal)); } }); } @@ -509,7 +509,7 @@ ThrowCompletionOr ECMAScriptFunctionObject::function_declaration_instantia else initial_value = MUST(environment->get_binding_value(vm, name, false)); - MUST(var_environment->initialize_binding(vm, name, initial_value)); + MUST(var_environment->initialize_binding(vm, name, initial_value, Environment::InitializeBindingHint::Normal)); }); } } @@ -523,7 +523,7 @@ ThrowCompletionOr ECMAScriptFunctionObject::function_declaration_instantia // The spec says 'initializedBindings' here but that does not exist and it then adds it to 'instantiatedVarNames' so it probably means 'instantiatedVarNames'. if (!instantiated_var_names.contains(function_name) && function_name != vm.names.arguments.as_string()) { MUST(var_environment->create_mutable_binding(vm, function_name, false)); - MUST(var_environment->initialize_binding(vm, function_name, js_undefined())); + MUST(var_environment->initialize_binding(vm, function_name, js_undefined(), Environment::InitializeBindingHint::Normal)); instantiated_var_names.set(function_name); } diff --git a/Userland/Libraries/LibJS/Runtime/Environment.h b/Userland/Libraries/LibJS/Runtime/Environment.h index 74388d74291171..e33fa08b2e08a9 100644 --- a/Userland/Libraries/LibJS/Runtime/Environment.h +++ b/Userland/Libraries/LibJS/Runtime/Environment.h @@ -23,6 +23,11 @@ class Environment : public Cell { JS_CELL(Environment, Cell); public: + enum class InitializeBindingHint { + Normal, + SyncDispose, + }; + virtual bool has_this_binding() const { return false; } virtual ThrowCompletionOr get_this_binding(VM&) const { return Value {}; } @@ -31,7 +36,7 @@ class Environment : public Cell { virtual ThrowCompletionOr has_binding([[maybe_unused]] DeprecatedFlyString const& name, [[maybe_unused]] Optional* out_index = nullptr) const { return false; } virtual ThrowCompletionOr create_mutable_binding(VM&, [[maybe_unused]] DeprecatedFlyString const& name, [[maybe_unused]] bool can_be_deleted) { return {}; } virtual ThrowCompletionOr create_immutable_binding(VM&, [[maybe_unused]] DeprecatedFlyString const& name, [[maybe_unused]] bool strict) { return {}; } - virtual ThrowCompletionOr initialize_binding(VM&, [[maybe_unused]] DeprecatedFlyString const& name, Value) { return {}; } + virtual ThrowCompletionOr initialize_binding(VM&, [[maybe_unused]] DeprecatedFlyString const& name, Value, InitializeBindingHint) { return {}; } virtual ThrowCompletionOr set_mutable_binding(VM&, [[maybe_unused]] DeprecatedFlyString const& name, Value, [[maybe_unused]] bool strict) { return {}; } virtual ThrowCompletionOr get_binding_value(VM&, [[maybe_unused]] DeprecatedFlyString const& name, [[maybe_unused]] bool strict) { return Value {}; } virtual ThrowCompletionOr delete_binding(VM&, [[maybe_unused]] DeprecatedFlyString const& name) { return false; } diff --git a/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.cpp b/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.cpp index 2fada09a3d0826..b2fba47a28a60b 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.cpp @@ -78,20 +78,22 @@ ThrowCompletionOr GlobalEnvironment::create_immutable_binding(VM& vm, Depr return MUST(m_declarative_record->create_immutable_binding(vm, name, strict)); } -// 9.1.1.4.4 InitializeBinding ( N, V ), https://tc39.es/ecma262/#sec-global-environment-records-initializebinding-n-v -ThrowCompletionOr GlobalEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value) +// 9.1.1.4.4 InitializeBinding ( N, V, hint ), https://tc39.es/ecma262/#sec-global-environment-records-initializebinding-n-v +ThrowCompletionOr GlobalEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value, InitializeBindingHint hint) { // 1. Let DclRec be envRec.[[DeclarativeRecord]]. // 2. If ! DclRec.HasBinding(N) is true, then if (MUST(m_declarative_record->has_binding(name))) { - // a. Return ! DclRec.InitializeBinding(N, V). - return MUST(m_declarative_record->initialize_binding(vm, name, value)); + // a. Return ! DclRec.InitializeBinding(N, V, hint). + return MUST(m_declarative_record->initialize_binding(vm, name, value, hint)); } // 3. Assert: If the binding exists, it must be in the object Environment Record. - // 4. Let ObjRec be envRec.[[ObjectRecord]]. - // 5. Return ? ObjRec.InitializeBinding(N, V). - return m_object_record->initialize_binding(vm, name, value); + // 4. Assert: hint is normal. + VERIFY(hint == Environment::InitializeBindingHint::Normal); + // 5. Let ObjRec be envRec.[[ObjectRecord]]. + // 6. Return ? ObjRec.InitializeBinding(N, V, normal). + return m_object_record->initialize_binding(vm, name, value, Environment::InitializeBindingHint::Normal); } // 9.1.1.4.5 SetMutableBinding ( N, V, S ), https://tc39.es/ecma262/#sec-global-environment-records-setmutablebinding-n-v-s @@ -263,8 +265,8 @@ ThrowCompletionOr GlobalEnvironment::create_global_var_binding(DeprecatedF // a. Perform ? ObjRec.CreateMutableBinding(N, D). TRY(m_object_record->create_mutable_binding(vm, name, can_be_deleted)); - // b. Perform ? ObjRec.InitializeBinding(N, undefined). - TRY(m_object_record->initialize_binding(vm, name, js_undefined())); + // b. Perform ? ObjRec.InitializeBinding(N, undefined, normal). + TRY(m_object_record->initialize_binding(vm, name, js_undefined(), Environment::InitializeBindingHint::Normal)); } // 6. Let varDeclaredNames be envRec.[[VarNames]]. diff --git a/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.h b/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.h index 65ed8437250804..1bae539f6c0f98 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.h +++ b/Userland/Libraries/LibJS/Runtime/GlobalEnvironment.h @@ -20,7 +20,7 @@ class GlobalEnvironment final : public Environment { virtual ThrowCompletionOr has_binding(DeprecatedFlyString const& name, Optional* = nullptr) const override; virtual ThrowCompletionOr create_mutable_binding(VM&, DeprecatedFlyString const& name, bool can_be_deleted) override; virtual ThrowCompletionOr create_immutable_binding(VM&, DeprecatedFlyString const& name, bool strict) override; - virtual ThrowCompletionOr initialize_binding(VM&, DeprecatedFlyString const& name, Value) override; + virtual ThrowCompletionOr initialize_binding(VM&, DeprecatedFlyString const& name, Value, Environment::InitializeBindingHint) override; virtual ThrowCompletionOr set_mutable_binding(VM&, DeprecatedFlyString const& name, Value, bool strict) override; virtual ThrowCompletionOr get_binding_value(VM&, DeprecatedFlyString const& name, bool strict) override; virtual ThrowCompletionOr delete_binding(VM&, DeprecatedFlyString const& name) override; diff --git a/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.cpp b/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.cpp index cdc4f1e74bfc6b..4e477d80330c5c 100644 --- a/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.cpp +++ b/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.cpp @@ -77,9 +77,12 @@ ThrowCompletionOr ObjectEnvironment::create_immutable_binding(VM&, Depreca } // 9.1.1.2.4 InitializeBinding ( N, V ), https://tc39.es/ecma262/#sec-object-environment-records-initializebinding-n-v -ThrowCompletionOr ObjectEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value) +ThrowCompletionOr ObjectEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint hint) { - // 1. Perform ? envRec.SetMutableBinding(N, V, false). + // 1. Assert: hint is normal. + VERIFY(hint == Environment::InitializeBindingHint::Normal); + + // 2. Perform ? envRec.SetMutableBinding(N, V, false). TRY(set_mutable_binding(vm, name, value, false)); // 2. Return unused. diff --git a/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.h b/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.h index bd3bbc7729b951..5998b1a63670e3 100644 --- a/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.h +++ b/Userland/Libraries/LibJS/Runtime/ObjectEnvironment.h @@ -22,7 +22,7 @@ class ObjectEnvironment : public Environment { virtual ThrowCompletionOr has_binding(DeprecatedFlyString const& name, Optional* = nullptr) const override; virtual ThrowCompletionOr create_mutable_binding(VM&, DeprecatedFlyString const& name, bool can_be_deleted) override; virtual ThrowCompletionOr create_immutable_binding(VM&, DeprecatedFlyString const& name, bool strict) override; - virtual ThrowCompletionOr initialize_binding(VM&, DeprecatedFlyString const& name, Value) override; + virtual ThrowCompletionOr initialize_binding(VM&, DeprecatedFlyString const& name, Value, Environment::InitializeBindingHint) override; virtual ThrowCompletionOr set_mutable_binding(VM&, DeprecatedFlyString const& name, Value, bool strict) override; virtual ThrowCompletionOr get_binding_value(VM&, DeprecatedFlyString const& name, bool strict) override; virtual ThrowCompletionOr delete_binding(VM&, DeprecatedFlyString const& name) override; diff --git a/Userland/Libraries/LibJS/Runtime/Reference.cpp b/Userland/Libraries/LibJS/Runtime/Reference.cpp index 06a16337a21a09..4d21d767051a21 100644 --- a/Userland/Libraries/LibJS/Runtime/Reference.cpp +++ b/Userland/Libraries/LibJS/Runtime/Reference.cpp @@ -231,11 +231,12 @@ DeprecatedString Reference::to_deprecated_string() const } // 6.2.4.8 InitializeReferencedBinding ( V, W ), https://tc39.es/ecma262/#sec-object.prototype.hasownproperty -ThrowCompletionOr Reference::initialize_referenced_binding(VM& vm, Value value) const +// 1.2.1.1 InitializeReferencedBinding ( V, W, hint ), https://tc39.es/proposal-explicit-resource-management/#sec-initializereferencedbinding +ThrowCompletionOr Reference::initialize_referenced_binding(VM& vm, Value value, Environment::InitializeBindingHint hint) const { VERIFY(!is_unresolvable()); VERIFY(m_base_type == BaseType::Environment); - return m_base_environment->initialize_binding(vm, m_name.as_string(), value); + return m_base_environment->initialize_binding(vm, m_name.as_string(), value, hint); } // 6.2.4.9 MakePrivateReference ( baseValue, privateIdentifier ), https://tc39.es/ecma262/#sec-makeprivatereference diff --git a/Userland/Libraries/LibJS/Runtime/Reference.h b/Userland/Libraries/LibJS/Runtime/Reference.h index f0d716b296323f..8c278cda6fd942 100644 --- a/Userland/Libraries/LibJS/Runtime/Reference.h +++ b/Userland/Libraries/LibJS/Runtime/Reference.h @@ -121,7 +121,7 @@ class Reference { return m_base_type == BaseType::Environment; } - ThrowCompletionOr initialize_referenced_binding(VM&, Value value) const; + ThrowCompletionOr initialize_referenced_binding(VM&, Value value, Environment::InitializeBindingHint hint = Environment::InitializeBindingHint::Normal) const; ThrowCompletionOr put_value(VM&, Value); ThrowCompletionOr get_value(VM&) const; diff --git a/Userland/Libraries/LibJS/SourceTextModule.cpp b/Userland/Libraries/LibJS/SourceTextModule.cpp index 2664002911ff70..3afb59c0dd2fd3 100644 --- a/Userland/Libraries/LibJS/SourceTextModule.cpp +++ b/Userland/Libraries/LibJS/SourceTextModule.cpp @@ -367,8 +367,8 @@ ThrowCompletionOr SourceTextModule::initialize_environment(VM& vm) // ii. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true). MUST(environment->create_immutable_binding(vm, import_entry.local_name, true)); - // iii. Perform ! env.InitializeBinding(in.[[LocalName]], namespace). - MUST(environment->initialize_binding(vm, import_entry.local_name, namespace_)); + // iii. Perform ! env.InitializeBinding(in.[[LocalName]], namespace, normal). + MUST(environment->initialize_binding(vm, import_entry.local_name, namespace_, Environment::InitializeBindingHint::Normal)); } // d. Else, else { @@ -387,8 +387,8 @@ ThrowCompletionOr SourceTextModule::initialize_environment(VM& vm) // 2. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true). MUST(environment->create_immutable_binding(vm, import_entry.local_name, true)); - // 3. Perform ! env.InitializeBinding(in.[[LocalName]], namespace). - MUST(environment->initialize_binding(vm, import_entry.local_name, namespace_)); + // 3. Perform ! env.InitializeBinding(in.[[LocalName]], namespace, normal). + MUST(environment->initialize_binding(vm, import_entry.local_name, namespace_, Environment::InitializeBindingHint::Normal)); } // iv. Else, else { @@ -442,8 +442,8 @@ ThrowCompletionOr SourceTextModule::initialize_environment(VM& vm) // 1. Perform ! env.CreateMutableBinding(dn, false). MUST(environment->create_mutable_binding(vm, name, false)); - // 2. Perform ! env.InitializeBinding(dn, undefined). - MUST(environment->initialize_binding(vm, name, js_undefined())); + // 2. Perform ! env.InitializeBinding(dn, undefined, normal). + MUST(environment->initialize_binding(vm, name, js_undefined(), Environment::InitializeBindingHint::Normal)); // 3. Append dn to declaredVarNames. declared_var_names.empend(name); @@ -484,8 +484,8 @@ ThrowCompletionOr SourceTextModule::initialize_environment(VM& vm) function_name = "default"sv; auto function = ECMAScriptFunctionObject::create(realm(), function_name, function_declaration.source_text(), function_declaration.body(), function_declaration.parameters(), function_declaration.function_length(), environment, private_environment, function_declaration.kind(), function_declaration.is_strict_mode(), function_declaration.might_need_arguments_object(), function_declaration.contains_direct_call_to_eval()); - // 2. Perform ! env.InitializeBinding(dn, fo). - MUST(environment->initialize_binding(vm, name, function)); + // 2. Perform ! env.InitializeBinding(dn, fo, normal). + MUST(environment->initialize_binding(vm, name, function, Environment::InitializeBindingHint::Normal)); } }); }); diff --git a/Userland/Libraries/LibJS/SyntheticModule.cpp b/Userland/Libraries/LibJS/SyntheticModule.cpp index 7e94f5a8a61244..332337343af5b5 100644 --- a/Userland/Libraries/LibJS/SyntheticModule.cpp +++ b/Userland/Libraries/LibJS/SyntheticModule.cpp @@ -61,8 +61,8 @@ ThrowCompletionOr SyntheticModule::link(VM& vm) // a. Perform ! envRec.CreateMutableBinding(exportName, false). MUST(environment->create_mutable_binding(vm, export_name, false)); - // b. Perform ! envRec.InitializeBinding(exportName, undefined). - MUST(environment->initialize_binding(vm, export_name, js_undefined())); + // b. Perform ! envRec.InitializeBinding(exportName, undefined, normal). + MUST(environment->initialize_binding(vm, export_name, js_undefined(), Environment::InitializeBindingHint::Normal)); } // 6. Return unused. From 7a5e042e37793b7135e957de337d4249675ae831 Mon Sep 17 00:00:00 2001 From: davidot Date: Tue, 20 Dec 2022 19:26:02 +0100 Subject: [PATCH 5/8] LibJS: Add Symbol.dispose --- Userland/Libraries/LibJS/Forward.h | 3 ++- .../LibJS/Tests/builtins/Symbol/well-known-symbol-existence.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 626936eb1988a0..c5938e98fc2db4 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -133,7 +133,8 @@ __JS_ENUMERATE(unscopables, unscopables) \ __JS_ENUMERATE(species, species) \ __JS_ENUMERATE(toPrimitive, to_primitive) \ - __JS_ENUMERATE(toStringTag, to_string_tag) + __JS_ENUMERATE(toStringTag, to_string_tag) \ + __JS_ENUMERATE(dispose, dispose) #define JS_ENUMERATE_REGEXP_FLAGS \ __JS_ENUMERATE(hasIndices, has_indices, d) \ diff --git a/Userland/Libraries/LibJS/Tests/builtins/Symbol/well-known-symbol-existence.js b/Userland/Libraries/LibJS/Tests/builtins/Symbol/well-known-symbol-existence.js index 204e69882c22e9..9a66f974af2203 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Symbol/well-known-symbol-existence.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Symbol/well-known-symbol-existence.js @@ -12,4 +12,5 @@ test("basic functionality", () => { expect(Symbol).toHaveProperty("species"); expect(Symbol).toHaveProperty("toPrimitive"); expect(Symbol).toHaveProperty("toStringTag"); + expect(Symbol).toHaveProperty("dispose"); }); From 349110d2a953958b4ae0b512d4cae32a36bd4d95 Mon Sep 17 00:00:00 2001 From: davidot Date: Tue, 20 Dec 2022 22:09:57 +0100 Subject: [PATCH 6/8] LibJS: Add using declaration support, RAII like operation in js In this patch only top level and not the more complicated for loop using statements are supported. Also, as noted in the latest meeting of tc39 async parts of the spec are not stage 3 thus not included. --- .prettierignore | 4 + Userland/Libraries/LibJS/AST.cpp | 96 ++++- Userland/Libraries/LibJS/AST.h | 21 + Userland/Libraries/LibJS/Parser.cpp | 156 +++++-- Userland/Libraries/LibJS/Parser.h | 19 +- .../LibJS/Runtime/AbstractOperations.cpp | 157 +++++++ .../LibJS/Runtime/AbstractOperations.h | 11 + .../LibJS/Runtime/DeclarativeEnvironment.cpp | 12 +- .../LibJS/Runtime/DeclarativeEnvironment.h | 5 + .../Runtime/ECMAScriptFunctionObject.cpp | 28 +- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 + Userland/Libraries/LibJS/SourceTextModule.cpp | 13 +- .../LibJS/Tests/modules/basic-modules.js | 4 + .../LibJS/Tests/modules/top-level-dispose.mjs | 14 + .../LibJS/Tests/using-declaration.js | 386 ++++++++++++++++++ 15 files changed, 860 insertions(+), 67 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs create mode 100644 Userland/Libraries/LibJS/Tests/using-declaration.js diff --git a/.prettierignore b/.prettierignore index 4930eee75a89a8..0113d996246e5e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,7 @@ Base/home/anon/Source/js Userland/Libraries/LibJS/Tests/invalid-lhs-in-assignment.js 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/modules/top-level-dispose.mjs +Userland/Libraries/LibJS/Tests/using-declaration.js diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index 5c67c1ea443052..2b5166d08019d2 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -235,21 +235,25 @@ Completion BlockStatement::execute(Interpreter& interpreter) const auto& vm = interpreter.vm(); Environment* old_environment { nullptr }; - ArmedScopeGuard restore_environment = [&] { - vm.running_execution_context().lexical_environment = old_environment; - }; // Optimization: We only need a new lexical environment if there are any lexical declarations. :^) - if (has_lexical_declarations()) { - old_environment = vm.running_execution_context().lexical_environment; - auto block_environment = new_declarative_environment(*old_environment); - block_declaration_instantiation(interpreter, block_environment); - vm.running_execution_context().lexical_environment = block_environment; - } else { - restore_environment.disarm(); - } + if (!has_lexical_declarations()) + return evaluate_statements(interpreter); - return evaluate_statements(interpreter); + old_environment = vm.running_execution_context().lexical_environment; + auto block_environment = new_declarative_environment(*old_environment); + block_declaration_instantiation(interpreter, block_environment); + vm.running_execution_context().lexical_environment = block_environment; + + // 5. Let blockValue be the result of evaluating StatementList. + auto block_value = evaluate_statements(interpreter); + + // 6. Set blockValue to DisposeResources(blockEnv, blockValue). + block_value = dispose_resources(vm, block_environment, block_value); + + vm.running_execution_context().lexical_environment = old_environment; + + return block_value; } Completion Program::execute(Interpreter& interpreter) const @@ -3015,6 +3019,48 @@ void VariableDeclaration::dump(int indent) const declarator.dump(indent + 1); } +// 6.2.1.2 Runtime Semantics: Evaluation, https://tc39.es/proposal-explicit-resource-management/#sec-let-and-const-declarations-runtime-semantics-evaluation +Completion UsingDeclaration::execute(Interpreter& interpreter) const +{ + // 1. Let next be BindingEvaluation of BindingList with parameter sync-dispose. + InterpreterNodeScope node_scope { interpreter, *this }; + auto& vm = interpreter.vm(); + + for (auto& declarator : m_declarations) { + VERIFY(declarator.target().has>()); + VERIFY(declarator.init()); + + auto& id = declarator.target().get>(); + + // 2. ReturnIfAbrupt(next). + auto reference = TRY(id->to_reference(interpreter)); + auto initializer_result = TRY(interpreter.vm().named_evaluation_if_anonymous_function(*declarator.init(), id->string())); + VERIFY(!initializer_result.is_empty()); + TRY(reference.initialize_referenced_binding(vm, initializer_result, Environment::InitializeBindingHint::SyncDispose)); + } + + // 3. Return empty. + return normal_completion({}); +} + +ThrowCompletionOr UsingDeclaration::for_each_bound_name(ThrowCompletionOrVoidCallback&& callback) const +{ + for (auto const& entry : m_declarations) { + VERIFY(entry.target().has>()); + TRY(callback(entry.target().get>()->string())); + } + + return {}; +} + +void UsingDeclaration::dump(int indent) const +{ + ASTNode::dump(indent); + print_indent(indent + 1); + for (auto& declarator : m_declarations) + declarator.dump(indent + 1); +} + void VariableDeclarator::dump(int indent) const { ASTNode::dump(indent); @@ -4154,11 +4200,13 @@ Completion SwitchStatement::execute_impl(Interpreter& interpreter) const // 2. Let switchValue be ? GetValue(exprRef). auto switch_value = TRY(m_discriminant->execute(interpreter)).release_value(); - // 3. Let oldEnv be the running execution context's LexicalEnvironment. - auto* old_environment = interpreter.lexical_environment(); + Completion result; // Optimization: Avoid creating a lexical environment if there are no lexical declarations. if (has_lexical_declarations()) { + // 3. Let oldEnv be the running execution context's LexicalEnvironment. + auto* old_environment = interpreter.lexical_environment(); + // 4. Let blockEnv be NewDeclarativeEnvironment(oldEnv). auto block_environment = new_declarative_environment(*old_environment); @@ -4167,15 +4215,23 @@ Completion SwitchStatement::execute_impl(Interpreter& interpreter) const // 6. Set the running execution context's LexicalEnvironment to blockEnv. vm.running_execution_context().lexical_environment = block_environment; - } - // 7. Let R be Completion(CaseBlockEvaluation of CaseBlock with argument switchValue). - auto result = case_block_evaluation(switch_value); + // 7. Let R be Completion(CaseBlockEvaluation of CaseBlock with argument switchValue). + result = case_block_evaluation(switch_value); - // 8. Set the running execution context's LexicalEnvironment to oldEnv. - vm.running_execution_context().lexical_environment = old_environment; + // 8. Let env be blockEnv's LexicalEnvironment. + // FIXME: blockEnv doesn't have a lexical env it is one?? Probably a spec issue + + // 9. Set R to DisposeResources(env, R). + result = dispose_resources(vm, block_environment, result); + + // 10. Set the running execution context's LexicalEnvironment to oldEnv. + vm.running_execution_context().lexical_environment = old_environment; + } else { + result = case_block_evaluation(switch_value); + } - // 9. Return R. + // 11. Return R. return result; } diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 8f55a475ccd577..9859dc2c9836ec 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -1719,6 +1719,27 @@ class VariableDeclaration final : public Declaration { NonnullRefPtrVector m_declarations; }; +class UsingDeclaration final : public Declaration { +public: + UsingDeclaration(SourceRange source_range, NonnullRefPtrVector declarations) + : Declaration(move(source_range)) + , m_declarations(move(declarations)) + { + } + + virtual Completion execute(Interpreter&) const override; + virtual void dump(int indent) const override; + + virtual ThrowCompletionOr for_each_bound_name(ThrowCompletionOrVoidCallback&& callback) const override; + + virtual bool is_constant_declaration() const override { return true; }; + + virtual bool is_lexical_declaration() const override { return true; } + +private: + NonnullRefPtrVector m_declarations; +}; + class ObjectProperty final : public ASTNode { public: enum class Type : u8 { diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index e8e5f8bf259d9f..50bc34e38cd704 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -253,6 +253,11 @@ class ScopePusher { return m_contains_await_expression; } + bool can_have_using_declaration() const + { + return m_scope_level != ScopeLevel::ScriptTopLevel; + } + private: void throw_identifier_declared(DeprecatedFlyString const& name, NonnullRefPtr const& declaration) { @@ -597,6 +602,14 @@ NonnullRefPtr Parser::parse_declaration() case TokenType::Let: case TokenType::Const: return parse_variable_declaration(); + case TokenType::Identifier: + if (m_state.current_token.original_value() == "using"sv) { + if (!m_state.current_scope_pusher->can_have_using_declaration()) + syntax_error("'using' not allowed outside of block, for loop or function"); + + return parse_using_declaration(); + } + [[fallthrough]]; default: expected("declaration"); consume(); @@ -2457,7 +2470,7 @@ NonnullRefPtr Parser::parse_return_statement() void Parser::parse_statement_list(ScopeNode& output_node, AllowLabelledFunction allow_labelled_functions) { while (!done()) { - if (match_declaration()) { + if (match_declaration(AllowUsingDeclaration::Yes)) { auto declaration = parse_declaration(); VERIFY(m_state.current_scope_pusher); m_state.current_scope_pusher->add_declaration(declaration); @@ -2949,7 +2962,37 @@ RefPtr Parser::parse_binding_pattern(Parser::AllowDuplicates all return pattern; } -NonnullRefPtr Parser::parse_variable_declaration(bool for_loop_variable_declaration) +RefPtr Parser::parse_lexical_binding() +{ + auto binding_start = push_start(); + + if (match_identifier()) { + auto name = consume_identifier().DeprecatedFlyString_value(); + return create_ast_node( + { m_source_code, binding_start.position(), position() }, + name); + } + if (!m_state.in_generator_function_context && match(TokenType::Yield)) { + if (m_state.strict_mode) + syntax_error("Identifier must not be a reserved word in strict mode ('yield')"); + + return create_ast_node( + { m_source_code, binding_start.position(), position() }, + consume().DeprecatedFlyString_value()); + } + if (!m_state.await_expression_is_valid && match(TokenType::Async)) { + if (m_program_type == Program::Type::Module) + syntax_error("Identifier must not be a reserved word in modules ('async')"); + + return create_ast_node( + { m_source_code, binding_start.position(), position() }, + consume().DeprecatedFlyString_value()); + } + + return {}; +} + +NonnullRefPtr Parser::parse_variable_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration) { auto rule_start = push_start(); DeclarationKind declaration_kind; @@ -2972,38 +3015,21 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l NonnullRefPtrVector declarations; for (;;) { Variant, NonnullRefPtr, Empty> target {}; - if (match_identifier()) { - auto identifier_start = push_start(); - auto name = consume_identifier().DeprecatedFlyString_value(); - target = create_ast_node( - { m_source_code, rule_start.position(), position() }, - name); - check_identifier_name_for_assignment_validity(name); - if ((declaration_kind == DeclarationKind::Let || declaration_kind == DeclarationKind::Const) && name == "let"sv) - syntax_error("Lexical binding may not be called 'let'"); - } else if (auto pattern = parse_binding_pattern(declaration_kind != DeclarationKind::Var ? AllowDuplicates::No : AllowDuplicates::Yes, AllowMemberExpressions::No)) { - target = pattern.release_nonnull(); - + if (auto pattern = parse_binding_pattern(declaration_kind != DeclarationKind::Var ? AllowDuplicates::No : AllowDuplicates::Yes, AllowMemberExpressions::No)) { if ((declaration_kind == DeclarationKind::Let || declaration_kind == DeclarationKind::Const)) { - target.get>()->for_each_bound_name([this](auto& name) { + pattern->for_each_bound_name([this](auto& name) { if (name == "let"sv) syntax_error("Lexical binding may not be called 'let'"); }); } - } else if (!m_state.in_generator_function_context && match(TokenType::Yield)) { - if (m_state.strict_mode) - syntax_error("Identifier must not be a reserved word in strict mode ('yield')"); - target = create_ast_node( - { m_source_code, rule_start.position(), position() }, - consume().DeprecatedFlyString_value()); - } else if (!m_state.await_expression_is_valid && match(TokenType::Async)) { - if (m_program_type == Program::Type::Module) - syntax_error("Identifier must not be a reserved word in modules ('async')"); + target = pattern.release_nonnull(); + } else if (auto lexical_binding = parse_lexical_binding()) { + check_identifier_name_for_assignment_validity(lexical_binding->string()); + if ((declaration_kind == DeclarationKind::Let || declaration_kind == DeclarationKind::Const) && lexical_binding->string() == "let"sv) + syntax_error("Lexical binding may not be called 'let'"); - target = create_ast_node( - { m_source_code, rule_start.position(), position() }, - consume().DeprecatedFlyString_value()); + target = lexical_binding.release_nonnull(); } if (target.has()) { @@ -3020,13 +3046,13 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l consume(); // In a for loop 'in' can be ambiguous so we do not allow it // 14.7.4 The for Statement, https://tc39.es/ecma262/#prod-ForStatement and 14.7.5 The for-in, for-of, and for-await-of Statements, https://tc39.es/ecma262/#prod-ForInOfStatement - if (for_loop_variable_declaration) + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::Yes) init = parse_expression(2, Associativity::Right, { TokenType::In }); else init = parse_expression(2); - } else if (!for_loop_variable_declaration && declaration_kind == DeclarationKind::Const) { + } else if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No && declaration_kind == DeclarationKind::Const) { syntax_error("Missing initializer in 'const' variable declaration"); - } else if (!for_loop_variable_declaration && target.has>()) { + } else if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No && target.has>()) { syntax_error("Missing initializer in destructuring assignment"); } @@ -3041,7 +3067,7 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l } break; } - if (!for_loop_variable_declaration) + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No) consume_or_insert_semicolon(); declarations.shrink_to_fit(); @@ -3050,6 +3076,55 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l return declaration; } +NonnullRefPtr Parser::parse_using_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration) +{ + // using [no LineTerminator here] BindingList[?In, ?Yield, ?Await, +Using] ; + auto rule_start = push_start(); + VERIFY(m_state.current_token.original_value() == "using"sv); + consume(TokenType::Identifier); + VERIFY(!m_state.current_token.trivia_contains_line_terminator()); + NonnullRefPtrVector declarations; + + for (;;) { + auto lexical_binding = parse_lexical_binding(); + if (!lexical_binding) { + expected("lexical binding"); + break; + } + + check_identifier_name_for_assignment_validity(lexical_binding->string()); + if (lexical_binding->string() == "let"sv) + syntax_error("Lexical binding may not be called 'let'"); + + RefPtr initializer; + if (match(TokenType::Equals)) { + consume(); + + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::Yes) + initializer = parse_expression(2, Associativity::Right, { TokenType::In }); + else + initializer = parse_expression(2); + } else if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No) { + consume(TokenType::Equals); + } + + declarations.append(create_ast_node( + { m_source_code, rule_start.position(), position() }, + lexical_binding.release_nonnull(), + move(initializer))); + + if (match(TokenType::Comma)) { + consume(); + continue; + } + break; + } + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No) + consume_or_insert_semicolon(); + + return create_ast_node({ m_source_code, rule_start.position(), position() }, move(declarations)); +} + NonnullRefPtr Parser::parse_throw_statement() { auto rule_start = push_start(); @@ -3499,7 +3574,7 @@ NonnullRefPtr Parser::parse_for_statement() RefPtr init; if (!match(TokenType::Semicolon)) { if (match_variable_declaration()) { - auto declaration = parse_variable_declaration(true); + auto declaration = parse_variable_declaration(IsForLoopVariableDeclaration::Yes); if (declaration->declaration_kind() == DeclarationKind::Var) { m_state.current_scope_pusher->add_declaration(declaration); } else { @@ -3763,7 +3838,7 @@ bool Parser::match_export_or_import() const || type == TokenType::Import; } -bool Parser::match_declaration() const +bool Parser::match_declaration(AllowUsingDeclaration allow_using) const { auto type = m_state.current_token.type(); @@ -3776,6 +3851,9 @@ bool Parser::match_declaration() const return lookahead_token.type() == TokenType::Function && !lookahead_token.trivia_contains_line_terminator(); } + if (allow_using == AllowUsingDeclaration::Yes && type == TokenType::Identifier && m_state.current_token.original_value() == "using"sv) + return try_match_using_declaration(); + return type == TokenType::Function || type == TokenType::Class || type == TokenType::Const @@ -3810,6 +3888,18 @@ bool Parser::try_match_let_declaration() const return false; } +bool Parser::try_match_using_declaration() const +{ + VERIFY(m_state.current_token.type() == TokenType::Identifier); + VERIFY(m_state.current_token.original_value() == "using"sv); + + auto token_after = next_token(); + if (token_after.trivia_contains_line_terminator()) + return false; + + return token_after.is_identifier_name(); +} + bool Parser::match_variable_declaration() const { auto type = m_state.current_token.type(); diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index e0f399b84d51b2..57a8ea08bd1302 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -88,7 +88,15 @@ class Parser { NonnullRefPtr parse_block_statement(); NonnullRefPtr parse_function_body(Vector const& parameters, FunctionKind function_kind, bool& contains_direct_call_to_eval); NonnullRefPtr parse_return_statement(); - NonnullRefPtr parse_variable_declaration(bool for_loop_variable_declaration = false); + + enum class IsForLoopVariableDeclaration { + No, + Yes + }; + + NonnullRefPtr parse_variable_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration = IsForLoopVariableDeclaration::No); + RefPtr parse_lexical_binding(); + NonnullRefPtr parse_using_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration = IsForLoopVariableDeclaration::No); NonnullRefPtr parse_for_statement(); enum class IsForAwaitLoop { @@ -208,8 +216,15 @@ class Parser { bool match_statement() const; bool match_export_or_import() const; bool match_assert_clause() const; - bool match_declaration() const; + + enum class AllowUsingDeclaration { + No, + Yes + }; + + bool match_declaration(AllowUsingDeclaration allow_using = AllowUsingDeclaration::No) const; bool try_match_let_declaration() const; + bool try_match_using_declaration() const; bool match_variable_declaration() const; bool match_identifier() const; bool match_identifier_name() const; diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp index dcfe8bec7d875b..bad2a4cf740fd0 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -31,6 +31,7 @@ #include #include #include +#include namespace JS { @@ -1316,4 +1317,160 @@ ThrowCompletionOr get_substitution(VM& vm, Utf16View const& matched, Utf return TRY_OR_THROW_OOM(vm, Utf16View { result }.to_utf8()); } +// 2.1.2 AddDisposableResource ( disposable, V, hint [ , method ] ), https://tc39.es/proposal-explicit-resource-management/#sec-adddisposableresource-disposable-v-hint-disposemethod +ThrowCompletionOr add_disposable_resource(VM& vm, Vector& disposable, Value value, Environment::InitializeBindingHint hint, FunctionObject* method) +{ + // NOTE: For now only sync is a valid hint + VERIFY(hint == Environment::InitializeBindingHint::SyncDispose); + + Optional resource; + + // 1. If method is not present then, + if (!method) { + // a. If V is null or undefined, return NormalCompletion(empty). + if (value.is_nullish()) + return {}; + + // b. If Type(V) is not Object, throw a TypeError exception. + if (!value.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, value.to_string_without_side_effects()); + + // c. Let resource be ? CreateDisposableResource(V, hint). + resource = TRY(create_disposable_resource(vm, value, hint)); + } + // 2. Else, + else { + // a. If V is null or undefined, then + if (value.is_nullish()) { + // i. Let resource be ? CreateDisposableResource(undefined, hint, method). + resource = TRY(create_disposable_resource(vm, js_undefined(), hint, method)); + } + // b. Else, + else { + // i. If Type(V) is not Object, throw a TypeError exception. + if (!value.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, value.to_string_without_side_effects()); + + // ii. Let resource be ? CreateDisposableResource(V, hint, method). + resource = TRY(create_disposable_resource(vm, value, hint, method)); + } + } + + // 3. Append resource to disposable.[[DisposableResourceStack]]. + VERIFY(resource.has_value()); + disposable.append(resource.release_value()); + + // 4. Return NormalCompletion(empty). + return {}; +} + +// 2.1.3 CreateDisposableResource ( V, hint [ , method ] ), https://tc39.es/proposal-explicit-resource-management/#sec-createdisposableresource +ThrowCompletionOr create_disposable_resource(VM& vm, Value value, Environment::InitializeBindingHint hint, FunctionObject* method) +{ + // 1. If method is not present, then + if (!method) { + // a. If V is undefined, throw a TypeError exception. + if (value.is_undefined()) + return vm.throw_completion(ErrorType::IsUndefined, "value"); + + // b. Set method to ? GetDisposeMethod(V, hint). + method = TRY(get_dispose_method(vm, value, hint)); + + // c. If method is undefined, throw a TypeError exception. + if (!method) + return vm.throw_completion(ErrorType::NoDisposeMethod, value.to_string_without_side_effects()); + } + // 2. Else, + // a. If IsCallable(method) is false, throw a TypeError exception. + // NOTE: This is guaranteed to never occur from the type. + VERIFY(method); + + // 3. Return the DisposableResource Record { [[ResourceValue]]: V, [[Hint]]: hint, [[DisposeMethod]]: method }. + // NOTE: Since we only support sync dispose we don't store the hint for now. + VERIFY(hint == Environment::InitializeBindingHint::SyncDispose); + return DisposableResource { + value, + *method + }; +} + +// 2.1.4 GetDisposeMethod ( V, hint ), https://tc39.es/proposal-explicit-resource-management/#sec-getdisposemethod +ThrowCompletionOr> get_dispose_method(VM& vm, Value value, Environment::InitializeBindingHint hint) +{ + // NOTE: We only have sync dispose for now which means we ignore step 1. + VERIFY(hint == Environment::InitializeBindingHint::SyncDispose); + + // 2. Else, + // a. Let method be ? GetMethod(V, @@dispose). + return GCPtr { TRY(value.get_method(vm, *vm.well_known_symbol_dispose())) }; +} + +// 2.1.5 Dispose ( V, hint, method ), https://tc39.es/proposal-explicit-resource-management/#sec-dispose +Completion dispose(VM& vm, Value value, NonnullGCPtr method) +{ + // 1. Let result be ? Call(method, V). + [[maybe_unused]] auto result = TRY(call(vm, *method, value)); + + // NOTE: Hint can only be sync-dispose so we ignore step 2. + // 2. If hint is async-dispose and result is not undefined, then + // a. Perform ? Await(result). + + // 3. Return undefined. + return js_undefined(); +} + +// 2.1.6 DisposeResources ( disposable, completion ), https://tc39.es/proposal-explicit-resource-management/#sec-disposeresources-disposable-completion-errors +Completion dispose_resources(VM& vm, Vector const& disposable, Completion completion) +{ + // 1. If disposable is not undefined, then + // NOTE: At this point disposable is always defined. + + // a. For each resource of disposable.[[DisposableResourceStack]], in reverse list order, do + for (auto const& resource : disposable.in_reverse()) { + // i. Let result be Dispose(resource.[[ResourceValue]], resource.[[Hint]], resource.[[DisposeMethod]]). + auto result = dispose(vm, resource.resource_value, resource.dispose_method); + + // ii. If result.[[Type]] is throw, then + if (result.is_error()) { + // 1. If completion.[[Type]] is throw, then + if (completion.is_error()) { + // a. Set result to result.[[Value]]. + + // b. Let suppressed be completion.[[Value]]. + auto suppressed = completion.value().value(); + + // c. Let error be a newly created SuppressedError object. + auto error = SuppressedError::create(*vm.current_realm()); + + // d. Perform ! DefinePropertyOrThrow(error, "error", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: result }). + MUST(error->define_property_or_throw(vm.names.error, { .value = result.value(), .writable = true, .enumerable = true, .configurable = true })); + + // e. Perform ! DefinePropertyOrThrow(error, "suppressed", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: suppressed }). + MUST(error->define_property_or_throw(vm.names.suppressed, { .value = suppressed, .writable = true, .enumerable = false, .configurable = true })); + + // f. Set completion to ThrowCompletion(error). + completion = throw_completion(error); + } + // 2. Else, + else { + // a. Set completion to result. + completion = result; + } + } + } + + // 2. Return completion. + return completion; +} + +Completion dispose_resources(VM& vm, GCPtr disposable, Completion completion) +{ + // 1. If disposable is not undefined, then + if (disposable) + return dispose_resources(vm, disposable->disposable_resource_stack(), completion); + + // 2. Return completion. + return completion; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/AbstractOperations.h index bfb9f42452b6f0..a72915e65104a3 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.h @@ -42,6 +42,17 @@ ThrowCompletionOr get_prototype_from_constructor(VM&, FunctionObject co Object* create_unmapped_arguments_object(VM&, Span arguments); Object* create_mapped_arguments_object(VM&, FunctionObject&, Vector const&, Span arguments, Environment&); +struct DisposableResource { + Value resource_value; + NonnullGCPtr dispose_method; +}; +ThrowCompletionOr add_disposable_resource(VM&, Vector& disposable, Value, Environment::InitializeBindingHint, FunctionObject* = nullptr); +ThrowCompletionOr create_disposable_resource(VM&, Value, Environment::InitializeBindingHint, FunctionObject* method = nullptr); +ThrowCompletionOr> get_dispose_method(VM&, Value, Environment::InitializeBindingHint); +Completion dispose(VM& vm, Value, NonnullGCPtr method); +Completion dispose_resources(VM& vm, Vector const& disposable, Completion completion); +Completion dispose_resources(VM& vm, GCPtr disposable, Completion completion); + enum class CanonicalIndexMode { DetectNumericRoundtrip, IgnoreNumericRoundtrip, diff --git a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp index 08904e5ad4d068..3f7365c7e433c9 100644 --- a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp +++ b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -42,6 +43,11 @@ void DeclarativeEnvironment::visit_edges(Visitor& visitor) Base::visit_edges(visitor); for (auto& binding : m_bindings) visitor.visit(binding.value); + + for (auto& disposable : m_disposable_resource_stack) { + visitor.visit(disposable.resource_value); + visitor.visit(disposable.dispose_method); + } } // 9.1.1.1.1 HasBinding ( N ), https://tc39.es/ecma262/#sec-declarative-environment-records-hasbinding-n @@ -97,7 +103,7 @@ ThrowCompletionOr DeclarativeEnvironment::create_immutable_binding(VM&, De // 9.1.1.1.4 InitializeBinding ( N, V ), https://tc39.es/ecma262/#sec-declarative-environment-records-initializebinding-n-v // 4.1.1.1.1 InitializeBinding ( N, V, hint ), https://tc39.es/proposal-explicit-resource-management/#sec-declarative-environment-records -ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM&, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint) +ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint hint) { auto binding_and_index = find_binding_and_index(name); VERIFY(binding_and_index.has_value()); @@ -106,7 +112,9 @@ ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM&, Deprecat // 1. Assert: envRec must have an uninitialized binding for N. VERIFY(binding.initialized == false); - // FIXME: 2. If hint is not normal, perform ? AddDisposableResource(envRec, V, hint). + // 2. If hint is not normal, perform ? AddDisposableResource(envRec, V, hint). + if (hint != Environment::InitializeBindingHint::Normal) + TRY(add_disposable_resource(vm, m_disposable_resource_stack, value, hint)); // 3. Set the bound value for N in envRec to V. binding.value = value; diff --git a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h index 5d792edfed52c7..afd0ef53b1dcf2 100644 --- a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h +++ b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,9 @@ class DeclarativeEnvironment : public Environment { ThrowCompletionOr get_binding_value_direct(VM&, Binding&, bool strict); ThrowCompletionOr set_mutable_binding_direct(VM&, Binding&, Value, bool strict); + friend Completion dispose_resources(VM&, GCPtr, Completion); + Vector const& disposable_resource_stack() const { return m_disposable_resource_stack; } + protected: DeclarativeEnvironment(); explicit DeclarativeEnvironment(Environment* parent_environment); @@ -116,6 +120,7 @@ class DeclarativeEnvironment : public Environment { virtual bool is_declarative_environment() const override { return true; } Vector m_bindings; + Vector m_disposable_resource_stack; }; template<> diff --git a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index ab5b4bff951466..ec974298af09ac 100644 --- a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -736,7 +736,7 @@ void async_block_start(VM& vm, NonnullRefPtr const& async_body, Promi auto& running_context = vm.running_execution_context(); // 3. Set the code evaluation state of asyncContext such that when evaluation is resumed for that execution context the following steps will be performed: - auto execution_steps = NativeFunction::create(realm, "", [&async_body, &promise_capability](auto& vm) -> ThrowCompletionOr { + auto execution_steps = NativeFunction::create(realm, "", [&async_body, &promise_capability, &async_context](auto& vm) -> ThrowCompletionOr { // a. Let result be the result of evaluating asyncBody. auto result = async_body->execute(vm.interpreter()); @@ -745,17 +745,24 @@ void async_block_start(VM& vm, NonnullRefPtr const& async_body, Promi // c. Remove asyncContext from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context. vm.pop_execution_context(); - // d. If result.[[Type]] is normal, then + // d. Let env be asyncContext's LexicalEnvironment. + auto* env = async_context.lexical_environment; + VERIFY(is(env)); + + // e. Set result to DisposeResources(env, result). + result = dispose_resources(vm, static_cast(env), result); + + // f. If result.[[Type]] is normal, then if (result.type() == Completion::Type::Normal) { // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « undefined »). MUST(call(vm, *promise_capability.resolve(), js_undefined(), js_undefined())); } - // e. Else if result.[[Type]] is return, then + // g. Else if result.[[Type]] is return, then else if (result.type() == Completion::Type::Return) { // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »). MUST(call(vm, *promise_capability.resolve(), js_undefined(), *result.value())); } - // f. Else, + // h. Else, else { // i. Assert: result.[[Type]] is throw. VERIFY(result.type() == Completion::Type::Throw); @@ -763,7 +770,7 @@ void async_block_start(VM& vm, NonnullRefPtr const& async_body, Promi // ii. Perform ! Call(promiseCapability.[[Reject]], undefined, « result.[[Value]] »). MUST(call(vm, *promise_capability.reject(), js_undefined(), *result.value())); } - // g. Return unused. + // i. Return unused. // NOTE: We don't support returning an empty/optional/unused value here. return js_undefined(); }); @@ -882,8 +889,15 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body() // 1. Perform ? FunctionDeclarationInstantiation(functionObject, argumentsList). TRY(function_declaration_instantiation(ast_interpreter)); - // 2. Return the result of evaluating FunctionStatementList. - return m_ecmascript_code->execute(*ast_interpreter); + // 2. Let result be result of evaluating FunctionStatementList. + auto result = m_ecmascript_code->execute(*ast_interpreter); + + // 3. Let env be the running execution context's LexicalEnvironment. + auto* env = vm.running_execution_context().lexical_environment; + VERIFY(is(env)); + + // 4. Return ? DisposeResources(env, result). + return dispose_resources(vm, static_cast(env), result); } // AsyncFunctionBody : FunctionBody else if (m_kind == FunctionKind::Async) { diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 37ca52de2ec905..6e2e07d2e0a661 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -85,6 +85,7 @@ M(ModuleNotFound, "Cannot find/open module: '{}'") \ M(ModuleNotFoundNoReferencingScript, "Cannot resolve module {} without any active script or module") \ M(NegativeExponent, "Exponent must be positive") \ + M(NoDisposeMethod, "{} does not have dispose method") \ M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \ M(NotAConstructor, "{} is not a constructor") \ M(NotAFunction, "{} is not a function") \ diff --git a/Userland/Libraries/LibJS/SourceTextModule.cpp b/Userland/Libraries/LibJS/SourceTextModule.cpp index 3afb59c0dd2fd3..7c56722f6ac59f 100644 --- a/Userland/Libraries/LibJS/SourceTextModule.cpp +++ b/Userland/Libraries/LibJS/SourceTextModule.cpp @@ -687,13 +687,20 @@ ThrowCompletionOr SourceTextModule::execute_module(VM& vm, GCPtrexecute(vm.interpreter()); - // d. Suspend moduleContext and remove it from the execution context stack. + // d. Let env be moduleContext's LexicalEnvironment. + auto* env = module_context.lexical_environment; + VERIFY(is(*env)); + + // e. Set result to DisposeResources(env, result). + result = dispose_resources(vm, static_cast(env), result); + + // f. Suspend moduleContext and remove it from the execution context stack. vm.pop_execution_context(); - // e. Resume the context that is now on the top of the execution context stack as the running execution context. + // g. Resume the context that is now on the top of the execution context stack as the running execution context. // FIXME: We don't have resume yet. - // f. If result is an abrupt completion, then + // h. If result is an abrupt completion, then if (result.is_error()) { // i. Return ? result. return result; diff --git a/Userland/Libraries/LibJS/Tests/modules/basic-modules.js b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js index 769356aabe187a..8df534cd3a57f2 100644 --- a/Userland/Libraries/LibJS/Tests/modules/basic-modules.js +++ b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js @@ -206,6 +206,10 @@ describe("in- and exports", () => { test("exporting anonymous function", () => { expectModulePassed("./anon-func-decl-default-export.mjs"); }); + + test("can have top level using declarations which trigger at the end of running a module", () => { + expectModulePassed("./top-level-dispose.mjs"); + }); }); describe("loops", () => { diff --git a/Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs b/Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs new file mode 100644 index 00000000000000..16678db0ffdaed --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs @@ -0,0 +1,14 @@ +export let passed = false; +let failed = false; + +if (passed) + failed = true; + +using a = { [Symbol.dispose]() { if (!failed) passed = true; } } + +if (passed) + failed = true; + +failed = true; +// Should trigger before +using b = { [Symbol.dispose]() { if (!passed) failed = false; } } diff --git a/Userland/Libraries/LibJS/Tests/using-declaration.js b/Userland/Libraries/LibJS/Tests/using-declaration.js new file mode 100644 index 00000000000000..588683e1ee1b7d --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/using-declaration.js @@ -0,0 +1,386 @@ +describe("basic usage", () => { + test("disposes after block exit", () => { + let disposed = false; + let inBlock = false; + { + expect(disposed).toBeFalse(); + using a = { [Symbol.dispose]() { disposed = true; } }; + inBlock = true; + expect(disposed).toBeFalse(); + } + + expect(inBlock).toBeTrue(); + expect(disposed).toBeTrue(); + }); + + test("disposes in reverse order after block exit", () => { + const disposed = []; + { + expect(disposed).toHaveLength(0); + using a = { [Symbol.dispose]() { disposed.push('a'); } }; + using b = { [Symbol.dispose]() { disposed.push('b'); } }; + expect(disposed).toHaveLength(0); + } + + expect(disposed).toEqual(['b', 'a']); + }); + + test("disposes in reverse order after block exit even in same declaration", () => { + const disposed = []; + { + expect(disposed).toHaveLength(0); + using a = { [Symbol.dispose]() { disposed.push('a'); } }, + b = { [Symbol.dispose]() { disposed.push('b'); } }; + expect(disposed).toHaveLength(0); + } + + expect(disposed).toEqual(['b', 'a']); + }); +}); + +describe("behavior with exceptions", () => { + function ExpectedError(name) { this.name = name; } + + test("is run even after throw", () => { + let disposed = false; + let inBlock = false; + let inCatch = false; + try { + expect(disposed).toBeFalse(); + using a = { [Symbol.dispose]() { disposed = true; } }; + inBlock = true; + expect(disposed).toBeFalse(); + throw new ExpectedError(); + expect().fail(); + } catch (e) { + expect(disposed).toBeTrue(); + expect(e).toBeInstanceOf(ExpectedError); + inCatch = true; + } + expect(disposed).toBeTrue(); + expect(inBlock).toBeTrue(); + expect(inCatch).toBeTrue(); + }); + + test("throws error if dispose method does", () => { + let disposed = false; + let endOfTry = false; + let inCatch = false; + try { + expect(disposed).toBeFalse(); + using a = { [Symbol.dispose]() { + disposed = true; + throw new ExpectedError(); + } }; + expect(disposed).toBeFalse(); + endOfTry = true; + } catch (e) { + expect(disposed).toBeTrue(); + expect(e).toBeInstanceOf(ExpectedError); + inCatch = true; + } + expect(disposed).toBeTrue(); + expect(endOfTry).toBeTrue(); + expect(inCatch).toBeTrue(); + }); + + test("if block and using throw get suppressed error", () => { + let disposed = false; + let inCatch = false; + try { + expect(disposed).toBeFalse(); + using a = { [Symbol.dispose]() { + disposed = true; + throw new ExpectedError('dispose'); + } }; + expect(disposed).toBeFalse(); + throw new ExpectedError('throw'); + } catch (e) { + expect(disposed).toBeTrue(); + expect(e).toBeInstanceOf(SuppressedError); + expect(e.error).toBeInstanceOf(ExpectedError); + expect(e.error.name).toBe('dispose'); + expect(e.suppressed).toBeInstanceOf(ExpectedError); + expect(e.suppressed.name).toBe('throw'); + inCatch = true; + } + expect(disposed).toBeTrue(); + expect(inCatch).toBeTrue(); + }); + + test("multiple throwing disposes give suppressed error", () => { + let inCatch = false; + try { + { + using a = { [Symbol.dispose]() { + throw new ExpectedError('a'); + } }; + + using b = { [Symbol.dispose]() { + throw new ExpectedError('b'); + } }; + } + + expect().fail(); + } catch (e) { + expect(e).toBeInstanceOf(SuppressedError); + expect(e.error).toBeInstanceOf(ExpectedError); + expect(e.error.name).toBe('a'); + expect(e.suppressed).toBeInstanceOf(ExpectedError); + expect(e.suppressed.name).toBe('b'); + inCatch = true; + } + expect(inCatch).toBeTrue(); + }); + + test("3 throwing disposes give chaining suppressed error", () => { + let inCatch = false; + try { + { + using a = { [Symbol.dispose]() { + throw new ExpectedError('a'); + } }; + + using b = { [Symbol.dispose]() { + throw new ExpectedError('b'); + } }; + + using c = { [Symbol.dispose]() { + throw new ExpectedError('c'); + } }; + } + + expect().fail(); + } catch (e) { + expect(e).toBeInstanceOf(SuppressedError); + expect(e.error).toBeInstanceOf(ExpectedError); + expect(e.error.name).toBe('a'); + expect(e.suppressed).toBeInstanceOf(SuppressedError); + + const inner = e.suppressed; + + expect(inner.error).toBeInstanceOf(ExpectedError); + expect(inner.error.name).toBe('b'); + expect(inner.suppressed).toBeInstanceOf(ExpectedError); + expect(inner.suppressed.name).toBe('c'); + inCatch = true; + } + expect(inCatch).toBeTrue(); + }); + + test("normal error and multiple disposing erorrs give chaining suppressed errors", () => { + let inCatch = false; + try { + using a = { [Symbol.dispose]() { + throw new ExpectedError('a'); + } }; + + using b = { [Symbol.dispose]() { + throw new ExpectedError('b'); + } }; + + throw new ExpectedError('top'); + } catch (e) { + expect(e).toBeInstanceOf(SuppressedError); + expect(e.error).toBeInstanceOf(ExpectedError); + expect(e.error.name).toBe('a'); + expect(e.suppressed).toBeInstanceOf(SuppressedError); + + const inner = e.suppressed; + + expect(inner.error).toBeInstanceOf(ExpectedError); + expect(inner.error.name).toBe('b'); + expect(inner.suppressed).toBeInstanceOf(ExpectedError); + expect(inner.suppressed.name).toBe('top'); + inCatch = true; + } + expect(inCatch).toBeTrue(); + }); +}); + +describe("works in a bunch of scopes", () => { + test("works in block", () => { + let dispose = false; + expect(dispose).toBeFalse(); + { + expect(dispose).toBeFalse(); + using a = { [Symbol.dispose]() { dispose = true; } } + expect(dispose).toBeFalse(); + } + expect(dispose).toBeTrue(); + }); + + test("works in static class block", () => { + let dispose = false; + expect(dispose).toBeFalse(); + class A { + static { + expect(dispose).toBeFalse(); + using a = { [Symbol.dispose]() { dispose = true; } } + expect(dispose).toBeFalse(); + } + } + expect(dispose).toBeTrue(); + }); + + test("works in function", () => { + let dispose = []; + function f(val) { + const disposeLength = dispose.length; + using a = { [Symbol.dispose]() { dispose.push(val); } } + expect(dispose.length).toBe(disposeLength); + } + expect(dispose).toEqual([]); + f(0); + expect(dispose).toEqual([0]); + f(1); + expect(dispose).toEqual([0, 1]); + }); + + test("switch block is treated as full block in function", () => { + let disposeFull = []; + let disposeInner = false; + + function pusher(val) { + return { + val, + [Symbol.dispose]() { disposeFull.push(val); } + }; + } + + switch (2) { + case 3: + using notDisposed = { [Symbol.dispose]() { expect().fail("not-disposed 1"); } }; + case 2: + expect(disposeFull).toEqual([]); + using a = pusher('a'); + expect(disposeFull).toEqual([]); + + using b = pusher('b'); + expect(disposeFull).toEqual([]); + expect(b.val).toBe('b'); + + expect(disposeInner).toBeFalse(); + // fallthrough + case 1: { + expect(disposeFull).toEqual([]); + expect(disposeInner).toBeFalse(); + + using inner = { [Symbol.dispose]() { disposeInner = true; } } + + expect(disposeInner).toBeFalse(); + } + expect(disposeInner).toBeTrue(); + using c = pusher('c'); + expect(c.val).toBe('c'); + break; + case 0: + using notDisposed2 = { [Symbol.dispose]() { expect().fail("not-disposed 2"); } }; + } + + expect(disposeInner).toBeTrue(); + expect(disposeFull).toEqual(['c', 'b', 'a']); + }); +}); + +describe("invalid using bindings", () => { + test("nullish values do not throw", () => { + using a = null, b = undefined; + expect(a).toBeNull(); + expect(b).toBeUndefined(); + }); + + test("non-object throws", () => { + [0, "a", true, NaN, 4n, Symbol.dispose].forEach(value => { + expect(() => { + using v = value; + }).toThrowWithMessage(TypeError, "is not an object"); + }); + }); + + test("object without dispose throws", () => { + expect(() => { + using a = {}; + }).toThrowWithMessage(TypeError, "does not have dispose method"); + }); + + test("object with non callable dispose throws", () => { + [0, "a", true, NaN, 4n, Symbol.dispose, [], {}].forEach(value => { + expect(() => { + using a = { [Symbol.dispose]: value }; + }).toThrowWithMessage(TypeError, "is not a function"); + }); + }); +}); + +describe("using is still a valid variable name", () => { + test("var", () => { + "use strict"; + var using = 1; + expect(using).toBe(1); + }); + + test("const", () => { + "use strict"; + const using = 1; + expect(using).toBe(1); + }); + + test("let", () => { + "use strict"; + let using = 1; + expect(using).toBe(1); + }); + + test("using", () => { + "use strict"; + using using = null; + expect(using).toBeNull(); + }); + + test("function", () => { + "use strict"; + function using() { return 1; } + expect(using()).toBe(1); + }); +}); + +describe("syntax errors / werid artifacts which remain valid", () => { + test("no patterns in using", () => { + expect("using {a} = {}").not.toEval(); + expect("using a, {a} = {}").not.toEval(); + expect("using a = null, [b] = [null]").not.toEval(); + }); + + test("using with array pattern is valid array access", () => { + const using = [0, 9999]; + const a = 1; + + expect(eval("using [a] = 1")).toBe(1); + expect(using[1]).toBe(1); + + expect(eval("using [{a: a}, a] = 2")).toBe(2); + expect(using[1]).toBe(2); + + expect(eval("using [a, a] = 3")).toBe(3); + expect(using[1]).toBe(3); + + expect(eval("using [[a, a], a] = 4")).toBe(4); + expect(using[1]).toBe(4); + + expect(eval("using [2, 1, a] = 5")).toBe(5); + expect(using[1]).toBe(5); + }); + + test("declaration without initializer", () => { + expect("using a").not.toEval(); + }); + + test("no repeat declarations in single using", () => { + expect("using a = null, a = null;").not.toEval(); + }); + + test("cannot have a using declaration named let", () => { + expect("using let = null").not.toEval(); + }); +}); From d39a4cac0bbd9e09951ca54e31bba0ac6c06a795 Mon Sep 17 00:00:00 2001 From: davidot Date: Fri, 23 Dec 2022 01:45:29 +0100 Subject: [PATCH 7/8] LibJS: Add using declaration support in for and for of loops The using declarations have kind of special behavior in for loops so this is seperated. --- .prettierignore | 1 + Userland/Libraries/LibJS/AST.cpp | 197 +++++++++++---- Userland/Libraries/LibJS/AST.h | 4 + Userland/Libraries/LibJS/Parser.cpp | 82 ++++-- Userland/Libraries/LibJS/Parser.h | 1 + .../Libraries/LibJS/Tests/using-for-loops.js | 239 ++++++++++++++++++ 6 files changed, 447 insertions(+), 77 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/using-for-loops.js diff --git a/.prettierignore b/.prettierignore index 0113d996246e5e..3f5155996bb6fe 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ Userland/Libraries/LibJS/Tests/modules/failing.mjs # FIXME: Remove once prettier is updated to support using declarations. 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/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index 2b5166d08019d2..f11092b4997f43 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -764,39 +764,66 @@ Completion ForStatement::loop_evaluation(Interpreter& interpreter, Vector loop_env; if (m_init) { - if (is(*m_init) && static_cast(*m_init).declaration_kind() != DeclarationKind::Var) { - auto loop_environment = new_declarative_environment(*old_environment); - auto& declaration = static_cast(*m_init); - declaration.for_each_bound_name([&](auto const& name) { - if (declaration.declaration_kind() == DeclarationKind::Const) { - MUST(loop_environment->create_immutable_binding(vm, name, true)); + Declaration const* declaration = nullptr; + + if (is(*m_init) && static_cast(*m_init).declaration_kind() != DeclarationKind::Var) + declaration = static_cast(m_init.ptr()); + else if (is(*m_init)) + declaration = static_cast(m_init.ptr()); + + if (declaration) { + loop_env = new_declarative_environment(*old_environment); + auto is_const = declaration->is_constant_declaration(); + declaration->for_each_bound_name([&](auto const& name) { + if (is_const) { + MUST(loop_env->create_immutable_binding(vm, name, true)); } else { - MUST(loop_environment->create_mutable_binding(vm, name, false)); + MUST(loop_env->create_mutable_binding(vm, name, false)); ++per_iteration_bindings_size; } }); - interpreter.vm().running_execution_context().lexical_environment = loop_environment; + interpreter.vm().running_execution_context().lexical_environment = loop_env; } (void)TRY(m_init->execute(interpreter)); } + // 10. Let bodyResult be Completion(ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet)). + auto body_result = for_body_evaluation(interpreter, label_set, per_iteration_bindings_size); + + // 11. Set bodyResult to DisposeResources(loopEnv, bodyResult). + if (loop_env) + body_result = dispose_resources(vm, loop_env.ptr(), body_result); + + // 12. Set the running execution context's LexicalEnvironment to oldEnv. + interpreter.vm().running_execution_context().lexical_environment = old_environment; + + // 13. Return ? bodyResult. + return body_result; +} + +// 14.7.4.3 ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ), https://tc39.es/ecma262/#sec-forbodyevaluation +// 6.3.1.2 ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ), https://tc39.es/proposal-explicit-resource-management/#sec-forbodyevaluation +Completion ForStatement::for_body_evaluation(JS::Interpreter& interpreter, Vector const& label_set, size_t per_iteration_bindings_size) const +{ + auto& vm = interpreter.vm(); + // 14.7.4.4 CreatePerIterationEnvironment ( perIterationBindings ), https://tc39.es/ecma262/#sec-createperiterationenvironment // NOTE: Our implementation of this AO is heavily dependent on DeclarativeEnvironment using a Vector with constant indices. // For performance, we can take advantage of the fact that the declarations of the initialization statement are created // in the same order each time CreatePerIterationEnvironment is invoked. - auto create_per_iteration_environment = [&]() { + auto create_per_iteration_environment = [&]() -> GCPtr { // 1. If perIterationBindings has any elements, then - if (per_iteration_bindings_size == 0) - return; + if (per_iteration_bindings_size == 0) { + // 2. Return unused. + return nullptr; + } // a. Let lastIterationEnv be the running execution context's LexicalEnvironment. auto* last_iteration_env = verify_cast(interpreter.lexical_environment()); @@ -820,49 +847,66 @@ Completion ForStatement::loop_evaluation(Interpreter& interpreter, Vectorexecute(interpreter)).release_value(); + // ii. Let testValue be Completion(GetValue(testRef)). + auto test_value = m_test->execute(interpreter); - // iii. If ToBoolean(testValue) is false, return V. - if (!test_value.to_boolean()) - return last_value; + // iii. If testValue is an abrupt completion, then + if (test_value.is_abrupt()) { + // 1. Return ? DisposeResources(thisIterationEnv, testValue). + return TRY(dispose_resources(vm, this_iteration_env, test_value)); + } + // iv. Else, + // 1. Set testValue to testValue.[[Value]]. + VERIFY(test_value.value().has_value()); + + // iii. If ToBoolean(testValue) is false, return ? DisposeResources(thisIterationEnv, Completion(V)). + if (!test_value.release_value().value().to_boolean()) + return TRY(dispose_resources(vm, this_iteration_env, test_value)); } // b. Let result be the result of evaluating stmt. auto result = m_body->execute(interpreter); - // c. If LoopContinues(result, labelSet) is false, return ? UpdateEmpty(result, V). + // c. Perform ? DisposeResources(thisIterationEnv, result). + TRY(dispose_resources(vm, this_iteration_env, result)); + + // d. If LoopContinues(result, labelSet) is false, return ? UpdateEmpty(result, V). if (!loop_continues(result, label_set)) return result.update_empty(last_value); - // d. If result.[[Value]] is not empty, set V to result.[[Value]]. + // e. If result.[[Value]] is not empty, set V to result.[[Value]]. if (result.value().has_value()) last_value = *result.value(); - // e. Perform ? CreatePerIterationEnvironment(perIterationBindings). - create_per_iteration_environment(); + // f. Set thisIterationEnv to ? CreatePerIterationEnvironment(perIterationBindings). + this_iteration_env = create_per_iteration_environment(); - // f. If increment is not [empty], then + // g. If increment is not [empty], then if (m_update) { // i. Let incRef be the result of evaluating increment. - // ii. Perform ? GetValue(incRef). - (void)TRY(m_update->execute(interpreter)); + // ii. Let incrResult be Completion(GetValue(incrRef)). + auto inc_ref = m_update->execute(interpreter); + + // ii. If incrResult is an abrupt completion, then + if (inc_ref.is_abrupt()) { + // 1. Return ? DisposeResources(thisIterationEnv, incrResult). + return TRY(dispose_resources(vm, this_iteration_env, inc_ref)); + } } } @@ -914,6 +958,10 @@ struct ForInOfHeadState { auto& declaration = static_cast(*expression_lhs); VERIFY(declaration.declarations().first().target().has>()); lhs_reference = TRY(declaration.declarations().first().target().get>()->to_reference(interpreter)); + } else if (is(*expression_lhs)) { + auto& declaration = static_cast(*expression_lhs); + VERIFY(declaration.declarations().first().target().has>()); + lhs_reference = TRY(declaration.declarations().first().target().get>()->to_reference(interpreter)); } else { VERIFY(is(*expression_lhs) || is(*expression_lhs) || is(*expression_lhs)); auto& expression = static_cast(*expression_lhs); @@ -923,14 +971,18 @@ struct ForInOfHeadState { } // h. Else, else { - VERIFY(expression_lhs && is(*expression_lhs)); + VERIFY(expression_lhs && (is(*expression_lhs) || is(*expression_lhs))); iteration_environment = new_declarative_environment(*interpreter.lexical_environment()); - auto& for_declaration = static_cast(*expression_lhs); + auto& for_declaration = static_cast(*expression_lhs); + DeprecatedFlyString first_name; // 14.7.5.4 Runtime Semantics: ForDeclarationBindingInstantiation, https://tc39.es/ecma262/#sec-runtime-semantics-fordeclarationbindinginstantiation // 1. For each element name of the BoundNames of ForBinding, do for_declaration.for_each_bound_name([&](auto const& name) { + if (first_name.is_empty()) + first_name = name; + // a. If IsConstantDeclaration of LetOrConst is true, then if (for_declaration.is_constant_declaration()) { // i. Perform ! environment.CreateImmutableBinding(name, true). @@ -945,18 +997,28 @@ struct ForInOfHeadState { interpreter.vm().running_execution_context().lexical_environment = iteration_environment; if (!destructuring) { - VERIFY(for_declaration.declarations().first().target().has>()); - lhs_reference = MUST(interpreter.vm().resolve_binding(for_declaration.declarations().first().target().get>()->string())); + VERIFY(!first_name.is_empty()); + lhs_reference = MUST(interpreter.vm().resolve_binding(first_name)); } } // i. If destructuring is false, then if (!destructuring) { VERIFY(lhs_reference.has_value()); - if (lhs_kind == LexicalBinding) - return lhs_reference->initialize_referenced_binding(vm, next_value); - else + if (lhs_kind == LexicalBinding) { + // 2. If IsUsingDeclaration of lhs is true, then + if (is(expression_lhs)) { + // a. Let status be Completion(InitializeReferencedBinding(lhsRef, nextValue, sync-dispose)). + return lhs_reference->initialize_referenced_binding(vm, next_value, Environment::InitializeBindingHint::SyncDispose); + } + // 3. Else, + else { + // a. Let status be Completion(InitializeReferencedBinding(lhsRef, nextValue, normal)). + return lhs_reference->initialize_referenced_binding(vm, next_value, Environment::InitializeBindingHint::Normal); + } + } else { return lhs_reference->put_value(vm, next_value); + } } // j. Else, @@ -984,7 +1046,7 @@ static ThrowCompletionOr for_in_of_head_execute(Interpreter& i auto& vm = interpreter.vm(); ForInOfHeadState state(lhs); - if (auto* ast_ptr = lhs.get_pointer>(); ast_ptr && is(*(*ast_ptr))) { + if (auto* ast_ptr = lhs.get_pointer>(); ast_ptr && is(ast_ptr->ptr())) { // Runtime Semantics: ForInOfLoopEvaluation, for any of: // ForInOfStatement : for ( var ForBinding in Expression ) Statement // ForInOfStatement : for ( ForDeclaration in Expression ) Statement @@ -994,24 +1056,34 @@ static ThrowCompletionOr for_in_of_head_execute(Interpreter& i // 14.7.5.6 ForIn/OfHeadEvaluation ( uninitializedBoundNames, expr, iterationKind ), https://tc39.es/ecma262/#sec-runtime-semantics-forinofheadevaluation Environment* new_environment = nullptr; - auto& variable_declaration = static_cast(*(*ast_ptr)); - VERIFY(variable_declaration.declarations().size() == 1); - state.destructuring = variable_declaration.declarations().first().target().has>(); - if (variable_declaration.declaration_kind() == DeclarationKind::Var) { - state.lhs_kind = ForInOfHeadState::VarBinding; - auto& variable = variable_declaration.declarations().first(); - // B.3.5 Initializers in ForIn Statement Heads, https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads - if (variable.init()) { - VERIFY(variable.target().has>()); - auto& binding_id = variable.target().get>()->string(); - auto reference = TRY(interpreter.vm().resolve_binding(binding_id)); - auto result = TRY(interpreter.vm().named_evaluation_if_anonymous_function(*variable.init(), binding_id)); - TRY(reference.put_value(vm, result)); + if (is(ast_ptr->ptr())) { + auto& variable_declaration = static_cast(*(*ast_ptr)); + VERIFY(variable_declaration.declarations().size() == 1); + state.destructuring = variable_declaration.declarations().first().target().has>(); + if (variable_declaration.declaration_kind() == DeclarationKind::Var) { + state.lhs_kind = ForInOfHeadState::VarBinding; + auto& variable = variable_declaration.declarations().first(); + // B.3.5 Initializers in ForIn Statement Heads, https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads + if (variable.init()) { + VERIFY(variable.target().has>()); + auto& binding_id = variable.target().get>()->string(); + auto reference = TRY(interpreter.vm().resolve_binding(binding_id)); + auto result = TRY(interpreter.vm().named_evaluation_if_anonymous_function(*variable.init(), binding_id)); + TRY(reference.put_value(vm, result)); + } + } else { + state.lhs_kind = ForInOfHeadState::LexicalBinding; + new_environment = new_declarative_environment(*interpreter.lexical_environment()); + variable_declaration.for_each_bound_name([&](auto const& name) { + MUST(new_environment->create_mutable_binding(vm, name, false)); + }); } } else { + VERIFY(is(ast_ptr->ptr())); + auto& declaration = static_cast(*(*ast_ptr)); state.lhs_kind = ForInOfHeadState::LexicalBinding; new_environment = new_declarative_environment(*interpreter.lexical_environment()); - variable_declaration.for_each_bound_name([&](auto const& name) { + declaration.for_each_bound_name([&](auto const& name) { MUST(new_environment->create_mutable_binding(vm, name, false)); }); } @@ -1096,16 +1168,24 @@ Completion ForInStatement::loop_evaluation(Interpreter& interpreter, Vectorexecute(interpreter); - // m. Set the running execution context's LexicalEnvironment to oldEnv. + // NOTE: Because of optimizations we only create a new lexical environment if there are bindings + // so we should only dispose if that is the case. + if (vm.running_execution_context().lexical_environment != old_environment) { + VERIFY(is(vm.running_execution_context().lexical_environment)); + // m. Set result to DisposeResources(iterationEnv, result). + result = dispose_resources(vm, static_cast(vm.running_execution_context().lexical_environment), result); + } + + // n. Set the running execution context's LexicalEnvironment to oldEnv. vm.running_execution_context().lexical_environment = old_environment; - // n. If LoopContinues(result, labelSet) is false, then + // o. If LoopContinues(result, labelSet) is false, then if (!loop_continues(result, label_set)) { // 1. Return UpdateEmpty(result, V). return result.update_empty(last_value); } - // o. If result.[[Value]] is not empty, set V to result.[[Value]]. + // p. If result.[[Value]] is not empty, set V to result.[[Value]]. if (result.value().has_value()) last_value = *result.value(); @@ -1154,6 +1234,11 @@ Completion ForOfStatement::loop_evaluation(Interpreter& interpreter, Vectorexecute(interpreter); + if (vm.running_execution_context().lexical_environment != old_environment) { + VERIFY(is(vm.running_execution_context().lexical_environment)); + result = dispose_resources(vm, static_cast(vm.running_execution_context().lexical_environment), result); + } + // m. Set the running execution context's LexicalEnvironment to oldEnv. vm.running_execution_context().lexical_environment = old_environment; diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 9859dc2c9836ec..5490a6f9229356 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -922,6 +922,8 @@ class ForStatement final : public IterationStatement { virtual Bytecode::CodeGenerationErrorOr generate_labelled_evaluation(Bytecode::Generator&, Vector const&) const override; private: + Completion for_body_evaluation(Interpreter&, Vector const&, size_t per_iteration_bindings_size) const; + RefPtr m_init; RefPtr m_test; RefPtr m_update; @@ -1736,6 +1738,8 @@ class UsingDeclaration final : public Declaration { virtual bool is_lexical_declaration() const override { return true; } + NonnullRefPtrVector const& declarations() const { return m_declarations; } + private: NonnullRefPtrVector m_declarations; }; diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index 50bc34e38cd704..426b996793d4f4 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -3573,7 +3573,38 @@ NonnullRefPtr Parser::parse_for_statement() RefPtr init; if (!match(TokenType::Semicolon)) { - if (match_variable_declaration()) { + + auto match_for_using_declaration = [&] { + if (!match(TokenType::Identifier) || m_state.current_token.original_value() != "using"sv) + return false; + + auto lookahead = next_token(); + if (lookahead.trivia_contains_line_terminator()) + return false; + + if (lookahead.original_value() == "of"sv) + return false; + + return token_is_identifier(lookahead); + }; + + if (match_for_using_declaration()) { + auto declaration = parse_using_declaration(IsForLoopVariableDeclaration::Yes); + + if (match_of(m_state.current_token)) { + if (declaration->declarations().size() != 1) + syntax_error("Must have exactly one declaration in for using of"); + else if (declaration->declarations().first().init()) + syntax_error("Using declaration cannot have initializer"); + + return parse_for_in_of_statement(move(declaration), is_await_loop); + } + + if (match(TokenType::In)) + syntax_error("Using declaration not allowed in for-in loop"); + + init = move(declaration); + } else if (match_variable_declaration()) { auto declaration = parse_variable_declaration(IsForLoopVariableDeclaration::Yes); if (declaration->declaration_kind() == DeclarationKind::Var) { m_state.current_scope_pusher->add_declaration(declaration); @@ -3586,15 +3617,22 @@ NonnullRefPtr Parser::parse_for_statement() }); } - init = move(declaration); - if (match_for_in_of()) - return parse_for_in_of_statement(*init, is_await_loop); - if (static_cast(*init).declaration_kind() == DeclarationKind::Const) { - for (auto& variable : static_cast(*init).declarations()) { + if (match_for_in_of()) { + if (declaration->declarations().size() > 1) + syntax_error("Multiple declarations not allowed in for..in/of"); + else if (declaration->declarations().size() < 1) + syntax_error("Need exactly one variable declaration in for..in/of"); + + return parse_for_in_of_statement(move(declaration), is_await_loop); + } + if (declaration->declaration_kind() == DeclarationKind::Const) { + for (auto const& variable : declaration->declarations()) { if (!variable.init()) syntax_error("Missing initializer in 'const' variable declaration"); } } + + init = move(declaration); } else if (match_expression()) { auto lookahead_token = next_token(); bool starts_with_async_of = match(TokenType::Async) && match_of(lookahead_token); @@ -3641,11 +3679,8 @@ NonnullRefPtr Parser::parse_for_in_of_statement(NonnullRefPtr(*lhs)) { auto& declaration = static_cast(*lhs); - if (declaration.declarations().size() > 1) { - syntax_error("Multiple declarations not allowed in for..in/of"); - } else if (declaration.declarations().size() < 1) { - syntax_error("Need exactly one variable declaration in for..in/of"); - } else { + // Syntax errors for wrong amounts of declaration should have already been hit. + if (!declaration.declarations().is_empty()) { // AnnexB extension B.3.5 Initializers in ForIn Statement Heads, https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads auto& variable = declaration.declarations().first(); if (variable.init()) { @@ -3655,7 +3690,7 @@ NonnullRefPtr Parser::parse_for_in_of_statement(NonnullRefPtris_identifier() && !is(*lhs) && !is(*lhs)) { + } else if (!lhs->is_identifier() && !is(*lhs) && !is(*lhs) && !is(*lhs)) { bool valid = false; if (is(*lhs) || is(*lhs)) { auto synthesized_binding_pattern = synthesize_binding_pattern(static_cast(*lhs)); @@ -3915,21 +3950,26 @@ bool Parser::match_variable_declaration() const bool Parser::match_identifier() const { - if (m_state.current_token.type() == TokenType::EscapedKeyword) { - if (m_state.current_token.value() == "let"sv) + return token_is_identifier(m_state.current_token); +} + +bool Parser::token_is_identifier(Token const& token) const +{ + if (token.type() == TokenType::EscapedKeyword) { + if (token.value() == "let"sv) return !m_state.strict_mode; - if (m_state.current_token.value() == "yield"sv) + if (token.value() == "yield"sv) return !m_state.strict_mode && !m_state.in_generator_function_context; - if (m_state.current_token.value() == "await"sv) + if (token.value() == "await"sv) return m_program_type != Program::Type::Module && !m_state.await_expression_is_valid && !m_state.in_class_static_init_block; return true; } - return m_state.current_token.type() == TokenType::Identifier - || m_state.current_token.type() == TokenType::Async - || (m_state.current_token.type() == TokenType::Let && !m_state.strict_mode) - || (m_state.current_token.type() == TokenType::Await && m_program_type != Program::Type::Module && !m_state.await_expression_is_valid && !m_state.in_class_static_init_block) - || (m_state.current_token.type() == TokenType::Yield && !m_state.in_generator_function_context && !m_state.strict_mode); // See note in Parser::parse_identifier(). + return token.type() == TokenType::Identifier + || token.type() == TokenType::Async + || (token.type() == TokenType::Let && !m_state.strict_mode) + || (token.type() == TokenType::Await && m_program_type != Program::Type::Module && !m_state.await_expression_is_valid && !m_state.in_class_static_init_block) + || (token.type() == TokenType::Yield && !m_state.in_generator_function_context && !m_state.strict_mode); // See note in Parser::parse_identifier(). } bool Parser::match_identifier_name() const diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index 57a8ea08bd1302..0e6692fc229d73 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -227,6 +227,7 @@ class Parser { bool try_match_using_declaration() const; bool match_variable_declaration() const; bool match_identifier() const; + bool token_is_identifier(Token const&) const; bool match_identifier_name() const; bool match_property_key() const; bool is_private_identifier_valid() const; diff --git a/Userland/Libraries/LibJS/Tests/using-for-loops.js b/Userland/Libraries/LibJS/Tests/using-for-loops.js new file mode 100644 index 00000000000000..1acd277d74db07 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/using-for-loops.js @@ -0,0 +1,239 @@ +describe("basic usage", () => { + test("using in normal for loop", () => { + let isDisposed = false; + let lastI = -1; + for ( + using x = { + i: 0, + tick() { + this.i++; + }, + done() { + return this.i === 3; + }, + [Symbol.dispose]() { + isDisposed = true; + }, + }; + !x.done(); + x.tick() + ) { + expect(isDisposed).toBeFalse(); + expect(x.i).toBeGreaterThan(lastI); + lastI = x.i; + } + + expect(isDisposed).toBeTrue(); + expect(lastI).toBe(2); + }); + + test("using in normal for loop with expression body", () => { + let isDisposed = false; + let outerI = 0; + for ( + using x = { + i: 0, + tick() { + this.i++; + outerI++; + }, + done() { + return this.i === 3; + }, + [Symbol.dispose]() { + isDisposed = true; + }, + }; + !x.done(); + x.tick() + ) + expect(isDisposed).toBeFalse(); + + expect(isDisposed).toBeTrue(); + expect(outerI).toBe(3); + }); + + test("using in for of loop", () => { + const disposable = []; + const values = []; + + function createDisposable(value) { + return { + value: value, + [Symbol.dispose]() { + expect(this.value).toBe(value); + disposable.push(value); + } + }; + } + + for (using a of [createDisposable('a'), createDisposable('b'), createDisposable('c')]) { + expect(disposable).toEqual(values); + values.push(a.value); + } + + expect(disposable).toEqual(['a', 'b', 'c']); + }); + + test("using in for of loop with expression body", () => { + let disposableCalls = 0; + let i = 0; + + const obj = { + [Symbol.dispose]() { + disposableCalls++; + } + }; + + for (using a of [obj, obj, obj]) + expect(disposableCalls).toBe(i++); + + expect(disposableCalls).toBe(3); + }); + + test("can have multiple declaration in normal for loop", () => { + let disposed = 0; + const a = { + [Symbol.dispose]() { + disposed++; + } + } + + expect(disposed).toBe(0); + for (using b = a, c = a; false;) + expect().fail(); + + expect(disposed).toBe(2); + }); + + test("can have using in block in for loop", () => { + const disposed = []; + const values = []; + for (let i = 0; i < 3; i++) { + using a = { + val: i, + [Symbol.dispose]() { + expect(i).toBe(this.val); + disposed.push(i); + }, + }; + expect(disposed).toEqual(values); + values.push(i); + } + expect(disposed).toEqual([0, 1, 2]); + }); + + test("can have using in block in for-in loop", () => { + const disposed = []; + const values = []; + for (const i in ['a', 'b', 'c']) { + using a = { + val: i, + [Symbol.dispose]() { + expect(i).toBe(this.val); + disposed.push(i); + }, + }; + expect(disposed).toEqual(values); + values.push(i); + } + expect(disposed).toEqual(["0", "1", "2"]); + }); + + test("dispose is called even if throw in for of loop", () => { + let disposableCalls = 0; + + const obj = { + [Symbol.dispose]() { + expect() + disposableCalls++; + } + }; + + try { + for (using a of [obj]) + throw new ExpectationError("Expected in for-of"); + + expect().fail("Should have thrown"); + } catch (e) { + expect(e).toBeInstanceOf(ExpectationError); + expect(e.message).toBe("Expected in for-of"); + expect(disposableCalls).toBe(1); + } + + expect(disposableCalls).toBe(1); + }); +}); + +describe("using is still a valid variable in loops", () => { + test("for loops var", () => { + let enteredLoop = false; + for (var using = 1; using < 2; using++) { + enteredLoop = true; + } + expect(enteredLoop).toBeTrue(); + }); + + test("for loops const", () => { + let enteredLoop = false; + for (const using = 1; using < 2; ) { + enteredLoop = true; + break; + } + expect(enteredLoop).toBeTrue(); + }); + + test("for loops let", () => { + let enteredLoop = false; + for (let using = 1; using < 2; using++) { + enteredLoop = true; + } + expect(enteredLoop).toBeTrue(); + }); + + test("using in", () => { + let enteredLoop = false; + for (using in [1]) { + enteredLoop = true; + expect(using).toBe("0"); + } + expect(enteredLoop).toBeTrue(); + }); + + test("using of", () => { + let enteredLoop = false; + for (using of [1]) { + enteredLoop = true; + expect(using).toBe(1); + } + expect(enteredLoop).toBeTrue(); + }); + + test("using using of", () => { + let enteredLoop = false; + for (using using of [null]) { + enteredLoop = true; + expect(using).toBeNull(); + } + expect(enteredLoop).toBeTrue(); + }); +}); + +describe("syntax errors", () => { + test("cannot have using as for loop body", () => { + expect("for (;;) using a = {};").not.toEval(); + expect("for (x in []) using a = {};").not.toEval(); + expect("for (x of []) using a = {};").not.toEval(); + }); + + test("must have one declaration without initializer in for loop", () => { + expect("for (using x = {} of []) {}").not.toEval(); + expect("for (using x, y of []) {}").not.toEval(); + }); + + test("cannot have using in for-in loop", () => { + expect("for (using x in []) {}").not.toEval(); + expect("for (using of in []) {}").not.toEval(); + expect("for (using in of []) {}").not.toEval(); + }); +}); From 04b432a092f13e63a50af2bb4ac0cfd3a935be69 Mon Sep 17 00:00:00 2001 From: davidot Date: Wed, 21 Dec 2022 11:48:14 +0100 Subject: [PATCH 8/8] 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" + ); + }); + }); +});