diff --git a/.prettierignore b/.prettierignore index 0113d99624..3f5155996b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ Userland/Libraries/LibJS/Tests/modules/failing.mjs # FIXME: Remove once prettier is updated to support using declarations. Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs Userland/Libraries/LibJS/Tests/using-declaration.js +Userland/Libraries/LibJS/Tests/using-for-loops.js diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index 2b5166d080..f11092b499 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -764,39 +764,66 @@ Completion ForStatement::loop_evaluation(Interpreter& interpreter, Vector loop_env; if (m_init) { - if (is(*m_init) && static_cast(*m_init).declaration_kind() != DeclarationKind::Var) { - auto loop_environment = new_declarative_environment(*old_environment); - auto& declaration = static_cast(*m_init); - declaration.for_each_bound_name([&](auto const& name) { - if (declaration.declaration_kind() == DeclarationKind::Const) { - MUST(loop_environment->create_immutable_binding(vm, name, true)); + Declaration const* declaration = nullptr; + + if (is(*m_init) && static_cast(*m_init).declaration_kind() != DeclarationKind::Var) + declaration = static_cast(m_init.ptr()); + else if (is(*m_init)) + declaration = static_cast(m_init.ptr()); + + if (declaration) { + loop_env = new_declarative_environment(*old_environment); + auto is_const = declaration->is_constant_declaration(); + declaration->for_each_bound_name([&](auto const& name) { + if (is_const) { + MUST(loop_env->create_immutable_binding(vm, name, true)); } else { - MUST(loop_environment->create_mutable_binding(vm, name, false)); + MUST(loop_env->create_mutable_binding(vm, name, false)); ++per_iteration_bindings_size; } }); - interpreter.vm().running_execution_context().lexical_environment = loop_environment; + interpreter.vm().running_execution_context().lexical_environment = loop_env; } (void)TRY(m_init->execute(interpreter)); } + // 10. Let bodyResult be Completion(ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet)). + auto body_result = for_body_evaluation(interpreter, label_set, per_iteration_bindings_size); + + // 11. Set bodyResult to DisposeResources(loopEnv, bodyResult). + if (loop_env) + body_result = dispose_resources(vm, loop_env.ptr(), body_result); + + // 12. Set the running execution context's LexicalEnvironment to oldEnv. + interpreter.vm().running_execution_context().lexical_environment = old_environment; + + // 13. Return ? bodyResult. + return body_result; +} + +// 14.7.4.3 ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ), https://tc39.es/ecma262/#sec-forbodyevaluation +// 6.3.1.2 ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ), https://tc39.es/proposal-explicit-resource-management/#sec-forbodyevaluation +Completion ForStatement::for_body_evaluation(JS::Interpreter& interpreter, Vector const& label_set, size_t per_iteration_bindings_size) const +{ + auto& vm = interpreter.vm(); + // 14.7.4.4 CreatePerIterationEnvironment ( perIterationBindings ), https://tc39.es/ecma262/#sec-createperiterationenvironment // NOTE: Our implementation of this AO is heavily dependent on DeclarativeEnvironment using a Vector with constant indices. // For performance, we can take advantage of the fact that the declarations of the initialization statement are created // in the same order each time CreatePerIterationEnvironment is invoked. - auto create_per_iteration_environment = [&]() { + auto create_per_iteration_environment = [&]() -> GCPtr { // 1. If perIterationBindings has any elements, then - if (per_iteration_bindings_size == 0) - return; + if (per_iteration_bindings_size == 0) { + // 2. Return unused. + return nullptr; + } // a. Let lastIterationEnv be the running execution context's LexicalEnvironment. auto* last_iteration_env = verify_cast(interpreter.lexical_environment()); @@ -820,49 +847,66 @@ Completion ForStatement::loop_evaluation(Interpreter& interpreter, Vectorexecute(interpreter)).release_value(); + // ii. Let testValue be Completion(GetValue(testRef)). + auto test_value = m_test->execute(interpreter); - // iii. If ToBoolean(testValue) is false, return V. - if (!test_value.to_boolean()) - return last_value; + // iii. If testValue is an abrupt completion, then + if (test_value.is_abrupt()) { + // 1. Return ? DisposeResources(thisIterationEnv, testValue). + return TRY(dispose_resources(vm, this_iteration_env, test_value)); + } + // iv. Else, + // 1. Set testValue to testValue.[[Value]]. + VERIFY(test_value.value().has_value()); + + // iii. If ToBoolean(testValue) is false, return ? DisposeResources(thisIterationEnv, Completion(V)). + if (!test_value.release_value().value().to_boolean()) + return TRY(dispose_resources(vm, this_iteration_env, test_value)); } // b. Let result be the result of evaluating stmt. auto result = m_body->execute(interpreter); - // c. If LoopContinues(result, labelSet) is false, return ? UpdateEmpty(result, V). + // c. Perform ? DisposeResources(thisIterationEnv, result). + TRY(dispose_resources(vm, this_iteration_env, result)); + + // d. If LoopContinues(result, labelSet) is false, return ? UpdateEmpty(result, V). if (!loop_continues(result, label_set)) return result.update_empty(last_value); - // d. If result.[[Value]] is not empty, set V to result.[[Value]]. + // e. If result.[[Value]] is not empty, set V to result.[[Value]]. if (result.value().has_value()) last_value = *result.value(); - // e. Perform ? CreatePerIterationEnvironment(perIterationBindings). - create_per_iteration_environment(); + // f. Set thisIterationEnv to ? CreatePerIterationEnvironment(perIterationBindings). + this_iteration_env = create_per_iteration_environment(); - // f. If increment is not [empty], then + // g. If increment is not [empty], then if (m_update) { // i. Let incRef be the result of evaluating increment. - // ii. Perform ? GetValue(incRef). - (void)TRY(m_update->execute(interpreter)); + // ii. Let incrResult be Completion(GetValue(incrRef)). + auto inc_ref = m_update->execute(interpreter); + + // ii. If incrResult is an abrupt completion, then + if (inc_ref.is_abrupt()) { + // 1. Return ? DisposeResources(thisIterationEnv, incrResult). + return TRY(dispose_resources(vm, this_iteration_env, inc_ref)); + } } } @@ -914,6 +958,10 @@ struct ForInOfHeadState { auto& declaration = static_cast(*expression_lhs); VERIFY(declaration.declarations().first().target().has>()); lhs_reference = TRY(declaration.declarations().first().target().get>()->to_reference(interpreter)); + } else if (is(*expression_lhs)) { + auto& declaration = static_cast(*expression_lhs); + VERIFY(declaration.declarations().first().target().has>()); + lhs_reference = TRY(declaration.declarations().first().target().get>()->to_reference(interpreter)); } else { VERIFY(is(*expression_lhs) || is(*expression_lhs) || is(*expression_lhs)); auto& expression = static_cast(*expression_lhs); @@ -923,14 +971,18 @@ struct ForInOfHeadState { } // h. Else, else { - VERIFY(expression_lhs && is(*expression_lhs)); + VERIFY(expression_lhs && (is(*expression_lhs) || is(*expression_lhs))); iteration_environment = new_declarative_environment(*interpreter.lexical_environment()); - auto& for_declaration = static_cast(*expression_lhs); + auto& for_declaration = static_cast(*expression_lhs); + DeprecatedFlyString first_name; // 14.7.5.4 Runtime Semantics: ForDeclarationBindingInstantiation, https://tc39.es/ecma262/#sec-runtime-semantics-fordeclarationbindinginstantiation // 1. For each element name of the BoundNames of ForBinding, do for_declaration.for_each_bound_name([&](auto const& name) { + if (first_name.is_empty()) + first_name = name; + // a. If IsConstantDeclaration of LetOrConst is true, then if (for_declaration.is_constant_declaration()) { // i. Perform ! environment.CreateImmutableBinding(name, true). @@ -945,18 +997,28 @@ struct ForInOfHeadState { interpreter.vm().running_execution_context().lexical_environment = iteration_environment; if (!destructuring) { - VERIFY(for_declaration.declarations().first().target().has>()); - lhs_reference = MUST(interpreter.vm().resolve_binding(for_declaration.declarations().first().target().get>()->string())); + VERIFY(!first_name.is_empty()); + lhs_reference = MUST(interpreter.vm().resolve_binding(first_name)); } } // i. If destructuring is false, then if (!destructuring) { VERIFY(lhs_reference.has_value()); - if (lhs_kind == LexicalBinding) - return lhs_reference->initialize_referenced_binding(vm, next_value); - else + if (lhs_kind == LexicalBinding) { + // 2. If IsUsingDeclaration of lhs is true, then + if (is(expression_lhs)) { + // a. Let status be Completion(InitializeReferencedBinding(lhsRef, nextValue, sync-dispose)). + return lhs_reference->initialize_referenced_binding(vm, next_value, Environment::InitializeBindingHint::SyncDispose); + } + // 3. Else, + else { + // a. Let status be Completion(InitializeReferencedBinding(lhsRef, nextValue, normal)). + return lhs_reference->initialize_referenced_binding(vm, next_value, Environment::InitializeBindingHint::Normal); + } + } else { return lhs_reference->put_value(vm, next_value); + } } // j. Else, @@ -984,7 +1046,7 @@ static ThrowCompletionOr for_in_of_head_execute(Interpreter& i auto& vm = interpreter.vm(); ForInOfHeadState state(lhs); - if (auto* ast_ptr = lhs.get_pointer>(); ast_ptr && is(*(*ast_ptr))) { + if (auto* ast_ptr = lhs.get_pointer>(); ast_ptr && is(ast_ptr->ptr())) { // Runtime Semantics: ForInOfLoopEvaluation, for any of: // ForInOfStatement : for ( var ForBinding in Expression ) Statement // ForInOfStatement : for ( ForDeclaration in Expression ) Statement @@ -994,24 +1056,34 @@ static ThrowCompletionOr for_in_of_head_execute(Interpreter& i // 14.7.5.6 ForIn/OfHeadEvaluation ( uninitializedBoundNames, expr, iterationKind ), https://tc39.es/ecma262/#sec-runtime-semantics-forinofheadevaluation Environment* new_environment = nullptr; - auto& variable_declaration = static_cast(*(*ast_ptr)); - VERIFY(variable_declaration.declarations().size() == 1); - state.destructuring = variable_declaration.declarations().first().target().has>(); - if (variable_declaration.declaration_kind() == DeclarationKind::Var) { - state.lhs_kind = ForInOfHeadState::VarBinding; - auto& variable = variable_declaration.declarations().first(); - // B.3.5 Initializers in ForIn Statement Heads, https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads - if (variable.init()) { - VERIFY(variable.target().has>()); - auto& binding_id = variable.target().get>()->string(); - auto reference = TRY(interpreter.vm().resolve_binding(binding_id)); - auto result = TRY(interpreter.vm().named_evaluation_if_anonymous_function(*variable.init(), binding_id)); - TRY(reference.put_value(vm, result)); + if (is(ast_ptr->ptr())) { + auto& variable_declaration = static_cast(*(*ast_ptr)); + VERIFY(variable_declaration.declarations().size() == 1); + state.destructuring = variable_declaration.declarations().first().target().has>(); + if (variable_declaration.declaration_kind() == DeclarationKind::Var) { + state.lhs_kind = ForInOfHeadState::VarBinding; + auto& variable = variable_declaration.declarations().first(); + // B.3.5 Initializers in ForIn Statement Heads, https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads + if (variable.init()) { + VERIFY(variable.target().has>()); + auto& binding_id = variable.target().get>()->string(); + auto reference = TRY(interpreter.vm().resolve_binding(binding_id)); + auto result = TRY(interpreter.vm().named_evaluation_if_anonymous_function(*variable.init(), binding_id)); + TRY(reference.put_value(vm, result)); + } + } else { + state.lhs_kind = ForInOfHeadState::LexicalBinding; + new_environment = new_declarative_environment(*interpreter.lexical_environment()); + variable_declaration.for_each_bound_name([&](auto const& name) { + MUST(new_environment->create_mutable_binding(vm, name, false)); + }); } } else { + VERIFY(is(ast_ptr->ptr())); + auto& declaration = static_cast(*(*ast_ptr)); state.lhs_kind = ForInOfHeadState::LexicalBinding; new_environment = new_declarative_environment(*interpreter.lexical_environment()); - variable_declaration.for_each_bound_name([&](auto const& name) { + declaration.for_each_bound_name([&](auto const& name) { MUST(new_environment->create_mutable_binding(vm, name, false)); }); } @@ -1096,16 +1168,24 @@ Completion ForInStatement::loop_evaluation(Interpreter& interpreter, Vectorexecute(interpreter); - // m. Set the running execution context's LexicalEnvironment to oldEnv. + // NOTE: Because of optimizations we only create a new lexical environment if there are bindings + // so we should only dispose if that is the case. + if (vm.running_execution_context().lexical_environment != old_environment) { + VERIFY(is(vm.running_execution_context().lexical_environment)); + // m. Set result to DisposeResources(iterationEnv, result). + result = dispose_resources(vm, static_cast(vm.running_execution_context().lexical_environment), result); + } + + // n. Set the running execution context's LexicalEnvironment to oldEnv. vm.running_execution_context().lexical_environment = old_environment; - // n. If LoopContinues(result, labelSet) is false, then + // o. If LoopContinues(result, labelSet) is false, then if (!loop_continues(result, label_set)) { // 1. Return UpdateEmpty(result, V). return result.update_empty(last_value); } - // o. If result.[[Value]] is not empty, set V to result.[[Value]]. + // p. If result.[[Value]] is not empty, set V to result.[[Value]]. if (result.value().has_value()) last_value = *result.value(); @@ -1154,6 +1234,11 @@ Completion ForOfStatement::loop_evaluation(Interpreter& interpreter, Vectorexecute(interpreter); + if (vm.running_execution_context().lexical_environment != old_environment) { + VERIFY(is(vm.running_execution_context().lexical_environment)); + result = dispose_resources(vm, static_cast(vm.running_execution_context().lexical_environment), result); + } + // m. Set the running execution context's LexicalEnvironment to oldEnv. vm.running_execution_context().lexical_environment = old_environment; diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 9859dc2c98..5490a6f922 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -922,6 +922,8 @@ public: virtual Bytecode::CodeGenerationErrorOr generate_labelled_evaluation(Bytecode::Generator&, Vector const&) const override; private: + Completion for_body_evaluation(Interpreter&, Vector const&, size_t per_iteration_bindings_size) const; + RefPtr m_init; RefPtr m_test; RefPtr m_update; @@ -1736,6 +1738,8 @@ public: virtual bool is_lexical_declaration() const override { return true; } + NonnullRefPtrVector const& declarations() const { return m_declarations; } + private: NonnullRefPtrVector m_declarations; }; diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index 50bc34e38c..426b996793 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -3573,7 +3573,38 @@ NonnullRefPtr Parser::parse_for_statement() RefPtr init; if (!match(TokenType::Semicolon)) { - if (match_variable_declaration()) { + + auto match_for_using_declaration = [&] { + if (!match(TokenType::Identifier) || m_state.current_token.original_value() != "using"sv) + return false; + + auto lookahead = next_token(); + if (lookahead.trivia_contains_line_terminator()) + return false; + + if (lookahead.original_value() == "of"sv) + return false; + + return token_is_identifier(lookahead); + }; + + if (match_for_using_declaration()) { + auto declaration = parse_using_declaration(IsForLoopVariableDeclaration::Yes); + + if (match_of(m_state.current_token)) { + if (declaration->declarations().size() != 1) + syntax_error("Must have exactly one declaration in for using of"); + else if (declaration->declarations().first().init()) + syntax_error("Using declaration cannot have initializer"); + + return parse_for_in_of_statement(move(declaration), is_await_loop); + } + + if (match(TokenType::In)) + syntax_error("Using declaration not allowed in for-in loop"); + + init = move(declaration); + } else if (match_variable_declaration()) { auto declaration = parse_variable_declaration(IsForLoopVariableDeclaration::Yes); if (declaration->declaration_kind() == DeclarationKind::Var) { m_state.current_scope_pusher->add_declaration(declaration); @@ -3586,15 +3617,22 @@ NonnullRefPtr Parser::parse_for_statement() }); } - init = move(declaration); - if (match_for_in_of()) - return parse_for_in_of_statement(*init, is_await_loop); - if (static_cast(*init).declaration_kind() == DeclarationKind::Const) { - for (auto& variable : static_cast(*init).declarations()) { + if (match_for_in_of()) { + if (declaration->declarations().size() > 1) + syntax_error("Multiple declarations not allowed in for..in/of"); + else if (declaration->declarations().size() < 1) + syntax_error("Need exactly one variable declaration in for..in/of"); + + return parse_for_in_of_statement(move(declaration), is_await_loop); + } + if (declaration->declaration_kind() == DeclarationKind::Const) { + for (auto const& variable : declaration->declarations()) { if (!variable.init()) syntax_error("Missing initializer in 'const' variable declaration"); } } + + init = move(declaration); } else if (match_expression()) { auto lookahead_token = next_token(); bool starts_with_async_of = match(TokenType::Async) && match_of(lookahead_token); @@ -3641,11 +3679,8 @@ NonnullRefPtr Parser::parse_for_in_of_statement(NonnullRefPtr(*lhs)) { auto& declaration = static_cast(*lhs); - if (declaration.declarations().size() > 1) { - syntax_error("Multiple declarations not allowed in for..in/of"); - } else if (declaration.declarations().size() < 1) { - syntax_error("Need exactly one variable declaration in for..in/of"); - } else { + // Syntax errors for wrong amounts of declaration should have already been hit. + if (!declaration.declarations().is_empty()) { // AnnexB extension B.3.5 Initializers in ForIn Statement Heads, https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads auto& variable = declaration.declarations().first(); if (variable.init()) { @@ -3655,7 +3690,7 @@ NonnullRefPtr Parser::parse_for_in_of_statement(NonnullRefPtris_identifier() && !is(*lhs) && !is(*lhs)) { + } else if (!lhs->is_identifier() && !is(*lhs) && !is(*lhs) && !is(*lhs)) { bool valid = false; if (is(*lhs) || is(*lhs)) { auto synthesized_binding_pattern = synthesize_binding_pattern(static_cast(*lhs)); @@ -3915,21 +3950,26 @@ bool Parser::match_variable_declaration() const bool Parser::match_identifier() const { - if (m_state.current_token.type() == TokenType::EscapedKeyword) { - if (m_state.current_token.value() == "let"sv) + return token_is_identifier(m_state.current_token); +} + +bool Parser::token_is_identifier(Token const& token) const +{ + if (token.type() == TokenType::EscapedKeyword) { + if (token.value() == "let"sv) return !m_state.strict_mode; - if (m_state.current_token.value() == "yield"sv) + if (token.value() == "yield"sv) return !m_state.strict_mode && !m_state.in_generator_function_context; - if (m_state.current_token.value() == "await"sv) + if (token.value() == "await"sv) return m_program_type != Program::Type::Module && !m_state.await_expression_is_valid && !m_state.in_class_static_init_block; return true; } - return m_state.current_token.type() == TokenType::Identifier - || m_state.current_token.type() == TokenType::Async - || (m_state.current_token.type() == TokenType::Let && !m_state.strict_mode) - || (m_state.current_token.type() == TokenType::Await && m_program_type != Program::Type::Module && !m_state.await_expression_is_valid && !m_state.in_class_static_init_block) - || (m_state.current_token.type() == TokenType::Yield && !m_state.in_generator_function_context && !m_state.strict_mode); // See note in Parser::parse_identifier(). + return token.type() == TokenType::Identifier + || token.type() == TokenType::Async + || (token.type() == TokenType::Let && !m_state.strict_mode) + || (token.type() == TokenType::Await && m_program_type != Program::Type::Module && !m_state.await_expression_is_valid && !m_state.in_class_static_init_block) + || (token.type() == TokenType::Yield && !m_state.in_generator_function_context && !m_state.strict_mode); // See note in Parser::parse_identifier(). } bool Parser::match_identifier_name() const diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index 57a8ea08bd..0e6692fc22 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -227,6 +227,7 @@ private: bool try_match_using_declaration() const; bool match_variable_declaration() const; bool match_identifier() const; + bool token_is_identifier(Token const&) const; bool match_identifier_name() const; bool match_property_key() const; bool is_private_identifier_valid() const; diff --git a/Userland/Libraries/LibJS/Tests/using-for-loops.js b/Userland/Libraries/LibJS/Tests/using-for-loops.js new file mode 100644 index 0000000000..1acd277d74 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/using-for-loops.js @@ -0,0 +1,239 @@ +describe("basic usage", () => { + test("using in normal for loop", () => { + let isDisposed = false; + let lastI = -1; + for ( + using x = { + i: 0, + tick() { + this.i++; + }, + done() { + return this.i === 3; + }, + [Symbol.dispose]() { + isDisposed = true; + }, + }; + !x.done(); + x.tick() + ) { + expect(isDisposed).toBeFalse(); + expect(x.i).toBeGreaterThan(lastI); + lastI = x.i; + } + + expect(isDisposed).toBeTrue(); + expect(lastI).toBe(2); + }); + + test("using in normal for loop with expression body", () => { + let isDisposed = false; + let outerI = 0; + for ( + using x = { + i: 0, + tick() { + this.i++; + outerI++; + }, + done() { + return this.i === 3; + }, + [Symbol.dispose]() { + isDisposed = true; + }, + }; + !x.done(); + x.tick() + ) + expect(isDisposed).toBeFalse(); + + expect(isDisposed).toBeTrue(); + expect(outerI).toBe(3); + }); + + test("using in for of loop", () => { + const disposable = []; + const values = []; + + function createDisposable(value) { + return { + value: value, + [Symbol.dispose]() { + expect(this.value).toBe(value); + disposable.push(value); + } + }; + } + + for (using a of [createDisposable('a'), createDisposable('b'), createDisposable('c')]) { + expect(disposable).toEqual(values); + values.push(a.value); + } + + expect(disposable).toEqual(['a', 'b', 'c']); + }); + + test("using in for of loop with expression body", () => { + let disposableCalls = 0; + let i = 0; + + const obj = { + [Symbol.dispose]() { + disposableCalls++; + } + }; + + for (using a of [obj, obj, obj]) + expect(disposableCalls).toBe(i++); + + expect(disposableCalls).toBe(3); + }); + + test("can have multiple declaration in normal for loop", () => { + let disposed = 0; + const a = { + [Symbol.dispose]() { + disposed++; + } + } + + expect(disposed).toBe(0); + for (using b = a, c = a; false;) + expect().fail(); + + expect(disposed).toBe(2); + }); + + test("can have using in block in for loop", () => { + const disposed = []; + const values = []; + for (let i = 0; i < 3; i++) { + using a = { + val: i, + [Symbol.dispose]() { + expect(i).toBe(this.val); + disposed.push(i); + }, + }; + expect(disposed).toEqual(values); + values.push(i); + } + expect(disposed).toEqual([0, 1, 2]); + }); + + test("can have using in block in for-in loop", () => { + const disposed = []; + const values = []; + for (const i in ['a', 'b', 'c']) { + using a = { + val: i, + [Symbol.dispose]() { + expect(i).toBe(this.val); + disposed.push(i); + }, + }; + expect(disposed).toEqual(values); + values.push(i); + } + expect(disposed).toEqual(["0", "1", "2"]); + }); + + test("dispose is called even if throw in for of loop", () => { + let disposableCalls = 0; + + const obj = { + [Symbol.dispose]() { + expect() + disposableCalls++; + } + }; + + try { + for (using a of [obj]) + throw new ExpectationError("Expected in for-of"); + + expect().fail("Should have thrown"); + } catch (e) { + expect(e).toBeInstanceOf(ExpectationError); + expect(e.message).toBe("Expected in for-of"); + expect(disposableCalls).toBe(1); + } + + expect(disposableCalls).toBe(1); + }); +}); + +describe("using is still a valid variable in loops", () => { + test("for loops var", () => { + let enteredLoop = false; + for (var using = 1; using < 2; using++) { + enteredLoop = true; + } + expect(enteredLoop).toBeTrue(); + }); + + test("for loops const", () => { + let enteredLoop = false; + for (const using = 1; using < 2; ) { + enteredLoop = true; + break; + } + expect(enteredLoop).toBeTrue(); + }); + + test("for loops let", () => { + let enteredLoop = false; + for (let using = 1; using < 2; using++) { + enteredLoop = true; + } + expect(enteredLoop).toBeTrue(); + }); + + test("using in", () => { + let enteredLoop = false; + for (using in [1]) { + enteredLoop = true; + expect(using).toBe("0"); + } + expect(enteredLoop).toBeTrue(); + }); + + test("using of", () => { + let enteredLoop = false; + for (using of [1]) { + enteredLoop = true; + expect(using).toBe(1); + } + expect(enteredLoop).toBeTrue(); + }); + + test("using using of", () => { + let enteredLoop = false; + for (using using of [null]) { + enteredLoop = true; + expect(using).toBeNull(); + } + expect(enteredLoop).toBeTrue(); + }); +}); + +describe("syntax errors", () => { + test("cannot have using as for loop body", () => { + expect("for (;;) using a = {};").not.toEval(); + expect("for (x in []) using a = {};").not.toEval(); + expect("for (x of []) using a = {};").not.toEval(); + }); + + test("must have one declaration without initializer in for loop", () => { + expect("for (using x = {} of []) {}").not.toEval(); + expect("for (using x, y of []) {}").not.toEval(); + }); + + test("cannot have using in for-in loop", () => { + expect("for (using x in []) {}").not.toEval(); + expect("for (using of in []) {}").not.toEval(); + expect("for (using in of []) {}").not.toEval(); + }); +});