From 738aaeb6e625619020f74bd9415f03e38fcee2b5 Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Fri, 4 Dec 2020 14:46:00 +0000 Subject: [PATCH] Bug 1658308 - Implement the .at() proposal. r=jandem Differential Revision: https://phabricator.services.mozilla.com/D98743 --- js/src/builtin/Array.cpp | 5 ++ js/src/builtin/Array.js | 29 +++++++++++ js/src/builtin/String.cpp | 4 ++ js/src/builtin/String.js | 33 +++++++++++++ js/src/builtin/TypedArray.js | 39 +++++++++++++++ js/src/tests/non262/Array/at.js | 41 ++++++++++++++++ js/src/tests/non262/String/at.js | 40 +++++++++++++++ js/src/tests/non262/TypedArray/at.js | 49 +++++++++++++++++++ js/src/vm/TypedArrayObject.cpp | 3 ++ js/xpconnect/tests/chrome/test_xrayToJS.xhtml | 6 +++ 10 files changed, 249 insertions(+) create mode 100644 js/src/tests/non262/Array/at.js create mode 100644 js/src/tests/non262/String/at.js create mode 100644 js/src/tests/non262/TypedArray/at.js diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp index 50ff8d649a1c..49e368d875a6 100644 --- a/js/src/builtin/Array.cpp +++ b/js/src/builtin/Array.cpp @@ -3701,6 +3701,11 @@ static const JSFunctionSpec array_methods[] = { JS_SELF_HOSTED_FN("flatMap", "ArrayFlatMap", 1, 0), JS_SELF_HOSTED_FN("flat", "ArrayFlat", 0, 0), +/* Proposal */ +#ifdef NIGHTLY_BUILD + JS_SELF_HOSTED_FN("at", "ArrayAt", 1, 0), +#endif + JS_FS_END}; static const JSFunctionSpec array_static_methods[] = { diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index 772b56e66e12..e5dd40f3495f 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -1140,3 +1140,32 @@ function FlattenIntoArray(target, source, sourceLen, start, depth, mapperFunctio // Step 4. return targetIndex; } + +// https://github.com/tc39/proposal-relative-indexing-method +// Array.prototype.at ( index ) +function ArrayAt(index) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var len = ToLength(O.length); + + // Step 3. + var relativeIndex = ToInteger(index); + + // Steps 4-5. + var k; + if (relativeIndex >= 0) { + k = relativeIndex; + } else { + k = len + relativeIndex; + } + + // Step 6. + if (k < 0 || k >= len) { + return undefined; + } + + // Step 7. + return O[k]; +} diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 5de1e3c48999..45a5334d66d7 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -3599,6 +3599,10 @@ static const JSFunctionSpec string_methods[] = { JS_SELF_HOSTED_FN("concat", "String_concat", 1, 0), JS_SELF_HOSTED_FN("slice", "String_slice", 2, 0), +#ifdef NIGHTLY_BUILD + JS_SELF_HOSTED_FN("at", "String_at", 1, 0), +#endif + /* HTML string methods. */ JS_SELF_HOSTED_FN("bold", "String_bold", 0, 0), JS_SELF_HOSTED_FN("italics", "String_italics", 0, 0), diff --git a/js/src/builtin/String.js b/js/src/builtin/String.js index 5ed429b4994e..efeb65360e84 100644 --- a/js/src/builtin/String.js +++ b/js/src/builtin/String.js @@ -937,6 +937,39 @@ function String_static_raw(callSite/*, ...substitutions*/) { return resultString; } +// https://github.com/tc39/proposal-relative-indexing-method +// String.prototype.at ( index ) +function String_at(index) { + // Step 1. + if (this === undefined || this === null) + ThrowIncompatibleMethod("at", this); + + // Step 2. + var string = ToString(this); + + // Step 3. + var len = string.length; + + // Step 4. + var relativeIndex = ToInteger(index); + + // Steps 5-6. + var k; + if (relativeIndex >= 0) { + k = relativeIndex; + } else { + k = len + relativeIndex; + } + + // Step 7. + if (k < 0 || k >= len) { + return undefined; + } + + // Step 8. + return string[k]; +} + // ES6 draft 2014-04-27 B.2.3.3 function String_big() { if (this === undefined || this === null) diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 9ae8180ef245..4303ac22e6f1 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -1337,6 +1337,45 @@ function TypedArraySubarray(begin, end) { return TypedArraySpeciesCreateWithBuffer(obj, buffer, beginByteOffset, newLength); } +// https://tc39.es/proposal-relative-indexing-method +// %TypedArray%.prototype.at ( index ) +function TypedArrayAt(index) { + // Step 1. + var obj = this; + + // Step 2. + // This function is not generic. + if (!IsObject(obj) || !IsTypedArray(obj)) { + return callFunction(CallTypedArrayMethodIfWrapped, obj, index, + "TypedArrayAt"); + } + GetAttachedArrayBuffer(obj); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len = TypedArrayLength(obj); + + // Step 4. + var relativeIndex = ToInteger(index); + + // Steps 5-6. + var k; + if (relativeIndex >= 0) { + k = relativeIndex; + } else { + k = len + relativeIndex; + } + + // Step 7. + if (k < 0 || k >= len) { + return undefined; + } + + // Step 8. + return obj[k]; +} + // ES6 draft rev30 (2014/12/24) 22.2.3.30 %TypedArray%.prototype.values() // // Uncloned functions with `$` prefix are allocated as extended function diff --git a/js/src/tests/non262/Array/at.js b/js/src/tests/non262/Array/at.js new file mode 100644 index 000000000000..0e7ca9cb14e3 --- /dev/null +++ b/js/src/tests/non262/Array/at.js @@ -0,0 +1,41 @@ +// |reftest| skip-if(!Array.prototype.at) + +function basic() { + assertEq([0].at(0), 0); + assertEq([0].at(-1), 0); + + assertEq([].at(0), undefined); + assertEq([].at(-1), undefined); + assertEq([].at(1), undefined); + + assertEq([0, 1].at(0), 0); + assertEq([0, 1].at(1), 1); + assertEq([0, 1].at(-2), 0); + assertEq([0, 1].at(-1), 1); + + assertEq([0, 1].at(2), undefined); + assertEq([0, 1].at(-3), undefined); + assertEq([0, 1].at(-4), undefined); + assertEq([0, 1].at(Infinity), undefined); + assertEq([0, 1].at(-Infinity), undefined); + assertEq([0, 1].at(NaN), 0); // ToInteger(NaN) = 0 +} + +function obj() { + var o = {length: 0, [0]: "a", at: Array.prototype.at}; + + assertEq(o.at(0), undefined); + assertEq(o.at(-1), undefined); + + o.length = 1; + assertEq(o.at(0), "a"); + assertEq(o.at(-1), "a"); + assertEq(o.at(1), undefined); + assertEq(o.at(-2), undefined); +} + +basic(); +obj(); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/non262/String/at.js b/js/src/tests/non262/String/at.js new file mode 100644 index 000000000000..88554edb41a7 --- /dev/null +++ b/js/src/tests/non262/String/at.js @@ -0,0 +1,40 @@ +// |reftest| skip-if(!String.prototype.at) + +function basic() { + assertEq("a".at(0), "a"); + assertEq("a".at(-1), "a"); + + assertEq("".at(0), undefined); + assertEq("".at(-1), undefined); + assertEq("".at(1), undefined); + + assertEq("ab".at(0), "a"); + assertEq("ab".at(1), "b"); + assertEq("ab".at(-2), "a"); + assertEq("ab".at(-1), "b"); + + assertEq("ab".at(2), undefined); + assertEq("ab".at(-3), undefined); + assertEq("ab".at(-4), undefined); + assertEq("ab".at(Infinity), undefined); + assertEq("ab".at(-Infinity), undefined); + assertEq("ab".at(NaN), "a"); // ToInteger(NaN) = 0 + + assertEq("\u{1f921}".at(0), "\u{d83e}"); + assertEq("\u{1f921}".at(1), "\u{dd21}"); +} + +function other() { + var n = 146; + assertEq(String.prototype.at.call(n, 0), "1"); + var obj = {}; + assertEq(String.prototype.at.call(obj, -1), "]"); + var b = true; + assertEq(String.prototype.at.call(b, 0), "t"); +} + +basic(); +other(); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/non262/TypedArray/at.js b/js/src/tests/non262/TypedArray/at.js new file mode 100644 index 000000000000..c9bc1697e27c --- /dev/null +++ b/js/src/tests/non262/TypedArray/at.js @@ -0,0 +1,49 @@ +// |reftest| skip-if(!Int32Array.prototype.at) + +for (var constructor of anyTypedArrayConstructors) { + assertEq(constructor.prototype.at.length, 1); + + assertEq(new constructor([0]).at(0), 0); + assertEq(new constructor([0]).at(-1), 0); + + assertEq(new constructor([]).at(0), undefined); + assertEq(new constructor([]).at(-1), undefined); + assertEq(new constructor([]).at(1), undefined); + + assertEq(new constructor([0, 1]).at(0), 0); + assertEq(new constructor([0, 1]).at(1), 1); + assertEq(new constructor([0, 1]).at(-2), 0); + assertEq(new constructor([0, 1]).at(-1), 1); + + assertEq(new constructor([0, 1]).at(2), undefined); + assertEq(new constructor([0, 1]).at(-3), undefined); + assertEq(new constructor([0, 1]).at(-4), undefined); + assertEq(new constructor([0, 1]).at(Infinity), undefined); + assertEq(new constructor([0, 1]).at(-Infinity), undefined); + assertEq(new constructor([0, 1]).at(NaN), 0); // ToInteger(NaN) = 0 + + // Called from other globals. + if (typeof newGlobal === "function") { + var at = newGlobal()[constructor.name].prototype.at; + assertEq(at.call(new constructor([1, 2, 3]), 2), 3); + } + + // Throws if `this` isn't a TypedArray. + var invalidReceivers = [undefined, null, 1, false, "", Symbol(), [], {}, /./, + new Proxy(new constructor(), {})]; + invalidReceivers.forEach(invalidReceiver => { + assertThrowsInstanceOf(() => { + constructor.prototype.at.call(invalidReceiver); + }, TypeError, "Assert that 'at' fails if this value is not a TypedArray"); + }); + + // Test that the length getter is never called. + assertEq(Object.defineProperty(new constructor([1, 2, 3]), "length", { + get() { + throw new Error("length accessor called"); + } + }).at(1), 2); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index 877fbe751f24..726475299177 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -1887,6 +1887,9 @@ bool TypedArrayObject::set(JSContext* cx, unsigned argc, Value* vp) { JS_SELF_HOSTED_FN("includes", "TypedArrayIncludes", 2, 0), JS_SELF_HOSTED_FN("toString", "ArrayToString", 0, 0), JS_SELF_HOSTED_FN("toLocaleString", "TypedArrayToLocaleString", 2, 0), +#ifdef NIGHTLY_BUILD + JS_SELF_HOSTED_FN("at", "TypedArrayAt", 1, 0), +#endif JS_FS_END}; /* static */ const JSFunctionSpec TypedArrayObject::staticFunctions[] = { diff --git a/js/xpconnect/tests/chrome/test_xrayToJS.xhtml b/js/xpconnect/tests/chrome/test_xrayToJS.xhtml index d60d8c05bf29..52bcb0774aa4 100644 --- a/js/xpconnect/tests/chrome/test_xrayToJS.xhtml +++ b/js/xpconnect/tests/chrome/test_xrayToJS.xhtml @@ -235,6 +235,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=933681 "includes", "forEach", "map", "reduce", "reduceRight", "filter", "some", "every", "find", "findIndex", "copyWithin", "fill", Symbol.iterator, Symbol.unscopables, "entries", "keys", "values", "constructor", "flat", "flatMap"]; + if (isNightlyBuild) { + gPrototypeProperties['Array'].push("at"); + } gConstructorProperties['Array'] = constructorProps(["isArray", "from", "of", Symbol.species]); for (var c of typedArrayClasses) { @@ -246,6 +249,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=933681 "set", "copyWithin", "find", "findIndex", "forEach","indexOf", "lastIndexOf", "includes", "reverse", "join", "every", "some", "reduce", "reduceRight", "entries", "keys", "values", "slice", "map", "filter"]; + if (isNightlyBuild) { + gPrototypeProperties['TypedArray'].push("at"); + } // There is no TypedArray constructor, looks like. is(window.TypedArray, undefined, "If this ever changes, add to this test!"); for (var c of errorObjectClasses) {