changeset 4330:cf0632d2db2c

7052202: JSR 292: Crash in sun.invoke.util.ValueConversions.fillArray Summary: Fix corner cases involving MethodHandles.permuteArguments with long or double argument lists. Reviewed-by: twisti, never
author jrose
date Tue, 14 Jun 2011 22:47:09 -0700
parents 7a341c412ea9
children a65fa0f6717e
files src/share/classes/java/lang/invoke/AdapterMethodHandle.java src/share/classes/java/lang/invoke/MethodHandleImpl.java src/share/classes/java/lang/invoke/MethodHandleNatives.java src/share/classes/java/lang/invoke/SwitchPoint.java test/java/lang/invoke/PermuteArgsTest.java
diffstat 5 files changed, 427 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/java/lang/invoke/AdapterMethodHandle.java	Tue Jun 07 14:01:12 2011 -0700
+++ b/src/share/classes/java/lang/invoke/AdapterMethodHandle.java	Tue Jun 14 22:47:09 2011 -0700
@@ -405,13 +405,13 @@
                 insertStackMove(stackMove)
                 );
     }
-    private static long makeSwapConv(int convOp, int srcArg, byte type, int destSlot) {
+    private static long makeSwapConv(int convOp, int srcArg, byte srcType, int destSlot, byte destType) {
         // more complex argument motion, requiring two slots to specify
         assert(convOp == OP_SWAP_ARGS || convOp == OP_ROT_ARGS);
         return ((long) srcArg << 32 |
                 (long) convOp << CONV_OP_SHIFT |
-                (int)  type   << CONV_SRC_TYPE_SHIFT |
-                (int)  type   << CONV_DEST_TYPE_SHIFT |
+                (int)  srcType << CONV_SRC_TYPE_SHIFT |
+                (int)  destType << CONV_DEST_TYPE_SHIFT |
                 (int)  destSlot << CONV_VMINFO_SHIFT
                 );
     }
@@ -943,24 +943,27 @@
         if (type2size(newType.parameterType(swapArg1)) !=
             type2size(newType.parameterType(swapArg2))) {
             // turn a swap into a pair of rotates:
-            // [x a b c y] => [a b c y x] => [y a b c x]
+            // [x a b c y] rot2(-1,argc=5) => [a b c y x] rot1(+1,argc=4) => target[y a b c x]
             int argc = swapArg2 - swapArg1 + 1;
             final int ROT = 1;
             ArrayList<Class<?>> rot1Params = new ArrayList<Class<?>>(target.type().parameterList());
             Collections.rotate(rot1Params.subList(swapArg1, swapArg1 + argc), -ROT);
             MethodType rot1Type = MethodType.methodType(target.type().returnType(), rot1Params);
             MethodHandle rot1 = makeRotateArguments(rot1Type, target, swapArg1, argc, +ROT);
+            assert(rot1 != null);
             if (argc == 2)  return rot1;
             MethodHandle rot2 = makeRotateArguments(newType, rot1, swapArg1, argc-1, -ROT);
+            assert(rot2 != null);
             return rot2;
         }
         if (!canSwapArguments(newType, target.type(), swapArg1, swapArg2))
             return null;
-        Class<?> swapType = newType.parameterType(swapArg1);
+        Class<?> type1 = newType.parameterType(swapArg1);
+        Class<?> type2 = newType.parameterType(swapArg2);
         // in  arglist: [0: ...keep1 | pos1: a1 | pos1+1: keep2... | pos2: a2 | pos2+1: keep3... ]
         // out arglist: [0: ...keep1 | pos1: a2 | pos1+1: keep2... | pos2: a1 | pos2+1: keep3... ]
         int swapSlot2  = newType.parameterSlotDepth(swapArg2 + 1);
-        long conv = makeSwapConv(OP_SWAP_ARGS, swapArg1, basicType(swapType), swapSlot2);
+        long conv = makeSwapConv(OP_SWAP_ARGS, swapArg1, basicType(type1), swapSlot2, basicType(type2));
         return new AdapterMethodHandle(target, newType, conv);
     }
 
@@ -1029,16 +1032,16 @@
         assert(MAX_ARG_ROTATION == 1);
         int srcArg, dstArg;
         int dstSlot;
-        byte basicType;
-        if (chunk2Slots <= chunk1Slots) {
+        int moveChunk;
+        if (rotateBy == 1) {
             // Rotate right/down N (rotateBy = +N, N small, c2 small):
             // in  arglist: [0: ...keep1 | arg1: c1...  | limit-N: c2 | limit: keep2... ]
             // out arglist: [0: ...keep1 | arg1: c2 | arg1+N: c1...   | limit: keep2... ]
             srcArg = limit-1;
             dstArg = firstArg;
-            dstSlot = depth0 - chunk2Slots;
-            basicType = basicType(newType.parameterType(srcArg));
-            assert(chunk2Slots == type2size(basicType));
+            //dstSlot = depth0 - chunk2Slots;  //chunk2Slots is not relevant
+            dstSlot = depth0 + MethodHandleNatives.OP_ROT_ARGS_DOWN_LIMIT_BIAS;
+            moveChunk = chunk2Slots;
         } else {
             // Rotate left/up N (rotateBy = -N, N small, c1 small):
             // in  arglist: [0: ...keep1 | arg1: c1 | arg1+N: c2...   | limit: keep2... ]
@@ -1046,10 +1049,12 @@
             srcArg = firstArg;
             dstArg = limit-1;
             dstSlot = depth2;
-            basicType = basicType(newType.parameterType(srcArg));
-            assert(chunk1Slots == type2size(basicType));
+            moveChunk = chunk1Slots;
         }
-        long conv = makeSwapConv(OP_ROT_ARGS, srcArg, basicType, dstSlot);
+        byte srcType = basicType(newType.parameterType(srcArg));
+        byte dstType = basicType(newType.parameterType(dstArg));
+        assert(moveChunk == type2size(srcType));
+        long conv = makeSwapConv(OP_ROT_ARGS, srcArg, srcType, dstSlot, dstType);
         return new AdapterMethodHandle(target, newType, conv);
     }
 
--- a/src/share/classes/java/lang/invoke/MethodHandleImpl.java	Tue Jun 07 14:01:12 2011 -0700
+++ b/src/share/classes/java/lang/invoke/MethodHandleImpl.java	Tue Jun 14 22:47:09 2011 -0700
@@ -788,6 +788,7 @@
                 .insertParameterTypes(keepPosArgs, arrayType);
         return spreadArguments(target, newType, keepPosArgs, arrayType, arrayLength);
     }
+    // called internally only
     static MethodHandle spreadArgumentsFromPos(MethodHandle target, MethodType newType, int spreadArgPos) {
         int arrayLength = target.type().parameterCount() - spreadArgPos;
         return spreadArguments(target, newType, spreadArgPos, Object[].class, arrayLength);
@@ -849,6 +850,12 @@
         MethodType ttype = target.type();
         MethodType ftype = filter.type();
         assert(ftype.parameterCount() == 1);
+        MethodHandle result = null;
+        if (AdapterMethodHandle.canCollectArguments(ttype, ftype, pos, false)) {
+            result = AdapterMethodHandle.makeCollectArguments(target, filter, pos, false);
+            if (result != null)  return result;
+        }
+        assert(MethodHandleNatives.workaroundWithoutRicochetFrames());  // this code is deprecated
         MethodType rtype = ttype.changeParameterType(pos, ftype.parameterType(0));
         MethodType gttype = ttype.generic();
         if (ttype != gttype) {
@@ -860,18 +867,11 @@
             filter = convertArguments(filter, gftype, ftype, 0);
             ftype = gftype;
         }
-        MethodHandle result = null;
-        if (AdapterMethodHandle.canCollectArguments(ttype, ftype, pos, false)) {
-            result = AdapterMethodHandle.makeCollectArguments(target, filter, pos, false);
-        }
-        if (result == null) {
-            assert(MethodHandleNatives.workaroundWithoutRicochetFrames());  // this code is deprecated
-            if (ftype == ttype) {
+        if (ftype == ttype) {
             // simple unary case
-                result = FilterOneArgument.make(filter, target);
-            } else {
-                result = FilterGeneric.makeArgumentFilter(pos, filter, target);
-            }
+            result = FilterOneArgument.make(filter, target);
+        } else {
+            result = FilterGeneric.makeArgumentFilter(pos, filter, target);
         }
         if (result.type() != rtype)
             result = result.asType(rtype);
@@ -920,7 +920,7 @@
             this.fallback = fallback;
         }
         static boolean preferRicochetFrame(MethodType type) {
-            return (type.parameterCount() >= INVOKES.length || type.hasPrimitives());
+            return true;  // always use RF if available
         }
         static MethodHandle make(MethodHandle test, MethodHandle target, MethodHandle fallback) {
             MethodType type = target.type();
@@ -931,12 +931,13 @@
                 MethodHandle invoke = INVOKES[nargs];
                 MethodType gtype = type.generic();
                 assert(invoke.type().dropParameterTypes(0,1) == gtype);
-                MethodHandle gtest = convertArguments(test, gtype.changeReturnType(boolean.class), test.type(), 0);
-                MethodHandle gtarget = convertArguments(target, gtype, type, 0);
-                MethodHandle gfallback = convertArguments(fallback, gtype, type, 0);
+                // Note: convertArguments(...2) avoids interface casts present in convertArguments(...0)
+                MethodHandle gtest = convertArguments(test, gtype.changeReturnType(boolean.class), test.type(), 2);
+                MethodHandle gtarget = convertArguments(target, gtype, type, 2);
+                MethodHandle gfallback = convertArguments(fallback, gtype, type, 2);
                 if (gtest == null || gtarget == null || gfallback == null)  return null;
                 MethodHandle gguard = new GuardWithTest(invoke, gtest, gtarget, gfallback);
-                return convertArguments(gguard, type, gtype, 0);
+                return convertArguments(gguard, type, gtype, 2);
             } else {
                 assert(MethodHandleNatives.workaroundWithoutRicochetFrames());  // this code is deprecated
                 MethodHandle invoke = VARARGS_INVOKE;
@@ -1221,11 +1222,12 @@
         if (nargs < GuardWithCatch.INVOKES.length) {
             MethodType gtype = type.generic();
             MethodType gcatchType = gtype.insertParameterTypes(0, Throwable.class);
-            MethodHandle gtarget = convertArguments(target, gtype, type, 0);
-            MethodHandle gcatcher = convertArguments(catcher, gcatchType, ctype, 0);
+            // Note: convertArguments(...2) avoids interface casts present in convertArguments(...0)
+            MethodHandle gtarget = convertArguments(target, gtype, type, 2);
+            MethodHandle gcatcher = convertArguments(catcher, gcatchType, ctype, 2);
             MethodHandle gguard = new GuardWithCatch(gtarget, exType, gcatcher);
             if (gtarget == null || gcatcher == null || gguard == null)  return null;
-            return convertArguments(gguard, type, gtype, 0);
+            return convertArguments(gguard, type, gtype, 2);
         } else {
             MethodType gtype = MethodType.genericMethodType(0, true);
             MethodType gcatchType = gtype.insertParameterTypes(0, Throwable.class);
--- a/src/share/classes/java/lang/invoke/MethodHandleNatives.java	Tue Jun 07 14:01:12 2011 -0700
+++ b/src/share/classes/java/lang/invoke/MethodHandleNatives.java	Tue Jun 14 22:47:09 2011 -0700
@@ -118,32 +118,20 @@
     /** Derived mode flag.  Only false on some old JVM implementations. */
     static final boolean HAVE_RICOCHET_FRAMES;
 
+    static final int OP_ROT_ARGS_DOWN_LIMIT_BIAS;
+
     private static native void registerNatives();
     static {
-        int     JVM_PUSH_LIMIT_;
-        int     JVM_STACK_MOVE_UNIT_;
-        int     CONV_OP_IMPLEMENTED_MASK_;
-        try {
-            registerNatives();
-            JVM_PUSH_LIMIT_ = getConstant(Constants.GC_JVM_PUSH_LIMIT);
-            JVM_STACK_MOVE_UNIT_ = getConstant(Constants.GC_JVM_STACK_MOVE_UNIT);
-            CONV_OP_IMPLEMENTED_MASK_ = getConstant(Constants.GC_CONV_OP_IMPLEMENTED_MASK);
-            //sun.reflect.Reflection.registerMethodsToFilter(MethodHandleImpl.class, "init");
-        } catch (UnsatisfiedLinkError ee) {
-            // ignore; if we use init() methods later we'll see linkage errors
-            JVM_PUSH_LIMIT_ = 3;  // arbitrary
-            JVM_STACK_MOVE_UNIT_ = -1;  // arbitrary
-            CONV_OP_IMPLEMENTED_MASK_ = 0;
-            JVM_PUSH_LIMIT = JVM_PUSH_LIMIT_;
-            JVM_STACK_MOVE_UNIT = JVM_STACK_MOVE_UNIT_;
-            throw ee;  // just die; hopeless to try to run with an older JVM
-        }
-        JVM_PUSH_LIMIT = JVM_PUSH_LIMIT_;
-        JVM_STACK_MOVE_UNIT = JVM_STACK_MOVE_UNIT_;
-        if (CONV_OP_IMPLEMENTED_MASK_ == 0)
-            CONV_OP_IMPLEMENTED_MASK_ = DEFAULT_CONV_OP_IMPLEMENTED_MASK;
-        CONV_OP_IMPLEMENTED_MASK = CONV_OP_IMPLEMENTED_MASK_;
-        HAVE_RICOCHET_FRAMES = (CONV_OP_IMPLEMENTED_MASK & (1<<OP_COLLECT_ARGS)) != 0;
+        registerNatives();
+        int k;
+        JVM_PUSH_LIMIT              = getConstant(Constants.GC_JVM_PUSH_LIMIT);
+        JVM_STACK_MOVE_UNIT         = getConstant(Constants.GC_JVM_STACK_MOVE_UNIT);
+        k                           = getConstant(Constants.GC_CONV_OP_IMPLEMENTED_MASK);
+        CONV_OP_IMPLEMENTED_MASK    = (k != 0) ? k : DEFAULT_CONV_OP_IMPLEMENTED_MASK;
+        k                           = getConstant(Constants.GC_OP_ROT_ARGS_DOWN_LIMIT_BIAS);
+        OP_ROT_ARGS_DOWN_LIMIT_BIAS = (k != 0) ? (byte)k : -1;
+        HAVE_RICOCHET_FRAMES        = (CONV_OP_IMPLEMENTED_MASK & (1<<OP_COLLECT_ARGS)) != 0;
+        //sun.reflect.Reflection.registerMethodsToFilter(MethodHandleImpl.class, "init");
     }
 
     // All compile-time constants go here.
@@ -154,7 +142,8 @@
         static final int // for getConstant
                 GC_JVM_PUSH_LIMIT = 0,
                 GC_JVM_STACK_MOVE_UNIT = 1,
-                GC_CONV_OP_IMPLEMENTED_MASK = 2;
+                GC_CONV_OP_IMPLEMENTED_MASK = 2,
+                GC_OP_ROT_ARGS_DOWN_LIMIT_BIAS = 3;
         static final int
                 ETF_HANDLE_OR_METHOD_NAME = 0, // all available data (immediate MH or method)
                 ETF_DIRECT_HANDLE         = 1, // ultimate method handle (will be a DMH, may be self)
--- a/src/share/classes/java/lang/invoke/SwitchPoint.java	Tue Jun 07 14:01:12 2011 -0700
+++ b/src/share/classes/java/lang/invoke/SwitchPoint.java	Tue Jun 14 22:47:09 2011 -0700
@@ -134,7 +134,7 @@
      * to return true for {@code hasBeenInvalidated},
      * it will always do so in the future.
      * On the other hand, a valid switch point visible to other threads may
-     * invalidated at any moment, due to a request by another thread.
+     * be invalidated at any moment, due to a request by another thread.
      * <p style="font-size:smaller;">
      * Since invalidation is a global and immediate operation,
      * the execution of this query, on a valid switchpoint,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/java/lang/invoke/PermuteArgsTest.java	Tue Jun 14 22:47:09 2011 -0700
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @summary unit tests for method handles which permute their arguments
+ * @run junit/othervm -ea -esa -DPermuteArgsTest.MAX_ARITY=8 test.java.lang.invoke.PermuteArgsTest
+ */
+/* Examples of manual runs:
+ * java -DPermuteArgsTest.{DRY_RUN=true,MAX_ARITY=253} test.java.lang.invoke.PermuteArgsTest
+ * java -DPermuteArgsTest.{VERBOSE=true,MAX_ARITY=5} test.java.lang.invoke.PermuteArgsTest
+ * java test.java.lang.invoke.PermuteArgsTest list3I[2,0,1] listJLJ[2,0,1]
+ */
+
+package test.java.lang.invoke;
+
+import org.junit.*;
+
+import java.util.*;
+import java.lang.reflect.*;
+
+import java.lang.invoke.*;
+import static java.lang.invoke.MethodHandles.*;
+import static java.lang.invoke.MethodType.*;
+
+public class PermuteArgsTest {
+    private static final Class CLASS = PermuteArgsTest.class;
+    private static final int MAX_ARITY = Integer.getInteger(CLASS.getSimpleName()+".MAX_ARITY", 8);
+    private static final boolean DRY_RUN = Boolean.getBoolean(CLASS.getSimpleName()+".DRY_RUN");
+    private static final boolean VERBOSE = Boolean.getBoolean(CLASS.getSimpleName()+".VERBOSE") || DRY_RUN;
+
+    static Object list2I(int x, int y) {
+        return Arrays.asList(x, y);
+    }
+    static Object list3I(int x, int y, int z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object list4I(int w, int x, int y, int z) {
+        return Arrays.asList(w, x, y, z);
+    }
+    static Object list2J(long x, long y) {
+        return Arrays.asList(x, y);
+    }
+    static Object list3J(long x, long y, long z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object list4J(long w, long x, long y, long z) {
+        return Arrays.asList(w, x, y, z);
+    }
+    static Object list2I2J(int w, int x, long y, long z) {
+        return Arrays.asList(w, x, y, z);
+    }
+    static Object list2J2I(long w, long x, int y, int z) {
+        return Arrays.asList(w, x, y, z);
+    }
+    static Object listLJJ(Object x, long y, long z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object listJLJ(long x, Object y, long z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object listJJL(long x, long y, Object z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object listJLL(long x, Object y, Object z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object listLJL(Object x, long y, Object z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object listLLJ(Object x, Object y, long z) {
+        return Arrays.asList(x, y, z);
+    }
+    static Object listJLLJ(long w, Object x, Object y, long z) {
+        return Arrays.asList(w, x, y, z);
+    }
+    static Object listLJJL(Object w, long x, long y, Object z) {
+        return Arrays.asList(w, x, y, z);
+    }
+    static Object listI_etc(int... va) {
+        ArrayList<Object> res = new ArrayList<Object>();
+        for (int x : va)  res.add(x);
+        return res;
+    }
+    static Object listIJL_etc(int x, long y, Object z, Object... va) {
+        ArrayList<Object> res = new ArrayList<Object>();
+        res.addAll(Arrays.asList(x, y, z));
+        res.addAll(Arrays.asList(va));
+        return res;
+    }
+
+    public static void main(String argv[]) throws Throwable {
+        if (argv.length > 0) {
+            for (String arg : argv) {
+                // arg ::= name[n,...]
+                int k = arg.indexOf('[');
+                String mhName = arg.substring(0, k).trim();
+                String permString = arg.substring(k);
+                testOnePermutation(mhName, permString);
+            }
+            return;
+        }
+        new PermuteArgsTest().test();
+    }
+    static int testCases;
+    @Test
+    public void test() throws Throwable {
+        testCases = 0;
+        Lookup lookup = lookup();
+        for (Method m : lookup.lookupClass().getDeclaredMethods()) {
+            if (m.getName().startsWith("list") &&
+                Modifier.isStatic(m.getModifiers())) {
+                test(m.getName(), lookup.unreflect(m));
+            }
+        }
+        System.out.println("ran a total of "+testCases+" test cases");
+    }
+
+    static int jump(int i, int min, int max) {
+        if (i >= min && i <= max-1) {
+            // jump faster
+            int len = max-min;
+            if (i < min + len/2)
+                i = min + len/2;
+            else
+                i = max-1;
+        }
+        return i;
+    }
+
+    static void test(String name, MethodHandle mh) throws Throwable {
+        if (VERBOSE)
+            System.out.println("mh = "+name+" : "+mh+" { "
+                               +Arrays.toString(junkArgs(mh.type().parameterArray())));
+        int testCases0 = testCases;
+        if (!mh.isVarargsCollector()) {
+            // normal case
+            testPermutations(mh);
+        } else {
+            // varargs case; add params up to MAX_ARITY
+            MethodType mt = mh.type();
+            int posArgs = mt.parameterCount() - 1;
+            int arity0 = Math.max(3, posArgs);
+            for (int arity = arity0; arity <= MAX_ARITY; arity++) {
+                MethodHandle mh1;
+                try {
+                    mh1 = adjustArity(mh, arity);
+                } catch (IllegalArgumentException ex) {
+                    System.out.println("*** mh = "+name+" : "+mh+"; arity = "+arity+" => "+ex);
+                    ex.printStackTrace();
+                    break;  // cannot get this arity for this type
+                }
+                test("("+arity+")"+name, mh1);
+                arity = jump(arity, arity0*2, MAX_ARITY);
+            }
+        }
+        if (VERBOSE)
+            System.out.println("ran "+(testCases - testCases0)+" test cases for "+name+" }");
+    }
+
+    static MethodHandle adjustArity(MethodHandle mh, int arity) {
+        MethodType mt = mh.type();
+        int posArgs = mt.parameterCount() - 1;
+        Class<?> reptype = mt.parameterType(posArgs).getComponentType();
+        MethodType mt1 = mt.dropParameterTypes(posArgs, posArgs+1);
+        while (mt1.parameterCount() < arity) {
+            Class<?> pt = reptype;
+            if (pt == Object.class && posArgs > 0)
+                // repeat types cyclically if possible:
+                pt = mt1.parameterType(mt1.parameterCount() - posArgs);
+            mt1 = mt1.appendParameterTypes(pt);
+        }
+        return mh.asType(mt1);
+    }
+    static MethodHandle findTestMH(String name, int[] perm) throws ReflectiveOperationException {
+        int arity = perm.length;
+        Lookup lookup = lookup();
+        for (Method m : lookup.lookupClass().getDeclaredMethods()) {
+            if (m.getName().equals(name) &&
+                Modifier.isStatic(m.getModifiers())) {
+                MethodHandle mh = lookup.unreflect(m);
+                int mhArity = mh.type().parameterCount();
+                if (mh.isVarargsCollector()) {
+                    if (mhArity-1 <= arity)
+                        return adjustArity(mh, arity);
+                } else if (mhArity == arity) {
+                    return mh;
+                }
+            }
+        }
+        throw new RuntimeException("no such method for arity "+arity+": "+name);
+    }
+
+    static void testPermutations(MethodHandle mh) throws Throwable {
+        HashSet<String> done = new HashSet<String>();
+        MethodType mt = mh.type();
+        int[] perm = nullPerm(mt.parameterCount());
+        final int MARGIN = (perm.length <= 10 ? 2 : 0);
+        int testCases0 = testCases;
+        for (int j = 0; j <= 1; j++) {
+            int maxStart = perm.length-1;
+            if (j != 0)  maxStart /= 2;
+            for (int start = 0; start <= maxStart; start++) {
+                int maxOmit = (maxStart - start) / 2;
+                if (start != 0)  maxOmit = 2;
+                if (j != 0)  maxOmit = 1;
+                for (int omit = 0; omit <= maxOmit; omit++) {
+                    int end = perm.length - omit;
+                    if (end - start >= 2) {
+                        //System.out.println("testPermutations"+Arrays.asList(start, end)+(j == 0 ? "" : " (reverse)"));
+                        testPermutations(mh, perm, start, end, done);
+                    }
+                    omit = jump(omit, (start == 0 && j == 0 ? MARGIN : 0), maxOmit);
+                }
+                start = jump(start, (j == 0 ? MARGIN : 0), maxStart);
+            }
+            // do everything in reverse:
+            reverse(perm, 0, perm.length);
+        }
+        switch (perm.length) {
+        case 2: assert(testCases - testCases0 == 2); break;
+        case 3: assert(testCases - testCases0 == 6); break;
+        case 4: assert(testCases - testCases0 == 24); break;
+        case 5: assert(testCases - testCases0 == 120); break;
+        case 6: assert(testCases - testCases0 > 720/3); break;
+        }
+    }
+
+    static void testPermutations(MethodHandle mh, int[] perm, int start, int end, Set<String> done) throws Throwable {
+        if (end - start <= 1)  return;
+        for (int j = 0; j <= 1; j++) {
+            testRotations(mh, perm, start, end, done);
+            if (end - start <= 2)  return;
+            reverse(perm, start, end);
+        }
+        if (end - start <= 3)  return;
+        int excess4 = (end - start) - 4;
+        // composed rotations:
+        int start2 = start + 1 + excess4/3;
+        int end2   = end       - excess4/3;
+        end2 = start2 + Math.min(start == 0 ? 4 : 3, end2 - start2);
+        int skips = (perm.length+3)/5;
+        for (int i = start; i < end; i++) {
+            rotate(perm, start, end);
+            if (skips > 1 && ((i-start) + (i-start)/7) % skips != 0)  continue;
+            for (int j = 0; j <= 1; j++) {
+                testPermutations(mh, perm, start2, end2, done);
+                reverse(perm, start, end);
+            }
+        }
+    }
+
+    static void testRotations(MethodHandle mh, int[] perm, int start, int end, Set<String> done) throws Throwable {
+        Object[] args = junkArgs(mh.type().parameterArray());
+        for (int i = start; i < end; i++) {
+            if (done.add(Arrays.toString(perm)))
+                testOnePermutation(mh, perm, args);
+            rotate(perm, start, end);
+        }
+    }
+
+    static void testOnePermutation(MethodHandle mh, int[] perm, Object[] args) throws Throwable {
+        MethodType mt = mh.type();
+        MethodType pmt = methodType(mt.returnType(), unpermuteArgs(perm, mt.parameterArray(), Class[].class));
+        if (VERBOSE)
+            System.out.println(Arrays.toString(perm));
+        testCases += 1;
+        if (DRY_RUN)
+            return;
+        Object res = permuteArguments(mh, pmt, perm).invokeWithArguments(unpermuteArgs(perm, args));
+        String str = String.valueOf(res);
+        if (!Arrays.toString(args).equals(str)) {
+            System.out.println(Arrays.toString(perm)+" "+str+" *** WRONG ***");
+        }
+    }
+
+    // For reproducing failures:
+    static void testOnePermutation(String mhName, String permString) throws Throwable {
+        String s = permString;
+        s = s.replace('[', ' ').replace(']', ' ').replace(',', ' ');  // easier to trim spaces
+        s = s.trim();
+        int[] perm = new int[s.length()];
+        int arity = 0;
+        while (!s.isEmpty()) {
+            int k = s.indexOf(' ');
+            if (k < 0)  k = s.length();
+            perm[arity++] = Integer.parseInt(s.substring(0, k));
+            s = s.substring(k).trim();
+        }
+        perm = Arrays.copyOf(perm, arity);
+        testOnePermutation(mhName, perm);
+    }
+    static void testOnePermutation(String mhName, int[] perm) throws Throwable {
+        MethodHandle mh = findTestMH(mhName, perm);
+        System.out.println("mh = "+mhName+" : "+mh+" { "
+                           +Arrays.toString(junkArgs(mh.type().parameterArray())));
+        Object[] args = junkArgs(mh.type().parameterArray());
+        testOnePermutation(mh, perm, args);
+        System.out.println("}");
+    }
+
+    static Object[] junkArgs(Class<?>[] ptypes) {
+        Object[] args = new Object[ptypes.length];
+        for (int i = 0; i < ptypes.length; i++) {
+            Class<?> pt = ptypes[i];
+            Object arg;
+            if (pt == Void.class)       arg = null;
+            else if (pt == int.class)   arg = (int)  i + 101;
+            else if (pt == long.class)  arg = (long) i + 10_000_000_001L;
+            else                        arg = "#" + (i + 1);
+            args[i] = arg;
+        }
+        return args;
+    }
+
+    static int[] nullPerm(int len) {
+        int[] perm = new int[len];
+        for (int i = 0; i < len; i++)
+            perm[i] = i;
+        return perm;
+    }
+    static void rotate(int[] perm) {
+        rotate(perm, 0, perm.length);
+    }
+    static void rotate(int[] perm, int start, int end) {
+        int x = perm[end-1];
+        for (int j = start; j < end; j++) {
+            int y = perm[j]; perm[j] = x; x = y;
+        }
+    }
+    static void reverse(int[] perm) {
+        reverse(perm, 0, perm.length);
+    }
+    static void reverse(int[] perm, int start, int end) {
+        int mid = start + (end - start)/2;
+        for (int j = start; j < mid; j++) {
+            int k = (end-1) - j;
+            int x = perm[j]; perm[j] = perm[k]; perm[k] = x;
+        }
+    }
+    // Permute the args according to the inverse of perm.
+    static Object[] unpermuteArgs(int[] perm, Object[] args) {
+        return unpermuteArgs(perm, args, Object[].class);
+    }
+    static <T> T[] unpermuteArgs(int[] perm, T[] args, Class<T[]> Tclass) {
+        T[] res = Arrays.copyOf(new Object[0], perm.length, Tclass);
+        for (int i = 0; i < perm.length; i++)
+            res[perm[i]] = args[i];
+        return res;
+    }
+}