Merge equivalent instance initializers

Bug: 189296638
Change-Id: Ibca97395154568252cf04d78c9ec54c3987a5de5
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
index 592ee31..cd8d6b7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
@@ -42,6 +42,10 @@
     spec.withInt(f -> f.opcode).withItem(f -> f.field).withItem(f -> f.declaringField);
   }
 
+  public CfFieldInstruction(int opcode, DexField field) {
+    this(opcode, field, field);
+  }
+
   public CfFieldInstruction(int opcode, DexField field, DexField declaringField) {
     this.opcode = opcode;
     this.field = field;
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 646c9a0..a42388a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2238,6 +2238,10 @@
     return createMethod(holder, createProto(voidType), classConstructorMethodName);
   }
 
+  public DexMethod createInstanceInitializer(DexType holder, DexType... parameters) {
+    return createMethod(holder, createProto(voidType, parameters), constructorMethodName);
+  }
+
   public DexMethod createInstanceInitializerWithFreshProto(
       DexMethod method, List<DexType> extraTypes, Predicate<DexMethod> isFresh) {
     assert method.isInstanceInitializer(this);
@@ -2258,8 +2262,6 @@
 
   private DexMethod createInstanceInitializerWithFreshProto(
       DexProto proto, List<DexType> extraTypes, Function<DexProto, Optional<DexMethod>> isFresh) {
-    assert !extraTypes.isEmpty();
-
     Queue<Iterable<DexProto>> tryProtos = new LinkedList<>();
     Iterator<DexProto> current = IterableUtils.singleton(proto).iterator();
 
@@ -2276,6 +2278,7 @@
       if (object.isPresent()) {
         return object.get();
       }
+      assert !extraTypes.isEmpty();
       tryProtos.add(
           Iterables.transform(extraTypes, extraType -> appendTypeToProto(tryProto, extraType)));
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index a38770a..96c3084 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -93,6 +93,11 @@
     return descriptor;
   }
 
+  public int getRequiredRegisters() {
+    assert !isVoidType();
+    return isWideType() ? 2 : 1;
+  }
+
   @Override
   public int computeHashCode() {
     return descriptor.hashCode();
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
new file mode 100644
index 0000000..b6d9757
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
@@ -0,0 +1,89 @@
+// 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.graph;
+
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
+import com.android.tools.r8.utils.ObjectUtils;
+import java.util.Iterator;
+
+public class DexTypeUtils {
+
+  public static DexType computeLeastUpperBound(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Iterable<DexType> types) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+
+    Iterator<DexType> iterator = types.iterator();
+    assert iterator.hasNext();
+
+    DexType result = iterator.next();
+    while (iterator.hasNext()) {
+      result = computeLeastUpperBound(appView, result, iterator.next());
+    }
+    return result;
+  }
+
+  public static DexType computeLeastUpperBound(
+      AppView<? extends AppInfoWithClassHierarchy> appView, DexType type, DexType other) {
+    if (type == other) {
+      return type;
+    }
+
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    if (type == dexItemFactory.objectType
+        || other == dexItemFactory.objectType
+        || type.isArrayType() != other.isArrayType()) {
+      return dexItemFactory.objectType;
+    }
+
+    if (type.isArrayType()) {
+      assert other.isArrayType();
+      int arrayDimension = type.getNumberOfLeadingSquareBrackets();
+      if (other.getNumberOfLeadingSquareBrackets() != arrayDimension) {
+        return dexItemFactory.objectType;
+      }
+
+      DexType baseType = type.toBaseType(dexItemFactory);
+      DexType otherBaseType = other.toBaseType(dexItemFactory);
+      if (baseType.isPrimitiveType() || otherBaseType.isPrimitiveType()) {
+        assert baseType != otherBaseType;
+        return dexItemFactory.objectType;
+      }
+
+      return dexItemFactory.createArrayType(
+          arrayDimension, computeLeastUpperBound(appView, baseType, otherBaseType));
+    }
+
+    assert !type.isArrayType();
+    assert !other.isArrayType();
+
+    boolean isInterface =
+        type.isClassType()
+            && ObjectUtils.getBooleanOrElse(
+                appView.definitionFor(type), DexClass::isInterface, false);
+    boolean otherIsInterface =
+        other.isClassType()
+            && ObjectUtils.getBooleanOrElse(
+                appView.definitionFor(other), DexClass::isInterface, false);
+    if (isInterface != otherIsInterface) {
+      return dexItemFactory.objectType;
+    }
+
+    if (isInterface) {
+      assert otherIsInterface;
+      InterfaceCollection interfaceCollection =
+          ClassTypeElement.computeLeastUpperBoundOfInterfaces(
+              appView, InterfaceCollection.singleton(type), InterfaceCollection.singleton(other));
+      return interfaceCollection.hasSingleKnownInterface()
+          ? interfaceCollection.getSingleKnownInterface()
+          : dexItemFactory.objectType;
+    }
+
+    assert !isInterface;
+    assert !otherIsInterface;
+
+    return ClassTypeElement.computeLeastUpperBoundOfClasses(appView.appInfo(), type, other);
+  }
+}
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 999869c..3927d11 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -62,13 +62,18 @@
   private final Mode mode;
   private final MergeGroup group;
   private final DexItemFactory dexItemFactory;
-  private final ClassInitializerMerger classInitializerMerger;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
 
   private final ClassMethodsBuilder classMethodsBuilder = new ClassMethodsBuilder();
   private final Reference2IntMap<DexType> classIdentifiers = new Reference2IntOpenHashMap<>();
-  private final ClassStaticFieldsMerger classStaticFieldsMerger;
+
+  // Field mergers.
   private final ClassInstanceFieldsMerger classInstanceFieldsMerger;
+  private final ClassStaticFieldsMerger classStaticFieldsMerger;
+
+  // Method mergers.
+  private final ClassInitializerMerger classInitializerMerger;
+  private final InstanceInitializerMergerCollection instanceInitializerMergers;
   private final Collection<VirtualMethodMerger> virtualMethodMergers;
 
   private ClassMerger(
@@ -82,10 +87,18 @@
     this.group = group;
     this.lensBuilder = lensBuilder;
     this.mode = mode;
-    this.virtualMethodMergers = virtualMethodMergers;
-    this.classInitializerMerger = ClassInitializerMerger.create(group);
+
+    // Field mergers.
     this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, group);
     this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(appView, lensBuilder, group);
+
+    // Method mergers.
+    this.classInitializerMerger = ClassInitializerMerger.create(group);
+    this.instanceInitializerMergers =
+        InstanceInitializerMergerCollection.create(
+            appView, classIdentifiers, group, classInstanceFieldsMerger, lensBuilder, mode);
+    this.virtualMethodMergers = virtualMethodMergers;
+
     buildClassIdentifierMap();
   }
 
@@ -175,12 +188,8 @@
   }
 
   void mergeInstanceInitializers(SyntheticArgumentClass syntheticArgumentClass) {
-    InstanceInitializerMergerCollection instanceInitializerMergers =
-        InstanceInitializerMergerCollection.create(appView, group, lensBuilder, mode);
     instanceInitializerMergers.forEach(
-        merger ->
-            merger.merge(
-                classMethodsBuilder, lensBuilder, classIdentifiers, syntheticArgumentClass));
+        merger -> merger.merge(classMethodsBuilder, syntheticArgumentClass));
   }
 
   void mergeMethods(
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 c05af00..4ad6f55 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -212,6 +212,9 @@
     for (ClassMerger merger : classMergers) {
       merger.mergeGroup(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
     }
+
+    // Clear type elements cache after IR building.
+    appView.dexItemFactory().clearTypeElementsCache();
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerCfCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerCfCode.java
new file mode 100644
index 0000000..839b6d5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerCfCode.java
@@ -0,0 +1,33 @@
+// 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.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexType;
+import java.util.List;
+
+/**
+ * Similar to CfCode, but with a marker that makes it possible to recognize this is synthesized by
+ * the horizontal class merger.
+ */
+public class HorizontalClassMergerCfCode extends CfCode {
+
+  HorizontalClassMergerCfCode(
+      DexType originalHolder,
+      int maxStack,
+      int maxLocals,
+      List<CfInstruction> instructions,
+      List<CfTryCatch> tryCatchRanges,
+      List<LocalVariableInfo> localVariables) {
+    super(originalHolder, maxStack, maxLocals, instructions, tryCatchRanges, localVariables);
+  }
+
+  @Override
+  public boolean isHorizontalClassMergingCode() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index abadbc4..20eb5a3 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -120,6 +120,11 @@
           newMethodSignatures);
     }
 
+    DexMethod getRenamedMethodSignature(DexMethod method) {
+      assert newMethodSignatures.containsKey(method);
+      return newMethodSignatures.get(method);
+    }
+
     void recordNewFieldSignature(DexField oldFieldSignature, DexField newFieldSignature) {
       fieldMap.put(oldFieldSignature, newFieldSignature);
     }
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 febdbe6..9a6390a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
@@ -4,14 +4,204 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import static com.android.tools.r8.ir.code.Opcodes.ARGUMENT;
+import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_CLASS;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_NUMBER;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.DEX_ITEM_BASED_CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.GOTO;
+import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.RETURN;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+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;
 
 public class InstanceInitializerAnalysis {
 
   public static InstanceInitializerDescription analyze(
-      ProgramMethod instanceInitializer, HorizontalClassMergerGraphLens.Builder lensBuilder) {
-    // TODO(b/189296638): Return an InstanceInitializerDescription if the given instance initializer
-    //  is parent constructor call followed by a sequence of instance field puts.
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      MergeGroup group,
+      ProgramMethod instanceInitializer,
+      ClassInstanceFieldsMerger instanceFieldsMerger,
+      Mode mode) {
+    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);
+    WorkList<BasicBlock> workList = WorkList.newIdentityWorkList(code.entryBlock());
+    while (workList.hasNext()) {
+      BasicBlock block = workList.next();
+      for (Instruction instruction : block.getInstructions()) {
+        switch (instruction.opcode()) {
+          case ARGUMENT:
+          case ASSUME:
+          case CONST_CLASS:
+          case CONST_NUMBER:
+          case CONST_STRING:
+          case DEX_ITEM_BASED_CONST_STRING:
+          case RETURN:
+            break;
+
+          case GOTO:
+            if (!workList.addIfNotSeen(instruction.asGoto().getTarget())) {
+              return invalid();
+            }
+            break;
+
+          case INSTANCE_PUT:
+            {
+              InstancePut instancePut = instruction.asInstancePut();
+
+              // This must initialize a field on the receiver.
+              if (!instancePut.object().getAliasedValue().isThis()) {
+                return invalid();
+              }
+
+              // Check that this writes a field on the enclosing class.
+              DexField fieldReference = instancePut.getField();
+              DexField lensRewrittenFieldReference =
+                  appView.graphLens().lookupField(fieldReference);
+              if (lensRewrittenFieldReference.getHolderType()
+                  != instanceInitializer.getHolderType()) {
+                return invalid();
+              }
+
+              ProgramField sourceField =
+                  instanceInitializer.getHolder().lookupProgramField(lensRewrittenFieldReference);
+              if (sourceField == null) {
+                return invalid();
+              }
+
+              InstanceFieldInitializationInfo initializationInfo =
+                  getInitializationInfo(instancePut.value(), appView, instanceInitializer);
+              if (initializationInfo == null) {
+                return invalid();
+              }
+
+              ProgramField targetField = instanceFieldsMerger.getTargetField(sourceField);
+              assert targetField != null;
+
+              builder.addInstancePut(targetField.getReference(), initializationInfo);
+              break;
+            }
+
+          case INVOKE_DIRECT:
+            {
+              InvokeDirect invoke = instruction.asInvokeDirect();
+
+              // This must initialize the receiver.
+              if (!invoke.getReceiver().getAliasedValue().isThis()) {
+                return invalid();
+              }
+
+              DexMethod invokedMethod = invoke.getInvokedMethod();
+              DexMethod lensRewrittenInvokedMethod =
+                  appView
+                      .graphLens()
+                      .lookupInvokeDirect(invokedMethod, instanceInitializer)
+                      .getReference();
+
+              // TODO(b/189296638): Consider allowing constructor forwarding.
+              if (!lensRewrittenInvokedMethod.isInstanceInitializer(appView.dexItemFactory())
+                  || lensRewrittenInvokedMethod.getHolderType() != group.getSuperType()) {
+                return invalid();
+              }
+
+              // Extract the non-receiver arguments.
+              List<InstanceFieldInitializationInfo> arguments =
+                  new ArrayList<>(invoke.arguments().size() - 1);
+              for (Value argument : Iterables.skip(invoke.arguments(), 1)) {
+                InstanceFieldInitializationInfo initializationInfo =
+                    getInitializationInfo(argument, appView, instanceInitializer);
+                if (initializationInfo == null) {
+                  return invalid();
+                }
+                arguments.add(initializationInfo);
+              }
+
+              if (!builder.addInvokeConstructor(invokedMethod, arguments)) {
+                return invalid();
+              }
+            }
+            break;
+
+          default:
+            // Not allowed.
+            return invalid();
+        }
+      }
+    }
+
+    return builder.isValid() ? builder.build() : null;
+  }
+
+  private static InstanceFieldInitializationInfo getInitializationInfo(
+      Value value,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      ProgramMethod instanceInitializer) {
+    InstanceFieldInitializationInfoFactory factory =
+        appView.instanceFieldInitializationInfoFactory();
+
+    Value root = value.getAliasedValue();
+    if (root.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
+      return factory.createArgumentInitializationInfo(
+          value.getDefinition().asArgument().getIndex());
+    }
+
+    AbstractValue abstractValue = value.getAbstractValue(appView, instanceInitializer);
+    if (abstractValue.isSingleConstValue()) {
+      return abstractValue.asSingleConstValue();
+    }
+
+    return null;
+  }
+
+  private static InstanceInitializerDescription invalid() {
     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 41f2c4e..b67c652 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
@@ -4,14 +4,44 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.cf.code.CfConstClass;
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfDexItemBasedConstString;
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLabel;
+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;
+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.ir.analysis.value.SingleConstValue;
+import com.android.tools.r8.ir.analysis.value.SingleDexItemBasedStringValue;
+import com.android.tools.r8.ir.code.Position;
+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;
 import java.util.Map;
 import java.util.Objects;
+import org.objectweb.asm.Opcodes;
 
 /**
  * A simple abstraction of an instance initializer's code, which allows a parent constructor call
@@ -19,24 +49,219 @@
  */
 public class InstanceInitializerDescription {
 
-  private final int arity;
-  private final Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments;
+  // Field assignments that happen prior to the parent constructor call.
+  //
+  // Most fields are generally assigned after the parent constructor call, but both javac and
+  // kotlinc may assign instance fields prior to the parent constructor call. For example, the
+  // synthetic this$0 field for non-static inner classes is typically assigned prior to the parent
+  // constructor call.
+  private final Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPre;
+
+  // Field assignments that happens after the parent constructor call.
+  private final Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPost;
+
+  // The parent constructor method and the arguments passed to it.
   private final DexMethod parentConstructor;
   private final List<InstanceFieldInitializationInfo> parentConstructorArguments;
 
+  // The constructor parameters, where reference types have been mapped to java.lang.Object, to
+  // ensure we don't group constructors such as <init>(int) and <init>(Object), since this would
+  // lead to type errors.
+  private final DexTypeList relaxedParameters;
+
   InstanceInitializerDescription(
-      int arity,
-      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments,
+      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPre,
+      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPost,
       DexMethod parentConstructor,
-      List<InstanceFieldInitializationInfo> parentConstructorArguments) {
-    this.arity = arity;
-    this.instanceFieldAssignments = instanceFieldAssignments;
+      List<InstanceFieldInitializationInfo> parentConstructorArguments,
+      DexTypeList relaxedParameters) {
+    this.instanceFieldAssignmentsPre = instanceFieldAssignmentsPre;
+    this.instanceFieldAssignmentsPost = instanceFieldAssignmentsPost;
     this.parentConstructor = parentConstructor;
     this.parentConstructorArguments = parentConstructorArguments;
+    this.relaxedParameters = relaxedParameters;
   }
 
-  public static Builder builder(ProgramMethod instanceInitializer) {
-    return new Builder(instanceInitializer);
+  public static Builder builder(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod instanceInitializer) {
+    return new Builder(appView.dexItemFactory(), instanceInitializer);
+  }
+
+  /**
+   * Transform this description into actual CF code.
+   *
+   * @param newMethodReference the reference of the method that is being synthesized
+   * @param originalMethodReference the original reference of the representative method
+   * @param syntheticMethodReference the original, synthetic reference of the new method reference
+   *     ($r8$init$synthetic)
+   */
+  public CfCode createCfCode(
+      DexMethod newMethodReference,
+      DexMethod originalMethodReference,
+      DexMethod syntheticMethodReference,
+      MergeGroup group,
+      boolean hasClassId,
+      int extraNulls) {
+    int[] argumentToLocalIndex =
+        new int[newMethodReference.getParameters().size() + 1 - extraNulls];
+    int maxLocals = 0;
+    argumentToLocalIndex[0] = maxLocals++;
+    for (int i = 1; i < argumentToLocalIndex.length; i++) {
+      argumentToLocalIndex[i] = maxLocals;
+      maxLocals += newMethodReference.getParameter(i - 1).getRequiredRegisters();
+    }
+
+    IntBox maxStack = new IntBox();
+    ImmutableList.Builder<CfInstruction> instructionBuilder = ImmutableList.builder();
+
+    // Set position.
+    Position callerPosition = Position.synthetic(0, syntheticMethodReference, null);
+    Position calleePosition = Position.synthetic(0, originalMethodReference, callerPosition);
+    CfPosition position = new CfPosition(new CfLabel(), calleePosition);
+    instructionBuilder.add(position);
+    instructionBuilder.add(position.getLabel());
+
+    // Assign class id.
+    if (group.hasClassIdField()) {
+      assert hasClassId;
+      int classIdLocalIndex = maxLocals - 1;
+      instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
+      instructionBuilder.add(new CfLoad(ValueType.INT, classIdLocalIndex));
+      instructionBuilder.add(new CfFieldInstruction(Opcodes.PUTFIELD, group.getClassIdField()));
+      maxStack.set(2);
+    } else {
+      assert !hasClassId;
+    }
+
+    // Assign each field.
+    addCfInstructionsForInstanceFieldAssignments(
+        instructionBuilder, instanceFieldAssignmentsPre, argumentToLocalIndex, maxStack);
+
+    // Load receiver for parent constructor call.
+    int stackHeightForParentConstructorCall = 1;
+    instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
+
+    // Load constructor arguments.
+    int i = 0;
+    for (InstanceFieldInitializationInfo initializationInfo : parentConstructorArguments) {
+      stackHeightForParentConstructorCall +=
+          addCfInstructionsForInitializationInfo(
+              instructionBuilder,
+              initializationInfo,
+              argumentToLocalIndex,
+              parentConstructor.getParameter(i));
+      i++;
+    }
+
+    // Invoke parent constructor.
+    instructionBuilder.add(new CfInvoke(Opcodes.INVOKESPECIAL, parentConstructor, false));
+    maxStack.setMax(stackHeightForParentConstructorCall);
+
+    // Assign each field.
+    addCfInstructionsForInstanceFieldAssignments(
+        instructionBuilder, instanceFieldAssignmentsPost, argumentToLocalIndex, maxStack);
+
+    // Return.
+    instructionBuilder.add(new CfReturnVoid());
+
+    return new HorizontalClassMergerCfCode(
+        newMethodReference.getHolderType(),
+        maxStack.get(),
+        maxLocals,
+        instructionBuilder.build(),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
+  private static void addCfInstructionsForInstanceFieldAssignments(
+      ImmutableList.Builder<CfInstruction> instructionBuilder,
+      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments,
+      int[] argumentToLocalIndex,
+      IntBox maxStack) {
+    instanceFieldAssignments.forEach(
+        (field, initializationInfo) -> {
+          // Load the receiver, the field value, and then set the field.
+          instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
+          int stackSizeForInitializationInfo =
+              addCfInstructionsForInitializationInfo(
+                  instructionBuilder, initializationInfo, argumentToLocalIndex, field.getType());
+          instructionBuilder.add(new CfFieldInstruction(Opcodes.PUTFIELD, field));
+          maxStack.setMax(stackSizeForInitializationInfo + 1);
+        });
+  }
+
+  private static int addCfInstructionsForInitializationInfo(
+      ImmutableList.Builder<CfInstruction> instructionBuilder,
+      InstanceFieldInitializationInfo initializationInfo,
+      int[] argumentToLocalIndex,
+      DexType type) {
+    if (initializationInfo.isArgumentInitializationInfo()) {
+      int argumentIndex = initializationInfo.asArgumentInitializationInfo().getArgumentIndex();
+      instructionBuilder.add(
+          new CfLoad(ValueType.fromDexType(type), argumentToLocalIndex[argumentIndex]));
+      return type.getRequiredRegisters();
+    }
+
+    assert initializationInfo.isSingleValue();
+    assert initializationInfo.asSingleValue().isSingleConstValue();
+
+    SingleConstValue singleConstValue = initializationInfo.asSingleValue().asSingleConstValue();
+    if (singleConstValue.isSingleConstClassValue()) {
+      instructionBuilder.add(
+          new CfConstClass(singleConstValue.asSingleConstClassValue().getType()));
+      return 1;
+    } else if (singleConstValue.isSingleDexItemBasedStringValue()) {
+      SingleDexItemBasedStringValue dexItemBasedStringValue =
+          singleConstValue.asSingleDexItemBasedStringValue();
+      instructionBuilder.add(
+          new CfDexItemBasedConstString(
+              dexItemBasedStringValue.getItem(), dexItemBasedStringValue.getNameComputationInfo()));
+      return 1;
+    } else if (singleConstValue.isSingleNumberValue()) {
+      if (type.isReferenceType()) {
+        assert singleConstValue.isNull();
+        instructionBuilder.add(new CfConstNull());
+        return 1;
+      } else {
+        instructionBuilder.add(
+            new CfConstNumber(
+                singleConstValue.asSingleNumberValue().getValue(), ValueType.fromDexType(type)));
+        return type.getRequiredRegisters();
+      }
+    } else {
+      assert singleConstValue.isSingleStringValue();
+      instructionBuilder.add(
+          new CfConstString(singleConstValue.asSingleStringValue().getDexString()));
+      return 1;
+    }
+  }
+
+  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
@@ -45,8 +270,8 @@
       return false;
     }
     InstanceInitializerDescription description = (InstanceInitializerDescription) obj;
-    return arity == description.arity
-        && instanceFieldAssignments.equals(description.instanceFieldAssignments)
+    return instanceFieldAssignmentsPre.equals(description.instanceFieldAssignmentsPre)
+        && instanceFieldAssignmentsPost.equals(description.instanceFieldAssignmentsPost)
         && parentConstructor == description.parentConstructor
         && parentConstructorArguments.equals(description.parentConstructorArguments);
   }
@@ -54,38 +279,67 @@
   @Override
   public int hashCode() {
     return Objects.hash(
-        arity, instanceFieldAssignments, parentConstructor, parentConstructorArguments);
+        instanceFieldAssignmentsPre,
+        instanceFieldAssignmentsPost,
+        parentConstructor,
+        parentConstructorArguments,
+        relaxedParameters);
   }
 
   public static class Builder {
 
-    private final int arity;
+    private final DexItemFactory dexItemFactory;
+    private final DexTypeList relaxedParameters;
 
-    private Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments =
+    private Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPre =
+        new LinkedHashMap<>();
+    private Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPost =
         new LinkedHashMap<>();
     private DexMethod parentConstructor;
     private List<InstanceFieldInitializationInfo> parentConstructorArguments;
 
-    Builder(ProgramMethod method) {
-      this.arity = method.getReference().getArity();
+    Builder(DexItemFactory dexItemFactory, ProgramMethod method) {
+      this.dexItemFactory = dexItemFactory;
+      this.relaxedParameters =
+          method
+              .getParameters()
+              .map(
+                  parameter -> parameter.isPrimitiveType() ? parameter : dexItemFactory.objectType);
     }
 
     public void addInstancePut(DexField field, InstanceFieldInitializationInfo value) {
-      instanceFieldAssignments.put(field, value);
+      // If the parent constructor is java.lang.Object.<init>() then group all the field assignments
+      // before the parent constructor call to allow more sharing.
+      if (parentConstructor == null
+          || parentConstructor == dexItemFactory.objectMembers.constructor) {
+        instanceFieldAssignmentsPre.put(field, value);
+      } else {
+        instanceFieldAssignmentsPost.put(field, value);
+      }
     }
 
-    public void addInvokeConstructor(
+    public boolean addInvokeConstructor(
         DexMethod method, List<InstanceFieldInitializationInfo> arguments) {
-      assert parentConstructor == null;
-      assert parentConstructorArguments == null;
-      parentConstructor = method;
-      parentConstructorArguments = arguments;
+      if (parentConstructor == null) {
+        parentConstructor = method;
+        parentConstructorArguments = arguments;
+        return true;
+      }
+      return false;
     }
 
     public InstanceInitializerDescription build() {
-      assert parentConstructor != null;
+      assert isValid();
       return new InstanceInitializerDescription(
-          arity, instanceFieldAssignments, parentConstructor, parentConstructorArguments);
+          instanceFieldAssignmentsPre,
+          instanceFieldAssignmentsPost,
+          parentConstructor,
+          parentConstructorArguments,
+          relaxedParameters);
+    }
+
+    public boolean isValid() {
+      return parentConstructor != null;
     }
   }
 }
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 b50c71f..862a23c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
@@ -10,11 +10,13 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeUtils;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -24,61 +26,62 @@
 import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
-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.ArrayUtils;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 
 public class InstanceInitializerMerger {
 
-  private final AppView<?> appView;
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Reference2IntMap<DexType> classIdentifiers;
   private final DexItemFactory dexItemFactory;
   private final MergeGroup group;
   private final List<ProgramMethod> instanceInitializers;
   private final InstanceInitializerDescription instanceInitializerDescription;
+  private final HorizontalClassMergerGraphLens.Builder lensBuilder;
   private final Mode mode;
 
   InstanceInitializerMerger(
-      AppView<?> appView, MergeGroup group, List<ProgramMethod> instanceInitializers, Mode mode) {
-    this(appView, group, instanceInitializers, mode, null);
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Reference2IntMap<DexType> classIdentifiers,
+      MergeGroup group,
+      List<ProgramMethod> instanceInitializers,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      Mode mode) {
+    this(appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode, null);
   }
 
   InstanceInitializerMerger(
-      AppView<?> appView,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Reference2IntMap<DexType> classIdentifiers,
       MergeGroup group,
       List<ProgramMethod> instanceInitializers,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode,
       InstanceInitializerDescription instanceInitializerDescription) {
     this.appView = appView;
+    this.classIdentifiers = classIdentifiers;
     this.dexItemFactory = appView.dexItemFactory();
     this.group = group;
     this.instanceInitializers = instanceInitializers;
     this.instanceInitializerDescription = instanceInitializerDescription;
+    this.lensBuilder = lensBuilder;
     this.mode = mode;
 
-    // Constructors should not be empty and all constructors should have the same prototype.
+    // Constructors should not be empty and all constructors should have the same prototype unless
+    // equivalent.
     assert !instanceInitializers.isEmpty();
-    assert instanceInitializers.stream().map(ProgramMethod::getProto).distinct().count() == 1;
-  }
-
-  /**
-   * The method reference template describes which arguments the constructor must have, and is used
-   * to generate the final reference by appending null arguments until it is fresh.
-   */
-  private DexMethod generateReferenceMethodTemplate() {
-    DexMethod methodTemplate = instanceInitializers.iterator().next().getReference();
-    if (instanceInitializers.size() > 1) {
-      methodTemplate = dexItemFactory.appendTypeToMethod(methodTemplate, dexItemFactory.intType);
-    }
-    return methodTemplate.withHolder(group.getTarget(), dexItemFactory);
+    assert instanceInitializers.stream().map(ProgramMethod::getProto).distinct().count() == 1
+        || instanceInitializerDescription != null;
   }
 
   public int getArity() {
@@ -89,6 +92,78 @@
     return instanceInitializers;
   }
 
+  private CfVersion getNewClassFileVersion() {
+    CfVersion classFileVersion = null;
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
+      if (instanceInitializer.getDefinition().hasClassFileVersion()) {
+        classFileVersion =
+            Ordered.maxIgnoreNull(
+                classFileVersion, instanceInitializer.getDefinition().getClassFileVersion());
+      }
+    }
+    return classFileVersion;
+  }
+
+  private DexMethod getNewMethodReference(ProgramMethod representative) {
+    return getNewMethodReference(representative, false);
+  }
+
+  private DexMethod getNewMethodReference(ProgramMethod representative, boolean needsClassId) {
+    DexType[] oldParameters = representative.getParameters().values;
+    DexType[] newParameters =
+        new DexType[representative.getParameters().size() + BooleanUtils.intValue(needsClassId)];
+    System.arraycopy(oldParameters, 0, newParameters, 0, oldParameters.length);
+    for (int i = 0; i < oldParameters.length; i++) {
+      final int parameterIndex = i;
+      newParameters[i] =
+          DexTypeUtils.computeLeastUpperBound(
+              appView,
+              Iterables.transform(
+                  instanceInitializers,
+                  instanceInitializer -> instanceInitializer.getParameter(parameterIndex)));
+    }
+    if (needsClassId) {
+      assert ArrayUtils.last(newParameters) == null;
+      newParameters[newParameters.length - 1] = dexItemFactory.intType;
+    }
+    return dexItemFactory.createInstanceInitializer(group.getTarget().getType(), newParameters);
+  }
+
+  private DexMethod getOriginalMethodReference() {
+    return appView.graphLens().getOriginalMethodSignature(getRepresentative().getReference());
+  }
+
+  private ProgramMethod getRepresentative() {
+    return ListUtils.first(instanceInitializers);
+  }
+
+  /**
+   * Returns a special original method signature for the synthesized constructor that did not exist
+   * prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
+   * constructor corresponds to the previous <init>() method on the target class, which could have
+   * unintended side-effects such as leading to unused argument removal being applied to the
+   * synthesized constructor all-though it by construction doesn't have any unused arguments.
+   */
+  private DexMethod getSyntheticMethodReference(
+      ClassMethodsBuilder classMethodsBuilder, ProgramMethod representative) {
+    return dexItemFactory.createFreshMethodNameWithoutHolder(
+        "$r8$init$synthetic",
+        representative.getProto(),
+        representative.getHolderType(),
+        classMethodsBuilder::isFresh);
+  }
+
+  private Int2ReferenceSortedMap<DexMethod> createClassIdToInstanceInitializerMap() {
+    assert !hasInstanceInitializerDescription();
+    Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
+      typeConstructorClassMap.put(
+          classIdentifiers.getInt(instanceInitializer.getHolderType()),
+          lensBuilder.getRenamedMethodSignature(instanceInitializer.getReference()));
+    }
+    return typeConstructorClassMap;
+  }
+
   public int size() {
     return instanceInitializers.size();
   }
@@ -96,12 +171,20 @@
   public static class Builder {
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private final Reference2IntMap<DexType> classIdentifiers;
     private int estimatedDexCodeSize;
     private final List<List<ProgramMethod>> instanceInitializerGroups = new ArrayList<>();
+    private final HorizontalClassMergerGraphLens.Builder lensBuilder;
     private final Mode mode;
 
-    public Builder(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    public Builder(
+        AppView<? extends AppInfoWithClassHierarchy> appView,
+        Reference2IntMap<DexType> classIdentifiers,
+        HorizontalClassMergerGraphLens.Builder lensBuilder,
+        Mode mode) {
       this.appView = appView;
+      this.classIdentifiers = classIdentifiers;
+      this.lensBuilder = lensBuilder;
       this.mode = mode;
       createNewGroup();
     }
@@ -136,7 +219,8 @@
       return ListUtils.map(
           instanceInitializerGroups,
           instanceInitializers ->
-              new InstanceInitializerMerger(appView, group, instanceInitializers, mode));
+              new InstanceInitializerMerger(
+                  appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode));
     }
 
     public InstanceInitializerMerger buildSingle(
@@ -145,56 +229,18 @@
       assert instanceInitializerGroups.size() == 1;
       List<ProgramMethod> instanceInitializers = ListUtils.first(instanceInitializerGroups);
       return new InstanceInitializerMerger(
-          appView, group, instanceInitializers, mode, instanceInitializerDescription);
+          appView,
+          classIdentifiers,
+          group,
+          instanceInitializers,
+          lensBuilder,
+          mode,
+          instanceInitializerDescription);
     }
   }
 
-  // Returns true if we can simply use an existing constructor as the new constructor.
-  private boolean isTrivialMerge(ClassMethodsBuilder classMethodsBuilder) {
-    if (group.hasClassIdField()) {
-      // We need to set the class id field.
-      return false;
-    }
-    DexMethod trivialInstanceInitializerReference =
-        ListUtils.first(instanceInitializers)
-            .getReference()
-            .withHolder(group.getTarget(), dexItemFactory);
-    if (!classMethodsBuilder.isFresh(trivialInstanceInitializerReference)) {
-      // We need to append null arguments for disambiguation.
-      return false;
-    }
-    return isMergeOfEquivalentInstanceInitializers();
-  }
-
-  private boolean isMergeOfEquivalentInstanceInitializers() {
-    Iterator<ProgramMethod> instanceInitializerIterator = instanceInitializers.iterator();
-    ProgramMethod firstInstanceInitializer = instanceInitializerIterator.next();
-    if (!instanceInitializerIterator.hasNext()) {
-      return true;
-    }
-    // We need all the constructors to be equivalent.
-    InstanceInitializerInfo instanceInitializerInfo =
-        firstInstanceInitializer
-            .getDefinition()
-            .getOptimizationInfo()
-            .getContextInsensitiveInstanceInitializerInfo();
-    if (!instanceInitializerInfo.hasParent()) {
-      // We don't know the parent constructor of the first constructor.
-      return false;
-    }
-    DexMethod parent = instanceInitializerInfo.getParent();
-    return Iterables.all(
-        instanceInitializers,
-        instanceInitializer ->
-            isSideEffectFreeInstanceInitializerWithParent(instanceInitializer, parent));
-  }
-
-  private boolean isSideEffectFreeInstanceInitializerWithParent(
-      ProgramMethod instanceInitializer, DexMethod parent) {
-    MethodOptimizationInfo optimizationInfo =
-        instanceInitializer.getDefinition().getOptimizationInfo();
-    return !optimizationInfo.mayHaveSideEffects()
-        && optimizationInfo.getContextInsensitiveInstanceInitializerInfo().getParent() == parent;
+  private boolean hasInstanceInitializerDescription() {
+    return instanceInitializerDescription != null;
   }
 
   private DexMethod moveInstanceInitializer(
@@ -219,128 +265,118 @@
     return method;
   }
 
-  private MethodAccessFlags getAccessFlags() {
+  private MethodAccessFlags getNewAccessFlags() {
     // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound
     return MethodAccessFlags.fromSharedAccessFlags(
         Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC, true);
   }
 
+  private Code getNewCode(
+      DexMethod newMethodReference,
+      DexMethod syntheticMethodReference,
+      boolean needsClassId,
+      int extraNulls) {
+    if (hasInstanceInitializerDescription()) {
+      if (mode.isInitial() || appView.options().isGeneratingClassFiles()) {
+        return instanceInitializerDescription.createCfCode(
+            newMethodReference,
+            getOriginalMethodReference(),
+            syntheticMethodReference,
+            group,
+            needsClassId,
+            extraNulls);
+      }
+      return instanceInitializerDescription.createDexCode(
+          newMethodReference,
+          getOriginalMethodReference(),
+          syntheticMethodReference,
+          group,
+          needsClassId,
+          extraNulls);
+    }
+    if (isSingleton() && !group.hasClassIdField()) {
+      return getRepresentative().getDefinition().getCode();
+    }
+    return new ConstructorEntryPointSynthesizedCode(
+        createClassIdToInstanceInitializerMap(),
+        newMethodReference,
+        group.hasClassIdField() ? group.getClassIdField() : null,
+        syntheticMethodReference);
+  }
+
+  private boolean isSingleton() {
+    return instanceInitializers.size() == 1;
+  }
+
   /** Synthesize a new method which selects the constructor based on a parameter type. */
   void merge(
       ClassMethodsBuilder classMethodsBuilder,
-      HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Reference2IntMap<DexType> classIdentifiers,
       SyntheticArgumentClass syntheticArgumentClass) {
-    // TODO(b/189296638): Handle merging of equivalent constructors when
-    //  `instanceInitializerDescription` is set.
-    if (isTrivialMerge(classMethodsBuilder)) {
-      mergeTrivial(classMethodsBuilder, lensBuilder);
-      return;
-    }
-
-    assert mode.isInitial();
-
-    // Tree map as must be sorted.
-    Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
-
-    // Move constructors to target class.
-    CfVersion classFileVersion = null;
-    for (ProgramMethod instanceInitializer : instanceInitializers) {
-      if (instanceInitializer.getDefinition().hasClassFileVersion()) {
-        classFileVersion =
-            Ordered.maxIgnoreNull(
-                classFileVersion, instanceInitializer.getDefinition().getClassFileVersion());
-      }
-      DexMethod movedInstanceInitializer =
-          moveInstanceInitializer(classMethodsBuilder, instanceInitializer);
-      lensBuilder.mapMethod(movedInstanceInitializer, movedInstanceInitializer);
-      lensBuilder.recordNewMethodSignature(
-          instanceInitializer.getReference(), movedInstanceInitializer);
-      typeConstructorClassMap.put(
-          classIdentifiers.getInt(instanceInitializer.getHolderType()), movedInstanceInitializer);
-    }
-
-    // Create merged constructor reference.
-    DexMethod methodReferenceTemplate = generateReferenceMethodTemplate();
-    DexMethod newConstructorReference =
-        dexItemFactory.createInstanceInitializerWithFreshProto(
-            methodReferenceTemplate,
-            syntheticArgumentClass.getArgumentClasses(),
-            classMethodsBuilder::isFresh);
-    int extraNulls = newConstructorReference.getArity() - methodReferenceTemplate.getArity();
-
     ProgramMethod representative = ListUtils.first(instanceInitializers);
-    DexMethod originalConstructorReference =
-        appView.graphLens().getOriginalMethodSignature(representative.getReference());
 
-    // Create a special original method signature for the synthesized constructor that did not exist
-    // prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
-    // constructor corresponds to the previous <init>() method on the target class, which could have
-    // unintended side-effects such as leading to unused argument removal being applied to the
-    // synthesized constructor all-though it by construction doesn't have any unused arguments.
-    DexMethod bridgeConstructorReference =
-        dexItemFactory.createFreshMethodNameWithoutHolder(
-            "$r8$init$bridge",
-            originalConstructorReference.getProto(),
-            originalConstructorReference.getHolderType(),
+    // Create merged instance initializer reference.
+    boolean needsClassId =
+        instanceInitializers.size() > 1
+            && (!hasInstanceInitializerDescription() || group.hasClassIdField());
+    assert mode.isInitial() || !needsClassId;
+
+    DexMethod newMethodReferenceTemplate = getNewMethodReference(representative, needsClassId);
+    assert mode.isInitial() || classMethodsBuilder.isFresh(newMethodReferenceTemplate);
+
+    DexMethod newMethodReference =
+        dexItemFactory.createInstanceInitializerWithFreshProto(
+            newMethodReferenceTemplate,
+            mode.isInitial() ? syntheticArgumentClass.getArgumentClasses() : ImmutableList.of(),
             classMethodsBuilder::isFresh);
+    int extraNulls = newMethodReference.getArity() - newMethodReferenceTemplate.getArity();
 
-    ConstructorEntryPointSynthesizedCode synthesizedCode =
-        new ConstructorEntryPointSynthesizedCode(
-            typeConstructorClassMap,
-            newConstructorReference,
-            group.hasClassIdField() ? group.getClassIdField() : null,
-            bridgeConstructorReference);
-    DexEncodedMethod newConstructor =
-        new DexEncodedMethod(
-            newConstructorReference,
-            getAccessFlags(),
-            MethodTypeSignature.noSignature(),
-            DexAnnotationSet.empty(),
-            ParameterAnnotationsList.empty(),
-            synthesizedCode,
-            true,
-            classFileVersion);
+    // Verify that the merge is a simple renaming in the final round of merging.
+    assert mode.isInitial() || newMethodReference == newMethodReferenceTemplate;
 
-    // Map each old constructor to the newly synthesized constructor in the graph lens.
-    for (ProgramMethod oldInstanceInitializer : instanceInitializers) {
+    // Move instance initializers to target class.
+    if (hasInstanceInitializerDescription()) {
+      lensBuilder.moveMethods(instanceInitializers, newMethodReference);
+    } else if (isSingleton() && !group.hasClassIdField()) {
+      lensBuilder.moveMethod(representative.getReference(), newMethodReference, true);
+    } else {
+      for (ProgramMethod instanceInitializer : instanceInitializers) {
+        DexMethod movedInstanceInitializer =
+            moveInstanceInitializer(classMethodsBuilder, instanceInitializer);
+        lensBuilder.mapMethod(movedInstanceInitializer, movedInstanceInitializer);
+        lensBuilder.recordNewMethodSignature(
+            instanceInitializer.getReference(), movedInstanceInitializer);
+      }
+    }
+
+    // Add a mapping from a synthetic name to the synthetic constructor.
+    DexMethod syntheticMethodReference =
+        getSyntheticMethodReference(classMethodsBuilder, representative);
+    if (!isSingleton() || group.hasClassIdField()) {
+      lensBuilder.recordNewMethodSignature(syntheticMethodReference, newMethodReference, true);
+    }
+
+    // Map each of the instance initializers to the new instance initializer in the graph lens.
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
       List<ExtraParameter> extraParameters = new ArrayList<>();
-      if (instanceInitializers.size() > 1) {
-        int classIdentifier = classIdentifiers.getInt(oldInstanceInitializer.getHolderType());
+      if (needsClassId) {
+        int classIdentifier = classIdentifiers.getInt(instanceInitializer.getHolderType());
         extraParameters.add(new ExtraConstantIntParameter(classIdentifier));
       }
       extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
       lensBuilder.mapMergedConstructor(
-          oldInstanceInitializer.getReference(), newConstructorReference, extraParameters);
+          instanceInitializer.getReference(), newMethodReference, extraParameters);
     }
 
-    // Add a mapping from a synthetic name to the synthetic constructor.
-    lensBuilder.recordNewMethodSignature(bridgeConstructorReference, newConstructorReference);
-
-    classMethodsBuilder.addDirectMethod(newConstructor);
-  }
-
-  private void mergeTrivial(
-      ClassMethodsBuilder classMethodsBuilder, HorizontalClassMergerGraphLens.Builder lensBuilder) {
-    ProgramMethod representative = ListUtils.first(instanceInitializers);
-    DexMethod newMethodReference =
-        representative.getReference().withHolder(group.getTarget(), dexItemFactory);
-    lensBuilder.moveMethods(instanceInitializers, newMethodReference, representative);
-
-    DexEncodedMethod newMethod =
-        representative.getHolder() == group.getTarget()
-            ? representative.getDefinition()
-            : representative.getDefinition().toTypeSubstitutedMethod(newMethodReference);
-    fixupAccessFlagsForTrivialMerge(newMethod.getAccessFlags());
-
-    classMethodsBuilder.addDirectMethod(newMethod);
-  }
-
-  private void fixupAccessFlagsForTrivialMerge(MethodAccessFlags accessFlags) {
-    if (!accessFlags.isPublic()) {
-      accessFlags.unsetPrivate();
-      accessFlags.unsetProtected();
-      accessFlags.setPublic();
-    }
+    DexEncodedMethod newInstanceInitializer =
+        new DexEncodedMethod(
+            newMethodReference,
+            getNewAccessFlags(),
+            MethodTypeSignature.noSignature(),
+            DexAnnotationSet.empty(),
+            ParameterAnnotationsList.empty(),
+            getNewCode(newMethodReference, syntheticMethodReference, needsClassId, extraNulls),
+            true,
+            getNewClassFileVersion());
+    classMethodsBuilder.addDirectMethod(newInstanceInitializer);
   }
 }
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 ed7e099..b5e485b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
@@ -10,9 +10,11 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerMerger.Builder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
@@ -30,14 +32,15 @@
       List<InstanceInitializerMerger> instanceInitializerMergers,
       Map<InstanceInitializerDescription, InstanceInitializerMerger>
           equivalentInstanceInitializerMergers) {
-    assert equivalentInstanceInitializerMergers.isEmpty();
     this.instanceInitializerMergers = instanceInitializerMergers;
     this.equivalentInstanceInitializerMergers = equivalentInstanceInitializerMergers;
   }
 
   public static InstanceInitializerMergerCollection create(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      Reference2IntMap<DexType> classIdentifiers,
       MergeGroup group,
+      ClassInstanceFieldsMerger instanceFieldsMerger,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode) {
     // Create an instance initializer merger for each group of instance initializers that are
@@ -50,12 +53,16 @@
                 DexEncodedMethod::isInstanceInitializer,
                 instanceInitializer -> {
                   InstanceInitializerDescription description =
-                      InstanceInitializerAnalysis.analyze(instanceInitializer, lensBuilder);
+                      InstanceInitializerAnalysis.analyze(
+                          appView, group, instanceInitializer, instanceFieldsMerger, mode);
                   if (description != null) {
                     buildersByDescription
                         .computeIfAbsent(
                             description,
-                            ignoreKey(() -> new InstanceInitializerMerger.Builder(appView, mode)))
+                            ignoreKey(
+                                () ->
+                                    new InstanceInitializerMerger.Builder(
+                                        appView, classIdentifiers, lensBuilder, mode)))
                         .addEquivalent(instanceInitializer);
                   } else {
                     buildersWithoutDescription.add(instanceInitializer);
@@ -86,7 +93,9 @@
               buildersByProto
                   .computeIfAbsent(
                       instanceInitializer.getDefinition().getProto(),
-                      ignore -> new InstanceInitializerMerger.Builder(appView, mode))
+                      ignore ->
+                          new InstanceInitializerMerger.Builder(
+                              appView, classIdentifiers, lensBuilder, mode))
                   .add(instanceInitializer));
       for (InstanceInitializerMerger.Builder builder : buildersByProto.values()) {
         instanceInitializerMergers.addAll(builder.build(group));
@@ -95,7 +104,8 @@
       buildersWithoutDescription.forEach(
           instanceInitializer ->
               instanceInitializerMergers.addAll(
-                  new InstanceInitializerMerger.Builder(appView, mode)
+                  new InstanceInitializerMerger.Builder(
+                          appView, classIdentifiers, lensBuilder, mode)
                       .add(instanceInitializer)
                       .build(group)));
     }
@@ -110,5 +120,6 @@
 
   public void forEach(Consumer<InstanceInitializerMerger> consumer) {
     instanceInitializerMergers.forEach(consumer);
+    equivalentInstanceInitializerMergers.values().forEach(consumer);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
index f2d8962..13df457 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
@@ -34,6 +34,14 @@
     this.nameComputationInfo = nameComputationInfo;
   }
 
+  public DexReference getItem() {
+    return item;
+  }
+
+  public NameComputationInfo<?> getNameComputationInfo() {
+    return nameComputationInfo;
+  }
+
   @Override
   public boolean isSingleDexItemBasedStringValue() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index aae7abe..4fc6914 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -87,6 +87,10 @@
     return true;
   }
 
+  public static <T> T last(T[] array) {
+    return array[array.length - 1];
+  }
+
   /**
    * Rewrites the input array based on the given function.
    *
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java
index 5d9722b..57108ff 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java
@@ -52,9 +52,8 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              // TODO(b/189296638): Should be 1.
               assertEquals(
-                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("C", "D");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java
index a5d7e26..5297a09 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java
@@ -53,8 +53,8 @@
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
 
-              // TODO(b/189296638): Should be 1, but requires changing the order of arguments at
-              //  constructor call sites.
+              // TODO(b/189296638): Enable constructor merging by changing the constructor
+              // arguments.
               assertEquals(
                   2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
             })
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java
index bfce2d6..7a7a997 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java
@@ -52,9 +52,8 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              // TODO(b/189296638): Should be 1.
               assertEquals(
-                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("CC", "DD");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java
index 5e144b0..3917d56 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
-import org.junit.AssumptionViolatedException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -52,9 +51,8 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              // TODO(b/189296638): Should be 2.
               assertEquals(
-                  3, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
 
               ClassSubject nullArgumentClassSubject =
                   inspector.allClasses().stream()
@@ -63,11 +61,7 @@
                               SyntheticItemsTestUtils.isHorizontalInitializerTypeArgument(
                                   clazz.getOriginalReference()))
                       .findFirst()
-                      // TODO(b/189296638): Should throw RuntimeException.
-                      .orElseThrow(
-                          () ->
-                              new AssumptionViolatedException(
-                                  "Expected Horizontal initializer type argument"));
+                      .orElseThrow(RuntimeException::new);
 
               assertThat(
                   aClassSubject.method("void", "<init>", "java.lang.Object", "int"), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java
index 59756bf..2bbd4e5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java
@@ -50,9 +50,8 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              // TODO(b/189296638): Should be 1.
               assertEquals(
-                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("C", "D");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java
index 8785c85..c45274f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java
@@ -61,8 +61,6 @@
 
   @NeverClassInline
   static class A extends Parent {
-    // When merging this initializer with B(L), it is important that we choose the signature AB(J)
-    // and not AB(I) or AB(Object), or the program will not verify.
     A(K k) {
       super(k);
     }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithPrimitiveAndReferencesParametersTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithPrimitiveAndReferencesParametersTest.java
new file mode 100644
index 0000000..48e6535
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithPrimitiveAndReferencesParametersTest.java
@@ -0,0 +1,92 @@
+// 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.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.KeepUnusedArguments;
+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 com.android.tools.r8.classmerging.horizontal.EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.A;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EquivalentConstructorsWithPrimitiveAndReferencesParametersTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public EquivalentConstructorsWithPrimitiveAndReferencesParametersTest(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).assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableUnusedArgumentAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A(42).m1();
+      new B(new Object()).m2();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+    @KeepUnusedArguments
+    A(int i) {}
+
+    @NeverInline
+    void m1() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+    @KeepUnusedArguments
+    B(Object o) {}
+
+    @NeverInline
+    void m2() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java
index d429057..4a66189 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java
@@ -54,9 +54,8 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              // TODO(b/189296638): Should be 1.
               assertEquals(
-                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("C", "D");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java
index a8b85e0..090699b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java
@@ -52,9 +52,8 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              // TODO(b/189296638): Should be 1.
               assertEquals(
-                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("C", "D");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
index e785238..c39b7b5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
@@ -44,6 +44,9 @@
         .addKeepMainRule(Main.class)
         .addKeepAttributeLineNumberTable()
         .addKeepAttributeSourceFile()
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
         .enableNoVerticalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -52,22 +55,22 @@
         .inspectStackTrace(
             (stackTrace, codeInspector) -> {
               assertThat(codeInspector.clazz(A.class), isPresent());
-                StackTrace expectedStackTraceWithMergedConstructor =
-                    StackTrace.builder()
-                        .add(expectedStackTrace)
-                        .add(
-                            2,
-                            StackTraceLine.builder()
-                                .setClassName(A.class.getTypeName())
-                                // TODO(b/124483578): The synthetic method should not be part of the
-                                //  retraced stack trace.
-                                .setMethodName("$r8$init$bridge")
-                                .setFileName(getClass().getSimpleName() + ".java")
-                                .setLineNumber(0)
-                                .build())
-                        .build();
-                assertThat(stackTrace, isSame(expectedStackTraceWithMergedConstructor));
-                assertThat(codeInspector.clazz(B.class), not(isPresent()));
+              StackTrace expectedStackTraceWithMergedConstructor =
+                  StackTrace.builder()
+                      .add(expectedStackTrace)
+                      .add(
+                          2,
+                          StackTraceLine.builder()
+                              .setClassName(A.class.getTypeName())
+                              // TODO(b/124483578): The synthetic method should not be part of the
+                              //  retraced stack trace.
+                              .setMethodName("$r8$init$synthetic")
+                              .setFileName(getClass().getSimpleName() + ".java")
+                              .setLineNumber(0)
+                              .build())
+                      .build();
+              assertThat(stackTrace, isSame(expectedStackTraceWithMergedConstructor));
+              assertThat(codeInspector.clazz(B.class), not(isPresent()));
             });
   }
 
@@ -82,12 +85,18 @@
 
   @NeverClassInline
   static class A extends Parent {
-    A() {}
+    A() {
+      // To avoid constructor equivalence.
+      System.out.println("A");
+    }
   }
 
   @NeverClassInline
   static class B extends Parent {
-    B() {}
+    B() {
+      // To avoid constructor equivalence.
+      System.out.println("B");
+    }
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorWithEquivalenceStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorWithEquivalenceStackTraceTest.java
new file mode 100644
index 0000000..9b5b87d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorWithEquivalenceStackTraceTest.java
@@ -0,0 +1,107 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.classmerging.horizontal.MergedConstructorStackTraceTest.A;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MergedConstructorWithEquivalenceStackTraceTest extends HorizontalClassMergingTestBase {
+
+  public StackTrace expectedStackTrace;
+
+  public MergedConstructorWithEquivalenceStackTraceTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    expectedStackTrace =
+        testForJvm()
+            .addTestClasspath()
+            .run(CfRuntime.getSystemRuntime(), Main.class)
+            .assertFailure()
+            .map(StackTrace::extractFromJvm);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepAttributeLineNumberTable()
+        .addKeepAttributeSourceFile()
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNoVerticalClassMergingAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .inspectStackTrace(
+            (stackTrace, codeInspector) -> {
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              StackTrace expectedStackTraceWithMergedConstructor =
+                  StackTrace.builder()
+                      .add(expectedStackTrace)
+                      // TODO(b/124483578): Stack trace lines from the merging of equivalent
+                      //  constructors should retrace to the set of lines from each of the
+                      //  individual source constructors.
+                      .map(1, stackTraceLine -> stackTraceLine.builderOf().setLineNumber(0).build())
+                      // TODO(b/124483578): The synthetic method should not be part of the retraced
+                      //  stack trace.
+                      .add(
+                          2,
+                          StackTraceLine.builder()
+                              .setClassName(A.class.getTypeName())
+                              .setMethodName("$r8$init$synthetic")
+                              .setFileName(getClass().getSimpleName() + ".java")
+                              .setLineNumber(0)
+                              .build())
+                      .build();
+              assertThat(stackTrace, isSame(expectedStackTraceWithMergedConstructor));
+              assertThat(codeInspector.clazz(B.class), not(isPresent()));
+            });
+  }
+
+  @NoVerticalClassMerging
+  static class Parent {
+    Parent() {
+      if (System.currentTimeMillis() >= 0) {
+        throw new RuntimeException();
+      }
+    }
+  }
+
+  @NeverClassInline
+  static class A extends Parent {
+    A() {}
+  }
+
+  @NeverClassInline
+  static class B extends Parent {
+    B() {}
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new A();
+      new B();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
index fdb7faa..a4c52ad 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
@@ -115,15 +115,11 @@
     private List<Path> additionalProgramFiles = new ArrayList<>();
     private Consumer<InternalOptions> optionsModifier = ConsumerUtils.emptyConsumer();
     private Path desugarJDKLibs = ToolHelper.getDesugarJDKLibs();
-    private Path desugarJDKLibsConfiguration = ToolHelper.DESUGAR_LIB_CONVERSIONS;
+    private Path desugarJDKLibsConfiguration = null;
     private StringResource desugaredLibraryConfiguration =
         StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting());
     private List<Path> libraryFiles = new ArrayList<>();
 
-    public static L8TestBuilder builder(AndroidApiLevel apiLevel, TemporaryFolder temp) {
-      return new L8TestBuilder(apiLevel, temp);
-    }
-
     private L8TestBuilder(AndroidApiLevel apiLevel, TemporaryFolder temp) {
       this.apiLevel = apiLevel;
       this.state = new TestState(temp);
@@ -239,11 +235,11 @@
     }
 
     private Collection<Path> getProgramFiles() {
-      return ImmutableList.<Path>builder()
-          .add(desugarJDKLibs)
-          .add(desugarJDKLibsConfiguration)
-          .addAll(additionalProgramFiles)
-          .build();
+      ImmutableList.Builder<Path> builder = ImmutableList.<Path>builder().add(desugarJDKLibs);
+      if (desugarJDKLibsConfiguration != null) {
+        builder.add(desugarJDKLibsConfiguration);
+      }
+      return builder.addAll(additionalProgramFiles).build();
     }
 
     private Collection<Path> getLibraryFiles() {
@@ -300,6 +296,7 @@
               },
               L8TestBuilder::setDebug)
           .addOptionsModifier(optionsModifier)
+          .setDesugarJDKLibsConfiguration(ToolHelper.DESUGAR_LIB_CONVERSIONS)
           // If we compile extended library here, it means we use TestNG. TestNG requires
           // annotations, hence we disable annotation removal. This implies that extra warnings are
           // generated.
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeV1620Test.java b/src/test/java/com/android/tools/r8/internal/YouTubeV1620Test.java
new file mode 100644
index 0000000..c1182bd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeV1620Test.java
@@ -0,0 +1,191 @@
+// 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.internal;
+
+import static com.android.tools.r8.ToolHelper.isLocalDevelopment;
+import static com.android.tools.r8.ToolHelper.shouldRunSlowTests;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.assertRewrittenProtoSchemasMatch;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepAllProtosRule;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepDynamicMethodSignatureRule;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepNewMessageInfoSignatureRule;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.L8TestCompileResult;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutionException;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class YouTubeV1620Test extends YouTubeCompilationTestBase {
+
+  private static final int MAX_APPLICATION_SIZE = 29750000;
+  private static final int MAX_DESUGARED_LIBRARY_SIZE = 425000;
+
+  private final TestParameters parameters;
+
+  private final Path dumpDirectory = Paths.get("YouTubeV1620-" + System.currentTimeMillis());
+  private final Reporter reporter = new Reporter();
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(Version.V9_0_0)
+        .withApiLevel(AndroidApiLevel.L)
+        .build();
+  }
+
+  public YouTubeV1620Test(TestParameters parameters) {
+    super(16, 20, parameters.getApiLevel());
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    assumeTrue(isLocalDevelopment());
+    assumeTrue(shouldRunSlowTests());
+
+    KeepRuleConsumer keepRuleConsumer = new PresentKeepRuleConsumer();
+    R8TestCompileResult r8CompileResult = compileApplicationWithR8(keepRuleConsumer);
+    L8TestCompileResult l8CompileResult = compileDesugaredLibraryWithL8(keepRuleConsumer);
+
+    inspect(r8CompileResult, l8CompileResult);
+
+    if (isLocalDevelopment()) {
+      dump(r8CompileResult, l8CompileResult);
+    }
+  }
+
+  @Test
+  public void testProtoRewriting() throws Exception {
+    assumeTrue(shouldRunSlowTests());
+
+    StringConsumer keepRuleConsumer = StringConsumer.emptyConsumer();
+    R8TestCompileResult r8CompileResult =
+        compileApplicationWithR8(
+            keepRuleConsumer,
+            builder ->
+                builder
+                    .addKeepRules(
+                        keepAllProtosRule(),
+                        keepDynamicMethodSignatureRule(),
+                        keepNewMessageInfoSignatureRule())
+                    .allowCheckDiscardedErrors(true));
+    assertRewrittenProtoSchemasMatch(
+        new CodeInspector(getProgramFiles()), r8CompileResult.inspector());
+  }
+
+  @After
+  public void teardown() {
+    reporter.failIfPendingErrors();
+  }
+
+  private R8TestCompileResult compileApplicationWithR8(StringConsumer keepRuleConsumer)
+      throws IOException, CompilationFailedException {
+    return compileApplicationWithR8(keepRuleConsumer, ThrowableConsumer.empty());
+  }
+
+  private R8TestCompileResult compileApplicationWithR8(
+      StringConsumer keepRuleConsumer, ThrowableConsumer<R8FullTestBuilder> configuration)
+      throws IOException, CompilationFailedException {
+    return testForR8(parameters.getBackend())
+        .addProgramFiles(getProgramFiles())
+        .addLibraryFiles(getLibraryFiles())
+        .addKeepRuleFiles(getKeepRuleFiles())
+        .addDontWarn("android.app.Activity$TranslucentConversionListener")
+        .allowDiagnosticMessages()
+        .allowUnusedDontWarnPatterns()
+        .allowUnusedProguardConfigurationRules()
+        .apply(configuration)
+        .setMinApi(getApiLevel())
+        .enableCoreLibraryDesugaring(
+            getApiLevel(),
+            keepRuleConsumer,
+            StringResource.fromFile(getDesugaredLibraryConfiguration()))
+        .compile()
+        .assertAllInfoMessagesMatch(
+            anyOf(
+                containsString("Ignoring option: -optimizations"),
+                containsString("Proguard configuration rule does not match anything"),
+                containsString("Invalid signature")))
+        .apply(this::printProtoStats);
+  }
+
+  private L8TestCompileResult compileDesugaredLibraryWithL8(KeepRuleConsumer keepRuleConsumer)
+      throws CompilationFailedException, IOException, ExecutionException {
+    return testForL8(getApiLevel())
+        .setDesugaredLibraryConfiguration(getDesugaredLibraryConfiguration())
+        .setDesugarJDKLibs(getDesugaredLibraryJDKLibs())
+        .addGeneratedKeepRules(keepRuleConsumer.get())
+        .addKeepRuleFiles(getDesugaredLibraryKeepRuleFiles())
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+        .compile();
+  }
+
+  private void inspect(R8TestCompileResult r8CompileResult, L8TestCompileResult l8CompileResult)
+      throws IOException, ResourceException {
+    r8CompileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors();
+    l8CompileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors();
+
+    int applicationSize = r8CompileResult.getApp().applicationSize();
+    if (applicationSize > MAX_APPLICATION_SIZE) {
+      reporter.error(
+          "Expected application size to be <="
+              + MAX_APPLICATION_SIZE
+              + ", but was "
+              + applicationSize);
+    }
+
+    int desugaredLibrarySize = l8CompileResult.getApp().applicationSize();
+    if (desugaredLibrarySize > MAX_DESUGARED_LIBRARY_SIZE) {
+      reporter.error(
+          "Expected desugared library size to be <="
+              + MAX_DESUGARED_LIBRARY_SIZE
+              + ", but was "
+              + desugaredLibrarySize);
+    }
+
+    if (isLocalDevelopment()) {
+      System.out.println("Dex size (application, excluding desugared library): " + applicationSize);
+      System.out.println("Dex size (desugared library): " + desugaredLibrarySize);
+      System.out.println("Dex size (total): " + (applicationSize + desugaredLibrarySize));
+    }
+  }
+
+  private void dump(R8TestCompileResult r8CompileResult, L8TestCompileResult l8CompileResult)
+      throws IOException {
+    assertTrue(isLocalDevelopment());
+    Files.createDirectories(dumpDirectory);
+    r8CompileResult
+        .writeToDirectory(dumpDirectory)
+        .writeProguardMap(dumpDirectory.resolve("mapping.txt"));
+    l8CompileResult
+        .writeSingleDexOutputToFile(dumpDirectory.resolve("classes5.dex"))
+        .writeGeneratedKeepRules(dumpDirectory.resolve("l8-keep.txt"))
+        .writeProguardMap(dumpDirectory.resolve("l8-mapping.txt"));
+  }
+}