diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index 378e972084..98e83e5af0 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -345,6 +345,60 @@ function ArrayWith(index, value) { return A; } +// ES2023 22.1.3.35 Array.prototype.toSpliced ( start, deleteCount, ...items ) +function ArrayToSpliced(start, deleteCount) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var len = ToLength(O.length); + + // Step 3. + var relativeStart = ToInteger(start); + var actualStart = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 4. + var insertCount = arguments.length > 2 ? arguments.length - 2 : 0; + + // Step 5. + var actualDeleteCount; + if (arguments.length === 0) { + actualDeleteCount = 0; + } else if (arguments.length === 1) { + actualDeleteCount = len - actualStart; + } else { + var dc = ToInteger(deleteCount); + actualDeleteCount = std_Math_min(std_Math_max(dc, 0), len - actualStart); + } + + // Steps 6-8. + var newLen = len + insertCount - actualDeleteCount; + if (newLen > MAX_NUMERIC_INDEX) ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + var A = std_Array(newLen); + + // Steps 9-10. + var k = 0; + for (; k < actualStart; k++) { + _DefineDataProperty(A, k, O[k]); + } + + // Step 11. + for (var i = 2; i < arguments.length; i++) { + _DefineDataProperty(A, k++, arguments[i]); + } + + // Steps 12-13. + for (var from = actualStart + actualDeleteCount; from < len; from++) { + _DefineDataProperty(A, k++, O[from]); + } + + // Step 14. + return A; +} + /* ES5 15.4.4.18. */ function ArrayForEach(callbackfn /*, thisArg*/) { /* Step 1. */ diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp index cde6d59b91..15c0471b6d 100644 --- a/js/src/jsarray.cpp +++ b/js/src/jsarray.cpp @@ -3209,6 +3209,7 @@ static const JSFunctionSpec array_methods[] = { JS_SELF_HOSTED_FN("findLast", "ArrayFindLast", 1,0), JS_SELF_HOSTED_FN("findLastIndex", "ArrayFindLastIndex", 1,0), JS_SELF_HOSTED_FN("toReversed", "ArrayToReversed", 0,0), + JS_SELF_HOSTED_FN("toSpliced", "ArrayToSpliced", 2,0), JS_SELF_HOSTED_FN("toSorted", "ArrayToSorted", 1,0), JS_SELF_HOSTED_FN("with", "ArrayWith", 2,0), @@ -3383,6 +3384,7 @@ array_proto_finish(JSContext* cx, JS::HandleObject ctor, JS::HandleObject proto) !DefineProperty(cx, unscopables, cx->names().includes, value) || !DefineProperty(cx, unscopables, cx->names().keys, value) || !DefineProperty(cx, unscopables, cx->names().toReversed, value) || + !DefineProperty(cx, unscopables, cx->names().toSpliced, value) || !DefineProperty(cx, unscopables, cx->names().values, value) || !DefineProperty(cx, unscopables, cx->names().toSorted, value) || !DefineProperty(cx, unscopables, cx->names().with, value)) diff --git a/js/src/tests/ecma_6/Array/toSpliced.html b/js/src/tests/ecma_6/Array/toSpliced.html new file mode 100644 index 0000000000..9a8f886016 --- /dev/null +++ b/js/src/tests/ecma_6/Array/toSpliced.html @@ -0,0 +1,59 @@ + + +Array.prototype.toSpliced test + +

+
+
diff --git a/js/src/tests/ecma_6/Array/toSpliced.js b/js/src/tests/ecma_6/Array/toSpliced.js
new file mode 100644
index 0000000000..141e409776
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/toSpliced.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/ */
+
+assertEq(typeof Array.prototype.toSpliced, "function");
+assertEq(Array.prototype.toSpliced.length, 2);
+
+let desc = Object.getOwnPropertyDescriptor(Array.prototype, "toSpliced");
+assertEq(desc.writable, true);
+assertEq(desc.enumerable, false);
+assertEq(desc.configurable, true);
+
+assertThrowsInstanceOf(function () {
+  Array.prototype.toSpliced.call(null, 0, 0);
+}, TypeError);
+assertThrowsInstanceOf(function () {
+  Array.prototype.toSpliced.call(undefined, 0, 0);
+}, TypeError);
+
+// Non-mutating behavior.
+let original = [1, 2, 3, 4];
+let spliced = original.toSpliced(1, 2, "a", "b");
+assertEq(original !== spliced, true);
+assertEq(original.join(","), "1,2,3,4");
+assertEq(spliced.join(","), "1,a,b,4");
+assertEq(original.toSpliced().join(","), "1,2,3,4");
+
+// Start index clamping and negatives.
+assertEq([1, 2, 3].toSpliced(-1, 1, 9).join(","), "1,2,9");
+assertEq([1, 2, 3].toSpliced(-99, 1, 9).join(","), "9,2,3");
+assertEq([1, 2, 3].toSpliced(99, 1, 9).join(","), "1,2,3,9");
+
+// deleteCount handling.
+assertEq([1, 2, 3].toSpliced(1).join(","), "1");
+assertEq([1, 2, 3].toSpliced(1, -3, 9).join(","), "1,9,2,3");
+
+// Holes are materialized as undefined in copied segments.
+let sparse = [1, , 3, 4];
+let sparseResult = sparse.toSpliced(2, 1, "x");
+assertEq(sparseResult.length, 4);
+assertEq(sparseResult[0], 1);
+assertEq(sparseResult[1], undefined);
+assertEq(1 in sparseResult, true);
+assertEq(sparseResult[2], "x");
+assertEq(sparseResult[3], 4);
+
+// Generic behavior on array-like values.
+let arrayLike = { 0: "a", 1: "b", 3: "d", length: 4 };
+let arrayLikeResult = Array.prototype.toSpliced.call(arrayLike, 1, 2, "X", "Y");
+assertEq(Array.isArray(arrayLikeResult), true);
+assertEq(arrayLikeResult.join(","), "a,X,Y,d");
+
+// Element reads are ascending and skipped region is not read.
+let accessLog = [];
+let getterArr = {
+  length: 5,
+  get 0() {
+    accessLog.push(0);
+    return "z0";
+  },
+  get 1() {
+    accessLog.push(1);
+    throw new Error("must not be read");
+  },
+  get 2() {
+    accessLog.push(2);
+    throw new Error("must not be read");
+  },
+  get 3() {
+    accessLog.push(3);
+    return "z3";
+  },
+  get 4() {
+    accessLog.push(4);
+    return "z4";
+  },
+};
+let getterResult = Array.prototype.toSpliced.call(getterArr, 1, 2, "X");
+assertEq(accessLog.join(","), "0,3,4");
+assertEq(getterResult.join(","), "z0,X,z3,z4");
+
+// constructor / Symbol.species is ignored and constructor is not read.
+let constructorAccessed = false;
+let speciesIgnored = [1, 2, 3];
+Object.defineProperty(speciesIgnored, "constructor", {
+  get: function () {
+    constructorAccessed = true;
+    throw new Error("constructor should not be read");
+  },
+  configurable: true,
+});
+let speciesIgnoredResult = speciesIgnored.toSpliced(1, 1, 9);
+assertEq(constructorAccessed, false);
+assertEq(Array.isArray(speciesIgnoredResult), true);
+assertEq(speciesIgnoredResult.join(","), "1,9,3");
+
+// Length limit check happens before indexed reads.
+let indexedRead = false;
+let tooLong = {
+  length: 4294967296,
+  get 0() {
+    indexedRead = true;
+    throw new Error("index getter should not run");
+  },
+};
+assertThrowsInstanceOf(function () {
+  Array.prototype.toSpliced.call(tooLong, 0, 0);
+}, RangeError);
+assertEq(indexedRead, false);
+
+// @@unscopables must include toSpliced.
+assertEq(Array.prototype[Symbol.unscopables].toSpliced, true);
+
+if (typeof reportCompare === "function") reportCompare(0, 0);
diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h
index 6466958917..64bab961cd 100644
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -430,6 +430,7 @@
     macro(toLocaleString, toLocaleString, "toLocaleString") \
     macro(toReversed, toReversed, "toReversed") \
     macro(toSource, toSource, "toSource") \
+    macro(toSpliced, toSpliced, "toSpliced") \
     macro(toSorted, toSorted, "toSorted") \
     macro(toString, toString, "toString") \
     macro(toUTCString, toUTCString, "toUTCString") \