From 80fecc615a632a5392e499563678de887e52f5df Mon Sep 17 00:00:00 2001 From: mattco98 Date: Sun, 26 Apr 2020 23:05:37 -0700 Subject: [PATCH] LibJS: Add spreading in array literals Implement the syntax and behavor necessary to support array literals such as [...[1, 2, 3]]. A type error is thrown if the target of the spread operator does not evaluate to an array (though it should eventually just check for an iterable). Note that the spread token's name is TripleDot, since the '...' token is used for two features: spread and rest. Calling it anything involving 'spread' or 'rest' would be a bit confusing. --- Libraries/LibJS/AST.cpp | 30 +++++++++++++++++++ Libraries/LibJS/AST.h | 18 +++++++++++ Libraries/LibJS/Lexer.cpp | 1 + Libraries/LibJS/Parser.cpp | 10 +++++-- Libraries/LibJS/Tests/array-spread.js | 43 +++++++++++++++++++++++++++ Libraries/LibJS/Token.h | 1 + 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 Libraries/LibJS/Tests/array-spread.js diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 2633cb6da4f..7b1b25e4a2e 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -736,6 +736,17 @@ void Identifier::dump(int indent) const printf("Identifier \"%s\"\n", m_string.characters()); } +void SpreadExpression::dump(int indent) const +{ + ASTNode::dump(indent); + m_target->dump(indent + 1); +} + +Value SpreadExpression::execute(Interpreter& interpreter) const +{ + return m_target->execute(interpreter); +} + Value ThisExpression::execute(Interpreter& interpreter) const { return interpreter.this_value(); @@ -1085,8 +1096,27 @@ Value ArrayExpression::execute(Interpreter& interpreter) const auto value = Value(); if (element) { value = element->execute(interpreter); + if (interpreter.exception()) return {}; + + if (element->is_spread_expression()) { + if (!value.is_array()) { + interpreter.throw_exception(String::format("%s is not iterable", value.to_string().characters())); + return {}; + } + + auto& array_to_spread = static_cast(value.as_object()); + for (auto& it : array_to_spread.elements()) { + if (it.is_empty()) { + array->elements().append(js_undefined()); + } else { + array->elements().append(it); + } + } + + continue; + } } array->elements().append(value); } diff --git a/Libraries/LibJS/AST.h b/Libraries/LibJS/AST.h index bec3f2910aa..54d6152a5d6 100644 --- a/Libraries/LibJS/AST.h +++ b/Libraries/LibJS/AST.h @@ -55,6 +55,7 @@ public: virtual Value execute(Interpreter&) const = 0; virtual void dump(int indent) const; virtual bool is_identifier() const { return false; } + virtual bool is_spread_expression() const { return false; } virtual bool is_member_expression() const { return false; } virtual bool is_scope_node() const { return false; } virtual bool is_program() const { return false; } @@ -520,6 +521,23 @@ private: FlyString m_string; }; +class SpreadExpression final : public Expression { +public: + explicit SpreadExpression(NonnullRefPtr target) + : m_target(target) + { + } + + virtual Value execute(Interpreter&) const override; + virtual void dump(int indent) const override; + virtual bool is_spread_expression() const override { return true; } + +private: + virtual const char* class_name() const override { return "SpreadExpression"; } + + NonnullRefPtr m_target; +}; + class ThisExpression final : public Expression { public: virtual Value execute(Interpreter&) const override; diff --git a/Libraries/LibJS/Lexer.cpp b/Libraries/LibJS/Lexer.cpp index 79eadc4bbf7..c59a8cd17e9 100644 --- a/Libraries/LibJS/Lexer.cpp +++ b/Libraries/LibJS/Lexer.cpp @@ -84,6 +84,7 @@ Lexer::Lexer(StringView source) s_three_char_tokens.set("<<=", TokenType::ShiftLeftEquals); s_three_char_tokens.set(">>=", TokenType::ShiftRightEquals); s_three_char_tokens.set(">>>", TokenType::UnsignedShiftRight); + s_three_char_tokens.set("...", TokenType::TripleDot); } if (s_two_char_tokens.is_empty()) { diff --git a/Libraries/LibJS/Parser.cpp b/Libraries/LibJS/Parser.cpp index caf0e7da750..63e0a5bd953 100644 --- a/Libraries/LibJS/Parser.cpp +++ b/Libraries/LibJS/Parser.cpp @@ -492,10 +492,16 @@ NonnullRefPtr Parser::parse_array_expression() consume(TokenType::BracketOpen); Vector> elements; - while (match_expression() || match(TokenType::Comma)) { + while (match_expression() || match(TokenType::TripleDot) || match(TokenType::Comma)) { RefPtr expression; - if (match_expression()) + + if (match(TokenType::TripleDot)) { + consume(TokenType::TripleDot); + expression = create_ast_node(parse_expression(0)); + } else if (match_expression()) { expression = parse_expression(0); + } + elements.append(expression); if (!match(TokenType::Comma)) break; diff --git a/Libraries/LibJS/Tests/array-spread.js b/Libraries/LibJS/Tests/array-spread.js new file mode 100644 index 00000000000..470b8cb09da --- /dev/null +++ b/Libraries/LibJS/Tests/array-spread.js @@ -0,0 +1,43 @@ +load("test-common.js"); + +function testArray(arr) { + return arr.length === 4 && + arr[0] === 0 && + arr[1] === 1 && + arr[2] === 2 && + arr[3] === 3; +} + +try { + let arr = [0, ...[1, 2], 3]; + assert(testArray(arr)); + + let a = [1, 2]; + arr = [0, ...a, 3]; + assert(testArray(arr)); + + let obj = { a: [1, 2] }; + arr = [0, ...obj.a, 3]; + assert(testArray(arr)); + + arr = [...[], ...[...[0, 1, 2]], 3]; + assert(testArray(arr)); + + assertThrowsError(() => { + [...1]; + }, { + error: TypeError, + message: "1 is not iterable", + }); + + assertThrowsError(() => { + [...{}]; + }, { + error: TypeError, + message: "[object Object] is not iterable", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Token.h b/Libraries/LibJS/Token.h index b3013326498..f60a288ce1a 100644 --- a/Libraries/LibJS/Token.h +++ b/Libraries/LibJS/Token.h @@ -115,6 +115,7 @@ namespace JS { __ENUMERATE_JS_TOKEN(This) \ __ENUMERATE_JS_TOKEN(Throw) \ __ENUMERATE_JS_TOKEN(Tilde) \ + __ENUMERATE_JS_TOKEN(TripleDot) \ __ENUMERATE_JS_TOKEN(Try) \ __ENUMERATE_JS_TOKEN(Typeof) \ __ENUMERATE_JS_TOKEN(UnsignedShiftRight) \