changeset 15318:5b6bd1fc3d46

8163369: Enable generating DMH classes at link time Reviewed-by: vlivanov, shade
author redestad
date Tue, 09 Aug 2016 10:00:31 +0200
parents 3a850cd34e59
children 8c57f4c293bb
files src/java.base/share/classes/java/lang/invoke/DirectMethodHandle.java src/java.base/share/classes/java/lang/invoke/InvokerBytecodeGenerator.java src/java.base/share/classes/java/lang/invoke/LambdaForm.java src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java
diffstat 4 files changed, 295 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/java/lang/invoke/DirectMethodHandle.java	Tue Aug 09 09:42:01 2016 +0200
+++ b/src/java.base/share/classes/java/lang/invoke/DirectMethodHandle.java	Tue Aug 09 10:00:31 2016 +0200
@@ -27,6 +27,7 @@
 
 import jdk.internal.misc.Unsafe;
 import jdk.internal.vm.annotation.ForceInline;
+import jdk.internal.vm.annotation.Stable;
 import sun.invoke.util.ValueConversions;
 import sun.invoke.util.VerifyAccess;
 import sun.invoke.util.VerifyType;
@@ -190,14 +191,15 @@
         boolean doesAlloc = (which == LF_NEWINVSPECIAL);
         String linkerName, lambdaName;
         switch (which) {
-        case LF_INVVIRTUAL:    linkerName = "linkToVirtual";    lambdaName = "DMH.invokeVirtual";    break;
-        case LF_INVSTATIC:     linkerName = "linkToStatic";     lambdaName = "DMH.invokeStatic";     break;
-        case LF_INVSTATIC_INIT:linkerName = "linkToStatic";     lambdaName = "DMH.invokeStaticInit"; break;
-        case LF_INVSPECIAL:    linkerName = "linkToSpecial";    lambdaName = "DMH.invokeSpecial";    break;
-        case LF_INVINTERFACE:  linkerName = "linkToInterface";  lambdaName = "DMH.invokeInterface";  break;
-        case LF_NEWINVSPECIAL: linkerName = "linkToSpecial";    lambdaName = "DMH.newInvokeSpecial"; break;
+        case LF_INVVIRTUAL:    linkerName = "linkToVirtual";    lambdaName = "invokeVirtual";    break;
+        case LF_INVSTATIC:     linkerName = "linkToStatic";     lambdaName = "invokeStatic";     break;
+        case LF_INVSTATIC_INIT:linkerName = "linkToStatic";     lambdaName = "invokeStaticInit"; break;
+        case LF_INVSPECIAL:    linkerName = "linkToSpecial";    lambdaName = "invokeSpecial";    break;
+        case LF_INVINTERFACE:  linkerName = "linkToInterface";  lambdaName = "invokeInterface";  break;
+        case LF_NEWINVSPECIAL: linkerName = "linkToSpecial";    lambdaName = "newInvokeSpecial"; break;
         default:  throw new InternalError("which="+which);
         }
+
         MethodType mtypeWithArg = mtype.appendParameterTypes(MemberName.class);
         if (doesAlloc)
             mtypeWithArg = mtypeWithArg
@@ -240,11 +242,26 @@
         names[LINKER_CALL] = new Name(linker, outArgs);
         lambdaName += "_" + shortenSignature(basicTypeSignature(mtype));
         LambdaForm lform = new LambdaForm(lambdaName, ARG_LIMIT, names, result);
+
         // This is a tricky bit of code.  Don't send it through the LF interpreter.
-        lform.compileToBytecode();
+        lform.compileToBytecode(Holder.class);
         return lform;
     }
 
+    /*
+     * NOTE: This method acts as an API hook for use by the
+     * GenerateJLIClassesPlugin to generate a class wrapping DirectMethodHandle
+     * methods for an array of method types.
+     */
+    static byte[] generateDMHClassBytes(String className, MethodType[] methodTypes, int[] types) {
+        LambdaForm[] forms = new LambdaForm[methodTypes.length];
+        for (int i = 0; i < forms.length; i++) {
+            forms[i] = makePreparedLambdaForm(methodTypes[i], types[i]);
+            methodTypes[i] = forms[i].methodType();
+        }
+        return InvokerBytecodeGenerator.generateCodeBytesForMultiple(className, forms, methodTypes);
+    }
+
     static Object findDirectMethodHandle(Name name) {
         if (name.function == NF_internalMemberName ||
             name.function == NF_internalMemberNameEnsureInit ||
@@ -487,7 +504,7 @@
     }
 
     // Caching machinery for field accessors:
-    private static byte
+    private static final byte
             AF_GETFIELD        = 0,
             AF_PUTFIELD        = 1,
             AF_GETSTATIC       = 2,
@@ -497,7 +514,7 @@
             AF_LIMIT           = 6;
     // Enumerate the different field kinds using Wrapper,
     // with an extra case added for checked references.
-    private static int
+    private static final int
             FT_LAST_WRAPPER    = Wrapper.values().length-1,
             FT_UNCHECKED_REF   = Wrapper.OBJECT.ordinal(),
             FT_CHECKED_REF     = FT_LAST_WRAPPER+1,
@@ -507,6 +524,7 @@
                 + (isVolatile ? FT_LIMIT : 0)
                 + ftypeKind);
     }
+    @Stable
     private static final LambdaForm[] ACCESSOR_FORMS
             = new LambdaForm[afIndex(AF_LIMIT, false, 0)];
     private static int ftypeKind(Class<?> ftype) {
@@ -549,10 +567,11 @@
         return lform;
     }
     private static LambdaForm preparedFieldLambdaForm(byte formOp, boolean isVolatile, Class<?> ftype) {
-        int afIndex = afIndex(formOp, isVolatile, ftypeKind(ftype));
+        int ftypeKind = ftypeKind(ftype);
+        int afIndex = afIndex(formOp, isVolatile, ftypeKind);
         LambdaForm lform = ACCESSOR_FORMS[afIndex];
         if (lform != null)  return lform;
-        lform = makePreparedFieldLambdaForm(formOp, isVolatile, ftypeKind(ftype));
+        lform = makePreparedFieldLambdaForm(formOp, isVolatile, ftypeKind);
         ACCESSOR_FORMS[afIndex] = lform;  // don't bother with a CAS
         return lform;
     }
@@ -682,4 +701,15 @@
             throw newInternalError(ex);
         }
     }
+
+    static {
+        // The DMH class will contain pre-generated DirectMethodHandles resolved
+        // speculatively using MemberName.getFactory().resolveOrNull. However, that
+        // doesn't initialize the class, which subtly breaks inlining etc. By forcing
+        // initialization of the Holder class we avoid these issues.
+        UNSAFE.ensureClassInitialized(Holder.class);
+    }
+
+    /* Placeholder class for DirectMethodHandles generated ahead of time */
+    private final class Holder {}
 }
--- a/src/java.base/share/classes/java/lang/invoke/InvokerBytecodeGenerator.java	Tue Aug 09 09:42:01 2016 +0200
+++ b/src/java.base/share/classes/java/lang/invoke/InvokerBytecodeGenerator.java	Tue Aug 09 10:00:31 2016 +0200
@@ -70,7 +70,7 @@
     private static final String LLV_SIG = "(L" + OBJ + ";L" + OBJ + ";)V";
 
     /** Name of its super class*/
-    private static final String superName = OBJ;
+    private static final String INVOKER_SUPER_NAME = OBJ;
 
     /** Name of new class */
     private final String className;
@@ -296,12 +296,15 @@
     /**
      * Set up class file generation.
      */
-    private void classFilePrologue() {
+    private ClassWriter classFilePrologue() {
         final int NOT_ACC_PUBLIC = 0;  // not ACC_PUBLIC
         cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
-        cw.visit(Opcodes.V1_8, NOT_ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_SUPER, className, null, superName, null);
+        cw.visit(Opcodes.V1_8, NOT_ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_SUPER, className, null, INVOKER_SUPER_NAME, null);
         cw.visitSource(sourceFile, null);
+        return cw;
+    }
 
+    private void methodPrologue() {
         String invokerDesc = invokerType.toMethodDescriptorString();
         mv = cw.visitMethod(Opcodes.ACC_STATIC, invokerName, invokerDesc, null, null);
     }
@@ -309,7 +312,7 @@
     /**
      * Tear down class file generation.
      */
-    private void classFileEpilogue() {
+    private void methodEpilogue() {
         mv.visitMaxs(0, 0);
         mv.visitEnd();
     }
@@ -644,6 +647,44 @@
      */
     private byte[] generateCustomizedCodeBytes() {
         classFilePrologue();
+        addMethod();
+        bogusMethod(lambdaForm);
+
+        final byte[] classFile = toByteArray();
+        maybeDump(className, classFile);
+        return classFile;
+    }
+
+    /*
+     * NOTE: This is used from GenerateJLIClassesPlugin via
+     * DirectMethodHandle::generateDMHClassBytes.
+     *
+     * Generate customized code for a set of LambdaForms of specified types into
+     * a class with a specified name.
+     */
+    static byte[] generateCodeBytesForMultiple(String className,
+            LambdaForm[] forms, MethodType[] types) {
+        assert(forms.length == types.length);
+
+        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
+        cw.visit(Opcodes.V1_8, Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL + Opcodes.ACC_SUPER,
+                className, null, INVOKER_SUPER_NAME, null);
+        cw.visitSource(className.substring(className.lastIndexOf('/') + 1), null);
+        for (int i = 0; i < forms.length; i++) {
+            InvokerBytecodeGenerator g
+                    = new InvokerBytecodeGenerator(className, forms[i], types[i]);
+            g.setClassWriter(cw);
+            g.addMethod();
+        }
+        return cw.toByteArray();
+    }
+
+    private void setClassWriter(ClassWriter cw) {
+        this.cw = cw;
+    }
+
+    private void addMethod() {
+        methodPrologue();
 
         // Suppress this method in backtraces displayed to the user.
         mv.visitAnnotation(LF_HIDDEN_SIG, true);
@@ -748,19 +789,19 @@
         // return statement
         emitReturn(onStack);
 
-        classFileEpilogue();
-        bogusMethod(lambdaForm);
+        methodEpilogue();
+    }
 
-        final byte[] classFile;
+    /*
+     * @throws BytecodeGenerationException if something goes wrong when
+     *         generating the byte code
+     */
+    private byte[] toByteArray() {
         try {
-            classFile = cw.toByteArray();
+            return cw.toByteArray();
         } catch (RuntimeException e) {
-            // ASM throws RuntimeException if something goes wrong - capture these and wrap them in a meaningful
-            // exception to support falling back to LambdaForm interpretation
             throw new BytecodeGenerationException(e);
         }
-        maybeDump(className, classFile);
-        return classFile;
     }
 
     @SuppressWarnings("serial")
@@ -1607,6 +1648,7 @@
 
     private byte[] generateLambdaFormInterpreterEntryPointBytes() {
         classFilePrologue();
+        methodPrologue();
 
         // Suppress this method in backtraces displayed to the user.
         mv.visitAnnotation(LF_HIDDEN_SIG, true);
@@ -1645,7 +1687,7 @@
         // return statement
         emitReturnInsn(basicType(rtype));
 
-        classFileEpilogue();
+        methodEpilogue();
         bogusMethod(invokerType);
 
         final byte[] classFile = cw.toByteArray();
@@ -1666,6 +1708,7 @@
     private byte[] generateNamedFunctionInvokerImpl(MethodTypeForm typeForm) {
         MethodType dstType = typeForm.erasedType();
         classFilePrologue();
+        methodPrologue();
 
         // Suppress this method in backtraces displayed to the user.
         mv.visitAnnotation(LF_HIDDEN_SIG, true);
@@ -1685,7 +1728,6 @@
             // Maybe unbox
             Class<?> dptype = dstType.parameterType(i);
             if (dptype.isPrimitive()) {
-                Class<?> sptype = dstType.basicType().wrap().parameterType(i);
                 Wrapper dstWrapper = Wrapper.forBasicType(dptype);
                 Wrapper srcWrapper = dstWrapper.isSubwordOrInt() ? Wrapper.INT : dstWrapper;  // narrow subword from int
                 emitUnboxing(srcWrapper);
@@ -1713,7 +1755,7 @@
         }
         emitReturnInsn(L_TYPE);  // NOTE: NamedFunction invokers always return a reference value.
 
-        classFileEpilogue();
+        methodEpilogue();
         bogusMethod(dstType);
 
         final byte[] classFile = cw.toByteArray();
--- a/src/java.base/share/classes/java/lang/invoke/LambdaForm.java	Tue Aug 09 09:42:01 2016 +0200
+++ b/src/java.base/share/classes/java/lang/invoke/LambdaForm.java	Tue Aug 09 10:00:31 2016 +0200
@@ -773,6 +773,26 @@
         }
     }
 
+    /**
+     * Generate optimizable bytecode for this form after first looking for a
+     * pregenerated version in a specified class.
+     */
+    void compileToBytecode(Class<?> lookupClass) {
+        if (vmentry != null && isCompiled) {
+            return;  // already compiled somehow
+        }
+        MethodType invokerType = methodType();
+        assert(vmentry == null || vmentry.getMethodType().basicType().equals(invokerType));
+        MemberName member = new MemberName(lookupClass, debugName, invokerType, REF_invokeStatic);
+        MemberName resolvedMember = MemberName.getFactory().resolveOrNull(REF_invokeStatic, member, lookupClass);
+        if (resolvedMember != null) {
+            vmentry = resolvedMember;
+            isCompiled = true;
+        } else {
+            compileToBytecode();
+        }
+    }
+
     private static void computeInitialPreparedForms() {
         // Find all predefined invokers and associate them with canonical empty lambda forms.
         for (MemberName m : MemberName.getFactory().getMethods(LambdaForm.class, false, null, null, null)) {
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java	Tue Aug 09 09:42:01 2016 +0200
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java	Tue Aug 09 10:00:31 2016 +0200
@@ -24,9 +24,11 @@
  */
 package jdk.tools.jlink.internal.plugins;
 
+import java.lang.invoke.MethodType;
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -48,14 +50,26 @@
 
     private static final String BMH_SPECIES_PARAM = "bmh-species";
 
+    private static final String DMH_PARAM = "dmh";
+
     private static final String DESCRIPTION = PluginsResourceBundle.getDescription(NAME);
 
     private static final String BMH = "java/lang/invoke/BoundMethodHandle";
+    private static final Method BMH_FACTORY_METHOD;
 
-    private static final Method FACTORY_METHOD;
+    private static final String DMH = "java/lang/invoke/DirectMethodHandle$Holder";
+    private static final String DMH_INVOKE_VIRTUAL = "invokeVirtual";
+    private static final String DMH_INVOKE_STATIC = "invokeStatic";
+    private static final String DMH_INVOKE_SPECIAL = "invokeSpecial";
+    private static final String DMH_NEW_INVOKE_SPECIAL = "newInvokeSpecial";
+    private static final String DMH_INVOKE_INTERFACE = "invokeInterface";
+    private static final String DMH_INVOKE_STATIC_INIT = "invokeStaticInit";
+    private static final Method DMH_FACTORY_METHOD;
 
     List<String> speciesTypes;
 
+    Map<String, List<String>> dmhMethods;
+
     public GenerateJLIClassesPlugin() {
     }
 
@@ -87,11 +101,9 @@
     /**
      * @return the default Species forms to generate.
      *
-     * This list was derived from running a Java concatenating strings
-     * with -Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT set
-     * plus a subset of octane. A better long-term solution is to define
-     * and run a set of quick generators and extracting this list as a
-     * step in the build process.
+     * This list was derived from running a small startup benchmark.
+     * A better long-term solution is to define and run a set of quick
+     * generators and extracting this list as a step in the build process.
      */
     public static List<String> defaultSpecies() {
         return List.of("LL", "L3", "L4", "L5", "L6", "L7", "L7I",
@@ -100,18 +112,51 @@
                 "LILL", "I", "LLILL");
     }
 
+    /**
+     * @return the list of default DirectMethodHandle methods to generate.
+     */
+    public static Map<String, List<String>> defaultDMHMethods() {
+        return Map.of(
+            DMH_INVOKE_VIRTUAL, List.of("_L", "L_L", "LI_I"),
+            DMH_INVOKE_SPECIAL, List.of("L_I", "L_L", "LF_L", "LD_L", "LL_L",
+                "L3_L", "L4_L", "L5_L", "L6_L", "L7_L", "LI_I", "LI_L", "LIL_I",
+                "LII_I", "LII_L", "LLI_L", "LLI_I", "LILI_I", "LIIL_L",
+                "LIILL_L", "LIILL_I", "LIIL_I", "LILIL_I", "LILILL_I",
+                "LILII_I", "LI3_I", "LI3L_I", "LI3LL_I", "LI3_L", "LI4_I"),
+            DMH_INVOKE_STATIC, List.of("II_I", "IL_I", "ILIL_I", "ILII_I",
+                "_I", "_L", "_V", "D_L", "F_L", "I_I", "II_L", "LI_L",
+                "L_V", "L_L", "LL_L", "L3_L", "L4_L", "L5_L", "L6_L",
+                "L7_L", "L8_L", "L9_L", "L9I_L", "L9II_L", "L9IIL_L",
+                "L10_L", "L11_L", "L12_L", "L13_L", "L13I_L", "L13II_L")
+        );
+    }
+
+    // Map from DirectMethodHandle method type to internal ID
+    private static final Map<String, Integer> DMH_METHOD_TYPE_MAP =
+            Map.of(
+                DMH_INVOKE_VIRTUAL,     0,
+                DMH_INVOKE_STATIC,      1,
+                DMH_INVOKE_SPECIAL,     2,
+                DMH_NEW_INVOKE_SPECIAL, 3,
+                DMH_INVOKE_INTERFACE,   4,
+                DMH_INVOKE_STATIC_INIT, 5
+            );
+
     @Override
     public void configure(Map<String, String> config) {
         String mainArgument = config.get(NAME);
 
         // Enable by default
         boolean bmhEnabled = true;
+        boolean dmhEnabled = true;
         if (mainArgument != null) {
-            Set<String> args = Arrays.stream(mainArgument.split(","))
-                    .collect(Collectors.toSet());
+            List<String> args = Arrays.asList(mainArgument.split(","));
             if (!args.contains(BMH_PARAM)) {
                 bmhEnabled = false;
             }
+            if (!args.contains(DMH_PARAM)) {
+                dmhEnabled = false;
+            }
         }
 
         if (!bmhEnabled) {
@@ -132,40 +177,63 @@
             speciesTypes = bmhSpecies.stream()
                     .map(type -> expandSignature(type))
                     .collect(Collectors.toList());
+        }
 
-            // Validation check
-            for (String type : speciesTypes) {
-                for (char c : type.toCharArray()) {
-                    if ("LIJFD".indexOf(c) < 0) {
-                        throw new PluginException("All characters must "
-                                + "correspond to a basic field type: LIJFD");
+        // DirectMethodHandles
+        if (!dmhEnabled) {
+            dmhMethods = Map.of();
+        } else {
+            dmhMethods = new HashMap<>();
+            for (String dmhParam : DMH_METHOD_TYPE_MAP.keySet()) {
+                String args = config.get(dmhParam);
+                if (args != null && !args.isEmpty()) {
+                    List<String> dmhMethodTypes = Arrays.stream(args.split(","))
+                            .map(String::trim)
+                            .filter(s -> !s.isEmpty())
+                            .collect(Collectors.toList());
+                    dmhMethods.put(dmhParam, dmhMethodTypes);
+                    // Validation check
+                    for (String type : dmhMethodTypes) {
+                        String[] typeParts = type.split("_");
+                        // check return type (second part)
+                        if (typeParts.length != 2 || typeParts[1].length() != 1
+                                || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) {
+                            throw new PluginException(
+                                    "Method type signature must be of form [LJIFD]*_[LJIFDV]");
+                        }
+                        // expand and check arguments (first part)
+                        expandSignature(typeParts[0]);
                     }
                 }
             }
+            if (dmhMethods.isEmpty()) {
+                dmhMethods = defaultDMHMethods();
+            }
+        }
+    }
+
+    private static void requireBasicType(char c) {
+        if ("LIJFD".indexOf(c) < 0) {
+            throw new PluginException(
+                    "Character " + c + " must correspond to a basic field type: LIJFD");
         }
     }
 
     @Override
     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
-        in.entries().forEach(data -> {
-            if (("/java.base/" + BMH + ".class").equals(data.path())) {
-                // Add BoundMethodHandle unchanged
-                out.add(data);
-                speciesTypes.forEach(types -> generateConcreteClass(types, data, out));
-            } else {
-                out.add(data);
-            }
-        });
-
+        // Copy all but DMH_ENTRY to out
+        in.transformAndCopy(entry -> entry.path().equals(DMH_ENTRY) ? null : entry, out);
+        speciesTypes.forEach(types -> generateBMHClass(types, out));
+        generateDMHClass(out);
         return out.build();
     }
 
     @SuppressWarnings("unchecked")
-    private void generateConcreteClass(String types, ResourcePoolEntry data, ResourcePoolBuilder out) {
+    private void generateBMHClass(String types, ResourcePoolBuilder out) {
         try {
             // Generate class
             Map.Entry<String, byte[]> result = (Map.Entry<String, byte[]>)
-                    FACTORY_METHOD.invoke(null, types);
+                    BMH_FACTORY_METHOD.invoke(null, types);
             String className = result.getKey();
             byte[] bytes = result.getValue();
 
@@ -179,13 +247,47 @@
         }
     }
 
+    private void generateDMHClass(ResourcePoolBuilder out) {
+        int count = 0;
+        for (List<String> entry : dmhMethods.values()) {
+            count += entry.size();
+        }
+        MethodType[] methodTypes = new MethodType[count];
+        int[] dmhTypes = new int[count];
+        int index = 0;
+        for (Map.Entry<String, List<String>> entry : dmhMethods.entrySet()) {
+            String dmhType = entry.getKey();
+            for (String type : entry.getValue()) {
+                methodTypes[index] = asMethodType(type);
+                dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType);
+                index++;
+            }
+        }
+        try {
+            byte[] bytes = (byte[])DMH_FACTORY_METHOD
+                    .invoke(null,
+                            DMH,
+                            methodTypes,
+                            dmhTypes);
+            ResourcePoolEntry ndata = ResourcePoolEntry.create(DMH_ENTRY, bytes);
+            out.add(ndata);
+        } catch (Exception ex) {
+            throw new PluginException(ex);
+        }
+    }
+    private static final String DMH_ENTRY = "/java.base/" + DMH + ".class";
+
     static {
         try {
             Class<?> BMHFactory = Class.forName("java.lang.invoke.BoundMethodHandle$Factory");
-            Method genClassMethod = BMHFactory.getDeclaredMethod("generateConcreteBMHClassBytes",
+            BMH_FACTORY_METHOD = BMHFactory.getDeclaredMethod("generateConcreteBMHClassBytes",
                     String.class);
-            genClassMethod.setAccessible(true);
-            FACTORY_METHOD = genClassMethod;
+            BMH_FACTORY_METHOD.setAccessible(true);
+
+            Class<?> DMHFactory = Class.forName("java.lang.invoke.DirectMethodHandle");
+            DMH_FACTORY_METHOD = DMHFactory.getDeclaredMethod("generateDMHClassBytes",
+                    String.class, MethodType[].class, int[].class);
+            DMH_FACTORY_METHOD.setAccessible(true);
         } catch (Exception e) {
             throw new PluginException(e);
         }
@@ -202,6 +304,7 @@
                 count *= 10;
                 count += (c - '0');
             } else {
+                requireBasicType(c);
                 for (int j = 1; j < count; j++) {
                     sb.append(last);
                 }
@@ -210,9 +313,52 @@
                 count = 0;
             }
         }
-        for (int j = 1; j < count; j++) {
-            sb.append(last);
+
+        // ended with a number, e.g., "L2": append last char count - 1 times
+        if (count > 1) {
+            requireBasicType(last);
+            for (int j = 1; j < count; j++) {
+                sb.append(last);
+            }
         }
         return sb.toString();
     }
+
+    private static MethodType asMethodType(String basicSignatureString) {
+        String[] parts = basicSignatureString.split("_");
+        assert(parts.length == 2);
+        assert(parts[1].length() == 1);
+        String parameters = expandSignature(parts[0]);
+        Class<?> rtype = primitiveType(parts[1].charAt(0));
+        Class<?>[] ptypes = new Class<?>[parameters.length()];
+        for (int i = 0; i < ptypes.length; i++) {
+            ptypes[i] = primitiveType(parameters.charAt(i));
+        }
+        return MethodType.methodType(rtype, ptypes);
+    }
+
+    private static Class<?> primitiveType(char c) {
+        switch (c) {
+            case 'F':
+                return float.class;
+            case 'D':
+                return double.class;
+            case 'I':
+                return int.class;
+            case 'L':
+                return Object.class;
+            case 'J':
+                return long.class;
+            case 'V':
+                return void.class;
+            case 'Z':
+            case 'B':
+            case 'S':
+            case 'C':
+                throw new IllegalArgumentException("Not a valid primitive: " + c +
+                        " (use I instead)");
+            default:
+                throw new IllegalArgumentException("Not a primitive: " + c);
+        }
+    }
 }