Merge "CF backend: Implement InvokeCustom (JVM InvokeDynamic)"
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 187333c..39d916a 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.cf.code.CfBinop;
 import com.android.tools.r8.cf.code.CfCheckCast;
 import com.android.tools.r8.cf.code.CfConstClass;
+import com.android.tools.r8.cf.code.CfConstMethodHandle;
+import com.android.tools.r8.cf.code.CfConstMethodType;
 import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfConstString;
@@ -450,6 +452,18 @@
     }
   }
 
+  public void print(CfConstMethodHandle handle) {
+    indent();
+    builder.append("ldc ");
+    builder.append(handle.getHandle().toString());
+  }
+
+  public void print(CfConstMethodType type) {
+    indent();
+    builder.append("ldc ");
+    builder.append(type.getType().toString());
+  }
+
   private String getLabel(CfLabel label) {
     return labels != null ? labels.get(label) : "L?";
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
new file mode 100644
index 0000000..b63b4d5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.code;
+
+import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.graph.DexMethodHandle;
+import org.objectweb.asm.MethodVisitor;
+
+public class CfConstMethodHandle extends CfInstruction {
+
+  private DexMethodHandle handle;
+
+  public CfConstMethodHandle(DexMethodHandle handle) {
+    this.handle = handle;
+  }
+
+  public DexMethodHandle getHandle() {
+    return handle;
+  }
+
+  @Override
+  public void write(MethodVisitor visitor) {
+    visitor.visitLdcInsn(handle.toAsmHandle());
+  }
+
+  @Override
+  public void print(CfPrinter printer) {
+    printer.print(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
new file mode 100644
index 0000000..8ec1545
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf.code;
+
+import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.graph.DexProto;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+public class CfConstMethodType extends CfInstruction {
+
+  private DexProto type;
+
+  public CfConstMethodType(DexProto type) {
+    this.type = type;
+  }
+
+  public DexProto getType() {
+    return type;
+  }
+
+  @Override
+  public void write(MethodVisitor visitor) {
+    visitor.visitLdcInsn(Type.getType(type.toDescriptorString()));
+  }
+
+  @Override
+  public void print(CfPrinter printer) {
+    printer.print(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java b/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java
index 5feecec..7ad397a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java
@@ -3,10 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.cf.LoadStoreHelper;
+import com.android.tools.r8.cf.TypeVerificationHelper;
+import com.android.tools.r8.cf.code.CfConstMethodHandle;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexMethodHandle;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
+import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import java.util.function.Function;
 
@@ -35,6 +40,11 @@
   }
 
   @Override
+  public void buildCf(CfBuilder builder) {
+    builder.add(new CfConstMethodHandle(methodHandle));
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return other.asConstMethodHandle().methodHandle == methodHandle;
   }
@@ -85,4 +95,14 @@
       AppInfo appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
     return TypeLatticeElement.fromDexType(appInfo.dexItemFactory.methodHandleType, false);
   }
+
+  @Override
+  public DexType computeVerificationType(TypeVerificationHelper helper) {
+    return helper.getFactory().methodHandleType;
+  }
+
+  @Override
+  public void insertLoadAndStores(InstructionListIterator it, LoadStoreHelper helper) {
+    helper.storeOutValue(this, it);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java b/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java
index b687743..01bfeb6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java
@@ -3,10 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.cf.LoadStoreHelper;
+import com.android.tools.r8.cf.TypeVerificationHelper;
+import com.android.tools.r8.cf.code.CfConstMethodType;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
+import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import java.util.function.Function;
 
@@ -35,6 +40,11 @@
   }
 
   @Override
+  public void buildCf(CfBuilder builder) {
+    builder.add(new CfConstMethodType(methodType));
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return other.asConstMethodType().methodType == methodType;
   }
@@ -85,4 +95,14 @@
       AppInfo appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
     return TypeLatticeElement.fromDexType(appInfo.dexItemFactory.methodTypeType, false);
   }
+
+  @Override
+  public DexType computeVerificationType(TypeVerificationHelper helper) {
+    return helper.getFactory().methodTypeType;
+  }
+
+  @Override
+  public void insertLoadAndStores(InstructionListIterator it, LoadStoreHelper helper) {
+    helper.storeOutValue(this, it);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index 17ebfb5..1a567ef 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -170,11 +170,11 @@
   @Override
   public void insertLoadAndStores(InstructionListIterator it, LoadStoreHelper helper) {
     helper.loadInValues(this, it);
-    if (method.proto.returnType.isVoidType()) {
+    if (getReturnType().isVoidType()) {
       return;
     }
     if (outValue == null) {
-      helper.popOutType(method.proto.returnType, this, it);
+      helper.popOutType(getReturnType(), this, it);
     } else {
       assert outValue.isUsed();
       helper.storeOutValue(this, it);
@@ -188,7 +188,7 @@
 
   @Override
   public DexType computeVerificationType(TypeVerificationHelper helper) {
-    return getInvokedMethod().proto.returnType;
+    return getReturnType();
   }
 
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index c0e1a30..718d23c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -3,19 +3,25 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokePolymorphicRange;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.ir.conversion.JarSourceCode;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.InliningOracle;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import java.util.Collection;
 import java.util.List;
+import org.objectweb.asm.Opcodes;
 
 public class InvokePolymorphic extends InvokeMethod {
 
@@ -78,6 +84,21 @@
   }
 
   @Override
+  public void buildCf(CfBuilder builder) {
+    DexMethod dexMethod = getInvokedMethod();
+    DexItemFactory factory = builder.getFactory();
+
+    if (dexMethod.holder.getInternalName().equals(JarSourceCode.INTERNAL_NAME_METHOD_HANDLE)) {
+      DexMethod method = factory.createMethod(dexMethod.holder, getProto(), dexMethod.name);
+      builder.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, method));
+    } else {
+      assert dexMethod.holder.getInternalName().equals(JarSourceCode.INTERNAL_NAME_VAR_HANDLE);
+      // VarHandle is new in Java 9
+      throw new Unimplemented();
+    }
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     if (!other.isInvokePolymorphic()) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
index d3ef914..9e9a096 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokeSuperRange;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
@@ -10,12 +11,14 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeEnvironment;
+import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import org.objectweb.asm.Opcodes;
 
 public class InvokeSuper extends InvokeMethodWithReceiver {
 
@@ -72,6 +75,11 @@
   }
 
   @Override
+  public void buildCf(CfBuilder builder) {
+    builder.add(new CfInvoke(Opcodes.INVOKESPECIAL, getInvokedMethod()));
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     if (!other.isInvokeSuper()) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 83995bd..60f9c79 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -102,6 +102,10 @@
     this.factory = factory;
   }
 
+  public DexItemFactory getFactory() {
+    return factory;
+  }
+
   public Code build(CodeRewriter rewriter, InternalOptions options, AppInfoWithSubtyping appInfo) {
     try {
       types = new TypeVerificationHelper(code, factory, appInfo).computeVerificationTypes();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index 894ba4f..26a94d2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -141,8 +141,8 @@
       "([Ljava/lang/Object;)Z";
 
   // Various internal names.
-  static final String INTERNAL_NAME_METHOD_HANDLE = "java/lang/invoke/MethodHandle";
-  static final String INTERNAL_NAME_VAR_HANDLE = "java/lang/invoke/VarHandle";
+  public static final String INTERNAL_NAME_METHOD_HANDLE = "java/lang/invoke/MethodHandle";
+  public static final String INTERNAL_NAME_VAR_HANDLE = "java/lang/invoke/VarHandle";
 
   // Language types.
   static final Type CLASS_TYPE = Type.getObjectType("java/lang/Class");
@@ -2601,6 +2601,8 @@
                 default:
                   throw new Unreachable();
               }
+            } else {
+              throw new Unreachable();
             }
             callSiteProto = application.getProto(insn.desc);
           }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 22a4812..5cb4ce8 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -44,58 +44,54 @@
 
   private final Reporter reporter;
 
-  private static final List<String> IGNORED_SINGLE_ARG_OPTIONS = ImmutableList
-      .of("protomapping",
-          "target"
-      );
-  private static final List<String> IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS = ImmutableList
-      .of("keepdirectories",
-          "runtype",
-          "laststageoutput"
-      );
-  private static final List<String> IGNORED_FLAG_OPTIONS = ImmutableList
-      .of("forceprocessing",
-          "dontusemixedcaseclassnames",
-          "dontpreverify",
-          "experimentalshrinkunusedprotofields",
-          "filterlibraryjarswithorginalprogramjars",
-          "dontskipnonpubliclibraryclasses",
-          "dontskipnonpubliclibraryclassmembers",
-          "invokebasemethod",
-          "android"
-      );
-  private static final List<String> IGNORED_CLASS_DESCRIPTOR_OPTIONS = ImmutableList
-      .of("isclassnamestring",
-          "whyarenotsimple"
-      );
+  private static final List<String> IGNORED_SINGLE_ARG_OPTIONS = ImmutableList.of(
+      "dontnote",
+      "protomapping",
+      "target");
 
-  private static final List<String> WARNED_SINGLE_ARG_OPTIONS = ImmutableList
-      .of("dontnote",
-          "printconfiguration",
-          // TODO -outjars (http://b/37137994) and -adaptresourcefilecontents (http://b/37139570)
-          // should be reported as errors, not just as warnings!
-          "outjars",
-          "adaptresourcefilecontents"
-      );
-  private static final List<String> WARNED_FLAG_OPTIONS = ImmutableList
-      .of(
-          // TODO(b/73707846): add support -addconfigurationdebugging
-          "addconfigurationdebugging"
-      );
-  private static final List<String> WARNED_CLASS_DESCRIPTOR_OPTIONS = ImmutableList
-      .of(
-          // TODO(b/73708157): add support -assumenoexternalsideeffects <class_spec>
-          "assumenoexternalsideeffects",
-          // TODO(b/73707404): add support -assumenoescapingparameters <class_spec>
-          "assumenoescapingparameters",
-          // TODO(b/73708085): add support -assumenoexternalreturnvalues <class_spec>
-          "assumenoexternalreturnvalues"
-      );
+  private static final List<String> IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS = ImmutableList.of(
+      "keepdirectories",
+      "runtype",
+      "laststageoutput");
+
+  private static final List<String> IGNORED_FLAG_OPTIONS = ImmutableList.of(
+      "forceprocessing",
+      "dontusemixedcaseclassnames",
+      "dontpreverify",
+      "experimentalshrinkunusedprotofields",
+      "filterlibraryjarswithorginalprogramjars",
+      "dontskipnonpubliclibraryclasses",
+      "dontskipnonpubliclibraryclassmembers",
+      "invokebasemethod",
+      "android");
+
+  private static final List<String> IGNORED_CLASS_DESCRIPTOR_OPTIONS = ImmutableList.of(
+      "isclassnamestring",
+      "whyarenotsimple");
+
+  private static final List<String> WARNED_SINGLE_ARG_OPTIONS = ImmutableList.of(
+      "printconfiguration",
+      // TODO -outjars (http://b/37137994) and -adaptresourcefilecontents (http://b/37139570)
+      // should be reported as errors, not just as warnings!
+      "outjars",
+      "adaptresourcefilecontents");
+
+  private static final List<String> WARNED_FLAG_OPTIONS = ImmutableList.of(
+      // TODO(b/73707846): add support -addconfigurationdebugging
+      "addconfigurationdebugging");
+
+  private static final List<String> WARNED_CLASS_DESCRIPTOR_OPTIONS = ImmutableList.of(
+      // TODO(b/73708157): add support -assumenoexternalsideeffects <class_spec>
+      "assumenoexternalsideeffects",
+      // TODO(b/73707404): add support -assumenoescapingparameters <class_spec>
+      "assumenoescapingparameters",
+      // TODO(b/73708085): add support -assumenoexternalreturnvalues <class_spec>
+      "assumenoexternalreturnvalues");
 
   // Those options are unsupported and are treated as compilation errors.
   // Just ignoring them would produce outputs incompatible with user expectations.
-  private static final List<String> UNSUPPORTED_FLAG_OPTIONS = ImmutableList
-      .of("skipnonpubliclibraryclasses");
+  private static final List<String> UNSUPPORTED_FLAG_OPTIONS =
+      ImmutableList.of("skipnonpubliclibraryclasses");
 
   public ProguardConfigurationParser(
       DexItemFactory dexItemFactory, Reporter reporter) {
@@ -197,28 +193,10 @@
       }
       TextPosition optionStart = getPosition();
       expectChar('-');
-      String option;
-      if (Iterables.any(IGNORED_SINGLE_ARG_OPTIONS, this::skipOptionWithSingleArg)
-          || Iterables.any(
-              IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS, this::skipOptionWithOptionalSingleArg)
-          || Iterables.any(IGNORED_FLAG_OPTIONS, this::skipFlag)
-          || Iterables.any(IGNORED_CLASS_DESCRIPTOR_OPTIONS, this::skipOptionWithClassSpec)
-          || parseOptimizationOption()) {
+      if (parseIgnoredOption() ||
+          parseIgnoredOptionAndWarn(optionStart) ||
+          parseUnsupportedOptionAndErr(optionStart)) {
         // Intentionally left empty.
-      } else if (
-          (option = Iterables.find(WARNED_SINGLE_ARG_OPTIONS,
-              this::skipOptionWithSingleArg, null)) != null
-              || (option = Iterables.find(WARNED_FLAG_OPTIONS,
-                  this::skipFlag, null)) != null
-              || (option = Iterables.find(WARNED_CLASS_DESCRIPTOR_OPTIONS,
-                  this::skipOptionWithClassSpec, null)) != null) {
-        warnIgnoringOptions(option, optionStart);
-      } else if (
-          (option = Iterables.find(UNSUPPORTED_FLAG_OPTIONS, this::skipFlag, null)) != null) {
-        reporter.error(new StringDiagnostic(
-            "Unsupported option: -" + option,
-            origin,
-            getPosition(optionStart)));
       } else if (acceptString("renamesourcefileattribute")) {
         skipWhitespace();
         if (isOptionalArgumentGiven()) {
@@ -368,6 +346,41 @@
       return true;
     }
 
+    private boolean parseUnsupportedOptionAndErr(TextPosition optionStart) {
+      String option = Iterables.find(UNSUPPORTED_FLAG_OPTIONS, this::skipFlag, null);
+      if (option != null) {
+        reporter.error(new StringDiagnostic(
+            "Unsupported option: -" + option, origin, getPosition(optionStart)));
+        return true;
+      }
+      return false;
+    }
+
+    private boolean parseIgnoredOptionAndWarn(TextPosition optionStart) {
+      String option =
+          Iterables.find(WARNED_CLASS_DESCRIPTOR_OPTIONS, this::skipOptionWithClassSpec, null);
+      if (option == null) {
+        option = Iterables.find(WARNED_FLAG_OPTIONS, this::skipFlag, null);
+        if (option == null) {
+          option = Iterables.find(WARNED_SINGLE_ARG_OPTIONS, this::skipOptionWithSingleArg, null);
+          if (option == null) {
+            return false;
+          }
+        }
+      }
+      warnIgnoringOptions(option, optionStart);
+      return true;
+    }
+
+    private boolean parseIgnoredOption() {
+      return Iterables.any(IGNORED_SINGLE_ARG_OPTIONS, this::skipOptionWithSingleArg)
+          || Iterables.any(IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS,
+                           this::skipOptionWithOptionalSingleArg)
+          || Iterables.any(IGNORED_FLAG_OPTIONS, this::skipFlag)
+          || Iterables.any(IGNORED_CLASS_DESCRIPTOR_OPTIONS, this::skipOptionWithClassSpec)
+          || parseOptimizationOption();
+    }
+
     private void parseInclude() throws ProguardRuleParserException {
       TextPosition start = getPosition();
       Path included = parseFileName();
diff --git a/src/main/java/com/android/tools/r8/utils/Reporter.java b/src/main/java/com/android/tools/r8/utils/Reporter.java
index 48a04cd..8ce1c09 100644
--- a/src/main/java/com/android/tools/r8/utils/Reporter.java
+++ b/src/main/java/com/android/tools/r8/utils/Reporter.java
@@ -38,6 +38,7 @@
       errorCount++;
     }
   }
+
   public void error(String message) {
     error(new StringDiagnostic(message));
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 2202332..f63f4f2 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -443,7 +443,12 @@
   private static final String ANGLER_BOOT_IMAGE = ANGLER_DIR + "/system/framework/boot.art";
 
   public static byte[] getClassAsBytes(Class clazz) throws IOException {
-    return ByteStreams.toByteArray(clazz.getResourceAsStream(clazz.getSimpleName() + ".class"));
+    String s = clazz.getSimpleName() + ".class";
+    Class outer = clazz.getEnclosingClass();
+    if (outer != null) {
+      s = outer.getSimpleName() + '$' + s;
+    }
+    return ByteStreams.toByteArray(clazz.getResourceAsStream(s));
   }
 
   public static String getArtDir(DexVm version) {
@@ -883,13 +888,15 @@
     return runJavaNoVerify(path, main);
   }
 
-  public static ProcessResult runJava(Path classpath, String mainClass) throws IOException {
-    return runJava(ImmutableList.of(classpath), mainClass);
+  public static ProcessResult runJava(Path classpath, String... args) throws IOException {
+    return runJava(ImmutableList.of(classpath), args);
   }
 
-  public static ProcessResult runJava(List<Path> classpath, String mainClass) throws IOException {
+  public static ProcessResult runJava(List<Path> classpath, String... args) throws IOException {
     String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR));
-    ProcessBuilder builder = new ProcessBuilder(getJavaExecutable(), "-cp", cp, mainClass);
+    List<String> cmdline = new ArrayList<String>(Arrays.asList(getJavaExecutable(), "-cp", cp));
+    cmdline.addAll(Arrays.asList(args));
+    ProcessBuilder builder = new ProcessBuilder(cmdline);
     return runProcess(builder);
   }
 
@@ -958,6 +965,11 @@
     return runArtRaw(Collections.singletonList(file), mainClass, null);
   }
 
+  public static ProcessResult runArtRaw(
+      String file, String mainClass, Consumer<ArtCommandBuilder> extras) throws IOException {
+    return runArtRaw(Collections.singletonList(file), mainClass, extras);
+  }
+
   public static ProcessResult runArtRaw(List<String> files, String mainClass,
       Consumer<ArtCommandBuilder> extras)
       throws IOException {
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java b/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java
new file mode 100644
index 0000000..c80047a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java
@@ -0,0 +1,189 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import org.objectweb.asm.*;
+
+// The method MethodHandleDump.transform() translates methods in MethodHandleTest that look like
+//     MethodType.methodType(TYPES)
+// and
+//     MethodHandles.lookup().findKIND(FOO.class, "METHOD", TYPE)
+// into LDC instructions.
+// This is necessary since there is no Java syntax that compiles to
+// LDC of a constant method handle or constant method type.
+//
+// The method dumpD() dumps a class equivalent to MethodHandleTest.D
+// that uses an LDC instruction instead of MethodHandles.lookup().findSpecial().
+// The LDC instruction loads an InvokeSpecial constant method handle to a C method,
+// so this LDC instruction must be in a subclass of C, and not directly on MethodHandleTest.
+public class MethodHandleDump implements Opcodes {
+
+  private static final Type viType = Type.getMethodType(Type.VOID_TYPE, Type.INT_TYPE);
+  private static final Type jiType = Type.getMethodType(Type.LONG_TYPE, Type.INT_TYPE);
+  private static final Type vicType =
+      Type.getMethodType(Type.VOID_TYPE, Type.INT_TYPE, Type.CHAR_TYPE);
+  private static final Type jicType =
+      Type.getMethodType(Type.LONG_TYPE, Type.INT_TYPE, Type.CHAR_TYPE);
+  private static final String cDesc = "com/android/tools/r8/cf/MethodHandleTest$C";
+  private static final String iDesc = "com/android/tools/r8/cf/MethodHandleTest$I";
+  private static final String viDesc = viType.getDescriptor();
+  private static final String jiDesc = jiType.getDescriptor();
+  private static final String vicDesc = vicType.getDescriptor();
+  private static final String jicDesc = jicType.getDescriptor();
+  private static final String intDesc = Type.INT_TYPE.getDescriptor();
+
+  public static byte[] transform(byte[] input) throws Exception {
+    ImmutableMap.Builder<String, Type> typesBuilder = ImmutableMap.builder();
+    ImmutableMap<String, Type> types =
+        typesBuilder
+            .put("viType", viType)
+            .put("jiType", jiType)
+            .put("vicType", vicType)
+            .put("jicType", jicType)
+            .build();
+
+    Builder<String, Handle> methodsBuilder = ImmutableMap.builder();
+    methodsBuilder
+        .put("scviMethod", new Handle(H_INVOKESTATIC, cDesc, "svi", viDesc, false))
+        .put("scjiMethod", new Handle(H_INVOKESTATIC, cDesc, "sji", jiDesc, false))
+        .put("scvicMethod", new Handle(H_INVOKESTATIC, cDesc, "svic", vicDesc, false))
+        .put("scjicMethod", new Handle(H_INVOKESTATIC, cDesc, "sjic", jicDesc, false))
+        .put("vcviMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vvi", viDesc, false))
+        .put("vcjiMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vji", jiDesc, false))
+        .put("vcvicMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vvic", vicDesc, false))
+        .put("vcjicMethod", new Handle(H_INVOKEVIRTUAL, cDesc, "vjic", jicDesc, false))
+        .put("siviMethod", new Handle(H_INVOKESTATIC, iDesc, "svi", viDesc, true))
+        .put("sijiMethod", new Handle(H_INVOKESTATIC, iDesc, "sji", jiDesc, true))
+        .put("sivicMethod", new Handle(H_INVOKESTATIC, iDesc, "svic", vicDesc, true))
+        .put("sijicMethod", new Handle(H_INVOKESTATIC, iDesc, "sjic", jicDesc, true))
+        .put("diviMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dvi", viDesc, true))
+        .put("dijiMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dji", jiDesc, true))
+        .put("divicMethod", new Handle(H_INVOKEINTERFACE, iDesc, "dvic", vicDesc, true))
+        .put("dijicMethod", new Handle(H_INVOKEINTERFACE, iDesc, "djic", jicDesc, true))
+        .put("vciSetField", new Handle(H_PUTFIELD, cDesc, "vi", intDesc, false))
+        .put("sciSetField", new Handle(H_PUTSTATIC, cDesc, "si", intDesc, false))
+        .put("vciGetField", new Handle(H_GETFIELD, cDesc, "vi", intDesc, false))
+        .put("sciGetField", new Handle(H_GETSTATIC, cDesc, "si", intDesc, false))
+        .put("iiSetField", new Handle(H_PUTSTATIC, iDesc, "ii", intDesc, true))
+        .put("iiGetField", new Handle(H_GETSTATIC, iDesc, "ii", intDesc, true))
+        .put("constructorMethod", new Handle(H_NEWINVOKESPECIAL, cDesc, "<init>", viDesc, false));
+    ImmutableMap<String, Handle> methods = methodsBuilder.build();
+    ClassReader cr = new ClassReader(input);
+    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
+    cr.accept(
+        new ClassVisitor(ASM6, cw) {
+
+          @Override
+          public MethodVisitor visitMethod(
+              int access, String name, String desc, String signature, String[] exceptions) {
+            switch (desc) {
+              case "()Ljava/lang/invoke/MethodType;":
+                {
+                  Type type = types.get(name);
+                  assert type != null : name;
+                  assert access == ACC_PUBLIC + ACC_STATIC;
+                  assert signature == null;
+                  assert exceptions == null;
+                  MethodVisitor mv = cw.visitMethod(access, name, desc, null, null);
+                  mv.visitCode();
+                  mv.visitLdcInsn(type);
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+              case "()Ljava/lang/invoke/MethodHandle;":
+                {
+                  Handle method = methods.get(name);
+                  assert access == ACC_PUBLIC + ACC_STATIC;
+                  assert method != null : name;
+                  assert signature == null;
+                  assert exceptions == null;
+                  MethodVisitor mv = cw.visitMethod(access, name, desc, null, null);
+                  mv.visitCode();
+                  mv.visitLdcInsn(method);
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+              default:
+                return super.visitMethod(access, name, desc, signature, exceptions);
+            }
+          }
+        },
+        0);
+    return cw.toByteArray();
+  }
+
+  public static byte[] dumpD() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    MethodVisitor mv;
+
+    cw.visit(
+        V1_8,
+        ACC_PUBLIC + ACC_SUPER,
+        "com/android/tools/r8/cf/MethodHandleTest$D",
+        null,
+        "com/android/tools/r8/cf/MethodHandleTest$C",
+        null);
+
+    cw.visitInnerClass(
+        "com/android/tools/r8/cf/MethodHandleTest$D",
+        "com/android/tools/r8/cf/MethodHandleTest",
+        "D",
+        ACC_PUBLIC + ACC_STATIC);
+
+    cw.visitInnerClass(
+        "com/android/tools/r8/cf/MethodHandleTest$C",
+        "com/android/tools/r8/cf/MethodHandleTest",
+        "C",
+        ACC_PUBLIC + ACC_STATIC);
+
+    cw.visitInnerClass(
+        "java/lang/invoke/MethodHandles$Lookup",
+        "java/lang/invoke/MethodHandles",
+        "Lookup",
+        ACC_PUBLIC + ACC_FINAL + ACC_STATIC);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "com/android/tools/r8/cf/MethodHandleTest$C", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv =
+          cw.visitMethod(
+              ACC_PUBLIC + ACC_STATIC,
+              "vcviSpecialMethod",
+              "()Ljava/lang/invoke/MethodHandle;",
+              null,
+              null);
+      mv.visitCode();
+      mv.visitLdcInsn(new Handle(H_INVOKESPECIAL, cDesc, "vvi", viDesc, false));
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(-1, -1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "vvi", "(I)V", null, null);
+      mv.visitCode();
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(0, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java
new file mode 100644
index 0000000..d42d443
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java
@@ -0,0 +1,385 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+
+public class MethodHandleTest {
+
+  public static class C {
+    public C(int i) {
+      System.out.println("C " + i);
+    }
+
+    public C() {
+      System.out.println("C");
+    }
+
+    public int vi;
+    public static int si;
+
+    public static void svi(int i) {
+      System.out.println("svi " + i);
+    }
+
+    public static long sji(int i) {
+      System.out.println("sji " + i);
+      return 42L;
+    }
+
+    public static void svic(int i, char c) {
+      System.out.println("svic " + i);
+    }
+
+    public static long sjic(int i, char c) {
+      System.out.println("sjic " + i);
+      return 42L;
+    }
+
+    public void vvi(int i) {
+      System.out.println("vvi " + i);
+    }
+
+    public long vji(int i) {
+      System.out.println("vji " + i);
+      return 42L;
+    }
+
+    public void vvic(int i, char c) {
+      System.out.println("vvic " + i);
+    }
+
+    public long vjic(int i, char c) {
+      System.out.println("vjic " + i);
+      return 42L;
+    }
+  }
+
+  public static class D extends C {
+    public static MethodHandle vcviSpecialMethod() {
+      try {
+        return MethodHandles.lookup().findSpecial(C.class, "vvi", viType(), D.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public void vvi(int i) {
+      // Overridden to output nothing.
+    }
+  }
+
+  public interface I {
+    int ii = 42;
+
+    static void svi(int i) {
+      System.out.println("svi " + i);
+    }
+
+    static long sji(int i) {
+      System.out.println("sji " + i);
+      return 42L;
+    }
+
+    static void svic(int i, char c) {
+      System.out.println("svic " + i);
+    }
+
+    static long sjic(int i, char c) {
+      System.out.println("sjic " + i);
+      return 42L;
+    }
+
+    default void dvi(int i) {
+      System.out.println("dvi " + i);
+    }
+
+    default long dji(int i) {
+      System.out.println("dji " + i);
+      return 42L;
+    }
+
+    default void dvic(int i, char c) {
+      System.out.println("dvic " + i);
+    }
+
+    default long djic(int i, char c) {
+      System.out.println("djic " + i);
+      return 42L;
+    }
+  }
+
+  public static class Impl implements I {}
+
+  public static void main(String[] args) {
+    // When MethodHandleTestRunner invokes this program with the JVM, "fail" is passed as arg.
+    // When invoked with Art, no arg is passed since interface fields may be modified on Art.
+    String expectedResult = args[0];
+    C c = new C(42);
+    I i = new Impl();
+    try {
+      scviMethod().invoke(1);
+      assertEquals(42L, (long) scjiMethod().invoke(2));
+      scvicMethod().invoke(3, 'x');
+      assertEquals(42L, (long) scjicMethod().invoke(4, 'x'));
+      vcviMethod().invoke(c, 5);
+      assertEquals(42L, (long) vcjiMethod().invoke(c, 6));
+      vcvicMethod().invoke(c, 7, 'x');
+      assertEquals(42L, (long) vcjicMethod().invoke(c, 8, 'x'));
+      siviMethod().invoke(9);
+      assertEquals(42L, (long) sijiMethod().invoke(10));
+      sivicMethod().invoke(11, 'x');
+      assertEquals(42L, (long) sijicMethod().invoke(12, 'x'));
+      diviMethod().invoke(i, 13);
+      assertEquals(42L, (long) dijiMethod().invoke(i, 14));
+      divicMethod().invoke(i, 15, 'x');
+      assertEquals(42L, (long) dijicMethod().invoke(i, 16, 'x'));
+      vciSetField().invoke(c, 17);
+      assertEquals(17, (int) vciGetField().invoke(c));
+      sciSetField().invoke(18);
+      assertEquals(18, (int) sciGetField().invoke());
+      String interfaceSetResult;
+      try {
+        iiSetField().invoke(19);
+        interfaceSetResult = "pass";
+      } catch (RuntimeException e) {
+        if (e.getCause() instanceof IllegalAccessException) {
+          interfaceSetResult = "exception";
+        } else {
+          throw e;
+        }
+      } catch (IllegalAccessError e) {
+        interfaceSetResult = "error";
+      }
+      if (!interfaceSetResult.equals(expectedResult)) {
+        throw new RuntimeException(
+            "Wrong outcome of iiSetField().invoke(): Expected "
+                + expectedResult
+                + " but got "
+                + interfaceSetResult);
+      }
+      assertEquals(interfaceSetResult.equals("pass") ? 19 : 42, (int) iiGetField().invoke());
+      MethodHandle methodHandle = D.vcviSpecialMethod();
+      methodHandle.invoke(new D(), 20);
+      constructorMethod().invoke(21);
+    } catch (Throwable e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static void assertEquals(long l, long x) {
+    if (l != x) {
+      throw new AssertionError("Not equal: " + l + " != " + x);
+    }
+  }
+
+  private static void assertEquals(int l, int x) {
+    if (l != x) {
+      throw new AssertionError("Not equal: " + l + " != " + x);
+    }
+  }
+
+  public static MethodType viType() {
+    return MethodType.methodType(void.class, int.class);
+  }
+
+  public static MethodType jiType() {
+    return MethodType.methodType(long.class, int.class);
+  }
+
+  public static MethodType vicType() {
+    return MethodType.methodType(void.class, int.class, char.class);
+  }
+
+  public static MethodType jicType() {
+    return MethodType.methodType(long.class, int.class, char.class);
+  }
+
+  public static MethodHandle scviMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(C.class, "svi", viType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle scjiMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(C.class, "sji", jiType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle scvicMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(C.class, "svic", vicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle scjicMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(C.class, "sjic", jicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle vcviMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(C.class, "vvi", viType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle vcjiMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(C.class, "vji", jiType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle vcvicMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(C.class, "vvic", vicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle vcjicMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(C.class, "vjic", jicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle siviMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(I.class, "svi", viType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle sijiMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(I.class, "sji", jiType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle sivicMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(I.class, "svic", vicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle sijicMethod() {
+    try {
+      return MethodHandles.lookup().findStatic(I.class, "sjic", jicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle diviMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(I.class, "dvi", viType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle dijiMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(I.class, "dji", jiType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle divicMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(I.class, "dvic", vicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle dijicMethod() {
+    try {
+      return MethodHandles.lookup().findVirtual(I.class, "djic", jicType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle vciSetField() {
+    try {
+      return MethodHandles.lookup().findSetter(C.class, "vi", int.class);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle sciSetField() {
+    try {
+      return MethodHandles.lookup().findStaticSetter(C.class, "si", int.class);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle vciGetField() {
+    try {
+      return MethodHandles.lookup().findGetter(C.class, "vi", int.class);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle sciGetField() {
+    try {
+      return MethodHandles.lookup().findStaticGetter(C.class, "si", int.class);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle iiSetField() {
+    try {
+      return MethodHandles.lookup().findStaticSetter(I.class, "ii", int.class);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle iiGetField() {
+    try {
+      return MethodHandles.lookup().findStaticGetter(I.class, "ii", int.class);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static MethodHandle constructorMethod() {
+    try {
+      return MethodHandles.lookup().findConstructor(C.class, viType());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
new file mode 100644
index 0000000..3870598
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -0,0 +1,120 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.nio.file.Path;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class MethodHandleTestRunner {
+  static final Class<?> CLASS = MethodHandleTest.class;
+
+  private boolean ldc = false;
+
+  @Rule public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void testMethodHandlesLookup() throws Exception {
+    // Run test with dynamic method lookups, i.e. using MethodHandles.lookup().find*()
+    ldc = false;
+    test();
+  }
+
+  @Test
+  public void testLdcMethodHandle() throws Exception {
+    // Run test with LDC methods, i.e. without java.lang.invoke.MethodHandles
+    ldc = true;
+    test();
+  }
+
+  private final Class[] inputClasses = {
+    MethodHandleTest.class,
+    MethodHandleTest.C.class,
+    MethodHandleTest.I.class,
+    MethodHandleTest.Impl.class,
+    MethodHandleTest.D.class,
+  };
+
+  private void test() throws Exception {
+    ProcessResult runInput = runInput();
+    Path outCf = temp.getRoot().toPath().resolve("cf.jar");
+    build(new ClassFileConsumer.ArchiveConsumer(outCf));
+    Path outDex = temp.getRoot().toPath().resolve("dex.zip");
+    build(new DexIndexedConsumer.ArchiveConsumer(outDex));
+
+    ProcessResult runCf =
+        ToolHelper.runJava(outCf, CLASS.getCanonicalName(), ldc ? "error" : "exception");
+    ProcessResult runDex =
+        ToolHelper.runArtRaw(
+            outDex.toString(),
+            CLASS.getCanonicalName(),
+            cmd -> cmd.appendProgramArgument(ldc ? "pass" : "exception"));
+    assertEquals(runInput.toString(), runCf.toString());
+    // Only compare stdout and exitCode since dex2oat prints to stderr.
+    if (runInput.exitCode != runDex.exitCode) {
+      System.out.println(runDex.stderr);
+    }
+    assertEquals(runInput.stdout, runDex.stdout);
+    assertEquals(runInput.exitCode, runDex.exitCode);
+  }
+
+  private void build(ProgramConsumer programConsumer) throws Exception {
+    // MethodHandle.invoke() only supported from Android O
+    // ConstMethodHandle only supported from Android P
+    AndroidApiLevel apiLevel = AndroidApiLevel.P;
+    Builder cfBuilder =
+        R8Command.builder()
+            .setMinApiLevel(apiLevel.getLevel())
+            .setMode(CompilationMode.DEBUG)
+            .addLibraryFiles(ToolHelper.getAndroidJar(apiLevel))
+            .setProgramConsumer(programConsumer);
+    for (Class<?> c : inputClasses) {
+      byte[] classAsBytes = getClassAsBytes(c);
+      cfBuilder.addClassProgramData(classAsBytes, Origin.unknown());
+    }
+    R8.run(cfBuilder.build());
+  }
+
+  private ProcessResult runInput() throws Exception {
+    Path out = temp.getRoot().toPath().resolve("input.jar");
+    ClassFileConsumer.ArchiveConsumer archiveConsumer = new ClassFileConsumer.ArchiveConsumer(out);
+    for (Class<?> c : inputClasses) {
+      archiveConsumer.accept(
+          getClassAsBytes(c), DescriptorUtils.javaTypeToDescriptor(c.getName()), null);
+    }
+    archiveConsumer.finished(null);
+    ProcessResult runInput = ToolHelper.runJava(out, CLASS.getName(), ldc ? "error" : "exception");
+    if (runInput.exitCode != 0) {
+      System.out.println(runInput);
+    }
+    assertEquals(0, runInput.exitCode);
+    return runInput;
+  }
+
+  private byte[] getClassAsBytes(Class<?> clazz) throws Exception {
+    if (ldc) {
+      if (clazz == MethodHandleTest.D.class) {
+        return MethodHandleDump.dumpD();
+      } else if (clazz == MethodHandleTest.class) {
+        return MethodHandleDump.transform(ToolHelper.getClassAsBytes(clazz));
+      }
+    }
+    return ToolHelper.getClassAsBytes(clazz);
+  }
+}