Bug 1121936 - Implement %TypedArray%.prototyp.{map,filter}. r=evilpie
authorEric Skoglund <eric@pagefault.se>
Thu, 19 Feb 2015 15:39:07 +0100
changeset 257003 5f26a8bdf93ea664f31ccb2542cfeb4d21bc81e5
parent 257002 b5ec2e74a50cf51222c895974df9b2caab9e6252
child 257004 05666fcd8e286a199f597032caa1b39246f618c8
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersevilpie
bugs1121936
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1121936 - Implement %TypedArray%.prototyp.{map,filter}. r=evilpie
js/src/builtin/TypedArray.js
js/src/tests/ecma_6/TypedArray/map-and-filter.js
js/src/vm/TypedArrayObject.cpp
js/xpconnect/tests/chrome/test_xrayToJS.xul
--- a/js/src/builtin/TypedArray.js
+++ b/js/src/builtin/TypedArray.js
@@ -93,16 +93,80 @@ function TypedArrayFill(value, start = 0
     for (; k < final; k++) {
         O[k] = value;
     }
 
     // Step 13.
     return O;
 }
 
+// ES6 draft 32 (2015-02-02) 22.2.3.9 %TypedArray%.prototype.filter(callbackfn[, thisArg])
+function TypedArrayFilter(callbackfn, thisArg = undefined) {
+    // Step 1.
+    var O = this;
+
+    // Steps 2-3.
+    // This function is not generic.
+    if (!IsObject(O) || !IsTypedArray(O)) {
+        return callFunction(CallTypedArrayMethodIfWrapped, this, callbackfn, thisArg,
+                           "TypedArrayFilter");
+    }
+
+    // Step 4.
+    var len = TypedArrayLength(O);
+
+    // Step 5.
+    if (arguments.length === 0)
+        ThrowError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.filter");
+    if (!IsCallable(callbackfn))
+        ThrowError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn));
+
+    // Step 6.
+    var T = thisArg;
+
+    // Step 7.
+    var defaultConstructor = _ConstructorForTypedArray(O);
+
+    // Steps 8-9.
+    var C = SpeciesConstructor(O, defaultConstructor);
+
+    // Step 10.
+    var kept = new List();
+
+    // Step 12.
+    var captured = 0;
+
+    // Steps 11, 13 and 13.g.
+    for (var k = 0; k < len; k++) {
+        // Steps 13.b-c.
+        var kValue = O[k];
+        // Steps 13.d-e.
+        var selected = ToBoolean(callFunction(callbackfn, T, kValue, k, O));
+        // Step 13.f.
+        if (selected) {
+            // Step 13.f.i.
+            kept.push(kValue);
+            // Step 13.f.ii.
+            captured++;
+        }
+    }
+
+    // Steps 14-15.
+    var A = new C(captured);
+
+    // Steps 16 and 17.c.
+    for (var n = 0; n < captured; n++) {
+        // Steps 17.a-b.
+        A[n] = kept[n];
+    }
+
+    // Step 18.
+    return A;
+}
+
 // ES6 draft rev28 (2014/10/14) 22.2.3.10 %TypedArray%.prototype.find(predicate[, thisArg]).
 function TypedArrayFind(predicate, thisArg = undefined) {
     // This function is not generic.
     if (!IsObject(this) || !IsTypedArray(this)) {
         return callFunction(CallTypedArrayMethodIfWrapped, this, predicate, thisArg,
                             "TypedArrayFind");
     }
 
@@ -348,16 +412,61 @@ function TypedArrayLastIndexOf(searchEle
         if (O[k] === searchElement)
             return k;
     }
 
     // Step 12.
     return -1;
 }
 
+// ES6 draft rev32 (2015-02-02) 22.2.3.18 %TypedArray%.prototype.map(callbackfn [, thisArg]).
+function TypedArrayMap(callbackfn, thisArg = undefined) {
+    // Step 1.
+    var O = this;
+
+    // Steps 2-3.
+    // This function is not generic.
+    if (!IsObject(O) || !IsTypedArray(O)) {
+        return callFunction(CallTypedArrayMethodIfWrapped, this, callbackfn, thisArg,
+                            "TypedArrayMap");
+    }
+
+    // Step 4.
+    var len = TypedArrayLength(O);
+
+    // Step 5.
+    if (arguments.length === 0)
+        ThrowError(JSMSG_MISSING_FUN_ARG, 0, '%TypedArray%.prototype.map');
+    if (!IsCallable(callbackfn))
+        ThrowError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn));
+
+    // Step 6.
+    var T = thisArg;
+
+    // Step 7.
+    var defaultConstructor = _ConstructorForTypedArray(O);
+
+    // Steps 8-9.
+    var C = SpeciesConstructor(O, defaultConstructor);
+
+    // Steps 10-11.
+    var A = new C(len);
+
+    // Steps 12, 13.a (implicit) and 13.h.
+    for (var k = 0; k < len; k++) {
+        // Steps 13.d-e.
+        var mappedValue = callFunction(callbackfn, T, O[k], k, O);
+        // Steps 13.f-g.
+        A[k] = mappedValue;
+    }
+
+    // Step 14.
+    return A;
+}
+
 // ES6 draft rev30 (2014/12/24) 22.2.3.19 %TypedArray%.prototype.reduce(callbackfn[, initialValue]).
 function TypedArrayReduce(callbackfn/*, initialValue*/) {
     // This function is not generic.
     if (!IsObject(this) || !IsTypedArray(this))
         return callFunction(CallTypedArrayMethodIfWrapped, this, callbackfn, "TypedArrayReduce");
 
     // Steps 1-2.
     var O = this;
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/TypedArray/map-and-filter.js
@@ -0,0 +1,297 @@
+const constructors = [
+    Int8Array,
+    Uint8Array,
+    Uint8ClampedArray,
+    Int16Array,
+    Uint16Array,
+    Int32Array,
+    Uint32Array,
+    Float32Array,
+    Float64Array
+];
+
+// Tests for TypedArray#map.
+for (var constructor of constructors) {
+    assertEq(constructor.prototype.map.length, 1);
+
+    // Basic tests.
+    assertDeepEq(new constructor([1, 3, 5]).map(v => v * 2), new constructor([2,6,10]));
+    assertDeepEq(new constructor([-1, 13, 5]).map(v => v - 2), new constructor([-3, 11, 3]));
+    assertDeepEq(new constructor(10).map(v => v), new constructor(10));
+    assertDeepEq(new constructor().map(v => v + 1), new constructor);
+    assertDeepEq(new constructor([1,2,3]).map(v => v), new constructor([1,2,3]));
+
+    var arr = new constructor([1, 2, 3, 4, 5]);
+    var sum = 0;
+    var count = 0;
+    assertDeepEq(arr.map((v, k, o) => {
+        count++;
+        sum += v;
+        assertEq(k, v - 1);
+        assertEq(o, arr);
+        return v;
+    }), arr);
+    assertEq(sum, 15);
+    assertEq(count, 5);
+
+    // Test that changing elements that have been visited does not affect the result.
+    var changeArr = new constructor([1,2,3,4,5]);
+    assertDeepEq(arr.map((v,k) => {
+        changeArr[k] = v + 1;
+        return v;
+    }), new constructor([1,2,3,4,5]));
+
+    // Tests for `thisArg` argument.
+    function assertThisArg(thisArg, thisValue) {
+        // In sloppy mode, `this` could be global object or a wrapper of `thisArg`.
+        assertDeepEq(arr.map(function(v) {
+            assertDeepEq(this, thisValue);
+            return v;
+        }, thisArg), arr);
+
+        // In strict mode, `this` strictly equals `thisArg`.
+        assertDeepEq(arr.map(function(v) {
+            "use strict";
+            assertDeepEq(this, thisArg);
+            return v;
+        }, thisArg), arr);
+
+        // Passing `thisArg` has no effect if callback is an arrow function.
+        var self = this;
+        assertDeepEq(arr.map((v) => {
+            assertEq(this, self);
+            return v;
+        }, thisArg), arr);
+    }
+    assertThisArg([1, 2, 3], [1, 2, 3]);
+    assertThisArg(Object, Object);
+    assertThisArg(1, Object(1));
+    assertThisArg("1", Object("1"));
+    assertThisArg(false, Object(false));
+    assertThisArg(undefined, this);
+    assertThisArg(null, this);
+
+    // Throw an exception in the callback.
+    var sum = 0;
+    var count = 0;
+    var thrown = false;
+    try {
+        arr.map((v, k, o) => {
+            count++;
+            sum += v;
+            assertEq(k, v - 1);
+            assertEq(o, arr);
+            if (v === 3) {
+                throw "map";
+            }
+            return v;
+        })
+    } catch(e) {
+        assertEq(e, "map");
+        thrown = true;
+    }
+    assertEq(thrown, true);
+    assertEq(sum, 6);
+    assertEq(count, 3);
+
+    // There is no callback or callback is not a function.
+    assertThrowsInstanceOf(() => {
+        arr.map();
+    }, TypeError);
+    var invalidCallbacks = [undefined, null, 1, false, "", Symbol(), [], {}, /./];
+    invalidCallbacks.forEach(callback => {
+        assertThrowsInstanceOf(() => {
+            arr.map(callback);
+        }, TypeError);
+    })
+
+    // Callback is a generator.
+    arr.map(function*(){
+        throw "This line will not be executed";
+    });
+
+    // Called from other globals.
+    if (typeof newGlobal === "function") {
+        var map = newGlobal()[constructor.name].prototype.map;
+        var sum = 0;
+        assertDeepEq(map.call(new constructor([1, 2, 3]), v => sum += v), new constructor([1,3,6]));
+        assertEq(sum, 6);
+    }
+
+    // Throws if `this` isn't a TypedArray.
+    var invalidReceivers = [undefined, null, 1, false, "", Symbol(), [], {}, /./,
+                            new Proxy(new constructor(), {})];
+    invalidReceivers.forEach(invalidReceiver => {
+        assertThrowsInstanceOf(() => {
+            constructor.prototype.filter.call(invalidReceiver, () => true);
+        }, TypeError, "Assert that map fails if this value is not a TypedArray");
+    });
+
+    // Test that the length getter is never called.
+    assertDeepEq(Object.defineProperty(new constructor([1, 2, 3]), "length", {
+        get() {
+            throw new Error("length accessor called");
+        }
+    }).map((b) => b), new constructor([1,2,3]));
+}
+
+// Test For TypedArray#filter.
+for (var constructor of constructors) {
+    assertEq(constructor.prototype.filter.length, 1)
+
+    // Basic tests.
+    assertDeepEq(new constructor([1,2,3]).filter(x => x == x), new constructor([1,2,3]));
+    assertDeepEq(new constructor([1,2,3,4]).filter(x => x % 2 == 0), new constructor([2,4]));
+    assertDeepEq(new constructor([1,2,3,4,5]).filter(x => x < 4), new constructor([1,2,3]));
+    assertDeepEq(new constructor().filter(x => x * 2 == 4), new constructor());
+
+    var arr = new constructor([1,2,3,4,5]);
+    var sum = 0;
+    var count = 0;
+    assertDeepEq(arr.filter((v, k, o) => {
+        count++;
+        sum += v;
+        assertEq(k, v - 1);
+        assertEq(o, arr);
+        return (v < 4);
+    }), new constructor([1,2,3]));
+    assertEq(sum, 15);
+    assertEq(count, 5);
+
+    // Test that changing elements that have been visited does not affect the result.
+    var changeArr = new constructor([1,2,3,4,5]);
+    assertDeepEq(arr.filter((v,k) => {
+        changeArr[k] = v + 1;
+        return true;
+    }), new constructor([1,2,3,4,5]));
+
+    // Tests for `thisArg` argument.
+    function assertThisArg(thisArg, thisValue) {
+        // In sloppy mode, `this` could be global object or a wrapper of `thisArg`.
+        assertDeepEq(arr.filter(function(v) {
+            assertDeepEq(this, thisValue);
+            return v;
+        }, thisArg), arr);
+
+        // In strict mode, `this` strictly equals `thisArg`.
+        assertDeepEq(arr.filter(function(v) {
+            "use strict";
+            assertDeepEq(this, thisArg);
+            return v;
+        }, thisArg), arr);
+
+        // Passing `thisArg` has no effect if callback is an arrow function.
+        var self = this;
+        assertDeepEq(arr.filter((v) => {
+            assertEq(this, self);
+            return v;
+        }, thisArg), arr);
+    }
+    assertThisArg([1, 2, 3], [1, 2, 3]);
+    assertThisArg(Object, Object);
+    assertThisArg(1, Object(1));
+    assertThisArg("1", Object("1"));
+    assertThisArg(false, Object(false));
+    assertThisArg(undefined, this);
+    assertThisArg(null, this);
+
+    // Throw an exception in the callback.
+    var sum = 0;
+    var count = 0;
+    var thrown = false;
+    try {
+        arr.filter((v, k, o) => {
+            count++;
+            sum += v;
+            assertEq(k, v - 1);
+            assertEq(o, arr);
+            if (v === 3) {
+                throw "filter";
+            }
+            return v;
+        })
+    } catch(e) {
+        assertEq(e, "filter");
+        thrown = true;
+    }
+    assertEq(thrown, true);
+    assertEq(sum, 6);
+    assertEq(count, 3);
+
+    // There is no callback or callback is not a function.
+    assertThrowsInstanceOf(() => {
+        arr.filter();
+    }, TypeError);
+    var invalidCallbacks = [undefined, null, 1, false, "", Symbol(), [], {}, /./];
+    invalidCallbacks.forEach(callback => {
+        assertThrowsInstanceOf(() => {
+            arr.filter(callback);
+        }, TypeError);
+    })
+
+    // Callback is a generator.
+    arr.filter(function*(){
+        throw "This line will not be executed";
+    });
+
+    // Called from other globals.
+    if (typeof newGlobal === "function") {
+        var filter = newGlobal()[constructor.name].prototype.filter;
+        var sum = 0;
+        assertDeepEq(filter.call(new constructor([1, 2, 3]), v => {sum += v; return true}),
+        new constructor([1,2,3]));
+        assertEq(sum, 6);
+    }
+
+    // Throws if `this` isn't a TypedArray.
+    var invalidReceivers = [undefined, null, 1, false, "", Symbol(), [], {}, /./,
+                            new Proxy(new constructor(), {})];
+    invalidReceivers.forEach(invalidReceiver => {
+        assertThrowsInstanceOf(() => {
+            constructor.prototype.filter.call(invalidReceiver, () => true);
+        }, TypeError, "Assert that filter fails if this value is not a TypedArray");
+    });
+
+    // Test that the length getter is never called.
+    assertDeepEq(Object.defineProperty(new constructor([1, 2, 3]), "length", {
+        get() {
+            throw new Error("length accessor called");
+        }
+    }).filter((b) => true), new constructor([1,2,3]));
+}
+
+// Test that changing Array.prototype[Symbol.iterator] does not affect the
+// behaviour of filter. See https://bugzilla.mozilla.org/show_bug.cgi?id=1121936#c18
+// for more details.
+
+// Object conforming to the "iterator" protocol
+var obj = {
+    v: 0,
+    next: function() {
+        if (this.v == 5) {
+	       return {done : true, value : this.v };
+        } else {
+	       this.v++;
+	       return { done : false, value : this.v };
+        }
+    }
+};
+
+// save
+var old = Array.prototype[Symbol.iterator];
+
+Array.prototype[Symbol.iterator] = obj;
+assertDeepEq(new Uint16Array([1,2,3]).filter(v => true), new Uint16Array([1,2,3]));
+
+// restore
+Array.prototype[Symbol.iterator] = old;
+
+// Test that defining accessors on Array.prototype doesn't affect the behaviour
+// of filter. See https://bugzilla.mozilla.org/show_bug.cgi?id=1121936#c18
+// for more details.
+Object.defineProperty(Array.prototype, 0, {configurable: true, get: function() { return 1; }, set: function() { this.b = 1; }});
+assertDeepEq(new Uint16Array([1,2,3]).filter(v => true), new Uint16Array([1,2,3]));
+delete Array.prototype[0];
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -797,22 +797,24 @@ TypedArrayObject::subarray(JSContext *cx
 
 /* static */ const JSFunctionSpec
 TypedArrayObject::protoFunctions[] = {
     JS_FN("subarray", TypedArrayObject::subarray, 2, 0),
     JS_FN("set", TypedArrayObject::set, 2, 0),
     JS_FN("copyWithin", TypedArrayObject::copyWithin, 2, 0),
     JS_SELF_HOSTED_FN("every", "TypedArrayEvery", 2, 0),
     JS_SELF_HOSTED_FN("fill", "TypedArrayFill", 3, 0),
+    JS_SELF_HOSTED_FN("filter", "TypedArrayFilter", 2, 0),
     JS_SELF_HOSTED_FN("find", "TypedArrayFind", 2, 0),
     JS_SELF_HOSTED_FN("findIndex", "TypedArrayFindIndex", 2, 0),
     JS_SELF_HOSTED_FN("forEach", "TypedArrayForEach", 2, 0),
     JS_SELF_HOSTED_FN("indexOf", "TypedArrayIndexOf", 2, 0),
     JS_SELF_HOSTED_FN("join", "TypedArrayJoin", 1, 0),
     JS_SELF_HOSTED_FN("lastIndexOf", "TypedArrayLastIndexOf", 2, 0),
+    JS_SELF_HOSTED_FN("map", "TypedArrayMap", 2, 0),
     JS_SELF_HOSTED_FN("reduce", "TypedArrayReduce", 1, 0),
     JS_SELF_HOSTED_FN("reduceRight", "TypedArrayReduceRight", 1, 0),
     JS_SELF_HOSTED_FN("reverse", "TypedArrayReverse", 0, 0),
     JS_SELF_HOSTED_FN("slice", "TypedArraySlice", 2, 0),
     JS_SELF_HOSTED_FN("some", "TypedArraySome", 2, 0),
     JS_SELF_HOSTED_FN("entries", "TypedArrayEntries", 0, 0),
     JS_SELF_HOSTED_FN("keys", "TypedArrayKeys", 0, 0),
     // Both of these are actually defined to the same object in FinishTypedArrayInit.
--- a/js/xpconnect/tests/chrome/test_xrayToJS.xul
+++ b/js/xpconnect/tests/chrome/test_xrayToJS.xul
@@ -172,17 +172,18 @@ https://bugzilla.mozilla.org/show_bug.cg
     gPrototypeProperties['Array'].push('includes');
   }
   for (var c of typedArrayClasses) {
     gPrototypeProperties[c] = ["constructor", "BYTES_PER_ELEMENT"];
   }
   gPrototypeProperties['TypedArray'] =
     ["length", "buffer", "byteLength", "byteOffset", Symbol.iterator, "subarray",
      "set", "copyWithin", "find", "findIndex", "forEach","indexOf", "lastIndexOf", "reverse",
-     "join", "every", "some", "reduce", "reduceRight", "entries", "keys", "values", "slice"];
+     "join", "every", "some", "reduce", "reduceRight", "entries", "keys", "values", "slice",
+     "map", "filter"];
   if (isNightlyBuild) {
     gPrototypeProperties['TypedArray'].push('includes');
   }
   for (var c of errorObjectClasses) {
       gPrototypeProperties[c] = ["constructor", "name",
                                  // We don't actually resolve these empty data properties
                                  // onto the Xray prototypes, but we list them here to make
                                  // the test happy.