Version 1.5.50

Cherry-pick: Implement instructionMayHaveSideEffects of const-class.
CL: https://r8-review.googlesource.com/c/r8/+/39740

Cherry-pick:
  Reproduce b/135210786: wrong side-effect analysis of const-class.
CL: https://r8-review.googlesource.com/c/r8/+/39700

Bug: 135210786
Change-Id: If674db04aa8a8e31544782c9dee68d23ed7bf251
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 20ca7f5..f244e33 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.5.49";
+  public static final String LABEL = "1.5.50";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index b62bcaa..928cb27 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -14,6 +14,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -726,6 +727,14 @@
     return false;
   }
 
+  public boolean isResolvable(AppView<?> appView) {
+    if (!isProgramClass()
+        && !appView.dexItemFactory().libraryTypesAssumedToBePresent.contains(type)) {
+      return false;
+    }
+    return Streams.stream(allImmediateSupertypes()).allMatch(type -> type.isResolvable(appView));
+  }
+
   public boolean isSerializable(AppView<? extends AppInfoWithSubtyping> appView) {
     return appView.appInfo().isSerializable(type);
   }
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 38a926c..2a931f0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -194,6 +194,7 @@
   public final DexString proxyDescriptor = createString("Ljava/lang/reflect/Proxy;");
   public final DexString serviceLoaderDescriptor = createString("Ljava/util/ServiceLoader;");
   public final DexString listDescriptor = createString("Ljava/util/List;");
+  public final DexString callableDescriptor = createString("Ljava/util/concurrent/Callable;");
 
   public final DexString throwableDescriptor = createString(throwableDescriptorString);
   public final DexString illegalAccessErrorDescriptor =
@@ -201,6 +202,8 @@
   public final DexString icceDescriptor = createString("Ljava/lang/IncompatibleClassChangeError;");
   public final DexString exceptionInInitializerErrorDescriptor =
       createString("Ljava/lang/ExceptionInInitializerError;");
+  public final DexString noClassDefFoundErrorDescriptor =
+      createString("Ljava/lang/NoClassDefFoundError;");
   public final DexString noSuchFieldErrorDescriptor = createString("Ljava/lang/NoSuchFieldError;");
   public final DexString npeDescriptor = createString("Ljava/lang/NullPointerException;");
   public final DexString reflectiveOperationExceptionDescriptor =
@@ -266,12 +269,14 @@
   public final DexType proxyType = createType(proxyDescriptor);
   public final DexType serviceLoaderType = createType(serviceLoaderDescriptor);
   public final DexType listType = createType(listDescriptor);
+  public final DexType callableType = createType(callableDescriptor);
 
   public final DexType throwableType = createType(throwableDescriptor);
   public final DexType illegalAccessErrorType = createType(illegalAccessErrorDescriptor);
   public final DexType icceType = createType(icceDescriptor);
   public final DexType exceptionInInitializerErrorType =
       createType(exceptionInInitializerErrorDescriptor);
+  public final DexType noClassDefFoundErrorType = createType(noClassDefFoundErrorDescriptor);
   public final DexType noSuchFieldErrorType = createType(noSuchFieldErrorDescriptor);
   public final DexType npeType = createType(npeDescriptor);
   public final DexType reflectiveOperationExceptionType =
@@ -417,7 +422,7 @@
           .build();
 
   public Set<DexType> libraryTypesAssumedToBePresent =
-      ImmutableSet.of(objectType, stringBufferType, stringBuilderType);
+      ImmutableSet.of(objectType, callableType, stringBufferType, stringBuilderType);
 
   public final Set<DexType> libraryTypesWithoutStaticInitialization =
       ImmutableSet.of(
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 801d6f8..d42caf4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -258,6 +258,11 @@
     return clazz != null && clazz.isProgramClass();
   }
 
+  public boolean isResolvable(AppView<?> appView) {
+    DexClass clazz = appView.definitionFor(this);
+    return clazz != null && clazz.isResolvable(appView);
+  }
+
   public int elementSizeForPrimitiveArrayType() {
     assert isPrimitiveArrayType();
     switch (descriptor.content[1]) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
index 3077daf..b122429 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AbstractError;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
@@ -82,29 +83,47 @@
 
   @Override
   public boolean instructionInstanceCanThrow() {
-    // TODO(christofferqa): Should return false in R8 if the class is in the program.
     return true;
   }
 
   @Override
-  public boolean instructionMayHaveSideEffects(AppView<?> appView, DexType context) {
+  public AbstractError instructionInstanceCanThrow(AppView<?> appView, DexType context) {
     DexType baseType = getValue().toBaseType(appView.dexItemFactory());
     if (baseType.isPrimitiveType()) {
-      return false;
+      return AbstractError.bottom();
     }
 
-    if (appView.enableWholeProgramOptimizations()) {
-      DexClass clazz = appView.definitionFor(baseType);
-      if (clazz != null && clazz.isProgramClass()) {
-        return false;
-      }
-    } else {
+    // Not applicable for D8.
+    if (!appView.enableWholeProgramOptimizations()) {
+      // Unless the type of interest is same as the context.
       if (baseType == context) {
-        return false;
+        return AbstractError.bottom();
       }
+      return AbstractError.top();
     }
 
-    return true;
+    DexClass clazz = appView.definitionFor(baseType);
+
+    if (clazz == null) {
+      return AbstractError.specific(appView.dexItemFactory().noClassDefFoundErrorType);
+    }
+    // * NoClassDefFoundError (resolution failure).
+    if (!clazz.isResolvable(appView)) {
+      return AbstractError.specific(appView.dexItemFactory().noClassDefFoundErrorType);
+    }
+    // * IllegalAccessError (not visible from the access context).
+    ConstraintWithTarget classVisibility =
+        ConstraintWithTarget.deriveConstraint(context, baseType, clazz.accessFlags, appView);
+    if (classVisibility == ConstraintWithTarget.NEVER) {
+      return AbstractError.specific(appView.dexItemFactory().illegalAccessErrorType);
+    }
+
+    return AbstractError.bottom();
+  }
+
+  @Override
+  public boolean instructionMayHaveSideEffects(AppView<?> appView, DexType context) {
+    return instructionInstanceCanThrow(appView, context).isThrowing();
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IllegalAccessConstClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IllegalAccessConstClassTest.java
new file mode 100644
index 0000000..88e9d07
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IllegalAccessConstClassTest.java
@@ -0,0 +1,114 @@
+// Copyright (c) 2019, 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.optimize.canonicalization;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+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.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Streams;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+// Will be changed to different package.
+class PackagePrivateClass {
+}
+
+// Will be changed to use the above class in a different package.
+class FakePackagePrivateClassConsumer {
+  public static void main(String... args) {
+    if (System.currentTimeMillis() < -2) {
+      System.out.println(PackagePrivateClass.class.getName());
+    } else if (System.currentTimeMillis() < -1) {
+      System.out.println(PackagePrivateClass.class.getSimpleName());
+    } else {
+      System.out.println("No need to load any classes");
+    }
+  }
+}
+
+@RunWith(Parameterized.class)
+public class IllegalAccessConstClassTest extends TestBase {
+  private static final Class<?> MAIN = FakePackagePrivateClassConsumer.class;
+  private static final String JAVA_OUTPUT = StringUtils.lines(
+      "No need to load any classes"
+  );
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  private final TestParameters parameters;
+
+  public IllegalAccessConstClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJVMOutput() throws Exception {
+    assumeTrue("Only run JVM reference on CF runtimes", parameters.isCfRuntime());
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
+
+    testForD8()
+        .release()
+        .addProgramClassFileData(
+            IllegalAccessConstClassTestDump.PackagePrivateClassDump.dump())
+        .addProgramClassFileData(
+            IllegalAccessConstClassTestDump.FakePackagePrivateClassConsumerDump.dump())
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(
+            IllegalAccessConstClassTestDump.PackagePrivateClassDump.dump())
+        .addProgramClassFileData(
+            IllegalAccessConstClassTestDump.FakePackagePrivateClassConsumerDump.dump())
+        .addKeepMainRule(MAIN)
+        .noMinification()
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject prvClass = inspector.clazz("PackagePrivateClass");
+    assertThat(prvClass, isPresent());
+
+    ClassSubject mainClass = inspector.clazz(MAIN);
+    assertThat(mainClass, isPresent());
+    MethodSubject mainMethod = mainClass.mainMethod();
+    assertThat(mainMethod, isPresent());
+    // No canonicalization of const-class instructions.
+    assertEquals(
+        2,
+        Streams.stream(mainMethod.iterateInstructions(InstructionSubject::isConstClass)).count());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IllegalAccessConstClassTestDump.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IllegalAccessConstClassTestDump.java
new file mode 100644
index 0000000..ea11c78
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IllegalAccessConstClassTestDump.java
@@ -0,0 +1,169 @@
+// Copyright (c) 2019, 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.optimize.canonicalization;
+
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+class IllegalAccessConstClassTestDump {
+
+  // Originated from the following code snippet:
+  //
+  //   class ...PackagePrivateClass {}
+  //
+  // then repackaged to the top-level.
+  static class PackagePrivateClassDump implements Opcodes {
+
+    public static byte[] dump () throws Exception {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      classWriter.visit(V1_8, ACC_SUPER, "PackagePrivateClass", null, "java/lang/Object", null);
+
+      classWriter.visitSource("IllegalAccessConstClassTest.java", null);
+
+      {
+        methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null);
+        methodVisitor.visitCode();
+        Label label0 = new Label();
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitLineNumber(25, label0);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitInsn(RETURN);
+        Label label1 = new Label();
+        methodVisitor.visitLabel(label1);
+        methodVisitor.visitLocalVariable("this", "LPackagePrivateClass;", null, label0, label1, 0);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return classWriter.toByteArray();
+    }
+  }
+
+  // Originated from the following code snippet:
+  //
+  //   class FakePackagePrivateClassConsumer {
+  //     public static void main(String... args) {
+  //       if (System.currentTimeMillis() < -2) {
+  //         System.out.println(PackagePrivateClass.class.getName());
+  //       } else if (System.currentTimeMillis() < -1) {
+  //         System.out.println(PackagePrivateClass.class.getSimpleName());
+  //       } else {
+  //         System.out.println("No need to load any classes");
+  //       }
+  //     }
+  //   }
+  //
+  // then rewritten to use the repackaged PackagePrivateClass instead.
+  static class FakePackagePrivateClassConsumerDump implements Opcodes {
+
+    public static byte[] dump () throws Exception {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      classWriter.visit(
+          V1_8,
+          ACC_SUPER,
+          "com/android/tools/r8/ir/optimize/canonicalization/FakePackagePrivateClassConsumer",
+          null,
+          "java/lang/Object",
+          null);
+
+      classWriter.visitSource("IllegalAccessConstClassTest.java", null);
+
+      {
+        methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null);
+        methodVisitor.visitCode();
+        Label label0 = new Label();
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitLineNumber(28, label0);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitInsn(RETURN);
+        Label label1 = new Label();
+        methodVisitor.visitLabel(label1);
+        methodVisitor.visitLocalVariable(
+            "this",
+            "Lcom/android/tools/r8/ir/optimize/canonicalization/FakePackagePrivateClassConsumer;",
+            null,
+            label0,
+            label1,
+            0);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor = classWriter.visitMethod(
+            ACC_PUBLIC | ACC_STATIC | ACC_VARARGS, "main", "([Ljava/lang/String;)V", null, null);
+        methodVisitor.visitCode();
+        Label label0 = new Label();
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitLineNumber(30, label0);
+        methodVisitor.visitMethodInsn(
+            INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
+        methodVisitor.visitLdcInsn(new Long(-2L));
+        methodVisitor.visitInsn(LCMP);
+        Label label1 = new Label();
+        methodVisitor.visitJumpInsn(IFGE, label1);
+        Label label2 = new Label();
+        methodVisitor.visitLabel(label2);
+        methodVisitor.visitLineNumber(31, label2);
+        methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+        methodVisitor.visitLdcInsn(Type.getType("LPackagePrivateClass;"));
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+        Label label3 = new Label();
+        methodVisitor.visitJumpInsn(GOTO, label3);
+        methodVisitor.visitLabel(label1);
+        methodVisitor.visitLineNumber(32, label1);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitMethodInsn(
+            INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
+        methodVisitor.visitLdcInsn(new Long(-1L));
+        methodVisitor.visitInsn(LCMP);
+        Label label4 = new Label();
+        methodVisitor.visitJumpInsn(IFGE, label4);
+        Label label5 = new Label();
+        methodVisitor.visitLabel(label5);
+        methodVisitor.visitLineNumber(33, label5);
+        methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+        methodVisitor.visitLdcInsn(Type.getType("LPackagePrivateClass;"));
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+        methodVisitor.visitJumpInsn(GOTO, label3);
+        methodVisitor.visitLabel(label4);
+        methodVisitor.visitLineNumber(35, label4);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+        methodVisitor.visitLdcInsn("No need to load any classes");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+        methodVisitor.visitLabel(label3);
+        methodVisitor.visitLineNumber(37, label3);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitInsn(RETURN);
+        Label label6 = new Label();
+        methodVisitor.visitLabel(label6);
+        methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label6, 0);
+        methodVisitor.visitMaxs(4, 1);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return classWriter.toByteArray();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/UnresolvableLibraryConstClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/UnresolvableLibraryConstClassTest.java
new file mode 100644
index 0000000..7d98e26
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/UnresolvableLibraryConstClassTest.java
@@ -0,0 +1,141 @@
+// Copyright (c) 2019, 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.optimize.canonicalization;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Streams;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+class LibraryClass {
+}
+
+@NeverMerge
+class ProgramClass1 extends LibraryClass {
+}
+
+class ProgramSubClass extends ProgramClass1 {
+}
+
+interface LibraryInterface {
+  void foo();
+}
+
+class ProgramClass2 implements LibraryInterface {
+  @Override
+  public void foo() {
+    System.out.println("ProgramClass2::foo");
+  }
+}
+
+class TestMain {
+  public static void main(String... args) {
+    if (System.currentTimeMillis() < -6) {
+      System.out.println(ProgramSubClass.class.getName());
+    } else if (System.currentTimeMillis() < -5) {
+      System.out.println(ProgramSubClass.class.getSimpleName());
+    } else if (System.currentTimeMillis() < -4) {
+      System.out.println(ProgramClass1.class.getName());
+    } else if (System.currentTimeMillis() < -3) {
+      System.out.println(ProgramClass1.class.getSimpleName());
+    } else if (System.currentTimeMillis() < -2) {
+      System.out.println(ProgramClass2.class.getName());
+    } else if (System.currentTimeMillis() < -1) {
+      System.out.println(ProgramClass2.class.getSimpleName());
+    } else {
+      System.out.println("No need to load any classes");
+    }
+  }
+}
+
+@RunWith(Parameterized.class)
+public class UnresolvableLibraryConstClassTest extends TestBase {
+  private static final Class<?> MAIN = TestMain.class;
+  private static final String JAVA_OUTPUT = StringUtils.lines(
+      "No need to load any classes"
+  );
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  private final TestParameters parameters;
+
+  public UnresolvableLibraryConstClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJVMOutput() throws Exception {
+    assumeTrue("Only run JVM reference on CF runtimes", parameters.isCfRuntime());
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
+
+    testForD8()
+        .release()
+        .addProgramClasses(ProgramClass1.class, ProgramClass2.class, ProgramSubClass.class, MAIN)
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addLibraryClasses(LibraryClass.class, LibraryInterface.class)
+        .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
+        .addProgramClasses(ProgramClass1.class, ProgramClass2.class, ProgramSubClass.class, MAIN)
+        .addKeepMainRule(MAIN)
+        .noMinification()
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject programClass1 = inspector.clazz(ProgramClass1.class);
+    assertThat(programClass1, isPresent());
+    ClassSubject programSubClass = inspector.clazz(ProgramSubClass.class);
+    assertThat(programSubClass, isPresent());
+    ClassSubject programClass2 = inspector.clazz(ProgramClass2.class);
+    assertThat(programClass2, isPresent());
+
+    ClassSubject mainClass = inspector.clazz(MAIN);
+    assertThat(mainClass, isPresent());
+    MethodSubject mainMethod = mainClass.mainMethod();
+    assertThat(mainMethod, isPresent());
+    // No canonicalization of const-class instructions.
+    assertEquals(
+        6,
+        Streams.stream(mainMethod.iterateInstructions(InstructionSubject::isConstClass)).count());
+  }
+}