diff --git a/.prettierignore b/.prettierignore index 4930eee75a89a8..0d31829d6e4636 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,9 @@ 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/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/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index dc4b5b0a8ed9ca..f11092b4997f43 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 @@ -323,7 +327,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; } @@ -760,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()); @@ -816,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)); + } } } @@ -910,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); @@ -919,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). @@ -941,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, @@ -980,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 @@ -990,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)); }); } @@ -1092,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(); @@ -1150,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; @@ -1973,7 +2062,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); @@ -3015,6 +3104,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); @@ -3864,7 +3995,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); @@ -4154,11 +4285,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 +4300,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..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; @@ -1719,6 +1721,29 @@ 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; } + + NonnullRefPtrVector const& declarations() const { return m_declarations; } + +private: + NonnullRefPtrVector m_declarations; +}; + class ObjectProperty final : public ASTNode { public: enum class Type : u8 { diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index a9f5381144fe0c..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 @@ -193,6 +196,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..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) \ @@ -39,6 +40,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) \ @@ -132,7 +134,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/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index e8e5f8bf259d9f..426b996793d4f4 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(); @@ -3498,8 +3573,39 @@ NonnullRefPtr Parser::parse_for_statement() RefPtr init; if (!match(TokenType::Semicolon)) { - if (match_variable_declaration()) { - auto declaration = parse_variable_declaration(true); + + 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); } else { @@ -3511,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); @@ -3566,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()) { @@ -3580,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)); @@ -3763,7 +3873,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 +3886,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 +3923,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(); @@ -3825,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 e0f399b84d51b2..0e6692fc229d73 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,10 +216,18 @@ 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 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/Runtime/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp index b904fec61267c3..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 { @@ -205,8 +206,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 +731,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 +906,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 +1006,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 +1036,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)); } } } @@ -1314,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/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 34f7dfed23f5e8..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) \ @@ -489,6 +493,7 @@ namespace JS { P(substring) \ P(subtract) \ P(sup) \ + P(suppressed) \ P(supportedLocalesOf) \ P(supportedValuesOf) \ P(symmetricDifference) \ @@ -554,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/DeclarativeEnvironment.cpp b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp index d427780ed9997e..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 @@ -96,26 +102,27 @@ 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& vm, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint hint) { 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. + // 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; - // 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 +139,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 +227,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..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 @@ -34,7 +35,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,10 +61,12 @@ 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); + 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); @@ -117,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/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/ECMAScriptFunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index 2c7c270886580d..ec974298af09ac 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); } @@ -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/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/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 37ca52de2ec905..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") \ @@ -85,6 +86,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/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/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index 83f29af207999d..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 @@ -63,6 +64,7 @@ #include #include #include +#include #include #include #include @@ -134,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(); }); @@ -154,6 +157,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..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 @@ -89,6 +91,8 @@ #include #include #include +#include +#include #include #include #include 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/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/SourceTextModule.cpp b/Userland/Libraries/LibJS/SourceTextModule.cpp index 2664002911ff70..7c56722f6ac59f 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)); } }); }); @@ -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/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. 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" + ); + }); + }); +}); 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"); + }); +}); 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"); }); 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/test-common.js b/Userland/Libraries/LibJS/Tests/test-common.js index 0c177e72808595..1ecfa8c0b673d4 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)}` + ); }); } @@ -199,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 ?? ""}` ); }); } 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(); + }); +}); 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(); + }); +});