diff --git a/.prettierignore b/.prettierignore index 4930eee75a..0113d99624 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,7 @@ Base/home/anon/Source/js Userland/Libraries/LibJS/Tests/invalid-lhs-in-assignment.js Userland/Libraries/LibJS/Tests/unicode-identifier-escape.js Userland/Libraries/LibJS/Tests/modules/failing.mjs + +# FIXME: Remove once prettier is updated to support using declarations. +Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs +Userland/Libraries/LibJS/Tests/using-declaration.js diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index 5c67c1ea44..2b5166d080 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -235,21 +235,25 @@ Completion BlockStatement::execute(Interpreter& interpreter) const auto& vm = interpreter.vm(); Environment* old_environment { nullptr }; - ArmedScopeGuard restore_environment = [&] { - vm.running_execution_context().lexical_environment = old_environment; - }; // Optimization: We only need a new lexical environment if there are any lexical declarations. :^) - if (has_lexical_declarations()) { - old_environment = vm.running_execution_context().lexical_environment; - auto block_environment = new_declarative_environment(*old_environment); - block_declaration_instantiation(interpreter, block_environment); - vm.running_execution_context().lexical_environment = block_environment; - } else { - restore_environment.disarm(); - } + if (!has_lexical_declarations()) + return evaluate_statements(interpreter); - return evaluate_statements(interpreter); + old_environment = vm.running_execution_context().lexical_environment; + auto block_environment = new_declarative_environment(*old_environment); + block_declaration_instantiation(interpreter, block_environment); + vm.running_execution_context().lexical_environment = block_environment; + + // 5. Let blockValue be the result of evaluating StatementList. + auto block_value = evaluate_statements(interpreter); + + // 6. Set blockValue to DisposeResources(blockEnv, blockValue). + block_value = dispose_resources(vm, block_environment, block_value); + + vm.running_execution_context().lexical_environment = old_environment; + + return block_value; } Completion Program::execute(Interpreter& interpreter) const @@ -3015,6 +3019,48 @@ void VariableDeclaration::dump(int indent) const declarator.dump(indent + 1); } +// 6.2.1.2 Runtime Semantics: Evaluation, https://tc39.es/proposal-explicit-resource-management/#sec-let-and-const-declarations-runtime-semantics-evaluation +Completion UsingDeclaration::execute(Interpreter& interpreter) const +{ + // 1. Let next be BindingEvaluation of BindingList with parameter sync-dispose. + InterpreterNodeScope node_scope { interpreter, *this }; + auto& vm = interpreter.vm(); + + for (auto& declarator : m_declarations) { + VERIFY(declarator.target().has>()); + VERIFY(declarator.init()); + + auto& id = declarator.target().get>(); + + // 2. ReturnIfAbrupt(next). + auto reference = TRY(id->to_reference(interpreter)); + auto initializer_result = TRY(interpreter.vm().named_evaluation_if_anonymous_function(*declarator.init(), id->string())); + VERIFY(!initializer_result.is_empty()); + TRY(reference.initialize_referenced_binding(vm, initializer_result, Environment::InitializeBindingHint::SyncDispose)); + } + + // 3. Return empty. + return normal_completion({}); +} + +ThrowCompletionOr UsingDeclaration::for_each_bound_name(ThrowCompletionOrVoidCallback&& callback) const +{ + for (auto const& entry : m_declarations) { + VERIFY(entry.target().has>()); + TRY(callback(entry.target().get>()->string())); + } + + return {}; +} + +void UsingDeclaration::dump(int indent) const +{ + ASTNode::dump(indent); + print_indent(indent + 1); + for (auto& declarator : m_declarations) + declarator.dump(indent + 1); +} + void VariableDeclarator::dump(int indent) const { ASTNode::dump(indent); @@ -4154,11 +4200,13 @@ Completion SwitchStatement::execute_impl(Interpreter& interpreter) const // 2. Let switchValue be ? GetValue(exprRef). auto switch_value = TRY(m_discriminant->execute(interpreter)).release_value(); - // 3. Let oldEnv be the running execution context's LexicalEnvironment. - auto* old_environment = interpreter.lexical_environment(); + Completion result; // Optimization: Avoid creating a lexical environment if there are no lexical declarations. if (has_lexical_declarations()) { + // 3. Let oldEnv be the running execution context's LexicalEnvironment. + auto* old_environment = interpreter.lexical_environment(); + // 4. Let blockEnv be NewDeclarativeEnvironment(oldEnv). auto block_environment = new_declarative_environment(*old_environment); @@ -4167,15 +4215,23 @@ Completion SwitchStatement::execute_impl(Interpreter& interpreter) const // 6. Set the running execution context's LexicalEnvironment to blockEnv. vm.running_execution_context().lexical_environment = block_environment; + + // 7. Let R be Completion(CaseBlockEvaluation of CaseBlock with argument switchValue). + result = case_block_evaluation(switch_value); + + // 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); } - // 7. Let R be Completion(CaseBlockEvaluation of CaseBlock with argument switchValue). - auto result = case_block_evaluation(switch_value); - - // 8. Set the running execution context's LexicalEnvironment to oldEnv. - vm.running_execution_context().lexical_environment = old_environment; - - // 9. Return R. + // 11. Return R. return result; } diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 8f55a475cc..9859dc2c98 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -1719,6 +1719,27 @@ private: NonnullRefPtrVector m_declarations; }; +class UsingDeclaration final : public Declaration { +public: + UsingDeclaration(SourceRange source_range, NonnullRefPtrVector declarations) + : Declaration(move(source_range)) + , m_declarations(move(declarations)) + { + } + + virtual Completion execute(Interpreter&) const override; + virtual void dump(int indent) const override; + + virtual ThrowCompletionOr for_each_bound_name(ThrowCompletionOrVoidCallback&& callback) const override; + + virtual bool is_constant_declaration() const override { return true; }; + + virtual bool is_lexical_declaration() const override { return true; } + +private: + NonnullRefPtrVector m_declarations; +}; + class ObjectProperty final : public ASTNode { public: enum class Type : u8 { diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index e8e5f8bf25..50bc34e38c 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -253,6 +253,11 @@ public: return m_contains_await_expression; } + bool can_have_using_declaration() const + { + return m_scope_level != ScopeLevel::ScriptTopLevel; + } + private: void throw_identifier_declared(DeprecatedFlyString const& name, NonnullRefPtr const& declaration) { @@ -597,6 +602,14 @@ NonnullRefPtr Parser::parse_declaration() case TokenType::Let: case TokenType::Const: return parse_variable_declaration(); + case TokenType::Identifier: + if (m_state.current_token.original_value() == "using"sv) { + if (!m_state.current_scope_pusher->can_have_using_declaration()) + syntax_error("'using' not allowed outside of block, for loop or function"); + + return parse_using_declaration(); + } + [[fallthrough]]; default: expected("declaration"); consume(); @@ -2457,7 +2470,7 @@ NonnullRefPtr Parser::parse_return_statement() void Parser::parse_statement_list(ScopeNode& output_node, AllowLabelledFunction allow_labelled_functions) { while (!done()) { - if (match_declaration()) { + if (match_declaration(AllowUsingDeclaration::Yes)) { auto declaration = parse_declaration(); VERIFY(m_state.current_scope_pusher); m_state.current_scope_pusher->add_declaration(declaration); @@ -2949,7 +2962,37 @@ RefPtr Parser::parse_binding_pattern(Parser::AllowDuplicates all return pattern; } -NonnullRefPtr Parser::parse_variable_declaration(bool for_loop_variable_declaration) +RefPtr Parser::parse_lexical_binding() +{ + auto binding_start = push_start(); + + if (match_identifier()) { + auto name = consume_identifier().DeprecatedFlyString_value(); + return create_ast_node( + { m_source_code, binding_start.position(), position() }, + name); + } + if (!m_state.in_generator_function_context && match(TokenType::Yield)) { + if (m_state.strict_mode) + syntax_error("Identifier must not be a reserved word in strict mode ('yield')"); + + return create_ast_node( + { m_source_code, binding_start.position(), position() }, + consume().DeprecatedFlyString_value()); + } + if (!m_state.await_expression_is_valid && match(TokenType::Async)) { + if (m_program_type == Program::Type::Module) + syntax_error("Identifier must not be a reserved word in modules ('async')"); + + return create_ast_node( + { m_source_code, binding_start.position(), position() }, + consume().DeprecatedFlyString_value()); + } + + return {}; +} + +NonnullRefPtr Parser::parse_variable_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration) { auto rule_start = push_start(); DeclarationKind declaration_kind; @@ -2972,38 +3015,21 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l NonnullRefPtrVector declarations; for (;;) { Variant, NonnullRefPtr, Empty> target {}; - if (match_identifier()) { - auto identifier_start = push_start(); - auto name = consume_identifier().DeprecatedFlyString_value(); - target = create_ast_node( - { m_source_code, rule_start.position(), position() }, - name); - check_identifier_name_for_assignment_validity(name); - if ((declaration_kind == DeclarationKind::Let || declaration_kind == DeclarationKind::Const) && name == "let"sv) - syntax_error("Lexical binding may not be called 'let'"); - } else if (auto pattern = parse_binding_pattern(declaration_kind != DeclarationKind::Var ? AllowDuplicates::No : AllowDuplicates::Yes, AllowMemberExpressions::No)) { - target = pattern.release_nonnull(); - + if (auto pattern = parse_binding_pattern(declaration_kind != DeclarationKind::Var ? AllowDuplicates::No : AllowDuplicates::Yes, AllowMemberExpressions::No)) { if ((declaration_kind == DeclarationKind::Let || declaration_kind == DeclarationKind::Const)) { - target.get>()->for_each_bound_name([this](auto& name) { + pattern->for_each_bound_name([this](auto& name) { if (name == "let"sv) syntax_error("Lexical binding may not be called 'let'"); }); } - } else if (!m_state.in_generator_function_context && match(TokenType::Yield)) { - if (m_state.strict_mode) - syntax_error("Identifier must not be a reserved word in strict mode ('yield')"); - target = create_ast_node( - { m_source_code, rule_start.position(), position() }, - consume().DeprecatedFlyString_value()); - } else if (!m_state.await_expression_is_valid && match(TokenType::Async)) { - if (m_program_type == Program::Type::Module) - syntax_error("Identifier must not be a reserved word in modules ('async')"); + target = pattern.release_nonnull(); + } else if (auto lexical_binding = parse_lexical_binding()) { + check_identifier_name_for_assignment_validity(lexical_binding->string()); + if ((declaration_kind == DeclarationKind::Let || declaration_kind == DeclarationKind::Const) && lexical_binding->string() == "let"sv) + syntax_error("Lexical binding may not be called 'let'"); - target = create_ast_node( - { m_source_code, rule_start.position(), position() }, - consume().DeprecatedFlyString_value()); + target = lexical_binding.release_nonnull(); } if (target.has()) { @@ -3020,13 +3046,13 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l consume(); // In a for loop 'in' can be ambiguous so we do not allow it // 14.7.4 The for Statement, https://tc39.es/ecma262/#prod-ForStatement and 14.7.5 The for-in, for-of, and for-await-of Statements, https://tc39.es/ecma262/#prod-ForInOfStatement - if (for_loop_variable_declaration) + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::Yes) init = parse_expression(2, Associativity::Right, { TokenType::In }); else init = parse_expression(2); - } else if (!for_loop_variable_declaration && declaration_kind == DeclarationKind::Const) { + } else if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No && declaration_kind == DeclarationKind::Const) { syntax_error("Missing initializer in 'const' variable declaration"); - } else if (!for_loop_variable_declaration && target.has>()) { + } else if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No && target.has>()) { syntax_error("Missing initializer in destructuring assignment"); } @@ -3041,7 +3067,7 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l } break; } - if (!for_loop_variable_declaration) + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No) consume_or_insert_semicolon(); declarations.shrink_to_fit(); @@ -3050,6 +3076,55 @@ NonnullRefPtr Parser::parse_variable_declaration(bool for_l return declaration; } +NonnullRefPtr Parser::parse_using_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration) +{ + // using [no LineTerminator here] BindingList[?In, ?Yield, ?Await, +Using] ; + auto rule_start = push_start(); + VERIFY(m_state.current_token.original_value() == "using"sv); + consume(TokenType::Identifier); + VERIFY(!m_state.current_token.trivia_contains_line_terminator()); + NonnullRefPtrVector declarations; + + for (;;) { + auto lexical_binding = parse_lexical_binding(); + if (!lexical_binding) { + expected("lexical binding"); + break; + } + + check_identifier_name_for_assignment_validity(lexical_binding->string()); + if (lexical_binding->string() == "let"sv) + syntax_error("Lexical binding may not be called 'let'"); + + RefPtr initializer; + if (match(TokenType::Equals)) { + consume(); + + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::Yes) + initializer = parse_expression(2, Associativity::Right, { TokenType::In }); + else + initializer = parse_expression(2); + } else if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No) { + consume(TokenType::Equals); + } + + declarations.append(create_ast_node( + { m_source_code, rule_start.position(), position() }, + lexical_binding.release_nonnull(), + move(initializer))); + + if (match(TokenType::Comma)) { + consume(); + continue; + } + break; + } + if (is_for_loop_variable_declaration == IsForLoopVariableDeclaration::No) + consume_or_insert_semicolon(); + + return create_ast_node({ m_source_code, rule_start.position(), position() }, move(declarations)); +} + NonnullRefPtr Parser::parse_throw_statement() { auto rule_start = push_start(); @@ -3499,7 +3574,7 @@ NonnullRefPtr Parser::parse_for_statement() RefPtr init; if (!match(TokenType::Semicolon)) { if (match_variable_declaration()) { - auto declaration = parse_variable_declaration(true); + auto declaration = parse_variable_declaration(IsForLoopVariableDeclaration::Yes); if (declaration->declaration_kind() == DeclarationKind::Var) { m_state.current_scope_pusher->add_declaration(declaration); } else { @@ -3763,7 +3838,7 @@ bool Parser::match_export_or_import() const || type == TokenType::Import; } -bool Parser::match_declaration() const +bool Parser::match_declaration(AllowUsingDeclaration allow_using) const { auto type = m_state.current_token.type(); @@ -3776,6 +3851,9 @@ bool Parser::match_declaration() const return lookahead_token.type() == TokenType::Function && !lookahead_token.trivia_contains_line_terminator(); } + if (allow_using == AllowUsingDeclaration::Yes && type == TokenType::Identifier && m_state.current_token.original_value() == "using"sv) + return try_match_using_declaration(); + return type == TokenType::Function || type == TokenType::Class || type == TokenType::Const @@ -3810,6 +3888,18 @@ bool Parser::try_match_let_declaration() const return false; } +bool Parser::try_match_using_declaration() const +{ + VERIFY(m_state.current_token.type() == TokenType::Identifier); + VERIFY(m_state.current_token.original_value() == "using"sv); + + auto token_after = next_token(); + if (token_after.trivia_contains_line_terminator()) + return false; + + return token_after.is_identifier_name(); +} + bool Parser::match_variable_declaration() const { auto type = m_state.current_token.type(); diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index e0f399b84d..57a8ea08bd 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -88,7 +88,15 @@ public: NonnullRefPtr parse_block_statement(); NonnullRefPtr parse_function_body(Vector const& parameters, FunctionKind function_kind, bool& contains_direct_call_to_eval); NonnullRefPtr parse_return_statement(); - NonnullRefPtr parse_variable_declaration(bool for_loop_variable_declaration = false); + + enum class IsForLoopVariableDeclaration { + No, + Yes + }; + + NonnullRefPtr parse_variable_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration = IsForLoopVariableDeclaration::No); + RefPtr parse_lexical_binding(); + NonnullRefPtr parse_using_declaration(IsForLoopVariableDeclaration is_for_loop_variable_declaration = IsForLoopVariableDeclaration::No); NonnullRefPtr parse_for_statement(); enum class IsForAwaitLoop { @@ -208,8 +216,15 @@ private: bool match_statement() const; bool match_export_or_import() const; bool match_assert_clause() const; - bool match_declaration() const; + + enum class AllowUsingDeclaration { + No, + Yes + }; + + bool match_declaration(AllowUsingDeclaration allow_using = AllowUsingDeclaration::No) const; bool try_match_let_declaration() const; + bool try_match_using_declaration() const; bool match_variable_declaration() const; bool match_identifier() const; bool match_identifier_name() const; diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp index dcfe8bec7d..bad2a4cf74 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -31,6 +31,7 @@ #include #include #include +#include namespace JS { @@ -1316,4 +1317,160 @@ ThrowCompletionOr get_substitution(VM& vm, Utf16View const& matched, Utf return TRY_OR_THROW_OOM(vm, Utf16View { result }.to_utf8()); } +// 2.1.2 AddDisposableResource ( disposable, V, hint [ , method ] ), https://tc39.es/proposal-explicit-resource-management/#sec-adddisposableresource-disposable-v-hint-disposemethod +ThrowCompletionOr add_disposable_resource(VM& vm, Vector& disposable, Value value, Environment::InitializeBindingHint hint, FunctionObject* method) +{ + // NOTE: For now only sync is a valid hint + VERIFY(hint == Environment::InitializeBindingHint::SyncDispose); + + Optional resource; + + // 1. If method is not present then, + if (!method) { + // a. If V is null or undefined, return NormalCompletion(empty). + if (value.is_nullish()) + return {}; + + // b. If Type(V) is not Object, throw a TypeError exception. + if (!value.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, value.to_string_without_side_effects()); + + // c. Let resource be ? CreateDisposableResource(V, hint). + resource = TRY(create_disposable_resource(vm, value, hint)); + } + // 2. Else, + else { + // a. If V is null or undefined, then + if (value.is_nullish()) { + // i. Let resource be ? CreateDisposableResource(undefined, hint, method). + resource = TRY(create_disposable_resource(vm, js_undefined(), hint, method)); + } + // b. Else, + else { + // i. If Type(V) is not Object, throw a TypeError exception. + if (!value.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, value.to_string_without_side_effects()); + + // ii. Let resource be ? CreateDisposableResource(V, hint, method). + resource = TRY(create_disposable_resource(vm, value, hint, method)); + } + } + + // 3. Append resource to disposable.[[DisposableResourceStack]]. + VERIFY(resource.has_value()); + disposable.append(resource.release_value()); + + // 4. Return NormalCompletion(empty). + return {}; +} + +// 2.1.3 CreateDisposableResource ( V, hint [ , method ] ), https://tc39.es/proposal-explicit-resource-management/#sec-createdisposableresource +ThrowCompletionOr create_disposable_resource(VM& vm, Value value, Environment::InitializeBindingHint hint, FunctionObject* method) +{ + // 1. If method is not present, then + if (!method) { + // a. If V is undefined, throw a TypeError exception. + if (value.is_undefined()) + return vm.throw_completion(ErrorType::IsUndefined, "value"); + + // b. Set method to ? GetDisposeMethod(V, hint). + method = TRY(get_dispose_method(vm, value, hint)); + + // c. If method is undefined, throw a TypeError exception. + if (!method) + return vm.throw_completion(ErrorType::NoDisposeMethod, value.to_string_without_side_effects()); + } + // 2. Else, + // a. If IsCallable(method) is false, throw a TypeError exception. + // NOTE: This is guaranteed to never occur from the type. + VERIFY(method); + + // 3. Return the DisposableResource Record { [[ResourceValue]]: V, [[Hint]]: hint, [[DisposeMethod]]: method }. + // NOTE: Since we only support sync dispose we don't store the hint for now. + VERIFY(hint == Environment::InitializeBindingHint::SyncDispose); + return DisposableResource { + value, + *method + }; +} + +// 2.1.4 GetDisposeMethod ( V, hint ), https://tc39.es/proposal-explicit-resource-management/#sec-getdisposemethod +ThrowCompletionOr> get_dispose_method(VM& vm, Value value, Environment::InitializeBindingHint hint) +{ + // NOTE: We only have sync dispose for now which means we ignore step 1. + VERIFY(hint == Environment::InitializeBindingHint::SyncDispose); + + // 2. Else, + // a. Let method be ? GetMethod(V, @@dispose). + return GCPtr { TRY(value.get_method(vm, *vm.well_known_symbol_dispose())) }; +} + +// 2.1.5 Dispose ( V, hint, method ), https://tc39.es/proposal-explicit-resource-management/#sec-dispose +Completion dispose(VM& vm, Value value, NonnullGCPtr method) +{ + // 1. Let result be ? Call(method, V). + [[maybe_unused]] auto result = TRY(call(vm, *method, value)); + + // NOTE: Hint can only be sync-dispose so we ignore step 2. + // 2. If hint is async-dispose and result is not undefined, then + // a. Perform ? Await(result). + + // 3. Return undefined. + return js_undefined(); +} + +// 2.1.6 DisposeResources ( disposable, completion ), https://tc39.es/proposal-explicit-resource-management/#sec-disposeresources-disposable-completion-errors +Completion dispose_resources(VM& vm, Vector const& disposable, Completion completion) +{ + // 1. If disposable is not undefined, then + // NOTE: At this point disposable is always defined. + + // a. For each resource of disposable.[[DisposableResourceStack]], in reverse list order, do + for (auto const& resource : disposable.in_reverse()) { + // i. Let result be Dispose(resource.[[ResourceValue]], resource.[[Hint]], resource.[[DisposeMethod]]). + auto result = dispose(vm, resource.resource_value, resource.dispose_method); + + // ii. If result.[[Type]] is throw, then + if (result.is_error()) { + // 1. If completion.[[Type]] is throw, then + if (completion.is_error()) { + // a. Set result to result.[[Value]]. + + // b. Let suppressed be completion.[[Value]]. + auto suppressed = completion.value().value(); + + // c. Let error be a newly created SuppressedError object. + auto error = SuppressedError::create(*vm.current_realm()); + + // d. Perform ! DefinePropertyOrThrow(error, "error", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: result }). + MUST(error->define_property_or_throw(vm.names.error, { .value = result.value(), .writable = true, .enumerable = true, .configurable = true })); + + // e. Perform ! DefinePropertyOrThrow(error, "suppressed", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: suppressed }). + MUST(error->define_property_or_throw(vm.names.suppressed, { .value = suppressed, .writable = true, .enumerable = false, .configurable = true })); + + // f. Set completion to ThrowCompletion(error). + completion = throw_completion(error); + } + // 2. Else, + else { + // a. Set completion to result. + completion = result; + } + } + } + + // 2. Return completion. + return completion; +} + +Completion dispose_resources(VM& vm, GCPtr disposable, Completion completion) +{ + // 1. If disposable is not undefined, then + if (disposable) + return dispose_resources(vm, disposable->disposable_resource_stack(), completion); + + // 2. Return completion. + return completion; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/AbstractOperations.h index bfb9f42452..a72915e651 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.h @@ -42,6 +42,17 @@ ThrowCompletionOr get_prototype_from_constructor(VM&, FunctionObject co Object* create_unmapped_arguments_object(VM&, Span arguments); Object* create_mapped_arguments_object(VM&, FunctionObject&, Vector const&, Span arguments, Environment&); +struct DisposableResource { + Value resource_value; + NonnullGCPtr dispose_method; +}; +ThrowCompletionOr add_disposable_resource(VM&, Vector& disposable, Value, Environment::InitializeBindingHint, FunctionObject* = nullptr); +ThrowCompletionOr create_disposable_resource(VM&, Value, Environment::InitializeBindingHint, FunctionObject* method = nullptr); +ThrowCompletionOr> get_dispose_method(VM&, Value, Environment::InitializeBindingHint); +Completion dispose(VM& vm, Value, NonnullGCPtr method); +Completion dispose_resources(VM& vm, Vector const& disposable, Completion completion); +Completion dispose_resources(VM& vm, GCPtr disposable, Completion completion); + enum class CanonicalIndexMode { DetectNumericRoundtrip, IgnoreNumericRoundtrip, diff --git a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp index 08904e5ad4..3f7365c7e4 100644 --- a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp +++ b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -42,6 +43,11 @@ void DeclarativeEnvironment::visit_edges(Visitor& visitor) Base::visit_edges(visitor); for (auto& binding : m_bindings) visitor.visit(binding.value); + + for (auto& disposable : m_disposable_resource_stack) { + visitor.visit(disposable.resource_value); + visitor.visit(disposable.dispose_method); + } } // 9.1.1.1.1 HasBinding ( N ), https://tc39.es/ecma262/#sec-declarative-environment-records-hasbinding-n @@ -97,7 +103,7 @@ ThrowCompletionOr DeclarativeEnvironment::create_immutable_binding(VM&, De // 9.1.1.1.4 InitializeBinding ( N, V ), https://tc39.es/ecma262/#sec-declarative-environment-records-initializebinding-n-v // 4.1.1.1.1 InitializeBinding ( N, V, hint ), https://tc39.es/proposal-explicit-resource-management/#sec-declarative-environment-records -ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM&, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint) +ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM& vm, DeprecatedFlyString const& name, Value value, Environment::InitializeBindingHint hint) { auto binding_and_index = find_binding_and_index(name); VERIFY(binding_and_index.has_value()); @@ -106,7 +112,9 @@ ThrowCompletionOr DeclarativeEnvironment::initialize_binding(VM&, Deprecat // 1. Assert: envRec must have an uninitialized binding for N. VERIFY(binding.initialized == false); - // FIXME: 2. If hint is not normal, perform ? AddDisposableResource(envRec, V, hint). + // 2. If hint is not normal, perform ? AddDisposableResource(envRec, V, hint). + if (hint != Environment::InitializeBindingHint::Normal) + TRY(add_disposable_resource(vm, m_disposable_resource_stack, value, hint)); // 3. Set the bound value for N in envRec to V. binding.value = value; diff --git a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h index 5d792edfed..afd0ef53b1 100644 --- a/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h +++ b/Userland/Libraries/LibJS/Runtime/DeclarativeEnvironment.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,9 @@ private: ThrowCompletionOr get_binding_value_direct(VM&, Binding&, bool strict); ThrowCompletionOr set_mutable_binding_direct(VM&, Binding&, Value, bool strict); + friend Completion dispose_resources(VM&, GCPtr, Completion); + Vector const& disposable_resource_stack() const { return m_disposable_resource_stack; } + protected: DeclarativeEnvironment(); explicit DeclarativeEnvironment(Environment* parent_environment); @@ -116,6 +120,7 @@ private: virtual bool is_declarative_environment() const override { return true; } Vector m_bindings; + Vector m_disposable_resource_stack; }; template<> diff --git a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index ab5b4bff95..ec974298af 100644 --- a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -736,7 +736,7 @@ void async_block_start(VM& vm, NonnullRefPtr const& async_body, Promi auto& running_context = vm.running_execution_context(); // 3. Set the code evaluation state of asyncContext such that when evaluation is resumed for that execution context the following steps will be performed: - auto execution_steps = NativeFunction::create(realm, "", [&async_body, &promise_capability](auto& vm) -> ThrowCompletionOr { + auto execution_steps = NativeFunction::create(realm, "", [&async_body, &promise_capability, &async_context](auto& vm) -> ThrowCompletionOr { // a. Let result be the result of evaluating asyncBody. auto result = async_body->execute(vm.interpreter()); @@ -745,17 +745,24 @@ void async_block_start(VM& vm, NonnullRefPtr const& async_body, Promi // c. Remove asyncContext from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context. vm.pop_execution_context(); - // d. If result.[[Type]] is normal, then + // d. Let env be asyncContext's LexicalEnvironment. + auto* env = async_context.lexical_environment; + VERIFY(is(env)); + + // e. Set result to DisposeResources(env, result). + result = dispose_resources(vm, static_cast(env), result); + + // f. If result.[[Type]] is normal, then if (result.type() == Completion::Type::Normal) { // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « undefined »). MUST(call(vm, *promise_capability.resolve(), js_undefined(), js_undefined())); } - // e. Else if result.[[Type]] is return, then + // g. Else if result.[[Type]] is return, then else if (result.type() == Completion::Type::Return) { // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »). MUST(call(vm, *promise_capability.resolve(), js_undefined(), *result.value())); } - // f. Else, + // h. Else, else { // i. Assert: result.[[Type]] is throw. VERIFY(result.type() == Completion::Type::Throw); @@ -763,7 +770,7 @@ void async_block_start(VM& vm, NonnullRefPtr const& async_body, Promi // ii. Perform ! Call(promiseCapability.[[Reject]], undefined, « result.[[Value]] »). MUST(call(vm, *promise_capability.reject(), js_undefined(), *result.value())); } - // g. Return unused. + // i. Return unused. // NOTE: We don't support returning an empty/optional/unused value here. return js_undefined(); }); @@ -882,8 +889,15 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body() // 1. Perform ? FunctionDeclarationInstantiation(functionObject, argumentsList). TRY(function_declaration_instantiation(ast_interpreter)); - // 2. Return the result of evaluating FunctionStatementList. - return m_ecmascript_code->execute(*ast_interpreter); + // 2. Let result be result of evaluating FunctionStatementList. + auto result = m_ecmascript_code->execute(*ast_interpreter); + + // 3. Let env be the running execution context's LexicalEnvironment. + auto* env = vm.running_execution_context().lexical_environment; + VERIFY(is(env)); + + // 4. Return ? DisposeResources(env, result). + return dispose_resources(vm, static_cast(env), result); } // AsyncFunctionBody : FunctionBody else if (m_kind == FunctionKind::Async) { diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 37ca52de2e..6e2e07d2e0 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -85,6 +85,7 @@ M(ModuleNotFound, "Cannot find/open module: '{}'") \ M(ModuleNotFoundNoReferencingScript, "Cannot resolve module {} without any active script or module") \ M(NegativeExponent, "Exponent must be positive") \ + M(NoDisposeMethod, "{} does not have dispose method") \ M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \ M(NotAConstructor, "{} is not a constructor") \ M(NotAFunction, "{} is not a function") \ diff --git a/Userland/Libraries/LibJS/SourceTextModule.cpp b/Userland/Libraries/LibJS/SourceTextModule.cpp index 3afb59c0dd..7c56722f6a 100644 --- a/Userland/Libraries/LibJS/SourceTextModule.cpp +++ b/Userland/Libraries/LibJS/SourceTextModule.cpp @@ -687,13 +687,20 @@ ThrowCompletionOr SourceTextModule::execute_module(VM& vm, GCPtrexecute(vm.interpreter()); - // d. Suspend moduleContext and remove it from the execution context stack. + // d. Let env be moduleContext's LexicalEnvironment. + auto* env = module_context.lexical_environment; + VERIFY(is(*env)); + + // e. Set result to DisposeResources(env, result). + result = dispose_resources(vm, static_cast(env), result); + + // f. Suspend moduleContext and remove it from the execution context stack. vm.pop_execution_context(); - // e. Resume the context that is now on the top of the execution context stack as the running execution context. + // g. Resume the context that is now on the top of the execution context stack as the running execution context. // FIXME: We don't have resume yet. - // f. If result is an abrupt completion, then + // h. If result is an abrupt completion, then if (result.is_error()) { // i. Return ? result. return result; diff --git a/Userland/Libraries/LibJS/Tests/modules/basic-modules.js b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js index 769356aabe..8df534cd3a 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 0000000000..16678db0ff --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs @@ -0,0 +1,14 @@ +export let passed = false; +let failed = false; + +if (passed) + failed = true; + +using a = { [Symbol.dispose]() { if (!failed) passed = true; } } + +if (passed) + failed = true; + +failed = true; +// Should trigger before +using b = { [Symbol.dispose]() { if (!passed) failed = false; } } diff --git a/Userland/Libraries/LibJS/Tests/using-declaration.js b/Userland/Libraries/LibJS/Tests/using-declaration.js new file mode 100644 index 0000000000..588683e1ee --- /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(); + }); +});