Browse Source

LibJS: add Array.prototype.reduceRight()

This patch adds `Array.prototype.reduceRight()` method to LibJS Runtime. The implementation is (to my best knowledge) conformant to https://tc39.es/ecma262/#sec-array.prototype.reduceright.

Short test in `LibJS/Tests/Array.prototype-generic-functions.js` demonstrates that the function can be applied to other objects besides `Array`.
Marcin Gasperowicz 5 years ago
parent
commit
99991761fd

+ 63 - 3
Libraries/LibJS/Runtime/ArrayPrototype.cpp

@@ -59,6 +59,7 @@ ArrayPrototype::ArrayPrototype()
     put_native_function("slice", slice, 2, attr);
     put_native_function("indexOf", index_of, 1, attr);
     put_native_function("reduce", reduce, 1, attr);
+    put_native_function("reduceRight", reduce_right, 1, attr);
     put_native_function("reverse", reverse, 0, attr);
     put_native_function("lastIndexOf", last_index_of, 1, attr);
     put_native_function("includes", includes, 1, attr);
@@ -398,6 +399,10 @@ Value ArrayPrototype::reduce(Interpreter& interpreter)
     if (interpreter.exception())
         return {};
 
+    auto* callback_function = callback_from_args(interpreter, "reduce");
+    if (!callback_function)
+        return {};
+
     size_t start = 0;
 
     auto accumulator = js_undefined();
@@ -420,13 +425,68 @@ Value ArrayPrototype::reduce(Interpreter& interpreter)
         }
     }
 
-    auto* callback_function = callback_from_args(interpreter, "reduce");
+    auto this_value = js_undefined();
+
+    for (size_t i = start; i < initial_length; ++i) {
+        auto value = this_object->get_by_index(i);
+        if (interpreter.exception())
+            return {};
+        if (value.is_empty())
+            continue;
+
+        MarkedValueList arguments(interpreter.heap());
+        arguments.append(accumulator);
+        arguments.append(value);
+        arguments.append(Value((i32)i));
+        arguments.append(this_object);
+
+        accumulator = interpreter.call(*callback_function, this_value, move(arguments));
+        if (interpreter.exception())
+            return {};
+    }
+
+    return accumulator;
+}
+
+Value ArrayPrototype::reduce_right(Interpreter& interpreter)
+{
+    auto* this_object = interpreter.this_value().to_object(interpreter);
+    if (!this_object)
+        return {};
+
+    auto initial_length = get_length(interpreter, *this_object);
+    if (interpreter.exception())
+        return {};
+
+    auto* callback_function = callback_from_args(interpreter, "reduceRight");
     if (!callback_function)
         return {};
 
+    int start = initial_length - 1;
+
+    auto accumulator = js_undefined();
+    if (interpreter.argument_count() > 1) {
+        accumulator = interpreter.argument(1);
+    } else {
+        bool start_found = false;
+        while (!start_found && start >= 0) {
+            auto value = this_object->get_by_index(start);
+            if (interpreter.exception())
+                return {};
+            start_found = !value.is_empty();
+            if (start_found)
+                accumulator = value;
+            start -= 1;
+        }
+        if (!start_found) {
+            interpreter.throw_exception<TypeError>("Reduce of empty array with no initial value");
+            return {};
+        }
+    }
+
     auto this_value = js_undefined();
 
-    for (size_t i = start; i < initial_length; ++i) {
+    for (int i = start; i >= 0; --i) {
         auto value = this_object->get_by_index(i);
         if (interpreter.exception())
             return {};
@@ -436,7 +496,7 @@ Value ArrayPrototype::reduce(Interpreter& interpreter)
         MarkedValueList arguments(interpreter.heap());
         arguments.append(accumulator);
         arguments.append(value);
-        arguments.append(Value((i32)i));
+        arguments.append(Value(i));
         arguments.append(this_object);
 
         accumulator = interpreter.call(*callback_function, this_value, move(arguments));

+ 1 - 0
Libraries/LibJS/Runtime/ArrayPrototype.h

@@ -52,6 +52,7 @@ private:
     static Value slice(Interpreter&);
     static Value index_of(Interpreter&);
     static Value reduce(Interpreter&);
+    static Value reduce_right(Interpreter&);
     static Value reverse(Interpreter&);
     static Value last_index_of(Interpreter&);
     static Value includes(Interpreter&);

+ 17 - 3
Libraries/LibJS/Tests/Array.prototype-generic-functions.js

@@ -83,17 +83,31 @@ try {
         assert(visited[2] === "baz");
     });
 
-    ["reduce"].forEach(name => {
+    {
         const visited = [];
-        Array.prototype[name].call(o, function (_, value) {
+        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");
-    });
+    }
+
+    {
+        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");
+    }
 
     console.log("PASS");
 } catch (e) {

+ 134 - 0
Libraries/LibJS/Tests/Array.prototype.reduceRight.js

@@ -0,0 +1,134 @@
+load("test-common.js");
+
+try {
+  assert(Array.prototype.reduceRight.length === 1);
+
+  assertThrowsError(
+    () => {
+      [1].reduceRight();
+    },
+    {
+      error: TypeError,
+      message: "Array.prototype.reduceRight() requires at least one argument",
+    }
+  );
+
+  assertThrowsError(
+    () => {
+      [1].reduceRight(undefined);
+    },
+    {
+      error: TypeError,
+      message: "undefined is not a function",
+    }
+  );
+
+  assertThrowsError(
+    () => {
+      [].reduceRight((a, x) => x);
+    },
+    {
+      error: TypeError,
+      message: "Reduce of empty array with no initial value",
+    }
+  );
+
+  assertThrowsError(
+    () => {
+      [, ,].reduceRight((a, x) => x);
+    },
+    {
+      error: TypeError,
+      message: "Reduce of empty array with no initial value",
+    }
+  );
+
+  [1, 2].reduceRight(() => {
+    assert(this === undefined);
+  });
+
+  var callbackCalled = 0;
+  var callback = () => {
+    callbackCalled++;
+    return true;
+  };
+
+  assert([1].reduceRight(callback) === 1);
+  assert(callbackCalled === 0);
+
+  assert([1].reduceRight(callback) === 1);
+  assert(callbackCalled === 0);
+
+  callbackCalled = 0;
+  assert([1, 2, 3].reduceRight(callback) === true);
+  assert(callbackCalled === 2);
+
+  callbackCalled = 0;
+  assert([1, 2, 3, ,].reduceRight(callback) === true);
+  assert(callbackCalled === 2);
+
+  callbackCalled = 0;
+  assert([, , , 1, , , 10, , 100, , ,].reduceRight(callback) === true);
+  assert(callbackCalled === 2);
+
+  var constantlySad = () => ":^(";
+  var result = [].reduceRight(constantlySad, ":^)");
+  assert(result === ":^)");
+
+  result = [":^0"].reduceRight(constantlySad, ":^)");
+  assert(result === ":^(");
+
+  result = [":^0"].reduceRight(constantlySad);
+  assert(result === ":^0");
+
+  result = [5, 4, 3, 2, 1].reduceRight((accum, elem) => "" + accum + elem);
+  assert(result === "12345");
+
+  result = [1, 2, 3, 4, 5, 6].reduceRight((accum, elem) => {
+    return "" + accum + elem;
+  }, 100);
+  assert(result === "100654321");
+
+  result = [6, 5, 4, 3, 2, 1].reduceRight((accum, elem) => {
+    return "" + accum + elem;
+  }, 100);
+  assert(result === "100123456");
+
+  var indexes = [];
+  result = ["foo", 1, true].reduceRight((a, v, i) => {
+    indexes.push(i);
+  });
+  assert(result === undefined);
+  assert(indexes.length === 2);
+  assert(indexes[0] === 1);
+  assert(indexes[1] === 0);
+
+  indexes = [];
+  result = ["foo", 1, true].reduceRight((a, v, i) => {
+    indexes.push(i);
+  }, "foo");
+  assert(result === undefined);
+  assert(indexes.length === 3);
+  assert(indexes[0] === 2);
+  assert(indexes[1] === 1);
+  assert(indexes[2] === 0);
+
+  var mutable = { prop: 0 };
+  result = ["foo", 1, true].reduceRight((a, v) => {
+    a.prop = v;
+    return a;
+  }, mutable);
+  assert(result === mutable);
+  assert(result.prop === "foo");
+
+  var a1 = [1, 2];
+  var a2 = null;
+  a1.reduceRight((a, v, i, t) => {
+    a2 = t;
+  });
+  assert(a1 === a2);
+
+  console.log("PASS");
+} catch (e) {
+  console.log("FAIL: " + e);
+}