mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 03:37:43 +00:00
LibJS: Implement parsing and executing for-await-of loops
This commit is contained in:
parent
b3699029e2
commit
e69276e704
5 changed files with 242 additions and 7 deletions
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
71
Userland/Libraries/LibJS/Tests/loops/for-await-of.js
Normal file
71
Userland/Libraries/LibJS/Tests/loops/for-await-of.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue