Bläddra i källkod

LibJS: Add support for SpreadExpressions in array literals for bytecode

For this it adds another opcode `Append $lhs` which appends the
accumulator to the Array in $lhs, optionally spreading it.
Hendiadyoin1 2 år sedan
förälder
incheckning
ae52ae8f9f

+ 38 - 22
Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp

@@ -7,6 +7,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <AK/Find.h>
 #include <AK/Format.h>
 #include <LibJS/AST.h>
 #include <LibJS/Bytecode/Generator.h>
@@ -1085,34 +1086,50 @@ Bytecode::CodeGenerationErrorOr<void> ObjectExpression::generate_bytecode(Byteco
 
 Bytecode::CodeGenerationErrorOr<void> ArrayExpression::generate_bytecode(Bytecode::Generator& generator) const
 {
-    Vector<Bytecode::Register> element_regs;
-    for (auto& element : m_elements) {
-        if (element && is<SpreadExpression>(*element)) {
-            return Bytecode::CodeGenerationError {
-                this,
-                "Unimplemented element kind: SpreadExpression"sv,
-            };
-        }
-        element_regs.append(generator.allocate_register());
+    if (m_elements.is_empty()) {
+        generator.emit<Bytecode::Op::NewArray>();
+        return {};
     }
-    size_t i = 0;
-    for (auto& element : m_elements) {
-        if (element) {
-            TRY(element->generate_bytecode(generator));
 
-            if (is<SpreadExpression>(*element))
-                VERIFY_NOT_REACHED();
-        } else {
+    auto first_spread = find_if(m_elements.begin(), m_elements.end(), [](auto el) { return el && is<SpreadExpression>(*el); });
+
+    Bytecode::Register args_start_reg { 0 };
+    for (auto it = m_elements.begin(); it != first_spread; ++it) {
+        auto reg = generator.allocate_register();
+        if (args_start_reg.index() == 0)
+            args_start_reg = reg;
+    }
+    u32 i = 0;
+    for (auto it = m_elements.begin(); it != first_spread; ++it, ++i) {
+        Bytecode::Register reg { args_start_reg.index() + i };
+        if (!*it)
             generator.emit<Bytecode::Op::LoadImmediate>(Value {});
+        else {
+            TRY((*it)->generate_bytecode(generator));
         }
-        auto& element_reg = element_regs[i++];
-        generator.emit<Bytecode::Op::Store>(element_reg);
+        generator.emit<Bytecode::Op::Store>(reg);
     }
-    if (element_regs.is_empty()) {
+
+    if (first_spread.index() != 0)
+        generator.emit_with_extra_register_slots<Bytecode::Op::NewArray>(2u, AK::Array { args_start_reg, Bytecode::Register { args_start_reg.index() + static_cast<u32>(first_spread.index() - 1) } });
+    else
         generator.emit<Bytecode::Op::NewArray>();
-    } else {
-        generator.emit_with_extra_register_slots<Bytecode::Op::NewArray>(2u, AK::Array { element_regs.first(), element_regs.last() });
+
+    if (first_spread != m_elements.end()) {
+        auto array_reg = generator.allocate_register();
+        generator.emit<Bytecode::Op::Store>(array_reg);
+        for (auto it = first_spread; it != m_elements.end(); ++it) {
+            if (!*it) {
+                generator.emit<Bytecode::Op::LoadImmediate>(Value {});
+                generator.emit<Bytecode::Op::Append>(array_reg, false);
+            } else {
+                TRY((*it)->generate_bytecode(generator));
+                generator.emit<Bytecode::Op::Append>(array_reg, *it && is<SpreadExpression>(**it));
+            }
+        }
+        generator.emit<Bytecode::Op::Load>(array_reg);
     }
+
     return {};
 }
 
@@ -2389,5 +2406,4 @@ Bytecode::CodeGenerationErrorOr<void> MetaProperty::generate_bytecode(Bytecode::
 
     VERIFY_NOT_REACHED();
 }
-
 }

+ 1 - 0
Userland/Libraries/LibJS/Bytecode/Instruction.h

@@ -12,6 +12,7 @@
 
 #define ENUMERATE_BYTECODE_OPS(O)    \
     O(Add)                           \
+    O(Append)                        \
     O(BitwiseAnd)                    \
     O(BitwiseNot)                    \
     O(BitwiseOr)                     \

+ 47 - 0
Userland/Libraries/LibJS/Bytecode/Op.cpp

@@ -182,6 +182,46 @@ ThrowCompletionOr<void> NewArray::execute_impl(Bytecode::Interpreter& interprete
     return {};
 }
 
+ThrowCompletionOr<void> Append::execute_impl(Bytecode::Interpreter& interpreter) const
+{
+    // Note: This OpCode is used to construct array literals containing at least one spread element,
+    //       Iterating over such a spread element to unpack it has to be visible by
+    //       the user courtesy of
+    //       https://tc39.es/ecma262/#sec-runtime-semantics-arrayaccumulation
+    //          SpreadElement : ... AssignmentExpression
+    //              1. Let spreadRef be ? Evaluation of AssignmentExpression.
+    //              2. Let spreadObj be ? GetValue(spreadRef).
+    //              3. Let iteratorRecord be ? GetIterator(spreadObj).
+    //              4. Repeat,
+    //                  a. Let next be ? IteratorStep(iteratorRecord).
+    //                  b. If next is false, return nextIndex.
+    //                  c. Let nextValue be ? IteratorValue(next).
+    //                  d. Perform ! CreateDataPropertyOrThrow(array, ! ToString(𝔽(nextIndex)), nextValue).
+    //                  e. Set nextIndex to nextIndex + 1.
+
+    auto& vm = interpreter.vm();
+
+    // Note: We know from codegen, that lhs is a plain array with only indexed properties
+    auto& lhs = interpreter.reg(m_lhs).as_array();
+    auto lhs_size = lhs.indexed_properties().array_like_size();
+
+    auto rhs = interpreter.accumulator();
+
+    if (m_is_spread) {
+        // ...rhs
+        size_t i = lhs_size;
+        TRY(get_iterator_values(vm, rhs, [&i, &lhs](Value iterator_value) -> Optional<Completion> {
+            lhs.indexed_properties().put(i, iterator_value, default_attributes);
+            ++i;
+            return {};
+        }));
+    } else {
+        lhs.indexed_properties().put(lhs_size, rhs, default_attributes);
+    }
+
+    return {};
+}
+
 // FIXME: Since the accumulator is a Value, we store an object there and have to convert back and forth between that an Iterator records. Not great.
 // Make sure to put this into the accumulator before the iterator object disappears from the stack to prevent the members from being GC'd.
 static Object* iterator_to_object(VM& vm, Iterator iterator)
@@ -943,6 +983,13 @@ String NewArray::to_string_impl(Bytecode::Executable const&) const
     return builder.to_string();
 }
 
+String Append::to_string_impl(Bytecode::Executable const&) const
+{
+    if (m_is_spread)
+        return String::formatted("Append lhs: **{}", m_lhs);
+    return String::formatted("Append lhs: {}", m_lhs);
+}
+
 String IteratorToArray::to_string_impl(Bytecode::Executable const&) const
 {
     return "IteratorToArray";

+ 18 - 0
Userland/Libraries/LibJS/Bytecode/Op.h

@@ -254,6 +254,24 @@ private:
     Register m_elements[];
 };
 
+class Append final : public Instruction {
+public:
+    Append(Register lhs, bool is_spread)
+        : Instruction(Type::Append)
+        , m_lhs(lhs)
+        , m_is_spread(is_spread)
+    {
+    }
+
+    ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
+    String to_string_impl(Bytecode::Executable const&) const;
+    void replace_references_impl(BasicBlock const&, BasicBlock const&) { }
+
+private:
+    Register m_lhs;
+    bool m_is_spread = false;
+};
+
 class IteratorToArray final : public Instruction {
 public:
     IteratorToArray()