From 541637e15aa395ca571222aa15015533ae8f5f4b Mon Sep 17 00:00:00 2001 From: davidot Date: Tue, 20 Dec 2022 22:09:57 +0100 Subject: [PATCH] LibJS: Add using declaration support, RAII like operation in js In this patch only top level and not the more complicated for loop using statements are supported. Also, as noted in the latest meeting of tc39 async parts of the spec are not stage 3 thus not included. --- .prettierignore | 4 + Userland/Libraries/LibJS/AST.cpp | 98 ++++- Userland/Libraries/LibJS/AST.h | 21 + Userland/Libraries/LibJS/Parser.cpp | 156 +++++-- Userland/Libraries/LibJS/Parser.h | 19 +- .../LibJS/Runtime/AbstractOperations.cpp | 157 +++++++ .../LibJS/Runtime/AbstractOperations.h | 11 + .../LibJS/Runtime/DeclarativeEnvironment.cpp | 12 +- .../LibJS/Runtime/DeclarativeEnvironment.h | 5 + .../Runtime/ECMAScriptFunctionObject.cpp | 28 +- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 + Userland/Libraries/LibJS/SourceTextModule.cpp | 13 +- .../LibJS/Tests/modules/basic-modules.js | 4 + .../LibJS/Tests/modules/top-level-dispose.mjs | 14 + .../LibJS/Tests/using-declaration.js | 386 ++++++++++++++++++ 15 files changed, 861 insertions(+), 68 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs create mode 100644 Userland/Libraries/LibJS/Tests/using-declaration.js diff --git a/.prettierignore b/.prettierignore index 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(); + }); +});