changeset 49:d99854a91f8d

remove dependency on language support; use Indify instead
author jrose
date Sat, 20 Nov 2010 01:03:38 -0800
parents 8d4143a1a4fa
children 3ac10f0fff3e
files netbeans/indy-demo/build.xml netbeans/indy-demo/nbproject/project.properties netbeans/indy-demo/src/FidgetDemo.java netbeans/indy-demo/src/GetNameDemo.java netbeans/indy-demo/src/Hello.java netbeans/indy-demo/src/Main.java netbeans/indy-demo/src/PrintArgsDemo.java netbeans/indy-demo/src/ShouldNotWork.java netbeans/indy-demo/src/VMILDemos.java netbeans/indy-demo/src/indify/Indify.java
diffstat 10 files changed, 1818 insertions(+), 41 deletions(-) [+]
line wrap: on
line diff
--- a/netbeans/indy-demo/build.xml	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/build.xml	Sat Nov 20 01:03:38 2010 -0800
@@ -7,6 +7,8 @@
 
     <import file="nbproject/build-impl.xml"/>
 
+    <property name="excludes" value="JavaDocExamples.java,recipes/*"/>
+
     <target name="-pre-compile">
         <echo>Relevant attributes and properties to javac:
     srcdir:         ${src.dir}
@@ -30,9 +32,31 @@
         </echo>
     </target>
 
+    <target name="-post-compile" unless="do.not.rewrite.bytecodes">
+        <delete dir="${build.indified-classes.dir}"/>
+        <java classname="indify.Indify" fork="true" failonerror="true" jvm="${platform.java}">
+            <classpath path="${build.classes.dir}"/>
+            <arg line="--quiet"/>
+            <arg line="--dest ${build.indified-classes.dir}"/>
+            <arg value="${build.classes.dir}"/>
+        </java>
+    </target>
+
     <target name="-post-jar">
         <delete file="dist/README.TXT"/>
 	<copy file="README.txt" todir="dist"/>
 	<echo>To run this project, you will need the JVM option -XX:+UnlockExperimentalVMOptions -XX:+EnableInvokeDynamic</echo>
     </target>
+
+    <target depends="init,compile" description="Run the main class under Indify.runApplication." name="run-under-indify"
+            xmlns:j2seproject1="http://www.netbeans.org/ns/j2se-project/1"
+            >
+        <j2seproject1:java classname="indify.Indify">
+            <customize>
+                <arg line="--quiet"/>
+                <arg line="--classpath ${build.classes.dir}"/>
+                <arg line="--java ${main.class} ${application.args}"/>
+            </customize>
+        </j2seproject1:java>
+    </target>
 </project>
--- a/netbeans/indy-demo/nbproject/project.properties	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/nbproject/project.properties	Sat Nov 20 01:03:38 2010 -0800
@@ -1,6 +1,7 @@
 application.title=InvokeDynamicDemo
 application.vendor=jrose
 build.classes.dir=${build.dir}/classes
+build.indified-classes.dir=${build.dir}/indified-classes
 build.classes.excludes=**/*.java,**/*.form
 # This directory is removed when the project is cleaned:
 build.dir=build
@@ -27,8 +28,8 @@
     ${reference.meth.jar}
 # Space-separated list of extra javac options
 javac.compilerargs=-XDinvokedynamic -Xlint:none -XDallowTransitionalJSR292=${allowTransitionalJSR292} ${javac.standalone.bcp} -Xbootclasspath/p:${reference.meth.jar}
-# choices: yes, no, only
-allowTransitionalJSR292=only
+# choices: yes, no
+allowTransitionalJSR292=no
 # assuming full JDK_7X:
 #javac.standalone.bcp =
 # pulling from a standalone langtools build:
@@ -63,6 +64,7 @@
 reference.langtools-javac.jar=${file.reference.davinci.sources.langtools}/dist/lib/javac.jar
 run.classpath=\
     ${javac.classpath}:\
+    ${build.indified-classes.dir}:\
     ${build.classes.dir}
 # Space-separated list of JVM arguments used when running the project
 # (you may also define separate properties like run-sys-prop.name=value instead of -Dname=value
--- a/netbeans/indy-demo/src/FidgetDemo.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/FidgetDemo.java	Sat Nov 20 01:03:38 2010 -0800
@@ -40,8 +40,7 @@
         for (int i = 0; i < 6; i++) {
             System.out.println("--- loop #"+i);
             for (String a : av) {
-                @BootstrapMethod(value=FidgetDemo.class, name="linkDynamic")
-                String result = (String) InvokeDynamic.<String>fidget(a);
+                String result = (String) INDY_fidget().invokeExact(a);
                 System.out.println(result);
             }
         }
@@ -93,7 +92,7 @@
     private static class CSite extends CallSite {
         int count;
         public CSite(Class caller, String name, MethodType type) throws NoAccessException {
-            //?? super(String.class, "gloat", make(void.class));
+            super(type);
             System.out.println("[link] new call site: "+this);
             setTarget(new Guard(this, which(0)).asMethodHandle());
         }
@@ -106,10 +105,23 @@
 
     // Set up a class-local bootstrap method.
     //static { Linkage.registerBootstrapMethod("linkDynamic"); }
-    private static CallSite linkDynamic(Class caller, String name, MethodType type) throws NoAccessException {
+    private static CallSite linkDynamic(Lookup caller, String name, MethodType type) throws NoAccessException {
         assert((Object)name == "fidget" &&
                 type == methodType(String.class, String.class));
-        return new CSite(caller, name, type);
+        return new CSite(caller.lookupClass(), name, type);
+    }
+
+    // names recognized by Indify:
+    private static MethodType MT_bsm() {
+        return methodType(CallSite.class, Lookup.class, String.class, MethodType.class);
+    }
+    private static MethodHandle INDY_fidget;
+    private static MethodHandle INDY_fidget() throws Throwable {
+        if (INDY_fidget != null)  return INDY_fidget;
+        System.out.println("*** INDY_fidget: running BSM manually");
+        CallSite site = (CallSite) lookup().findStatic(FidgetDemo.class, "linkDynamic", MT_bsm())
+            .invokeGeneric(lookup(), "fidget", methodType(String.class, String.class));
+        return INDY_fidget = site.dynamicInvoker();
     }
 }
 
--- a/netbeans/indy-demo/src/GetNameDemo.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/GetNameDemo.java	Sat Nov 20 01:03:38 2010 -0800
@@ -23,8 +23,9 @@
  */
 
 import java.dyn.*;
+import static java.dyn.MethodHandles.*;
+import static java.dyn.MethodType.*;
 
-@BootstrapMethod(value=GetNameDemo.class, name="bootstrapDynamic")
 class GetNameDemo {
     public static void main(String... av) throws Throwable {
         if (av.length == 0)  av = new String[]{ "fred.txt" };
@@ -33,19 +34,48 @@
             System.out.println("--- loop #"+i);
             String result1 = file.getName();
             System.out.println(result1);
-            String result2 = (String) InvokeDynamic.<String>getName(file);
+            String result2 = (String) INDY_getName().invokeExact(file);
             System.out.println(result2);
-            String result3 = (String) InvokeDynamic.<String>toString((CharSequence) result1);
+            String result3 = (String) INDY_toString().invokeExact((CharSequence) result1);
             System.out.println(result3);
-            String result4 = (String) InvokeDynamic.<String>#"static:\=java\|lang\|Integer:toHexString"(result1.length());
+            String result4 = (String) INDY_toHexString().invokeExact(result1.length());
             System.out.println(result4);
         }
     }
 
+    private static final MethodHandle[] INDY_CACHES = new MethodHandle[3];  // INDY_{getName,toString,toHexString}
+    private static MethodHandle INDY_getName() throws Throwable {
+        if (INDY_CACHES[0] != null)  return INDY_CACHES[0];
+        return INDY_CACHES[0] =
+            ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                                "getName", methodType(String.class, java.io.File.class)
+                                                )).dynamicInvoker();
+    }
+    private static MethodHandle INDY_toString() throws Throwable {
+        if (INDY_CACHES[1] != null)  return INDY_CACHES[1];
+        return INDY_CACHES[1] =
+            ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                               "toString", methodType(String.class, CharSequence.class)
+                                               )).dynamicInvoker();
+    }
+    private static MethodHandle INDY_toHexString() throws Throwable {
+        if (INDY_CACHES[2] != null)  return INDY_CACHES[2];
+        return INDY_CACHES[2] =
+            ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                               "static:\\=java\\|lang\\|Integer:toHexString", methodType(String.class, int.class)
+                                               )).dynamicInvoker();
+    }
+    private static MethodType MT_bsm() {
+        return methodType(CallSite.class, Lookup.class, String.class, MethodType.class);
+    }
+    private static MethodHandle MH_bsm() throws ReflectiveOperationException {
+        return lookup().findStatic(lookup().lookupClass(), "bootstrapDynamic", MT_bsm());
+    }
+
     // Linkage logic for all invokedynamic call sites in this class.
-    private static MethodHandle linkDynamic(Class caller, String name, MethodType type) throws NoAccessException {
+    private static MethodHandle linkDynamic(Lookup lookup, String name, MethodType type) throws NoAccessException {
+        Class<?> caller = lookup.lookupClass();
         System.out.println("[link] linkDynamic "+caller+" "+name+type);
-        MethodHandles.Lookup lookup = MethodHandles.lookup();
         MethodHandle target = null;  // result of call linkage
         Class<?>     recvType = null;  // receiver (1st arg) type if any
         if (type.parameterCount() >= 1) {
@@ -79,7 +109,7 @@
 
     // Set up a class-local bootstrap method.
     //static { Linkage.registerBootstrapMethod("bootstrapDynamic"); }
-    private static CallSite bootstrapDynamic(Class caller, String name, MethodType type) throws NoAccessException {
+    private static CallSite bootstrapDynamic(Lookup caller, String name, MethodType type) throws NoAccessException {
         MethodHandle target = linkDynamic(caller, name, type);
         System.out.println("[link] set site target to "+target);
         if (!target.type().equals(type)) {
--- a/netbeans/indy-demo/src/Hello.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/Hello.java	Sat Nov 20 01:03:38 2010 -0800
@@ -22,17 +22,17 @@
  */
 
 import java.dyn.*;
+import java.dyn.MethodHandles.Lookup;
 
-@BootstrapMethod(value=Hello.class, name="bootstrapDynamic")
 public class Hello {
     public static void main(String... av) throws Throwable {
         if (av.length == 0)  av = new String[] { "world" };
         greeter(av[0] + " (from a statically linked call site)");
         for (String whom : av) {
-            greeter.<void>invokeExact(whom);  // strongly typed direct call
+            greeter.invokeExact(whom);  // strongly typed direct call
             // previous line generates invokevirtual MethodHandle.invokeExact(String)void
             Object x = whom;
-            x = InvokeDynamic.hail(x);      // weakly typed invokedynamic
+            x = INDY_hail().invokeExact(x);      // weakly typed invokedynamic
             // previous line generates invokedynamic hail(Object)Object
         }
     }
@@ -49,10 +49,25 @@
     }
 
     // Set up a class-local bootstrap method.
-    //static { Linkage.registerBootstrapMethod("bootstrapDynamic"); }
-    private static CallSite bootstrapDynamic(Class caller, String name, MethodType type) {
+    private static CallSite bootstrapDynamic(Lookup caller, String name, MethodType type) {
         assert(type.parameterCount() == 1 && name.equals("hail"));  // in lieu of MOP
         System.out.println("set target to adapt "+greeter);
         return new CallSite(greeter.asType(type));
     }
+
+    private static MethodType MT_bsm() {
+        return MethodType.methodType(CallSite.class, Lookup.class, String.class, MethodType.class);
+    }
+    private static MethodHandle MH_bsm() throws ReflectiveOperationException {
+        return MethodHandles.lookup().findStatic(MethodHandles.lookup().lookupClass(), "bootstrapDynamic", MT_bsm());
+    }
+
+    private static MethodHandle INDY_hail;
+    private static MethodHandle INDY_hail() throws Throwable {
+        if (INDY_hail != null)  return INDY_hail;
+        System.out.println("*** INDY_hail: running BSM manually");
+        CallSite site = (CallSite) MH_bsm()
+            .invokeGeneric(MethodHandles.lookup(), "hail", MethodType.methodType(Object.class, Object.class));
+        return INDY_hail = site.dynamicInvoker();
+    }
 }
--- a/netbeans/indy-demo/src/Main.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/Main.java	Sat Nov 20 01:03:38 2010 -0800
@@ -28,22 +28,23 @@
 
 public class Main {
     static final Class[] CLASSES = {
+        // Commented classes have not yet been converted to Indify format.
         Hello.class,
         PrintArgsDemo.class,
-        JavaDocExamples.class,
+        //JavaDocExamples.class,
         GetNameDemo.class,
         FidgetDemo.class,
-        recipes.PlainOldJava.class,
-        recipes.FastAndSlow.class,
-        recipes.InlineCache.class,
-        recipes.Curry.class
+        //recipes.PlainOldJava.class,
+        //recipes.FastAndSlow.class,
+        //recipes.InlineCache.class,
+        //recipes.Curry.class
     };
     public static void doClasses(Class[] classes, String[] args) throws Throwable {
         for (Class c : classes) {
             MethodHandle main = lookup().findStatic(c, "main",
                     methodType(void.class, String[].class));
             System.out.println("******** "+c.getName()+"."+main);
-            main.<void>invokeExact(args);
+            main.invokeExact(args);
         }
     }
     public static void main(String[] args) throws Throwable {
--- a/netbeans/indy-demo/src/PrintArgsDemo.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/PrintArgsDemo.java	Sat Nov 20 01:03:38 2010 -0800
@@ -22,22 +22,22 @@
  */
 
 import java.dyn.*;
+import static java.dyn.MethodHandles.*;
+import static java.dyn.MethodType.*;
 
 class PrintArgsDemo {
-    @BootstrapMethod(value=PrintArgsDemo.class, name="bootstrapDynamic")
     public static void main(String... av) throws Throwable {
         System.out.println("Printing some argument lists, starting with a empty one:");
-        InvokeDynamic.<void>nothing();
-        InvokeDynamic.<void>foo("foo arg");
-        InvokeDynamic.<void>bar("bar arg", 1);
-        test(); //InvokeDynamic.<void>baz("baz arg", 2, 3.14);
+        INDY_nothing().invokeExact();
+        INDY_foo().invokeExact("foo arg");
+        INDY_bar().invokeExact("bar arg", 1);
+        test(); //INDY_baz.invokeExact("baz arg", 2, 3.14);
         System.out.println("Done printing argument lists.");
     }
 
     //// BEGIN JAVADOC EXAMPLE
-@BootstrapMethod(value=PrintArgsDemo.class, name="bootstrapDynamic")
 static void test() throws Throwable {
-    InvokeDynamic.<void>baz("baz arg", 2, 3.14);
+    INDY_baz().invokeExact("baz arg", 2, 3.14);
 }
 private static void printArgs(Object... args) {
   System.out.println(java.util.Arrays.deepToString(args));
@@ -51,9 +51,36 @@
           "printArgs", MethodType.methodType(void.class, Object[].class));
   } catch (NoAccessException ex) { throw new RuntimeException(ex); }
 }
-private static CallSite bootstrapDynamic(Class caller, String name, MethodType type) {
+private static CallSite bootstrapDynamic(Lookup caller, String name, MethodType type) {
   // ignore caller and name, but match the type:
   return new CallSite(MethodHandles.collectArguments(printArgs, type));
 }
     //// END JAVADOC EXAMPLE
+
+    private static MethodHandle INDY_nothing() throws Throwable {
+        return ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                                  "nothing", methodType(void.class)
+                                                  )).dynamicInvoker();
+    }
+    private static MethodHandle INDY_foo() throws Throwable {
+        return ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                                  "foo", methodType(void.class, String.class)
+                                                  )).dynamicInvoker();
+    }
+    private static MethodHandle INDY_bar() throws Throwable {
+        return ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                                  "bar", methodType(void.class, String.class, int.class)
+                                                  )).dynamicInvoker();
+    }
+    private static MethodHandle INDY_baz() throws Throwable {
+        return ((CallSite) MH_bsm().invokeGeneric(lookup(),
+                                                  "baz", methodType(void.class, String.class, int.class, double.class)
+                                                  )).dynamicInvoker();
+    }
+    private static MethodType MT_bsm() {
+        return methodType(CallSite.class, Lookup.class, String.class, MethodType.class);
+    }
+    private static MethodHandle MH_bsm() throws ReflectiveOperationException {
+        return lookup().findStatic(lookup().lookupClass(), "bootstrapDynamic", MT_bsm());
+    }
 }
--- a/netbeans/indy-demo/src/ShouldNotWork.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/ShouldNotWork.java	Sat Nov 20 01:03:38 2010 -0800
@@ -49,9 +49,9 @@
         Sub(String x) throws Throwable {
             super(x);
             System.out.println("invoking "+SPECIAL1);
-            SPECIAL1.<void>invokeExact((Super)this, x+" via method handle");
+            SPECIAL1.invokeExact((Super)this, x+" via method handle");
             System.out.println("should fail:  invoking "+SPECIAL2);
-            SPECIAL2.<void>invokeExact(this);
+            SPECIAL2.invokeExact(this);
         }
         @Override
         void foo(String x) {
--- a/netbeans/indy-demo/src/VMILDemos.java	Wed Nov 17 19:31:06 2010 -0800
+++ b/netbeans/indy-demo/src/VMILDemos.java	Sat Nov 20 01:03:38 2010 -0800
@@ -37,7 +37,7 @@
     .findVirtual(PrintStream.class,
       "println", MethodType.methodType(
       void.class, String.class));
-println.<void>invokeExact(
+println.invokeExact(
     System.out, "hello, world");
 //Figure 3. Creating and using a direct method handle.
 {;;;}
@@ -57,7 +57,7 @@
 int pos = 0;  // receiver in leading position
 MethodHandle println2out = MethodHandles
    .insertArguments(println, pos, System.out);
-println2out.<void>invokeExact("hello, world");
+println2out.invokeExact("hello, world");
 //Figure 6. Creating and using a bound method handle.
 {;;;}
     }
@@ -79,7 +79,7 @@
     } };
 }
 {;;;} }; new C().
-greet("hello").<void>invokeExact("world");
+greet("hello").invokeExact("world");
 // prints "hello, world"
     */
 //Figure 8. Anonymous class specifying a method handle.
@@ -159,9 +159,9 @@
         }
         static void test() throws Throwable {
             CatBox box = new CatBox(123);
-            System.out.println("thing1 = "+(int)box.getter().<int>invokeExact());
-            box.setter().<void>invokeExact(456);
-            System.out.println("thing2 = "+(int)box.getter().<int>invokeExact());
+            System.out.println("thing1 = "+(int)box.getter().invokeExact());
+            box.setter().invokeExact(456);
+            System.out.println("thing2 = "+(int)box.getter().invokeExact());
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/netbeans/indy-demo/src/indify/Indify.java	Sat Nov 20 01:03:38 2010 -0800
@@ -0,0 +1,1666 @@
+/*
+ * Copyright (c) 2010, 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.
+ */
+
+package indify;
+
+import java.util.*;
+import java.io.*;
+import java.lang.reflect.Modifier;
+import java.util.regex.*;
+
+/**
+ * Transform one or more class files to incorporate JSR 292 features,
+ * such as {@code invokedynamic}.
+ * <p>
+ * Static private methods named MH_x and MT_x (where x is arbitrary)
+ * must be stereotyped generators of MethodHandle and MethodType
+ * constants.  All calls to them are transformed to {@code CONSTANT_MethodHandle}
+ * and {@code CONSTANT_MethodType} "ldc" instructions.
+ * The stereotyped code must create method types by calls to {@code methodType} or
+ * {@code fromMethodDescriptorString}.  The "lookup" argument must be created
+ * by calls to {@code java.dyn.MethodHandles#lookup MethodHandles.lookup}.
+ * The class and string arguments must be constant.
+ * The following methods of {@code java.dyn.MethodHandle.Lookup Lookup} are
+ * allowed for method handle creation: {@code findStatic}, {@code findVirtual},
+ * {@code findConstructor}, {@code findSpecial},
+ * {@code findGetter}, {@code findSetter},
+ * {@code findStaticGetter}, or {@code findStaticSetter}.
+ * The call to one of these methods must be followed immediately
+ * by an {@code areturn} instruction.
+ * The net result of the call to the MH_x or MT_x method must be
+ * the creation of a constant method handle.  Thus, replacing calls
+ * to MH_x or MT_x methods by {@code ldc} instructions should leave
+ * the meaning of the program unchanged.
+ * <p>
+ * Static private methods named INDY_x must be stereotyped generators
+ * of {@code invokedynamic} call sites.
+ * All calls to them must be immediately followed by
+ * {@code invokeExact} calls. 
+ * All such pairs of calls are transformed to {@code invokedynamic}
+ * instructions.  Each INDY_x method must begin with a call to a
+ * MH_x method, which is taken to be its bootstrap method.
+ * The method must be immediately invoked (via {@code invokeGeneric}
+ * on constant lookup, name, and type arguments.  An object array of
+ * constants may also be appended to the {@code invokeGeneric call}.
+ * This call must be cast to {@code CallSite}, and the result must be
+ * immediately followed by a call to {@code dynamicInvoker}, with the
+ * resulting method handle returned.
+ * <p>
+ * The net result of all of these actions is equivalent to the JVM's
+ * execution of an {@code invokedynamic} instruction in the unlinked state.
+ * Running this code once should produce the same results as running
+ * the corresponding {@code invokedynamic} instruction.
+ * In order to model the caching behavior, the code of an INDY_x
+ * method is allowed to begin with getstatic, aaload, and if_acmpne
+ * instructions which load a static method handle value and return it
+ * if the value is non-null.
+ * <p>
+ * Example usage:
+ * <blockquote><pre>
+$ JAVA_HOME=(some recent OpenJDK 7 build)
+$ ant
+$ $JAVA_HOME/bin/java -cp build/classes indify.Indify --overwrite --dest build/testout build/classes/indify/Example.class
+$ $JAVA_HOME/bin/java -XX:+UnlockExperimentalVMOptions -XX:+EnableInvokeDynamic -cp build/classes indify.Example
+MT = (java.lang.Object)java.lang.Object
+MH = adder(int,int)java.lang.Integer
+adder(1,2) = 3
+calling indy:  42
+$ $JAVA_HOME/bin/java -XX:+UnlockExperimentalVMOptions -XX:+EnableInvokeDynamic -cp build/testout indify.Example
+(same output as above)
+ * </pre></blockquote>
+ * <p>
+ * Until the format of {@code CONSTANT_InvokeDynamic} entries is finalized,
+ * the {@code --transitionalJSR292} switch is recommended (and turned on by default).
+ * <p>
+ * Note on packaging and reuse:  This is a standalone single-source program.
+ * In this form, it may be useful for test harnesses, small experiments, and javadoc examples.
+ * A version of this transformation built on top of <a href="http://asm.ow2.org/">http://asm.ow2.org/</a> would be welcome.
+ * @author John Rose
+ */
+public class Indify {
+    public static void main(String... av) throws IOException {
+        new Indify().run(av);
+    }
+
+    public File dest;
+    public String[] classpath = {"."};
+    public boolean keepgoing = false;
+    public boolean overwrite = false;
+    public boolean quiet = false;
+    public boolean verbose = false;
+    public boolean transitionalJSR292 = true;
+    public boolean all = false;
+
+    public void run(String... av) throws IOException {
+        List<String> avl = new ArrayList<>(Arrays.asList(av));
+        parseOptions(avl);
+        if (avl.isEmpty())
+            throw new RuntimeException("Usage: indify [--dest dir] [option...] file...");
+        if ("--java".equals(avl.get(0))) {
+            avl.remove(0);
+            try {
+                runApplication(avl.toArray(new String[0]));
+            } catch (Exception ex) {
+                throw new RuntimeException(ex);
+            }
+            return;
+        }
+        Exception err = null;
+        for (String a : avl) {
+            try {
+                indify(a);
+            } catch (Exception ex) {
+                if (err == null)  err = ex;
+                System.err.println("failure on "+a);
+                if (!keepgoing)  break;
+            }
+        }
+        if (err != null) {
+            if (err instanceof IOException)  throw (IOException) err;
+            throw (RuntimeException) err;
+        }
+    }
+
+    /** Execute the given application under a class loader which indifies all application classes. */
+    public void runApplication(String... av) throws Exception {
+        List<String> avl = new ArrayList<>(Arrays.asList(av));
+        String mainClassName = avl.remove(0);
+        av = avl.toArray(new String[0]);
+        Class<?> mainClass = Class.forName(mainClassName, true, makeClassLoader());
+        java.lang.reflect.Method main = mainClass.getMethod("main", String[].class);
+        main.invoke(null, (Object) av);
+    }
+
+    public void parseOptions(List<String> av) throws IOException {
+        for (; !av.isEmpty(); av.remove(0)) {
+            String a = av.get(0);
+            if (a.startsWith("-")) {
+                String a2 = a.substring(a.indexOf('=')+1);
+                if (a.equals("--java"))
+                    return;
+                else if (a.startsWith("--dest=") || a.startsWith("-d="))
+                    dest = new File(a2);
+                else if (a.equals("--dest") || a.equals("-d"))
+                    dest = new File(av.remove(1));
+                else if (a.equals("--classpath") || a.equals("-cp"))
+                    classpath = av.remove(1).split("["+File.pathSeparatorChar+"]");
+                else if (a.equals("-k") || a.equals("--keepgoing"))
+                    keepgoing = true;  // print errors but keep going
+                else if (a.equals("--overwrite"))
+                    overwrite = true;  // overwrite output files
+                else if (a.equals("--all"))
+                    all = true;  // copy all classes, even if no patterns
+                else if (a.equals("--quiet") || a.equals("-q"))
+                    quiet = true;  // less output
+                else if (a.equals("--verbose"))
+                    verbose = true;  // more output
+                else if (a.equals("--transitionalJSR292") ||
+                         a.equals("--transitionalJSR292=yes"))
+                    transitionalJSR292 = true;  // use older invokedynamic format
+                else if (a.equals("--transitionalJSR292=no"))
+                    transitionalJSR292 = false;  // do not use older invokedynamic format
+                else
+                    throw new RuntimeException("unrecognized flag: "+a);
+                continue;
+            } else {
+                break;
+            }
+        }
+        if (dest == null && !overwrite)
+            throw new RuntimeException("no output specified; need --dest d or --overwrite");
+    }
+
+    public void indify(String a) throws IOException {
+        File f = new File(a);
+        String fn = f.getName();
+        if (fn.endsWith(".class") && f.isFile())
+            indifyFile(f, dest);
+        else if (fn.endsWith(".jar") && f.isFile())
+            indifyJar(f, dest);
+        else if (f.isDirectory())
+            indifyTree(f, dest);
+        else if (!keepgoing)
+            throw new RuntimeException("unrecognized file: "+a);
+    }
+    
+    private void ensureDirectory(File dir) {
+        if (dir.mkdirs() && !quiet)
+            System.err.println("created "+dir);
+    }
+
+    public void indifyFile(File f, File dest) throws IOException {
+        if (verbose)  System.err.println("reading "+f);
+        ClassFile cf = new ClassFile(f);
+        Logic logic = new Logic(cf);
+        boolean changed = logic.transform();
+        if (!quiet)  logic.reportPatternMethods();
+        if (changed || all) {
+            File outfile;
+            if (dest != null) {
+                ensureDirectory(dest);
+                outfile = classPathFile(dest, cf.classname());
+            } else {
+                outfile = f;  // overwrite input file, no matter where it is
+            }
+            cf.writeTo(outfile);
+            if (!quiet)  System.err.println("wrote "+outfile);
+        }
+    }
+
+    File classPathFile(File pathDir, String className) {
+        String qualname = className+".class";
+        qualname = qualname.replace('/', File.separatorChar);
+        return new File(pathDir, qualname);
+    }
+
+    public void indifyJar(File f, Object dest) throws IOException {
+        throw new UnsupportedOperationException("Not yet implemented");
+    }
+
+    public void indifyTree(File f, File dest) throws IOException {
+        if (verbose)  System.err.println("reading directory: "+f);
+        for (File f2 : f.listFiles(new FilenameFilter() {
+                public boolean accept(File dir, String name) {
+                    if (name.endsWith(".class"))  return true;
+                    if (name.contains("."))  return false;
+                    // return true if it might be a package name:
+                    return Character.isJavaIdentifierStart(name.charAt(0));
+                }})) {
+            if (f2.getName().endsWith(".class"))
+                indifyFile(f2, dest);
+            else if (f2.isDirectory())
+                indifyTree(f2, dest);
+        }
+    }
+
+    public ClassLoader makeClassLoader() {
+        return new Loader();
+    }
+    private class Loader extends ClassLoader {
+        Loader() {
+            this(Indify.class.getClassLoader());
+        }
+        Loader(ClassLoader parent) {
+            super(parent);
+        }
+        public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+            File f = findClassInPath(name);
+            if (f != null) {
+                try {
+                    Class<?> c = transformAndLoadClass(f);
+                    if (c != null) {
+                        if (resolve)  resolveClass(c);
+                        return c;
+                    }
+                } catch (Exception ex) {
+                }
+            }
+            return super.loadClass(name, resolve);
+        }
+        private File findClassInPath(String name) {
+            for (String s : classpath) {
+                File f = classPathFile(new File(s), name);
+                if (f.exists() && f.canRead()) {
+                    return f;
+                }
+            }
+            return null;
+        }
+        protected Class<?> findClass(String name) throws ClassNotFoundException {
+            try {
+                return transformAndLoadClass(findClassInPath(name));
+            } catch (IOException ex) {
+                throw new ClassNotFoundException("IO error", ex);
+            }
+        }
+        private Class<?> transformAndLoadClass(File f) throws ClassNotFoundException, IOException {
+            if (verbose)  System.out.println("Loading class from "+f);
+            ClassFile cf = new ClassFile(f);
+            Logic logic = new Logic(cf);
+            boolean changed = logic.transform();
+            if (verbose && !changed)  System.out.println("(no change)");
+            if (verbose)  logic.reportPatternMethods();
+            byte[] bytes = cf.toByteArray();
+            return defineClass(null, bytes, 0, bytes.length);
+        }
+    }
+
+    private class Logic {
+        // Indify logic, per se.
+        ClassFile cf;
+        final char[] poolMarks;
+        final Map<Method,Constant> constants = new HashMap<>();
+        final Map<Method,String> indySignatures = new HashMap<>();
+        Logic(ClassFile cf) {
+            this.cf = cf;
+            poolMarks = new char[cf.pool.size()];
+        }
+        boolean transform() {
+            if (!initializeMarks())  return false;
+            if (!findPatternMethods())  return false;
+            Pool pool = cf.pool;
+            //for (Constant c : cp)  System.out.println("  # "+c);
+            for (Method m : cf.methods) {
+                // Transform references.
+                int blab = 0;
+                for (Instruction i = m.instructions(); i != null; i = i.next()) {
+                    if (i.bc != opc_invokestatic)  continue;
+                    int methi = i.u2At(1);
+                    if (poolMarks[methi] == 0)  continue;
+                    Short[] ref = pool.getMemberRef((short)methi);
+                    Method conm = findMember(cf.methods, ref[1], ref[2]);
+                    if (conm == null)  continue;
+                    Constant con = constants.get(conm);
+                    if (con == null)  continue;
+                    if (blab++ == 0 && !quiet)
+                        System.err.println("patching "+cf.classname()+"."+m);
+                    //if (blab == 1) { for (Instruction j = m.instructions(); j != null; j = j.next()) System.out.println("  |"+j); }
+                    if (con.tag == CONSTANT_InvokeDynamic ||
+                        con.tag == CONSTANT_InvokeDynamic_17) {
+                        // need to patch the following instruction too,
+                        // but there are usually intervening argument pushes too
+                        Instruction i2 = findPop(i);
+                        Short[] ref2 = null;
+                        short ref2i = 0;
+                        if (i2 != null && i2.bc == opc_invokevirtual &&
+                                poolMarks[(char)(ref2i = (short) i2.u2At(1))] == 'D')
+                            ref2 = pool.getMemberRef(ref2i);
+                        if (ref2 == null || !"invokeExact".equals(pool.getString(ref2[1]))) {
+                            System.err.println(m+": failed to create invokedynamic at "+i.pc);
+                            continue;
+                        }
+                        String invType = pool.getString(ref2[2]);
+                        String bsmType = indySignatures.get(conm);
+                        if (!invType.equals(bsmType)) {
+                            System.err.println(m+": warning: "+conm+" call type and local invoke type differ: "
+                                    +bsmType+", "+invType);
+                        }
+                        assert(i.len == 3 || i2.len == 3);
+                        if (!quiet)  System.err.println(i+" "+conm+";...; "+i2+" => invokedynamic "+con);
+                        int start = i.pc + 3, end = i2.pc;
+                        System.arraycopy(i.codeBase, start, i.codeBase, i.pc, end-start);
+                        i.forceNext(0);  // force revisit of new instruction
+                        i2.u1AtPut(-3, opc_invokedynamic);
+                        i2.u2AtPut(-2, con.index);
+                        i2.u2AtPut(0, (short)0);
+                        i2.u1AtPut(2, opc_nop);
+                        //System.out.println(new Instruction(i.codeBase, i2.pc-3));
+                    } else {
+                        if (!quiet)  System.err.println(i+" "+conm+" => ldc "+con);
+                        assert(i.len == 3);
+                        i.u1AtPut(0, opc_ldc_w);
+                        i.u2AtPut(1, con.index);
+                    }
+                }
+                //if (blab >= 1) { for (Instruction j = m.instructions(); j != null; j = j.next()) System.out.println("    |"+j); }
+            }
+            cf.methods.removeAll(constants.keySet());
+            return true;
+        }
+        
+        // Scan forward from the instruction to find where the stack p
+        // below the current sp at the instruction.
+        Instruction findPop(Instruction i) {
+            //System.out.println("findPop from "+i);
+            Pool pool = cf.pool;
+            JVMState jvm = new JVMState();
+        decode:
+            for (i = i.clone().next(); i != null; i = i.next()) {
+                String pops = INSTRUCTION_POPS[i.bc];
+                //System.out.println("  "+i+" "+jvm.stack+" : "+pops.replace("$", " => "));
+                if (pops == null)  break;
+                if (jvm.stackMotion(i.bc))  continue decode;
+                if (pops.indexOf('Q') >= 0) {
+                    Short[] ref = pool.getMemberRef((short) i.u2At(1));
+                    String type = simplifyType(pool.getString(CONSTANT_Utf8, ref[2]));
+                    switch (i.bc) {
+                    case opc_getstatic:
+                    case opc_getfield:          
+                    case opc_putstatic:
+                    case opc_putfield:          
+                        pops = pops.replace("Q", type);
+                        break;
+                    default:
+                        if (!type.startsWith("("))
+                            throw new InternalError(i.toString());
+                        pops = pops.replace("Q$Q", type.substring(1).replace(")","$"));
+                        break;
+                    }
+                    //System.out.println("special type: "+type+" => "+pops);
+                }
+                int npops = pops.indexOf('$');
+                if (npops < 0)  throw new InternalError();
+                if (npops > jvm.sp())  return i;
+                List<Object> args = jvm.args(npops);
+                int k = 0;
+                for (Object x : args) {
+                    char have = (Character) x;
+                    char want = pops.charAt(k++);
+                    if (have == 'X' || want == 'X')  continue;
+                    if (have != want)  break decode;
+                }
+                if (pops.charAt(k++) != '$')  break decode;
+                args.clear();
+                while (k < pops.length())
+                    args.add(pops.charAt(k++));
+            }
+            System.err.println("*** bailout on jvm: "+jvm.stack+" "+i);
+            return null;
+        }
+        
+        boolean findPatternMethods() {
+            boolean found = false;
+            for (char mark : "THI".toCharArray()) {
+                for (Method m : cf.methods) {
+                    if (!Modifier.isPrivate(m.access))  continue;
+                    if (!Modifier.isStatic(m.access))  continue;
+                    if (nameAndTypeMark(m.name, m.type) == mark) {
+                        Constant con = scanPattern(m, mark);
+                        if (con == null)  continue;
+                        constants.put(m, con);
+                        found = true;
+                    }
+                }
+            }
+            return found;
+        }
+
+        void reportPatternMethods() {
+            if (!constants.keySet().isEmpty())
+                System.err.println("pattern methods found: "+constants.keySet());
+            for (Method m : cf.methods) {
+                if (nameMark(cf.pool.getString(m.name)) != 0 &&
+                    !constants.containsKey(m))
+                    System.err.println("warning: method has special name but fails to match pattern: "+m);
+            }
+        }
+
+        // mark constant pool entries according to participation in patterns
+        boolean initializeMarks() {
+            boolean changed = false;
+            for (;;) {
+                boolean changed1 = false;
+                int cpindex = -1;
+                for (Constant e : cf.pool) {
+                    ++cpindex;
+                    if (e == null)  continue;
+                    char mark = poolMarks[cpindex];
+                    if (mark != 0)  continue;
+                    switch (e.tag) {
+                    case CONSTANT_Utf8:
+                        mark = nameMark(e.itemString()); break;
+                    case CONSTANT_NameAndType:
+                        mark = nameAndTypeMark(e.itemIndexes()); break;
+                    case CONSTANT_Class: {
+                        int n1 = e.itemIndex();
+                        char nmark = poolMarks[(char)n1];
+                        if ("DJ".indexOf(nmark) >= 0)
+                            mark = nmark;
+                        break;
+                    }
+                    case CONSTANT_Field:
+                    case CONSTANT_Method: {
+                        Short[] n12 = e.itemIndexes();
+                        short cl = n12[0];
+                        short nt = n12[1];
+                        char cmark = poolMarks[(char)cl];
+                        if (cmark != 0) {
+                            mark = cmark;  // it is a java.dyn.* or java.lang.* method
+                            break;
+                        }
+                        String cls = cf.pool.getString(CONSTANT_Class, cl);
+                        if (cls.equals(cf.classname())) {
+                            switch (poolMarks[(char)nt]) {
+                            // it is a private MH/MT/INDY method
+                            case 'T': case 'H': case 'I':
+                                mark = poolMarks[(char)nt];
+                                break;
+                            }
+                        }
+                        break;
+                    }
+                    default:  break;
+                    }
+                    if (mark != 0) {
+                        poolMarks[cpindex] = mark;
+                        changed1 = true;
+                    }
+                }
+                if (!changed1)
+                    break;
+                changed = true;
+            }
+            return changed;
+        }
+        char nameMark(String s) {
+            if (s.startsWith("MT_"))                return 'T';
+            else if (s.startsWith("MH_"))           return 'H';
+            else if (s.startsWith("INDY_"))         return 'I';
+            else if (s.startsWith("java/dyn/"))     return 'D';
+            else if (s.startsWith("java/lang/"))    return 'J';
+            return 0;
+        }
+        char nameAndTypeMark(Short[] n12) {
+            return nameAndTypeMark(n12[0], n12[1]);
+        }
+        char nameAndTypeMark(short n1, short n2) {
+            char mark = poolMarks[(char)n1];
+            if (mark == 0)  return 0;
+            String descr = cf.pool.getString(CONSTANT_Utf8, n2);
+            String requiredType;
+            switch (poolMarks[(char)n1]) {
+            case 'H': requiredType = "()Ljava/dyn/MethodHandle;";  break;
+            case 'T': requiredType = "()Ljava/dyn/MethodType;";    break;
+            case 'I': requiredType = "()Ljava/dyn/MethodHandle;";  break;
+            default:  return 0;
+            }
+            if (descr.equals(requiredType))  return mark;
+            return 0;
+        }
+
+        private class JVMState {
+            final List<Object> stack = new ArrayList<>();
+            int sp() { return stack.size(); }
+            void push(Object x) { stack.add(x); }
+            void pushAt(int pos, Object x) { stack.add(stack.size()+pos, x); }
+            Object pop() { return stack.remove(sp()-1); }
+            Object top() { return stack.get(sp()-1); }
+            List<Object> args(boolean hasRecv, String type) {
+                return args(argsize(type) + (hasRecv ? 1 : 0));
+            }
+            List<Object> args(int argsize) {
+                return stack.subList(sp()-argsize, sp());
+            }
+            boolean stackMotion(int bc) {
+                switch (bc) {
+                case opc_pop:    pop();             break;
+                case opc_pop2:   pop(); pop();      break;
+                case opc_swap:   pushAt(-1, pop()); break;
+                case opc_dup:    push(top());       break;
+                case opc_dup_x1: pushAt(-2, top()); break;
+                case opc_dup_x2: pushAt(-3, top()); break;
+                // ? also: dup2{,_x1,_x2}
+                default:  return false;
+                }
+                return true;
+            }
+        }
+
+        private Constant scanPattern(Method m, char patternMark) {
+            if (verbose)  System.err.println("scan "+m+" for pattern="+patternMark);
+            int wantTag;
+            switch (patternMark) {
+            case 'T': wantTag = CONSTANT_MethodType; break;
+            case 'H': wantTag = CONSTANT_MethodHandle; break;
+            case 'I': wantTag = CONSTANT_InvokeDynamic; break;
+            default: throw new InternalError();
+            }
+            Instruction i = m.instructions();
+            JVMState jvm = new JVMState();
+            Pool pool = cf.pool;
+            int branchCount = 0;
+            Object arg;
+            List<Object> args;
+            List<Object> bsmArgs = null;  // args to invokeGeneric
+        decode:
+            for (; i != null; i = i.next()) {
+                //System.out.println(jvm.stack+" "+i);
+                int bc = i.bc;
+                switch (bc) {
+                case opc_ldc:           jvm.push(pool.get(i.u1At(1))); break;
+                case opc_ldc_w:         jvm.push(pool.get(i.u2At(1))); break;
+                case opc_aconst_null:   jvm.push(null);              break;
+                case opc_bipush:        jvm.push((byte)  i.u1At(1)); break;
+                case opc_sipush:        jvm.push((short) i.u2At(1)); break;
+
+                // these support creation of a restarg array
+                case opc_anewarray:
+                    arg = jvm.pop();
+                    if (!(arg instanceof Integer))  break decode;
+                    arg = Arrays.asList(new Object[(Integer)arg]);
+                    jvm.push(arg);
+                    break;
+                case opc_dup:
+                    jvm.push(jvm.top()); break;
+                case opc_aastore:
+                    args = jvm.args(3);  // array, index, value
+                    if (args.get(0) instanceof List &&
+                        args.get(1) instanceof Integer) {
+                        ((List<Object>)args.get(0)).set( (Integer)args.get(1), args.get(2) );
+                    }
+                    args.clear();
+                    break;
+
+                case opc_getstatic:
+                {
+                    // int.class compiles to getstatic Integer.TYPE
+                    int fieldi = i.u2At(1);
+                    char mark = poolMarks[fieldi];
+                    //System.err.println("getstatic "+fieldi+Arrays.asList(pool.getStrings(pool.getMemberRef((short)fieldi)))+mark);
+                    if (mark == 'J') {
+                        Short[] ref = pool.getMemberRef((short) fieldi);
+                        String name = pool.getString(CONSTANT_Utf8, ref[1]);
+                        if ("TYPE".equals(name)) {
+                            String wrapperName = pool.getString(CONSTANT_Class, ref[0]).replace('/', '.');
+                            // a primitive type descriptor
+                            Class<?> primClass;
+                            try {
+                                primClass = (Class<?>) Class.forName(wrapperName).getField(name).get(null);
+                            } catch (Exception ex) {
+                                throw new InternalError("cannot load "+wrapperName+"."+name);
+                            }
+                            jvm.push(primClass);
+                            break;
+                        }
+                    }
+                    // unknown field; keep going...
+                    jvm.push(UNKNOWN_CON);
+                    break;
+                }
+                case opc_putstatic:
+                {
+                    if (patternMark != 'I')  break decode;
+                    jvm.pop();
+                    // unknown field; keep going...
+                    break;
+                }
+
+                case opc_invokestatic:
+                case opc_invokevirtual:
+                {
+                    boolean hasRecv = (bc == opc_invokevirtual);
+                    int methi = i.u2At(1);
+                    char mark = poolMarks[methi];
+                    Short[] ref = pool.getMemberRef((short)methi);
+                    String type = pool.getString(CONSTANT_Utf8, ref[2]);
+                    //System.out.println("invoke "+pool.getString(CONSTANT_Utf8, ref[1])+" "+Arrays.asList(ref)+" : "+type);
+                    args = jvm.args(hasRecv, type);
+                    String intrinsic = null;
+                    Constant con;
+                    if (mark == 'D') {
+                        intrinsic = pool.getString(CONSTANT_Utf8, ref[1]);
+                        byte refKind = -1;
+                        switch (intrinsic) {
+                        case "findGetter":          refKind = REF_getField;         break;
+                        case "findStaticGetter":    refKind = REF_getStatic;        break;
+                        case "findSetter":          refKind = REF_putField;         break;
+                        case "findStaticSetter":    refKind = REF_putStatic;        break;
+                        case "findVirtual":         refKind = REF_invokeVirtual;    break;
+                        case "findStatic":          refKind = REF_invokeStatic;     break;
+                        case "findSpecial":         refKind = REF_invokeSpecial;    break;
+                        case "findConstructor":     refKind = REF_newInvokeSpecial; break;
+                        }
+                        if (refKind >= 0 && (con = parseMemberLookup(refKind, args)) != null) {
+                            args.clear(); args.add(con);
+                            continue;
+                        }
+                    }
+                    Method ownMethod = null;
+                    if (mark == 'T' || mark == 'H' || mark == 'I') {
+                        ownMethod = findMember(cf.methods, ref[1], ref[2]);
+                    }
+                    if ("fromMethodDescriptorString".equals(intrinsic)) {
+                        con = makeMethodTypeCon(args.get(0));
+                        args.clear(); args.add(con);
+                        continue;
+                    } else if ("methodType".equals(intrinsic)) {
+                        flattenVarargs(args);  // there are several overloadings, some with varargs
+                        StringBuilder buf = new StringBuilder();
+                        String rtype = null;
+                        for (Object typeArg : args) {
+                            if (typeArg instanceof Class) {
+                                Class<?> argClass = (Class<?>) typeArg;
+                                if (argClass.isPrimitive()) {
+                                    char tchar;
+                                    switch (argClass.getName()) {
+                                    case "void":    tchar = 'V'; break;
+                                    case "boolean": tchar = 'Z'; break;
+                                    case "byte":    tchar = 'B'; break;
+                                    case "char":    tchar = 'C'; break;
+                                    case "short":   tchar = 'S'; break;
+                                    case "int":     tchar = 'I'; break;
+                                    case "long":    tchar = 'J'; break;
+                                    case "float":   tchar = 'F'; break;
+                                    case "double":  tchar = 'D'; break;
+                                    default:  throw new InternalError(argClass.toString());
+                                    }
+                                    buf.append(tchar);
+                                } else {
+                                    // should not happen, but...
+                                    buf.append('L').append(argClass.getName().replace('.','/')).append(';');
+                                }
+                            } else if (typeArg instanceof Constant) {
+                                Constant argCon = (Constant) typeArg;
+                                if (argCon.tag == CONSTANT_Class) {
+                                    buf.append('L').append(pool.get(argCon.itemIndex()).itemString()).append(';');
+                                } else {
+                                    break decode;
+                                }
+                            } else {
+                                break decode;
+                            }
+                            if (rtype == null) {
+                                // first arg is treated differently
+                                rtype = buf.toString();
+                                buf.setLength(0);
+                                buf.append('(');
+                            }
+                        }
+                        buf.append(')').append(rtype);
+                        con = con = makeMethodTypeCon(buf.toString());
+                        args.clear(); args.add(con);
+                        continue;
+                    } else if ("lookup".equals(intrinsic)) {
+                        args.clear(); args.add(intrinsic);
+                        continue;
+                    } else if ("lookupClass".equals(intrinsic) && args.equals(Arrays.asList("lookup"))) {
+                        // fold lookup().lookupClass() to the enclosing class
+                        args.clear(); args.add(pool.get(cf.thisc));
+                        continue;
+                    } else if ("dynamicInvoker".equals(intrinsic)) {
+                        args.clear(); args.add(intrinsic);
+                        continue;
+                    } else if ("invokeGeneric".equals(intrinsic) ||
+                               "invokeWithArguments".equals(intrinsic)) {
+                        if (patternMark != 'I')  break decode;
+                        if ("invokeWithArguments".equals(intrinsic))
+                            flattenVarargs(args);
+                        bsmArgs = new ArrayList(args);
+                        args.clear(); args.add("invokeGeneric");
+                        continue;
+                    } else if (!hasRecv && ownMethod != null && patternMark != 0) {
+                        con = constants.get(ownMethod);
+                        if (con == null)  break decode;
+                        args.clear(); args.add(con);
+                        continue;
+                    } else if (patternMark == 'I' && type.endsWith(")V")) {
+                        args.clear();
+                        continue;
+                    }
+                    break decode;  // bail out for most calls
+                }
+                case opc_areturn:
+                {
+                    ++branchCount;
+                    if (bsmArgs != null) {
+                        // parse bsmArgs as (MH, lookup, MT, String, [extra])
+                        Constant indyCon = makeInvokeDynamicCon(bsmArgs);
+                        if (indyCon != null) {
+                            Constant typeCon = (Constant) bsmArgs.get(3);
+                            indySignatures.put(m, pool.getString(typeCon.itemIndex()));
+                        }
+                        return indyCon;
+                    }
+                    arg = jvm.pop();
+                    if (branchCount == 2 && UNKNOWN_CON.equals(arg))
+                        break;  // merge to next path
+                    if (!isConstant(arg, wantTag))  break decode;
+                    return (Constant) arg;
+                }
+                default:
+                    if (jvm.stackMotion(i.bc))  break;
+                    if (bc >= opc_iconst_MIN && bc <= opc_iconst_MAX)
+                        { jvm.push((int)(bc - opc_iconst_0)); break; }
+                    if (patternMark == 'I') {
+                        // these support caching paths in INDY_x methods
+                        if (bc == opc_aload || bc >= opc_aload_0 && bc <= opc_aload_MAX)
+                            { jvm.push(UNKNOWN_CON); break; }
+                        if (bc == opc_astore || bc >= opc_astore_0 && bc <= opc_astore_MAX)
+                            { jvm.pop(); break; }
+                        switch (bc) {
+                        case opc_getfield:
+                        case opc_aaload:
+                            jvm.push(UNKNOWN_CON); break;
+                        case opc_ifnull:
+                        case opc_ifnonnull:
+                            // ignore branch target
+                            if (++branchCount != 1)  break decode;
+                            jvm.pop();
+                            break;
+                        case opc_checkcast:
+                            arg = jvm.top();
+                            if ("invokeWithArguments".equals(arg) ||
+                                "invokeGeneric".equals(arg))
+                                break;  // assume it is a helpful cast
+                            break decode;
+                        default:
+                            break decode;  // bail out
+                        }
+                        continue decode; // go to next instruction
+                    }
+                    break decode;  // bail out
+                } //end switch
+            }
+            System.err.println(m+": bailout on "+i+" jvm stack: "+jvm.stack);
+            return null;
+        }
+        private final String UNKNOWN_CON = "<unknown>";
+
+        private void flattenVarargs(List<Object> args) {
+            int size = args.size();
+            if (size > 0 && args.get(size-1) instanceof List)
+                args.addAll((List<Object>) args.remove(size-1));
+        }
+
+        private boolean isConstant(Object x, int tag) {
+            return x instanceof Constant && ((Constant)x).tag == tag;
+        }
+        private Constant makeMethodTypeCon(Object x) {
+            short utfIndex;
+            if (x instanceof String)
+                utfIndex = (short) cf.pool.addConstant(CONSTANT_Utf8, x).index;
+            else if (isConstant(x, CONSTANT_String))
+                utfIndex = ((Constant)x).itemIndex();
+            else  return null;
+            return cf.pool.addConstant(CONSTANT_MethodType, utfIndex);
+        }
+        private Constant parseMemberLookup(byte refKind, List<Object> args) {
+            // E.g.: lookup().findStatic(Foo.class, "name", MethodType)
+            if (args.size() != 4)  return null;
+            int argi = 0;
+            if (!"lookup".equals(args.get(argi++)))  return null;
+            short refindex, cindex, ntindex, nindex, tindex;
+            Object con;
+            if (!isConstant(con = args.get(argi++), CONSTANT_Class))  return null;
+            cindex = (short)((Constant)con).index;
+            if (!isConstant(con = args.get(argi++), CONSTANT_String))  return null;
+            nindex = ((Constant)con).itemIndex();
+            if (isConstant(con = args.get(argi++), CONSTANT_MethodType) ||
+                isConstant(con, CONSTANT_Class)) {
+                tindex = ((Constant)con).itemIndex();
+            } else return null;
+            ntindex = (short) cf.pool.addConstant(CONSTANT_NameAndType,
+                    new Short[]{ nindex, tindex }).index;
+            byte reftag = CONSTANT_Method;
+            if (refKind <= REF_putStatic)      
+                reftag = CONSTANT_Field;
+            else if (refKind == REF_invokeInterface)
+                reftag = CONSTANT_InterfaceMethod;
+            Constant ref = cf.pool.addConstant(reftag, new Short[]{ cindex, ntindex });
+            return cf.pool.addConstant(CONSTANT_MethodHandle, new Object[]{ refKind, (short)ref.index });
+        }
+        private Constant makeInvokeDynamicCon(List<Object> args) {
+            // E.g.: MH_bsm.invokeGeneric(lookup(), "name", MethodType, "extraArg")
+            if (args.size() != 4 && args.size() != 5)  return null;
+            int argi = 0;
+            short nindex, tindex;
+            List<Short> indexes = new ArrayList<>();
+            Object con;
+            if (!isConstant(con = args.get(argi++), CONSTANT_MethodHandle))  return null;
+            indexes.add((short) ((Constant)con).index);
+            if (!"lookup".equals(args.get(argi++)))  return null;
+            if (!isConstant(con = args.get(argi++), CONSTANT_String))  return null;
+            nindex = ((Constant)con).itemIndex();
+            if (!isConstant(con = args.get(argi++), CONSTANT_MethodType))  return null;
+            tindex = ((Constant)con).itemIndex();
+            indexes.add( (short) cf.pool.addConstant(CONSTANT_NameAndType,
+                    new Short[]{ nindex, tindex }).index );
+            if (transitionalJSR292) {
+                if (argi != args.size())  return null;
+                return cf.pool.addConstant(CONSTANT_InvokeDynamic_17,
+                        indexes.toArray(new Short[indexes.size()]));
+            } else if (argi == args.size()) {
+                indexes.add((short)0);
+            } else {
+                List<Object> extraArgs;
+                if (args.get(argi) instanceof List)
+                    extraArgs = (List<Object>) args.get(argi);
+                else
+                    extraArgs = Arrays.asList(args.get(argi));
+                indexes.add((short)extraArgs.size());
+                for (Object x : extraArgs) {
+                    if (!(x instanceof Constant))  return null;
+                    indexes.add((short) ((Constant)x).index);
+                }
+            }
+            return cf.pool.addConstant(CONSTANT_InvokeDynamic,
+                    indexes.toArray(new Short[indexes.size()]));
+        }
+    }
+    
+    private DataInputStream openInput(File f) throws IOException {
+        return new DataInputStream(new BufferedInputStream(new FileInputStream(f)));
+    }
+    
+    private DataOutputStream openOutput(File f) throws IOException {
+        if (!overwrite && f.exists())
+            throw new IOException("file already exists: "+f);
+        ensureDirectory(f.getParentFile());
+        return new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
+    }
+
+    static byte[] readRawBytes(DataInputStream in, int size) throws IOException {
+        byte[] bytes = new byte[size];
+        int nr = in.read(bytes);
+        if (nr != size)
+            throw new InternalError("wrong size: "+nr);
+        return bytes;
+    }
+   
+    private interface Chunk {
+        void readFrom(DataInputStream in) throws IOException;
+        void writeTo(DataOutputStream out) throws IOException;
+    }
+
+    private static class CountedList<T> extends ArrayList<T> implements Chunk {
+        final Class<? extends T> itemClass;
+        final int rowlen;
+        CountedList(Class<? extends T> itemClass, int rowlen) {
+            this.itemClass = itemClass;
+            this.rowlen = rowlen;
+        }
+        CountedList(Class<? extends T> itemClass) { this(itemClass, -1); }
+        public void readFrom(DataInputStream in) throws IOException {
+            int count = in.readUnsignedShort();
+            while (size() < count) {
+                if (rowlen < 0) {
+                    add(readInput(in, itemClass));
+                } else {
+                    Class<?> elemClass = itemClass.getComponentType();
+                    Object[] row = (Object[]) java.lang.reflect.Array.newInstance(elemClass, rowlen);
+                    for (int i = 0; i < rowlen; i++)
+                        row[i] = readInput(in, elemClass);
+                    add(itemClass.cast(row));
+                }        
+            }
+        }
+        public void writeTo(DataOutputStream out) throws IOException {
+            out.writeShort((short)size());
+            for (T item : this) {
+                writeOutput(out, item);
+            }
+        }
+    }
+
+    private static <T> T readInput(DataInputStream in, Class<T> dataClass) throws IOException {
+        Object data;
+        if (dataClass == Integer.class) {
+            data = in.readInt();
+        } else if (dataClass == Short.class) {
+            data = in.readShort();
+        } else if (dataClass == Byte.class) {
+            data = in.readByte();
+        } else if (dataClass == String.class) {
+            data = in.readUTF();
+        } else if (Chunk.class.isAssignableFrom(dataClass)) {
+            T obj;
+            try { obj = dataClass.newInstance(); }
+                catch (Exception ex) { throw new RuntimeException(ex); }
+            ((Chunk)obj).readFrom(in);
+            data = obj;
+        } else {
+            throw new InternalError("bad input datum: "+dataClass);
+        }
+        return dataClass.cast(data);
+    }
+    private static <T> T readInput(byte[] bytes, Class<T> dataClass) {
+        try {
+            return readInput(new DataInputStream(new ByteArrayInputStream(bytes)), dataClass);
+        } catch (IOException ex) {
+            throw new InternalError();
+        }
+    }
+    private static void readInputs(DataInputStream in, Object... data) throws IOException {
+        for (Object x : data)  ((Chunk)x).readFrom(in);
+    }
+
+    private static void writeOutput(DataOutputStream out, Object data) throws IOException {
+        if (data == null) {
+            return;
+        } if (data instanceof Integer) {
+            out.writeInt((Integer)data);
+        } else if (data instanceof Long) {
+            out.writeLong((Long)data);
+        } else if (data instanceof Short) {
+            out.writeShort((Short)data);
+        } else if (data instanceof Byte) {
+            out.writeByte((Byte)data);
+        } else if (data instanceof String) {
+            out.writeUTF((String)data);
+        } else if (data instanceof byte[]) {
+            out.write((byte[])data);
+        } else if (data instanceof Object[]) {
+            for (Object x : (Object[]) data)
+                writeOutput(out, x);
+        } else if (data instanceof Chunk) {
+            Chunk x = (Chunk) data;
+            x.writeTo(out);
+        } else if (data instanceof List) {
+            for (Object x : (List<?>) data)
+                writeOutput(out, x);
+        } else {
+            throw new InternalError("bad output datum: "+data+" : "+data.getClass().getName());
+        }
+    }
+    private static void writeOutputs(DataOutputStream out, Object... data) throws IOException {
+        for (Object x : data)  writeOutput(out, x);
+    }
+    
+    public static abstract class Outer {
+        public abstract List<? extends Inner> inners();
+        protected void linkInners() {
+            for (Inner i : inners()) {
+                i.linkOuter(this);
+                if (i instanceof Outer)
+                    ((Outer)i).linkInners();
+            }
+        }
+        public <T extends Outer> T outer(Class<T> c) {
+            for (Outer walk = this;; walk = ((Inner)walk).outer()) {
+                if (c.isInstance(walk))
+                    return c.cast(walk);
+                //if (!(walk instanceof Inner))  return null;
+            }
+        }
+    }
+    public interface Inner { Outer outer(); void linkOuter(Outer o); }
+    public static abstract class InnerOuter extends Outer implements Inner {
+        public Outer outer;
+        public Outer outer() { return outer; }
+        public void linkOuter(Outer o) { assert(outer == null); outer = o; }
+    }
+    public static class Constant<T> implements Chunk {
+        public final byte tag;
+        public final T item;
+        public final int index;
+        public Constant(int index, byte tag, T item) {
+            this.index = index;
+            this.tag = tag;
+            this.item = item;
+        }
+        public Constant checkTag(byte tag) {
+            if (this.tag != tag)  throw new InternalError(this.toString());
+            return this;
+        }
+        public String itemString() { return (String)item; }
+        public Short itemIndex() { return (Short)item; }
+        public Short[] itemIndexes() { return (Short[])item; }
+        public void readFrom(DataInputStream in) throws IOException {
+            throw new InternalError("do not call");
+        }
+        public void writeTo(DataOutputStream out) throws IOException {
+            writeOutputs(out, tag, item);
+        }
+        public String toString() {
+            return index+":"+tag+"="+(item instanceof Object[] ? Arrays.toString((Object[])item) : item.toString());
+        }
+    }
+
+    public static class Pool extends CountedList<Constant> implements Chunk {
+        public Pool() {
+            super(Constant.class);
+        }
+        public void readFrom(DataInputStream in) throws IOException {
+            int count = in.readUnsignedShort();
+            add(null);  // always ignore first item
+            while (size() < count) {
+                readConstant(in);
+            }
+        }
+        public void writeTo(DataOutputStream out) throws IOException {
+            super.writeTo(out);
+        }
+        public <T> Constant<T> addConstant(byte tag, T item) {
+            Constant<T> con = new Constant<>(size(), tag, item);
+            add(con);
+            return con;
+        }
+        private void readConstant(DataInputStream in) throws IOException {
+            byte tag = in.readByte();
+            int index = size();
+            Object arg;
+            switch (tag) {
+            case CONSTANT_Utf8:
+                arg = in.readUTF(); break;
+            case CONSTANT_Integer:
+            case CONSTANT_Float:
+                arg = in.readInt(); break;
+            case CONSTANT_Long:
+            case CONSTANT_Double:
+                add(new Constant(index, tag, in.readLong()));
+                add(null);
+                return;
+            case CONSTANT_Class:
+            case CONSTANT_String:
+                arg = in.readShort(); break;
+            case CONSTANT_Field:
+            case CONSTANT_Method:
+            case CONSTANT_InterfaceMethod:
+            case CONSTANT_NameAndType:
+                // read an ordered pair
+                arg = new Short[] { in.readShort(), in.readShort() };
+                break;
+            case CONSTANT_MethodHandle:
+                // read an ordered pair; first part is a u1 (not u2)
+                arg = new Object[] { in.readByte(), in.readShort() };
+                break;
+            case CONSTANT_MethodType:
+                arg = in.readShort(); break;
+            case CONSTANT_InvokeDynamic_17:
+                arg = in.readInt(); break;
+            case CONSTANT_InvokeDynamic:
+            {
+                ArrayList<Object> indy = new ArrayList<>();
+                indy.add(in.readInt());  // <BSM:N&T>
+                short bsmac = in.readShort();
+                indy.add(bsmac);
+                for (int i = 0; i < bsmac; i++)
+                    indy.add(in.read());
+                add(new Constant(index, tag, indy.toArray()));
+                return;
+            }
+            default:
+                throw new InternalError("bad CP tag "+tag);
+            }
+            add(new Constant(index, tag, arg));
+        }
+
+        // Access:
+        public Constant get(int index) {
+            // extra 1-bits get into the shorts
+            return super.get((char) index);
+        }
+        String getString(byte tag, short index) {
+            get(index).checkTag(tag);
+            return getString(index);
+        }
+        String getString(short index) {
+            Object v = get(index).item;
+            if (v instanceof Short)
+                v = get((Short)v).checkTag(CONSTANT_Utf8).item;
+            return (String) v;
+        }
+        String[] getStrings(Short[] indexes) {
+            String[] res = new String[indexes.length];
+            for (int i = 0; i < indexes.length; i++)
+                res[i] = getString(indexes[i]);
+            return res;
+        }
+        Short[] getMemberRef(short index) {
+            Short[] cls_nnt = get(index).itemIndexes();
+            Short[] name_type = get(cls_nnt[1]).itemIndexes();
+            return new Short[]{ cls_nnt[0], name_type[0], name_type[1] };
+        }
+    }
+
+    public class ClassFile extends Outer implements Chunk {
+        ClassFile(File f) throws IOException {
+            DataInputStream in = openInput(f);
+            try {
+                readFrom(in);
+            } finally {
+                if (in != null)  in.close();
+            }
+        }
+        
+        public int                magic, version;  // <min:maj>
+        public final Pool         pool       = new Pool();
+        public short              access, thisc, superc;
+        public final List<Short>  interfaces = new CountedList<>(Short.class);
+        public final List<Field>  fields     = new CountedList<>(Field.class);
+        public final List<Method> methods    = new CountedList<>(Method.class);
+        public final List<Attr>   attrs      = new CountedList<>(Attr.class);
+
+        public final void readFrom(DataInputStream in) throws IOException {
+            magic = in.readInt(); version = in.readInt();
+            if (magic != 0xCAFEBABE)  throw new IOException("bad magic number");
+            pool.readFrom(in);
+            access = in.readShort(); thisc = in.readShort(); superc = in.readShort();
+            readInputs(in, interfaces, fields, methods, attrs);
+            if (in.read() >= 0)  throw new IOException("junk after end of file");
+            recordUtf8s();
+            linkInners();
+        }
+        
+        void writeTo(File f) throws IOException {
+            DataOutputStream out = openOutput(f);
+            try {
+                writeTo(out);
+            } finally {
+                out.close();
+            }
+        }
+
+        public void writeTo(DataOutputStream out) throws IOException {
+            writeOutputs(out, magic, version, pool,
+                         access, thisc, superc, interfaces,
+                         fields, methods, attrs);
+        }
+
+        public byte[] toByteArray() {
+            try {
+                ByteArrayOutputStream buf = new ByteArrayOutputStream();
+                writeTo(new DataOutputStream(buf));
+                return buf.toByteArray();
+            } catch (IOException ex) {
+                throw new InternalError();
+            }
+        }
+        
+        public List<Inner> inners() {
+            List<Inner> inns = new ArrayList<>();
+            inns.addAll(fields); inns.addAll(methods); inns.addAll(attrs);
+            return inns;
+        }
+
+        // derived stuff:
+        public String classname() { return pool.getString(CONSTANT_Class, thisc); }
+        int Code_index;
+
+        private Map<String,Short> cpoolUTF8 = new TreeMap<>();
+        private void recordUtf8s() {
+            int cpindex = -1;
+            for (Constant c : pool) {
+                ++cpindex;
+                if (c != null && c.tag == CONSTANT_Utf8) {
+                    cpoolUTF8.put(c.itemString(), (short) cpindex);
+                }
+            }
+            Code_index = cpoolUTF8("Code");
+        }
+
+        //FIXME
+        int cpoolUTF8(String name) {
+            Short x = cpoolUTF8.get(name);
+            return (x == null ? 0 : (char)(int) x);
+        }
+    }
+
+    private static Attr findAttribute(List<Attr> attrs, String name) {
+        if (attrs.isEmpty())  return null;
+        ClassFile cf = attrs.get(0).outer(ClassFile.class);
+        return findAttribute(attrs, cf.cpoolUTF8(name));
+    }
+    private static Attr findAttribute(List<Attr> attrs, int name) {
+        if (name == 0)  return null;
+        for (Attr a : attrs) {
+            if (a.name == name)  return a;
+        }
+        return null;
+    }
+
+    private static <T extends Member> T findMember(List<T> mems, int name, int type) {
+        if (name == 0 || type == 0)  return null;
+        for (T m : mems) {
+            if (m.name == name && m.type == type)  return m;
+        }
+        return null;
+    }
+
+    public static class Member extends InnerOuter implements Chunk {
+        public short access, name, type;
+        public final List<Attr> attrs = new CountedList<>(Attr.class);
+        public void readFrom(DataInputStream in) throws IOException {
+            access = in.readShort(); name = in.readShort(); type = in.readShort();
+            readInputs(in, attrs);
+        }
+        public void writeTo(DataOutputStream out) throws IOException {
+            writeOutputs(out, access, name, type, attrs);
+        }
+        public List<Attr> inners() { return attrs; }
+        public ClassFile outer() { return (ClassFile) outer; }
+        public String toString() {
+            if (outer == null)  return super.toString();
+            Pool pool = outer().pool;
+            return (pool.getString(CONSTANT_Utf8, name) +
+                    (this instanceof Method? "" : ":") +
+                    simplifyType(pool.getString(CONSTANT_Utf8, type)));
+        }
+    }
+    public static class Field extends Member {
+    }
+    public static class Method extends Member {
+        public Code code() {
+            Attr a = findAttribute(attrs, "Code");
+            if (a == null)  return null;
+            return (Code) a.item;
+        }
+        public Instruction instructions() {
+            Code code = code();
+            if (code == null)  return null;
+            return code.instructions();
+        }
+    }
+
+    public static class Attr extends InnerOuter implements Chunk {
+        public short name;
+        public int size;
+        public Object item;
+        public void readFrom(DataInputStream in) throws IOException {
+            name = in.readShort();
+            size = in.readInt();
+            item = readRawBytes(in, size);
+        }
+        public void writeTo(DataOutputStream out) throws IOException {
+            out.writeShort(name);
+            // write the 4-byte size header and then the contents:
+            byte[] bytes;
+            int trueSize;
+            if (item instanceof byte[]) {
+                bytes = (byte[]) item;
+                out.writeInt(trueSize = bytes.length);
+                out.write(bytes);
+            } else {
+                trueSize = flatten(out);
+            }
+            if (trueSize != size && size >= 0)
+                System.err.println("warning: attribute size changed "+size+" to "+trueSize);
+        }
+        public void linkOuter(Outer o) {
+            super.linkOuter(o);
+            if (item instanceof byte[] &&
+                outer instanceof Method &&
+                ((Method)outer).outer().Code_index == name) {
+                    item = readInput((byte[])item, Code.class);
+            }
+        }
+        public List<Inner> inners() {
+            if (item instanceof Inner)
+                return Collections.nCopies(1, (Inner)item);
+            return Collections.emptyList();
+        }
+        public byte[] flatten() {
+            ByteArrayOutputStream buf = new ByteArrayOutputStream(size);
+            flatten(buf);
+            return buf.toByteArray();
+        }
+        public int flatten(DataOutputStream out) throws IOException {
+            ByteArrayOutputStream buf = new ByteArrayOutputStream(size);
+            int trueSize = flatten(buf);
+            out.writeInt(trueSize);
+            buf.writeTo(out);
+            return trueSize;
+        }
+        private int flatten(ByteArrayOutputStream buf) {
+            try {
+                writeOutput(new DataOutputStream(buf), item);
+                return buf.size();
+            } catch (IOException ex) {
+                throw new InternalError();
+            }
+        }
+    }
+    
+    public static class Code extends InnerOuter implements Chunk {
+        public short stacks, locals;
+        public byte[] bytes;
+        public final List<Short[]> etable = new CountedList<>(Short[].class, 4);
+        public final List<Attr> attrs = new CountedList<>(Attr.class);
+        // etable[N] = (N)*{ startpc, endpc, handlerpc, catchtype }
+        public void readFrom(DataInputStream in) throws IOException {
+            stacks = in.readShort(); locals = in.readShort();
+            bytes = readRawBytes(in, in.readInt());
+            readInputs(in, etable, attrs);
+        }
+        public void writeTo(DataOutputStream out) throws IOException {
+            writeOutputs(out, stacks, locals, bytes.length, bytes, etable, attrs);
+        }
+        public List<Attr> inners() { return attrs; }
+        public Instruction instructions() {
+            return new Instruction(bytes, 0);
+        }
+    }
+
+    // lots of constants
+    private static final byte
+        CONSTANT_Utf8              = 1,
+        CONSTANT_Integer           = 3,
+        CONSTANT_Float             = 4,
+        CONSTANT_Long              = 5,
+        CONSTANT_Double            = 6,
+        CONSTANT_Class             = 7,
+        CONSTANT_String            = 8,
+        CONSTANT_Field             = 9,
+        CONSTANT_Method            = 10,
+        CONSTANT_InterfaceMethod   = 11,
+        CONSTANT_NameAndType       = 12,
+        CONSTANT_MethodHandle      = 15,  // JSR 292
+        CONSTANT_MethodType        = 16,  // JSR 292
+        CONSTANT_InvokeDynamic_17  = 17,  // JSR 292, only occurs in old class files
+        CONSTANT_InvokeDynamic     = 18;  // JSR 292
+    private static final byte
+        REF_getField               = 1,
+        REF_getStatic              = 2,
+        REF_putField               = 3,
+        REF_putStatic              = 4,
+        REF_invokeVirtual          = 5,
+        REF_invokeStatic           = 6,
+        REF_invokeSpecial          = 7,
+        REF_newInvokeSpecial       = 8,
+        REF_invokeInterface        = 9;
+
+    private static final int
+        opc_nop                    = 0,
+        opc_aconst_null            = 1,
+        opc_iconst_MIN             = 2,
+        opc_iconst_0               = 3,
+        opc_iconst_MAX             = 8,
+        opc_bipush                 = 16,
+        opc_sipush                 = 17,
+        opc_ldc                    = 18,
+        opc_ldc_w                  = 19,
+        opc_ldc2_w                 = 20,
+        opc_aload                  = 25,
+        opc_aload_0                = 42,
+        opc_aload_MAX              = 45,
+        opc_aaload                 = 50,
+        opc_astore                 = 58,
+        opc_astore_0               = 75,
+        opc_astore_MAX             = 78,
+        opc_aastore                = 83,
+        opc_pop                    = 87,
+        opc_pop2                   = 88,
+        opc_dup                    = 89,
+        opc_dup_x1                 = 90,
+        opc_dup_x2                 = 91,
+        opc_dup2                   = 92,
+        opc_dup2_x1                = 93,
+        opc_dup2_x2                = 94,
+        opc_swap                   = 95,
+        opc_tableswitch            = 170,
+        opc_lookupswitch           = 171,
+        opc_areturn                = 176,
+        opc_getstatic              = 178,
+        opc_putstatic              = 179,
+        opc_getfield               = 180,
+        opc_putfield               = 181,
+        opc_invokevirtual          = 182,
+        opc_invokespecial          = 183,
+        opc_invokestatic           = 184,
+        opc_invokeinterface        = 185,
+        opc_invokedynamic          = 186,
+        opc_anewarray              = 189,
+        opc_checkcast              = 192,
+        opc_ifnull                 = 198,
+        opc_ifnonnull              = 199,
+        opc_wide                   = 196;
+
+    private static final String INSTRUCTION_FORMATS =
+        "nop$ aconst_null$L iconst_m1$I iconst_0$I iconst_1$I "+
+        "iconst_2$I iconst_3$I iconst_4$I iconst_5$I lconst_0$J_ "+
+        "lconst_1$J_ fconst_0$F fconst_1$F fconst_2$F dconst_0$D_ "+
+        "dconst_1$D_ bipush=bx$I sipush=bxx$I ldc=bk$X ldc_w=bkk$X "+
+        "ldc2_w=bkk$X_ iload=bl/wbll$I lload=bl/wbll$J_ fload=bl/wbll$F "+
+        "dload=bl/wbll$D_ aload=bl/wbll$L iload_0$I iload_1$I "+
+        "iload_2$I iload_3$I lload_0$J_ lload_1$J_ lload_2$J_ "+
+        "lload_3$J_ fload_0$F fload_1$F fload_2$F fload_3$F dload_0$D_ "+
+        "dload_1$D_ dload_2$D_ dload_3$D_ aload_0$L aload_1$L "+
+        "aload_2$L aload_3$L iaload$LI$I laload$LI$J_ faload$LI$F "+
+        "daload$LI$D_ aaload$LI$L baload$LI$I caload$LI$I saload$LI$I "+
+        "istore=bl/wbll$I$ lstore=bl/wbll$J_$ fstore=bl/wbll$F$ "+
+        "dstore=bl/wbll$D_$ astore=bl/wbll$L$ istore_0$I$ istore_1$I$ "+
+        "istore_2$I$ istore_3$I$ lstore_0$J_$ lstore_1$J_$ "+
+        "lstore_2$J_$ lstore_3$J_$ fstore_0$F$ fstore_1$F$ fstore_2$F$ "+
+        "fstore_3$F$ dstore_0$D_$ dstore_1$D_$ dstore_2$D_$ "+
+        "dstore_3$D_$ astore_0$L$ astore_1$L$ astore_2$L$ astore_3$L$ "+
+        "iastore$LII$ lastore$LIJ_$ fastore$LIF$ dastore$LID_$ "+
+        "aastore$LIL$ bastore$LII$ castore$LII$ sastore$LII$ pop$X$ "+
+        "pop2$XX$ dup$X$XX dup_x1$XX$XXX dup_x2$XXX$XXXX dup2$XX$XXXX "+
+        "dup2_x1$XXX$XXXXX dup2_x2$XXXX$XXXXXX swap$XX$XX "+
+        "iadd$II$I ladd$J_J_$J_ fadd$FF$F dadd$D_D_$D_ isub$II$I "+
+        "lsub$J_J_$J_ fsub$FF$F dsub$D_D_$D_ imul$II$I lmul$J_J_$J_ "+
+        "fmul$FF$F dmul$D_D_$D_ idiv$II$I ldiv$J_J_$J_ fdiv$FF$F "+
+        "ddiv$D_D_$D_ irem$II$I lrem$J_J_$J_ frem$FF$F drem$D_D_$D_ "+
+        "ineg$I$I lneg$J_$J_ fneg$F$F dneg$D_$D_ ishl$II$I lshl$J_I$J_ "+
+        "ishr$II$I lshr$J_I$J_ iushr$II$I lushr$J_I$J_ iand$II$I "+
+        "land$J_J_$J_ ior$II$I lor$J_J_$J_ ixor$II$I lxor$J_J_$J_ "+
+        "iinc=blx/wbllxx$ i2l$I$J_ i2f$I$F i2d$I$D_ l2i$J_$I l2f$J_$F "+
+        "l2d$J_$D_ f2i$F$I f2l$F$J_ f2d$F$D_ d2i$D_$I d2l$D_$J_ "+
+        "d2f$D_$F i2b$I$I i2c$I$I i2s$I$I lcmp fcmpl fcmpg dcmpl dcmpg "+
+        "ifeq=boo ifne=boo iflt=boo ifge=boo ifgt=boo ifle=boo "+
+        "if_icmpeq=boo if_icmpne=boo if_icmplt=boo if_icmpge=boo "+
+        "if_icmpgt=boo if_icmple=boo if_acmpeq=boo if_acmpne=boo "+
+        "goto=boo jsr=boo ret=bl/wbll tableswitch=* lookupswitch=* "+
+        "ireturn lreturn freturn dreturn areturn return "+
+        "getstatic=bkf$Q putstatic=bkf$Q$ getfield=bkf$L$Q "+
+        "putfield=bkf$LQ$ invokevirtual=bkm$LQ$Q "+
+        "invokespecial=bkm$LQ$Q invokestatic=bkm$Q$Q "+
+        "invokeinterface=bkixx$LQ$Q invokedynamic=bkd__$Q$Q new=bkc$L "+
+        "newarray=bx$I$L anewarray=bkc$I$L arraylength$L$I athrow "+
+        "checkcast=bkc$L$L instanceof=bkc$L$I monitorenter$L "+
+        "monitorexit$L wide=* multianewarray=bkcx ifnull=boo "+
+        "ifnonnull=boo goto_w=boooo jsr_w=boooo ";
+    private static final String[] INSTRUCTION_NAMES;
+    private static final String[] INSTRUCTION_POPS;
+    private static final int[] INSTRUCTION_INFO;
+    static {
+        String[] insns = INSTRUCTION_FORMATS.split(" ");
+        assert(insns[opc_lookupswitch].startsWith("lookupswitch"));
+        assert(insns[opc_tableswitch].startsWith("tableswitch"));
+        assert(insns[opc_wide].startsWith("wide"));
+        assert(insns[opc_invokedynamic].startsWith("invokedynamic"));
+        int[] info = new int[256];
+        String[] names = new String[256];
+        String[] pops = new String[256];
+        for (int i = 0; i < insns.length; i++) {
+            String insn = insns[i];
+            int dl = insn.indexOf('$');
+            if (dl > 0) {
+                String p = insn.substring(dl+1);
+                if (p.indexOf('$') < 0)  p = "$" + p;
+                pops[i] = p;
+                insn = insn.substring(0, dl);
+            }
+            int eq = insn.indexOf('=');
+            if (eq < 0) {
+                info[i] = 1;
+                names[i] = insn;
+                continue;
+            }
+            names[i] = insn.substring(0, eq);
+            String fmt = insn.substring(eq+1);
+            if (fmt.equals("*")) {
+                info[i] = 0;
+                continue;
+            }
+            int sl = fmt.indexOf('/');
+            if (sl < 0) {
+                info[i] = (char) fmt.length();
+            } else {
+                String wfmt = fmt.substring(sl+1);
+                fmt = fmt.substring(0, sl);
+                info[i] = (char)( fmt.length() + (wfmt.length() * 16) );
+            }
+        }
+        INSTRUCTION_INFO = info;
+        INSTRUCTION_NAMES = names;
+        INSTRUCTION_POPS = pops;
+    }
+    
+    public static class Instruction implements Cloneable {
+        byte[] codeBase;
+        int pc;
+        int bc;
+        int info;
+        int wide;
+        int len;
+        Instruction(byte[] codeBase, int pc) {
+            this.codeBase = codeBase;
+            init(pc);
+        }
+        public Instruction clone() {
+            try {
+                return (Instruction) super.clone();
+            } catch (CloneNotSupportedException ex) {
+                throw new InternalError();
+            }
+        }
+        private Instruction init(int pc) {
+            this.pc = pc;
+            this.bc = codeBase[pc] & 0xFF;
+            this.info = INSTRUCTION_INFO[bc];
+            this.wide = 0;
+            this.len = (info & 0x0F);
+            if (len == 0)
+                computeLength();
+            return this;
+        }
+        Instruction next() {
+            if (len == 0 && bc != 0)  throw new InternalError();
+            int npc = pc + len;
+            if (npc == codeBase.length)
+                return null;
+            return init(npc);
+        }
+        void forceNext(int newLen) {
+            bc = opc_nop;
+            len = newLen;
+        }
+
+        public String toString() {
+            StringBuilder buf = new StringBuilder();
+            buf.append(pc).append(":").append(INSTRUCTION_NAMES[bc]);
+            switch (len) {
+            case 3: buf.append(" ").append(u2At(1)); break;
+            case 5: buf.append(" ").append(u2At(1)).append(" ").append(u2At(3)); break;
+            default:  for (int i = 1; i < len; i++)  buf.append(" ").append(u1At(1));
+            }
+            return buf.toString();
+        }
+
+        // these are the hard parts
+        private void computeLength() {
+            int cases;
+            switch (bc) {
+            case opc_wide:
+                bc = codeBase[pc + 1];
+                info = INSTRUCTION_INFO[bc];
+                len = ((info >> 4) & 0x0F);
+                if (len == 0)  throw new RuntimeException("misplaced wide bytecode: "+bc);
+                return;
+
+            case opc_tableswitch:
+                cases = (u4At(alignedIntOffset(2)) - u4At(alignedIntOffset(1)) + 1);
+                len = alignedIntOffset(3 + cases*1);
+                return;
+
+            case opc_lookupswitch:
+                cases = u4At(alignedIntOffset(1));
+                len = alignedIntOffset(2 + cases*2);
+                return;
+
+            default:
+                throw new RuntimeException("unknown bytecode: "+bc);
+            }
+        }
+        // switch code
+        // clget the Nth int (where 0 is the first after the opcode itself)
+        public int alignedIntOffset(int n) {
+            int pos = pc + 1;
+            pos += ((-pos) & 0x03);  // align it
+            pos += (n * 4);
+            return pos - pc;
+        }
+        public int u1At(int pos) {
+            return (codeBase[pc+pos] & 0xFF);
+        }
+        public int u2At(int pos) {
+            return (u1At(pos+0)<<8) + u1At(pos+1);
+        }
+        public int u4At(int pos) {
+            return (u2At(pos+0)<<16) + u2At(pos+2);
+        }
+        public void u1AtPut(int pos, int x) {
+            codeBase[pc+pos] = (byte)x;
+        }
+        public void u2AtPut(int pos, int x) {
+            codeBase[pc+pos+0] = (byte)(x >> 8);
+            codeBase[pc+pos+1] = (byte)(x >> 0);
+        }
+    }
+
+    static String simplifyType(String type) {
+        String simpleType = OBJ_SIGNATURE.matcher(type).replaceAll("L");
+        assert(simpleType.matches("^\\([A-Z]*\\)[A-Z]$"));
+        // change (DD)D to (D_D_)D_
+        simpleType = WIDE_SIGNATURE.matcher(simpleType).replaceAll("\\0_");
+        return simpleType;
+    }
+    static int argsize(String type) {
+        return simplifyType(type).length()-3;
+    }
+    private static final Pattern OBJ_SIGNATURE = Pattern.compile("\\[*L[^;]*;|\\[+[A-Z]");
+    private static final Pattern WIDE_SIGNATURE = Pattern.compile("[JD]");
+}