diff options
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Libraries/LibJS/AST.cpp | 117 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/AST.h | 19 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Parser.cpp | 34 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Parser.h | 8 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/loops/for-await-of.js | 71 |
5 files changed, 242 insertions, 7 deletions
diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index dbb032dae2..f45de4533b 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -894,6 +894,112 @@ Value ForOfStatement::execute(Interpreter& interpreter, GlobalObject& global_obj return last_value; } +Value ForAwaitOfStatement::execute(Interpreter& interpreter, GlobalObject& global_object) const +{ + InterpreterNodeScope node_scope { interpreter, *this }; + + // 14.7.5.6 ForIn/OfHeadEvaluation ( uninitializedBoundNames, expr, iterationKind ), https://tc39.es/ecma262/#sec-runtime-semantics-forinofheadevaluation + // Note: Performs only steps 1 through 5. + auto for_of_head_state = TRY_OR_DISCARD(for_in_of_head_execute(interpreter, global_object, m_lhs, m_rhs)); + + auto rhs_result = for_of_head_state.rhs_value; + + // NOTE: Perform step 7 from ForIn/OfHeadEvaluation. And since this is always async we only have to do step 7.d. + // d. Return ? GetIterator(exprValue, iteratorHint). + auto* iterator = TRY_OR_DISCARD(get_iterator(global_object, rhs_result, IteratorHint::Async)); + VERIFY(iterator); + + auto& vm = interpreter.vm(); + + // 14.7.5.7 ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind, labelSet [ , iteratorKind ] ), https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset + // NOTE: Here iteratorKind is always async. + // 2. Let oldEnv be the running execution context's LexicalEnvironment. + Environment* old_environment = interpreter.lexical_environment(); + auto restore_scope = ScopeGuard([&] { + interpreter.vm().running_execution_context().lexical_environment = old_environment; + }); + // 3. Let V be undefined. + auto last_value = js_undefined(); + + // NOTE: Step 4 and 5 are just extracting properties from the head which is done already in for_in_of_head_execute. + // And these are only used in step 6.g through 6.k which is done with for_of_head_state.execute_head. + + // 6. Repeat, + while (true) { + // NOTE: Since we don't have iterator records yet we have to extract the function first. + auto next_method = TRY_OR_DISCARD(iterator->get(vm.names.next)); + if (!next_method.is_function()) { + vm.throw_exception<TypeError>(global_object, ErrorType::IterableNextNotAFunction); + return {}; + } + + // a. Let nextResult be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]). + auto next_result = TRY_OR_DISCARD(call(global_object, next_method, iterator)); + // b. If iteratorKind is async, set nextResult to ? Await(nextResult). + next_result = TRY_OR_DISCARD(await(global_object, next_result)); + // c. If Type(nextResult) is not Object, throw a TypeError exception. + if (!next_result.is_object()) { + vm.throw_exception<TypeError>(global_object, ErrorType::IterableNextBadReturn); + return {}; + } + + // d. Let done be ? IteratorComplete(nextResult). + auto done = TRY_OR_DISCARD(iterator_complete(global_object, next_result.as_object())); + + // e. If done is true, return NormalCompletion(V). + if (done) + return last_value; + + // f. Let nextValue be ? IteratorValue(nextResult). + auto next_value = TRY_OR_DISCARD(iterator_value(global_object, next_result.as_object())); + + // NOTE: This performs steps g. through to k. + TRY_OR_DISCARD(for_of_head_state.execute_head(interpreter, global_object, next_value)); + + // l. Let result be the result of evaluating stmt. + auto result = m_body->execute(interpreter, global_object); + + // m. Set the running execution context's LexicalEnvironment to oldEnv. + interpreter.vm().running_execution_context().lexical_environment = old_environment; + + // NOTE: Since execute does not return a completion we have to have a number of checks here. + // n. If LoopContinues(result, labelSet) is false, then + if (auto* exception = vm.exception()) { + // FIXME: We should return the result of AsyncIteratorClose but cannot return completions yet. + // 3. If iteratorKind is async, return ? AsyncIteratorClose(iteratorRecord, status). + TRY_OR_DISCARD(async_iterator_close(*iterator, throw_completion(exception->value()))); + return {}; + } + + if (interpreter.vm().should_unwind()) { + if (interpreter.vm().should_unwind_until(ScopeType::Continuable, m_labels)) { + // NOTE: In this case LoopContinues is not actually false so we don't perform step 6.n.ii.3. + interpreter.vm().stop_unwind(); + } else if (interpreter.vm().should_unwind_until(ScopeType::Breakable, m_labels)) { + interpreter.vm().stop_unwind(); + // 2. Set status to UpdateEmpty(result, V). + if (!result.is_empty()) + last_value = result; + // 3. If iteratorKind is async, return ? AsyncIteratorClose(iteratorRecord, status). + TRY_OR_DISCARD(async_iterator_close(*iterator, normal_completion(last_value))); + return last_value; + } else { + // 2. Set status to UpdateEmpty(result, V). + if (!result.is_empty()) + last_value = result; + // 3. If iteratorKind is async, return ? AsyncIteratorClose(iteratorRecord, status). + TRY_OR_DISCARD(async_iterator_close(*iterator, normal_completion(last_value))); + return last_value; + } + } + // o. If result.[[Value]] is not empty, set V to result.[[Value]]. + if (!result.is_empty()) + last_value = result; + } + + VERIFY_NOT_REACHED(); +} + Value BinaryExpression::execute(Interpreter& interpreter, GlobalObject& global_object) const { InterpreterNodeScope node_scope { interpreter, *this }; @@ -2089,6 +2195,17 @@ void ForOfStatement::dump(int indent) const body().dump(indent + 1); } +void ForAwaitOfStatement::dump(int indent) const +{ + ASTNode::dump(indent); + + print_indent(indent); + outln("ForAwaitOf"); + m_lhs.visit([&](auto& lhs) { lhs->dump(indent + 1); }); + m_rhs->dump(indent + 1); + m_body->dump(indent + 1); +} + Value Identifier::execute(Interpreter& interpreter, GlobalObject& global_object) const { InterpreterNodeScope node_scope { interpreter, *this }; diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 49e0ab7bf5..f0e630fd1d 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -753,6 +753,25 @@ private: NonnullRefPtr<Statement> m_body; }; +class ForAwaitOfStatement final : public IterationStatement { +public: + ForAwaitOfStatement(SourceRange source_range, Variant<NonnullRefPtr<ASTNode>, NonnullRefPtr<BindingPattern>> lhs, NonnullRefPtr<Expression> rhs, NonnullRefPtr<Statement> body) + : IterationStatement(source_range) + , m_lhs(move(lhs)) + , m_rhs(move(rhs)) + , m_body(move(body)) + { + } + + virtual Value execute(Interpreter&, GlobalObject&) const override; + virtual void dump(int indent) const override; + +private: + Variant<NonnullRefPtr<ASTNode>, NonnullRefPtr<BindingPattern>> m_lhs; + NonnullRefPtr<Expression> m_rhs; + NonnullRefPtr<Statement> m_body; +}; + enum class BinaryOp { Addition, Subtraction, diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index 3ef3907a99..01e5df3aeb 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -3142,15 +3142,34 @@ NonnullRefPtr<IfStatement> Parser::parse_if_statement() NonnullRefPtr<Statement> Parser::parse_for_statement() { auto rule_start = push_start(); + auto is_await_loop = IsForAwaitLoop::No; + auto match_of = [&](Token const& token) { return token.type() == TokenType::Identifier && token.original_value() == "of"sv; }; - auto match_for_in_of = [&] { - return match(TokenType::In) || match_of(m_state.current_token); + + auto match_for_in_of = [&]() { + bool is_of = match_of(m_state.current_token); + if (is_await_loop == IsForAwaitLoop::Yes) { + if (!is_of) + syntax_error("for await loop is only valid with 'of'"); + else if (!m_state.in_async_function_context) + syntax_error("for await loop is only valid in async function or generator"); + return true; + } + + return match(TokenType::In) || is_of; }; consume(TokenType::For); + if (match(TokenType::Await)) { + consume(); + if (!m_state.in_async_function_context) + syntax_error("for-await-of is only allowed in async function context"); + is_await_loop = IsForAwaitLoop::Yes; + } + consume(TokenType::ParenOpen); Optional<ScopePusher> scope_pusher; @@ -3172,7 +3191,7 @@ NonnullRefPtr<Statement> Parser::parse_for_statement() init = move(declaration); if (match_for_in_of()) - return parse_for_in_of_statement(*init); + return parse_for_in_of_statement(*init, is_await_loop); if (static_cast<VariableDeclaration&>(*init).declaration_kind() == DeclarationKind::Const) { for (auto& variable : static_cast<VariableDeclaration&>(*init).declarations()) { if (!variable.init()) @@ -3185,9 +3204,10 @@ NonnullRefPtr<Statement> Parser::parse_for_statement() init = parse_expression(0, Associativity::Right, { TokenType::In }); if (match_for_in_of()) { - if (starts_with_async_of && match_of(m_state.current_token)) + if (is_await_loop != IsForAwaitLoop::Yes + && starts_with_async_of && match_of(m_state.current_token)) syntax_error("for-of loop may not start with async of"); - return parse_for_in_of_statement(*init); + return parse_for_in_of_statement(*init, is_await_loop); } } else { syntax_error("Unexpected token in for loop"); @@ -3215,7 +3235,7 @@ NonnullRefPtr<Statement> Parser::parse_for_statement() return create_ast_node<ForStatement>({ m_state.current_token.filename(), rule_start.position(), position() }, move(init), move(test), move(update), move(body)); } -NonnullRefPtr<Statement> Parser::parse_for_in_of_statement(NonnullRefPtr<ASTNode> lhs) +NonnullRefPtr<Statement> Parser::parse_for_in_of_statement(NonnullRefPtr<ASTNode> lhs, IsForAwaitLoop is_for_await_loop) { Variant<NonnullRefPtr<ASTNode>, NonnullRefPtr<BindingPattern>> for_declaration = lhs; auto rule_start = push_start(); @@ -3272,6 +3292,8 @@ NonnullRefPtr<Statement> Parser::parse_for_in_of_statement(NonnullRefPtr<ASTNode auto body = parse_statement(); if (is_in) return create_ast_node<ForInStatement>({ m_state.current_token.filename(), rule_start.position(), position() }, move(for_declaration), move(rhs), move(body)); + if (is_for_await_loop == IsForAwaitLoop::Yes) + return create_ast_node<ForAwaitOfStatement>({ m_state.current_token.filename(), rule_start.position(), position() }, move(for_declaration), move(rhs), move(body)); return create_ast_node<ForOfStatement>({ m_state.current_token.filename(), rule_start.position(), position() }, move(for_declaration), move(rhs), move(body)); } diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index 01b0e0a8f0..7255058b7a 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -77,7 +77,13 @@ public: NonnullRefPtr<ReturnStatement> parse_return_statement(); NonnullRefPtr<VariableDeclaration> parse_variable_declaration(bool for_loop_variable_declaration = false); NonnullRefPtr<Statement> parse_for_statement(); - NonnullRefPtr<Statement> parse_for_in_of_statement(NonnullRefPtr<ASTNode> lhs); + + enum class IsForAwaitLoop { + No, + Yes + }; + + NonnullRefPtr<Statement> parse_for_in_of_statement(NonnullRefPtr<ASTNode> lhs, IsForAwaitLoop is_await); NonnullRefPtr<IfStatement> parse_if_statement(); NonnullRefPtr<ThrowStatement> parse_throw_statement(); NonnullRefPtr<TryStatement> parse_try_statement(); diff --git a/Userland/Libraries/LibJS/Tests/loops/for-await-of.js b/Userland/Libraries/LibJS/Tests/loops/for-await-of.js new file mode 100644 index 0000000000..f590e7462f --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/loops/for-await-of.js @@ -0,0 +1,71 @@ +describe("basic behavior", () => { + test("empty array", () => { + var enteredFunction = false; + var rejected = false; + async function f() { + enteredFunction = true; + for await (const v of []) { + expect().fail("Should not enter loop"); + } + } + + f().then( + () => { + expect(enteredFunction).toBeTrue(); + }, + () => { + rejected = true; + } + ); + runQueuedPromiseJobs(); + expect(enteredFunction).toBeTrue(); + expect(rejected).toBeFalse(); + }); + + test("sync iterator", () => { + var loopIterations = 0; + var rejected = false; + async function f() { + for await (const v of [1]) { + expect(v).toBe(1); + loopIterations++; + } + } + + f().then( + () => { + expect(loopIterations).toBe(1); + }, + () => { + rejected = true; + } + ); + runQueuedPromiseJobs(); + expect(loopIterations).toBe(1); + expect(rejected).toBeFalse(); + }); +}); + +describe("only allowed in async functions", () => { + test("async functions", () => { + expect("async function foo() { for await (const v of []) return v; }").toEval(); + expect("(async function () { for await (const v of []) return v; })").toEval(); + expect("async () => { for await (const v of []) return v; }").toEval(); + }); + + test("regular functions", () => { + expect("function foo() { for await (const v of []) return v; }").not.toEval(); + expect("(function () { for await (const v of []) return v; })").not.toEval(); + expect("() => { for await (const v of []) return v; }").not.toEval(); + }); + + test("generator functions", () => { + expect("function* foo() { for await (const v of []) return v; }").not.toEval(); + expect("(function* () { for await (const v of []) return v; })").not.toEval(); + }); + + test("async genrator functions", () => { + expect("async function* foo() { for await (const v of []) yield v; }").toEval(); + expect("(async function* () { for await (const v of []) yield v; })").toEval(); + }); +}); |