diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index b6efc629c64..bda796be0de 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -903,6 +903,182 @@ function TypedArraySome(callbackfn, thisArg = undefined) { return false; } +// For sorting small arrays +function InsertionSort(array, from, to, comparefn) { + var item, swap; + for (var i = from + 1; i <= to; i++) { + item = array[i]; + for (var j = i - 1; j >= from; j--) { + swap = array[j]; + if (comparefn(swap, item) <= 0) + break + array[j + 1] = swap; + } + array[j + 1] = item; + } +} + +function SwapArrayElements(array, i, j) { + var swap = array[i]; + array[i] = array[j]; + array[j] = swap; +} + +// Rearranges the elements in array[from:to + 1] and returns an index j such that: +// - from < j < to +// - each element in array[from:j] is less than or equal to array[j] +// - each element in array[j + 1:to + 1] greater than or equal to array[j]. +function Partition(array, from, to, comparefn) { + assert(to - from >= 3, + "Partition will not work with less than three elements"); + + var median_i = (from + to) >> 1; + + var i = from + 1; + var j = to; + + SwapArrayElements(array, median_i, i); + + // Median of three pivot selection + if (comparefn(array[from], array[to]) > 0) + SwapArrayElements(array, from, to); + + if (comparefn(array[i], array[to]) > 0) + SwapArrayElements(array, i, to); + + if (comparefn(array[from], array[i]) > 0) + SwapArrayElements(array, from, i); + + var pivot_i = i; + + // Hoare partition method + for(;;) { + do i++; while (comparefn(array[i], array[pivot_i]) < 0); + do j--; while (comparefn(array[j], array[pivot_i]) > 0); + if (i > j) + break; + SwapArrayElements(array, i, j); + } + + SwapArrayElements(array, pivot_i, j); + return j; +} + +// In-place QuickSort +function QuickSort(array, len, comparefn) { + // Managing the stack ourselves seems to provide a small performance boost + var stack = new List(); + var top = 0; + + var start = 0; + var end = len - 1; + + var pivot_i, i, j, l_len, r_len; + + for (;;) { + // Insertion sort for the first N elements where N is some value + // determined by performance testing. + if (end - start <= 23) { + InsertionSort(array, start, end, comparefn); + if (top < 1) + break; + end = stack[--top]; + start = stack[--top]; + } else { + pivot_i = Partition(array, start, end, comparefn); + + // Calculate the left and right sub-array lengths and save + // stack space by directly modifying start/end so that + // we sort the longest of the two during the next iteration. + // This reduces the maximum stack size to log2(len) + l_len = (pivot_i - 1) - start; + r_len = end - (pivot_i + 1); + + if (r_len > l_len) { + stack[top++] = start; + stack[top++] = pivot_i - 1; + start = pivot_i + 1; + } else { + stack[top++] = pivot_i + 1; + stack[top++] = end; + end = pivot_i - 1; + } + + } + } + return array; +} + +// ES6 draft 20151210 22.2.3.26 +// Cases are ordered according to likelihood of occurrence +// as opposed to the ordering in the spec. +function TypedArrayCompare(x, y) { + // Step 1. + assert(typeof x === "number" && typeof y === "number", + "x and y are not numbers."); + + // Steps 6 - 7. + var diff = x - y; + if (diff) + return diff; + + // Steps 8 - 10. + if (x === 0 && y === 0) + return (1/x > 0 ? 1 : 0) - (1/y > 0 ? 1 : 0); + + // Step 2. Implemented in TypedArraySort + + // Step 3. + if (Number_isNaN(x) && Number_isNaN(y)) + return 0; + + // Steps 4 - 5. + if (Number_isNaN(x) || Number_isNaN(y)) + return Number_isNaN(x) ? 1 : -1; + +} + +// ES6 draft 20151210 22.2.3.26 %TypedArray%.prototype.sort ( comparefn ). +function TypedArraySort(comparefn) { + // This function is not generic. + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction(CallTypedArrayMethodIfWrapped, this, comparefn, + "TypedArraySort"); + } + + // Step 1. + var obj = this; + + // Step 2. + var buffer = TypedArrayBuffer(obj); + if (IsDetachedBuffer(buffer)) + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + + // Step 3. + var len = TypedArrayLength(obj); + + if (comparefn === undefined) { + comparefn = TypedArrayCompare; + } else { + // To satisfy step 2 from TypedArray SortCompare described in 22.2.3.26 + // the user supplied comparefn is wrapped. + var wrappedCompareFn = comparefn; + comparefn = function(x, y) { + // Step a. + var v = wrappedCompareFn(x, y); + // Step b. + if (IsDetachedBuffer(buffer)) + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + // Step c. is redundant, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1121937#c36 + // Step d. + return v; + } + } + + return QuickSort(obj, len, comparefn); +} + // ES6 draft 20150304 %TypedArray%.prototype.subarray function TypedArraySubarray(begin, end) { // Step 1. diff --git a/js/src/tests/ecma_6/TypedArray/sort_basics.js b/js/src/tests/ecma_6/TypedArray/sort_basics.js new file mode 100644 index 00000000000..76cf0c96546 --- /dev/null +++ b/js/src/tests/ecma_6/TypedArray/sort_basics.js @@ -0,0 +1,95 @@ +// Note: failed runs should include their "SEED" value in error messages, +// setting "const SEED" to that value will recreate the data from any such run. +const SEED = (Math.random() * 10) + 1; + +// An xorshift pseudo-random number generator see: +// https://en.wikipedia.org/wiki/Xorshift#xorshift.2A +// This generator will always produce a value, n, where +// 0 <= n <= 255 +function *xorShiftGenerator(seed, size) { + let x = seed; + for (let i = 0; i < size; i++) { + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + yield x % 256; + } +} + +// Fill up an array buffer with random values and return it in raw form. +// 'size' is the desired length of the view we will place atop the buffer, +// 'width' is the bit-width of the view we plan on placing atop the buffer, +// and 'seed' is an initial value supplied to a pseudo-random number generator. +function genRandomArrayBuffer(size, width, seed) { + let buf = new ArrayBuffer((width / 8) * size); + let arr = new Uint8Array(buf); + let len = 0; + // We generate a random number, n, where 0 <= n <= 255 for every space + // available in our buffer. + for (let n of xorShiftGenerator(seed, buf.byteLength)) + arr[len++] = n; + return buf; +} + +// Because we can generate any possible combination of bits, some floating point +// entries will take on -Infinity, Infinity, and NaN values. This function ensures +// that a is <= b, where, like the default comparator, -Infinity < Infinity and +// every non-NaN < NaN. +function lte(a, b) { + if (isNaN(b)) + return true; + return a <= b; +} + +// A a >= b counterpart to the helper function above. +function gte(a, b) { + return lte(b, a); +} + +// A custom comparator. +function cmp(a, b) { + return lte(a, b) ? gte(a, b) ? 0 : -1 : 1; +} + +function SortTest(dataType, dataSource) { + let typedArray = new dataType(dataSource); + let originalValues = Array.from(typedArray); + + // Test the default comparator + typedArray.sort(); + + // Test against regular array sort + assertDeepEq(Array.from(typedArray), Array.from(originalValues).sort(cmp), + `The array is not properly sorted! seed: ${SEED}`); + + // Another sanity check + for (let i=0; i < typedArray.length - 1; i++) + assertEq(lte(typedArray[i], typedArray[i + 1]), true, + `The array is not properly sorted! ${typedArray[i]} > ${typedArray[i + 1]}, seed: ${SEED}`) + + // Test custom comparators + typedArray.sort((x, y) => cmp(y, x)); + + // The array should be in reverse order + for (let i=typedArray.length - 2; i >= 0; i--) + assertEq(gte(typedArray[i], typedArray[i + 1]), true, + `The array is not properly sorted! ${typedArray[i]} < ${typedArray[i + 1]}, seed: ${SEED}`) +} + +function RunTests(arrayLength) { + SortTest(Int32Array, genRandomArrayBuffer(arrayLength, 32, SEED)); + SortTest(Uint32Array, genRandomArrayBuffer(arrayLength, 32, SEED)); + SortTest(Int16Array, genRandomArrayBuffer(arrayLength, 16, SEED)); + SortTest(Uint16Array, genRandomArrayBuffer(arrayLength, 16, SEED)); + SortTest(Int8Array, genRandomArrayBuffer(arrayLength, 8, SEED)); + SortTest(Uint8Array, genRandomArrayBuffer(arrayLength, 8, SEED)); + SortTest(Float32Array, genRandomArrayBuffer(arrayLength, 32, SEED)); + SortTest(Float64Array, genRandomArrayBuffer(arrayLength, 64, SEED)); +} + +RunTests(256); +RunTests(16); +RunTests(0); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/TypedArray/sort_comparators.js b/js/src/tests/ecma_6/TypedArray/sort_comparators.js new file mode 100644 index 00000000000..ca190948fce --- /dev/null +++ b/js/src/tests/ecma_6/TypedArray/sort_comparators.js @@ -0,0 +1,32 @@ +// Ensure that sorts finish even if a comparator adds items +// Note: the array is not expected to be properly sorted. +let outsideArray = new Int32Array([1, 99, 2]); +function addingComparator(x, y) { + if (x == 99 || y == 99) { + outsideArray[0] = 101; + outsideArray[outsideArray.length - 1] = 102; + } + return x - y; +} +outsideArray.sort(addingComparator); +assertEq(outsideArray.every(x => [1, 2, 99, 101, 102].includes(x)), true); + +// Ensure that sorts finish even if a comparator calls sort again +// Note: the array is not expected to be properly sorted. +outsideArray = new Int32Array([1, 99, 2]); +function recursiveComparator(x, y) { + outsideArray.sort(); + return x - y; +} +outsideArray.sort(recursiveComparator); +assertEq(outsideArray.every(x => outsideArray.includes(x)), true) + +// Ensure that NaN's returned from custom comparators behave as / are converted +// to +0s. +let nanComparatorData = [2112, 42, 1111, 34]; +let nanComparatorArray = new Int32Array(nanComparatorData); +nanComparatorArray.sort((x, y) => NaN); +assertEq(nanComparatorData.every(x => nanComparatorArray.includes(x)), true); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/TypedArray/sort_errors.js b/js/src/tests/ecma_6/TypedArray/sort_errors.js new file mode 100644 index 00000000000..b5330d7319f --- /dev/null +++ b/js/src/tests/ecma_6/TypedArray/sort_errors.js @@ -0,0 +1,47 @@ +// Ensure that TypedArrays throw when attempting to sort a detached ArrayBuffer +assertThrowsInstanceOf(() => { + let buffer = new ArrayBuffer(32); + let array = new Int32Array(buffer); + neuter(buffer, "change-data"); + array.sort(); +}, TypeError); + +// Ensure that TypedArray.prototype.sort will not sort non-TypedArrays +assertThrowsInstanceOf(() => { + let array = [4, 3, 2, 1]; + Int32Array.prototype.sort.call(array); +}, TypeError); + +assertThrowsInstanceOf(() => { + Int32Array.prototype.sort.call({a: 1, b: 2}); +}, TypeError); + +assertThrowsInstanceOf(() => { + Int32Array.prototype.sort.call(Int32Array.prototype); +}, TypeError); + +assertThrowsInstanceOf(() => { + let buf = new ArrayBuffer(32); + Int32Array.prototype.sort.call(buf); +}, TypeError); + +// Ensure that comparator errors are propagataed +function badComparator(x, y) { + if (x == 99 && y == 99) + throw new TypeError; + return x - y; +} + +assertThrowsInstanceOf(() => { + let array = new Uint8Array([99, 99, 99, 99]); + array.sort(badComparator); +}, TypeError); + +assertThrowsInstanceOf(() => { + let array = new Uint8Array([1, 99, 2, 99]); + array.sort(badComparator); +}, TypeError); + + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/TypedArray/sort_globals.js b/js/src/tests/ecma_6/TypedArray/sort_globals.js new file mode 100644 index 00000000000..08f9af7ab73 --- /dev/null +++ b/js/src/tests/ecma_6/TypedArray/sort_globals.js @@ -0,0 +1,9 @@ +// TypedArray.prototype.sort should work across globals +let g2 = newGlobal(); +assertDeepEq( + Int32Array.prototype.sort.call(new g2.Int32Array([3, 2, 1])), + new Int32Array([1, 2, 3]) +); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/TypedArray/sort_small.js b/js/src/tests/ecma_6/TypedArray/sort_small.js new file mode 100644 index 00000000000..386f92e1db0 --- /dev/null +++ b/js/src/tests/ecma_6/TypedArray/sort_small.js @@ -0,0 +1,54 @@ +function swapElements(arr, i, j) { + var swap = arr[i]; + arr[i] = arr[j]; + arr[j] = swap; +} + +// Yield every permutation of the elements in some iterable. +function *permutations(items) { + if (items.length == 0) { + yield []; + } else { + for (let i = 0; i < items.length; i++) { + swapElements(items, 0, i); + for (let e of permutations(items.slice(1, items.length))) + yield [items[0]].concat(e); + } + } +} + +// Pre-sorted test data, it's important that these arrays remain in ascending order. +let i32 = [-2147483648, -320000, -244000, 2147483647] +let u32 = [0, 987632, 4294967295] +let i16 = [-32768, -999, 1942, 32767] +let u16 = [0, 65535, 65535] +let i8 = [-128, 127] +let u8 = [255] + +// Test the behavior in the default comparator as described in 22.2.3.26. +// The spec boils down to, -0s come before +0s, and NaNs always come last. +// Float Arrays are used because all other types convert -0 and NaN to +0. +let f32 = [-2147483647, -2147483646.99, -0, 0, 2147483646.99, NaN] +let f64 = [-2147483646.99, -0, 0, 4147483646.99, NaN] +let nans = [1/undefined, NaN, Number.NaN] + +// Sort every possible permutation of an arrays +function sortAllPermutations(dataType, testData) { + let reference = new dataType(testData); + for (let permutation of permutations(testData)) + assertDeepEq((new dataType(permutation)).sort(), reference); +} + +sortAllPermutations(Int32Array, i32); +sortAllPermutations(Uint32Array, u32); +sortAllPermutations(Int16Array, i16); +sortAllPermutations(Uint16Array, u16); +sortAllPermutations(Int8Array, i8); +sortAllPermutations(Uint8Array, u8); +sortAllPermutations(Float32Array, f32); +sortAllPermutations(Float64Array, f64); +sortAllPermutations(Float32Array, nans); +sortAllPermutations(Float64Array, nans); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/ecma_6/TypedArray/sort_snans.js b/js/src/tests/ecma_6/TypedArray/sort_snans.js new file mode 100644 index 00000000000..d653666db4a --- /dev/null +++ b/js/src/tests/ecma_6/TypedArray/sort_snans.js @@ -0,0 +1,72 @@ +// Ensure that signaling NaN's don't cause problems while sorting + +function getNaNArray(length) { + let a = []; + for (let i = 0; i < length; i++) + a.push(NaN); + return a; +}; + +// Test every skipNth value in some range n, where start <= n <= end +// and start/stop should be 32-bit integers with bit patterns that +// form Float32 NaNs. +function testFloat32NaNRanges(start, end) { + let skipN = 10e3; + + // sample the space of possible NaNs to save time + let sampleSize = Math.floor((end - start)/ skipN); + + let NaNArray = new Float32Array(getNaNArray(sampleSize)); + let buffer = new ArrayBuffer(4 * sampleSize); + let uintView = new Uint32Array(buffer); + let floatView = new Float32Array(buffer); + + uintView[0] = start; + for (let i = 1; i < sampleSize; i++) { + uintView[i] = uintView[0] + (i * skipN); + } + + floatView.sort(); + assertDeepEq(floatView, NaNArray); +}; + +// Test every skipNth value in some range n, where start <= n <= end +// and startHi, startLow and endHi, endLow should be 32-bit integers which, +// when combined (Hi + Low), form Float64 NaNs. +function testFloat64NaNRanges(startHi, startLow, endHi, endLow) { + let skipN = 10e6; + + let sampleSizeHi = Math.floor((endHi - startHi)/skipN); + let sampleSizeLow = Math.floor((endLow - startLow)/skipN); + + let NaNArray = new Float64Array(getNaNArray(sampleSizeHi + sampleSizeLow)); + let buffer = new ArrayBuffer(8 * (sampleSizeHi + sampleSizeLow)); + let uintView = new Uint32Array(buffer); + let floatView = new Float64Array(buffer); + + // Fill in all of the low bits first. + for (let i = 0; i <= sampleSizeLow; i++) { + uintView[i * 2] = startLow + (i * skipN); + uintView[(i * 2) + 1] = startHi; + } + + // Then the high bits. + for (let i = sampleSizeLow; i <= sampleSizeLow + sampleSizeHi; i++) { + uintView[i * 2] = endLow; + uintView[(i * 2) + 1] = startHi + ((i - sampleSizeLow) * skipN); + } + + floatView.sort(); + assertDeepEq(floatView, NaNArray); +}; + +// Float32 Signaling NaN ranges +testFloat32NaNRanges(0x7F800001, 0x7FBFFFFF); +testFloat32NaNRanges(0xFF800001, 0xFFBFFFFF); + +// Float64 Signaling NaN ranges +testFloat64NaNRanges(0x7FF00000, 0x00000001, 0x7FF7FFFF, 0xFFFFFFFF); +testFloat64NaNRanges(0xFFF00000, 0x00000001, 0xFFF7FFFF, 0xFFFFFFFF); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index eee869becc1..0412cccabb6 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -865,6 +865,7 @@ TypedArrayObject::protoFunctions[] = { 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("sort", "TypedArraySort", 1, 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.