Bug 1121937 - Implement %TypedArray%.prototype.sort; r=jorendorff

This commit is contained in:
Morgan Phillips 2016-01-06 20:55:23 -06:00
parent 2e53a982ee
commit 002eb344dd
8 changed files with 486 additions and 0 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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.