Insert casts needed for the program to type check after vertical class merging

Bug: 199561570
Change-Id: I1fb0e1fb49f0f1c840ac8da0c4c1d40ae2bd0f49
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 0225c1d..99b9cb7 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -561,6 +561,10 @@
     }
   }
 
+  public boolean hasVerticallyMergedClasses() {
+    return verticallyMergedClasses != null;
+  }
+
   /**
    * Get the result of vertical class merging. Returns null if vertical class merging has not been
    * run.
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 82f09f8..450bc51 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -217,7 +217,7 @@
   }
 
   public int getNumberOfArguments() {
-    return getReference().getArity() + BooleanUtils.intValue(isInstance());
+    return getReference().getNumberOfArguments(isStatic());
   }
 
   public CompilationState getCompilationState() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index c2eeffa..d7f282f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.StructuralMapping;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
@@ -69,6 +70,10 @@
     return getParameter(argumentIndex - 1);
   }
 
+  public int getNumberOfArguments(boolean isStatic) {
+    return getArity() + BooleanUtils.intValue(!isStatic);
+  }
+
   public DexType getParameter(int index) {
     return proto.getParameter(index);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 0332c6e..85ac375 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -221,6 +221,7 @@
         newValue.addUser(this);
       }
     }
+    oldValue.removeUser(this);
   }
 
   public void replaceValue(int index, Value newValue) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index 6c22403..92998f3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -46,6 +47,29 @@
     this.method = target;
   }
 
+  public static InvokeMethod create(
+      Type type, DexMethod target, Value result, List<Value> arguments, boolean itf) {
+    switch (type) {
+      case DIRECT:
+        return new InvokeDirect(target, result, arguments, itf);
+      case INTERFACE:
+        return new InvokeInterface(target, result, arguments);
+      case STATIC:
+        return new InvokeStatic(target, result, arguments, itf);
+      case SUPER:
+        return new InvokeSuper(target, result, arguments, itf);
+      case VIRTUAL:
+        assert !itf;
+        return new InvokeVirtual(target, result, arguments);
+      case CUSTOM:
+      case MULTI_NEW_ARRAY:
+      case NEW_ARRAY:
+      case POLYMORPHIC:
+      default:
+        throw new Unreachable("Unexpected invoke type: " + type);
+    }
+  }
+
   public abstract boolean getInterfaceBit();
 
   @Override
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 303d31c..3ec06bd 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
@@ -62,6 +62,7 @@
 import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
 import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.ConstClass;
@@ -97,12 +98,12 @@
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.verticalclassmerging.InterfaceTypeToClassTypeLensCodeRewriterHelper;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiFunction;
@@ -149,7 +150,9 @@
     DexItemFactory factory = appView.dexItemFactory();
     // Rewriting types that affects phi can cause us to compute TOP for cyclic phi's. To solve this
     // we track all phi's that needs to be re-computed.
-    ListIterator<BasicBlock> blocks = code.listIterator();
+    BasicBlockIterator blocks = code.listIterator();
+    InterfaceTypeToClassTypeLensCodeRewriterHelper interfaceTypeToClassTypeRewriterHelper =
+        InterfaceTypeToClassTypeLensCodeRewriterHelper.create(appView, code, methodProcessor);
     boolean mayHaveUnreachableBlocks = false;
     while (blocks.hasNext()) {
       BasicBlock block = blocks.next();
@@ -370,15 +373,17 @@
                 boolean isInterface =
                     getBooleanOrElse(
                         appView.definitionFor(actualTarget.holder), DexClass::isInterface, false);
-                Invoke newInvoke =
-                    Invoke.create(
-                        actualInvokeType,
-                        actualTarget,
-                        null,
-                        newOutValue,
-                        newInValues,
-                        isInterface);
+                InvokeMethod newInvoke =
+                    InvokeMethod.create(
+                        actualInvokeType, actualTarget, newOutValue, newInValues, isInterface);
+
                 iterator.replaceCurrentInstruction(newInvoke);
+
+                // Insert casts for the program to type check if interfaces has been vertically
+                // merged into their unique (non-interface) subclass. See also b/199561570.
+                interfaceTypeToClassTypeRewriterHelper.insertCastsForOperandsIfNeeded(
+                    invoke, newInvoke, lensLookup, blocks, block, iterator);
+
                 if (newOutValue != null && newOutValue.getType() != current.getOutType()) {
                   affectedPhis.addAll(newOutValue.uniquePhiUsers());
                 }
@@ -698,6 +703,10 @@
     if (!affectedPhis.isEmpty()) {
       new DestructivePhiTypeUpdater(appView).recomputeAndPropagateTypes(code, affectedPhis);
     }
+
+    // Finalize cast insertion.
+    interfaceTypeToClassTypeRewriterHelper.processWorklist();
+
     assert code.isConsistentSSABeforeTypesAreCorrect();
     assert code.hasNoMergedClasses(appView);
   }
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/EmptyInterfaceTypeToClassTypeLensCodeRewriterHelper.java b/src/main/java/com/android/tools/r8/verticalclassmerging/EmptyInterfaceTypeToClassTypeLensCodeRewriterHelper.java
new file mode 100644
index 0000000..0d4cb33
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/EmptyInterfaceTypeToClassTypeLensCodeRewriterHelper.java
@@ -0,0 +1,31 @@
+// 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.verticalclassmerging;
+
+import com.android.tools.r8.graph.GraphLens.MethodLookupResult;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+
+public class EmptyInterfaceTypeToClassTypeLensCodeRewriterHelper
+    extends InterfaceTypeToClassTypeLensCodeRewriterHelper {
+
+  @Override
+  public void insertCastsForOperandsIfNeeded(
+      InvokeMethod originalInvoke,
+      InvokeMethod rewrittenInvoke,
+      MethodLookupResult lookupResult,
+      BasicBlockIterator blockIterator,
+      BasicBlock block,
+      InstructionListIterator instructionIterator) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void processWorklist() {
+    // Intentionally empty.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java
new file mode 100644
index 0000000..6712600
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelper.java
@@ -0,0 +1,57 @@
+// 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.verticalclassmerging;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.GraphLens.MethodLookupResult;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+
+/**
+ * Inserts check-cast instructions after vertical class merging when this is needed for the program
+ * to type check.
+ *
+ * <p>Any class type is assignable to any interface type. If an interface I is merged into its
+ * unique (non-interface) subtype C, then assignments that used to be valid may no longer be valid
+ * due to the stronger type checking imposed by the JVM. Therefore, casts are inserted where
+ * necessary for the program to type check after vertical class merging.
+ *
+ * <p>Example: If the interface I is merged into its unique subclass C, then the invoke-interface
+ * instruction will be rewritten by the {@link com.android.tools.r8.ir.conversion.LensCodeRewriter}
+ * to an invoke-virtual instruction. After this rewriting, the program no longer type checks, and
+ * therefore a cast is inserted before the invoke-virtual instruction: {@code C c = (C) o}.
+ *
+ * <pre>
+ *   Object o = get();
+ *   o.m(); // invoke-interface {o}, void I.m()
+ * </pre>
+ */
+public abstract class InterfaceTypeToClassTypeLensCodeRewriterHelper {
+
+  public static InterfaceTypeToClassTypeLensCodeRewriterHelper create(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCode code,
+      MethodProcessor methodProcessor) {
+    if (methodProcessor.isPrimaryMethodProcessor() && appView.hasVerticallyMergedClasses()) {
+      return new InterfaceTypeToClassTypeLensCodeRewriterHelperImpl(appView, code);
+    }
+    return new EmptyInterfaceTypeToClassTypeLensCodeRewriterHelper();
+  }
+
+  public abstract void insertCastsForOperandsIfNeeded(
+      InvokeMethod originalInvoke,
+      InvokeMethod rewrittenInvoke,
+      MethodLookupResult lookupResult,
+      BasicBlockIterator blockIterator,
+      BasicBlock block,
+      InstructionListIterator instructionIterator);
+
+  public abstract void processWorklist();
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java
new file mode 100644
index 0000000..a376d91
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InterfaceTypeToClassTypeLensCodeRewriterHelperImpl.java
@@ -0,0 +1,204 @@
+// 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.verticalclassmerging;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens.MethodLookupResult;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.OptionalBool;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+// TODO(b/199561570): Extend this to insert casts for users that are not an instance of
+//  invoke-method (e.g., array-put, instance-put, static-put, return).
+public class InterfaceTypeToClassTypeLensCodeRewriterHelperImpl
+    extends InterfaceTypeToClassTypeLensCodeRewriterHelper {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final IRCode code;
+
+  private final Map<Instruction, Deque<WorklistItem>> worklist = new IdentityHashMap<>();
+
+  public InterfaceTypeToClassTypeLensCodeRewriterHelperImpl(
+      AppView<? extends AppInfoWithClassHierarchy> appView, IRCode code) {
+    this.appView = appView;
+    this.code = code;
+  }
+
+  @Override
+  public void insertCastsForOperandsIfNeeded(
+      InvokeMethod originalInvoke,
+      InvokeMethod rewrittenInvoke,
+      MethodLookupResult lookupResult,
+      BasicBlockIterator blockIterator,
+      BasicBlock block,
+      InstructionListIterator instructionIterator) {
+    DexMethod originalInvokedMethod = originalInvoke.getInvokedMethod();
+    DexMethod rewrittenInvokedMethod = rewrittenInvoke.getInvokedMethod();
+    if (lookupResult.getPrototypeChanges().getArgumentInfoCollection().hasRemovedArguments()) {
+      // There is no argument removal before the primary optimization pass.
+      assert false;
+      return;
+    }
+
+    // Intentionally iterate the arguments of the original invoke, since the rewritten invoke could
+    // have extra arguments added.
+    for (int operandIndex = 0; operandIndex < originalInvoke.arguments().size(); operandIndex++) {
+      Value operand = rewrittenInvoke.getArgument(operandIndex);
+      DexType originalType =
+          originalInvokedMethod.getArgumentType(operandIndex, originalInvoke.isInvokeStatic());
+      DexType rewrittenType =
+          rewrittenInvokedMethod.getArgumentType(operandIndex, rewrittenInvoke.isInvokeStatic());
+      if (needsCastForOperand(operand, block, originalType, rewrittenType).isPossiblyTrue()) {
+        worklist
+            .computeIfAbsent(rewrittenInvoke, ignoreKey(ArrayDeque::new))
+            .addLast(new WorklistItem(operandIndex, originalType, rewrittenType));
+      }
+    }
+  }
+
+  @Override
+  public void processWorklist() {
+    if (worklist.isEmpty()) {
+      return;
+    }
+
+    BasicBlockIterator blockIterator = code.listIterator();
+    boolean isCodeFullyRewrittenWithLens = true;
+    while (blockIterator.hasNext()) {
+      BasicBlock block = blockIterator.next();
+      InstructionListIterator instructionIterator = block.listIterator(code);
+      while (instructionIterator.hasNext()) {
+        Instruction instruction = instructionIterator.next();
+        Deque<WorklistItem> worklistItems = worklist.get(instruction);
+        if (worklistItems == null) {
+          continue;
+        }
+        for (WorklistItem worklistItem : worklistItems) {
+          Value operand = instruction.getOperand(worklistItem.operandIndex);
+          DexType originalType = worklistItem.originalType;
+          DexType rewrittenType = worklistItem.rewrittenType;
+          OptionalBool needsCastForOperand =
+              needsCastForOperand(
+                  operand, block, originalType, rewrittenType, isCodeFullyRewrittenWithLens);
+          assert !needsCastForOperand.isUnknown();
+          if (needsCastForOperand.isTrue()) {
+            insertCastForOperand(
+                operand, rewrittenType, instruction, blockIterator, block, instructionIterator);
+          }
+        }
+      }
+    }
+  }
+
+  private void insertCastForOperand(
+      Value operand,
+      DexType castType,
+      Instruction rewrittenUser,
+      BasicBlockIterator blockIterator,
+      BasicBlock block,
+      InstructionListIterator instructionIterator) {
+    Instruction previous = instructionIterator.previous();
+    assert previous == rewrittenUser;
+
+    CheckCast checkCast =
+        CheckCast.builder()
+            .setCastType(castType)
+            .setObject(operand)
+            .setFreshOutValue(code, castType.toTypeElement(appView), operand.getLocalInfo())
+            .setPosition(rewrittenUser)
+            .build();
+    if (block.hasCatchHandlers()) {
+      instructionIterator
+          .splitCopyCatchHandlers(code, blockIterator, appView.options())
+          .listIterator(code)
+          .add(checkCast);
+    } else {
+      instructionIterator.add(checkCast);
+    }
+    rewrittenUser.replaceValue(operand, checkCast.outValue());
+
+    Instruction next = instructionIterator.next();
+    assert next == rewrittenUser;
+  }
+
+  private boolean isOperandRewrittenWithLens(
+      Value operand, BasicBlock blockWithUser, boolean isCodeFullyRewrittenWithLens) {
+    if (isCodeFullyRewrittenWithLens) {
+      return true;
+    }
+    if (operand.isPhi()) {
+      return false;
+    }
+    Instruction definition = operand.getDefinition();
+    return definition.isArgument() || operand.getBlock() == blockWithUser;
+  }
+
+  private OptionalBool needsCastForOperand(
+      Value operand, BasicBlock blockWithUser, DexType originalType, DexType rewrittenType) {
+    return needsCastForOperand(operand, blockWithUser, originalType, rewrittenType, false);
+  }
+
+  private OptionalBool needsCastForOperand(
+      Value operand,
+      BasicBlock blockWithUser,
+      DexType originalType,
+      DexType rewrittenType,
+      boolean isCodeFullyRewrittenWithLens) {
+    if (!originalType.isClassType() || !rewrittenType.isClassType()) {
+      return OptionalBool.FALSE;
+    }
+    // The original type should be an interface type.
+    DexProgramClass originalClass = asProgramClassOrNull(appView.definitionFor(originalType));
+    if (originalClass == null || !originalClass.isInterface()) {
+      return OptionalBool.FALSE;
+    }
+    // The rewritten type should be a (non-interface) class type.
+    DexProgramClass rewrittenClass = asProgramClassOrNull(appView.definitionFor(rewrittenType));
+    if (rewrittenClass == null || rewrittenClass.isInterface()) {
+      return OptionalBool.FALSE;
+    }
+    // If the operand has not yet been rewritten with the lens, we delay the type check until
+    // after lens code rewriting.
+    if (!isOperandRewrittenWithLens(operand, blockWithUser, isCodeFullyRewrittenWithLens)) {
+      assert !isCodeFullyRewrittenWithLens;
+      return OptionalBool.UNKNOWN;
+    }
+    // The operand should not be subtype of the rewritten type.
+    TypeElement rewrittenTypeElement = rewrittenType.toTypeElement(appView);
+    return OptionalBool.of(
+        !operand.getType().lessThanOrEqualUpToNullability(rewrittenTypeElement, appView));
+  }
+
+  private static class WorklistItem {
+
+    final int operandIndex;
+    final DexType originalType;
+    final DexType rewrittenType;
+
+    WorklistItem(int operandIndex, DexType originalType, DexType rewrittenType) {
+      this.operandIndex = operandIndex;
+      this.originalType = originalType;
+      this.rewrittenType = rewrittenType;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InterfaceInvokeWithObjectReceiverInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InterfaceInvokeWithObjectReceiverInliningTest.java
index 88e264b..301870a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InterfaceInvokeWithObjectReceiverInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InterfaceInvokeWithObjectReceiverInliningTest.java
@@ -67,11 +67,7 @@
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
-        // TODO(b/199561570): Should succeed with 0.
-        .applyIf(
-            enableInlining || !enableVerticalClassMerging,
-            runResult -> runResult.assertSuccessWithOutputLines("0"),
-            runResult -> runResult.assertFailureWithErrorThatThrows(VerifyError.class));
+        .assertSuccessWithOutputLines("0");
   }
 
   private static byte[] getTransformedMain() throws IOException {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/StaticInvokeWithMultipleObjectsForInterfaceTypesTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/StaticInvokeWithMultipleObjectsForInterfaceTypesTest.java
new file mode 100644
index 0000000..0c9a135
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/StaticInvokeWithMultipleObjectsForInterfaceTypesTest.java
@@ -0,0 +1,160 @@
+// 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.ir.optimize.inliner;
+
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StaticInvokeWithMultipleObjectsForInterfaceTypesTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableInlining;
+
+  @Parameter(1)
+  public boolean enableVerticalClassMerging;
+
+  @Parameter(2)
+  public TestParameters parameters;
+
+  @Parameters(name = "{2}, inlining: {0}, vertical class merging: {1}")
+  public static List<Object[]> parameters() {
+    return buildParameters(
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    assumeFalse(enableInlining);
+    assumeFalse(enableVerticalClassMerging);
+    testForRuntime(parameters)
+        .addProgramClasses(I.class, J.class, A.class, B.class)
+        .addProgramClassFileData(getTransformedMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "0");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(I.class, J.class, A.class, B.class)
+        .addProgramClassFileData(getTransformedMain())
+        .addKeepMainRule(Main.class)
+        // Keep getA() and getB() to prevent that we optimize it into having static return type A/B.
+        .addKeepRules("-keepclassmembers class " + Main.class.getTypeName() + " { *** get?(...); }")
+        .addInliningAnnotations()
+        .addNoVerticalClassMergingAnnotations()
+        .applyIf(!enableInlining, R8TestBuilder::enableInliningAnnotations)
+        .applyIf(
+            !enableVerticalClassMerging, R8TestBuilder::enableNoVerticalClassMergingAnnotations)
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "0");
+  }
+
+  private static byte[] getTransformedMain() throws IOException {
+    return transformer(Main.class)
+        .transformMethodInsnInMethod(
+            "main",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              if (name.startsWith("get")) {
+                visitor.visitMethodInsn(opcode, owner, name, "(I)Ljava/lang/Object;", isInterface);
+              } else {
+                visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+              }
+            })
+        .setReturnType(MethodPredicate.onName("getA"), Object.class.getTypeName())
+        .setReturnType(MethodPredicate.onName("getB"), Object.class.getTypeName())
+        .transform();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // Transformed from `I getA(int)` to `Object getA(int)` and `J getB(int)` to
+      // `Object getB(int)`.
+      test(getA(args.length), getB(args.length));
+    }
+
+    // @Keep
+    static /*Object*/ I getA(int f) {
+      return new A(f);
+    }
+
+    // @Keep
+    static /*Object*/ J getB(int f) {
+      return new B(f);
+    }
+
+    @NeverInline
+    static void test(I i, J j) {
+      i.m();
+      j.m();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NoVerticalClassMerging
+  interface I {
+
+    void m();
+  }
+
+  @NoHorizontalClassMerging
+  static class A implements I {
+
+    int f;
+
+    A(int f) {
+      this.f = f;
+    }
+
+    @Override
+    public void m() {
+      System.out.println(f);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NoVerticalClassMerging
+  interface J {
+
+    void m();
+  }
+
+  @NoHorizontalClassMerging
+  static class B implements J {
+
+    int f;
+
+    B(int f) {
+      this.f = f;
+    }
+
+    @Override
+    public void m() {
+      System.out.println(f);
+    }
+  }
+}