Handle non-standard <init> calls in class inliner and merger

This CL aborts class inlining in case of the following code pattern (where B is a subtype of A), instead of failing with an assertion error:
  new-instance v0, B
  invoke-direct A.<init>, v0

Furthermore, in case A is merged into B by the vertical class merger, compilation fails with an error telling the user to add a -keep rule for class A. The motivation for this is that the vertical class merger cannot easily recognize the above code pattern, since it runs prior to IR construction. Therefore, we currently allow merging A and B although this will lead to invalid code, because this code pattern does generally not occur in practice (it leads to a verification error on the JVM, but not on Art).

Bug: 116282409
Change-Id: Ia57badc8cb52e3164421ddd908918c7b4699e415
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 7bed8ae..b2743bc 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -364,16 +364,17 @@
         appView.setGraphLense(new MemberRebindingAnalysis(appViewWithLiveness).run());
         if (options.enableVerticalClassMerging) {
           timing.begin("ClassMerger");
-          VerticalClassMerger classMerger =
+          VerticalClassMerger verticalClassMerger =
               new VerticalClassMerger(
                   application, appViewWithLiveness, executorService, options, timing);
-          appView.setGraphLense(classMerger.run());
+          appView.setGraphLense(verticalClassMerger.run());
+          appView.setVerticallyMergedClasses(verticalClassMerger.getMergedClasses());
           timing.end();
           application = application.asDirect().rewrittenWithLense(appView.getGraphLense());
           appViewWithLiveness.setAppInfo(
               appViewWithLiveness
                   .getAppInfo()
-                  .prunedCopyFrom(application, classMerger.getRemovedClasses())
+                  .prunedCopyFrom(application, verticalClassMerger.getRemovedClasses())
                   .rewrittenWithLense(application.asDirect(), appView.getGraphLense()));
         }
         // Collect switch maps and ordinals maps.
@@ -389,11 +390,8 @@
       Set<DexCallSite> desugaredCallSites;
       CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
       try {
-        IRConverter converter =
-            new IRConverter(
-                appView.getAppInfo(), options, timing, printer, appView.getGraphLense());
+        IRConverter converter = new IRConverter(appView, options, timing, printer);
         application = converter.optimize(application, executorService);
-        appView.setGraphLense(converter.getGraphLense());
         desugaredCallSites = converter.getDesugaredCallSites();
       } finally {
         timing.end();
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 64a4ce4..47671c6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -5,12 +5,14 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.VerticalClassMerger.VerticallyMergedClasses;
 
 public class AppView<T extends AppInfo> {
 
   private T appInfo;
   private final DexItemFactory dexItemFactory;
   private GraphLense graphLense;
+  private VerticallyMergedClasses verticallyMergedClasses;
 
   public AppView(T appInfo, GraphLense graphLense) {
     this.appInfo = appInfo;
@@ -18,6 +20,11 @@
     this.graphLense = graphLense;
   }
 
+  public T appInfo() {
+    return appInfo;
+  }
+
+  // TODO(christofferqa): Remove and carry out renamings in a separate CL.
   public T getAppInfo() {
     return appInfo;
   }
@@ -26,10 +33,20 @@
     this.appInfo = appInfo;
   }
 
+  public DexItemFactory dexItemFactory() {
+    return dexItemFactory;
+  }
+
+  // TODO(christofferqa): Remove and carry out renamings in a separate CL.
   public DexItemFactory getDexItemFactory() {
     return dexItemFactory;
   }
 
+  public GraphLense graphLense() {
+    return graphLense;
+  }
+
+  // TODO(christofferqa): Remove and carry out renamings in a separate CL.
   public GraphLense getGraphLense() {
     return graphLense;
   }
@@ -38,6 +55,16 @@
     this.graphLense = graphLense;
   }
 
+  // Get the result of vertical class merging. Returns null if vertical class merging has not been
+  // run.
+  public VerticallyMergedClasses verticallyMergedClasses() {
+    return verticallyMergedClasses;
+  }
+
+  public void setVerticallyMergedClasses(VerticallyMergedClasses verticallyMergedClasses) {
+    this.verticallyMergedClasses = verticallyMergedClasses;
+  }
+
   public AppView<AppInfoWithLiveness> withLiveness() {
     return new AppViewWithLiveness();
   }
@@ -49,6 +76,11 @@
     }
 
     @Override
+    public AppInfoWithLiveness appInfo() {
+      return AppView.this.appInfo().withLiveness();
+    }
+
+    @Override
     public AppInfoWithLiveness getAppInfo() {
       return AppView.this.getAppInfo().withLiveness();
     }
@@ -61,11 +93,21 @@
     }
 
     @Override
+    public DexItemFactory dexItemFactory() {
+      return AppView.this.dexItemFactory();
+    }
+
+    @Override
     public DexItemFactory getDexItemFactory() {
       return AppView.this.dexItemFactory;
     }
 
     @Override
+    public GraphLense graphLense() {
+      return AppView.this.graphLense();
+    }
+
+    @Override
     public GraphLense getGraphLense() {
       return AppView.this.getGraphLense();
     }
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index f1c1700..68f4c95 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -86,7 +86,6 @@
 
   public LazyCfCode(
       DexMethod method, Origin origin, ReparseContext context, JarApplicationReader application) {
-
     this.method = method;
     this.origin = origin;
     this.context = context;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 1ee2f6b..7b9ce55 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -147,8 +148,7 @@
 
   public static CallGraph build(
       DexApplication application,
-      AppInfoWithLiveness appInfo,
-      GraphLense graphLense,
+      AppView<AppInfoWithLiveness> appView,
       InternalOptions options,
       Timing timing) {
     CallGraph graph = new CallGraph(options);
@@ -157,7 +157,7 @@
     for (DexClass clazz : classes) {
       for (DexEncodedMethod method : clazz.allMethodsSorted()) {
         Node node = graph.ensureMethodNode(method);
-        InvokeExtractor extractor = new InvokeExtractor(appInfo, graphLense, node, graph);
+        InvokeExtractor extractor = new InvokeExtractor(appView, node, graph);
         method.registerCodeReferences(extractor);
       }
     }
@@ -169,7 +169,7 @@
     timing.end();
     assert cycleEliminator.breakCycles() == 0; // This time the cycles should be gone.
 
-    graph.fillCallSiteSets(appInfo);
+    graph.fillCallSiteSets(appView.appInfo());
     return graph;
   }
 
@@ -232,8 +232,7 @@
     });
     Set<DexEncodedMethod> methods =
         leaves.stream().map(x -> x.method).collect(Collectors.toCollection(LinkedHashSet::new));
-    // TODO(b/116282409): Resolve why shuffling makes art.none.r8.Art800_smaliTest flaky.
-    return methods;
+    return shuffle.order(methods);
   }
 
   public static class CycleEliminator {
@@ -478,16 +477,15 @@
 
   private static class InvokeExtractor extends UseRegistry {
 
-    AppInfoWithLiveness appInfo;
-    GraphLense graphLense;
-    Node caller;
-    CallGraph graph;
+    private final AppInfoWithLiveness appInfo;
+    private final GraphLense graphLense;
+    private final Node caller;
+    private final CallGraph graph;
 
-    InvokeExtractor(AppInfoWithLiveness appInfo, GraphLense graphLense, Node caller,
-        CallGraph graph) {
-      super(appInfo.dexItemFactory);
-      this.appInfo = appInfo;
-      this.graphLense = graphLense;
+    InvokeExtractor(AppView<AppInfoWithLiveness> appView, Node caller, CallGraph graph) {
+      super(appView.dexItemFactory());
+      this.appInfo = appView.appInfo();
+      this.graphLense = appView.graphLense();
       this.caller = caller;
       this.graph = graph;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index f4a1e4e..99d92b4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotation;
@@ -91,8 +92,10 @@
 
   private static final int PEEPHOLE_OPTIMIZATION_PASSES = 2;
 
-  private final Timing timing;
   public final AppInfo appInfo;
+  public final AppView<? extends AppInfoWithSubtyping> appView;
+
+  private final Timing timing;
   private final Outliner outliner;
   private final StringConcatRewriter stringConcatRewriter;
   private final LambdaRewriter lambdaRewriter;
@@ -103,7 +106,6 @@
   private final ClassStaticizer classStaticizer;
   private final InternalOptions options;
   private final CfgPrinter printer;
-  private GraphLense graphLense;
   private final CodeRewriter codeRewriter;
   private final MemberValuePropagation memberValuePropagation;
   private final LensCodeRewriter lensCodeRewriter;
@@ -124,19 +126,20 @@
   // the current class being optimized.
   private ConcurrentHashMap<DexType, DexProgramClass> cachedClasses = new ConcurrentHashMap<>();
 
+  // The argument `appView` is only available when full program optimizations are allowed
+  // (i.e., when running R8).
   private IRConverter(
       AppInfo appInfo,
       InternalOptions options,
       Timing timing,
       CfgPrinter printer,
-      GraphLense graphLense,
-      boolean enableWholeProgramOptimizations) {
+      AppView<? extends AppInfoWithSubtyping> appView) {
     assert appInfo != null;
     assert options != null;
     assert options.programConsumer != null;
     this.timing = timing != null ? timing : new Timing("internal");
     this.appInfo = appInfo;
-    this.graphLense = graphLense != null ? graphLense : GraphLense.getIdentityLense();
+    this.appView = appView;
     this.options = options;
     this.printer = printer;
     this.codeRewriter = new CodeRewriter(this, libraryMethodsReturningReceiver(), options);
@@ -155,16 +158,16 @@
             ? new CovariantReturnTypeAnnotationTransformer(this, appInfo.dexItemFactory)
             : null;
     this.stringOptimizer = new StringOptimizer();
-    this.enableWholeProgramOptimizations = enableWholeProgramOptimizations;
+    this.enableWholeProgramOptimizations = appView != null;
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasLiveness();
       this.nonNullTracker = new NonNullTracker();
-      this.inliner = new Inliner(this, options);
+      this.inliner = new Inliner(appView.withLiveness(), this, options);
       this.outliner = new Outliner(appInfo.withLiveness(), options, this);
       this.memberValuePropagation =
           options.enableValuePropagation ?
               new MemberValuePropagation(appInfo.withLiveness()) : null;
-      this.lensCodeRewriter = new LensCodeRewriter(graphLense, appInfo.withSubtyping());
+      this.lensCodeRewriter = new LensCodeRewriter(appView, options);
       if (appInfo.hasLiveness()) {
         if (!appInfo.withLiveness().identifierNameStrings.isEmpty() && options.enableMinification) {
           this.identifierNameStringMarker =
@@ -196,13 +199,8 @@
         ? new ClassStaticizer(appInfo.withLiveness(), this) : null;
   }
 
-  public void setGraphLense(GraphLense graphLense) {
-    assert graphLense != null;
-    this.graphLense = graphLense;
-  }
-
-  public GraphLense getGraphLense() {
-    return graphLense;
+  public GraphLense graphLense() {
+    return appView != null ? appView.graphLense() : GraphLense.getIdentityLense();
   }
 
   public Set<DexCallSite> getDesugaredCallSites() {
@@ -216,33 +214,26 @@
   /**
    * Create an IR converter for processing methods with full program optimization disabled.
    */
-  public IRConverter(
-      AppInfo appInfo,
-      InternalOptions options) {
-    this(appInfo, options, null, null, null, false);
+  public IRConverter(AppInfo appInfo, InternalOptions options) {
+    this(appInfo, options, null, null, null);
   }
 
   /**
    * Create an IR converter for processing methods with full program optimization disabled.
    */
-  public IRConverter(
-      AppInfo appInfo,
-      InternalOptions options,
-      Timing timing,
-      CfgPrinter printer) {
-    this(appInfo, options, timing, printer, null, false);
+  public IRConverter(AppInfo appInfo, InternalOptions options, Timing timing, CfgPrinter printer) {
+    this(appInfo, options, timing, printer, null);
   }
 
   /**
    * Create an IR converter for processing methods with full program optimization enabled.
    */
   public IRConverter(
-      AppInfoWithSubtyping appInfo,
+      AppView<AppInfoWithSubtyping> appView,
       InternalOptions options,
       Timing timing,
-      CfgPrinter printer,
-      GraphLense graphLense) {
-    this(appInfo, options, timing, printer, graphLense, true);
+      CfgPrinter printer) {
+    this(appView.getAppInfo(), options, timing, printer, appView);
   }
 
   private boolean enableInterfaceMethodDesugaring() {
@@ -472,8 +463,7 @@
     OptimizationFeedback directFeedback = new OptimizationFeedbackDirect();
     {
       timing.begin("Build call graph");
-      CallGraph callGraph =
-          CallGraph.build(application, appInfo.withLiveness(), graphLense, options, timing);
+      CallGraph callGraph = CallGraph.build(application, appView.withLiveness(), options, timing);
       timing.end();
       timing.begin("IR conversion phase 1");
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler =
@@ -545,6 +535,7 @@
   private void forEachSelectedOutliningMethod(
       ExecutorService executorService, BiConsumer<IRCode, DexEncodedMethod> consumer)
       throws ExecutionException {
+    assert appView != null;
     assert !options.skipIR;
     Set<DexEncodedMethod> methods = outliner.getMethodsSelectedForOutlining();
     List<Future<?>> futures = new ArrayList<>();
@@ -554,7 +545,8 @@
               () -> {
                 IRCode code =
                     method.buildIR(
-                        appInfo, graphLense, options, appInfo.originFor(method.method.holder));
+                        appInfo, appView.graphLense(), options,
+                        appInfo.originFor(method.method.holder));
                 assert code != null;
                 assert !method.getCode().isOutlineCode();
                 // Instead of repeating all the optimizations of rewriteCode(), only run the
@@ -562,7 +554,7 @@
                 // StringBuilder/StringBuffer method invocations, and removeDeadCode() to remove
                 // unused out-values.
                 codeRewriter.rewriteMoveResult(code);
-                DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
+                DeadCodeRemover.removeDeadCode(code, codeRewriter, appView.graphLense(), options);
                 consumer.accept(code, method);
                 return null;
               }));
@@ -712,7 +704,7 @@
       return;
     }
     IRCode code =
-        method.buildIR(appInfo, graphLense, options, appInfo.originFor(method.method.holder));
+        method.buildIR(appInfo, graphLense(), options, appInfo.originFor(method.method.holder));
     if (code == null) {
       feedback.markProcessed(method, ConstraintWithTarget.NEVER);
       return;
@@ -742,7 +734,7 @@
       if (lensCodeRewriter != null) {
         lensCodeRewriter.rewrite(code, method);
       } else {
-        assert graphLense.isIdentityLense();
+        assert graphLense().isIdentityLense();
       }
     }
 
@@ -774,8 +766,7 @@
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       assert !options.debug;
       new TypeAnalysis(appInfo, method).widening(method, code);
-      inliner.performInlining(
-          method, code, isProcessedConcurrently, callSiteInformation);
+      inliner.performInlining(method, code, isProcessedConcurrently, callSiteInformation);
     }
     if (!options.debug) {
       stringOptimizer.computeConstStringLength(code, appInfo.dexItemFactory);
@@ -813,7 +804,7 @@
     // Dead code removal. Performed after simplifications to remove code that becomes dead
     // as a result of those simplifications. The following optimizations could reveal more
     // dead code which is removed right before register allocation in performRegisterAllocation.
-    DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
+    DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense(), options);
     assert code.isConsistentSSA();
 
     if (options.enableDesugaring && enableTryWithResourcesDesugaring()) {
@@ -829,7 +820,7 @@
 
     if (classInliner != null) {
       // Class inliner should work before lambda merger, so if it inlines the
-      // lambda, it is not get collected by merger.
+      // lambda, it does not get collected by merger.
       assert options.enableInlining && inliner != null;
       classInliner.processMethodCode(
           appInfo.withLiveness(), codeRewriter, method, code, isProcessedConcurrently,
@@ -948,7 +939,7 @@
   private void finalizeToCf(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     assert !method.getCode().isDexCode();
     CfBuilder builder = new CfBuilder(method, code, options.itemFactory);
-    CfCode result = builder.build(codeRewriter, graphLense, options, appInfo);
+    CfCode result = builder.build(codeRewriter, graphLense(), options, appInfo);
     method.setCode(result);
     markProcessed(method, code, feedback);
   }
@@ -992,7 +983,7 @@
   private RegisterAllocator performRegisterAllocation(IRCode code, DexEncodedMethod method) {
     // Always perform dead code elimination before register allocation. The register allocator
     // does not allow dead code (to make sure that we do not waste registers for unneeded values).
-    DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
+    DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense(), options);
     materializeInstructionBeforeLongOperationsWorkaround(code);
     workaroundForwardingInitializerBug(code);
     LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index ef490f7..283c163 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.graph.UseRegistry.MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY;
 
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -34,6 +35,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeCustom;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMultiNewArray;
 import com.android.tools.r8.ir.code.InvokeNewArray;
@@ -42,6 +44,8 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.VerticalClassMerger.VerticallyMergedClasses;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.ListIterator;
@@ -50,14 +54,19 @@
 
 public class LensCodeRewriter {
 
-  private final GraphLense graphLense;
   private final AppInfoWithSubtyping appInfo;
+  private final GraphLense graphLense;
+  private final VerticallyMergedClasses verticallyMergedClasses;
+  private final InternalOptions options;
 
   private final Map<DexProto, DexProto> protoFixupCache = new ConcurrentHashMap<>();
 
-  public LensCodeRewriter(GraphLense graphLense, AppInfoWithSubtyping appInfo) {
-    this.graphLense = graphLense;
-    this.appInfo = appInfo;
+  public LensCodeRewriter(
+      AppView<? extends AppInfoWithSubtyping> appView, InternalOptions options) {
+    this.appInfo = appView.appInfo();
+    this.graphLense = appView.graphLense();
+    this.verticallyMergedClasses = appView.verticallyMergedClasses();
+    this.options = options;
   }
 
   private Value makeOutValue(Instruction insn, IRCode code) {
@@ -121,6 +130,9 @@
           if (!invokedHolder.isClassType()) {
             continue;
           }
+          if (invoke.isInvokeDirect()) {
+            checkInvokeDirect(method.method, invoke.asInvokeDirect());
+          }
           GraphLenseLookupResult lenseLookup =
               graphLense.lookupMethod(invokedMethod, method, invoke.getType());
           DexMethod actualTarget = lenseLookup.getMethod();
@@ -246,6 +258,49 @@
     assert code.isConsistentSSA();
   }
 
+  // If the given invoke is on the form "invoke-direct A.<init>, v0, ..." and the definition of
+  // value v0 is "new-instance v0, B", where B is a subtype of A (see the Art800 and B116282409
+  // tests), then fail with a compilation error if A has previously been merged into B.
+  //
+  // The motivation for this is that the vertical class merger cannot easily recognize the above
+  // code pattern, since it runs prior to IR construction. Therefore, we currently allow merging
+  // A and B although this will lead to invalid code, because this code pattern does generally
+  // not occur in practice (it leads to a verification error on the JVM, but not on Art).
+  private void checkInvokeDirect(DexMethod method, InvokeDirect invoke) {
+    if (verticallyMergedClasses == null) {
+      // No need to check the invocation.
+      return;
+    }
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+    if (invokedMethod.name != appInfo.dexItemFactory.constructorMethodName) {
+      // Not a constructor call.
+      return;
+    }
+    if (invoke.arguments().isEmpty()) {
+      // The new instance should always be passed to the constructor call, but continue gracefully.
+      return;
+    }
+    Value receiver = invoke.arguments().get(0);
+    if (!receiver.isPhi() && receiver.definition.isNewInstance()) {
+      NewInstance newInstance = receiver.definition.asNewInstance();
+      if (newInstance.clazz != invokedMethod.holder
+          && verticallyMergedClasses.hasBeenMergedIntoSubtype(invokedMethod.holder)) {
+        // Generated code will not work. Fail with a compilation error.
+        throw options.reporter.fatalError(
+            String.format(
+                "Unable to rewrite `invoke-direct %s.<init>(new %s, ...)` in method `%s` after "
+                    + "type `%s` was merged into `%s`. Please add the following rule to your "
+                    + "Proguard configuration file: `-keep,allowobfuscation class %s`.",
+                invokedMethod.holder.toSourceString(),
+                newInstance.clazz,
+                method.toSourceString(),
+                invokedMethod.holder,
+                verticallyMergedClasses.getTargetFor(invokedMethod.holder),
+                invokedMethod.holder.toSourceString()));
+      }
+    }
+  }
+
   private List<DexValue> rewriteBootstrapArgs(
       List<DexValue> bootstrapArgs, DexEncodedMethod method, MethodHandleUse use) {
     List<DexValue> newBoostrapArgs = null;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 045cf52..82e3bff 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -423,9 +423,12 @@
     }
     if (converter.enableWholeProgramOptimizations &&
         (!processor.methodsWithMovedCode.isEmpty() || !processor.movedMethods.isEmpty())) {
-      converter.setGraphLense(
-          new InterfaceMethodDesugaringLense(processor.movedMethods,
-              processor.methodsWithMovedCode, converter.getGraphLense(), factory));
+      converter.appView.setGraphLense(
+          new InterfaceMethodDesugaringLense(
+              processor.movedMethods,
+              processor.methodsWithMovedCode,
+              converter.appView.graphLense(),
+              factory));
     }
     return processor.syntheticClasses;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index c87c849..ffca4d1 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -174,8 +174,8 @@
       lambdaClass.target.ensureAccessibility();
     }
     if (converter.enableWholeProgramOptimizations && !methodMapping.isEmpty()) {
-      converter.setGraphLense(
-        new LambdaRewriterGraphLense(methodMapping, converter.getGraphLense(), factory));
+      converter.appView.setGraphLense(
+          new LambdaRewriterGraphLense(methodMapping, converter.appView.graphLense(), factory));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
index 7d6dd7b..e1e534e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
@@ -92,7 +92,7 @@
   }
 
   public static boolean isSynthesizedCloseResourceMethod(DexMethod method, IRConverter converter) {
-    DexMethod original = converter.getGraphLense().getOriginalMethodSignature(method);
+    DexMethod original = converter.graphLense().getOriginalMethodSignature(method);
     assert original != null;
     // We consider all methods of *any* class with expected name and signature
     // to be synthesized by java 9 compiler for try-with-resources, reasoning:
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index e655c0e..fc11b78 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -67,10 +67,11 @@
   }
 
   private DexEncodedMethod validateCandidate(InvokeMethod invoke, DexType invocationContext) {
-    DexEncodedMethod candidate = invoke.lookupSingleTarget(inliner.appInfo, invocationContext);
+    DexEncodedMethod candidate =
+        invoke.lookupSingleTarget(inliner.appView.appInfo(), invocationContext);
     if ((candidate == null)
         || (candidate.getCode() == null)
-        || inliner.appInfo.definitionFor(candidate.method.getHolder()).isLibraryClass()) {
+        || inliner.appView.appInfo().definitionFor(candidate.method.getHolder()).isLibraryClass()) {
       if (info != null) {
         info.exclude(invoke, "No inlinee");
       }
@@ -90,12 +91,12 @@
 
   private Reason computeInliningReason(DexEncodedMethod target) {
     if (target.getOptimizationInfo().forceInline()
-        || (inliner.appInfo.hasLiveness()
-            && inliner.appInfo.withLiveness().forceInline.contains(target.method))) {
+        || (inliner.appView.appInfo().hasLiveness()
+            && inliner.appView.withLiveness().appInfo().forceInline.contains(target.method))) {
       return Reason.FORCE;
     }
-    if (inliner.appInfo.hasLiveness()
-        && inliner.appInfo.withLiveness().alwaysInline.contains(target.method)) {
+    if (inliner.appView.appInfo().hasLiveness()
+        && inliner.appView.withLiveness().appInfo().alwaysInline.contains(target.method)) {
       return Reason.ALWAYS;
     }
     if (callSiteInformation.hasSingleCallSite(target)) {
@@ -117,7 +118,7 @@
     if (method.method.getHolder() == targetHolder) {
       return true;
     }
-    DexClass clazz = inliner.appInfo.definitionFor(targetHolder);
+    DexClass clazz = inliner.appView.appInfo().definitionFor(targetHolder);
     assert clazz != null;
     if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
       return true;
@@ -135,7 +136,7 @@
    * methods.
    */
   private boolean classInitializationHasNoSideffects(DexType classToCheck) {
-    DexClass clazz = inliner.appInfo.definitionFor(classToCheck);
+    DexClass clazz = inliner.appView.appInfo().definitionFor(classToCheck);
     if ((clazz == null)
         || clazz.hasNonTrivialClassInitializer()
         || clazz.defaultValuesForStaticFieldsMayTriggerAllocation()) {
@@ -191,7 +192,7 @@
       return false;
     }
 
-    DexClass holder = inliner.appInfo.definitionFor(candidate.method.getHolder());
+    DexClass holder = inliner.appView.appInfo().definitionFor(candidate.method.getHolder());
     if (holder.isInterface()) {
       // Art978_virtual_interfaceTest correctly expects an IncompatibleClassChangeError exception at
       // runtime.
@@ -271,12 +272,12 @@
       if (info != null) {
         info.exclude(invoke, "receiver for candidate can be null");
       }
-      assert !inliner.appInfo.forceInline.contains(candidate.method);
+      assert !inliner.appView.appInfo().forceInline.contains(candidate.method);
       return null;
     }
 
     Reason reason = computeInliningReason(candidate);
-    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
+    if (!candidate.isInliningCandidate(method, reason, inliner.appView.appInfo())) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -303,7 +304,7 @@
 
     Reason reason = computeInliningReason(candidate);
     // Determine if this should be inlined no matter how big it is.
-    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
+    if (!candidate.isInliningCandidate(method, reason, inliner.appView.appInfo())) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -388,7 +389,7 @@
       }
       assert IteratorUtils.peekNext(blockIterator) == state;
       // TODO(b/72693244): could be done when Value is created.
-      new TypeAnalysis(inliner.appInfo, code.method).narrowing(nonNullValues);
+      new TypeAnalysis(inliner.appView.appInfo(), code.method).narrowing(nonNullValues);
     }
     // TODO(b/72693244): need a test where refined env in inlinee affects the caller.
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index fcb7293..000eac2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -6,13 +6,13 @@
 import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -47,8 +47,8 @@
   // Threshold found empirically by testing on GMS Core.
   private static final int CONTROL_FLOW_RESOLUTION_BLOCKS_THRESHOLD = 15;
 
+  protected final AppView<? extends AppInfoWithLiveness> appView;
   private final IRConverter converter;
-  protected final AppInfoWithLiveness appInfo;
   final InternalOptions options;
 
   // State for inlining methods which are known to be called twice.
@@ -59,11 +59,14 @@
 
   private final Set<DexMethod> blackList = Sets.newIdentityHashSet();
 
-  public Inliner(IRConverter converter, InternalOptions options) {
+  public Inliner(
+      AppView<? extends AppInfoWithLiveness> appView,
+      IRConverter converter,
+      InternalOptions options) {
+    this.appView = appView;
     this.converter = converter;
-    this.appInfo = converter.appInfo.withLiveness();
     this.options = options;
-    fillInBlackList(appInfo);
+    fillInBlackList(appView.appInfo());
   }
 
   private void fillInBlackList(AppInfoWithLiveness appInfo) {
@@ -73,7 +76,7 @@
 
   public boolean isBlackListed(DexEncodedMethod method) {
     return blackList.contains(method.method)
-        || appInfo.neverInline.contains(method.method)
+        || appView.appInfo().neverInline.contains(method.method)
         || TwrCloseResourceRewriter.isSynthesizedCloseResourceMethod(method.method, converter);
   }
 
@@ -89,7 +92,7 @@
 
   public ConstraintWithTarget computeInliningConstraint(IRCode code, DexEncodedMethod method) {
     ConstraintWithTarget result = ConstraintWithTarget.ALWAYS;
-    InliningConstraints inliningConstraints = new InliningConstraints(appInfo);
+    InliningConstraints inliningConstraints = new InliningConstraints(appView.appInfo());
     InstructionIterator it = code.instructionIterator();
     while (it.hasNext()) {
       Instruction instruction = it.next();
@@ -100,7 +103,7 @@
         break;
       }
       // TODO(b/111080693): we may need to collect all meaningful constraints.
-      result = ConstraintWithTarget.meet(result, state, appInfo);
+      result = ConstraintWithTarget.meet(result, state, appView.appInfo());
     }
     return result;
   }
@@ -110,7 +113,7 @@
       return false;
     }
     // The class needs also to be visible for us to have access.
-    DexClass targetClass = appInfo.definitionFor(target.method.holder);
+    DexClass targetClass = appView.appInfo().definitionFor(target.method.holder);
     return isVisibleWithFlags(target.method.holder, method.method.holder, targetClass.accessFlags);
   }
 
@@ -122,7 +125,7 @@
       return target == context;
     }
     if (flags.isProtected()) {
-      return context.isSubtypeOf(target, appInfo) || target.isSamePackage(context);
+      return context.isSubtypeOf(target, appView.appInfo()) || target.isSamePackage(context);
     }
     // package-private
     return target.isSamePackage(context);
@@ -167,7 +170,7 @@
           .collect(Collectors.toList());
       for (DexEncodedMethod method : methods) {
         DexEncodedMethod mappedMethod =
-            converter.getGraphLense().mapDexEncodedMethod(appInfo, method);
+            converter.graphLense().mapDexEncodedMethod(appView.appInfo(), method);
         converter.processMethod(
             mappedMethod,
             feedback,
@@ -412,16 +415,16 @@
 
     public IRCode buildInliningIR(
         ValueNumberGenerator generator,
-        AppInfoWithSubtyping appInfo,
-        GraphLense graphLense,
+        AppView<? extends AppInfoWithSubtyping> appView,
         InternalOptions options,
         Position callerPosition) {
       // Build the IR for a yet not processed method, and perform minimal IR processing.
-      Origin origin = appInfo.originFor(target.method.holder);
+      Origin origin = appView.appInfo().originFor(target.method.holder);
       IRCode code =
-          target.buildInliningIR(appInfo, graphLense, options, generator, callerPosition, origin);
+          target.buildInliningIR(
+              appView.appInfo(), appView.graphLense(), options, generator, callerPosition, origin);
       if (!target.isProcessed()) {
-        new LensCodeRewriter(graphLense, appInfo).rewrite(code, target);
+        new LensCodeRewriter(appView, options).rewrite(code, target);
       }
       return code;
     }
@@ -477,7 +480,7 @@
       if (instruction.inValues().contains(unInitializedObject)) {
         if (instruction.isInvokeDirect() && !seenSuperInvoke) {
           DexMethod target = instruction.asInvokeDirect().getInvokedMethod();
-          seenSuperInvoke = appInfo.dexItemFactory.isConstructor(target);
+          seenSuperInvoke = appView.dexItemFactory().isConstructor(target);
           boolean callOnConstructorThatCallsConstructorSameClass =
               calleeMethodHolder == target.holder;
           boolean callOnSupertypeOfThisInConstructor =
@@ -504,7 +507,7 @@
           return false;
         }
         DexField field = instruction.asInstancePut().getField();
-        DexEncodedField target = appInfo.lookupInstanceTarget(field.getHolder(), field);
+        DexEncodedField target = appView.appInfo().lookupInstanceTarget(field.getHolder(), field);
         if (target != null && target.accessFlags.isFinal()) {
           return false;
         }
@@ -594,11 +597,10 @@
             }
             assert invokePosition.callerPosition == null
                 || invokePosition.getOutermostCaller().method
-                    == converter.getGraphLense().getOriginalMethodSignature(method.method);
+                    == converter.graphLense().getOriginalMethodSignature(method.method);
 
             IRCode inlinee =
-                result.buildInliningIR(code.valueNumberGenerator,
-                    appInfo, converter.getGraphLense(), options, invokePosition);
+                result.buildInliningIR(code.valueNumberGenerator, appView, options, invokePosition);
             if (inlinee != null) {
               if (block.hasCatchHandlers() && !(oracle instanceof ForcedInliningOracle)) {
                 // Inlining could lead to an explosion of move-exception and resolution moves. As an
@@ -652,7 +654,7 @@
               strategy.markInlined(inlinee);
               if (!strategy.exceededAllowance() || result.ignoreInstructionBudget()) {
                 iterator.inlineInvoke(
-                    appInfo, code, inlinee, blockIterator, blocksToRemove, downcast);
+                    appView.appInfo(), code, inlinee, blockIterator, blocksToRemove, downcast);
                 strategy.updateTypeInformationIfNeeded(inlinee, blockIterator, block);
 
                 // If we inlined the invoke from a bridge method, it is no longer a bridge method.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index 052c589..e02222f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -865,7 +865,7 @@
       if (outlineMethods.size() >= options.outline.threshold) {
         for (DexEncodedMethod outlineMethod : outlineMethods) {
           methodsSelectedForOutlining.add(
-              converter.getGraphLense().mapDexEncodedMethod(appInfo, outlineMethod));
+              converter.graphLense().mapDexEncodedMethod(appInfo, outlineMethod));
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 9baa647..832fc56 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -457,9 +457,11 @@
       return null;
     }
 
-    assert init.holder == eligibleClass
-        : "Inlined constructor? [invoke: " + initInvoke +
-        ", expected class: " + eligibleClass + "]";
+    if (init.holder != eligibleClass) {
+      // Calling a constructor on a class that is different from the type of the instance.
+      // Gracefully abort class inlining (see the test B116282409).
+      return null;
+    }
 
     DexEncodedMethod definition = findSingleTarget(init, true);
     if (definition == null || isProcessedConcurrently.test(definition)) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index b7c830f..916679d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -362,9 +362,9 @@
     }
 
     if (!methodMapping.isEmpty() || fieldMapping.isEmpty()) {
-      classStaticizer.converter.setGraphLense(
+      classStaticizer.converter.appView.setGraphLense(
           new ClassStaticizerGraphLense(
-              classStaticizer.converter.getGraphLense(),
+              classStaticizer.converter.graphLense(),
               classStaticizer.factory,
               fieldMapping,
               methodMapping,
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 5e03071..9eafe9a 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -88,6 +88,24 @@
  */
 public class VerticalClassMerger {
 
+  public static class VerticallyMergedClasses {
+
+    private final Map<DexType, DexType> mergedClasses;
+
+    private VerticallyMergedClasses(Map<DexType, DexType> mergedClasses) {
+      this.mergedClasses = mergedClasses;
+    }
+
+    public DexType getTargetFor(DexType type) {
+      assert mergedClasses.containsKey(type);
+      return mergedClasses.get(type);
+    }
+
+    public boolean hasBeenMergedIntoSubtype(DexType type) {
+      return mergedClasses.containsKey(type);
+    }
+  }
+
   private enum AbortReason {
     ALREADY_MERGED,
     ALWAYS_INLINE,
@@ -213,6 +231,10 @@
     initializeMergeCandidates(classes);
   }
 
+  public VerticallyMergedClasses getMergedClasses() {
+    return new VerticallyMergedClasses(mergedClasses);
+  }
+
   private void initializeMergeCandidates(Iterable<DexProgramClass> classes) {
     for (DexProgramClass clazz : classes) {
       if (isMergeCandidate(clazz, pinnedTypes) && isStillMergeCandidate(clazz)) {
diff --git a/src/main/java/com/android/tools/r8/utils/Reporter.java b/src/main/java/com/android/tools/r8/utils/Reporter.java
index a64e25d..37f57da 100644
--- a/src/main/java/com/android/tools/r8/utils/Reporter.java
+++ b/src/main/java/com/android/tools/r8/utils/Reporter.java
@@ -55,6 +55,14 @@
   /**
    * @throws AbortException always.
    */
+  public RuntimeException fatalError(String message) {
+    fatalError(new StringDiagnostic(message));
+    throw new Unreachable();
+  }
+
+  /**
+   * @throws AbortException always.
+   */
   public RuntimeException fatalError(Diagnostic error) {
     error(error);
     failIfPendingErrors();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java b/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java
new file mode 100644
index 0000000..6ea10cd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B116282409.java
@@ -0,0 +1,168 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.jasmin.JasminTestBase;
+import com.android.tools.r8.utils.AbortException;
+import com.android.tools.r8.utils.AndroidApp;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class B116282409 extends JasminTestBase {
+
+  private final Backend backend;
+
+  private final boolean enableVerticalClassMerging;
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Parameters(name = "Backend: {0}, vertical class merging: {1}")
+  public static Collection<Object[]> data() {
+    return ImmutableList.of(
+        new Object[] {Backend.CF, false},
+        new Object[] {Backend.CF, true},
+        new Object[] {Backend.DEX, false},
+        new Object[] {Backend.DEX, true});
+  }
+
+  public B116282409(Backend backend, boolean enableVerticalClassMerging) {
+    this.backend = backend;
+    this.enableVerticalClassMerging = enableVerticalClassMerging;
+  }
+
+  @Test
+  public void test() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    // Create a class A with a default constructor that prints "In A.<init>()".
+    ClassBuilder classBuilder = jasminBuilder.addClass("A");
+    classBuilder.addMethod(
+        "public",
+        "<init>",
+        ImmutableList.of(),
+        "V",
+        ".limit stack 2",
+        ".limit locals 1",
+        "aload_0",
+        "invokespecial java/lang/Object/<init>()V",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "ldc \"In A.<init>()\"",
+        "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
+        "return");
+
+    // Create a class B that inherits from A.
+    classBuilder = jasminBuilder.addClass("B", "A");
+
+    // Also add a simple method that the class inliner would inline.
+    classBuilder.addVirtualMethod(
+        "m", "I", ".limit stack 5", ".limit locals 5", "bipush 42", "ireturn");
+
+    // Add a test class that initializes an instance of B using A.<init>.
+    classBuilder = jasminBuilder.addClass("TestClass");
+    classBuilder.addMainMethod(
+        ".limit stack 5",
+        ".limit locals 5",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "new B",
+        "dup",
+        "invokespecial A/<init>()V", // NOTE: usually B.<init>
+        "invokevirtual B/m()I",
+        "invokevirtual java/io/PrintStream/println(I)V",
+        "return");
+
+    // Build app.
+    if (enableVerticalClassMerging) {
+      exception.expect(CompilationFailedException.class);
+      exception.expectCause(
+          new CustomExceptionMatcher(
+              "Unable to rewrite `invoke-direct A.<init>(new B, ...)` in method "
+                  + "`void TestClass.main(java.lang.String[])` after type `A` was merged into `B`.",
+              "Please add the following rule to your Proguard configuration file: "
+                  + "`-keep,allowobfuscation class A`."));
+    }
+
+    AndroidApp output =
+        compileWithR8(
+            jasminBuilder.build(),
+            keepMainProguardConfiguration("TestClass"),
+            options -> options.enableVerticalClassMerging = enableVerticalClassMerging,
+            backend);
+    assertFalse(enableVerticalClassMerging);
+
+    // Run app.
+    ProcessResult vmResult = runOnVMRaw(output, "TestClass", backend);
+    if (backend == Backend.CF) {
+      // Verify that the input code does not run with java.
+      ProcessResult javaResult = runOnJavaRaw(jasminBuilder, "TestClass");
+      assertNotEquals(0, javaResult.exitCode);
+      assertThat(javaResult.stderr, containsString("VerifyError"));
+      assertThat(javaResult.stderr, containsString("Call to wrong initialization method"));
+
+      // The same should be true for the generated program.
+      assertNotEquals(0, vmResult.exitCode);
+      assertThat(vmResult.stderr, containsString("VerifyError"));
+      assertThat(vmResult.stderr, containsString("Call to wrong initialization method"));
+    } else {
+      assert backend == Backend.DEX;
+      assertEquals(0, vmResult.exitCode);
+      assertEquals(String.join(System.lineSeparator(), "In A.<init>()", "42", ""), vmResult.stdout);
+    }
+  }
+
+  private static class CustomExceptionMatcher extends BaseMatcher<Throwable> {
+
+    private List<String> messages;
+
+    public CustomExceptionMatcher(String... messages) {
+      this.messages = Arrays.asList(messages);
+    }
+
+    @Override
+    public void describeTo(Description description) {
+      description
+          .appendText("a string containing ")
+          .appendText(
+              String.join(
+                  ", ", messages.stream().map(m -> "\"" + m + "\"").collect(Collectors.toList())));
+    }
+
+    @Override
+    public boolean matches(Object o) {
+      if (o instanceof AbortException) {
+        AbortException exception = (AbortException) o;
+        if (exception.getMessage() != null
+            && messages.stream().allMatch(message -> exception.getMessage().contains(message))) {
+          return true;
+        }
+        if (exception.getCause() != null) {
+          return matches(exception.getCause());
+        }
+      }
+      return false;
+    }
+  }
+}