Support compiling with features that are not supported in DEX.

This change refactors the errors for unsupported features to non-final
errors. The --map-diagnostics flag/API can be used to make these
warnings instead. In so doing, the compiler will replace the unsupported
instructions by unconditional throwing instructions with a descriptive
error message.

This CL also adds additional diagnostics types for each of the
unsupported features as part of the future compiler API.

Bug: b/174733673
Bug: b/154778581
Bug: b/198142625
Change-Id: I05d7965d7f9d49b97a5ac06bce301d7a861be802
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
index d52f83c..4bff728 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
@@ -14,10 +14,10 @@
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InitClassLens;
 import com.android.tools.r8.graph.JarApplicationReader;
@@ -34,10 +34,11 @@
 import com.android.tools.r8.optimize.interfaces.analysis.CfAnalysisConfig;
 import com.android.tools.r8.optimize.interfaces.analysis.CfFrameState;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.ListIterator;
 import org.objectweb.asm.ConstantDynamic;
 import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
 
 public class CfConstDynamic extends CfInstruction implements CfTypeInstruction {
 
@@ -47,12 +48,12 @@
       DexString name,
       DexType type,
       DexMethodHandle bootstrapMethod,
-      Object[] bootstrapMethodArguments) {
+      List<DexValue> bootstrapMethodArguments) {
     assert name != null;
     assert type != null;
     assert bootstrapMethod != null;
     assert bootstrapMethodArguments != null;
-    assert bootstrapMethodArguments.length == 0;
+    assert bootstrapMethodArguments.isEmpty();
 
     reference = new ConstantDynamicReference(name, type, bootstrapMethod, bootstrapMethodArguments);
   }
@@ -79,7 +80,7 @@
     return reference.getBootstrapMethod();
   }
 
-  public Object[] getBootstrapMethodArguments() {
+  public List<DexValue> getBootstrapMethodArguments() {
     return reference.getBootstrapMethodArguments();
   }
 
@@ -87,53 +88,20 @@
       ConstantDynamic insn, JarApplicationReader application, DexType clazz) {
     String constantName = insn.getName();
     String constantDescriptor = insn.getDescriptor();
-    // TODO(b/178172809): Handle bootstrap arguments.
-    if (insn.getBootstrapMethodArgumentCount() > 0) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (has arguments to bootstrap method)");
-    }
-    if (insn.getBootstrapMethod().getTag() != Opcodes.H_INVOKESTATIC) {
-      throw new CompilationError("Unsupported dynamic constant (not invoke static)");
-    }
-    if (insn.getBootstrapMethod().getOwner().equals("java/lang/invoke/ConstantBootstraps")) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (runtime provided bootstrap method)");
-    }
-    if (application.getTypeFromName(insn.getBootstrapMethod().getOwner()) != clazz) {
-      throw new CompilationError("Unsupported dynamic constant (different owner)");
-    }
-    // Resolve the bootstrap method.
     DexMethodHandle bootstrapMethodHandle =
         DexMethodHandle.fromAsmHandle(insn.getBootstrapMethod(), application, clazz);
-    if (!bootstrapMethodHandle.member.isDexMethod()) {
-      throw new CompilationError("Unsupported dynamic constant (invalid method handle)");
-    }
-    DexMethod bootstrapMethod = bootstrapMethodHandle.asMethod();
-    if (bootstrapMethod.getProto().returnType != application.getTypeFromDescriptor("[Z")
-        && bootstrapMethod.getProto().returnType
-            != application.getTypeFromDescriptor("Ljava/lang/Object;")) {
-      throw new CompilationError("Unsupported dynamic constant (unsupported constant type)");
-    }
-    if (bootstrapMethod.getProto().getParameters().size() != 3) {
-      throw new CompilationError("Unsupported dynamic constant (unsupported signature)");
-    }
-    if (bootstrapMethod.getProto().getParameters().get(0) != application.getFactory().lookupType) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (unexpected type of first argument to bootstrap method");
-    }
-    if (bootstrapMethod.getProto().getParameters().get(1) != application.getFactory().stringType) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (unexpected type of second argument to bootstrap method");
-    }
-    if (bootstrapMethod.getProto().getParameters().get(2) != application.getFactory().classType) {
-      throw new CompilationError(
-          "Unsupported dynamic constant (unexpected type of third argument to bootstrap method");
+    int argumentCount = insn.getBootstrapMethodArgumentCount();
+    List<DexValue> bootstrapMethodArguments = new ArrayList<>(argumentCount);
+    for (int i = 0; i < argumentCount; i++) {
+      Object argument = insn.getBootstrapMethodArgument(i);
+      DexValue dexValue = DexValue.fromAsmBootstrapArgument(argument, application, clazz);
+      bootstrapMethodArguments.add(dexValue);
     }
     return new CfConstDynamic(
         application.getString(constantName),
         application.getTypeFromDescriptor(constantDescriptor),
         bootstrapMethodHandle,
-        new Object[] {});
+        bootstrapMethodArguments);
   }
 
   @Override
@@ -186,8 +154,30 @@
       NamingLens namingLens,
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
-    // TODO(b/198142625): Support CONSTANT_Dynamic for R8 cf to cf.
-    throw new CompilationError("Unsupported dynamic constant (not desugaring)");
+    DexMethodHandle rewrittenHandle =
+        rewriter.rewriteDexMethodHandle(
+            reference.getBootstrapMethod(), NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context);
+    List<DexValue> rewrittenArguments =
+        rewriter.rewriteBootstrapArguments(
+            reference.getBootstrapMethodArguments(), NOT_ARGUMENT_TO_LAMBDA_METAFACTORY, context);
+    Object[] bsmArgs = new Object[rewrittenArguments.size()];
+    for (int i = 0; i < rewrittenArguments.size(); i++) {
+      bsmArgs[i] = CfInvokeDynamic.decodeBootstrapArgument(rewrittenArguments.get(i), namingLens);
+    }
+    ConstantDynamic constantDynamic =
+        new ConstantDynamic(
+            reference.getName().toString(),
+            getConstantTypeDescriptor(graphLens, namingLens, dexItemFactory),
+            rewrittenHandle.toAsmHandle(namingLens),
+            bsmArgs);
+    visitor.visitLdcInsn(constantDynamic);
+  }
+
+  private String getConstantTypeDescriptor(
+      GraphLens graphLens, NamingLens namingLens, DexItemFactory factory) {
+    DexType rewrittenType = graphLens.lookupType(reference.getType());
+    DexType renamedType = namingLens.lookupType(rewrittenType, factory);
+    return renamedType.toDescriptorString();
   }
 
   @Override
@@ -212,7 +202,7 @@
     registry.registerTypeReference(reference.getType());
     registry.registerMethodHandle(
         reference.getBootstrapMethod(), NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
-    assert reference.getBootstrapMethodArguments().length == 0;
+    assert reference.getBootstrapMethodArguments().isEmpty();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
index 45228d7..b880c47 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
@@ -98,7 +98,7 @@
     return 5;
   }
 
-  private Object decodeBootstrapArgument(DexValue value, NamingLens lens) {
+  public static Object decodeBootstrapArgument(DexValue value, NamingLens lens) {
     switch (value.getValueKind()) {
       case DOUBLE:
         return value.asDexValueDouble().getValue();
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 6167f9b..a6f6a52 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -7,10 +7,10 @@
 
 import com.android.tools.r8.ByteBufferProvider;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.DefaultInterfaceMethodDiagnostic;
-import com.android.tools.r8.errors.InvokeCustomDiagnostic;
-import com.android.tools.r8.errors.PrivateInterfaceMethodDiagnostic;
-import com.android.tools.r8.errors.StaticInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedDefaultInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedPrivateInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedStaticInterfaceMethodDiagnostic;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
@@ -290,9 +290,9 @@
       if (!options.canUseDefaultAndStaticInterfaceMethods()
           && !options.testing.allowStaticInterfaceMethodsForPreNApiLevel) {
         throw options.reporter.fatalError(
-            new StaticInterfaceMethodDiagnostic(holder.getOrigin(), MethodPosition.create(method)));
+            new UnsupportedStaticInterfaceMethodDiagnostic(
+                holder.getOrigin(), MethodPosition.create(method)));
       }
-
     } else {
       if (method.isInstanceInitializer()) {
         throw new CompilationError(
@@ -301,7 +301,7 @@
       if (!method.accessFlags.isAbstract() && !method.accessFlags.isPrivate() &&
           !options.canUseDefaultAndStaticInterfaceMethods()) {
         throw options.reporter.fatalError(
-            new DefaultInterfaceMethodDiagnostic(
+            new UnsupportedDefaultInterfaceMethodDiagnostic(
                 holder.getOrigin(), MethodPosition.create(method)));
       }
     }
@@ -311,7 +311,8 @@
         return;
       }
       throw options.reporter.fatalError(
-          new PrivateInterfaceMethodDiagnostic(holder.getOrigin(), MethodPosition.create(method)));
+          new UnsupportedPrivateInterfaceMethodDiagnostic(
+              holder.getOrigin(), MethodPosition.create(method)));
     }
 
     if (!method.accessFlags.isPublic()) {
@@ -1382,7 +1383,7 @@
   private void checkThatInvokeCustomIsAllowed() {
     if (!options.canUseInvokeCustom()) {
       throw options.reporter.fatalError(
-          new InvokeCustomDiagnostic(Origin.unknown(), Position.UNKNOWN));
+          new UnsupportedInvokeCustomDiagnostic(Origin.unknown(), Position.UNKNOWN));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/errors/ConstMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ConstMethodHandleDiagnostic.java
deleted file mode 100644
index d77a5f5..0000000
--- a/src/main/java/com/android/tools/r8/errors/ConstMethodHandleDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class ConstMethodHandleDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public ConstMethodHandleDiagnostic(Origin origin, MethodPosition position) {
-    super("const-method-handle", AndroidApiLevel.P, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.P, "Const-method-handle", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/ConstMethodTypeDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ConstMethodTypeDiagnostic.java
deleted file mode 100644
index 2a9eff5..0000000
--- a/src/main/java/com/android/tools/r8/errors/ConstMethodTypeDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class ConstMethodTypeDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public ConstMethodTypeDiagnostic(Origin origin, MethodPosition position) {
-    super("const-method-type", AndroidApiLevel.P, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.P, "Const-method-type", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/ConstantDynamicDesugarDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ConstantDynamicDesugarDiagnostic.java
new file mode 100644
index 0000000..d80b530
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/ConstantDynamicDesugarDiagnostic.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2022, 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.errors;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+/** Common type for all diagnostics related to constant-dynamic desugaring. */
+@Keep
+public class ConstantDynamicDesugarDiagnostic implements DesugarDiagnostic {
+
+  private final Origin origin;
+  private final Position position;
+  private final String message;
+
+  public ConstantDynamicDesugarDiagnostic(Origin origin, Position position, String message) {
+    this.origin = origin;
+    this.position = position;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/DefaultInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/DefaultInterfaceMethodDiagnostic.java
deleted file mode 100644
index 21b15b2..0000000
--- a/src/main/java/com/android/tools/r8/errors/DefaultInterfaceMethodDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2020, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class DefaultInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public DefaultInterfaceMethodDiagnostic(Origin origin, MethodPosition position) {
-    super("default-interface-method", AndroidApiLevel.N, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.N, "Default interface methods", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/InvokeCustomDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InvokeCustomDiagnostic.java
deleted file mode 100644
index 0f8c6ab..0000000
--- a/src/main/java/com/android/tools/r8/errors/InvokeCustomDiagnostic.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) 2020, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.Position;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class InvokeCustomDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public InvokeCustomDiagnostic(Origin origin, Position position) {
-    super("invoke-custom", AndroidApiLevel.O, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(AndroidApiLevel.O, "Invoke-customs", null);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InvokePolymorphicMethodHandleDiagnostic.java
deleted file mode 100644
index 1efbb31..0000000
--- a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicMethodHandleDiagnostic.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2022, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class InvokePolymorphicMethodHandleDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public InvokePolymorphicMethodHandleDiagnostic(Origin origin, MethodPosition position) {
-    super("invoke-polymorphic-method-handle", AndroidApiLevel.O, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.O,
-        "MethodHandle.invoke and MethodHandle.invokeExact",
-        getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicVarHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/InvokePolymorphicVarHandleDiagnostic.java
deleted file mode 100644
index 9758581..0000000
--- a/src/main/java/com/android/tools/r8/errors/InvokePolymorphicVarHandleDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class InvokePolymorphicVarHandleDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public InvokePolymorphicVarHandleDiagnostic(Origin origin, MethodPosition position) {
-    super("invoke-polymorphic-var-handle", AndroidApiLevel.P, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.P, "Call to polymorphic signature of VarHandle", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/PrivateInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/PrivateInterfaceMethodDiagnostic.java
deleted file mode 100644
index 9c85254..0000000
--- a/src/main/java/com/android/tools/r8/errors/PrivateInterfaceMethodDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2020, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class PrivateInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public PrivateInterfaceMethodDiagnostic(Origin origin, MethodPosition position) {
-    super("private-interface-method", AndroidApiLevel.N, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.N, "Private interface methods", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/StaticInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/StaticInterfaceMethodDiagnostic.java
deleted file mode 100644
index 9bc4f48..0000000
--- a/src/main/java/com/android/tools/r8/errors/StaticInterfaceMethodDiagnostic.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2020, 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.errors;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.utils.AndroidApiLevel;
-
-public class StaticInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
-
-  public StaticInterfaceMethodDiagnostic(Origin origin, MethodPosition position) {
-    super("static-interface-method", AndroidApiLevel.N, origin, position);
-  }
-
-  @Override
-  public String getDiagnosticMessage() {
-    return UnsupportedFeatureDiagnostic.makeMessage(
-        AndroidApiLevel.N, "Static interface methods", getPosition().toString());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedConstDynamicDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedConstDynamicDiagnostic.java
new file mode 100644
index 0000000..b75f686
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedConstDynamicDiagnostic.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2022, 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.errors;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.utils.InternalOptions;
+
+@Keep
+public class UnsupportedConstDynamicDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "const-dynamic";
+
+  public UnsupportedConstDynamicDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, InternalOptions.constantDynamicApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        InternalOptions.constantDynamicApiLevel(), DESCRIPTOR, getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodHandleDiagnostic.java
new file mode 100644
index 0000000..375adf1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodHandleDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.constantMethodHandleApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedConstMethodHandleDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "const-method-handle";
+
+  public UnsupportedConstMethodHandleDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, constantMethodHandleApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        constantMethodHandleApiLevel(), DESCRIPTOR, getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodTypeDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodTypeDiagnostic.java
new file mode 100644
index 0000000..5809011
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedConstMethodTypeDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.constantMethodTypeApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedConstMethodTypeDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "const-method-type";
+
+  public UnsupportedConstMethodTypeDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, constantMethodTypeApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        constantMethodTypeApiLevel(), DESCRIPTOR, getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedDefaultInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedDefaultInterfaceMethodDiagnostic.java
new file mode 100644
index 0000000..6905dcc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedDefaultInterfaceMethodDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.defaultInterfaceMethodsApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedDefaultInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "default-interface-method";
+
+  public UnsupportedDefaultInterfaceMethodDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, defaultInterfaceMethodsApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        defaultInterfaceMethodsApiLevel(), "Default interface methods", getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java
index 74c3ad6..1ac0482 100644
--- a/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedFeatureDiagnostic.java
@@ -15,12 +15,14 @@
   public static String makeMessage(
       AndroidApiLevel minApiLevel, String unsupportedFeatures, String sourceString) {
     String message =
-        unsupportedFeatures
-            + " are only supported starting with "
-            + minApiLevel.getName()
-            + " (--min-api "
-            + minApiLevel.getLevel()
-            + ")";
+        minApiLevel == null
+            ? (unsupportedFeatures + " are not supported at any API level known by the compiler")
+            : (unsupportedFeatures
+                + " are only supported starting with "
+                + minApiLevel.getName()
+                + " (--min-api "
+                + minApiLevel.getLevel()
+                + ")");
     message = (sourceString != null) ? message + ": " + sourceString : message;
     return message;
   }
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedInvokeCustomDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokeCustomDiagnostic.java
new file mode 100644
index 0000000..ca67569
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokeCustomDiagnostic.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.invokeCustomApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedInvokeCustomDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "invoke-custom";
+
+  public UnsupportedInvokeCustomDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, invokeCustomApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(invokeCustomApiLevel(), "Invoke-customs", null);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicMethodHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicMethodHandleDiagnostic.java
new file mode 100644
index 0000000..23718f3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicMethodHandleDiagnostic.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.invokePolymorphicOnMethodHandleApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedInvokePolymorphicMethodHandleDiagnostic
+    extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "invoke-polymorphic-method-handle";
+
+  public UnsupportedInvokePolymorphicMethodHandleDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, invokePolymorphicOnMethodHandleApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        invokePolymorphicOnMethodHandleApiLevel(),
+        "MethodHandle.invoke and MethodHandle.invokeExact",
+        getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicVarHandleDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicVarHandleDiagnostic.java
new file mode 100644
index 0000000..49f73a6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedInvokePolymorphicVarHandleDiagnostic.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.invokePolymorphicOnVarHandleApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedInvokePolymorphicVarHandleDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "invoke-polymorphic-var-handle";
+
+  public UnsupportedInvokePolymorphicVarHandleDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, invokePolymorphicOnVarHandleApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        invokePolymorphicOnVarHandleApiLevel(),
+        "Call to polymorphic signature of VarHandle",
+        getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedPrivateInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedPrivateInterfaceMethodDiagnostic.java
new file mode 100644
index 0000000..0a52151
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedPrivateInterfaceMethodDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.privateInterfaceMethodsApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedPrivateInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "private-interface-method";
+
+  public UnsupportedPrivateInterfaceMethodDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, privateInterfaceMethodsApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        privateInterfaceMethodsApiLevel(), "Private interface methods", getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/errors/UnsupportedStaticInterfaceMethodDiagnostic.java b/src/main/java/com/android/tools/r8/errors/UnsupportedStaticInterfaceMethodDiagnostic.java
new file mode 100644
index 0000000..3220744
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/UnsupportedStaticInterfaceMethodDiagnostic.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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.errors;
+
+import static com.android.tools.r8.utils.InternalOptions.staticInterfaceMethodsApiLevel;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class UnsupportedStaticInterfaceMethodDiagnostic extends UnsupportedFeatureDiagnostic {
+
+  // API: MUST NOT CHANGE!
+  private static final String DESCRIPTOR = "static-interface-method";
+
+  public UnsupportedStaticInterfaceMethodDiagnostic(Origin origin, Position position) {
+    super(DESCRIPTOR, staticInterfaceMethodsApiLevel(), origin, position);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return UnsupportedFeatureDiagnostic.makeMessage(
+        staticInterfaceMethodsApiLevel(), "Static interface methods", getPosition().toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 68deb8e..9da9fd8 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -422,11 +422,13 @@
       parameterLabel.write(
           appView, method, dexItemFactory, graphLens, initClassLens, namingLens, rewriter, visitor);
     }
+    boolean discardFrames =
+        classFileVersion.isLessThan(CfVersion.V1_6)
+            || (appView.enableWholeProgramOptimizations()
+                && classFileVersion.isEqualTo(CfVersion.V1_6)
+                && !options.shouldKeepStackMapTable());
     for (CfInstruction instruction : instructions) {
-      if (instruction instanceof CfFrame
-          && (classFileVersion.isLessThan(CfVersion.V1_6)
-              || (classFileVersion.isEqualTo(CfVersion.V1_6)
-                  && !options.shouldKeepStackMapTable()))) {
+      if (discardFrames && instruction instanceof CfFrame) {
         continue;
       }
       instruction.write(
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index fbd4b5a..c0aaa95 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -666,6 +666,8 @@
 
   public final DexType metafactoryType =
       createStaticallyKnownType("Ljava/lang/invoke/LambdaMetafactory;");
+  public final DexType constantBootstrapsType =
+      createStaticallyKnownType("Ljava/lang/invoke/ConstantBootstraps;");
   public final DexType callSiteType = createStaticallyKnownType("Ljava/lang/invoke/CallSite;");
   public final DexType lookupType =
       createStaticallyKnownType("Ljava/lang/invoke/MethodHandles$Lookup;");
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index 2cb6abd..c455396 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -312,7 +312,7 @@
 
   public abstract AbstractValue toAbstractValue(AbstractValueFactory factory);
 
-  static DexValue fromAsmBootstrapArgument(
+  public static DexValue fromAsmBootstrapArgument(
       Object value, JarApplicationReader application, DexType clazz) {
     if (value instanceof Integer) {
       return DexValue.DexValueInt.create((Integer) value);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 70fddd9..4a29cb6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -17,12 +17,8 @@
 
 import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.ConstMethodHandleDiagnostic;
-import com.android.tools.r8.errors.ConstMethodTypeDiagnostic;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
-import com.android.tools.r8.errors.InvokePolymorphicMethodHandleDiagnostic;
-import com.android.tools.r8.errors.InvokePolymorphicVarHandleDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
@@ -483,6 +479,14 @@
     this.basicBlockNumberGenerator = new NumberGenerator();
   }
 
+  private Origin getOrigin() {
+    return origin;
+  }
+
+  private MethodPosition getPosition() {
+    return MethodPosition.create(method);
+  }
+
   public DexItemFactory dexItemFactory() {
     return appView.dexItemFactory();
   }
@@ -1226,11 +1230,7 @@
   }
 
   public void addConstMethodHandle(int dest, DexMethodHandle methodHandle) {
-    if (!appView.options().canUseConstantMethodHandle()) {
-      throw appView
-          .reporter()
-          .fatalError(new ConstMethodHandleDiagnostic(origin, MethodPosition.create(method)));
-    }
+    assert appView.options().canUseConstantMethodHandle();
     TypeElement typeLattice =
         TypeElement.fromDexType(
             appView.dexItemFactory().methodHandleType, definitelyNotNull(), appView);
@@ -1240,11 +1240,7 @@
   }
 
   public void addConstMethodType(int dest, DexProto methodType) {
-    if (!appView.options().canUseConstantMethodType()) {
-      throw appView
-          .reporter()
-          .fatalError(new ConstMethodTypeDiagnostic(origin, MethodPosition.create(method)));
-    }
+    assert appView.options().canUseConstantMethodType();
     TypeElement typeLattice =
         TypeElement.fromDexType(
             appView.dexItemFactory().methodTypeType, definitelyNotNull(), appView);
@@ -1502,23 +1498,23 @@
     add(new RecordFieldValues(fields, out, arguments));
   }
 
+  private boolean verifyRepresentablePolymorphicInvoke(Type type, DexItem item) {
+    if (type != Type.POLYMORPHIC) {
+      return true;
+    }
+    assert item instanceof DexMethod;
+    if (((DexMethod) item).holder == appView.dexItemFactory().methodHandleType) {
+      assert appView.options().canUseInvokePolymorphicOnMethodHandle();
+    }
+    if (((DexMethod) item).holder == appView.dexItemFactory().varHandleType) {
+      assert appView.options().canUseInvokePolymorphicOnVarHandle();
+    }
+    return true;
+  }
+
   public void addInvoke(
       Type type, DexItem item, DexProto callSiteProto, List<Value> arguments, boolean itf) {
-    if (type == Type.POLYMORPHIC) {
-      assert item instanceof DexMethod;
-      if (!appView.options().canUseInvokePolymorphic()) {
-        throw appView
-            .reporter()
-            .fatalError(
-                new InvokePolymorphicMethodHandleDiagnostic(origin, MethodPosition.create(method)));
-      } else if (!appView.options().canUseInvokePolymorphicOnVarHandle()
-          && ((DexMethod) item).holder == appView.dexItemFactory().varHandleType) {
-        throw appView
-            .reporter()
-            .fatalError(
-                new InvokePolymorphicVarHandleDiagnostic(origin, MethodPosition.create(method)));
-      }
-    }
+    assert verifyRepresentablePolymorphicInvoke(type, item);
     add(Invoke.create(type, item, callSiteProto, null, arguments, itf));
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index b0694ab..dfc82e0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -544,7 +544,8 @@
     if (options.testing.forceIRForCfToCfDesugar) {
       return true;
     }
-    return !options.isCfDesugaring();
+    assert method.getDefinition().getCode().isCfCode();
+    return !options.isGeneratingClassFiles();
   }
 
   private void checkPrefixMerging(ProgramMethod method) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
index 4adcf5f..eb12093 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
@@ -181,7 +181,7 @@
     return methodHandle;
   }
 
-  private List<DexValue> rewriteBootstrapArguments(
+  public List<DexValue> rewriteBootstrapArguments(
       List<DexValue> bootstrapArgs, MethodHandleUse use, ProgramMethod context) {
     List<DexValue> newBootstrapArgs = null;
     boolean changed = false;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index a0fd25e..3e41a70 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -137,6 +137,7 @@
     if (recordRewriter != null) {
       desugarings.add(recordRewriter);
     }
+    yieldingDesugarings.add(new UnrepresentableInDexInstructionRemover(appView));
   }
 
   static NonEmptyCfInstructionDesugaringCollection createForCfToCfNonDesugar(AppView<?> appView) {
@@ -157,6 +158,8 @@
         new NonEmptyCfInstructionDesugaringCollection(appView, noAndroidApiLevelCompute());
     desugaringCollection.desugarings.add(new InvokeSpecialToSelfDesugaring(appView));
     desugaringCollection.desugarings.add(new InvokeToPrivateRewriter());
+    desugaringCollection.yieldingDesugarings.add(
+        new UnrepresentableInDexInstructionRemover(appView));
     return desugaringCollection;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/UnrepresentableInDexInstructionRemover.java b/src/main/java/com/android/tools/r8/ir/desugar/UnrepresentableInDexInstructionRemover.java
new file mode 100644
index 0000000..6d6dc0f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/UnrepresentableInDexInstructionRemover.java
@@ -0,0 +1,397 @@
+// Copyright (c) 2022, 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.ir.desugar;
+
+import static com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.synthesizeThrowRuntimeExceptionWithMessageMethod;
+
+import com.android.tools.r8.cf.code.CfConstDynamic;
+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;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodTypeDiagnostic;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicMethodHandleDiagnostic;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.MethodPosition;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Non desugared invoke-dynamic instructions as well as MethodHandle.invokeX instructions cannot be
+ * represented below O API level. Desugar them into throwing stubs to allow compilation to proceed
+ * under the assumption that the code is dead code.
+ */
+public class UnrepresentableInDexInstructionRemover implements CfInstructionDesugaring {
+
+  private abstract static class InstructionMatcher {
+    final AppView<?> appView;
+    final String descriptor;
+    final AndroidApiLevel supportedApiLevel;
+    // TODO(b/237250957): Using ConcurrentHashMap.newKeySet() causes failures on:
+    //  HelloWorldCompiledOnArtTest.testHelloCompiledWithX8Dex[Y, api:21, spec: JDK8, D8_L8DEBUG]
+    final Set<DexMethod> reported = Sets.newConcurrentHashSet();
+
+    InstructionMatcher(AppView<?> appView, String descriptor, AndroidApiLevel supportedApiLevel) {
+      this.appView = appView;
+      this.descriptor = descriptor;
+      this.supportedApiLevel = supportedApiLevel;
+    }
+
+    // Rewrite implementation for each instruction case.
+    abstract DesugarDescription compute(CfInstruction instruction);
+
+    abstract UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position);
+
+    // Helpers
+
+    void report(ProgramMethod context) {
+      if (reported.add(context.getReference())) {
+        UnsupportedFeatureDiagnostic diagnostic =
+            makeDiagnostic(context.getOrigin(), MethodPosition.create(context));
+        assert (diagnostic.getSupportedApiLevel() == -1 && supportedApiLevel == null)
+            || (diagnostic.getSupportedApiLevel() == supportedApiLevel.getLevel());
+        appView.reporter().error(diagnostic);
+      }
+    }
+
+    void invokeThrowingStub(
+        MethodProcessingContext methodProcessingContext,
+        CfInstructionDesugaringEventConsumer eventConsumer,
+        ProgramMethod context,
+        ImmutableList.Builder<CfInstruction> builder) {
+      UtilityMethodForCodeOptimizations throwUtility =
+          synthesizeThrowRuntimeExceptionWithMessageMethod(appView, methodProcessingContext);
+      ProgramMethod throwMethod = throwUtility.uncheckedGetMethod();
+      eventConsumer.acceptThrowMethod(throwMethod, context);
+      builder.add(
+          createMessageString(),
+          new CfInvoke(Opcodes.INVOKESTATIC, throwMethod.getReference(), false),
+          new CfStackInstruction(Opcode.Pop));
+    }
+
+    CfConstString createMessageString() {
+      return new CfConstString(
+          appView
+              .dexItemFactory()
+              .createString(
+                  "Instruction is unrepresentable in DEX "
+                      + appView.options().getMinApiLevel().getDexVersion()
+                      + ": "
+                      + descriptor));
+    }
+
+    static void pop(DexType type, Builder<CfInstruction> builder) {
+      assert !type.isVoidType();
+      builder.add(new CfStackInstruction(type.isWideType() ? Opcode.Pop2 : Opcode.Pop));
+    }
+
+    static void pop(Iterable<DexType> types, Builder<CfInstruction> builder) {
+      types.forEach(t -> pop(t, builder));
+    }
+
+    static Builder<CfInstruction> pushReturnValue(DexType type, Builder<CfInstruction> builder) {
+      if (!type.isVoidType()) {
+        builder.add(createDefaultValueForType(type));
+      }
+      return builder;
+    }
+
+    static CfInstruction createDefaultValueForType(DexType type) {
+      assert !type.isVoidType();
+      if (type.isPrimitiveType()) {
+        return new CfConstNumber(0, ValueType.fromDexType(type));
+      }
+      assert type.isReferenceType();
+      return new CfConstNull();
+    }
+  }
+
+  private static class InvokeDynamicMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseInvokeCustom()) {
+        builder.add(new InvokeDynamicMatcher(appView));
+      }
+    }
+
+    InvokeDynamicMatcher(AppView<?> appView) {
+      super(appView, "invoke-dynamic", InternalOptions.invokeCustomApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedInvokeCustomDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      CfInvokeDynamic invokeDynamic = instruction.asInvokeDynamic();
+      if (invokeDynamic == null) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                DexCallSite callSite = invokeDynamic.getCallSite();
+                pop(callSite.getMethodProto().getParameters(), replacement);
+                localStackAllocator.allocateLocalStack(1);
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                pushReturnValue(callSite.getMethodProto().getReturnType(), replacement);
+                return replacement.build();
+              })
+          .build();
+    }
+  }
+
+  private static class InvokePolymorphicMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseInvokePolymorphicOnMethodHandle()) {
+        builder.add(new InvokePolymorphicMatcher(appView));
+      }
+    }
+
+    InvokePolymorphicMatcher(AppView<?> appView) {
+      super(
+          appView, "invoke-polymorphic", InternalOptions.invokePolymorphicOnMethodHandleApiLevel());
+    }
+
+    boolean isPolymorphicInvoke(CfInvoke invoke) {
+      return appView.dexItemFactory().polymorphicMethods.isPolymorphicInvoke(invoke.getMethod());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedInvokePolymorphicMethodHandleDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      CfInvoke invoke = instruction.asInvoke();
+      if (invoke == null || !isPolymorphicInvoke(invoke)) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                if (!invoke.isInvokeStatic()) {
+                  pop(dexItemFactory.objectType, replacement);
+                }
+                pop(invoke.getMethod().getParameters(), replacement);
+                localStackAllocator.allocateLocalStack(1);
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                pushReturnValue(invoke.getMethod().getReturnType(), replacement);
+                return replacement.build();
+              })
+          .build();
+    }
+  }
+
+  private static class ConstMethodHandleMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseConstantMethodHandle()) {
+        builder.add(new ConstMethodHandleMatcher(appView));
+      }
+    }
+
+    ConstMethodHandleMatcher(AppView<?> appView) {
+      super(appView, "const-method-handle", InternalOptions.constantMethodHandleApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedConstMethodHandleDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      if (!(instruction instanceof CfConstMethodHandle)) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                return replacement.add(new CfConstNull()).build();
+              })
+          .build();
+    }
+  }
+
+  private static class ConstMethodTypeMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseConstantMethodType()) {
+        builder.add(new ConstMethodTypeMatcher(appView));
+      }
+    }
+
+    ConstMethodTypeMatcher(AppView<?> appView) {
+      super(appView, "const-method-type", InternalOptions.constantMethodTypeApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedConstMethodTypeDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      if (!(instruction instanceof CfConstMethodType)) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                return replacement.add(new CfConstNull()).build();
+              })
+          .build();
+    }
+  }
+
+  private static class ConstDynamicMatcher extends InstructionMatcher {
+    static void addIfNeeded(AppView<?> appView, Builder<InstructionMatcher> builder) {
+      InternalOptions options = appView.options();
+      if (!options.canUseConstantDynamic()) {
+        builder.add(new ConstDynamicMatcher(appView));
+      }
+    }
+
+    ConstDynamicMatcher(AppView<?> appView) {
+      super(appView, "const-dynamic", InternalOptions.constantDynamicApiLevel());
+    }
+
+    @Override
+    UnsupportedFeatureDiagnostic makeDiagnostic(Origin origin, Position position) {
+      return new UnsupportedConstDynamicDiagnostic(origin, position);
+    }
+
+    @Override
+    DesugarDescription compute(CfInstruction instruction) {
+      final CfConstDynamic constDynamic = instruction.asConstDynamic();
+      if (constDynamic == null) {
+        return null;
+      }
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  context,
+                  methodProcessingContext,
+                  dexItemFactory) -> {
+                report(context);
+                Builder<CfInstruction> replacement = ImmutableList.builder();
+                invokeThrowingStub(methodProcessingContext, eventConsumer, context, replacement);
+                return pushReturnValue(constDynamic.getType(), replacement).build();
+              })
+          .build();
+    }
+  }
+
+  private final List<InstructionMatcher> matchers;
+
+  public UnrepresentableInDexInstructionRemover(AppView<?> appView) {
+    Builder<InstructionMatcher> builder = ImmutableList.builder();
+    InvokeDynamicMatcher.addIfNeeded(appView, builder);
+    InvokePolymorphicMatcher.addIfNeeded(appView, builder);
+    ConstMethodHandleMatcher.addIfNeeded(appView, builder);
+    ConstMethodTypeMatcher.addIfNeeded(appView, builder);
+    ConstDynamicMatcher.addIfNeeded(appView, builder);
+    matchers = builder.build();
+  }
+
+  private DesugarDescription compute(CfInstruction instruction) {
+    for (InstructionMatcher matcher : matchers) {
+      DesugarDescription result = matcher.compute(instruction);
+      if (result != null) {
+        return result;
+      }
+    }
+    return DesugarDescription.nothing();
+  }
+
+  @Override
+  public Collection<CfInstruction> desugarInstruction(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext,
+      CfInstructionDesugaringCollection desugaringCollection,
+      DexItemFactory dexItemFactory) {
+    return compute(instruction)
+        .desugarInstruction(
+            freshLocalProvider,
+            localStackAllocator,
+            eventConsumer,
+            context,
+            methodProcessingContext,
+            dexItemFactory);
+  }
+
+  @Override
+  public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    return compute(instruction).needsDesugaring();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java
index 8853e8b..b9826d5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicInstructionDesugaring.java
@@ -7,16 +7,20 @@
 import com.android.tools.r8.cf.code.CfConstDynamic;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.ConstantDynamicDesugarDiagnostic;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.DesugarDescription;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.utils.Box;
 import java.util.Collection;
 import java.util.HashMap;
@@ -33,9 +37,90 @@
     this.appView = appView;
   }
 
+  private DesugarDescription report(String message, ProgramMethod context) {
+    return DesugarDescription.builder()
+        .addScanEffect(
+            () ->
+                appView
+                    .reporter()
+                    .error(
+                        new ConstantDynamicDesugarDiagnostic(
+                            context.getOrigin(), MethodPosition.create(context), message)))
+        .build();
+  }
+
+  private DesugarDescription computeDesugaring(CfInstruction instruction, ProgramMethod context) {
+    if (!instruction.isConstDynamic()) {
+      return DesugarDescription.nothing();
+    }
+    CfConstDynamic constDynamic = instruction.asConstDynamic();
+    if (!constDynamic.getBootstrapMethodArguments().isEmpty()) {
+      // TODO(b/178172809): Handle bootstrap arguments.
+      return report("Unsupported dynamic constant (has arguments to bootstrap method)", context);
+    }
+    if (!constDynamic.getBootstrapMethod().type.isInvokeStatic()) {
+      return report("Unsupported dynamic constant (not invoke static)", context);
+    }
+    DexItemFactory factory = appView.dexItemFactory();
+    DexMethod bootstrapMethod = constDynamic.getBootstrapMethod().asMethod();
+    DexType holder = bootstrapMethod.getHolderType();
+    if (holder == factory.constantBootstrapsType) {
+      return report("Unsupported dynamic constant (runtime provided bootstrap method)", context);
+    }
+    if (holder != context.getHolderType()) {
+      return report("Unsupported dynamic constant (different owner)", context);
+    }
+    if (bootstrapMethod.getProto().returnType != factory.booleanArrayType
+        && bootstrapMethod.getProto().returnType != factory.objectType) {
+      return report("Unsupported dynamic constant (unsupported constant type)", context);
+    }
+    if (bootstrapMethod.getProto().getParameters().size() != 3) {
+      return report("Unsupported dynamic constant (unsupported signature)", context);
+    }
+    if (bootstrapMethod.getProto().getParameters().get(0) != factory.lookupType) {
+      return report(
+          "Unsupported dynamic constant (unexpected type of first argument to bootstrap method",
+          context);
+    }
+    if (bootstrapMethod.getProto().getParameters().get(1) != factory.stringType) {
+      return report(
+          "Unsupported dynamic constant (unexpected type of second argument to bootstrap method",
+          context);
+    }
+    if (bootstrapMethod.getProto().getParameters().get(2) != factory.classType) {
+      return report(
+          "Unsupported dynamic constant (unexpected type of third argument to bootstrap method",
+          context);
+    }
+    return DesugarDescription.builder()
+        .setDesugarRewrite(
+            (freshLocalProvider,
+                localStackAllocator,
+                eventConsumer,
+                context1,
+                methodProcessingContext,
+                dexItemFactory) ->
+                desugarConstDynamicInstruction(
+                    instruction.asConstDynamic(),
+                    freshLocalProvider,
+                    localStackAllocator,
+                    eventConsumer,
+                    context1,
+                    methodProcessingContext))
+        .build();
+  }
+
+  @Override
+  public void scan(ProgramMethod method, CfInstructionDesugaringEventConsumer eventConsumer) {
+    for (CfInstruction instruction :
+        method.getDefinition().getCode().asCfCode().getInstructions()) {
+      computeDesugaring(instruction, method).scan();
+    }
+  }
+
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
-    return instruction.isConstDynamic();
+    return computeDesugaring(instruction, context).needsDesugaring();
   }
 
   @Override
@@ -47,17 +132,15 @@
       ProgramMethod context,
       MethodProcessingContext methodProcessingContext,
       CfInstructionDesugaringCollection desugaringCollection,
-      DexItemFactory dexItemFactory) {
-    if (instruction.isConstDynamic()) {
-      return desugarConstDynamicInstruction(
-          instruction.asConstDynamic(),
-          freshLocalProvider,
-          localStackAllocator,
-          eventConsumer,
-          context,
-          methodProcessingContext);
-    }
-    return null;
+      DexItemFactory factory) {
+    return computeDesugaring(instruction, context)
+        .desugarInstruction(
+            freshLocalProvider,
+            localStackAllocator,
+            eventConsumer,
+            context,
+            methodProcessingContext,
+            factory);
   }
 
   private Collection<CfInstruction> desugarConstDynamicInstruction(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
index da8257c..43d76c7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
@@ -6,20 +6,22 @@
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import java.util.List;
 import java.util.Objects;
 
 public class ConstantDynamicReference {
   private final DexString name;
   private final DexType type;
   private final DexMethodHandle bootstrapMethod;
-  private final Object[] bootstrapMethodArguments;
+  private final List<DexValue> bootstrapMethodArguments;
 
   public ConstantDynamicReference(
       DexString name,
       DexType type,
       DexMethodHandle bootstrapMethod,
-      Object[] bootstrapMethodArguments) {
-    assert bootstrapMethodArguments.length == 0;
+      List<DexValue> bootstrapMethodArguments) {
+    assert bootstrapMethodArguments.isEmpty();
     this.name = name;
     this.type = type;
     this.bootstrapMethod = bootstrapMethod;
@@ -38,7 +40,7 @@
     return bootstrapMethod;
   }
 
-  public Object[] getBootstrapMethodArguments() {
+  public List<DexValue> getBootstrapMethodArguments() {
     return bootstrapMethodArguments;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
index 257fd7d..33e5159 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
@@ -172,6 +172,37 @@
         .CfUtilityMethodsForCodeOptimizationsTemplates_throwNoSuchMethodError(options, method);
   }
 
+  public static UtilityMethodForCodeOptimizations synthesizeThrowRuntimeExceptionWithMessageMethod(
+      AppView<?> appView, MethodProcessingContext methodProcessingContext) {
+    InternalOptions options = appView.options();
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    DexProto proto =
+        dexItemFactory.createProto(dexItemFactory.runtimeExceptionType, dexItemFactory.stringType);
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+    ProgramMethod syntheticMethod =
+        syntheticItems.createMethod(
+            kinds -> kinds.THROW_RTE,
+            methodProcessingContext.createUniqueContext(),
+            appView,
+            builder ->
+                builder
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setClassFileVersion(CfVersion.V1_8)
+                    .setApiLevelForDefinition(appView.computedMinApiLevel())
+                    .setApiLevelForCode(appView.computedMinApiLevel())
+                    .setCode(
+                        method -> getThrowRuntimeExceptionWithMessageCodeTemplate(method, options))
+                    .setProto(proto));
+    return new UtilityMethodForCodeOptimizations(syntheticMethod);
+  }
+
+  private static CfCode getThrowRuntimeExceptionWithMessageCodeTemplate(
+      DexMethod method, InternalOptions options) {
+    return CfUtilityMethodsForCodeOptimizations
+        .CfUtilityMethodsForCodeOptimizationsTemplates_throwRuntimeExceptionWithMessage(
+            options, method);
+  }
+
   public static class UtilityMethodForCodeOptimizations {
 
     private final ProgramMethod method;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
index 0cb8e39..d512ff4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
@@ -34,6 +34,7 @@
     factory.createSynthesizedType("Ljava/lang/IllegalAccessError;");
     factory.createSynthesizedType("Ljava/lang/IncompatibleClassChangeError;");
     factory.createSynthesizedType("Ljava/lang/NoSuchMethodError;");
+    factory.createSynthesizedType("Ljava/lang/RuntimeException;");
   }
 
   public static CfCode
@@ -145,6 +146,34 @@
         ImmutableList.of());
   }
 
+  public static CfCode
+      CfUtilityMethodsForCodeOptimizationsTemplates_throwRuntimeExceptionWithMessage(
+          InternalOptions options, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        3,
+        1,
+        ImmutableList.of(
+            label0,
+            new CfNew(options.itemFactory.createType("Ljava/lang/RuntimeException;")),
+            new CfStackInstruction(CfStackInstruction.Opcode.Dup),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInvoke(
+                183,
+                options.itemFactory.createMethod(
+                    options.itemFactory.createType("Ljava/lang/RuntimeException;"),
+                    options.itemFactory.createProto(
+                        options.itemFactory.voidType, options.itemFactory.stringType),
+                    options.itemFactory.createString("<init>")),
+                false),
+            new CfThrow(),
+            label1),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
   public static CfCode CfUtilityMethodsForCodeOptimizationsTemplates_toStringIfNotNull(
       InternalOptions options, DexMethod method) {
     CfLabel label0 = new CfLabel();
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 29ca8e4..631115f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -75,6 +75,7 @@
   public final SyntheticKind THROW_IAE = generator.forSingleMethod("ThrowIAE");
   public final SyntheticKind THROW_ICCE = generator.forSingleMethod("ThrowICCE");
   public final SyntheticKind THROW_NSME = generator.forSingleMethod("ThrowNSME");
+  public final SyntheticKind THROW_RTE = generator.forSingleMethod("ThrowRTE");
   public final SyntheticKind TWR_CLOSE_RESOURCE = generator.forSingleMethod("TwrCloseResource");
   public final SyntheticKind SERVICE_LOADER = generator.forSingleMethod("ServiceLoad");
   public final SyntheticKind OUTLINE = generator.forSingleMethod("Outline");
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 7759cc3..7d6d982 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -509,10 +509,8 @@
   }
 
   public boolean shouldKeepStackMapTable() {
-    assert isCfDesugaring() || isRelocatorCompilation() || getProguardConfiguration() != null;
-    return isCfDesugaring()
-        || isRelocatorCompilation()
-        || getProguardConfiguration().getKeepAttributes().stackMapTable;
+    assert isRelocatorCompilation() || getProguardConfiguration() != null;
+    return isRelocatorCompilation() || getProguardConfiguration().getKeepAttributes().stackMapTable;
   }
 
   public boolean shouldRerunEnqueuer() {
@@ -2070,28 +2068,76 @@
     return CfVersion.V1_5;
   }
 
-  public boolean canUseInvokePolymorphicOnVarHandle() {
-    return hasFeaturePresentFrom(AndroidApiLevel.P);
+  public static AndroidApiLevel invokePolymorphicOnMethodHandleApiLevel() {
+    return AndroidApiLevel.O;
   }
 
-  public boolean canUseInvokePolymorphic() {
-    return hasFeaturePresentFrom(AndroidApiLevel.O);
+  public boolean canUseInvokePolymorphicOnMethodHandle() {
+    return hasFeaturePresentFrom(invokePolymorphicOnMethodHandleApiLevel());
+  }
+
+  public static AndroidApiLevel invokePolymorphicOnVarHandleApiLevel() {
+    return AndroidApiLevel.P;
+  }
+
+  public boolean canUseInvokePolymorphicOnVarHandle() {
+    return hasFeaturePresentFrom(invokePolymorphicOnMethodHandleApiLevel());
+  }
+
+  public static AndroidApiLevel constantMethodHandleApiLevel() {
+    return AndroidApiLevel.P;
   }
 
   public boolean canUseConstantMethodHandle() {
-    return hasFeaturePresentFrom(AndroidApiLevel.P);
+    return hasFeaturePresentFrom(constantMethodHandleApiLevel());
+  }
+
+  public static AndroidApiLevel constantMethodTypeApiLevel() {
+    return AndroidApiLevel.P;
   }
 
   public boolean canUseConstantMethodType() {
-    return hasFeaturePresentFrom(AndroidApiLevel.P);
+    return hasFeaturePresentFrom(constantMethodTypeApiLevel());
+  }
+
+  public static AndroidApiLevel invokeCustomApiLevel() {
+    return AndroidApiLevel.O;
   }
 
   public boolean canUseInvokeCustom() {
-    return hasFeaturePresentFrom(AndroidApiLevel.O);
+    return hasFeaturePresentFrom(invokeCustomApiLevel());
+  }
+
+  public static AndroidApiLevel constantDynamicApiLevel() {
+    return null;
+  }
+
+  public boolean canUseConstantDynamic() {
+    return hasFeaturePresentFrom(constantDynamicApiLevel());
+  }
+
+  public static AndroidApiLevel defaultAndStaticInterfaceMethodsApiLevel() {
+    return AndroidApiLevel.N;
+  }
+
+  public static AndroidApiLevel defaultInterfaceMethodsApiLevel() {
+    return defaultAndStaticInterfaceMethodsApiLevel();
+  }
+
+  public static AndroidApiLevel staticInterfaceMethodsApiLevel() {
+    return defaultAndStaticInterfaceMethodsApiLevel();
   }
 
   public boolean canUseDefaultAndStaticInterfaceMethods() {
-    return hasFeaturePresentFrom(AndroidApiLevel.N);
+    return hasFeaturePresentFrom(defaultInterfaceMethodsApiLevel());
+  }
+
+  public static AndroidApiLevel privateInterfaceMethodsApiLevel() {
+    return AndroidApiLevel.N;
+  }
+
+  public boolean canUsePrivateInterfaceMethods() {
+    return hasFeaturePresentFrom(privateInterfaceMethodsApiLevel());
   }
 
   public boolean canUseNestBasedAccess() {
@@ -2137,10 +2183,6 @@
     throw new Unreachable();
   }
 
-  public boolean canUsePrivateInterfaceMethods() {
-    return hasFeaturePresentFrom(AndroidApiLevel.N);
-  }
-
   // Debug entries may be dropped only if the source file content allows being omitted from
   // stack traces, or if the VM will report the source file even with a null valued debug info.
   public boolean allowDiscardingResidualDebugInfo() {
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index 66e5fc8..dbfd4c6 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -106,10 +106,10 @@
       command.add(outJar.toString());
       command.add("-printmapping");
       command.add(mapFile.toString());
-      if (!enableTreeShaking) {
+      if (enableTreeShaking.isFalse()) {
         command.add("-dontshrink");
       }
-      if (!enableMinification) {
+      if (enableMinification.isFalse()) {
         command.add("-dontobfuscate");
       }
       ProcessBuilder pbuilder = new ProcessBuilder(command);
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
index 7c3a605..cd5e1ca 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
@@ -56,16 +56,4 @@
   R8TestRunner test(String testName, String packageName, String mainClass) {
     return new R8TestRunner(testName, packageName, mainClass);
   }
-
-  @Test
-  public void varHandle() throws Throwable {
-    test("varhandle", "varhandle", "VarHandleTests")
-        .withBuilderTransformation(
-            builder ->
-                builder.addProguardConfiguration(
-                    ImmutableList.of("-dontwarn java.lang.invoke.VarHandle"), Origin.unknown()))
-        .withMinApiLevel(AndroidApiLevel.P.getLevel())
-        .withKeepAll()
-        .run();
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index ebe8352..3bdfa4b 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -82,8 +82,12 @@
       builder.addProguardConfiguration(keepRules, Origin.unknown());
     }
     builder.addMainDexRulesFiles(mainDexRulesFiles);
-    builder.setDisableTreeShaking(!enableTreeShaking);
-    builder.setDisableMinification(!enableMinification);
+    if (enableTreeShaking.isFalse()) {
+      builder.setDisableTreeShaking(true);
+    }
+    if (enableMinification.isFalse()) {
+      builder.setDisableMinification(true);
+    }
     StringBuilder proguardMapBuilder = new StringBuilder();
     if (createDefaultProguardMapConsumer) {
       builder.setProguardMapConsumer(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index ed69ecf..caed567 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -228,14 +228,6 @@
         .run();
   }
 
-  @Test
-  public void invokeCustomErrorDueToMinSdk() throws Throwable {
-    test("invokecustom-error-due-to-min-sdk", "invokecustom", "InvokeCustom")
-        .withMinApiLevel(AndroidApiLevel.O.getLevel())
-        .withKeepAll()
-        .run();
-  }
-
   abstract RunExamplesAndroidPTest<B>.TestRunner<?> test(String testName, String packageName,
       String mainClass);
 
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index a20964a..485ac54 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -120,42 +120,17 @@
     abstract void build(Path inputFile, Path out) throws Throwable;
   }
 
-  private static List<String> minSdkErrorExpected =
-      ImmutableList.of("varhandle-error-due-to-min-sdk");
+  private static List<String> minSdkErrorExpected = ImmutableList.of();
 
   private static Map<DexVm.Version, List<String>> failsOn;
 
   static {
     ImmutableMap.Builder<DexVm.Version, List<String>> builder = ImmutableMap.builder();
     builder
-        .put(DexVm.Version.V4_0_4, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V4_4_4, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V5_1_1, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V6_0_1, ImmutableList.of(
-            "native-private-interface-methods", // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V7_0_0, ImmutableList.of(
-            // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.V8_1_0, ImmutableList.of(
-            // Dex version not supported
-            "varhandle"
-        ))
-        .put(DexVm.Version.DEFAULT, ImmutableList.of(
-            // TODO(b/72536415): Update runtime when the support will be ready
-            "varhandle"
-        ));
+        .put(DexVm.Version.V4_0_4, ImmutableList.of("native-private-interface-methods"))
+        .put(DexVm.Version.V4_4_4, ImmutableList.of("native-private-interface-methods"))
+        .put(DexVm.Version.V5_1_1, ImmutableList.of("native-private-interface-methods"))
+        .put(DexVm.Version.V6_0_1, ImmutableList.of("native-private-interface-methods"));
     failsOn = builder.build();
   }
 
@@ -168,20 +143,9 @@
               + "1\n2\n3\n4\n5\n6\n7\n8\n99\n"
               + "i1\ni2\ni3\ni4\ni5\ni6\ni7\ni8\ni99\n",
           "native-private-interface-methods",
-          "0: s>i>a\n"
-              + "1: d>i>s>i>a\n"
-              + "2: l>i>s>i>a\n"
-              + "3: x>s\n"
-              + "4: c>d>i>s>i>a\n",
+          "0: s>i>a\n" + "1: d>i>s>i>a\n" + "2: l>i>s>i>a\n" + "3: x>s\n" + "4: c>d>i>s>i>a\n",
           "desugared-private-interface-methods",
-          "0: s>i>a\n"
-              + "1: d>i>s>i>a\n"
-              + "2: l>i>s>i>a\n"
-              + "3: x>s\n"
-              + "4: c>d>i>s>i>a\n",
-          "varhandle",
-          "true\nfalse\n"
-      );
+          "0: s>i>a\n" + "1: d>i>s>i>a\n" + "2: l>i>s>i>a\n" + "3: x>s\n" + "4: c>d>i>s>i>a\n");
 
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@@ -241,22 +205,6 @@
   }
 
   @Test
-  public void varHandle() throws Throwable {
-    test("varhandle", "varhandle", "VarHandleTests")
-        .withMinApiLevel(AndroidApiLevel.P.getLevel())
-        .withKeepAll()
-        .run();
-  }
-
-  @Test
-  public void varHandleErrorDueToMinSdk() throws Throwable {
-    test("varhandle-error-due-to-min-sdk", "varhandle", "VarHandleTests")
-        .withMinApiLevel(AndroidApiLevel.O.getLevel())
-        .withKeepAll()
-        .run();
-  }
-
-  @Test
   public void testTwrCloseResourceMethod() throws Throwable {
     TestRunner<?> test = test("twr-close-resource", "twrcloseresource", "TwrCloseResourceTest");
     test
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 4383be5..98f1627 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1818,6 +1818,10 @@
     return AndroidApiLevel.O;
   }
 
+  public static AndroidApiLevel apiLevelWithInvokePolymorphicSupport() {
+    return AndroidApiLevel.O;
+  }
+
   public static AndroidApiLevel apiLevelWithConstMethodHandleSupport() {
     return AndroidApiLevel.P;
   }
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 52d84fc..2b29085 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -250,6 +251,12 @@
     return self();
   }
 
+  public T mapUnsupportedFeaturesToWarnings() {
+    return setDiagnosticsLevelModifier(
+        (level, diagnostic) ->
+            diagnostic instanceof UnsupportedFeatureDiagnostic ? DiagnosticsLevel.WARNING : level);
+  }
+
   public T allowStdoutMessages() {
     // Default ignored.
     return self();
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 8b4c482..5d9d7f5 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -75,6 +75,8 @@
   private ProgramConsumer programConsumer;
   private MainDexClassesCollector mainDexClassesCollector;
   private StringConsumer mainDexListConsumer;
+  // TODO(b/186010707): This could become implicit once min always be set when fixed.
+  private boolean noMinApiLevel = false;
   private int minApiLevel = -1;
   private boolean optimizeMultidexForLinearAlloc = false;
   private Consumer<InternalOptions> optionsConsumer = DEFAULT_OPTIONS;
@@ -219,7 +221,7 @@
               .addIfNotNull(mainDexListConsumer)
               .build());
     }
-    if (backend.isDex() || !isTestShrinkerBuilder()) {
+    if (!noMinApiLevel && (backend.isDex() || !isTestShrinkerBuilder())) {
       assert !builder.isMinApiLevelSet()
           : "Don't set the API level directly through BaseCompilerCommand.Builder in tests";
       // TODO(b/186010707): This will always be set when fixed.
@@ -364,6 +366,12 @@
     return self();
   }
 
+  public T setNoMinApi() {
+    this.minApiLevel = -1;
+    this.noMinApiLevel = true;
+    return self();
+  }
+
   /** @deprecated use {@link #setMinApi(AndroidApiLevel)} instead. */
   @Deprecated
   public T setMinApi(TestRuntime runtime) {
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index bf47b6e..9072aa3 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Sets;
 import java.io.IOException;
@@ -29,8 +30,8 @@
         T extends TestShrinkerBuilder<C, B, CR, RR, T>>
     extends TestCompilerBuilder<C, B, CR, RR, T> {
 
-  protected boolean enableTreeShaking = true;
-  protected boolean enableMinification = true;
+  protected OptionalBool enableTreeShaking = OptionalBool.UNKNOWN;
+  protected OptionalBool enableMinification = OptionalBool.UNKNOWN;
 
   private final Set<Class<? extends Annotation>> addedTestingAnnotations =
       Sets.newIdentityHashSet();
@@ -67,7 +68,7 @@
   }
 
   public T treeShaking(boolean enable) {
-    enableTreeShaking = enable;
+    enableTreeShaking = OptionalBool.of(enable);
     return self();
   }
 
@@ -76,7 +77,7 @@
   }
 
   public T minification(boolean enable) {
-    enableMinification = enable;
+    enableMinification = OptionalBool.of(enable);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java b/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java
deleted file mode 100644
index f400169..0000000
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleDump.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// 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.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.ClassWriter;
-import org.objectweb.asm.Handle;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.Type;
-
-// 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 String cDesc = "com/android/tools/r8/cf/MethodHandleTest$C";
-  private static final String eDesc = "com/android/tools/r8/cf/MethodHandleTest$E";
-  private static final String fDesc = "com/android/tools/r8/cf/MethodHandleTest$F";
-  private static final String iDesc = "com/android/tools/r8/cf/MethodHandleTest$I";
-  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 Type veType = Type.getMethodType(Type.VOID_TYPE, Type.getObjectType(eDesc));
-  private static final Type fType = Type.getMethodType(Type.getObjectType(fDesc));
-  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)
-            .put("veType", veType)
-            .put("fType", fType)
-            .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(InternalOptions.ASM_VERSION, 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/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
deleted file mode 100644
index cc9b61a..0000000
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ /dev/null
@@ -1,209 +0,0 @@
-// 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 static org.junit.Assert.assertTrue;
-
-import com.android.tools.r8.ByteDataView;
-import com.android.tools.r8.ClassFileConsumer;
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.DexIndexedConsumer;
-import com.android.tools.r8.NoVerticalClassMerging;
-import com.android.tools.r8.ProgramConsumer;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.R8Command.Builder;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.cf.MethodHandleTest.C;
-import com.android.tools.r8.cf.MethodHandleTest.I;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class MethodHandleTestRunner extends TestBase {
-  static final Class<?> CLASS = MethodHandleTest.class;
-
-  enum LookupType {
-    DYNAMIC,
-    CONSTANT,
-  }
-
-  enum MinifyMode {
-    NONE,
-    MINIFY,
-  }
-
-  private CompilationMode compilationMode;
-  private LookupType lookupType;
-  private ProcessResult runInput;
-  private MinifyMode minifyMode;
-
-  @Parameters(name = "{0}_{1}_{2}")
-  public static List<String[]> data() {
-    List<String[]> res = new ArrayList<>();
-    for (LookupType lookupType : LookupType.values()) {
-      for (MinifyMode minifyMode : MinifyMode.values()) {
-        if (lookupType == LookupType.DYNAMIC && minifyMode == MinifyMode.MINIFY) {
-          // Skip because we don't keep the members looked up dynamically.
-          continue;
-        }
-        for (CompilationMode compilationMode : CompilationMode.values()) {
-          res.add(new String[] {lookupType.name(), minifyMode.name(), compilationMode.name()});
-        }
-      }
-    }
-    return res;
-  }
-
-  public MethodHandleTestRunner(String lookupType, String minifyMode, String compilationMode) {
-    this.lookupType = LookupType.valueOf(lookupType);
-    this.minifyMode = MinifyMode.valueOf(minifyMode);
-    this.compilationMode = CompilationMode.valueOf(compilationMode);
-  }
-
-  @Test
-  public void test() throws Exception {
-    runInput();
-    runCf();
-    // TODO(mathiasr): Once we include a P runtime, change this to "P and above".
-    if (ToolHelper.getDexVm() == DexVm.ART_DEFAULT && ToolHelper.artSupported()) {
-      runDex();
-    }
-  }
-
-  private final Class<?>[] inputClasses = {
-    MethodHandleTest.class,
-    MethodHandleTest.C.class,
-    MethodHandleTest.I.class,
-    MethodHandleTest.Impl.class,
-    MethodHandleTest.D.class,
-    MethodHandleTest.E.class,
-    MethodHandleTest.F.class,
-    NoVerticalClassMerging.class
-  };
-
-  private void runInput() throws Exception {
-    Path out = temp.getRoot().toPath().resolve("input.jar");
-    ClassFileConsumer.ArchiveConsumer archiveConsumer = new ClassFileConsumer.ArchiveConsumer(out);
-    for (Class<?> c : inputClasses) {
-      archiveConsumer.accept(
-          ByteDataView.of(getClassAsBytes(c)),
-          DescriptorUtils.javaTypeToDescriptor(c.getName()),
-          null);
-    }
-    archiveConsumer.finished(null);
-    String expected = lookupType == LookupType.CONSTANT ? "error" : "exception";
-    runInput = ToolHelper.runJava(out, CLASS.getName(), expected);
-    if (runInput.exitCode != 0) {
-      System.out.println(runInput);
-    }
-    assertEquals(0, runInput.exitCode);
-  }
-
-  private void runCf() throws Exception {
-    Path outCf = temp.getRoot().toPath().resolve("cf.jar");
-    build(new ClassFileConsumer.ArchiveConsumer(outCf));
-    String expected = lookupType == LookupType.CONSTANT ? "error" : "exception";
-    ProcessResult runCf = ToolHelper.runJava(outCf, CLASS.getCanonicalName(), expected);
-    assertEquals(runCf.stderr, 0, runCf.exitCode);
-    assertEquals(runInput.toString(), runCf.toString());
-    // Ensure that we did not inline the const method handle
-    ensureConstHandleNotInlined(outCf);
-  }
-
-  private void ensureConstHandleNotInlined(Path file) throws IOException, ExecutionException {
-    CodeInspector inspector = new CodeInspector(file);
-    MethodSubject subject = inspector.clazz(MethodHandleTest.D.class).method(
-        "java.lang.MethodHandle", "vcviSpecialMethod");
-    assertTrue(inspector.clazz(MethodHandleTest.D.class)
-        .method("java.lang.invoke.MethodHandle", "vcviSpecialMethod").isPresent());
-  }
-
-  private void runDex() throws Exception {
-    Path outDex = temp.getRoot().toPath().resolve("dex.zip");
-    build(new DexIndexedConsumer.ArchiveConsumer(outDex));
-    String expected = lookupType == LookupType.CONSTANT ? "pass" : "exception";
-    ProcessResult runDex =
-        ToolHelper.runArtRaw(
-            outDex.toString(),
-            CLASS.getCanonicalName(),
-            cmd -> cmd.appendProgramArgument(expected));
-    // Only compare stdout and exitCode since dex2oat prints to stderr.
-    if (runInput.exitCode != runDex.exitCode) {
-      System.out.println(runDex.stderr);
-    }
-    assertEquals(runInput.exitCode, runDex.exitCode);
-    assertEquals(runInput.stdout, runDex.stdout);
-  }
-
-  private void build(ProgramConsumer programConsumer) throws Exception {
-    // MethodHandle.invoke() only supported from Android O
-    // ConstMethodHandle only supported from Android P
-    Builder builder =
-        R8Command.builder()
-            .setMode(compilationMode)
-            .setProgramConsumer(programConsumer)
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true);
-    if (programConsumer instanceof ClassFileConsumer) {
-      builder.addLibraryFiles(ToolHelper.getJava8RuntimeJar());
-    } else {
-      AndroidApiLevel apiLevel = AndroidApiLevel.P;
-      builder
-          .setMinApiLevel(apiLevel.getLevel())
-          .addLibraryFiles(ToolHelper.getAndroidJar(apiLevel));
-    }
-    for (Class<?> c : inputClasses) {
-      byte[] classAsBytes = getClassAsBytes(c);
-      builder.addClassProgramData(classAsBytes, Origin.unknown());
-    }
-    if (minifyMode == MinifyMode.MINIFY) {
-      ToolHelper.allowTestProguardOptions(builder);
-      builder.addProguardConfiguration(
-          Arrays.asList(
-              keepMainProguardConfiguration(MethodHandleTest.class),
-              noVerticalClassMergingRule(),
-              // Prevent the second argument of C.svic(), C.sjic(), I.sjic() and I.svic() from
-              // being removed although they are never used unused. This is needed since these
-              // methods are accessed reflectively.
-              "-keep,allowobfuscation public class " + C.class.getTypeName() + " {",
-              "  static void svic(int, char);",
-              "  static long sjic(int, char);",
-              "}",
-              "-keep,allowobfuscation public interface " + I.class.getTypeName() + " {",
-              "  static long sjic(int, char);",
-              "  static void svic(int, char);",
-              "}"),
-          Origin.unknown());
-    }
-    ToolHelper.runR8(builder.build());
-  }
-
-  private byte[] getClassAsBytes(Class<?> clazz) throws Exception {
-    if (lookupType == LookupType.CONSTANT) {
-      if (clazz == MethodHandleTest.D.class) {
-        return MethodHandleDump.dumpD();
-      } else if (clazz == MethodHandleTest.class) {
-        return MethodHandleDump.transform(ToolHelper.getClassAsBytes(clazz));
-      }
-    }
-    return ToolHelper.getClassAsBytes(clazz);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/InvokeMethodHandleRuntimeErrorTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/InvokeMethodHandleRuntimeErrorTest.java
new file mode 100644
index 0000000..889fd91
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/InvokeMethodHandleRuntimeErrorTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2022, 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.methodhandles;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.references.Reference.classFromClass;
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandle;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Test that unrepresentable MethodHandle invokes are replaced by throwing instructions. See
+ * b/174733673.
+ */
+@RunWith(Parameterized.class)
+public class InvokeMethodHandleRuntimeErrorTest extends TestBase {
+
+  private static final String EXPECTED = StringUtils.lines("I.target");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InvokeMethodHandleRuntimeErrorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean hasCompileSupport() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  @Test
+  public void testReference() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(Main.class, I.class, Super.class)
+        .addProgramClassFileData(getInvokeCustomTransform())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8()
+        .addProgramClasses(Main.class, I.class, Super.class)
+        .addProgramClassFileData(getInvokeCustomTransform())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              if (hasCompileSupport()) {
+                diagnostics.assertNoMessages();
+              } else {
+                diagnostics
+                    .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+                    .assertOnlyWarnings();
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            hasCompileSupport(),
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r ->
+                r.assertFailureWithErrorThatThrows(RuntimeException.class)
+                    .assertStderrMatches(containsString("const-method-handle")));
+  }
+
+  private static byte[] getInvokeCustomTransform() throws Throwable {
+    ClassReference symbolicHolder = classFromClass(InvokeCustom.class);
+    MethodReference method = methodFromMethod(InvokeCustom.class.getMethod("target"));
+    return transformer(InvokeCustom.class)
+        .transformMethodInsnInMethod(
+            "test",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              // Replace null argument by a const method handle.
+              visitor.visitInsn(Opcodes.POP);
+              visitor.visitLdcInsn(
+                  new Handle(
+                      Opcodes.H_INVOKEVIRTUAL,
+                      symbolicHolder.getBinaryName(),
+                      method.getMethodName(),
+                      method.getMethodDescriptor(),
+                      false));
+              visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+            })
+        .transform();
+  }
+
+  interface I {
+    default void target() {
+      System.out.println("I.target");
+    }
+  }
+
+  static class Super implements I {}
+
+  static class InvokeCustom extends Super {
+
+    public static void doInvoke(MethodHandle handle) throws Throwable {
+      handle.invoke(new InvokeCustom());
+    }
+
+    public static void test() throws Throwable {
+      doInvoke(null /* will be const method handle */);
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) throws Throwable {
+      InvokeCustom.test();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleDump.java b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleDump.java
new file mode 100644
index 0000000..4881866
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleDump.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2022, 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.methodhandles;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.google.common.collect.ImmutableMap;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+// 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 String cDesc = TestBase.binaryName(MethodHandleTest.C.class);
+  private static final String eDesc = TestBase.binaryName(MethodHandleTest.E.class);
+  private static final String fDesc = TestBase.binaryName(MethodHandleTest.F.class);
+  private static final String iDesc = TestBase.binaryName(MethodHandleTest.I.class);
+  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 Type veType = Type.getMethodType(Type.VOID_TYPE, Type.getObjectType(eDesc));
+  private static final Type fType = Type.getMethodType(Type.getObjectType(fDesc));
+  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();
+
+  public static byte[] getTransformedClass() throws Exception {
+    ImmutableMap<String, Type> types =
+        ImmutableMap.<String, Type>builder()
+            .put("viType", viType)
+            .put("jiType", jiType)
+            .put("vicType", vicType)
+            .put("jicType", jicType)
+            .put("veType", veType)
+            .put("fType", fType)
+            .build();
+
+    ImmutableMap<String, Handle> methods =
+        ImmutableMap.<String, Handle>builder()
+            .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(
+                "constructorMethod", new Handle(H_NEWINVOKESPECIAL, cDesc, "<init>", viDesc, false))
+            .build();
+
+    return ClassFileTransformer.create(MethodHandleTest.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @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 = super.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 = super.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);
+                }
+              }
+            })
+        .transform();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTest.java
similarity index 72%
rename from src/test/java/com/android/tools/r8/cf/MethodHandleTest.java
rename to src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTest.java
index 690dd6b..497e19f 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTest.java
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2022, 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;
+package com.android.tools.r8.cf.methodhandles;
 
 import com.android.tools.r8.NoVerticalClassMerging;
 import java.lang.invoke.MethodHandle;
@@ -16,13 +16,6 @@
       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);
     }
@@ -60,20 +53,6 @@
     }
   }
 
-  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 static class E {
     // Class that is only mentioned in parameter list of LDC(MethodType)-instruction.
   }
@@ -84,7 +63,6 @@
 
   @NoVerticalClassMerging
   public interface I {
-    int ii = 42;
 
     static void svi(int i) {
       System.out.println("svi " + i);
@@ -128,7 +106,6 @@
   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 {
@@ -148,33 +125,6 @@
       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);
       System.out.println(veType().parameterType(0).getName().lastIndexOf('.'));
       System.out.println(fType().returnType().getName().lastIndexOf('.'));
@@ -189,12 +139,6 @@
     }
   }
 
-  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);
   }
@@ -347,54 +291,6 @@
     }
   }
 
-  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());
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTestRunner.java
new file mode 100644
index 0000000..1812854
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/MethodHandleTestRunner.java
@@ -0,0 +1,207 @@
+// Copyright (c) 2022, 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.methodhandles;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.C;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.E;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.F;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.I;
+import com.android.tools.r8.cf.methodhandles.MethodHandleTest.Impl;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MethodHandleTestRunner extends TestBase {
+  static final Class<?> CLASS = MethodHandleTest.class;
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  enum MinifyMode {
+    NONE,
+    MINIFY,
+  }
+
+  private String getExpected() {
+    return StringUtils.lines(
+        "C 42", "svi 1", "sji 2", "svic 3", "sjic 4", "vvi 5", "vji 6", "vvic 7", "vjic 8", "svi 9",
+        "sji 10", "svic 11", "sjic 12", "dvi 13", "dji 14", "dvic 15", "djic 16", "C 21", "37",
+        "37");
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+  private final MinifyMode minifyMode;
+
+  @Parameters(name = "{0}, lookup:{1}, minify:{2}")
+  public static List<Object[]> data() {
+    List<Object[]> res = new ArrayList<>();
+    for (TestParameters params :
+        TestParameters.builder()
+            .withCfRuntimes()
+            .withDexRuntimesStartingFromExcluding(Version.V7_0_0)
+            // .withApiLevelsStartingAtIncluding(AndroidApiLevel.P)
+            .withAllApiLevels()
+            .build()) {
+      for (LookupType lookupType : LookupType.values()) {
+        for (MinifyMode minifyMode : MinifyMode.values()) {
+          if (lookupType == LookupType.DYNAMIC && minifyMode == MinifyMode.MINIFY) {
+            // Skip because we don't keep the members looked up dynamically.
+            continue;
+          }
+          res.add(new Object[] {params, lookupType.name(), minifyMode.name()});
+        }
+      }
+    }
+    return res;
+  }
+
+  public MethodHandleTestRunner(TestParameters parameters, String lookupType, String minifyMode) {
+    this.parameters = parameters;
+    this.lookupType = LookupType.valueOf(lookupType);
+    this.minifyMode = MinifyMode.valueOf(minifyMode);
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(getInputClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), CLASS.getName())
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() && minifyMode == MinifyMode.NONE);
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClasses(getInputClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), CLASS.getName())
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestBuilder<?> builder =
+        testForR8(parameters.getBackend())
+            .setMinApi(parameters.getApiLevel())
+            .addProgramClasses(getInputClasses())
+            .addProgramClassFileData(getTransformedClasses())
+            .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+            .addNoVerticalClassMergingAnnotations();
+    if (minifyMode == MinifyMode.MINIFY) {
+      builder
+          .enableProguardTestOptions()
+          .addKeepMainRule(MethodHandleTest.class)
+          .addKeepRules(
+              // Prevent the second argument of C.svic(), C.sjic(), I.sjic() and I.svic() from
+              // being removed although they are never used unused. This is needed since these
+              // methods are accessed reflectively.
+              "-keep,allowobfuscation public class " + typeName(C.class) + " {",
+              "  static void svic(int, char);",
+              "  static long sjic(int, char);",
+              "}",
+              "-keep,allowobfuscation public interface " + typeName(I.class) + " {",
+              "  static long sjic(int, char);",
+              "  static void svic(int, char);",
+              "}");
+      // TODO(b/235810300): The compiler fails with assertion in AppInfoWithLiveness.
+      if (lookupType == LookupType.CONSTANT && hasConstMethodCompileSupport()) {
+        builder.allowDiagnosticMessages();
+        assertThrows(CompilationFailedException.class, builder::compile);
+        return;
+      }
+    } else {
+      builder.noTreeShaking();
+      builder.noMinification();
+    }
+    builder
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), CLASS.getCanonicalName())
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || (lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport())) {
+      diagnostics
+          .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport()) {
+      result
+          .assertFailureWithErrorThatThrows(RuntimeException.class)
+          .assertStderrMatches(containsString("invoke-polymorphic"));
+      return;
+    }
+    if (lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport()) {
+      result
+          .assertFailureWithErrorThatThrows(RuntimeException.class)
+          .assertStderrMatches(containsString("const-method-handle"));
+      return;
+    }
+    result.assertSuccessWithOutput(getExpected());
+  }
+
+  private List<Class<?>> getInputClasses() {
+    Builder<Class<?>> builder =
+        ImmutableList.<Class<?>>builder().add(C.class, I.class, Impl.class, E.class, F.class);
+    if (lookupType == LookupType.DYNAMIC) {
+      builder.add(MethodHandleTest.class);
+    }
+    return builder.build();
+  }
+
+  private List<byte[]> getTransformedClasses() throws Exception {
+    if (lookupType == LookupType.DYNAMIC) {
+      return ImmutableList.of();
+    }
+    return ImmutableList.of(MethodHandleDump.getTransformedClass());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
new file mode 100644
index 0000000..ccb901d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
@@ -0,0 +1,137 @@
+// Copyright (c) 2022, 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.methodhandles;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicMethodHandleDiagnostic;
+import com.android.tools.r8.examples.JavaExampleClassProxy;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Test for VarHandle (these are a refactoring of the old example test setup.) */
+@RunWith(Parameterized.class)
+public class VarHandleTest extends TestBase {
+
+  private static final String PKG = "varhandle";
+  private static final String EXAMPLE = "examplesJava9/" + PKG;
+  private final JavaExampleClassProxy MAIN =
+      new JavaExampleClassProxy(EXAMPLE, PKG + ".VarHandleTests");
+
+  private static final String EXPECTED = StringUtils.lines("true", "false");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimesStartingFromIncluding(CfVm.JDK9)
+        .withDexRuntimes()
+        .withAllApiLevels()
+        .build();
+  }
+
+  public VarHandleTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private boolean hasMethodHandlesClass() {
+    return parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O);
+  }
+
+  private boolean hasFindStaticVarHandleMethod() {
+    // API docs list this as present from T(33), but it was included from 28
+    return parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.P);
+  }
+
+  private boolean hasVarHandleInLibrary() {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.T);
+  }
+
+  public List<Path> getProgramInputs() {
+    return ImmutableList.of(JavaExampleClassProxy.examplesJar(EXAMPLE));
+  }
+
+  @Test
+  public void testReference() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(getProgramInputs())
+        .run(parameters.getRuntime(), MAIN.typeName())
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    assumeTrue(parameters.isDexRuntime());
+    assumeFalse(
+        "TODO(b/204855476): The default VM throws unsupported. Ignore it and reconsider for 8.0.0",
+        parameters.isDexRuntimeVersion(Version.DEFAULT));
+    testForD8()
+        .addProgramFiles(getProgramInputs())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              if (hasInvokePolymorphicCompileSupport()) {
+                diagnostics.assertNoMessages();
+              } else {
+                diagnostics
+                    .assertAllWarningsMatch(
+                        diagnosticType(UnsupportedInvokePolymorphicMethodHandleDiagnostic.class))
+                    .assertOnlyWarnings();
+              }
+            })
+        .run(parameters.getRuntime(), MAIN.typeName())
+        .applyIf(
+            !hasMethodHandlesClass(),
+            r ->
+                r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class)
+                    .assertStderrMatches(containsString("java.lang.invoke.MethodHandles")),
+            !hasFindStaticVarHandleMethod(),
+            r ->
+                r.assertFailureWithErrorThatThrows(NoSuchMethodError.class)
+                    .assertStderrMatches(containsString("findStaticVarHandle")),
+            !hasInvokePolymorphicCompileSupport(),
+            r ->
+                r.assertFailureWithErrorThatThrows(RuntimeException.class)
+                    .assertStderrMatches(containsString("invoke-polymorphic")),
+            r -> r.assertSuccessWithOutput(EXPECTED));
+  }
+
+  @Test
+  public void testR8() throws Throwable {
+    // This just tests R8 on the targets where the program is fully supported.
+    assumeTrue(hasInvokePolymorphicCompileSupport() && hasFindStaticVarHandleMethod());
+    testForR8(parameters.getBackend())
+        .addProgramFiles(getProgramInputs())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassAndMembersRules(MAIN.typeName())
+        .applyIf(!hasVarHandleInLibrary(), b -> b.addDontWarn("java.lang.invoke.VarHandle"))
+        .run(parameters.getRuntime(), MAIN.typeName())
+        .assertSuccessWithOutput(EXPECTED);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/fields/C.java b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/C.java
new file mode 100644
index 0000000..0ccb1e8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/C.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2022, 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.methodhandles.fields;
+
+// This is a top-level class.
+// The use of handles will check generics on C and fail if it cannot find the outer class.
+public class C {
+
+  public int vi;
+  public static int si;
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/fields/ClassFieldMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/ClassFieldMethodHandleTest.java
new file mode 100644
index 0000000..062f22e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/ClassFieldMethodHandleTest.java
@@ -0,0 +1,229 @@
+// Copyright (c) 2022, 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.methodhandles.fields;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.H_GETFIELD;
+import static org.objectweb.asm.Opcodes.H_GETSTATIC;
+import static org.objectweb.asm.Opcodes.H_PUTFIELD;
+import static org.objectweb.asm.Opcodes.H_PUTSTATIC;
+
+import com.android.tools.r8.DiagnosticsMatcher;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableMap;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class ClassFieldMethodHandleTest extends TestBase {
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+
+  @Parameters(name = "{0}, lookup:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParameters.builder().withAllRuntimesAndApiLevels().build(), LookupType.values());
+  }
+
+  public ClassFieldMethodHandleTest(TestParameters parameters, LookupType lookupType) {
+    this.parameters = parameters;
+    this.lookupType = lookupType;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(C.class)
+        .addProgramClassFileData(getTransformedMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepClassAndMembersRules(C.class, Main.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private boolean hasMethodHandlesRuntimeSupport() {
+    return parameters.isCfRuntime()
+        || parameters
+            .asDexRuntime()
+            .maxSupportedApiLevel()
+            .isGreaterThanOrEqualTo(AndroidApiLevel.O);
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport()) {
+      diagnostics
+          .assertAllWarningsMatch(
+              DiagnosticsMatcher.diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (lookupType == LookupType.DYNAMIC && hasInvokePolymorphicCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (hasConstMethodCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (lookupType == LookupType.DYNAMIC && !hasMethodHandlesRuntimeSupport()) {
+      result.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
+    } else {
+      result.assertFailureWithErrorThatMatches(
+          containsString(
+              lookupType == LookupType.DYNAMIC ? "invoke-polymorphic" : "const-method-handle"));
+    }
+  }
+
+  private String getExpected() {
+    return StringUtils.lines("AOK");
+  }
+
+  byte[] getTransformedMain() throws Exception {
+    return transformer(Main.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                MethodVisitor mv =
+                    super.visitMethod(access, name, descriptor, signature, exceptions);
+                if (lookupType == LookupType.CONSTANT && name.endsWith("Field")) {
+                  String fieldName = name.startsWith("sci") ? "si" : "vi";
+                  int type =
+                      ImmutableMap.<String, Integer>builder()
+                          .put("sciSetField", H_PUTSTATIC)
+                          .put("sciGetField", H_GETSTATIC)
+                          .put("vciSetField", H_PUTFIELD)
+                          .put("vciGetField", H_GETFIELD)
+                          .build()
+                          .get(name);
+                  mv.visitCode();
+                  mv.visitLdcInsn(new Handle(type, binaryName(C.class), fieldName, "I", false));
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+                return mv;
+              }
+            })
+        .transform();
+  }
+
+  public static class Main {
+
+    public static MethodHandle vciSetField() {
+      try {
+        return MethodHandles.lookup().findSetter(C.class, "vi", 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 sciSetField() {
+      try {
+        return MethodHandles.lookup().findStaticSetter(C.class, "si", 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 void assertEquals(int x, int y) {
+      if (x != y) {
+        throw new AssertionError("failed!");
+      }
+    }
+
+    public static void main(String[] args) throws Throwable {
+      C c = new C();
+      vciSetField().invoke(c, 17);
+      assertEquals(17, (int) vciGetField().invoke(c));
+      sciSetField().invoke(18);
+      assertEquals(18, (int) sciGetField().invoke());
+      System.out.println("AOK");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/fields/InterfaceFieldMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/InterfaceFieldMethodHandleTest.java
new file mode 100644
index 0000000..3d62b40
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/fields/InterfaceFieldMethodHandleTest.java
@@ -0,0 +1,217 @@
+// Copyright (c) 2022, 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.methodhandles.fields;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.H_GETSTATIC;
+import static org.objectweb.asm.Opcodes.H_PUTSTATIC;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class InterfaceFieldMethodHandleTest extends TestBase {
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+
+  @Parameters(name = "{0}, lookup:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParameters.builder()
+            // Runtimes without Handle APIs fail in various ways. Start testing beyond that point.
+            .withDexRuntimesStartingFromExcluding(Version.V7_0_0)
+            .withAllApiLevels()
+            .withCfRuntimes()
+            .build(),
+        LookupType.values());
+  }
+
+  public InterfaceFieldMethodHandleTest(TestParameters parameters, LookupType lookupType) {
+    this.parameters = parameters;
+    this.lookupType = lookupType;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getTransformedMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepClassAndMembersRules(I.class, Main.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport()) {
+      diagnostics
+          .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (parameters.isDexRuntimeVersion(Version.V13_0_0)
+        && lookupType == LookupType.CONSTANT
+        && hasConstMethodCompileSupport()) {
+      // TODO(b/235576668): VM 13 throws an escaping IAE outside the guarded range.
+      result
+          .assertFailureWithErrorThatThrows(IllegalAccessError.class)
+          .assertStderrMatches(containsString("Main.main"));
+      return;
+    }
+    if (lookupType == LookupType.DYNAMIC && hasInvokePolymorphicCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (hasConstMethodCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else {
+      result.assertFailureWithErrorThatMatches(
+          containsString(
+              lookupType == LookupType.DYNAMIC ? "invoke-polymorphic" : "const-method-handle"));
+    }
+  }
+
+  private String getExpected() {
+    if (lookupType == LookupType.CONSTANT && parameters.isDexRuntimeVersion(Version.V9_0_0)) {
+      // VM 9 will assign the value in the setter in contrast to RI.
+      return StringUtils.lines("42", "pass", "19");
+    }
+    return StringUtils.lines("42", lookupType == LookupType.DYNAMIC ? "exception" : "error", "42");
+  }
+
+  byte[] getTransformedMain() throws Exception {
+    return transformer(Main.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                MethodVisitor mv =
+                    super.visitMethod(access, name, descriptor, signature, exceptions);
+                if (lookupType == LookupType.CONSTANT && name.endsWith("Field")) {
+                  int type = name.equals("iiSetField") ? H_PUTSTATIC : H_GETSTATIC;
+                  mv.visitCode();
+                  mv.visitLdcInsn(new Handle(type, binaryName(I.class), "ii", "I", true));
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+                return mv;
+              }
+            })
+        .transform();
+  }
+
+  public interface I {
+    int ii = 42;
+  }
+
+  public static class Main {
+
+    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 void read() throws Throwable {
+      System.out.println(iiGetField().invoke());
+    }
+
+    public static void main(String[] args) throws Throwable {
+      read();
+      // Note: having the try-catch inlined here hits ART issue b/235576668.
+      try {
+        iiSetField().invoke(19);
+        System.out.println("pass");
+      } catch (IllegalAccessError e) {
+        System.out.println("error");
+      } catch (RuntimeException e) {
+        System.out.println("exception");
+      }
+      read();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
new file mode 100644
index 0000000..459ce08
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
@@ -0,0 +1,206 @@
+// Copyright (c) 2022, 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.methodhandles.invokespecial;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.H_INVOKESPECIAL;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class InvokeSpecialMethodHandleTest extends TestBase {
+
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
+
+  private final TestParameters parameters;
+  private final LookupType lookupType;
+
+  @Parameters(name = "{0}, lookup:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParameters.builder()
+            // Runtimes without Handle APIs fail in various ways. Start testing beyond that point.
+            .withDexRuntimesStartingFromExcluding(Version.V7_0_0)
+            .withAllApiLevels()
+            .withCfRuntimes()
+            .build(),
+        LookupType.values());
+  }
+
+  public InvokeSpecialMethodHandleTest(TestParameters parameters, LookupType lookupType) {
+    this.parameters = parameters;
+    this.lookupType = lookupType;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(C.class, Main.class)
+        .addProgramClassFileData(getTransformedD())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class, Main.class)
+        .addProgramClassFileData(getTransformedD())
+        .setMinApi(parameters.getApiLevel())
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepClassAndMembersRules(C.class, D.class, Main.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addProgramClasses(C.class, Main.class)
+        .addProgramClassFileData(getTransformedD())
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(this::checkDiagnostics)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkResult);
+  }
+
+  private boolean hasConstMethodCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithConstMethodHandleSupport());
+  }
+
+  private boolean hasInvokePolymorphicCompileSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokePolymorphicSupport());
+  }
+
+  private void checkDiagnostics(TestDiagnosticMessages diagnostics) {
+    if ((lookupType == LookupType.DYNAMIC && !hasInvokePolymorphicCompileSupport())
+        || (lookupType == LookupType.CONSTANT && !hasConstMethodCompileSupport())) {
+      diagnostics
+          .assertAllWarningsMatch(diagnosticType(UnsupportedFeatureDiagnostic.class))
+          .assertOnlyWarnings();
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    if (lookupType == LookupType.DYNAMIC && hasInvokePolymorphicCompileSupport()) {
+      result.assertSuccessWithOutput(getExpected());
+    } else if (lookupType == LookupType.CONSTANT && hasConstMethodCompileSupport()) {
+      if (parameters.isDexRuntimeVersion(Version.V9_0_0)) {
+        // VM 9 incorrectly prints out the overridden method despite the direct target.
+        result.assertSuccessWithOutput("");
+      } else if (parameters.isDexRuntimeVersion(Version.V13_0_0)) {
+        // TODO(b/235807678): Subsequent ART VMs incorrectly throw IAE.
+        result.assertFailureWithErrorThatThrows(IllegalAccessError.class);
+      } else if (parameters.isDexRuntime()
+          && parameters.asDexRuntime().getVersion().isNewerThan(Version.V9_0_0)) {
+        // VMs between 9 and 13 segfault.
+        result.assertFailureWithErrorThatMatches(containsString("HandleUnexpectedSignal"));
+      } else {
+        result.assertSuccessWithOutput(getExpected());
+      }
+    } else {
+      result.assertFailureWithErrorThatMatches(
+          containsString(
+              lookupType == LookupType.DYNAMIC ? "invoke-polymorphic" : "const-method-handle"));
+    }
+  }
+
+  private String getExpected() {
+    return StringUtils.lines("vvi 20");
+  }
+
+  byte[] getTransformedD() throws Exception {
+    return transformer(D.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                MethodVisitor mv =
+                    super.visitMethod(access, name, descriptor, signature, exceptions);
+                if (lookupType == LookupType.CONSTANT && name.equals("vcviSpecialMethod")) {
+                  mv.visitCode();
+                  mv.visitLdcInsn(
+                      new Handle(H_INVOKESPECIAL, binaryName(C.class), "vvi", "(I)V", false));
+                  mv.visitInsn(ARETURN);
+                  mv.visitMaxs(-1, -1);
+                  mv.visitEnd();
+                  return null;
+                }
+                return mv;
+              }
+            })
+        .transform();
+  }
+
+  public static class C {
+
+    public void vvi(int i) {
+      System.out.println("vvi " + i);
+    }
+  }
+
+  public static class D extends C {
+
+    public void vvi(int i) {
+      // Overridden to output nothing.
+    }
+
+    public static MethodHandle vcviSpecialMethod() {
+      try {
+        return MethodHandles.lookup()
+            .findSpecial(C.class, "vvi", MethodType.methodType(void.class, int.class), D.class);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) throws Throwable {
+      MethodHandle methodHandle = D.vcviSpecialMethod();
+      methodHandle.invoke(new D(), 20);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/code/invokedynamic/InvokeCustomRuntimeErrorTest.java b/src/test/java/com/android/tools/r8/code/invokedynamic/InvokeCustomRuntimeErrorTest.java
new file mode 100644
index 0000000..cf34d5d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/code/invokedynamic/InvokeCustomRuntimeErrorTest.java
@@ -0,0 +1,198 @@
+// Copyright (c) 2022, 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.code.invokedynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.CallSite;
+import java.lang.invoke.ConstantCallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.invoke.MethodType;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Test that unrepresentable invoke-dynamic instructions are replaced by throwing instructions. See
+ * b/174733673.
+ */
+@RunWith(Parameterized.class)
+public class InvokeCustomRuntimeErrorTest extends TestBase {
+
+  private static final String EXPECTED = StringUtils.lines("A::foo");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InvokeCustomRuntimeErrorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private boolean hasCompileSupport() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithInvokeCustomSupport());
+  }
+
+  @Test
+  public void testReference() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8CfNoDesugaring() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
+    // Explicitly test that no-desugaring will maintain a passthrough of the CF code.
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .setNoMinApi()
+        .disableDesugaring()
+        .compile()
+        .assertNoMessages()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8DexNoDesugaring() throws Throwable {
+    assumeTrue(parameters.isDexRuntime() && parameters.getApiLevel().equals(AndroidApiLevel.B));
+    // Explicitly test that no-desugaring will still strip instructions.
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .setMinApi(parameters.getApiLevel())
+        .disableDesugaring()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics
+                    .assertAllWarningsMatch(diagnosticType(UnsupportedInvokeCustomDiagnostic.class))
+                    .assertOnlyWarnings())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertStderrMatches(containsString("invoke-dynamic"));
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    // For CF compilations we desugar to API level B, thus it should always fail.
+    AndroidApiLevel minApi =
+        parameters.isDexRuntime() ? parameters.getApiLevel() : AndroidApiLevel.B;
+    boolean expectedSuccess = parameters.isDexRuntime() && hasCompileSupport();
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, A.class)
+        .addProgramClassFileData(getTransformedTestClass())
+        .setMinApi(minApi)
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              if (expectedSuccess) {
+                diagnostics.assertNoMessages();
+              } else {
+                diagnostics
+                    .assertAllWarningsMatch(diagnosticType(UnsupportedInvokeCustomDiagnostic.class))
+                    .assertOnlyWarnings();
+              }
+            })
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            expectedSuccess,
+            r -> r.assertSuccessWithOutput(EXPECTED),
+            r ->
+                r.assertFailureWithErrorThatThrows(RuntimeException.class)
+                    .assertStderrMatches(containsString("invoke-dynamic")));
+  }
+
+  private byte[] getTransformedTestClass() throws Exception {
+    ClassReference aClass = Reference.classFromClass(A.class);
+    MethodReference iFoo = Reference.methodFromMethod(I.class.getDeclaredMethod("foo"));
+    MethodReference bsm =
+        Reference.methodFromMethod(
+            TestClass.class.getDeclaredMethod(
+                "bsmCreateCallSite",
+                Lookup.class,
+                String.class,
+                MethodType.class,
+                MethodHandle.class));
+    return transformer(TestClass.class)
+        .transformMethodInsnInMethod(
+            "main",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              if (name.equals("replaced")) {
+                visitor.visitInvokeDynamicInsn(
+                    iFoo.getMethodName(),
+                    "(" + aClass.getDescriptor() + ")V",
+                    new Handle(
+                        Opcodes.H_INVOKESTATIC,
+                        bsm.getHolderClass().getBinaryName(),
+                        bsm.getMethodName(),
+                        bsm.getMethodDescriptor(),
+                        false),
+                    new Handle(
+                        Opcodes.H_INVOKEVIRTUAL,
+                        aClass.getBinaryName(),
+                        iFoo.getMethodName(),
+                        iFoo.getMethodDescriptor(),
+                        false));
+              } else {
+                visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+              }
+            })
+        .transform();
+  }
+
+  public interface I {
+    void foo();
+  }
+
+  public static class A implements I {
+
+    @Override
+    public void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  static class TestClass {
+
+    public static CallSite bsmCreateCallSite(
+        MethodHandles.Lookup caller, String name, MethodType type, MethodHandle handle)
+        throws Throwable {
+      return new ConstantCallSite(handle);
+    }
+
+    public static void replaced(Object o) {
+      throw new RuntimeException("unreachable!");
+    }
+
+    public static void main(String[] args) {
+      replaced(new A());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java b/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java
index d6eeb25..efcbe63 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/diagnostics/UnsupportedFeaturesDiagnosticApiTest.java
@@ -10,10 +10,19 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.compilerapi.CompilerApiTest;
 import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
-import com.android.tools.r8.errors.InvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstMethodTypeDiagnostic;
+import com.android.tools.r8.errors.UnsupportedDefaultInterfaceMethodDiagnostic;
 import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokeCustomDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicMethodHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedInvokePolymorphicVarHandleDiagnostic;
+import com.android.tools.r8.errors.UnsupportedPrivateInterfaceMethodDiagnostic;
+import com.android.tools.r8.errors.UnsupportedStaticInterfaceMethodDiagnostic;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import org.junit.Test;
 
@@ -30,12 +39,29 @@
 
   @Test
   public void test() throws Exception {
+    check(UnsupportedDefaultInterfaceMethodDiagnostic::new, "default-interface-method", 24);
+    check(UnsupportedStaticInterfaceMethodDiagnostic::new, "static-interface-method", 24);
+    check(UnsupportedPrivateInterfaceMethodDiagnostic::new, "private-interface-method", 24);
+    check(UnsupportedInvokeCustomDiagnostic::new, "invoke-custom", 26);
+    check(
+        UnsupportedInvokePolymorphicMethodHandleDiagnostic::new,
+        "invoke-polymorphic-method-handle",
+        26);
+    check(
+        UnsupportedInvokePolymorphicVarHandleDiagnostic::new, "invoke-polymorphic-var-handle", 28);
+    check(UnsupportedConstMethodHandleDiagnostic::new, "const-method-handle", 28);
+    check(UnsupportedConstMethodTypeDiagnostic::new, "const-method-type", 28);
+    check(UnsupportedConstDynamicDiagnostic::new, "const-dynamic", -1);
+  }
+
+  public void check(
+      BiFunction<Origin, Position, UnsupportedFeatureDiagnostic> makeFn,
+      String descriptor,
+      int level) {
     ApiTest test = new ApiTest(ApiTest.PARAMETERS);
     test.run(
-        new InvokeCustomDiagnostic(Origin.unknown(), Position.UNKNOWN),
-        result -> {
-          assertEquals("invoke-custom @ 26", result);
-        });
+        makeFn.apply(Origin.unknown(), Position.UNKNOWN),
+        result -> assertEquals(descriptor + " @ " + level, result));
   }
 
   public static class ApiTest extends CompilerApiTest {
@@ -49,7 +75,16 @@
           new DiagnosticsHandler() {
             @Override
             public void warning(Diagnostic warning) {
-              if (warning instanceof UnsupportedFeatureDiagnostic) {
+              if (warning instanceof UnsupportedConstDynamicDiagnostic
+                  || warning instanceof UnsupportedConstMethodHandleDiagnostic
+                  || warning instanceof UnsupportedConstMethodTypeDiagnostic
+                  || warning instanceof UnsupportedDefaultInterfaceMethodDiagnostic
+                  || warning instanceof UnsupportedInvokeCustomDiagnostic
+                  || warning instanceof UnsupportedInvokePolymorphicMethodHandleDiagnostic
+                  || warning instanceof UnsupportedInvokePolymorphicVarHandleDiagnostic
+                  || warning instanceof UnsupportedPrivateInterfaceMethodDiagnostic
+                  || warning instanceof UnsupportedStaticInterfaceMethodDiagnostic
+                  || warning instanceof UnsupportedFeatureDiagnostic) {
                 UnsupportedFeatureDiagnostic unsupportedFeature =
                     (UnsupportedFeatureDiagnostic) warning;
                 String featureDescriptor = unsupportedFeature.getFeatureDescriptor();
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java
index 48cbeaf..8c1d8f1 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicHolderTest.java
@@ -4,16 +4,23 @@
 
 package com.android.tools.r8.desugar.constantdynamic;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsLevel;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.errors.ConstantDynamicDesugarDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.IOException;
 import org.junit.Test;
@@ -37,7 +44,7 @@
   @Test
   public void testReference() throws Exception {
     assumeTrue(parameters.isCfRuntime());
-    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
     assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
 
     testForJvm()
@@ -46,33 +53,70 @@
         .assertSuccessWithOutputLines("null");
   }
 
-  @Test(expected = CompilationFailedException.class)
+  @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
 
     testForD8()
         .addProgramClassFileData(getTransformedMain())
         .setMinApi(parameters.getApiLevel())
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) ->
+                (diagnostic instanceof UnsupportedFeatureDiagnostic
+                        || diagnostic instanceof ConstantDynamicDesugarDiagnostic)
+                    ? DiagnosticsLevel.WARNING
+                    : level)
         .compileWithExpectedDiagnostics(
             diagnostics ->
-                diagnostics.assertErrorMessageThatMatches(
-                    containsString(
-                        "Unsupported dynamic constant (runtime provided bootstrap method)")));
+                diagnostics.assertWarningsMatch(
+                    diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                    allOf(
+                        diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                        diagnosticMessage(
+                            containsString(
+                                "Unsupported dynamic constant (runtime provided bootstrap"
+                                    + " method)")))))
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
+  // TODO(b/198142625): Support const-dynamic in IR CF/CF.
   @Test(expected = CompilationFailedException.class)
-  public void testR8() throws Exception {
-    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+  public void testR8Cf() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
 
     testForR8(parameters.getBackend())
         .addProgramClassFileData(getTransformedMain())
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(Main.class)
+        .compile();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedMain())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .allowDiagnosticWarningMessages()
+        .mapUnsupportedFeaturesToWarnings()
         .compileWithExpectedDiagnostics(
-            diagnostics ->
-                diagnostics.assertErrorMessageThatMatches(
-                    containsString(
-                        "Unsupported dynamic constant (runtime provided bootstrap method)")));
+            diagnostics -> {
+              if (parameters.isDexRuntime()) {
+                diagnostics.assertWarningsMatch(
+                    allOf(
+                        diagnosticType(UnsupportedFeatureDiagnostic.class),
+                        diagnosticMessage(containsString("const-dynamic"))));
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
   private byte[] getTransformedMain() throws IOException {
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
index 1ae1ccf..87f3382 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
@@ -4,19 +4,23 @@
 package com.android.tools.r8.desugar.constantdynamic;
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
-import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
-import static com.android.tools.r8.OriginMatcher.hasParent;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
 import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsLevel;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.errors.ConstantDynamicDesugarDiagnostic;
+import com.android.tools.r8.errors.UnsupportedConstDynamicDiagnostic;
+import com.android.tools.r8.errors.UnsupportedFeatureDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
@@ -49,7 +53,7 @@
   @Test
   public void testReference() throws Exception {
     assumeTrue(parameters.isCfRuntime());
-    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
     assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
 
     testForJvm()
@@ -59,65 +63,130 @@
   }
 
   @Test
+  public void testD8CfNoDesugar() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setNoMinApi()
+        .disableDesugaring()
+        .compile()
+        .assertNoMessages()
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
   public void testD8Cf() throws Exception {
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForD8(Backend.CF)
-                .addProgramClassFileData(getTransformedClasses())
-                .setMinApi(parameters.getApiLevel())
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      diagnostics.assertOnlyErrors();
-                      diagnostics.assertErrorsMatch(
-                          diagnosticMessage(
-                              containsString("Unsupported dynamic constant (different owner)")));
-                    }));
+    assumeTrue(parameters.isCfRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) ->
+                (diagnostic instanceof ConstantDynamicDesugarDiagnostic
+                        || diagnostic instanceof UnsupportedFeatureDiagnostic)
+                    ? DiagnosticsLevel.WARNING
+                    : level)
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics
+                    .assertAllWarningsMatch(
+                        anyOf(
+                            allOf(
+                                diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                                diagnosticMessage(containsString("const-dynamic"))),
+                            allOf(
+                                diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                                diagnosticMessage(
+                                    containsString(
+                                        "Unsupported dynamic constant (different owner)")))))
+                    .assertOnlyWarnings())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForD8(parameters.getBackend())
-                .addProgramClassFileData(getTransformedClasses())
-                .setMinApi(parameters.getApiLevel())
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      diagnostics.assertOnlyErrors();
-                      diagnostics.assertErrorsMatch(
-                          allOf(
-                              diagnosticMessage(
-                                  containsString("Unsupported dynamic constant (different owner)")),
-                              diagnosticOrigin(hasParent(Origin.unknown()))));
-                    }));
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) ->
+                (diagnostic instanceof ConstantDynamicDesugarDiagnostic
+                        || diagnostic instanceof UnsupportedFeatureDiagnostic)
+                    ? DiagnosticsLevel.WARNING
+                    : level)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics.assertOnlyWarnings();
+              diagnostics.assertAllWarningsMatch(
+                  anyOf(
+                      allOf(
+                          diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                          diagnosticMessage(containsString("const-dynamic"))),
+                      allOf(
+                          diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                          diagnosticMessage(
+                              containsString("Unsupported dynamic constant (different owner)")))));
+            })
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
   }
 
   @Test
-  public void testR8() throws Exception {
-    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
-
+  public void testR8Cf() {
+    assumeTrue(parameters.isCfRuntime() && parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
     assertThrows(
         CompilationFailedException.class,
         () ->
             testForR8(parameters.getBackend())
                 .addProgramClassFileData(getTransformedClasses())
-                .setMinApi(parameters.getApiLevel())
                 .addKeepMainRule(MAIN_CLASS)
                 .compileWithExpectedDiagnostics(
                     diagnostics -> {
                       diagnostics.assertOnlyErrors();
                       diagnostics.assertErrorsMatch(
-                          allOf(
-                              diagnosticMessage(
-                                  containsString("Unsupported dynamic constant (different owner)")),
-                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                          diagnosticMessage(
+                              containsString("Unsupported dynamic constant (not desugaring)")));
                     }));
   }
 
+  @Test
+  public void testR8Dex() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .allowDiagnosticMessages()
+        .mapUnsupportedFeaturesToWarnings()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics
+                  .assertAllWarningsMatch(
+                      anyOf(
+                          allOf(
+                              diagnosticType(UnsupportedConstDynamicDiagnostic.class),
+                              diagnosticMessage(containsString("const-dynamic"))),
+                          allOf(
+                              diagnosticType(ConstantDynamicDesugarDiagnostic.class),
+                              diagnosticMessage(
+                                  containsString(
+                                      "Unsupported dynamic constant (different owner)")))))
+                  .assertOnlyWarnings();
+            })
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .assertFailureWithErrorThatMatches(containsString("const-dynamic"));
+  }
+
   private Collection<byte[]> getTransformedClasses() throws IOException {
     return ImmutableList.of(
         transformer(A.class)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java b/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
index b8ad8d9..2bc9233 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
@@ -29,4 +29,8 @@
   public static NoSuchMethodError throwNoSuchMethodError() {
     throw new NoSuchMethodError();
   }
+
+  public static RuntimeException throwRuntimeExceptionWithMessage(String message) {
+    throw new RuntimeException(message);
+  }
 }