Bug 715181 - Self-host Array.sort; r=till
authorMorgan Phillips <winter2718@gmail.com>
Sun, 24 Jan 2016 19:32:22 -0600
changeset 281471 1c4b0a89fd5b
parent 281470 4bd447a3d7da
child 281472 a86aa13dc19c
push id29940
push usercbook@mozilla.com
push date2016-01-25 10:50 +0000
treeherdermozilla-central@67c66c2878ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstill
bugs715181
milestone46.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 715181 - Self-host Array.sort; r=till
js/src/builtin/Array.js
js/src/builtin/Sorting.js
js/src/builtin/TypedArray.js
js/src/jit-test/tests/self-hosting/invoke-self-hosted-with-primitive-this.js
js/src/jsarray.cpp
js/src/moz.build
js/src/tests/ecma_6/Array/sort_basics.js
js/src/tests/ecma_6/Array/sort_small.js
js/src/tests/ecma_6/TypedArray/sort_basics.js
js/src/tests/ecma_6/TypedArray/sort_small.js
js/src/tests/shell.js
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -190,16 +190,46 @@ function ArrayStaticSome(list, callbackf
     if (arguments.length < 2)
         ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.some');
     if (!IsCallable(callbackfn))
         ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(1, callbackfn));
     var T = arguments.length > 2 ? arguments[2] : void 0;
     return callFunction(ArraySome, list, callbackfn, T);
 }
 
+/* ES6 draft 2016-1-15 22.1.3.25 Array.prototype.sort (comparefn) */
+function ArraySort(comparefn) {
+    /* Step 1. */
+    var O = ToObject(this);
+
+    /* Step 2. */
+    var len = TO_UINT32(O.length);
+
+    /* 22.1.3.25.1 Runtime Semantics: SortCompare( x, y ) */
+    var wrappedCompareFn = comparefn;
+    comparefn = function(x, y) {
+        /* Steps 1-3. */
+        if (x === undefined) {
+            if (y === undefined)
+                return 0;
+           return 1;
+        }
+        if (y === undefined)
+            return -1;
+
+        /* Step 4.a. */
+        var v = ToNumber(wrappedCompareFn(x, y));
+
+        /* Step 4.b-c. */
+        return v !== v ? 0 : v;
+    }
+
+    return MergeSort(O, len, comparefn);
+}
+
 /* ES5 15.4.4.18. */
 function ArrayForEach(callbackfn/*, thisArg*/) {
     /* Step 1. */
     var O = ToObject(this);
 
     /* Steps 2-3. */
     var len = TO_UINT32(O.length);
 
new file mode 100644
--- /dev/null
+++ b/js/src/builtin/Sorting.js
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// We use varying sorts across the self-hosted codebase. All sorts are
+// consolidated here to avoid confusion and re-implementation of existing
+// algorithms.
+
+// 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;
+}
+
+// A helper function for MergeSort.
+function Merge(array, start, mid, end, lBuffer, rBuffer, comparefn) {
+    var i, j, k;
+
+    var sizeLeft = mid - start + 1;
+    var sizeRight =  end - mid;
+
+    // Copy our virtual arrays into separate buffers.
+    for (i = 0; i < sizeLeft; i++)
+        lBuffer[i] = array[start + i];
+
+    for (j = 0; j < sizeRight; j++)
+        rBuffer[j] = array[mid + 1 + j];
+
+    i = 0;
+    j = 0;
+    k = start;
+    while (i < sizeLeft && j < sizeRight) {
+        if (comparefn(lBuffer[i], rBuffer[j]) <= 0) {
+            array[k] = lBuffer[i];
+            i++;
+        } else {
+            array[k] = rBuffer[j];
+            j++;
+        }
+        k++;
+    }
+
+    // Empty out any remaining elements in the buffer.
+    while (i < sizeLeft) {
+        array[k] = lBuffer[i];
+        i++;
+        k++;
+    }
+
+    while (j < sizeRight) {
+        array[k] = rBuffer[j];
+        j++;
+        k++;
+    }
+}
+
+// Iterative, bottom up, mergesort.
+function MergeSort(array, len, comparefn) {
+    // Insertion sort for small arrays, where "small" is defined by performance
+    // testing.
+    if (len < 24) {
+       InsertionSort(array, 0, len - 1, comparefn);
+       return array;
+    }
+
+    // We do all of our allocating up front
+    var lBuffer = new List();
+    var rBuffer = new List();
+    var mid, end, endOne, endTwo;
+
+    for (var windowSize = 1; windowSize < len; windowSize = 2*windowSize) {
+        for (var start = 0; start < len - 1; start += 2*windowSize) {
+            assert(windowSize < len, "The window size is larger than the array length!");
+            // The midpoint between the two subarrays.
+            mid = start + windowSize - 1;
+            // To keep from going over the edge.
+            end = start + 2 * windowSize - 1;
+            end = end < len - 1 ? end : len - 1;
+            // Skip lopsided runs to avoid doing useless work
+            if (mid > end)
+                continue;
+            Merge(array, start, mid, end, lBuffer, rBuffer, comparefn);
+        }
+    }
+    return array;
+}
+
+// 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 medianIndex = (from + to) >> 1;
+
+    var i = from + 1;
+    var j = to;
+
+    SwapArrayElements(array, medianIndex, 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 pivotIndex = i;
+
+    // Hoare partition method.
+    for(;;) {
+        do i++; while (comparefn(array[i], array[pivotIndex]) < 0);
+        do j--; while (comparefn(array[j], array[pivotIndex]) > 0);
+        if (i > j)
+            break;
+        SwapArrayElements(array, i, j);
+    }
+
+    SwapArrayElements(array, pivotIndex, 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 pivotIndex, i, j, leftLen, rightLen;
+
+    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 {
+            pivotIndex = 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).
+            leftLen = (pivotIndex - 1) - start;
+            rightLen = end - (pivotIndex + 1);
+
+            if (rightLen > leftLen) {
+                stack[top++] = start;
+                stack[top++] = pivotIndex - 1;
+                start = pivotIndex + 1;
+            } else {
+                stack[top++] = pivotIndex + 1;
+                stack[top++] = end;
+                end = pivotIndex - 1;
+            }
+
+        }
+    }
+    return array;
+}
--- a/js/src/builtin/TypedArray.js
+++ b/js/src/builtin/TypedArray.js
@@ -934,122 +934,16 @@ function TypedArraySome(callbackfn, this
         if (testResult)
             return true;
     }
 
     // Step 10.
     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.");
 
--- a/js/src/jit-test/tests/self-hosting/invoke-self-hosted-with-primitive-this.js
+++ b/js/src/jit-test/tests/self-hosting/invoke-self-hosted-with-primitive-this.js
@@ -1,7 +1,7 @@
 try {
 	[0,0].sort(Array.some)
 	"".replace(RegExp(), Array.reduce)
 } catch (error) {
-	if (!(error instanceof TypeError && error.message == "0 is not a function"))
+	if (!(error instanceof TypeError && /^\w is not a function$/.test(error.message)))
 		throw error;
-}
\ No newline at end of file
+}
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -1812,16 +1812,53 @@ js::array_sort(JSContext* cx, unsigned a
     } else {
         fval.setNull();
     }
 
     RootedObject obj(cx, ToObject(cx, args.thisv()));
     if (!obj)
         return false;
 
+    ComparatorMatchResult comp = MatchNumericComparator(cx, fval);
+    if (comp == Match_Failure)
+        return false;
+
+    if (!fval.isNull() && comp == Match_None) {
+        /*
+         * Non-optimized user supplied comparators perform much better when
+         * called from within a self-hosted sorting function.
+         */
+        RootedAtom selfHostedSortAtom(cx, Atomize(cx, "ArraySort", 9));
+        RootedPropertyName selfHostedSortName(cx, selfHostedSortAtom->asPropertyName());
+        RootedValue selfHostedSortValue(cx);
+
+        if (!GlobalObject::getIntrinsicValue(cx, cx->global(), selfHostedSortName,
+            &selfHostedSortValue)) {
+            return false;
+        }
+
+        MOZ_ASSERT(selfHostedSortValue.isObject());
+        MOZ_ASSERT(selfHostedSortValue.toObject().is<JSFunction>());
+
+        InvokeArgs iargs(cx);
+
+        if (!iargs.init(1))
+            return false;
+
+        iargs.setCallee(selfHostedSortValue);
+        iargs.setThis(args.thisv());
+        iargs[0].set(fval);
+
+        if (!Invoke(cx, iargs))
+            return false;
+
+        args.rval().set(iargs.rval());
+        return true;
+    }
+
     uint32_t len;
     if (!GetLengthProperty(cx, obj, &len))
         return false;
     if (len < 2) {
         /* [] and [a] remain unchanged when sorted. */
         args.rval().setObject(*obj);
         return true;
     }
@@ -1912,37 +1949,23 @@ js::array_sort(JSContext* cx, unsigned a
                                SortComparatorLexicographicInt32())) {
                     return false;
                 }
             } else {
                 if (!SortLexicographically(cx, &vec, n))
                     return false;
             }
         } else {
-            ComparatorMatchResult comp = MatchNumericComparator(cx, fval);
-            if (comp == Match_Failure)
-                return false;
-
-            if (comp != Match_None) {
-                if (allInts) {
-                    JS_ALWAYS_TRUE(vec.resize(n * 2));
-                    if (!MergeSort(vec.begin(), n, vec.begin() + n, SortComparatorInt32s[comp]))
-                        return false;
-                } else {
-                    if (!SortNumerically(cx, &vec, n, comp))
-                        return false;
-                }
+            if (allInts) {
+                JS_ALWAYS_TRUE(vec.resize(n * 2));
+                if (!MergeSort(vec.begin(), n, vec.begin() + n, SortComparatorInt32s[comp]))
+                    return false;
             } else {
-                FastInvokeGuard fig(cx, fval);
-                JS_ALWAYS_TRUE(vec.resize(n * 2));
-                if (!MergeSort(vec.begin(), n, vec.begin() + n,
-                               SortComparatorFunction(cx, fval, fig)))
-                {
+                if (!SortNumerically(cx, &vec, n, comp))
                     return false;
-                }
             }
         }
 
         if (!InitArrayElements(cx, obj, 0, uint32_t(n), vec.begin(), ShouldUpdateTypes::DontUpdate))
             return false;
     }
 
     /* Set undefs that sorted after the rest of elements. */
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -706,16 +706,17 @@ selfhosted.inputs = [
     'builtin/Map.js',
     'builtin/Module.js',
     'builtin/Number.js',
     'builtin/Object.js',
     'builtin/Reflect.js',
     'builtin/RegExp.js',
     'builtin/String.js',
     'builtin/Set.js',
+    'builtin/Sorting.js',
     'builtin/TypedArray.js',
     'builtin/TypedObject.js',
     'builtin/WeakSet.js'
 ]
 
 if CONFIG['JS_HAS_CTYPES']:
     if CONFIG['MOZ_NATIVE_FFI']:
         CXXFLAGS += CONFIG['MOZ_FFI_CFLAGS']
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/sort_basics.js
@@ -0,0 +1,34 @@
+// 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;
+
+// Create an array filled with random values, 'size' is the desired length of
+// the array and 'seed' is an initial value supplied to a pseudo-random number
+// generator.
+function genRandomArray(size, seed) {
+    return Array.from(XorShiftGenerator(seed, size));
+}
+
+function SortTest(size, seed) {
+    let arrOne    = genRandomArray(size, seed);
+    let arrTwo    = Array.from(arrOne);
+    let arrThree  = Array.from(arrOne);
+
+    // Test numeric comparators against typed array sort.
+    assertDeepEq(Array.from((Int32Array.from(arrOne)).sort()),
+                 arrTwo.sort((x, y) => (x - y)),
+                 `The arr is not properly sorted! seed: ${SEED}`);
+
+    // Use multiplication to kill comparator optimization and trigger
+    // self-hosted sorting.
+    assertDeepEq(Array.from((Int32Array.from(arrOne)).sort()),
+                 arrThree.sort((x, y) => (1*x - 1*y)),
+                 `The arr is not properly sorted! seed: ${SEED}`);
+}
+
+SortTest(2048, SEED);
+SortTest(16, SEED);
+SortTest(0, SEED);
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/sort_small.js
@@ -0,0 +1,33 @@
+// Sort every possible permutation of some arrays.
+function sortAllPermutations(data, comparefn) {
+    for (let permutation of Permutations(Array.from(data))) {
+        let sorted = (Array.from(permutation)).sort(comparefn);
+        for (let i in sorted) {
+            assertEq(sorted[i], data[i],
+            [`[${permutation}].sort(${comparefn})`,
+            `returned ${sorted}, expected ${data}`].join(' '));
+        }
+    }
+}
+
+let lex  = [2112, "bob", "is", "my", "name"];
+let nans = [1/undefined, NaN, Number.NaN]
+
+let num1  = [-11, 0, 0, 100, 101];
+let num2  = [-11, 100, 201234.23, undefined, undefined];
+
+sortAllPermutations(lex);
+sortAllPermutations(nans);
+
+sortAllPermutations(nans, (x, y) => x - y);
+// Multiplication kills comparator optimization.
+sortAllPermutations(nans, (x, y) => (1*x - 1*y));
+
+sortAllPermutations(num1, (x, y) => x - y);
+sortAllPermutations(num1, (x, y) => (1*x - 1*y));
+
+sortAllPermutations(num2, (x, y) => x - y);
+sortAllPermutations(num2, (x, y) => (1*x - 1*y));
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/tests/ecma_6/TypedArray/sort_basics.js
+++ b/js/src/tests/ecma_6/TypedArray/sort_basics.js
@@ -1,37 +1,23 @@
 // 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))
+    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.
--- a/js/src/tests/ecma_6/TypedArray/sort_small.js
+++ b/js/src/tests/ecma_6/TypedArray/sort_small.js
@@ -1,27 +1,8 @@
-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]
 
@@ -30,17 +11,17 @@ let u8   = [255]
 // 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))
+    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);
--- a/js/src/tests/shell.js
+++ b/js/src/tests/shell.js
@@ -368,16 +368,50 @@ function enterFunc (funcName)
 {
   if (!funcName.match(/\(\)$/))
     funcName += "()";
 
   callStack.push(funcName);
 }
 
 /*
+ * 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;
+    }
+}
+
+/*
+ * Yield every permutation of the elements in some iterable.
+ */
+function *Permutations(items) {
+    if (items.length == 0) {
+        yield [];
+    } else {
+        let swap;
+        for (let i = 0; i < items.length; i++) {
+            swap = items[0];
+            items[0] = items[i];
+            items[i] = swap;
+            for (let e of Permutations(items.slice(1, items.length)))
+                yield [items[0]].concat(e);
+        }
+    }
+}
+
+/*
  * Pops the top funcName off the call stack.  funcName is optional, and can be
  * used to check push-pop balance.
  */
 function exitFunc (funcName)
 {
   var lastFunc = callStack.pop();
 
   if (funcName)