diff options
author | Linus Groh <mail@linusgroh.de> | 2020-04-21 19:21:26 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-05-25 18:45:36 +0200 |
commit | 07af2e6b2cf2dd0f6bd94e8918f5a1894d83633b (patch) | |
tree | 90592a51ce54d94d1e6ae149754501d17eaed5a9 /Libraries/LibJS | |
parent | c378a1c730c998dc7f69571a3123a633010d2a3a (diff) | |
download | serenity-07af2e6b2cf2dd0f6bd94e8918f5a1894d83633b.zip |
LibJS: Implement basic for..in and for..of loops
Diffstat (limited to 'Libraries/LibJS')
-rw-r--r-- | Libraries/LibJS/AST.cpp | 144 | ||||
-rw-r--r-- | Libraries/LibJS/AST.h | 53 | ||||
-rw-r--r-- | Libraries/LibJS/Parser.cpp | 85 | ||||
-rw-r--r-- | Libraries/LibJS/Parser.h | 9 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/Array.prototype-generic-functions.js | 80 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/for-in-basic.js | 41 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/for-of-basic.js | 43 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/test-common.js | 7 |
8 files changed, 372 insertions, 90 deletions
diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 2053c9d314..6dffc8fd21 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -322,6 +322,128 @@ Value ForStatement::execute(Interpreter& interpreter) const return last_value; } +static FlyString variable_from_for_declaration(Interpreter& interpreter, NonnullRefPtr<ASTNode> node, RefPtr<BlockStatement> wrapper) +{ + FlyString variable_name; + if (node->is_variable_declaration()) { + auto* variable_declaration = static_cast<const VariableDeclaration*>(node.ptr()); + ASSERT(!variable_declaration->declarations().is_empty()); + if (variable_declaration->declaration_kind() != DeclarationKind::Var) { + wrapper = create_ast_node<BlockStatement>(); + interpreter.enter_scope(*wrapper, {}, ScopeType::Block); + } + variable_declaration->execute(interpreter); + variable_name = variable_declaration->declarations().first().id().string(); + } else if (node->is_identifier()) { + variable_name = static_cast<const Identifier&>(*node).string(); + } else { + ASSERT_NOT_REACHED(); + } + return variable_name; +} + +Value ForInStatement::execute(Interpreter& interpreter) const +{ + if (!m_lhs->is_variable_declaration() && !m_lhs->is_identifier()) { + // FIXME: Implement "for (foo.bar in baz)", "for (foo[0] in bar)" + ASSERT_NOT_REACHED(); + } + RefPtr<BlockStatement> wrapper; + auto variable_name = variable_from_for_declaration(interpreter, m_lhs, wrapper); + auto wrapper_cleanup = ScopeGuard([&] { + if (wrapper) + interpreter.exit_scope(*wrapper); + }); + auto last_value = js_undefined(); + auto rhs_result = m_rhs->execute(interpreter); + if (interpreter.exception()) + return {}; + auto* object = rhs_result.to_object(interpreter); + while (object) { + auto property_names = object->get_own_properties(*object, Object::GetOwnPropertyMode::Key, Attribute::Enumerable); + for (auto& property_name : static_cast<Array&>(property_names.as_object()).elements()) { + interpreter.set_variable(variable_name, property_name); + last_value = interpreter.run(*m_body); + if (interpreter.exception()) + return {}; + if (interpreter.should_unwind()) { + if (interpreter.should_unwind_until(ScopeType::Continuable)) { + interpreter.stop_unwind(); + } else if (interpreter.should_unwind_until(ScopeType::Breakable)) { + interpreter.stop_unwind(); + break; + } else { + return js_undefined(); + } + } + } + object = object->prototype(); + } + return last_value; +} + +Value ForOfStatement::execute(Interpreter& interpreter) const +{ + if (!m_lhs->is_variable_declaration() && !m_lhs->is_identifier()) { + // FIXME: Implement "for (foo.bar of baz)", "for (foo[0] of bar)" + ASSERT_NOT_REACHED(); + } + RefPtr<BlockStatement> wrapper; + auto variable_name = variable_from_for_declaration(interpreter, m_lhs, wrapper); + auto wrapper_cleanup = ScopeGuard([&] { + if (wrapper) + interpreter.exit_scope(*wrapper); + }); + auto last_value = js_undefined(); + auto rhs_result = m_rhs->execute(interpreter); + if (interpreter.exception()) + return {}; + // FIXME: We need to properly implement the iterator protocol + auto is_iterable = rhs_result.is_array() || rhs_result.is_string() || (rhs_result.is_object() && rhs_result.as_object().is_string_object()); + if (!is_iterable) + return interpreter.throw_exception<TypeError>("for..of right-hand side must be iterable"); + + size_t index = 0; + auto next = [&]() -> Optional<Value> { + if (rhs_result.is_array()) { + auto array_elements = static_cast<Array*>(&rhs_result.as_object())->elements(); + if (index < array_elements.size()) + return Value(array_elements.at(index)); + } else if (rhs_result.is_string()) { + auto string = rhs_result.as_string().string(); + if (index < string.length()) + return js_string(interpreter, string.substring(index, 1)); + } else if (rhs_result.is_object() && rhs_result.as_object().is_string_object()) { + auto string = static_cast<StringObject*>(&rhs_result.as_object())->primitive_string().string(); + if (index < string.length()) + return js_string(interpreter, string.substring(index, 1)); + } + return {}; + }; + + for (;;) { + auto next_item = next(); + if (!next_item.has_value()) + break; + interpreter.set_variable(variable_name, next_item.value()); + last_value = interpreter.run(*m_body); + if (interpreter.exception()) + return {}; + if (interpreter.should_unwind()) { + if (interpreter.should_unwind_until(ScopeType::Continuable)) { + interpreter.stop_unwind(); + } else if (interpreter.should_unwind_until(ScopeType::Breakable)) { + interpreter.stop_unwind(); + break; + } else { + return js_undefined(); + } + } + ++index; + } + return last_value; +} + Value BinaryExpression::execute(Interpreter& interpreter) const { auto lhs_result = m_lhs->execute(interpreter); @@ -801,6 +923,28 @@ void ForStatement::dump(int indent) const body().dump(indent + 1); } +void ForInStatement::dump(int indent) const +{ + ASTNode::dump(indent); + + print_indent(indent); + printf("ForIn\n"); + lhs().dump(indent + 1); + rhs().dump(indent + 1); + body().dump(indent + 1); +} + +void ForOfStatement::dump(int indent) const +{ + ASTNode::dump(indent); + + print_indent(indent); + printf("ForOf\n"); + lhs().dump(indent + 1); + rhs().dump(indent + 1); + body().dump(indent + 1); +} + Value Identifier::execute(Interpreter& interpreter) const { auto value = interpreter.get_variable(string()); diff --git a/Libraries/LibJS/AST.h b/Libraries/LibJS/AST.h index 7d68284889..31c95e5f9c 100644 --- a/Libraries/LibJS/AST.h +++ b/Libraries/LibJS/AST.h @@ -341,6 +341,54 @@ private: NonnullRefPtr<Statement> m_body; }; +class ForInStatement : public Statement { +public: + ForInStatement(NonnullRefPtr<ASTNode> lhs, NonnullRefPtr<Expression> rhs, NonnullRefPtr<Statement> body) + : m_lhs(move(lhs)) + , m_rhs(move(rhs)) + , m_body(move(body)) + { + } + + const ASTNode& lhs() const { return *m_lhs; } + const Expression& rhs() const { return *m_rhs; } + const Statement& body() const { return *m_body; } + + virtual Value execute(Interpreter&) const override; + virtual void dump(int indent) const override; + +private: + virtual const char* class_name() const override { return "ForInStatement"; } + + NonnullRefPtr<ASTNode> m_lhs; + NonnullRefPtr<Expression> m_rhs; + NonnullRefPtr<Statement> m_body; +}; + +class ForOfStatement : public Statement { +public: + ForOfStatement(NonnullRefPtr<ASTNode> lhs, NonnullRefPtr<Expression> rhs, NonnullRefPtr<Statement> body) + : m_lhs(move(lhs)) + , m_rhs(move(rhs)) + , m_body(move(body)) + { + } + + const ASTNode& lhs() const { return *m_lhs; } + const Expression& rhs() const { return *m_rhs; } + const Statement& body() const { return *m_body; } + + virtual Value execute(Interpreter&) const override; + virtual void dump(int indent) const override; + +private: + virtual const char* class_name() const override { return "ForOfStatement"; } + + NonnullRefPtr<ASTNode> m_lhs; + NonnullRefPtr<Expression> m_rhs; + NonnullRefPtr<Statement> m_body; +}; + enum class BinaryOp { Addition, Subtraction, @@ -678,6 +726,11 @@ enum class DeclarationKind { class VariableDeclarator final : public ASTNode { public: + VariableDeclarator(NonnullRefPtr<Identifier> id) + : m_id(move(id)) + { + } + VariableDeclarator(NonnullRefPtr<Identifier> id, RefPtr<Expression> init) : m_id(move(id)) , m_init(move(init)) diff --git a/Libraries/LibJS/Parser.cpp b/Libraries/LibJS/Parser.cpp index e68e965cf1..09eb4fc239 100644 --- a/Libraries/LibJS/Parser.cpp +++ b/Libraries/LibJS/Parser.cpp @@ -592,8 +592,7 @@ NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token token) syntax_error( message, m_parser_state.m_current_token.line_number(), - m_parser_state.m_current_token.line_column() - ); + m_parser_state.m_current_token.line_column()); } return create_ast_node<StringLiteral>(string); } @@ -651,14 +650,14 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal(bool is_tagged) return create_ast_node<TemplateLiteral>(expressions); } -NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associativity associativity) +NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associativity associativity, Vector<TokenType> forbidden) { auto expression = parse_primary_expression(); while (match(TokenType::TemplateLiteralStart)) { auto template_literal = parse_template_literal(true); expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal)); } - while (match_secondary_expression()) { + while (match_secondary_expression(forbidden)) { int new_precedence = operator_precedence(m_parser_state.m_current_token.type()); if (new_precedence < min_precedence) break; @@ -974,7 +973,7 @@ NonnullRefPtr<FunctionNodeType> Parser::parse_function_node(bool needs_function_ return create_ast_node<FunctionNodeType>(name, move(body), move(parameters), function_length, NonnullRefPtrVector<VariableDeclaration>()); } -NonnullRefPtr<VariableDeclaration> Parser::parse_variable_declaration() +NonnullRefPtr<VariableDeclaration> Parser::parse_variable_declaration(bool with_semicolon) { DeclarationKind declaration_kind; @@ -1010,10 +1009,11 @@ NonnullRefPtr<VariableDeclaration> Parser::parse_variable_declaration() } break; } - consume_or_insert_semicolon(); + if (with_semicolon) + consume_or_insert_semicolon(); auto declaration = create_ast_node<VariableDeclaration>(declaration_kind, move(declarations)); - if (declaration->declaration_kind() == DeclarationKind::Var) + if (declaration_kind == DeclarationKind::Var) m_parser_state.m_var_scopes.last().append(declaration); else m_parser_state.m_let_scopes.last().append(declaration); @@ -1177,57 +1177,46 @@ NonnullRefPtr<IfStatement> Parser::parse_if_statement() return create_ast_node<IfStatement>(move(predicate), move(consequent), move(alternate)); } -NonnullRefPtr<ForStatement> Parser::parse_for_statement() +NonnullRefPtr<Statement> Parser::parse_for_statement() { + auto match_for_in_of = [&]() { + return match(TokenType::In) || (match(TokenType::Identifier) && m_parser_state.m_current_token.value() == "of"); + }; + consume(TokenType::For); consume(TokenType::ParenOpen); - bool first_semicolon_consumed = false; bool in_scope = false; RefPtr<ASTNode> init; - switch (m_parser_state.m_current_token.type()) { - case TokenType::Semicolon: - break; - default: + if (!match(TokenType::Semicolon)) { if (match_expression()) { - init = parse_expression(0); + init = parse_expression(0, Associativity::Right, { TokenType::In }); + if (match_for_in_of()) + return parse_for_in_of_statement(*init); } else if (match_variable_declaration()) { - if (m_parser_state.m_current_token.type() != TokenType::Var) { + if (!match(TokenType::Var)) { m_parser_state.m_let_scopes.append(NonnullRefPtrVector<VariableDeclaration>()); in_scope = true; } - - init = parse_variable_declaration(); - first_semicolon_consumed = true; + init = parse_variable_declaration(false); + if (match_for_in_of()) + return parse_for_in_of_statement(*init); } else { ASSERT_NOT_REACHED(); } - break; } - - if (!first_semicolon_consumed) - consume(TokenType::Semicolon); + consume(TokenType::Semicolon); RefPtr<Expression> test; - switch (m_parser_state.m_current_token.type()) { - case TokenType::Semicolon: - break; - default: + if (!match(TokenType::Semicolon)) test = parse_expression(0); - break; - } consume(TokenType::Semicolon); RefPtr<Expression> update; - switch (m_parser_state.m_current_token.type()) { - case TokenType::ParenClose: - break; - default: + if (!match(TokenType::ParenClose)) update = parse_expression(0); - break; - } consume(TokenType::ParenClose); @@ -1240,6 +1229,28 @@ NonnullRefPtr<ForStatement> Parser::parse_for_statement() return create_ast_node<ForStatement>(move(init), move(test), move(update), move(body)); } +NonnullRefPtr<Statement> Parser::parse_for_in_of_statement(NonnullRefPtr<ASTNode> lhs) +{ + if (lhs->is_variable_declaration()) { + auto declarations = static_cast<VariableDeclaration*>(lhs.ptr())->declarations(); + if (declarations.size() > 1) { + syntax_error("multiple declarations not allowed in for..in/of"); + lhs = create_ast_node<ErrorExpression>(); + } + if (declarations.first().init() != nullptr) { + syntax_error("variable initializer not allowed in for..in/of"); + lhs = create_ast_node<ErrorExpression>(); + } + } + auto in_or_of = consume(); + auto rhs = parse_expression(0); + consume(TokenType::ParenClose); + auto body = parse_statement(); + if (in_or_of.type() == TokenType::In) + return create_ast_node<ForInStatement>(move(lhs), move(rhs), move(body)); + return create_ast_node<ForOfStatement>(move(lhs), move(rhs), move(body)); +} + NonnullRefPtr<DebuggerStatement> Parser::parse_debugger_statement() { consume(TokenType::Debugger); @@ -1296,9 +1307,11 @@ bool Parser::match_unary_prefixed_expression() const || type == TokenType::Delete; } -bool Parser::match_secondary_expression() const +bool Parser::match_secondary_expression(Vector<TokenType> forbidden) const { auto type = m_parser_state.m_current_token.type(); + if (forbidden.contains_slow(type)) + return false; return type == TokenType::Plus || type == TokenType::PlusEquals || type == TokenType::Minus @@ -1410,7 +1423,7 @@ void Parser::consume_or_insert_semicolon() Token Parser::consume(TokenType expected_type) { - if (m_parser_state.m_current_token.type() != expected_type) { + if (!match(expected_type)) { expected(Token::name(expected_type)); } return consume(); diff --git a/Libraries/LibJS/Parser.h b/Libraries/LibJS/Parser.h index 1f5d2f4fec..1b2f5f9056 100644 --- a/Libraries/LibJS/Parser.h +++ b/Libraries/LibJS/Parser.h @@ -50,8 +50,9 @@ public: NonnullRefPtr<Statement> parse_statement(); NonnullRefPtr<BlockStatement> parse_block_statement(); NonnullRefPtr<ReturnStatement> parse_return_statement(); - NonnullRefPtr<VariableDeclaration> parse_variable_declaration(); - NonnullRefPtr<ForStatement> parse_for_statement(); + NonnullRefPtr<VariableDeclaration> parse_variable_declaration(bool with_semicolon = true); + NonnullRefPtr<Statement> parse_for_statement(); + NonnullRefPtr<Statement> parse_for_in_of_statement(NonnullRefPtr<ASTNode> lhs); NonnullRefPtr<IfStatement> parse_if_statement(); NonnullRefPtr<ThrowStatement> parse_throw_statement(); NonnullRefPtr<TryStatement> parse_try_statement(); @@ -65,7 +66,7 @@ public: NonnullRefPtr<DebuggerStatement> parse_debugger_statement(); NonnullRefPtr<ConditionalExpression> parse_conditional_expression(NonnullRefPtr<Expression> test); - NonnullRefPtr<Expression> parse_expression(int min_precedence, Associativity associate = Associativity::Right); + NonnullRefPtr<Expression> parse_expression(int min_precedence, Associativity associate = Associativity::Right, Vector<TokenType> forbidden = {}); NonnullRefPtr<Expression> parse_primary_expression(); NonnullRefPtr<Expression> parse_unary_prefixed_expression(); NonnullRefPtr<ObjectExpression> parse_object_expression(); @@ -105,7 +106,7 @@ private: Associativity operator_associativity(TokenType) const; bool match_expression() const; bool match_unary_prefixed_expression() const; - bool match_secondary_expression() const; + bool match_secondary_expression(Vector<TokenType> forbidden = {}) const; bool match_statement() const; bool match_variable_declaration() const; bool match_identifier_name() const; diff --git a/Libraries/LibJS/Tests/Array.prototype-generic-functions.js b/Libraries/LibJS/Tests/Array.prototype-generic-functions.js index d9cd763269..6a981b0985 100644 --- a/Libraries/LibJS/Tests/Array.prototype-generic-functions.js +++ b/Libraries/LibJS/Tests/Array.prototype-generic-functions.js @@ -73,67 +73,47 @@ try { const o = { length: 5, 0: "foo", 1: "bar", 3: "baz" }; { - const visited = []; - Array.prototype.every.call(o, function (value) { - visited.push(value); - return true; - }); - assert(visited.length === 3); - assert(visited[0] === "foo"); - assert(visited[1] === "bar"); - assert(visited[2] === "baz"); + assertVisitsAll(visit => { + Array.prototype.every.call(o, function (value) { + visit(value); + return true; + }); + }, ["foo", "bar", "baz"]); } ["find", "findIndex"].forEach(name => { - const visited = []; - Array.prototype[name].call(o, function (value) { - visited.push(value); - return false; - }); - assert(visited.length === 5); - assert(visited[0] === "foo"); - assert(visited[1] === "bar"); - assert(visited[2] === undefined); - assert(visited[3] === "baz"); - assert(visited[4] === undefined); + assertVisitsAll(visit => { + Array.prototype[name].call(o, function (value) { + visit(value); + return false; + }); + }, ["foo", "bar", undefined, "baz", undefined]); }); ["filter", "forEach", "map", "some"].forEach(name => { - const visited = []; - Array.prototype[name].call(o, function (value) { - visited.push(value); - return false; - }); - assert(visited.length === 3); - assert(visited[0] === "foo"); - assert(visited[1] === "bar"); - assert(visited[2] === "baz"); + assertVisitsAll(visit => { + Array.prototype[name].call(o, function (value) { + visit(value); + return false; + }); + }, ["foo", "bar", "baz"]); }); - { - const visited = []; - Array.prototype.reduce.call(o, function (_, value) { - visited.push(value); - return false; - }, "initial"); - - assert(visited.length === 3); - assert(visited[0] === "foo"); - assert(visited[1] === "bar"); - assert(visited[2] === "baz"); + assertVisitsAll(visit => { + Array.prototype.reduce.call(o, function (_, value) { + visit(value); + return false; + }, "initial"); + }, ["foo", "bar", "baz"]); } { - const visited = []; - Array.prototype.reduceRight.call(o, function (_, value) { - visited.push(value); - return false; - }, "initial"); - - assert(visited.length === 3); - assert(visited[2] === "foo"); - assert(visited[1] === "bar"); - assert(visited[0] === "baz"); + assertVisitsAll(visit => { + Array.prototype.reduceRight.call(o, function (_, value) { + visit(value); + return false; + }, "initial"); + }, ["baz", "bar", "foo"]); } console.log("PASS"); diff --git a/Libraries/LibJS/Tests/for-in-basic.js b/Libraries/LibJS/Tests/for-in-basic.js new file mode 100644 index 0000000000..1d6189c301 --- /dev/null +++ b/Libraries/LibJS/Tests/for-in-basic.js @@ -0,0 +1,41 @@ +load("test-common.js"); + +try { + assertVisitsAll(visit => { + for (const property in "") { + visit(property); + } + }, []); + + assertVisitsAll(visit => { + for (const property in 123) { + visit(property); + } + }, []); + + assertVisitsAll(visit => { + for (const property in {}) { + visit(property); + } + }, []); + + assertVisitsAll(visit => { + for (const property in "hello") { + visit(property); + } + }, ["0", "1", "2", "3", "4"]); + + assertVisitsAll(visit => { + for (const property in {a: 1, b: 2, c: 2}) { + visit(property); + } + }, ["a", "b", "c"]); + + var property; + for (property in "abc"); + assert(property === "2"); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/for-of-basic.js b/Libraries/LibJS/Tests/for-of-basic.js new file mode 100644 index 0000000000..f01fb80750 --- /dev/null +++ b/Libraries/LibJS/Tests/for-of-basic.js @@ -0,0 +1,43 @@ +load("test-common.js"); + +try { + assertThrowsError(() => { + for (const _ of 123) {} + }, { + error: TypeError, + message: "for..of right-hand side must be iterable" + }); + + assertThrowsError(() => { + for (const _ of {foo: 1, bar: 2}) {} + }, { + error: TypeError, + message: "for..of right-hand side must be iterable" + }); + + assertVisitsAll(visit => { + for (const num of [1, 2, 3]) { + visit(num); + } + }, [1, 2, 3]); + + assertVisitsAll(visit => { + for (const char of "hello") { + visit(char); + } + }, ["h", "e", "l", "l", "o"]); + + assertVisitsAll(visit => { + for (const char of new String("hello")) { + visit(char); + } + }, ["h", "e", "l", "l", "o"]); + + var char; + for (char of "abc"); + assert(char === "c"); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/test-common.js b/Libraries/LibJS/Tests/test-common.js index a31cc67a62..7be49e77bb 100644 --- a/Libraries/LibJS/Tests/test-common.js +++ b/Libraries/LibJS/Tests/test-common.js @@ -50,6 +50,13 @@ function assertThrowsError(testFunction, options) { } } +const assertVisitsAll = (testFunction, expectedOutput) => { + const visited = []; + testFunction(value => visited.push(value)); + assert(visited.length === expectedOutput.length); + expectedOutput.forEach((value, i) => assert(visited[i] === value)); +}; + /** * Check whether the difference between two numbers is less than 0.000001. * @param {Number} a First number |