Relax limitations of extra class merger

Change-Id: I5803acbe54601988f8b3c6e0a735b5a2d879b6e7
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 43a0826..c7c65f7 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -721,6 +721,10 @@
         SyntheticFinalization.finalizeWithClassHierarchy(appView);
       }
 
+      // Clear the reference type lattice element cache. This is required since class merging may
+      // need to build IR.
+      appView.dexItemFactory().clearTypeElementsCache();
+
       // Run horizontal class merging. This runs even if shrinking is disabled to ensure synthetics
       // are always merged.
       HorizontalClassMerger.createForFinalClassMerging(appView)
@@ -918,9 +922,6 @@
                     DexMethod originalMethod =
                         appView.graphLens().getOriginalMethodSignature(method.getReference());
                     if (originalMethod != method.getReference()) {
-                      DexMethod originalMethod2 =
-                          appView.graphLens().getOriginalMethodSignature(method.getReference());
-                      appView.graphLens().getOriginalMethodSignature(method.getReference());
                       DexEncodedMethod definition = method.getDefinition();
                       Code code = definition.getCode();
                       if (code == null) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index 28e3dd8..6969f94 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
-import com.android.tools.r8.horizontalclassmerging.policies.AtMostOneClassInitializer;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckSyntheticClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.LimitClassGroups;
@@ -27,14 +26,14 @@
 import com.android.tools.r8.horizontalclassmerging.policies.NoIndirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInnerClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceFieldAnnotations;
-import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceInitializers;
+import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceInitializerMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
 import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
-import com.android.tools.r8.horizontalclassmerging.policies.NoNonPrivateVirtualMethods;
 import com.android.tools.r8.horizontalclassmerging.policies.NoServiceLoaders;
 import com.android.tools.r8.horizontalclassmerging.policies.NoVerticallyMergedClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoVirtualMethodMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.OnlyDirectlyConnectedOrUnrelatedInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.PreserveMethodCharacteristics;
@@ -82,12 +81,6 @@
           new NoDeadEnumLiteMaps(appViewWithLiveness, mode),
           new NoIllegalInlining(appViewWithLiveness, mode),
           new NoVerticallyMergedClasses(appViewWithLiveness, mode));
-    } else {
-      assert mode.isFinal();
-      // TODO(b/181846319): Allow constructors, as long as the constructor protos remain unchanged
-      //  (in particular, we can't add nulls at constructor call sites).
-      // TODO(b/181846319): Allow virtual methods, as long as they do not require any merging.
-      builder.add(new NoInstanceInitializers(mode), new NoNonPrivateVirtualMethods(mode));
     }
 
     if (appView.options().horizontalClassMergerOptions().isRestrictedToSynthetics()) {
@@ -170,7 +163,10 @@
     } else {
       assert mode.isFinal();
       // TODO(b/185472598): Add support for merging class initializers with dex code.
-      builder.add(new AtMostOneClassInitializer(mode), new NoConstructorCollisions(appView, mode));
+      builder.add(
+          new NoInstanceInitializerMerging(mode),
+          new NoVirtualMethodMerging(appView, mode),
+          new NoConstructorCollisions(appView, mode));
     }
 
     addMultiClassPoliciesForInterfaceMerging(appView, mode, builder);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java
deleted file mode 100644
index a2b5887..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java
+++ /dev/null
@@ -1,27 +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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-
-public class AtMostOneClassInitializer extends AtMostOneClassThatMatchesPolicy {
-
-  public AtMostOneClassInitializer(Mode mode) {
-    // TODO(b/182124475): Allow merging groups with multiple <clinit> methods in the final round of
-    //  merging.
-    assert mode.isFinal();
-  }
-
-  @Override
-  boolean atMostOneOf(DexProgramClass clazz) {
-    return clazz.hasClassInitializer();
-  }
-
-  @Override
-  public String getName() {
-    return "AtMostOneClassInitializer";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java
deleted file mode 100644
index 0acfe13..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java
+++ /dev/null
@@ -1,29 +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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NoInstanceInitializers extends SingleClassPolicy {
-
-  public NoInstanceInitializers(Mode mode) {
-    // TODO(b/181846319): Allow constructors, as long as the constructor protos remain unchanged
-    //  (in particular, we can't add nulls at constructor call sites).
-    assert mode.isFinal();
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass clazz) {
-    return !clazz.getMethodCollection().hasDirectMethods(DexEncodedMethod::isInstanceInitializer);
-  }
-
-  @Override
-  public String getName() {
-    return "NoInstanceInitializers";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java
deleted file mode 100644
index 81358f1..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java
+++ /dev/null
@@ -1,27 +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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NoNonPrivateVirtualMethods extends SingleClassPolicy {
-
-  public NoNonPrivateVirtualMethods(Mode mode) {
-    // TODO(b/181846319): Allow virtual methods as long as they do not require any merging.
-    assert mode.isFinal();
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass clazz) {
-    return clazz.isInterface() || !clazz.getMethodCollection().hasVirtualMethods();
-  }
-
-  @Override
-  public String getName() {
-    return "NoNonPrivateVirtualMethods";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 7bd9cf9..6917a40 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -325,6 +325,10 @@
       return false;
     }
     ProgramMethod context = code.context();
+    if (!toBeReplaced.instructionMayHaveSideEffects(appView, context)) {
+      removeOrReplaceByDebugLocalRead();
+      return true;
+    }
     if (toBeReplaced.instructionMayHaveSideEffects(
         appView, context, Instruction.SideEffectAssumption.CLASS_ALREADY_INITIALIZED)) {
       return false;
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 1a721c9..fea728d 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
@@ -182,7 +182,6 @@
    * (i.e., whether we are running R8). See {@link AppView#enableWholeProgramOptimizations()}.
    */
   public IRConverter(AppView<?> appView, Timing timing, CfgPrinter printer) {
-    assert appView.appInfo().hasLiveness() || appView.graphLens().isIdentityLens();
     assert appView.options() != null;
     assert appView.options().programConsumer != null;
     assert timing != null;
diff --git a/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java b/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java
index 568aa74..8074250 100644
--- a/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
-import java.nio.file.Path;
 
 public class R8CompatTestBuilder extends R8TestBuilder<R8CompatTestBuilder> {
 
@@ -21,6 +20,11 @@
   }
 
   @Override
+  public boolean isR8CompatTestBuilder() {
+    return true;
+  }
+
+  @Override
   R8CompatTestBuilder self() {
     return this;
   }
diff --git a/src/test/java/com/android/tools/r8/R8FullTestBuilder.java b/src/test/java/com/android/tools/r8/R8FullTestBuilder.java
index 3343329..a8f6d60 100644
--- a/src/test/java/com/android/tools/r8/R8FullTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8FullTestBuilder.java
@@ -24,6 +24,11 @@
   }
 
   @Override
+  public boolean isR8TestBuilder() {
+    return true;
+  }
+
+  @Override
   R8FullTestBuilder self() {
     return this;
   }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 488480c..608f635 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -108,7 +108,7 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 18, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 16, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
@@ -147,7 +147,7 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 18, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 16, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index aad62cb..c421c83 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -44,6 +44,14 @@
     return false;
   }
 
+  public boolean isR8TestBuilder() {
+    return false;
+  }
+
+  public boolean isR8CompatTestBuilder() {
+    return false;
+  }
+
   @Override
   public boolean isTestShrinkerBuilder() {
     return true;
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
index 76ebd58..07ad8f0 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
@@ -9,24 +9,35 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.code.InvokeVirtual;
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
-import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class B77836766 extends TestBase {
 
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public B77836766(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
   /**
    * The below Jasmin code mimics the following Kotlin code:
    *
@@ -93,15 +104,18 @@
     ClassBuilder itf2 = jasminBuilder.addInterface("Itf2");
     itf2.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
 
-    ClassBuilder cls2 = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
+    ClassBuilder cls2Class = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
     // Mimic Kotlin's "internal" class
-    cls2.setAccess("");
-    cls2.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+    cls2Class.setAccess("");
+    cls2Class.addBridgeMethod(
+        "foo",
+        ImmutableList.of("Ljava/lang/Integer;"),
+        "V",
         ".limit stack 2",
         ".limit locals 2",
         "aload_0",
         "aload_1",
-        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
+        "invokevirtual " + cls2Class.name + "/foo(Ljava/lang/Object;)V",
         "return");
 
     ClassBuilder mainClass = jasminBuilder.addClass("Main");
@@ -115,54 +129,66 @@
         "aload_0",
         "ldc \"Hello\"",
         "invokevirtual " + cls1.name + "/foo(Ljava/lang/String;)V",
-        "new " + cls2.name,
+        "new " + cls2Class.name,
         "dup",
-        "invokespecial " + cls2.name + "/<init>()V",
+        "invokespecial " + cls2Class.name + "/<init>()V",
         "astore_0",
         "aload_0",
         "iconst_0",
         "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
-        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Integer;)V",
-        "return"
-    );
+        "invokevirtual " + cls2Class.name + "/foo(Ljava/lang/Integer;)V",
+        "return");
 
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noHorizontalClassMerging(cls2Class.name)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject absSubject = inspector.clazz(absCls.name);
+              assertThat(absSubject, isPresent());
+              ClassSubject cls1Subject = inspector.clazz(cls1.name);
+              assertThat(cls1Subject, isPresent());
+              ClassSubject cls2Subject = inspector.clazz(cls2Class.name);
+              assertThat(cls2Subject, isPresent());
 
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+              // Cls1#foo and Cls2#foo should not refer to each other.
+              // They can invoke their own bridge method or AbsCls#foo (via member rebinding).
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject absSubject = inspector.clazz(absCls.name);
-    assertThat(absSubject, isPresent());
-    ClassSubject cls1Subject = inspector.clazz(cls1.name);
-    assertThat(cls1Subject, isPresent());
-    ClassSubject cls2Subject = inspector.clazz(cls2.name);
-    assertThat(cls2Subject, isPresent());
+              // Cls2#foo has been moved to AbsCls#foo as a result of bridge hoisting.
+              MethodSubject fooInCls2 = cls2Subject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooInCls2, not(isPresent()));
 
-    // Cls1#foo and Cls2#foo should not refer to each other.
-    // They can invoke their own bridge method or AbsCls#foo (via member rebinding).
+              MethodSubject fooFromCls2InAbsCls =
+                  absSubject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooFromCls2InAbsCls, isPresent());
 
-    // Cls2#foo has been moved to AbsCls#foo as a result of bridge hoisting.
-    MethodSubject fooInCls2 = cls2Subject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooInCls2, not(isPresent()));
+              // Cls1#foo has been moved to AbsCls#foo as a result of bridge hoisting.
+              MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.String");
+              assertThat(fooInCls1, not(isPresent()));
 
-    MethodSubject fooFromCls2InAbsCls = absSubject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooFromCls2InAbsCls, isPresent());
-    DexCode code = fooFromCls2InAbsCls.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              MethodSubject fooFromCls1InAbsCls =
+                  absSubject.method("void", "foo", "java.lang.String");
+              assertThat(fooFromCls1InAbsCls, isPresent());
 
-    // Cls1#foo has been moved to AbsCls#foo as a result of bridge hoisting.
-    MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.String");
-    assertThat(fooInCls1, not(isPresent()));
+              if (parameters.isDexRuntime()) {
+                DexCode code = fooFromCls2InAbsCls.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
 
-    MethodSubject fooFromCls1InAbsCls = absSubject.method("void", "foo", "java.lang.String");
-    assertThat(fooFromCls1InAbsCls, isPresent());
-    code = fooFromCls1InAbsCls.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
+                code = fooFromCls1InAbsCls.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("Hello0");
   }
 
   /**
@@ -199,13 +225,17 @@
     ClassBuilder itf = jasminBuilder.addInterface("ItfInteger");
     itf.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
 
-    ClassBuilder cls1 = jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
-    cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+    ClassBuilder derivedIntegerClass =
+        jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
+    derivedIntegerClass.addBridgeMethod(
+        "foo",
+        ImmutableList.of("Ljava/lang/Integer;"),
+        "V",
         ".limit stack 2",
         ".limit locals 2",
         "aload_0",
         "aload_1",
-        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
+        "invokevirtual " + derivedIntegerClass.name + "/foo(Ljava/lang/Object;)V",
         "return");
 
     ClassBuilder cls2 = jasminBuilder.addClass("DerivedString", baseCls.name);
@@ -221,14 +251,14 @@
     mainClass.addMainMethod(
         ".limit stack 5",
         ".limit locals 2",
-        "new " + cls1.name,
+        "new " + derivedIntegerClass.name,
         "dup",
-        "invokespecial " + cls1.name + "/<init>()V",
+        "invokespecial " + derivedIntegerClass.name + "/<init>()V",
         "astore_0",
         "aload_0",
         "iconst_0",
         "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
-        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Integer;)V",
+        "invokevirtual " + derivedIntegerClass.name + "/foo(Ljava/lang/Integer;)V",
         "new " + cls2.name,
         "dup",
         "invokespecial " + cls2.name + "/<init>()V",
@@ -236,41 +266,51 @@
         "aload_0",
         "ldc \"Bar\"",
         "invokevirtual " + cls2.name + "/bar(Ljava/lang/String;)V",
-        "return"
-    );
+        "return");
 
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noHorizontalClassMerging(derivedIntegerClass.name)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject baseSubject = inspector.clazz(baseCls.name);
+              assertThat(baseSubject, isPresent());
+              ClassSubject cls1Subject = inspector.clazz(derivedIntegerClass.name);
+              assertThat(cls1Subject, isPresent());
+              ClassSubject cls2Subject = inspector.clazz(cls2.name);
+              assertThat(cls2Subject, isPresent());
 
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+              // Cls1#foo and Cls2#bar should refer to Base#foo.
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject baseSubject = inspector.clazz(baseCls.name);
-    assertThat(baseSubject, isPresent());
-    ClassSubject cls1Subject = inspector.clazz(cls1.name);
-    assertThat(cls1Subject, isPresent());
-    ClassSubject cls2Subject = inspector.clazz(cls2.name);
-    assertThat(cls2Subject, isPresent());
+              MethodSubject barInCls2 = cls2Subject.method("void", "bar", "java.lang.String");
+              assertThat(barInCls2, isPresent());
 
-    // Cls1#foo and Cls2#bar should refer to Base#foo.
+              // Cls1#foo has been moved to Base#foo as a result of bridge hoisting.
+              MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooInCls1, not(isPresent()));
 
-    MethodSubject barInCls2 = cls2Subject.method("void", "bar", "java.lang.String");
-    assertThat(barInCls2, isPresent());
-    DexCode code = barInCls2.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              MethodSubject fooInBase = baseSubject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooInBase, isPresent());
 
-    // Cls1#foo has been moved to Base#foo as a result of bridge hoisting.
-    MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooInCls1, not(isPresent()));
+              if (parameters.isDexRuntime()) {
+                DexCode code = barInCls2.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
 
-    MethodSubject fooInBase = baseSubject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooInBase, isPresent());
-    code = fooInBase.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+                code = fooInBase.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("0Bar");
   }
 
   /**
@@ -337,25 +377,34 @@
         "return"
     );
 
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject baseSubject = inspector.clazz(baseCls.name);
+              assertThat(baseSubject, isPresent());
+              ClassSubject subSubject = inspector.clazz(subCls.name);
+              assertThat(subSubject, isPresent());
 
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+              // DerivedString2#bar should refer to Base#foo.
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject baseSubject = inspector.clazz(baseCls.name);
-    assertThat(baseSubject, isPresent());
-    ClassSubject subSubject = inspector.clazz(subCls.name);
-    assertThat(subSubject, isPresent());
+              MethodSubject barInSub = subSubject.method("void", "bar", "java.lang.String");
+              assertThat(barInSub, isPresent());
 
-    // DerivedString2#bar should refer to Base#foo.
-
-    MethodSubject barInSub = subSubject.method("void", "bar", "java.lang.String");
-    assertThat(barInSub, isPresent());
-    DexCode code = barInSub.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              if (parameters.isDexRuntime()) {
+                DexCode code = barInSub.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("0Bar");
   }
 
   /*
@@ -413,40 +462,33 @@
         "invokevirtual " + cls.name + "/bar(Ljava/lang/String;)V",
         "return"
     );
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject baseSubject = inspector.clazz(cls.name);
-    assertThat(baseSubject, isPresent());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject baseSubject = inspector.clazz(cls.name);
+              assertThat(baseSubject, isPresent());
 
-    // Base#bar should remain as-is, i.e., refer to Base#foo(Object).
+              // Base#bar should remain as-is, i.e., refer to Base#foo(Object).
 
-    MethodSubject barInSub = baseSubject.method("void", "bar", "java.lang.String");
-    assertThat(barInSub, isPresent());
-    DexCode code = barInSub.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
-  }
+              MethodSubject barInSub = baseSubject.method("void", "bar", "java.lang.String");
+              assertThat(barInSub, isPresent());
 
-  private AndroidApp runAndVerifyOnJvmAndArt(
-      JasminBuilder jasminBuilder, String mainClassName, String proguardConfig) throws Exception {
-    // Run input program on java.
-    Path outputDirectory = temp.newFolder().toPath();
-    jasminBuilder.writeClassFiles(outputDirectory);
-    ProcessResult javaResult = ToolHelper.runJava(outputDirectory, mainClassName);
-    assertEquals(0, javaResult.exitCode);
-
-    AndroidApp processedApp = compileWithR8(jasminBuilder.build(), proguardConfig, this::configure);
-
-    // Run processed (output) program on ART
-    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
-    assertEquals(javaResult.stdout, artResult.stdout);
-    assertEquals(-1, artResult.stderr.indexOf("VerifyError"));
-
-    return processedApp;
+              if (parameters.isDexRuntime()) {
+                DexCode code = barInSub.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("0Bar");
   }
 
   private void configure(InternalOptions options) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java
new file mode 100644
index 0000000..e6752f8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2021, 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.classmerging;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StatelessSingletonClassesMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StatelessSingletonClassesMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .noClassStaticizing()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B");
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      A.INSTANCE.f();
+      B.INSTANCE.g();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    static final A INSTANCE = new A();
+
+    @NeverInline
+    void f() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    static final B INSTANCE = new B();
+
+    @NeverInline
+    void g() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
index 07cea40..1cd81d7 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsNot.not;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -30,10 +30,10 @@
         .assertSuccessWithOutputLines("c", "foo: foo")
         .inspect(
             codeInspector -> {
-              assertThat(codeInspector.clazz(A.class), not(isPresent()));
-              assertThat(codeInspector.clazz(B.class), isPresent());
+              assertThat(codeInspector.clazz(A.class), isAbsent());
+              assertThat(codeInspector.clazz(B.class), isAbsent());
               assertThat(codeInspector.clazz(C.class), isPresent());
-              assertThat(codeInspector.clazz(D.class), not(isPresent()));
+              assertThat(codeInspector.clazz(D.class), isAbsent());
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
index d6b6643..7bc50fa 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
@@ -5,13 +5,13 @@
 package com.android.tools.r8.classmerging.horizontal;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 
 public class InstantiatedAndUninstantiatedClassMergingTest extends HorizontalClassMergingTestBase {
@@ -36,14 +36,14 @@
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
-        .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
             inspector -> {
               assertThat(inspector.clazz(Instantiated.class), isPresent());
-              assertThat(inspector.clazz(Uninstantiated.class), isPresent());
+              assertThat(
+                  inspector.clazz(Uninstantiated.class),
+                  notIf(isPresent(), testBuilder.isR8CompatTestBuilder()));
             })
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Instantiated", "Uninstantiated");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
index 955ff04..e2eef5f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -26,13 +27,18 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .assertIsCompleteMergeGroup(I.class, J.class)
+                    .applyIf(
+                        !parameters.canUseDefaultAndStaticInterfaceMethods(),
+                        i -> i.assertIsCompleteMergeGroup(B1.class, B2.class))
+                    .assertNoOtherClassesMerged())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
-        .addHorizontallyMergedClassesInspector(
-            inspector ->
-                inspector.assertIsCompleteMergeGroup(I.class, J.class).assertNoOtherClassesMerged())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("J", "B2")
         .inspect(
@@ -41,7 +47,9 @@
               assertThat(codeInspector.clazz(J.class), isAbsent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B1.class), isPresent());
-              assertThat(codeInspector.clazz(B2.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(B2.class),
+                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
               assertThat(codeInspector.clazz(C1.class), isPresent());
               assertThat(codeInspector.clazz(C2.class), isPresent());
             });
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
index 3cbaed0..f07d6b6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -51,6 +52,7 @@
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
             .enableNeverClassInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .enableNoVerticalClassMergingAnnotations()
             .addOptionsModification(
                 options -> {
@@ -112,5 +114,6 @@
   @NeverClassInline
   static class C extends A {}
 
+  @NoHorizontalClassMerging
   static class Uninstantiated {}
 }
diff --git a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
index 85c8459..3897351 100644
--- a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
+++ b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
@@ -118,10 +118,6 @@
           for (DataEntryResource dataResource : getOriginalDataResources()) {
             ImmutableList<String> object =
                 dataResourceConsumer.get(getExpectedRenamingFor(dataResource.getName(), mapper));
-            if (object == null) {
-              object =
-                  dataResourceConsumer.get(getExpectedRenamingFor(dataResource.getName(), mapper));
-            }
             assertNotNull("Resource not renamed as expected: " + dataResource.getName(), object);
           }
         });
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
index b39fd97..8179e48 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
@@ -68,6 +69,14 @@
     return horizontallyMergedClasses.getTargets();
   }
 
+  public HorizontallyMergedClassesInspector applyIf(
+      boolean condition, ThrowableConsumer<HorizontallyMergedClassesInspector> consumer) {
+    if (condition) {
+      consumer.acceptWithRuntimeException(this);
+    }
+    return this;
+  }
+
   public HorizontallyMergedClassesInspector assertMergedInto(Class<?> from, Class<?> target) {
     assertEquals(
         horizontallyMergedClasses.getMergeTargetOrDefault(toDexType(from)), toDexType(target));