Allow storing arbitrary class types in fields with an interface type

This CL updates the type checker to accept field-assignments where an object of class type C is assigned to a field that has interface type I and C is not a subtype of I. This is needed since interface types are treated like Object according to the JVM specification (https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.10.1.2-100). Prior to this CL, the body of methods with such instructions were replaced by "throw null".

Note that, although R8 should generally not encounter such field-assignments, we have seen such code in libraries that have been shrunk with another compiler than R8.

With this CL, R8 is technically still unsound. Due to assignment rules in the JVM specification, it is not safe to trust that a value that is being read from a field with interface type I actually has type I. This CL additionally extends the `InvalidTypeTest` to demonstrate that all of DX, D8, Proguard, and R8 are unsound in this regard.

Bug: 120277396
Change-Id: Ic403d769d26adab3a5cc3e30cb7e9f24e2cd41dd
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java b/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
index 0da99ae..be7709c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.FieldInstruction;
@@ -73,7 +74,18 @@
     TypeLatticeElement valueType = value.getTypeLattice();
     TypeLatticeElement fieldType = TypeLatticeElement.fromDexType(
         instruction.getField().type, valueType.isNullable(), appInfo);
-    return isSubtypeOf(valueType, fieldType);
+    if (isSubtypeOf(valueType, fieldType)) {
+      return true;
+    }
+
+    if (valueType.isReference()) {
+      // Interface types are treated like Object according to the JVM spec.
+      // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.10.1.2-100
+      DexClass clazz = appInfo.definitionFor(instruction.getField().type);
+      return clazz != null && clazz.isInterface();
+    }
+
+    return false;
   }
 
   public boolean check(Throw instruction) {
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index 109b42d..c8d6bd9 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -191,8 +191,15 @@
 
   @Override
   public ProguardTestBuilder addProgramFiles(Collection<Path> files) {
-    throw new Unimplemented(
-        "No support for adding paths directly (we need to compute the descriptor)");
+    for (Path file : files) {
+      if (FileUtils.isJarFile(file)) {
+        injars.add(file);
+      } else {
+        throw new Unimplemented(
+            "No support for adding paths directly (we need to compute the descriptor)");
+      }
+    }
+    return self();
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 0642d44..5efc26d 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -904,9 +904,6 @@
   // Tests that are invalid dex files and on which R8/D8 fails and that is OK.
   private static final Multimap<String, TestCondition> expectedToFailWithCompiler =
       new ImmutableListMultimap.Builder<String, TestCondition>()
-          // Test uses separate compilation to generate an input program where an object of
-          // type X is stored in a field that has static type Y, and X is not a subtype of Y.
-          .put("069-field-type", TestCondition.match(TestCondition.R8_COMPILER))
           // When starting from the Jar frontend we see the A$B class both from the Java source
           // code and from the smali dex code. We reject that because there are then two definitions
           // of the same class in the application. When running from the final dex files there is
diff --git a/src/test/java/com/android/tools/r8/internal/NestTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/NestTreeShakeJarVerificationTest.java
index 6514635..8166511 100644
--- a/src/test/java/com/android/tools/r8/internal/NestTreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/NestTreeShakeJarVerificationTest.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import java.util.stream.Stream;
@@ -19,21 +18,19 @@
 
   @Test
   public void buildAndTreeShakeFromDeployJar() throws Exception {
-    AndroidApp app =
-        runAndCheckVerification(
-            CompilerUnderTest.R8,
-            CompilationMode.RELEASE,
-            null,
-            ImmutableList.of(BASE + PG_CONF, BASE + PG_CONF_NO_OPT),
-            options -> options.testing.allowTypeErrors = true,
-            ImmutableList.of(BASE + DEPLOY_JAR));
+    runAndCheckVerification(
+        CompilerUnderTest.R8,
+        CompilationMode.RELEASE,
+        null,
+        ImmutableList.of(BASE + PG_CONF, BASE + PG_CONF_NO_OPT),
+        null,
+        ImmutableList.of(BASE + DEPLOY_JAR));
     assertEquals(0, filterKotlinMetadata(handler.warnings).count());
     assertEquals(0, filterKotlinMetadata(handler.infos).count());
   }
 
   private Stream<Diagnostic> filterKotlinMetadata(List<Diagnostic> warnings) {
-    return warnings.stream().filter(diagnostic -> {
-      return diagnostic.getDiagnosticMessage().contains("kotlin.Metadata");
-    });
+    return warnings.stream()
+        .filter(diagnostic -> diagnostic.getDiagnosticMessage().contains("kotlin.Metadata"));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index 7f970b9..1d0a1ba 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -47,8 +47,6 @@
               // Store the generated Proguard map.
               options.proguardMapConsumer =
                   (proguardMap, handler) -> result.proguardMap = proguardMap;
-              // Allow type errors (methods that do not type check are assumed to be unreachable).
-              options.testing.allowTypeErrors = true;
             });
     return result;
   }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
index 3ba32fa..67979ba 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
@@ -44,6 +44,5 @@
 
   private void configure(InternalOptions options) {
     options.ignoreMissingClasses = true;
-    options.testing.allowTypeErrors = true;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java
index cb98933..1b650e5 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java
@@ -9,7 +9,6 @@
 public class R8GMSCoreV4VerificationTest extends GMSCoreCompilationTestBase {
   @Test
   public void verify() throws Exception {
-    runR8AndCheckVerification(
-        CompilationMode.RELEASE, GMSCORE_V4_DIR, options -> options.testing.allowTypeErrors = true);
+    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V4_DIR);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java
index ad82d3d..df73644 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java
@@ -9,7 +9,6 @@
 public class R8GMSCoreV5VerificationTest extends GMSCoreCompilationTestBase {
   @Test
   public void verify() throws Exception {
-    runR8AndCheckVerification(
-        CompilationMode.RELEASE, GMSCORE_V5_DIR, options -> options.testing.allowTypeErrors = true);
+    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V5_DIR);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java
index b473fd3..a872ff5 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java
@@ -9,7 +9,6 @@
 public class R8GMSCoreV6VerificationTest extends GMSCoreCompilationTestBase {
   @Test
   public void verify() throws Exception {
-    runR8AndCheckVerification(
-        CompilationMode.RELEASE, GMSCORE_V6_DIR, options -> options.testing.allowTypeErrors = true);
+    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V6_DIR);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java
index ce82086..2cda255 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java
@@ -10,7 +10,6 @@
 
   @Test
   public void verify() throws Exception {
-    runR8AndCheckVerification(
-        CompilationMode.RELEASE, GMSCORE_V7_DIR, options -> options.testing.allowTypeErrors = true);
+    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V7_DIR);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java
index c5b712d..3f48336 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java
@@ -10,7 +10,6 @@
 
   @Test
   public void verify() throws Exception {
-    runR8AndCheckVerification(
-        CompilationMode.RELEASE, GMSCORE_V8_DIR, options -> options.testing.allowTypeErrors = true);
+    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V8_DIR);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
index b16cf92..8435276 100644
--- a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
@@ -17,11 +17,6 @@
 
   public void runR8AndCheckVerification(CompilationMode mode, String input) throws Exception {
     runAndCheckVerification(
-        CompilerUnderTest.R8,
-        mode,
-        BASE + APK,
-        null,
-        options -> options.testing.allowTypeErrors = true,
-        ImmutableList.of(BASE + input));
+        CompilerUnderTest.R8, mode, BASE + APK, null, null, ImmutableList.of(BASE + input));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java b/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java
index f8d8e7c..69232b8 100644
--- a/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java
@@ -131,13 +131,12 @@
 
     // Run processed (output) program on ART
     ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
-    assertNotEquals(0, artResult.exitCode);
-    assertThat(artResult.stderr, containsString("java.lang.NullPointerException"));
+    assertEquals(0, artResult.exitCode);
     assertThat(artResult.stderr, not(containsString("DoFieldPut")));
 
     CodeInspector inspector = new CodeInspector(processedApp);
     ClassSubject itf1Subject = inspector.clazz(itf1.name);
-    assertThat(itf1Subject, not(isPresent()));
+    assertThat(itf1Subject, isPresent());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
index 5362c38..4248497 100644
--- a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
@@ -9,11 +9,16 @@
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.core.IsNot.not;
 
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.DXTestRunResult;
+import com.android.tools.r8.ProguardTestRunResult;
+import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
 import com.android.tools.r8.jasmin.JasminTestBase;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
@@ -27,6 +32,14 @@
 @RunWith(Parameterized.class)
 public class InvalidTypesTest extends JasminTestBase {
 
+  private enum Compiler {
+    DX,
+    D8,
+    JAVAC,
+    PROGUARD,
+    R8
+  }
+
   private enum Mode {
     NO_INVOKE,
     INVOKE_UNVERIFIABLE_METHOD,
@@ -51,29 +64,34 @@
 
   private final Backend backend;
   private final Mode mode;
+  private final boolean useInterface;
 
-  public InvalidTypesTest(Backend backend, Mode mode) {
+  public InvalidTypesTest(Backend backend, Mode mode, boolean useInterface) {
     this.backend = backend;
     this.mode = mode;
+    this.useInterface = useInterface;
   }
 
-  @Parameters(name = "Backend: {0}, mode: {1}")
+  @Parameters(name = "Backend: {0}, mode: {1}, use interface: {2}")
   public static Collection<Object[]> parameters() {
-    return buildParameters(Backend.values(), Mode.values());
+    return buildParameters(Backend.values(), Mode.values(), BooleanUtils.values());
   }
 
   @Test
   public void test() throws Exception {
     JasminBuilder jasminBuilder = new JasminBuilder();
 
-    ClassBuilder classA = jasminBuilder.addClass("A");
-    classA.addDefaultConstructor();
-
-    ClassBuilder classB = jasminBuilder.addClass("B");
-    classB.addDefaultConstructor();
+    if (useInterface) {
+      jasminBuilder.addInterface("A");
+    } else {
+      jasminBuilder.addClass("A").addDefaultConstructor();
+    }
+    jasminBuilder.addClass("B").addDefaultConstructor();
+    jasminBuilder.addInterface("I");
 
     ClassBuilder mainClass = jasminBuilder.addClass("TestClass");
     mainClass.addStaticField("f", "LA;");
+    mainClass.addStaticField("g", "LI;");
     mainClass.addMainMethod(
         ".limit stack 2",
         ".limit locals 1",
@@ -92,21 +110,43 @@
     mainClass
         .staticMethodBuilder("m", ImmutableList.of(), "V")
         .setCode(
-            "getstatic java/lang/System/out Ljava/io/PrintStream;",
+            // Print "Unexpected outcome of getstatic" if reading TestClass.f yields `null`.
             "getstatic TestClass/f LA;",
-            "invokevirtual java/lang/Object/toString()Ljava/lang/String;",
+            "ifnonnull Label0",
+            "getstatic java/lang/System/out Ljava/io/PrintStream;",
+            "ldc \"Unexpected outcome of getstatic\"",
             "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
-            "return")
+            // Print "Unexpected outcome of checkcast" if TestClass.f can be casted to A.
+            "Label0:",
+            "getstatic TestClass/f LA;",
+            "checkcast A", // (should throw)
+            "pop",
+            "getstatic java/lang/System/out Ljava/io/PrintStream;",
+            "ldc \"Unexpected outcome of checkcast\"",
+            "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
+            "goto Label1",
+            "Catch:",
+            "pop",
+            // Print "Unexpected outcome of instanceof" if TestClass.f is an instance of A.
+            "Label1:",
+            "getstatic TestClass/f LA;",
+            "instanceof A", // (should return false)
+            "ifeq Return",
+            "getstatic java/lang/System/out Ljava/io/PrintStream;",
+            "ldc \"Unexpected outcome of instanceof\"",
+            "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
+            // Return.
+            "Return:",
+            "return",
+            ".catch java/lang/Throwable from Label0 to Catch using Catch")
         .build();
 
     ClassBuilder UnverifiableClass = jasminBuilder.addClass("UnverifiableClass");
+    UnverifiableClass.staticMethodBuilder("<clinit>", ImmutableList.of(), "V")
+        .setCode("new B", "dup", "invokespecial B/<init>()V", "putstatic TestClass/g LI;", "return")
+        .build();
     UnverifiableClass.staticMethodBuilder("unverifiableMethod", ImmutableList.of(), "V")
         .setCode(
-            "new A",
-            "dup",
-            "invokespecial A/<init>()V",
-            "putstatic TestClass/f LA;",
-            "invokestatic TestClass/m()V",
             "new B",
             "dup",
             "invokespecial B/<init>()V",
@@ -127,54 +167,79 @@
 
     if (backend == Backend.CF) {
       TestRunResult<?> jvmResult = testForJvm().addClasspath(inputJar).run(mainClass.name);
-      checkTestRunResult(jvmResult, false);
+      checkTestRunResult(jvmResult, Compiler.JAVAC);
+
+      ProguardTestRunResult proguardResult =
+          testForProguard()
+              .addProgramFiles(inputJar)
+              .addKeepMainRule(mainClass.name)
+              .addKeepRules("-keep class TestClass { public static I g; }")
+              .run(mainClass.name);
+      checkTestRunResult(proguardResult, Compiler.PROGUARD);
     } else {
       assert backend == Backend.DEX;
 
-      TestRunResult<?> dxResult = testForDX().addProgramFiles(inputJar).run(mainClass.name);
-      checkTestRunResult(dxResult, false);
+      DXTestRunResult dxResult = testForDX().addProgramFiles(inputJar).run(mainClass.name);
+      checkTestRunResult(dxResult, Compiler.DX);
 
-      TestRunResult<?> d8Result = testForD8().addProgramFiles(inputJar).run(mainClass.name);
-      checkTestRunResult(d8Result, false);
+      D8TestRunResult d8Result = testForD8().addProgramFiles(inputJar).run(mainClass.name);
+      checkTestRunResult(d8Result, Compiler.D8);
     }
 
-    TestRunResult r8Result =
+    R8TestRunResult r8Result =
         testForR8(backend)
             .addProgramFiles(inputJar)
             .addKeepMainRule(mainClass.name)
-            .addOptionsModification(options -> options.testing.allowTypeErrors = true)
+            .addKeepRules(
+                "-keep class TestClass { public static I g; }",
+                "-neverinline class TestClass { public static void m(); }")
+            .addOptionsModification(
+                options -> {
+                  if (mode == Mode.INVOKE_UNVERIFIABLE_METHOD) {
+                    options.testing.allowTypeErrors = true;
+                  }
+                })
+            .enableInliningAnnotations()
             .run(mainClass.name);
-    checkTestRunResult(r8Result, true);
+    checkTestRunResult(r8Result, Compiler.R8);
   }
 
-  private void checkTestRunResult(TestRunResult<?> result, boolean isR8) {
+  private void checkTestRunResult(TestRunResult<?> result, Compiler compiler) {
     switch (mode) {
       case NO_INVOKE:
-        result.assertSuccessWithOutput(getExpectedOutput(isR8));
+        result.assertSuccessWithOutput(getExpectedOutput(compiler));
         break;
 
       case INVOKE_VERIFIABLE_METHOD_ON_UNVERIFIABLE_CLASS:
-        if (isR8) {
-          result.assertSuccessWithOutput(getExpectedOutput(isR8));
+        if (useInterface) {
+          result.assertSuccessWithOutput(getExpectedOutput(compiler));
         } else {
-          result
-              .assertFailureWithOutput(getExpectedOutput(isR8))
-              .assertFailureWithErrorThatMatches(getMatcherForExpectedError());
+          if (compiler == Compiler.R8 || compiler == Compiler.PROGUARD) {
+            result.assertSuccessWithOutput(getExpectedOutput(compiler));
+          } else {
+            result
+                .assertFailureWithOutput(getExpectedOutput(compiler))
+                .assertFailureWithErrorThatMatches(getMatcherForExpectedError());
+          }
         }
         break;
 
       case INVOKE_UNVERIFIABLE_METHOD:
-        if (isR8) {
-          result
-              .assertFailureWithOutput(getExpectedOutput(isR8))
-              .assertFailureWithErrorThatMatches(
-                  allOf(
-                      containsString("java.lang.NullPointerException"),
-                      not(containsString("java.lang.VerifyError"))));
+        if (useInterface) {
+          result.assertSuccessWithOutput(getExpectedOutput(compiler));
         } else {
-          result
-              .assertFailureWithOutput(getExpectedOutput(isR8))
-              .assertFailureWithErrorThatMatches(getMatcherForExpectedError());
+          if (compiler == Compiler.R8) {
+            result
+                .assertFailureWithOutput(getExpectedOutput(compiler))
+                .assertFailureWithErrorThatMatches(
+                    allOf(
+                        containsString("java.lang.NullPointerException"),
+                        not(containsString("java.lang.VerifyError"))));
+          } else {
+            result
+                .assertFailureWithOutput(getExpectedOutput(compiler))
+                .assertFailureWithErrorThatMatches(getMatcherForExpectedError());
+          }
         }
         break;
 
@@ -183,14 +248,44 @@
     }
   }
 
-  private String getExpectedOutput(boolean isR8) {
+  private String getExpectedOutput(Compiler compiler) {
     if (mode == Mode.NO_INVOKE) {
       return StringUtils.joinLines("Hello!", "Goodbye!", "");
     }
-    if (isR8 && mode == Mode.INVOKE_VERIFIABLE_METHOD_ON_UNVERIFIABLE_CLASS) {
-      return StringUtils.joinLines("Hello!", "In verifiable method!", "Goodbye!", "");
+    if (mode == Mode.INVOKE_VERIFIABLE_METHOD_ON_UNVERIFIABLE_CLASS) {
+      if (useInterface) {
+        return StringUtils.joinLines("Hello!", "In verifiable method!", "Goodbye!", "");
+      } else {
+        if (compiler == Compiler.R8 || compiler == Compiler.PROGUARD) {
+          // The unverifiable method has been removed as a result of tree shaking, so the code does
+          // not fail with a verification error when trying to load class `UnverifiableClass`.
+          return StringUtils.joinLines("Hello!", "In verifiable method!", "Goodbye!", "");
+        } else {
+          // The code fails with a verification error because the verifiableMethod() is being called
+          // on `UnverifiableClass`, which does not verify due to unverifiableMethod().
+          return StringUtils.joinLines("Hello!", "");
+        }
+      }
     }
-    return StringUtils.joinLines("Hello!", "");
+    assert mode == Mode.INVOKE_UNVERIFIABLE_METHOD;
+    if (useInterface) {
+      if (compiler == Compiler.R8) {
+        return StringUtils.joinLines(
+            "Hello!",
+            "Unexpected outcome of getstatic",
+            "Unexpected outcome of checkcast",
+            "Goodbye!",
+            "");
+      } else if (compiler == Compiler.PROGUARD) {
+        return StringUtils.joinLines("Hello!", "Unexpected outcome of checkcast", "Goodbye!", "");
+      } else if (compiler == Compiler.DX || compiler == Compiler.D8) {
+        return StringUtils.joinLines("Hello!", "Unexpected outcome of checkcast", "Goodbye!", "");
+      } else {
+        return StringUtils.joinLines("Hello!", "Goodbye!", "");
+      }
+    } else {
+      return StringUtils.joinLines("Hello!", "");
+    }
   }
 
   private Matcher<String> getMatcherForExpectedError() {