Leverage constructor equivalence in final round of class merging

This extends the InstanceInitializerAnalysis to the final round of class merging.

To ensure that the final round of class merging results in a simple renaming, the NoInstanceInitializerMerging policy is updated to check for instance initializer equivalence. Since this equivalence relies on the mapping from the instance fields of source classes to the instance fields on the target class, this must now be built at the point of policy execution.

Change-Id: I8267702df7929db6c0b6dcfa74dd77cca7a33759
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 182c91c..41dd2c1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -314,6 +314,15 @@
     fields(predicate).forEach(consumer);
   }
 
+  public void forEachInstanceField(Consumer<DexEncodedField> consumer) {
+    forEachInstanceFieldMatching(alwaysTrue(), consumer);
+  }
+
+  public void forEachInstanceFieldMatching(
+      Predicate<DexEncodedField> predicate, Consumer<DexEncodedField> consumer) {
+    instanceFields(predicate).forEach(consumer);
+  }
+
   public TraversalContinuation traverseFields(Function<DexEncodedField, TraversalContinuation> fn) {
     for (DexEncodedField field : fields()) {
       if (fn.apply(field).shouldBreak()) {
@@ -393,6 +402,10 @@
     return Arrays.asList(instanceFields);
   }
 
+  public Iterable<DexEncodedField> instanceFields(Predicate<? super DexEncodedField> predicate) {
+    return Iterables.filter(Arrays.asList(instanceFields), predicate::test);
+  }
+
   public void appendInstanceField(DexEncodedField field) {
     DexEncodedField[] newFields = new DexEncodedField[instanceFields.length + 1];
     System.arraycopy(instanceFields, 0, newFields, 0, instanceFields.length);
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index be89c66..378640d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -259,7 +259,7 @@
     }
     for (TryHandler handler : handlers) {
       for (TypeAddrPair pair : handler.pairs) {
-        registry.registerTypeReference(pair.type);
+        registry.registerExceptionGuard(pair.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 d3e536e..dedc362 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2245,6 +2245,10 @@
     return createMethod(holder, createProto(voidType, parameters), constructorMethodName);
   }
 
+  public DexMethod createInstanceInitializer(DexType holder, DexTypeList parameters) {
+    return createInstanceInitializer(holder, parameters.values);
+  }
+
   public DexMethod createInstanceInitializerWithFreshProto(
       DexMethod method, List<DexType> extraTypes, Predicate<DexMethod> isFresh) {
     assert method.isInstanceInitializer(this);
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 6be5551..0ebd1da 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -199,6 +199,10 @@
     forEachField(field -> consumer.accept(new ProgramField(this, field)));
   }
 
+  public void forEachProgramInstanceField(Consumer<? super ProgramField> consumer) {
+    forEachInstanceField(field -> consumer.accept(new ProgramField(this, field)));
+  }
+
   public void forEachProgramMember(Consumer<? super ProgramMember<?, ?>> consumer) {
     forEachProgramField(consumer);
     forEachProgramMethod(consumer);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
index e0eaf2c..21f8a5c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -8,12 +8,9 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerGraphLens.Builder;
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
 import com.android.tools.r8.utils.IterableUtils;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
-import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -22,6 +19,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class ClassInstanceFieldsMerger {
 
@@ -31,10 +29,6 @@
 
   private DexEncodedField classIdField;
 
-  // Map from target class field to all fields which should be merged into that field.
-  private final MutableBidirectionalManyToOneMap<DexEncodedField, DexEncodedField> fieldMappings =
-      BidirectionalManyToOneHashMap.newLinkedHashMap();
-
   public ClassInstanceFieldsMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
@@ -42,7 +36,6 @@
     this.appView = appView;
     this.group = group;
     this.lensBuilder = lensBuilder;
-    group.forEachSource(this::addFields);
   }
 
   /**
@@ -55,13 +48,17 @@
    * Bar has fields 'A b' and 'B a'), we make a prepass that matches fields with the same reference
    * type.
    */
-  private void addFields(DexProgramClass clazz) {
+  public static void mapFields(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      DexProgramClass source,
+      DexProgramClass target,
+      BiConsumer<DexEncodedField, DexEncodedField> consumer) {
     Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByExactInfo =
-        getAvailableFieldsByExactInfo();
+        getAvailableFieldsByExactInfo(target);
     List<DexEncodedField> needsMerge = new ArrayList<>();
 
     // Pass 1: Match fields that have the exact same type.
-    for (DexEncodedField oldField : clazz.instanceFields()) {
+    for (DexEncodedField oldField : source.instanceFields()) {
       InstanceFieldInfo info = InstanceFieldInfo.createExact(oldField);
       LinkedList<DexEncodedField> availableFieldsWithExactSameInfo =
           availableFieldsByExactInfo.get(info);
@@ -69,7 +66,7 @@
         needsMerge.add(oldField);
       } else {
         DexEncodedField newField = availableFieldsWithExactSameInfo.removeFirst();
-        fieldMappings.put(oldField, newField);
+        consumer.accept(oldField, newField);
         if (availableFieldsWithExactSameInfo.isEmpty()) {
           availableFieldsByExactInfo.remove(info);
         }
@@ -78,7 +75,7 @@
 
     // Pass 2: Match fields that do not have the same reference type.
     Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByRelaxedInfo =
-        getAvailableFieldsByRelaxedInfo(availableFieldsByExactInfo);
+        getAvailableFieldsByRelaxedInfo(appView, availableFieldsByExactInfo);
     for (DexEncodedField oldField : needsMerge) {
       assert oldField.getType().isReferenceType();
       DexEncodedField newField =
@@ -87,14 +84,15 @@
               .removeFirst();
       assert newField != null;
       assert newField.getType().isReferenceType();
-      fieldMappings.put(oldField, newField);
+      consumer.accept(oldField, newField);
     }
   }
 
-  private Map<InstanceFieldInfo, LinkedList<DexEncodedField>> getAvailableFieldsByExactInfo() {
+  private static Map<InstanceFieldInfo, LinkedList<DexEncodedField>> getAvailableFieldsByExactInfo(
+      DexProgramClass target) {
     Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByInfo =
         new LinkedHashMap<>();
-    for (DexEncodedField field : group.getTarget().instanceFields()) {
+    for (DexEncodedField field : target.instanceFields()) {
       availableFieldsByInfo
           .computeIfAbsent(InstanceFieldInfo.createExact(field), ignore -> new LinkedList<>())
           .add(field);
@@ -102,8 +100,10 @@
     return availableFieldsByInfo;
   }
 
-  private Map<InstanceFieldInfo, LinkedList<DexEncodedField>> getAvailableFieldsByRelaxedInfo(
-      Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByExactInfo) {
+  private static Map<InstanceFieldInfo, LinkedList<DexEncodedField>>
+      getAvailableFieldsByRelaxedInfo(
+          AppView<? extends AppInfoWithClassHierarchy> appView,
+          Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByExactInfo) {
     Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByRelaxedInfo =
         new LinkedHashMap<>();
     availableFieldsByExactInfo.forEach(
@@ -125,26 +125,21 @@
     }
   }
 
-  public ProgramField getTargetField(ProgramField field) {
-    if (field.getHolder() == group.getTarget()) {
-      return field;
-    }
-    DexEncodedField targetField = fieldMappings.get(field.getDefinition());
-    return new ProgramField(group.getTarget(), targetField);
-  }
-
   public void setClassIdField(DexEncodedField classIdField) {
     this.classIdField = classIdField;
   }
 
   public DexEncodedField[] merge() {
+    assert group.hasInstanceFieldMap();
     List<DexEncodedField> newFields = new ArrayList<>();
     if (classIdField != null) {
       newFields.add(classIdField);
     }
-    fieldMappings.forEachManyToOneMapping(
-        (sourceFields, targetField) ->
-            newFields.add(mergeSourceFieldsToTargetField(targetField, sourceFields)));
+    group
+        .getInstanceFieldMap()
+        .forEachManyToOneMapping(
+            (sourceFields, targetField) ->
+                newFields.add(mergeSourceFieldsToTargetField(targetField, sourceFields)));
     return newFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index 3927d11..d1e98c7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -27,12 +27,10 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.code.ClassInitializerMerger;
-import com.android.tools.r8.horizontalclassmerging.code.SyntheticClassInitializerConverter;
+import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter;
 import com.android.tools.r8.ir.analysis.value.NumberFromIntervalValue;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
-import com.android.tools.r8.shaking.KeepClassInfo;
-import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -40,7 +38,6 @@
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -78,6 +75,7 @@
 
   private ClassMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCodeProvider codeProvider,
       Mode mode,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       MergeGroup group,
@@ -96,7 +94,7 @@
     this.classInitializerMerger = ClassInitializerMerger.create(group);
     this.instanceInitializerMergers =
         InstanceInitializerMergerCollection.create(
-            appView, classIdentifiers, group, classInstanceFieldsMerger, lensBuilder, mode);
+            appView, classIdentifiers, codeProvider, group, lensBuilder, mode);
     this.virtualMethodMergers = virtualMethodMergers;
 
     buildClassIdentifierMap();
@@ -109,14 +107,14 @@
 
   void mergeDirectMethods(
       SyntheticArgumentClass syntheticArgumentClass,
-      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
-    mergeInstanceInitializers(syntheticArgumentClass);
-    mergeStaticClassInitializers(syntheticClassInitializerConverterBuilder);
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
+    mergeInstanceInitializers(syntheticArgumentClass, syntheticInitializerConverterBuilder);
+    mergeStaticClassInitializers(syntheticInitializerConverterBuilder);
     group.forEach(this::mergeDirectMethods);
   }
 
   void mergeStaticClassInitializers(
-      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
     if (classInitializerMerger.isEmpty()) {
       return;
     }
@@ -145,7 +143,7 @@
     if (!definition.getCode().isCfCode()) {
       assert appView.options().isGeneratingDex();
       assert mode.isFinal();
-      syntheticClassInitializerConverterBuilder.add(group);
+      syntheticInitializerConverterBuilder.add(new ProgramMethod(group.getTarget(), definition));
     }
   }
 
@@ -187,16 +185,20 @@
         classMethodsBuilder::isFresh);
   }
 
-  void mergeInstanceInitializers(SyntheticArgumentClass syntheticArgumentClass) {
+  void mergeInstanceInitializers(
+      SyntheticArgumentClass syntheticArgumentClass,
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
     instanceInitializerMergers.forEach(
-        merger -> merger.merge(classMethodsBuilder, syntheticArgumentClass));
+        merger ->
+            merger.merge(
+                classMethodsBuilder, syntheticArgumentClass, syntheticInitializerConverterBuilder));
   }
 
   void mergeMethods(
       SyntheticArgumentClass syntheticArgumentClass,
-      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
     mergeVirtualMethods();
-    mergeDirectMethods(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
+    mergeDirectMethods(syntheticArgumentClass, syntheticInitializerConverterBuilder);
     classMethodsBuilder.setClassMethods(group.getTarget());
   }
 
@@ -316,51 +318,30 @@
 
   public void mergeGroup(
       SyntheticArgumentClass syntheticArgumentClass,
-      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
     fixAccessFlags();
     fixNestMemberAttributes();
     mergeAnnotations();
     mergeInterfaces();
     mergeFields();
-    mergeMethods(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
+    mergeMethods(syntheticArgumentClass, syntheticInitializerConverterBuilder);
   }
 
   public static class Builder {
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private final IRCodeProvider codeProvider;
     private Mode mode;
     private final MergeGroup group;
 
-    public Builder(AppView<? extends AppInfoWithClassHierarchy> appView, MergeGroup group) {
+    public Builder(
+        AppView<? extends AppInfoWithClassHierarchy> appView,
+        IRCodeProvider codeProvider,
+        MergeGroup group,
+        Mode mode) {
       this.appView = appView;
+      this.codeProvider = codeProvider;
       this.group = group;
-    }
-
-    Builder setMode(Mode mode) {
       this.mode = mode;
-      return this;
-    }
-
-    private void selectTarget() {
-      Iterable<DexProgramClass> candidates = Iterables.filter(group, DexClass::isPublic);
-      if (IterableUtils.isEmpty(candidates)) {
-        candidates = group;
-      }
-      Iterator<DexProgramClass> candidateIterator = candidates.iterator();
-      DexProgramClass target = IterableUtils.first(candidates);
-      while (candidateIterator.hasNext()) {
-        DexProgramClass current = candidateIterator.next();
-        KeepClassInfo keepClassInfo = appView.getKeepInfo().getClassInfo(current);
-        if (keepClassInfo.isMinificationAllowed(appView.options())) {
-          target = current;
-          break;
-        }
-        // Select the target with the shortest name.
-        if (current.getType().getDescriptor().size() < target.getType().getDescriptor().size) {
-          target = current;
-        }
-      }
-      group.setTarget(
-          appView.testing().horizontalClassMergingTarget.apply(appView, candidates, target));
     }
 
     private List<VirtualMethodMerger> createVirtualMethodMergers() {
@@ -393,8 +374,6 @@
 
     public ClassMerger build(
         HorizontalClassMergerGraphLens.Builder lensBuilder) {
-      selectTarget();
-
       List<VirtualMethodMerger> virtualMethodMergers = createVirtualMethodMergers();
 
       boolean requiresClassIdField =
@@ -405,7 +384,7 @@
         createClassIdField();
       }
 
-      return new ClassMerger(appView, mode, lensBuilder, group, virtualMethodMergers);
+      return new ClassMerger(appView, codeProvider, mode, lensBuilder, group, virtualMethodMergers);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 4ad6f55..666bd64 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.horizontalclassmerging.code.SyntheticClassInitializerConverter;
+import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
 import com.android.tools.r8.shaking.KeepInfoCollection;
@@ -65,6 +65,10 @@
     if (options.isEnabled(mode)) {
       timing.begin("HorizontalClassMerger (" + mode.toString() + ")");
       run(runtimeTypeCheckInfo, executorService, timing);
+
+      // Clear type elements cache after IR building.
+      appView.dexItemFactory().clearTypeElementsCache();
+
       timing.end();
     } else {
       appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty(), mode);
@@ -74,8 +78,11 @@
   private void run(
       RuntimeTypeCheckInfo runtimeTypeCheckInfo, ExecutorService executorService, Timing timing)
       throws ExecutionException {
+    IRCodeProvider codeProvider = new IRCodeProvider(appView);
+
     // Run the policies on all program classes to produce a final grouping.
-    List<Policy> policies = PolicyScheduler.getPolicies(appView, mode, runtimeTypeCheckInfo);
+    List<Policy> policies =
+        PolicyScheduler.getPolicies(appView, codeProvider, mode, runtimeTypeCheckInfo);
     Collection<MergeGroup> groups = new PolicyExecutor().run(getInitialGroups(), policies, timing);
 
     // If there are no groups, then end horizontal class merging.
@@ -88,21 +95,20 @@
         new HorizontalClassMergerGraphLens.Builder();
 
     // Merge the classes.
-    List<ClassMerger> classMergers = initializeClassMergers(lensBuilder, groups);
+    List<ClassMerger> classMergers = initializeClassMergers(codeProvider, lensBuilder, groups);
     SyntheticArgumentClass syntheticArgumentClass =
         mode.isInitial()
             ? new SyntheticArgumentClass.Builder(appView.withLiveness()).build(groups)
             : null;
-    SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder =
-        SyntheticClassInitializerConverter.builder(appView);
-    applyClassMergers(
-        classMergers, syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
+    SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder =
+        SyntheticInitializerConverter.builder(appView, codeProvider);
+    applyClassMergers(classMergers, syntheticArgumentClass, syntheticInitializerConverterBuilder);
 
-    SyntheticClassInitializerConverter syntheticClassInitializerConverter =
-        syntheticClassInitializerConverterBuilder.build();
-    if (!syntheticClassInitializerConverter.isEmpty()) {
+    SyntheticInitializerConverter syntheticInitializerConverter =
+        syntheticInitializerConverterBuilder.build();
+    if (!syntheticInitializerConverter.isEmpty()) {
       assert mode.isFinal();
-      syntheticClassInitializerConverterBuilder.build().convert(executorService);
+      syntheticInitializerConverterBuilder.build().convert(executorService);
     }
 
     // Generate the graph lens.
@@ -194,12 +200,16 @@
    * be merged and how the merging should be performed.
    */
   private List<ClassMerger> initializeClassMergers(
+      IRCodeProvider codeProvider,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Collection<MergeGroup> groups) {
     List<ClassMerger> classMergers = new ArrayList<>(groups.size());
     for (MergeGroup group : groups) {
       assert group.isNonTrivial();
-      classMergers.add(new ClassMerger.Builder(appView, group).setMode(mode).build(lensBuilder));
+      assert group.hasInstanceFieldMap();
+      assert group.hasTarget();
+      classMergers.add(
+          new ClassMerger.Builder(appView, codeProvider, group, mode).build(lensBuilder));
     }
     return classMergers;
   }
@@ -208,13 +218,10 @@
   private void applyClassMergers(
       Collection<ClassMerger> classMergers,
       SyntheticArgumentClass syntheticArgumentClass,
-      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
     for (ClassMerger merger : classMergers) {
-      merger.mergeGroup(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
+      merger.mergeGroup(syntheticArgumentClass, syntheticInitializerConverterBuilder);
     }
-
-    // Clear type elements cache after IR building.
-    appView.dexItemFactory().clearTypeElementsCache();
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/IRCodeProvider.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/IRCodeProvider.java
new file mode 100644
index 0000000..6976e5a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/IRCodeProvider.java
@@ -0,0 +1,35 @@
+// 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+
+public class IRCodeProvider {
+
+  private final AppView<AppInfo> appViewForConversion;
+
+  public IRCodeProvider(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    // At this point the code rewritings described by repackaging and synthetic finalization have
+    // not been applied to the code objects. These code rewritings will be applied in the
+    // application writer. We therefore simulate that we are in D8, to allow building IR for each of
+    // the class initializers without applying the unapplied code rewritings, to avoid that we apply
+    // the lens more than once to the same piece of code.
+    AppView<AppInfo> appViewForConversion =
+        AppView.createForD8(AppInfo.createInitialAppInfo(appView.appInfo().app()));
+    appViewForConversion.setGraphLens(appView.graphLens());
+    this.appViewForConversion = appViewForConversion;
+  }
+
+  public IRCode buildIR(ProgramMethod method) {
+    return method
+        .getDefinition()
+        .getCode()
+        .buildIR(method, appViewForConversion, method.getOrigin());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
index 9a6390a..bba2edd 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
@@ -21,20 +21,14 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
-import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.utils.WorkList;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.List;
@@ -43,36 +37,12 @@
 
   public static InstanceInitializerDescription analyze(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCodeProvider codeProvider,
       MergeGroup group,
-      ProgramMethod instanceInitializer,
-      ClassInstanceFieldsMerger instanceFieldsMerger,
-      Mode mode) {
+      ProgramMethod instanceInitializer) {
     InstanceInitializerDescription.Builder builder =
         InstanceInitializerDescription.builder(appView, instanceInitializer);
-
-    if (mode.isFinal()) {
-      // TODO(b/189296638): We can't build IR in the final round of class merging without simulating
-      //  that we're in D8.
-      MethodOptimizationInfo optimizationInfo =
-          instanceInitializer.getDefinition().getOptimizationInfo();
-      if (optimizationInfo.mayHaveSideEffects()) {
-        return null;
-      }
-      InstanceInitializerInfo instanceInitializerInfo =
-          optimizationInfo.getContextInsensitiveInstanceInitializerInfo();
-      if (!instanceInitializerInfo.hasParent()) {
-        // We don't know the parent constructor of the first constructor.
-        return null;
-      }
-      DexMethod parent = instanceInitializerInfo.getParent();
-      if (parent.getArity() > 0) {
-        return null;
-      }
-      builder.addInvokeConstructor(parent, ImmutableList.of());
-      return builder.build();
-    }
-
-    IRCode code = instanceInitializer.buildIR(appView);
+    IRCode code = codeProvider.buildIR(instanceInitializer);
     WorkList<BasicBlock> workList = WorkList.newIdentityWorkList(code.entryBlock());
     while (workList.hasNext()) {
       BasicBlock block = workList.next();
@@ -118,12 +88,12 @@
               }
 
               InstanceFieldInitializationInfo initializationInfo =
-                  getInitializationInfo(instancePut.value(), appView, instanceInitializer);
+                  getInitializationInfo(appView, instancePut.value());
               if (initializationInfo == null) {
                 return invalid();
               }
 
-              ProgramField targetField = instanceFieldsMerger.getTargetField(sourceField);
+              ProgramField targetField = group.getTargetInstanceField(sourceField);
               assert targetField != null;
 
               builder.addInstancePut(targetField.getReference(), initializationInfo);
@@ -157,7 +127,7 @@
                   new ArrayList<>(invoke.arguments().size() - 1);
               for (Value argument : Iterables.skip(invoke.arguments(), 1)) {
                 InstanceFieldInitializationInfo initializationInfo =
-                    getInitializationInfo(argument, appView, instanceInitializer);
+                    getInitializationInfo(appView, argument);
                 if (initializationInfo == null) {
                   return invalid();
                 }
@@ -181,23 +151,28 @@
   }
 
   private static InstanceFieldInitializationInfo getInitializationInfo(
-      Value value,
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      ProgramMethod instanceInitializer) {
-    InstanceFieldInitializationInfoFactory factory =
-        appView.instanceFieldInitializationInfoFactory();
-
+      AppView<? extends AppInfoWithClassHierarchy> appView, Value value) {
     Value root = value.getAliasedValue();
-    if (root.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
-      return factory.createArgumentInitializationInfo(
-          value.getDefinition().asArgument().getIndex());
+    if (root.isPhi()) {
+      return null;
     }
 
-    AbstractValue abstractValue = value.getAbstractValue(appView, instanceInitializer);
-    if (abstractValue.isSingleConstValue()) {
-      return abstractValue.asSingleConstValue();
+    Instruction definition = root.getDefinition();
+    if (definition.isArgument()) {
+      return appView
+          .instanceFieldInitializationInfoFactory()
+          .createArgumentInitializationInfo(root.getDefinition().asArgument().getIndex());
     }
-
+    if (definition.isConstNumber()) {
+      return appView
+          .abstractValueFactory()
+          .createSingleNumberValue(definition.asConstNumber().getRawValue());
+    }
+    if (definition.isConstString()) {
+      return appView
+          .abstractValueFactory()
+          .createSingleStringValue(definition.asConstString().getValue());
+    }
     return null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
index b67c652..eea6e6b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
@@ -16,13 +16,9 @@
 import com.android.tools.r8.cf.code.CfLoad;
 import com.android.tools.r8.cf.code.CfPosition;
 import com.android.tools.r8.cf.code.CfReturnVoid;
-import com.android.tools.r8.code.Instruction;
-import com.android.tools.r8.code.InvokeDirectRange;
-import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -35,7 +31,6 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
 import com.android.tools.r8.utils.IntBox;
-import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -236,34 +231,6 @@
     }
   }
 
-  public DexCode createDexCode(
-      DexMethod newMethodReference,
-      DexMethod originalMethodReference,
-      DexMethod syntheticMethodReference,
-      MergeGroup group,
-      boolean hasClassId,
-      int extraNulls) {
-    assert !hasClassId;
-    assert extraNulls == 0;
-    assert parentConstructorArguments.isEmpty();
-    assert instanceFieldAssignmentsPre.isEmpty();
-    assert instanceFieldAssignmentsPost.isEmpty();
-    Instruction[] instructions = new Instruction[2];
-    instructions[0] = new InvokeDirectRange(0, 1, parentConstructor);
-    instructions[1] = new ReturnVoid();
-    int incomingRegisterSize =
-        1 + IterableUtils.sumInt(newMethodReference.getParameters(), DexType::getRequiredRegisters);
-    int outgoingRegisterSize = 1;
-    return new DexCode(
-        incomingRegisterSize,
-        incomingRegisterSize,
-        outgoingRegisterSize,
-        instructions,
-        new DexCode.Try[0],
-        new DexCode.TryHandler[0],
-        null);
-  }
-
   @Override
   public boolean equals(Object obj) {
     if (obj == null || getClass() != obj.getClass()) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
index 862a23c..e17c574 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.code.ConstructorEntryPointSynthesizedCode;
+import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter;
 import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
@@ -277,16 +278,7 @@
       boolean needsClassId,
       int extraNulls) {
     if (hasInstanceInitializerDescription()) {
-      if (mode.isInitial() || appView.options().isGeneratingClassFiles()) {
-        return instanceInitializerDescription.createCfCode(
-            newMethodReference,
-            getOriginalMethodReference(),
-            syntheticMethodReference,
-            group,
-            needsClassId,
-            extraNulls);
-      }
-      return instanceInitializerDescription.createDexCode(
+      return instanceInitializerDescription.createCfCode(
           newMethodReference,
           getOriginalMethodReference(),
           syntheticMethodReference,
@@ -311,7 +303,8 @@
   /** Synthesize a new method which selects the constructor based on a parameter type. */
   void merge(
       ClassMethodsBuilder classMethodsBuilder,
-      SyntheticArgumentClass syntheticArgumentClass) {
+      SyntheticArgumentClass syntheticArgumentClass,
+      SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) {
     ProgramMethod representative = ListUtils.first(instanceInitializers);
 
     // Create merged instance initializer reference.
@@ -378,5 +371,15 @@
             true,
             getNewClassFileVersion());
     classMethodsBuilder.addDirectMethod(newInstanceInitializer);
+
+    if (mode.isFinal()) {
+      if (appView.options().isGeneratingDex() && !newInstanceInitializer.getCode().isDexCode()) {
+        syntheticInitializerConverterBuilder.add(
+            new ProgramMethod(group.getTarget(), newInstanceInitializer));
+      } else {
+        assert !appView.options().isGeneratingClassFiles()
+            || newInstanceInitializer.getCode().isCfCode();
+      }
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
index b5e485b..dbf0943 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
@@ -39,8 +39,8 @@
   public static InstanceInitializerMergerCollection create(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       Reference2IntMap<DexType> classIdentifiers,
+      IRCodeProvider codeProvider,
       MergeGroup group,
-      ClassInstanceFieldsMerger instanceFieldsMerger,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode) {
     // Create an instance initializer merger for each group of instance initializers that are
@@ -54,7 +54,7 @@
                 instanceInitializer -> {
                   InstanceInitializerDescription description =
                       InstanceInitializerAnalysis.analyze(
-                          appView, group, instanceInitializer, instanceFieldsMerger, mode);
+                          appView, codeProvider, group, instanceInitializer);
                   if (description != null) {
                     buildersByDescription
                         .computeIfAbsent(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index ba5d775..458f0ba 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -6,12 +6,20 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.shaking.KeepClassInfo;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.IteratorUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import com.google.common.collect.Iterables;
 import java.util.Collection;
 import java.util.Iterator;
@@ -29,6 +37,8 @@
   private DexProgramClass target = null;
   private Metadata metadata = null;
 
+  private BidirectionalManyToOneMap<DexEncodedField, DexEncodedField> instanceFieldMap;
+
   public MergeGroup() {
     this.classes = new LinkedList<>();
   }
@@ -104,6 +114,31 @@
     this.classIdField = classIdField;
   }
 
+  public boolean hasInstanceFieldMap() {
+    return instanceFieldMap != null;
+  }
+
+  public BidirectionalManyToOneMap<DexEncodedField, DexEncodedField> getInstanceFieldMap() {
+    assert hasInstanceFieldMap();
+    return instanceFieldMap;
+  }
+
+  public void selectInstanceFieldMap(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    assert hasTarget();
+    MutableBidirectionalManyToOneMap<DexEncodedField, DexEncodedField> instanceFieldMap =
+        BidirectionalManyToOneHashMap.newLinkedHashMap();
+    forEachSource(
+        source ->
+            ClassInstanceFieldsMerger.mapFields(appView, source, target, instanceFieldMap::put));
+    setInstanceFieldMap(instanceFieldMap);
+  }
+
+  public void setInstanceFieldMap(
+      BidirectionalManyToOneMap<DexEncodedField, DexEncodedField> instanceFieldMap) {
+    assert !hasInstanceFieldMap();
+    this.instanceFieldMap = instanceFieldMap;
+  }
+
   public Iterable<DexProgramClass> getSources() {
     assert hasTarget();
     return Iterables.filter(classes, clazz -> clazz != target);
@@ -122,8 +157,40 @@
     return target;
   }
 
-  public void setTarget(DexProgramClass target) {
-    assert classes.contains(target);
+  public ProgramField getTargetInstanceField(ProgramField field) {
+    assert hasTarget();
+    assert hasInstanceFieldMap();
+    if (field.getHolder() == getTarget()) {
+      return field;
+    }
+    DexEncodedField targetField = getInstanceFieldMap().get(field.getDefinition());
+    return new ProgramField(getTarget(), targetField);
+  }
+
+  public void selectTarget(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    Iterable<DexProgramClass> candidates = Iterables.filter(getClasses(), DexClass::isPublic);
+    if (IterableUtils.isEmpty(candidates)) {
+      candidates = getClasses();
+    }
+    Iterator<DexProgramClass> candidateIterator = candidates.iterator();
+    DexProgramClass target = IterableUtils.first(candidates);
+    while (candidateIterator.hasNext()) {
+      DexProgramClass current = candidateIterator.next();
+      KeepClassInfo keepClassInfo = appView.getKeepInfo().getClassInfo(current);
+      if (keepClassInfo.isMinificationAllowed(appView.options())) {
+        target = current;
+        break;
+      }
+      // Select the target with the shortest name.
+      if (current.getType().getDescriptor().size() < target.getType().getDescriptor().size) {
+        target = current;
+      }
+    }
+    setTarget(appView.testing().horizontalClassMergingTarget.apply(appView, candidates, target));
+  }
+
+  private void setTarget(DexProgramClass target) {
+    assert !hasTarget();
     this.target = target;
   }
 
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 6969f94..0d3bf0d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckSyntheticClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.FinalizeMergeGroup;
 import com.android.tools.r8.horizontalclassmerging.policies.LimitClassGroups;
 import com.android.tools.r8.horizontalclassmerging.policies.MinimizeInstanceFieldCasts;
 import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotationClasses;
@@ -56,12 +57,13 @@
 
   public static List<Policy> getPolicies(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCodeProvider codeProvider,
       Mode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     List<Policy> policies =
         ImmutableList.<Policy>builder()
             .addAll(getSingleClassPolicies(appView, mode, runtimeTypeCheckInfo))
-            .addAll(getMultiClassPolicies(appView, mode, runtimeTypeCheckInfo))
+            .addAll(getMultiClassPolicies(appView, codeProvider, mode, runtimeTypeCheckInfo))
             .build();
     assert verifyPolicyOrderingConstraints(policies);
     return policies;
@@ -142,6 +144,7 @@
 
   private static List<Policy> getMultiClassPolicies(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCodeProvider codeProvider,
       Mode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     ImmutableList.Builder<Policy> builder = ImmutableList.builder();
@@ -162,16 +165,24 @@
           new MinimizeInstanceFieldCasts());
     } else {
       assert mode.isFinal();
-      // TODO(b/185472598): Add support for merging class initializers with dex code.
       builder.add(
-          new NoInstanceInitializerMerging(mode),
           new NoVirtualMethodMerging(appView, mode),
           new NoConstructorCollisions(appView, mode));
     }
 
     addMultiClassPoliciesForInterfaceMerging(appView, mode, builder);
 
-    return builder.add(new LimitClassGroups(appView)).build();
+    builder.add(new LimitClassGroups(appView));
+
+    if (mode.isFinal()) {
+      // This needs to reason about equivalence of instance initializers, which relies on the
+      // mapping from instance fields on source classes to the instance fields on target classes.
+      // This policy therefore selects a target for each merge group and creates the mapping for
+      // instance fields. For this reason we run this policy in the very end.
+      builder.add(new NoInstanceInitializerMerging(appView, codeProvider, mode));
+    }
+
+    return builder.add(new FinalizeMergeGroup(appView, mode)).build();
   }
 
   private static void addRequiredMultiClassPolicies(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticClassInitializerConverter.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java
similarity index 66%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticClassInitializerConverter.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java
index a31333c..d2414ed 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticClassInitializerConverter.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticInitializerConverter.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.IRCodeProvider;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
@@ -23,19 +23,24 @@
  * Converts synthetic class initializers that have been created as a result of merging class
  * initializers into a single class initializer to DEX.
  */
-public class SyntheticClassInitializerConverter {
+public class SyntheticInitializerConverter {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
-  private final List<MergeGroup> groups;
+  private final IRCodeProvider codeProvider;
+  private final List<ProgramMethod> methods;
 
-  private SyntheticClassInitializerConverter(
-      AppView<? extends AppInfoWithClassHierarchy> appView, List<MergeGroup> groups) {
+  private SyntheticInitializerConverter(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCodeProvider codeProvider,
+      List<ProgramMethod> methods) {
     this.appView = appView;
-    this.groups = groups;
+    this.codeProvider = codeProvider;
+    this.methods = methods;
   }
 
-  public static Builder builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return new Builder(appView);
+  public static Builder builder(
+      AppView<? extends AppInfoWithClassHierarchy> appView, IRCodeProvider codeProvider) {
+    return new Builder(appView, codeProvider);
   }
 
   public void convert(ExecutorService executorService) throws ExecutionException {
@@ -51,14 +56,9 @@
     // Build IR for each of the class initializers and finalize.
     IRConverter converter = new IRConverter(appViewForConversion, Timing.empty());
     ThreadUtils.processItems(
-        groups,
-        group -> {
-          ProgramMethod classInitializer = group.getTarget().getProgramClassInitializer();
-          IRCode code =
-              classInitializer
-                  .getDefinition()
-                  .getCode()
-                  .buildIR(classInitializer, appViewForConversion, classInitializer.getOrigin());
+        methods,
+        method -> {
+          IRCode code = codeProvider.buildIR(method);
           converter.removeDeadCodeAndFinalizeIR(
               code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
         },
@@ -66,25 +66,28 @@
   }
 
   public boolean isEmpty() {
-    return groups.isEmpty();
+    return methods.isEmpty();
   }
 
   public static class Builder {
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
-    private final List<MergeGroup> groups = new ArrayList<>();
+    private final IRCodeProvider codeProvider;
+    private final List<ProgramMethod> methods = new ArrayList<>();
 
-    private Builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    private Builder(
+        AppView<? extends AppInfoWithClassHierarchy> appView, IRCodeProvider codeProvider) {
       this.appView = appView;
+      this.codeProvider = codeProvider;
     }
 
-    public Builder add(MergeGroup group) {
-      this.groups.add(group);
+    public Builder add(ProgramMethod method) {
+      this.methods.add(method);
       return this;
     }
 
-    public SyntheticClassInitializerConverter build() {
-      return new SyntheticClassInitializerConverter(appView, groups);
+    public SyntheticInitializerConverter build() {
+      return new SyntheticInitializerConverter(appView, codeProvider, methods);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
new file mode 100644
index 0000000..705c8cd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/FinalizeMergeGroup.java
@@ -0,0 +1,75 @@
+// 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.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Identifies when instance initializer merging is required and bails out. This is needed to ensure
+ * that we don't need to append extra null arguments at constructor call sites, such that the result
+ * of the final round of class merging can be described as a renaming only.
+ *
+ * <p>This policy requires that all instance initializers with the same signature (relaxed, by
+ * converting references types to java.lang.Object) have the same behavior.
+ */
+public class FinalizeMergeGroup extends MultiClassPolicy {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Mode mode;
+
+  public FinalizeMergeGroup(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    this.appView = appView;
+    this.mode = mode;
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    if (mode.isInitial() || group.isInterfaceGroup()) {
+      group.selectTarget(appView);
+      group.selectInstanceFieldMap(appView);
+    } else {
+      // In the final round of merging each group should be finalized by the
+      // NoInstanceInitializerMerging policy.
+      assert verifyAlreadyFinalized(group);
+    }
+    return ListUtils.newLinkedList(group);
+  }
+
+  @Override
+  public String getName() {
+    return "FinalizeMergeGroup";
+  }
+
+  @Override
+  public boolean isIdentityForInterfaceGroups() {
+    return true;
+  }
+
+  private boolean verifyAlreadyFinalized(MergeGroup group) {
+    assert group.hasTarget();
+    assert group.getClasses().contains(group.getTarget());
+    assert group.hasInstanceFieldMap();
+    Set<DexType> types =
+        SetUtils.newIdentityHashSet(
+            builder -> group.forEach(clazz -> builder.accept(clazz.getType())));
+    group
+        .getInstanceFieldMap()
+        .forEach(
+            (sourceField, targetField) -> {
+              assert types.contains(sourceField.getHolderType());
+              assert types.contains(targetField.getHolderType());
+            });
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
index 0ff172c..02b6bbd 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
@@ -4,103 +4,261 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.ClassInstanceFieldsMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.IRCodeProvider;
+import com.android.tools.r8.horizontalclassmerging.InstanceInitializerAnalysis;
+import com.android.tools.r8.horizontalclassmerging.InstanceInitializerDescription;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
-import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.google.common.collect.Iterators;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
 
 /**
  * Identifies when instance initializer merging is required and bails out. This is needed to ensure
  * that we don't need to append extra null arguments at constructor call sites, such that the result
  * of the final round of class merging can be described as a renaming only.
+ *
+ * <p>This policy requires that all instance initializers with the same signature (relaxed, by
+ * converting references types to java.lang.Object) have the same behavior.
  */
 public class NoInstanceInitializerMerging extends MultiClassPolicy {
 
-  public NoInstanceInitializerMerging(Mode mode) {
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final IRCodeProvider codeProvider;
+
+  public NoInstanceInitializerMerging(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCodeProvider codeProvider,
+      Mode mode) {
     assert mode.isFinal();
+    this.appView = appView;
+    this.codeProvider = codeProvider;
   }
 
   @Override
   public Collection<MergeGroup> apply(MergeGroup group) {
+    assert !group.hasTarget();
+    assert !group.hasInstanceFieldMap();
+
+    if (group.isInterfaceGroup()) {
+      return ListUtils.newLinkedList(group);
+    }
+
+    // When we merge equivalent instance initializers with different protos, we find the least upper
+    // bound of each parameter type. As a result of this, the final instance initializer signatures
+    // are not known until all instance initializers in the group are known. Therefore, we disallow
+    // merging of classes that have multiple methods with the same relaxed method signature (where
+    // reference parameters are converted to java.lang.Object), to ensure that merging will result
+    // in a simple renaming (specifically, we must not need to append null arguments to constructor
+    // calls due to constructor collisions).
+    group.removeIf(this::hasMultipleInstanceInitializersWithSameRelaxedSignature);
+
+    if (group.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    // We want to allow merging of equivalent instance initializers. Equivalence depends on the
+    // mapping of instance fields, so we must compute this mapping now.
+    group.selectTarget(appView);
+    group.selectInstanceFieldMap(appView);
+
     Map<MergeGroup, Map<DexMethodSignature, ProgramMethod>> newGroups = new LinkedHashMap<>();
 
-    for (DexProgramClass clazz : group) {
-      Map<DexMethodSignature, ProgramMethod> classSignatures = new HashMap<>();
-      clazz.forEachProgramInstanceInitializer(
-          method -> classSignatures.put(method.getMethodSignature(), method));
+    // Caching of instance initializer descriptions, which are used to determine equivalence.
+    // TODO(b/181846319): Make this cache available to the instance initializer merger so that we
+    //  don't reanalyze instance initializers.
+    ProgramMethodMap<Optional<InstanceInitializerDescription>> instanceInitializerDescriptions =
+        ProgramMethodMap.create();
+    Function<ProgramMethod, Optional<InstanceInitializerDescription>>
+        instanceInitializerDescriptionProvider =
+            instanceInitializer ->
+                getOrComputeInstanceInitializerDescription(
+                    group, instanceInitializer, instanceInitializerDescriptions);
 
+    // Partition group into smaller groups where there are no (non-equivalent) instance initializer
+    // collisions.
+    for (DexProgramClass clazz : group) {
       MergeGroup newGroup = null;
+      Map<DexMethodSignature, ProgramMethod> classInstanceInitializers =
+          getInstanceInitializersByRelaxedSignature(clazz);
       for (Entry<MergeGroup, Map<DexMethodSignature, ProgramMethod>> entry : newGroups.entrySet()) {
-        Map<DexMethodSignature, ProgramMethod> groupSignatures = entry.getValue();
-        if (canAddClassToGroup(classSignatures.values(), groupSignatures)) {
-          newGroup = entry.getKey();
-          groupSignatures.putAll(classSignatures);
+        MergeGroup candidateGroup = entry.getKey();
+        Map<DexMethodSignature, ProgramMethod> groupInstanceInitializers = entry.getValue();
+        if (canAddClassToGroup(
+            classInstanceInitializers,
+            groupInstanceInitializers,
+            instanceInitializerDescriptionProvider)) {
+          newGroup = candidateGroup;
+          classInstanceInitializers.forEach(groupInstanceInitializers::put);
           break;
         }
       }
-
       if (newGroup != null) {
         newGroup.add(clazz);
       } else {
-        newGroups.put(new MergeGroup(clazz), classSignatures);
+        newGroups.put(new MergeGroup(clazz), classInstanceInitializers);
       }
     }
 
-    return removeTrivialGroups(newGroups.keySet());
+    // Remove trivial groups and finalize the newly created groups.
+    Collection<MergeGroup> newNonTrivialGroups = removeTrivialGroups(newGroups.keySet());
+    setInstanceFieldMaps(newNonTrivialGroups, group);
+    return newNonTrivialGroups;
   }
 
   private boolean canAddClassToGroup(
-      Iterable<ProgramMethod> classMethods,
-      Map<DexMethodSignature, ProgramMethod> groupSignatures) {
-    for (ProgramMethod classMethod : classMethods) {
-      ProgramMethod groupMethod = groupSignatures.get(classMethod.getMethodSignature());
-      if (groupMethod != null && !equivalent(classMethod, groupMethod)) {
+      Map<DexMethodSignature, ProgramMethod> classInstanceInitializers,
+      Map<DexMethodSignature, ProgramMethod> groupInstanceInitializers,
+      Function<ProgramMethod, Optional<InstanceInitializerDescription>>
+          instanceInitializerDescriptionProvider) {
+    for (Entry<DexMethodSignature, ProgramMethod> entry : classInstanceInitializers.entrySet()) {
+      DexMethodSignature relaxedSignature = entry.getKey();
+      ProgramMethod classInstanceInitializer = entry.getValue();
+      ProgramMethod groupInstanceInitializer = groupInstanceInitializers.get(relaxedSignature);
+      if (groupInstanceInitializer == null) {
+        continue;
+      }
+
+      Optional<InstanceInitializerDescription> classInstanceInitializerDescription =
+          instanceInitializerDescriptionProvider.apply(classInstanceInitializer);
+      if (!classInstanceInitializerDescription.isPresent()) {
+        return false;
+      }
+
+      Optional<InstanceInitializerDescription> groupInstanceInitializerDescription =
+          instanceInitializerDescriptionProvider.apply(groupInstanceInitializer);
+      if (!groupInstanceInitializerDescription.isPresent()
+          || !classInstanceInitializerDescription.equals(groupInstanceInitializerDescription)) {
         return false;
       }
     }
     return true;
   }
 
-  // For now, only recognize constructors with 0 parameters that call the same parent constructor.
-  private boolean equivalent(ProgramMethod method, ProgramMethod other) {
-    if (!method.getProto().getParameters().isEmpty()) {
+  private boolean hasMultipleInstanceInitializersWithSameRelaxedSignature(DexProgramClass clazz) {
+    Iterator<ProgramMethod> instanceInitializers = clazz.programInstanceInitializers().iterator();
+    if (!instanceInitializers.hasNext()) {
+      // No instance initializers.
       return false;
     }
 
-    MethodOptimizationInfo optimizationInfo = method.getDefinition().getOptimizationInfo();
-    InstanceInitializerInfo instanceInitializerInfo =
-        optimizationInfo.getContextInsensitiveInstanceInitializerInfo();
-    if (instanceInitializerInfo.isDefaultInstanceInitializerInfo()) {
+    ProgramMethod first = instanceInitializers.next();
+    if (!instanceInitializers.hasNext()) {
+      // Only a single instance initializer.
       return false;
     }
 
-    InstanceInitializerInfo otherInstanceInitializerInfo =
-        other.getDefinition().getOptimizationInfo().getContextInsensitiveInstanceInitializerInfo();
-    assert otherInstanceInitializerInfo.isNonTrivialInstanceInitializerInfo();
-    if (!instanceInitializerInfo.hasParent()
-        || instanceInitializerInfo.getParent().getArity() > 0) {
-      return false;
-    }
+    Set<DexMethod> seen = SetUtils.newIdentityHashSet(getRelaxedSignature(first));
+    return Iterators.any(
+        instanceInitializers,
+        instanceInitializer -> !seen.add(getRelaxedSignature(instanceInitializer)));
+  }
 
-    if (instanceInitializerInfo.getParent() != otherInstanceInitializerInfo.getParent()) {
-      return false;
+  private Map<DexMethodSignature, ProgramMethod> getInstanceInitializersByRelaxedSignature(
+      DexProgramClass clazz) {
+    Map<DexMethodSignature, ProgramMethod> result = new HashMap<>();
+    for (ProgramMethod instanceInitializer : clazz.programInstanceInitializers()) {
+      DexMethodSignature relaxedSignature = getRelaxedSignature(instanceInitializer).getSignature();
+      ProgramMethod previous = result.put(relaxedSignature, instanceInitializer);
+      assert previous == null;
     }
+    return result;
+  }
 
-    return !method.getDefinition().getOptimizationInfo().mayHaveSideEffects()
-        && !other.getDefinition().getOptimizationInfo().mayHaveSideEffects();
+  private Optional<InstanceInitializerDescription> getOrComputeInstanceInitializerDescription(
+      MergeGroup group,
+      ProgramMethod instanceInitializer,
+      ProgramMethodMap<Optional<InstanceInitializerDescription>> instanceInitializerDescriptions) {
+    return instanceInitializerDescriptions.computeIfAbsent(
+        instanceInitializer,
+        key -> {
+          InstanceInitializerDescription instanceInitializerDescription =
+              InstanceInitializerAnalysis.analyze(
+                  appView, codeProvider, group, instanceInitializer);
+          return Optional.ofNullable(instanceInitializerDescription);
+        });
+  }
+
+  private DexMethod getRelaxedSignature(ProgramMethod instanceInitializer) {
+    DexType objectType = appView.dexItemFactory().objectType;
+    DexTypeList parameters = instanceInitializer.getParameters();
+    DexTypeList relaxedParameters =
+        parameters.map(parameter -> parameter.isPrimitiveType() ? parameter : objectType);
+    return parameters != relaxedParameters
+        ? appView
+            .dexItemFactory()
+            .createInstanceInitializer(instanceInitializer.getHolderType(), relaxedParameters)
+        : instanceInitializer.getReference();
+  }
+
+  private void setInstanceFieldMaps(Iterable<MergeGroup> newGroups, MergeGroup group) {
+    for (MergeGroup newGroup : newGroups) {
+      // Set target.
+      newGroup.selectTarget(appView);
+
+      // Construct mapping from instance fields on old target to instance fields on new target.
+      // Note the importance of this: If we create a fresh mapping from the instance fields of each
+      // source class to the new target class, we could invalidate the constructor equivalence.
+      Map<DexEncodedField, DexEncodedField> oldTargetToNewTargetInstanceFieldMap =
+          new IdentityHashMap<>();
+      if (newGroup.getTarget() != group.getTarget()) {
+        ClassInstanceFieldsMerger.mapFields(
+            appView,
+            group.getTarget(),
+            newGroup.getTarget(),
+            oldTargetToNewTargetInstanceFieldMap::put);
+      }
+
+      // Construct mapping from source to target fields.
+      MutableBidirectionalManyToOneMap<DexEncodedField, DexEncodedField> instanceFieldMap =
+          BidirectionalManyToOneHashMap.newLinkedHashMap();
+      newGroup.forEachSource(
+          source ->
+              source.forEachProgramInstanceField(
+                  sourceField -> {
+                    DexEncodedField oldTargetInstanceField =
+                        group.getTargetInstanceField(sourceField).getDefinition();
+                    DexEncodedField newTargetInstanceField =
+                        oldTargetToNewTargetInstanceFieldMap.getOrDefault(
+                            oldTargetInstanceField, oldTargetInstanceField);
+                    instanceFieldMap.put(sourceField.getDefinition(), newTargetInstanceField);
+                  }));
+      newGroup.setInstanceFieldMap(instanceFieldMap);
+    }
   }
 
   @Override
   public String getName() {
     return "NoInstanceInitializerMerging";
   }
+
+  @Override
+  public boolean isIdentityForInterfaceGroups() {
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index 98ea2c4..070e63f 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -25,6 +25,12 @@
     return result;
   }
 
+  public static <T> Set<T> newIdentityHashSet(ForEachable<T> forEachable) {
+    Set<T> result = Sets.newIdentityHashSet();
+    forEachable.forEach(result::add);
+    return result;
+  }
+
   public static <T> Set<T> newIdentityHashSet(Iterable<T> c) {
     Set<T> result = Sets.newIdentityHashSet();
     c.forEach(result::add);
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index ba0ced5..a92437f 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -116,14 +116,14 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 16, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 10, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
         .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 2, "lambdadesugaring"))
         .run();
   }
 
@@ -155,14 +155,14 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 16, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 10, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
         .withMinApiLevel(AndroidApiLevel.N)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 2, "lambdadesugaring"))
         .run();
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
new file mode 100644
index 0000000..fadf934
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
@@ -0,0 +1,87 @@
+// 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.NeverPropagateValue;
+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 StatefulSingletonClassesMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StatefulSingletonClassesMergingTest(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()
+        .enableMemberValuePropagationAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .noClassStaticizing()
+        .setMinApi(parameters.getApiLevel())
+        .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("A");
+
+    @NeverPropagateValue private final String data;
+
+    A(String data) {
+      this.data = data;
+    }
+
+    @NeverInline
+    void f() {
+      System.out.println(data);
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    static final B INSTANCE = new B("B");
+
+    @NeverPropagateValue private final String data;
+
+    B(String data) {
+      this.data = data;
+    }
+
+    @NeverInline
+    void g() {
+      System.out.println(data);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java
index e063a4b..d784a77 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 
 public class DistinguishExceptionClassesTest extends HorizontalClassMergingTestBase {
@@ -20,6 +21,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("test success")