Merge commit '38f9d71955d87ddea83a96fd960443225a4a7038' into dev-release
diff --git a/build.gradle b/build.gradle
index 35cc8df..8a0cc1f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -217,6 +217,8 @@
         exclude group: 'com.google.code.findbugs'
         exclude group: 'com.google.j2objc'
         exclude group: 'org.codehaus.mojo'
+        exclude group: 'com.google.guava', module: 'listenablefuture'
+        exclude group: 'com.google.guava', module: 'failureaccess'
     })
     implementation group: 'it.unimi.dsi', name: 'fastutil', version: fastutilVersion
     implementation "org.jetbrains.kotlinx:kotlinx-metadata-jvm:$kotlinExtMetadataJVMVersion"
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 37fad8c..073cc8d 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -947,10 +947,7 @@
             DexWritableCode code = method.getDefinition().getCode().asDexWritableCode();
             DexWritableCode rewrittenCode =
                 code.rewriteCodeWithJumboStrings(
-                    method,
-                    mapping,
-                    application.dexItemFactory,
-                    options.testing.forceJumboStringProcessing);
+                    method, mapping, appView, options.testing.forceJumboStringProcessing);
             method.setCode(rewrittenCode.asCode(), appView);
           });
     }
diff --git a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
index a68f99f..13f6812 100644
--- a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
+++ b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
@@ -53,6 +53,7 @@
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.BooleanSupplier;
 
 public class JumboStringRewriter {
 
@@ -92,6 +93,7 @@
 
   private final DexEncodedMethod method;
   private final DexString firstJumboString;
+  private final BooleanSupplier materializeInfoForNativePc;
   private final DexItemFactory factory;
   private final Map<DexInstruction, List<DexInstruction>> instructionTargets =
       new IdentityHashMap<>();
@@ -105,12 +107,20 @@
   private final Map<TryHandler, List<DexInstruction>> handlerTargets = new IdentityHashMap<>();
 
   public JumboStringRewriter(
-      DexEncodedMethod method, DexString firstJumboString, DexItemFactory factory) {
+      DexEncodedMethod method,
+      DexString firstJumboString,
+      BooleanSupplier materializeInfoForNativePc,
+      DexItemFactory factory) {
     this.method = method;
     this.firstJumboString = firstJumboString;
+    this.materializeInfoForNativePc = materializeInfoForNativePc;
     this.factory = factory;
   }
 
+  private DexCode getCode() {
+    return method.getCode().asDexCode();
+  }
+
   public DexCode rewrite() {
     // Build maps from everything in the code that uses offsets or direct addresses to reference
     // instructions to the actual instruction referenced.
@@ -124,7 +134,7 @@
     TryHandler[] newHandlers = rewriteHandlerOffsets();
     DexDebugInfo newDebugInfo = rewriteDebugInfoOffsets();
     // Set the new code on the method.
-    DexCode oldCode = method.getCode().asDexCode();
+    DexCode oldCode = getCode();
     DexCode newCode =
         new DexCode(
             oldCode.registerSize,
@@ -185,7 +195,7 @@
   }
 
   private Try[] rewriteTryOffsets() {
-    DexCode code = method.getCode().asDexCode();
+    DexCode code = getCode();
     Try[] result = new Try[code.tries.length];
     for (int i = 0; i < code.tries.length; i++) {
       Try theTry = code.tries[i];
@@ -197,7 +207,7 @@
   }
 
   private TryHandler[] rewriteHandlerOffsets() {
-    DexCode code = method.getCode().asDexCode();
+    DexCode code = getCode();
     TryHandler[] result = new TryHandler[code.handlers.length];
     for (int i = 0; i < code.handlers.length; i++) {
       TryHandler handler = code.handlers[i];
@@ -218,7 +228,7 @@
   }
 
   private DexDebugInfo rewriteDebugInfoOffsets() {
-    DexCode code = method.getCode().asDexCode();
+    DexCode code = getCode();
     if (!debugEventTargets.isEmpty()) {
       assert debugEventBasedInfo != null;
       int lastOriginalOffset = 0;
@@ -256,7 +266,7 @@
   @SuppressWarnings("JdkObsolete")
   private List<DexInstruction> expandCode() {
     LinkedList<DexInstruction> instructions = new LinkedList<>();
-    Collections.addAll(instructions, method.getCode().asDexCode().instructions);
+    Collections.addAll(instructions, getCode().instructions);
     int offsetDelta;
     do {
       ListIterator<DexInstruction> it = instructions.listIterator();
@@ -436,7 +446,7 @@
   }
 
   private void recordInstructionTargets(Int2ReferenceMap<DexInstruction> offsetToInstruction) {
-    DexInstruction[] instructions = method.getCode().asDexCode().instructions;
+    DexInstruction[] instructions = getCode().instructions;
     for (DexInstruction instruction : instructions) {
       if (instruction instanceof DexFormat22t) { // IfEq, IfGe, IfGt, IfLe, IfLt, IfNe
         DexFormat22t condition = (DexFormat22t) instruction;
@@ -486,14 +496,15 @@
   }
 
   private void recordDebugEventTargets(Int2ReferenceMap<DexInstruction> offsetToInstruction) {
-    // TODO(b/213411850): Merging pc based D8 builds will map out of PC for any jumbo processed
-    //  method. Instead we should rather retain the PC encoding by bumping the max-pc and recording
-    //  the line number translation. We actually need to do so to support merging with native PC
-    //  support as in that case we can't reflect the change in the line table, only in the mapping.
-    EventBasedDebugInfo eventBasedInfo =
-        DexDebugInfo.convertToEventBased(method.getCode().asDexCode(), factory);
+    EventBasedDebugInfo eventBasedInfo = DexDebugInfo.convertToEventBased(getCode(), factory);
     if (eventBasedInfo == null) {
-      return;
+      if (materializeInfoForNativePc.getAsBoolean()) {
+        eventBasedInfo =
+            DexDebugInfo.createEventBasedDebugInfoForNativePc(
+                method.getParameters().size(), getCode(), factory);
+      } else {
+        return;
+      }
     }
     debugEventBasedInfo = eventBasedInfo;
     int address = 0;
@@ -516,7 +527,7 @@
 
   private void recordTryAndHandlerTargets(
       Int2ReferenceMap<DexInstruction> offsetToInstruction, DexInstruction lastInstruction) {
-    DexCode code = method.getCode().asDexCode();
+    DexCode code = getCode();
     for (Try theTry : code.tries) {
       DexInstruction start = offsetToInstruction.get(theTry.startAddress);
       DexInstruction end = null;
@@ -553,7 +564,7 @@
 
   private void recordTargets() {
     Int2ReferenceMap<DexInstruction> offsetToInstruction = new Int2ReferenceOpenHashMap<>();
-    DexInstruction[] instructions = method.getCode().asDexCode().instructions;
+    DexInstruction[] instructions = getCode().instructions;
     boolean containsPayloads = false;
     for (DexInstruction instruction : instructions) {
       offsetToInstruction.put(instruction.getOffset(), instruction);
diff --git a/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java b/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
index f9acf30..b252ad4 100644
--- a/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitBoundaryOptimizationUtils.java
@@ -66,20 +66,23 @@
     }
 
     // Next perform startup checks.
-    StartupProfile startupProfile = appView.getStartupProfile();
-    OptionalBool callerIsStartupMethod = isStartupMethod(caller, startupProfile);
-    if (callerIsStartupMethod.isTrue()) {
-      // If the caller is a startup method, then only allow inlining if the callee is also a startup
-      // method.
-      if (isStartupMethod(callee, startupProfile).isFalse()) {
-        return false;
-      }
-    } else if (callerIsStartupMethod.isFalse()) {
-      // If the caller is not a startup method, then only allow inlining if the caller is not a
-      // startup class or the callee is a startup class.
-      if (startupProfile.isStartupClass(caller.getHolderType())
-          && !startupProfile.isStartupClass(callee.getHolderType())) {
-        return false;
+    if (!callee.getOptimizationInfo().forceInline()) {
+      StartupProfile startupProfile = appView.getStartupProfile();
+      OptionalBool callerIsStartupMethod = isStartupMethod(caller, startupProfile);
+      if (callerIsStartupMethod.isTrue()) {
+        // If the caller is a startup method, then only allow inlining if the callee is also a
+        // startup
+        // method.
+        if (isStartupMethod(callee, startupProfile).isFalse()) {
+          return false;
+        }
+      } else if (callerIsStartupMethod.isFalse()) {
+        // If the caller is not a startup method, then only allow inlining if the caller is not a
+        // startup class or the callee is a startup class.
+        if (startupProfile.isStartupClass(caller.getHolderType())
+            && !startupProfile.isStartupClass(callee.getHolderType())) {
+          return false;
+        }
       }
     }
     return true;
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
index b0e0aba..31b2efe 100644
--- a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
@@ -324,7 +324,7 @@
 
   @Override
   public DexWritableCode rewriteCodeWithJumboStrings(
-      ProgramMethod method, ObjectToOffsetMapping mapping, DexItemFactory factory, boolean force) {
+      ProgramMethod method, ObjectToOffsetMapping mapping, AppView<?> appView, boolean force) {
     // Intentionally empty. This piece of code does not have any const-string instructions.
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 039a0f0..5932bb5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -170,6 +170,17 @@
     };
   }
 
+  public DexCode withNewInstructions(DexInstruction[] newInstructions) {
+    return new DexCode(
+        this.registerSize,
+        this.incomingRegisterSize,
+        this.outgoingRegisterSize,
+        newInstructions,
+        this.tries,
+        this.handlers,
+        this.getDebugInfo());
+  }
+
   @Override
   public DexCode self() {
     return this;
@@ -201,7 +212,7 @@
 
   @Override
   public DexWritableCode rewriteCodeWithJumboStrings(
-      ProgramMethod method, ObjectToOffsetMapping mapping, DexItemFactory factory, boolean force) {
+      ProgramMethod method, ObjectToOffsetMapping mapping, AppView<?> appView, boolean force) {
     DexString firstJumboString = null;
     if (force) {
       firstJumboString = mapping.getFirstString();
@@ -215,7 +226,12 @@
       }
     }
     return firstJumboString != null
-        ? new JumboStringRewriter(method.getDefinition(), firstJumboString, factory).rewrite()
+        ? new JumboStringRewriter(
+                method.getDefinition(),
+                firstJumboString,
+                () -> appView.options().shouldMaterializeLineInfoForNativePcEncoding(method),
+                appView.dexItemFactory())
+            .rewrite()
         : this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
index 8826ec4..1a0023a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
@@ -314,11 +314,18 @@
         code, pcBasedDebugInfo.maxPc);
     // Generate a line event at each throwing instruction.
     DexInstruction[] instructions = code.instructions;
-    return forceConvertToEventBasedDebugInfo(pcBasedDebugInfo, instructions, factory);
+    return forceConvertToEventBasedDebugInfo(
+        PcBasedDebugInfo.START_LINE, pcBasedDebugInfo.getParameterCount(), instructions, factory);
   }
 
-  public static EventBasedDebugInfo forceConvertToEventBasedDebugInfo(
-      PcBasedDebugInfo pcBasedDebugInfo, DexInstruction[] instructions, DexItemFactory factory) {
+  public static EventBasedDebugInfo createEventBasedDebugInfoForNativePc(
+      int parameterCount, DexCode code, DexItemFactory factory) {
+    assert code.getDebugInfo() == null;
+    return forceConvertToEventBasedDebugInfo(0, parameterCount, code.instructions, factory);
+  }
+
+  private static EventBasedDebugInfo forceConvertToEventBasedDebugInfo(
+      int startLine, int parameterCount, DexInstruction[] instructions, DexItemFactory factory) {
     List<DexDebugEvent> events = new ArrayList<>(instructions.length);
     int delta = 0;
     for (DexInstruction instruction : instructions) {
@@ -329,9 +336,7 @@
       delta += instruction.getSize();
     }
     return new EventBasedDebugInfo(
-        PcBasedDebugInfo.START_LINE,
-        new DexString[pcBasedDebugInfo.getParameterCount()],
-        events.toArray(DexDebugEvent.EMPTY_ARRAY));
+        startLine, new DexString[parameterCount], events.toArray(DexDebugEvent.EMPTY_ARRAY));
   }
 
   public static DexDebugInfoForWriting convertToWritable(DexDebugInfo debugInfo) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexWritableCode.java b/src/main/java/com/android/tools/r8/graph/DexWritableCode.java
index 99f8679..afe2afb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexWritableCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexWritableCode.java
@@ -96,7 +96,7 @@
 
   /** Rewrites the code to have JumboString bytecode if required by mapping. */
   DexWritableCode rewriteCodeWithJumboStrings(
-      ProgramMethod method, ObjectToOffsetMapping mapping, DexItemFactory factory, boolean force);
+      ProgramMethod method, ObjectToOffsetMapping mapping, AppView<?> appView, boolean force);
 
   void setCallSiteContexts(ProgramMethod method);
 
diff --git a/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java b/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java
index 19d78c1..4905063 100644
--- a/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java
+++ b/src/main/java/com/android/tools/r8/graph/ThrowExceptionCode.java
@@ -197,7 +197,7 @@
 
   @Override
   public DexWritableCode rewriteCodeWithJumboStrings(
-      ProgramMethod method, ObjectToOffsetMapping mapping, DexItemFactory factory, boolean force) {
+      ProgramMethod method, ObjectToOffsetMapping mapping, AppView<?> appView, boolean force) {
     // Intentionally empty. This piece of code does not have any const-string instructions.
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java b/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java
index 707e45a..90c6d8d 100644
--- a/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java
+++ b/src/main/java/com/android/tools/r8/graph/ThrowNullCode.java
@@ -219,7 +219,7 @@
 
   @Override
   public DexWritableCode rewriteCodeWithJumboStrings(
-      ProgramMethod method, ObjectToOffsetMapping mapping, DexItemFactory factory, boolean force) {
+      ProgramMethod method, ObjectToOffsetMapping mapping, AppView<?> appView, boolean force) {
     // Intentionally empty. This piece of code does not have any const-string instructions.
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index 0bb5989..ed02fe2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -288,7 +288,7 @@
 
   @Override
   public void buildLir(LirBuilder<Value, ?> builder) {
-    builder.addCheckCast(type, object());
+    builder.addCheckCast(type, object(), ignoreCompatRules);
   }
 
   public static class Builder extends BuilderBase<Builder, CheckCast> {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
index a42e312..86c95b3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
@@ -223,7 +223,7 @@
 
   @Override
   public void buildLir(LirBuilder<Value, ?> builder) {
-    builder.addConstClass(getType());
+    builder.addConstClass(getType(), ignoreCompatRules);
   }
 
   public static class Builder extends BuilderBase<Builder, ConstClass> {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index 9a3a024..7e17e7e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -96,7 +97,7 @@
       // Then we can redirect any known value. This can lead to dead code.
       If theIf = block.exit().asIf();
       Set<Phi> allowedPhis = getAllowedPhis(nonConstNumberOperand(theIf).asPhi());
-      Set<Phi> foundPhis = Sets.newIdentityHashSet();
+      Set<Phi> foundPhis = new LinkedHashSet<>();
       WorkList.newIdentityWorkList(block)
           .process(
               (current, workList) -> {
@@ -135,9 +136,7 @@
           }
         }
       }
-      List<Phi> sortedFoundPhis = new ArrayList<>(foundPhis);
-      sortedFoundPhis.sort(Phi::compareTo);
-      for (Phi phi : sortedFoundPhis) {
+      for (Phi phi : foundPhis) {
         BasicBlock phiBlock = phi.getBlock();
         for (int i = 0; i < phi.getOperands().size(); i++) {
           Value value = phi.getOperand(i);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index 4643e0a..f715e77 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -322,7 +322,7 @@
                     .contextIndependentDefinitionForWithResolutionResult(type)
                     .toSingleClassWithProgramOverLibrary();
             assert theApi.equals(api.max(appView.options().getMinApiLevel()))
-                || (clazz != null && clazz.isProgramClass());
+                || (clazz != null && !clazz.isLibraryClass());
           });
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index ea4f7b1..8777089 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -509,11 +509,11 @@
     }
 
     @Override
-    public void onConstClass(DexType type) {
+    public void onConstClass(DexType type, boolean ignoreCompatRules) {
       Value dest =
           getOutValueForNextInstruction(
               type.toTypeElement(appView, Nullability.definitelyNotNull()));
-      addInstruction(new ConstClass(dest, type));
+      addInstruction(new ConstClass(dest, type, ignoreCompatRules));
     }
 
     @Override
@@ -739,9 +739,9 @@
     }
 
     @Override
-    public void onCheckCast(DexType type, EV value) {
+    public void onCheckCast(DexType type, EV value, boolean ignoreCompatRules) {
       Value dest = getOutValueForNextInstruction(type.toTypeElement(appView));
-      addInstruction(new CheckCast(dest, getValue(value), type));
+      addInstruction(new CheckCast(dest, getValue(value), type, ignoreCompatRules));
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index b88f1f7..f6ca030 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -392,8 +392,9 @@
     return addOneItemInstruction(LirOpcodes.LDC, string);
   }
 
-  public LirBuilder<V, EV> addConstClass(DexType type) {
-    return addOneItemInstruction(LirOpcodes.LDC, type);
+  public LirBuilder<V, EV> addConstClass(DexType type, boolean ignoreCompatRules) {
+    int opcode = ignoreCompatRules ? LirOpcodes.CONSTCLASS_IGNORE_COMPAT : LirOpcodes.LDC;
+    return addOneItemInstruction(opcode, type);
   }
 
   public LirBuilder<V, EV> addConstMethodHandle(DexMethodHandle methodHandle) {
@@ -483,9 +484,10 @@
     return addOneValueInstruction(LirOpcodes.ARRAYLENGTH, array);
   }
 
-  public LirBuilder<V, EV> addCheckCast(DexType type, V value) {
+  public LirBuilder<V, EV> addCheckCast(DexType type, V value, boolean ignoreCompatRules) {
+    int opcode = ignoreCompatRules ? LirOpcodes.CHECKCAST_IGNORE_COMPAT : LirOpcodes.CHECKCAST;
     return addInstructionTemplate(
-        LirOpcodes.CHECKCAST, Collections.singletonList(type), Collections.singletonList(value));
+        opcode, Collections.singletonList(type), Collections.singletonList(value));
   }
 
   public LirBuilder<V, EV> addSafeCheckCast(DexType type, V value) {
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index 8863892..6803d0b 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -209,6 +209,8 @@
   int INVOKEPOLYMORPHIC = 222;
   int RECORDFIELDVALUES = 223;
   int CHECKCAST_SAFE = 224;
+  int CHECKCAST_IGNORE_COMPAT = 225;
+  int CONSTCLASS_IGNORE_COMPAT = 226;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -543,6 +545,10 @@
         return "RECORDFIELDVALUES";
       case CHECKCAST_SAFE:
         return "CHECKCAST_SAFE";
+      case CHECKCAST_IGNORE_COMPAT:
+        return "CHECKCAST_IGNORE_COMPAT";
+      case CONSTCLASS_IGNORE_COMPAT:
+        return "CONSTCLASS_IGNORE_COMPAT";
 
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index 73ad12b..444df4e 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -99,7 +99,7 @@
     onInstruction();
   }
 
-  public void onConstClass(DexType type) {
+  public void onConstClass(DexType type, boolean ignoreCompatRules) {
     onInstruction();
   }
 
@@ -468,7 +468,7 @@
     onInstruction();
   }
 
-  public void onCheckCast(DexType type, EV value) {
+  public void onCheckCast(DexType type, EV value, boolean ignoreCompatRules) {
     onInstruction();
   }
 
@@ -530,7 +530,7 @@
             return;
           }
           if (item instanceof DexType) {
-            onConstClass((DexType) item);
+            onConstClass((DexType) item, false);
             return;
           }
           if (item instanceof DexMethodHandle) {
@@ -543,6 +543,12 @@
           }
           throw new Unimplemented();
         }
+      case LirOpcodes.CONSTCLASS_IGNORE_COMPAT:
+        {
+          DexItem item = getConstantItem(view.getNextConstantOperand());
+          onConstClass((DexType) item, true);
+          return;
+        }
       case LirOpcodes.ICONST_M1:
       case LirOpcodes.ICONST_0:
       case LirOpcodes.ICONST_1:
@@ -1110,7 +1116,14 @@
         {
           DexType type = getNextDexTypeOperand(view);
           EV value = getNextValueOperand(view);
-          onCheckCast(type, value);
+          onCheckCast(type, value, false);
+          return;
+        }
+      case LirOpcodes.CHECKCAST_IGNORE_COMPAT:
+        {
+          DexType type = getNextDexTypeOperand(view);
+          EV value = getNextValueOperand(view);
+          onCheckCast(type, value, true);
           return;
         }
       case LirOpcodes.CHECKCAST_SAFE:
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index 5092894..96d9cef 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -162,7 +162,7 @@
   }
 
   @Override
-  public void onConstClass(DexType type) {
+  public void onConstClass(DexType type, boolean ignoreCompatRules) {
     appendOutValue().append("class(").append(type).append(")");
   }
 
@@ -343,7 +343,7 @@
   }
 
   @Override
-  public void onCheckCast(DexType type, EV value) {
+  public void onCheckCast(DexType type, EV value, boolean ignoreCompatRules) {
     appendOutValue();
     appendValueArguments(value);
     builder.append(type);
@@ -351,7 +351,7 @@
 
   @Override
   public void onSafeCheckCast(DexType type, EV value) {
-    onCheckCast(type, value);
+    onCheckCast(type, value, true);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
index 43c2c1c..fce5c53 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.desugar.records.RecordCfToCfRewriter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardClassFilter;
+import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -91,11 +92,28 @@
       return;
     }
     if (code.isDexCode()) {
-      for (DexInstruction instruction : code.asDexCode().instructions) {
-        if (instruction.isConstString()) {
-          DexConstString cnst = instruction.asConstString();
-          cnst.BBBB = getRenamedStringLiteral(cnst.getString());
-        }
+      DexInstruction[] instructions = code.asDexCode().instructions;
+      DexInstruction[] newInstructions =
+          ArrayUtils.map(
+              instructions,
+              (DexInstruction instruction) -> {
+                if (instruction.isConstString()) {
+                  DexConstString cnst = instruction.asConstString();
+                  DexString renamedStringLiteral = getRenamedStringLiteral(cnst.getString());
+                  if (!renamedStringLiteral.equals(cnst.getString())) {
+                    DexConstString dexConstString =
+                        new DexConstString(instruction.asConstString().AA, renamedStringLiteral);
+                    dexConstString.setOffset(instruction.getOffset());
+                    return dexConstString;
+                  }
+                }
+                return instruction;
+              },
+              DexInstruction.EMPTY_ARRAY);
+      if (instructions != newInstructions) {
+        encodedMethod.setCode(
+            code.asDexCode().withNewInstructions(newInstructions),
+            encodedMethod.getParameterInfo());
       }
     } else if (code.isCfCode()) {
       for (CfInstruction instruction : code.asCfCode().getInstructions()) {
@@ -163,22 +181,24 @@
     assert code != null;
     if (code.isDexCode()) {
       DexInstruction[] instructions = code.asDexCode().instructions;
-      boolean updated = false;
-      for (int i = 0; i < instructions.length; ++i) {
-        DexInstruction instruction = instructions[i];
-        if (instruction.isDexItemBasedConstString()) {
-          DexItemBasedConstString cnst = instruction.asDexItemBasedConstString();
-          DexString replacement =
-              cnst.getNameComputationInfo()
-                  .computeNameFor(cnst.getItem(), appView, appView.graphLens(), lens);
-          DexConstString constString = new DexConstString(cnst.AA, replacement);
-          constString.setOffset(instruction.getOffset());
-          instructions[i] = constString;
-          updated = true;
-        }
-      }
-      if (updated) {
-        code.asDexCode().flushCachedValues();
+      DexInstruction[] newInstructions =
+          ArrayUtils.map(
+              instructions,
+              (DexInstruction instruction) -> {
+                if (instruction.isDexItemBasedConstString()) {
+                  DexItemBasedConstString cnst = instruction.asDexItemBasedConstString();
+                  DexString replacement =
+                      cnst.getNameComputationInfo()
+                          .computeNameFor(cnst.getItem(), appView, appView.graphLens(), lens);
+                  DexConstString constString = new DexConstString(cnst.AA, replacement);
+                  constString.setOffset(instruction.getOffset());
+                  return constString;
+                }
+                return instruction;
+              },
+              DexInstruction.EMPTY_ARRAY);
+      if (newInstructions != instructions) {
+        programMethod.setCode(code.asDexCode().withNewInstructions(newInstructions), appView);
       }
     } else if (code.isCfCode()) {
       List<CfInstruction> instructions = code.asCfCode().getInstructions();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
index a1aec5a..313b6ca 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
@@ -170,7 +170,14 @@
           && currentClass.getSuperType() != appView.dexItemFactory().objectType) {
         return classType.lessThanOrEqualUpToNullability(upperBound, appView);
       }
-      return classType.equalUpToNullability(upperBound);
+      // If the upper bound does not have any interfaces we simply activate the method state when
+      // meeting the upper bound class type in the downwards traversal over the class hierarchy.
+      if (classType.getInterfaces().isEmpty()) {
+        return classType.equalUpToNullability(upperBound);
+      }
+      // If the upper bound has interfaces, we check if the current class is a subtype of *both* the
+      // upper bound class type and the upper bound interface types.
+      return classType.lessThanOrEqualUpToNullability(upperBound, appView);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
index b6236d8..e730778 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -392,15 +392,6 @@
         newInstructions[i] = instruction;
       }
     }
-    return modified
-        ? new DexCode(
-            code.registerSize,
-            code.incomingRegisterSize,
-            code.outgoingRegisterSize,
-            newInstructions,
-            code.tries,
-            code.handlers,
-            code.getDebugInfo())
-        : code;
+    return modified ? code.withNewInstructions(newInstructions) : code;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index b2ce050..89edec3 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -52,6 +52,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
@@ -2515,6 +2516,10 @@
     throw new Unreachable();
   }
 
+  public boolean canUseDexPc2PcAsDebugInformation() {
+    return isGeneratingDex() && lineNumberOptimization == LineNumberOptimization.ON;
+  }
+
   // Debug entries may be dropped only if the source file content allows being omitted from
   // stack traces, or if the VM will report the source file even with a null valued debug info.
   public boolean allowDiscardingResidualDebugInfo() {
@@ -2522,8 +2527,10 @@
     return sourceFileProvider != null && sourceFileProvider.allowDiscardingSourceFile();
   }
 
-  public boolean canUseDexPc2PcAsDebugInformation() {
-    return isGeneratingDex() && lineNumberOptimization == LineNumberOptimization.ON;
+  public boolean allowDiscardingResidualDebugInfo(ProgramMethod method) {
+    // TODO(b/146565491): We can drop debug info once fixed at a known min-api.
+    DexString sourceFile = method.getHolder().getSourceFile();
+    return sourceFile == null || sourceFile.equals(itemFactory.defaultSourceFileAttribute);
   }
 
   public boolean canUseNativeDexPcInsteadOfDebugInfo() {
@@ -2532,6 +2539,19 @@
         && allowDiscardingResidualDebugInfo();
   }
 
+  public boolean canUseNativeDexPcInsteadOfDebugInfo(ProgramMethod method) {
+    return canUseDexPc2PcAsDebugInformation()
+        && hasMinApi(AndroidApiLevel.O)
+        && allowDiscardingResidualDebugInfo(method);
+  }
+
+  public boolean shouldMaterializeLineInfoForNativePcEncoding(ProgramMethod method) {
+    assert method.getDefinition().getCode().asDexCode() != null;
+    assert method.getDefinition().getCode().asDexCode().getDebugInfo() == null;
+    return method.getHolder().originatesFromDexResource()
+        && canUseNativeDexPcInsteadOfDebugInfo(method);
+  }
+
   public boolean isInterfaceMethodDesugaringEnabled() {
     // This condition is to filter out tests that never set program consumer.
     if (!hasConsumer()) {
diff --git a/src/test/java/com/android/tools/r8/debuginfo/composepc/ComposePcEncodingTest.java b/src/test/java/com/android/tools/r8/debuginfo/composepc/ComposePcEncodingTest.java
new file mode 100644
index 0000000..311ea41
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/composepc/ComposePcEncodingTest.java
@@ -0,0 +1,143 @@
+// Copyright (c) 2023, 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.debuginfo.composepc;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.retrace.RetraceFrameElement;
+import com.android.tools.r8.retrace.RetraceFrameResult;
+import com.android.tools.r8.retrace.RetraceStackTraceContext;
+import com.android.tools.r8.retrace.RetracedMethodReference;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.nio.file.Path;
+import java.util.OptionalInt;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ComposePcEncodingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public ComposePcEncodingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private byte[] getTransformedClass() throws Exception {
+    return transformer(TestClass.class)
+        .removeLineNumberTable(MethodPredicate.onName("unusedKeptAndNoLineInfo"))
+        .transform();
+  }
+
+  private boolean isNativePcSupported() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithPcAsLineNumberSupport());
+  }
+
+  @Test
+  public void test() throws Exception {
+    MethodReference unusedKeptAndNoLineInfo =
+        Reference.methodFromMethod(TestClass.class.getDeclaredMethod("unusedKeptAndNoLineInfo"));
+
+    // R8 compiles to DEX with pc2pc encoding or native-pc encoding.
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(getTransformedClass())
+            .addKeepMainRule(TestClass.class)
+            .addKeepMethodRules(unusedKeptAndNoLineInfo)
+            .setMinApi(parameters)
+            .addKeepAttributeLineNumberTable()
+            .compile()
+            .inspect(
+                inspector -> {
+                  // Expected residual line info of 1 for pc2pc encoding and some value for native.
+                  int residualLine = isNativePcSupported() ? 123 : 1;
+                  // Check the expected status of the DEX debug info object for the "no lines".
+                  MethodSubject methodNoLines = inspector.method(unusedKeptAndNoLineInfo);
+                  assertThat(methodNoLines, isPresent());
+                  // TODO(b/232212653): This should be true in pc2pc compilation with a single line.
+                  assertFalse(methodNoLines.hasLineNumberTable());
+                  // Check that "retracing" the pinned method with no lines maps to "noline/zero".
+                  RetraceFrameResult retraceResult =
+                      inspector
+                          .retrace()
+                          .retraceFrame(
+                              RetraceStackTraceContext.empty(),
+                              OptionalInt.of(residualLine),
+                              unusedKeptAndNoLineInfo);
+                  assertFalse(retraceResult.isAmbiguous());
+                  RetraceFrameElement frameElement = retraceResult.stream().findFirst().get();
+                  assertEquals(0, frameElement.getOuterFrames().size());
+                  RetracedMethodReference topFrame = frameElement.getTopFrame();
+                  assertTrue(topFrame.isKnown());
+                  // TODO(b/232212653): Retrace should map back to the "no line" value of zero.
+                  assertFalse(topFrame.hasPosition());
+                });
+
+    compileResult
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .inspectStackTrace(ComposePcEncodingTest::checkStackTrace);
+
+    Path r8OutputDex = compileResult.writeToZip();
+    Path r8OutputMap =
+        FileUtils.writeTextFile(
+            temp.newFolder().toPath().resolve("out.map"), compileResult.getProguardMap());
+
+    // D8 (re)merges DEX with an artificial jumbo-string to force a remapping of PC values.
+    testForD8(parameters.getBackend())
+        .addProgramFiles(r8OutputDex)
+        .setMinApi(parameters)
+        // We only optimize line info in release mode and with a mapping file output enabled.
+        .release()
+        .internalEnableMappingOutput()
+        .apply(b -> b.getBuilder().setProguardInputMapFile(r8OutputMap))
+        // Forcing jumbo processing will shift the PC values on the methods.
+        .addOptionsModification(o -> o.testing.forceJumboStringProcessing = true)
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class)
+        .inspectFailure(
+            inspector -> {
+              MethodSubject methodNoLines = inspector.method(unusedKeptAndNoLineInfo);
+              assertThat(methodNoLines, isPresent());
+              // TODO(b/213411850): This should depend on native pc support.
+              assertTrue(methodNoLines.hasLineNumberTable());
+            })
+        .inspectStackTrace(ComposePcEncodingTest::checkStackTrace);
+  }
+
+  private static void checkStackTrace(StackTrace stackTrace) {
+    StackTraceLine.Builder builder =
+        StackTraceLine.builder()
+            .setClassName(typeName(TestClass.class))
+            .setFileName(TestClass.class.getSimpleName() + ".java");
+    assertThat(
+        stackTrace,
+        StackTrace.isSame(
+            StackTrace.builder()
+                .add(builder.setMethodName("bar").setLineNumber(15).build())
+                .add(builder.setMethodName("main").setLineNumber(25).build())
+                .build()));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/composepc/TestClass.java b/src/test/java/com/android/tools/r8/debuginfo/composepc/TestClass.java
new file mode 100644
index 0000000..caae028
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/composepc/TestClass.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2023, 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.debuginfo.composepc;
+
+class TestClass {
+
+  public static void foo() {
+    System.out.println("AAA");
+  }
+
+  public static void bar() {
+    System.out.println("BBB");
+    if (System.nanoTime() > 0) {
+      throw new RuntimeException(); // LINE 15 - update ComposePcEncodingTest if changed.
+    }
+  }
+
+  public static void baz() {
+    System.out.println("CCC");
+  }
+
+  public static void main(String[] args) {
+    foo();
+    bar(); // LINE 25 - update ComposePcEncodingTest if changed.
+    baz();
+  }
+
+  // Line removed by transform.
+  public static void unusedKeptAndNoLineInfo() {
+    System.out.println("DDDD");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java b/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java
index bf49805..7bc5d20 100644
--- a/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java
+++ b/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java
@@ -158,6 +158,6 @@
             .disableMethodNotNullCheck()
             .disableAndroidApiLevelCheck()
             .build();
-    return new JumboStringRewriter(method, string, factory).rewrite();
+    return new JumboStringRewriter(method, string, () -> false, factory).rewrite();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationUpperBoundWithInterfacesTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationUpperBoundWithInterfacesTest.java
new file mode 100644
index 0000000..7ac6f56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationUpperBoundWithInterfacesTest.java
@@ -0,0 +1,89 @@
+// Copyright (c) 2023, 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.optimize.argumentpropagation;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+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;
+
+/** Regression test for b/284188592. */
+@RunWith(Parameterized.class)
+public class ArgumentPropagationUpperBoundWithInterfacesTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("42");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // A value with upper bound class type B and interface I.
+      B b = System.currentTimeMillis() > 0 ? new C() : new D();
+      // A virtual invoke with arguments [42] and upper bound class type B and interface I.
+      b.foo(42);
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  interface I {}
+
+  @NoVerticalClassMerging
+  abstract static class A {
+
+    abstract void foo(int i);
+  }
+
+  abstract static class B extends A {}
+
+  @NoHorizontalClassMerging
+  static class C extends B implements I {
+
+    @Override
+    void foo(int i) {
+      System.out.println(i);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class D extends B implements I {
+
+    @Override
+    void foo(int i) {
+      System.out.println(i);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/startup/ForceInlineAfterVerticalClassMergingStartupTest.java b/src/test/java/com/android/tools/r8/startup/ForceInlineAfterVerticalClassMergingStartupTest.java
new file mode 100644
index 0000000..b3951cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/startup/ForceInlineAfterVerticalClassMergingStartupTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2023, 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.startup;
+
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.startup.profile.ExternalStartupClass;
+import com.android.tools.r8.startup.profile.ExternalStartupItem;
+import com.android.tools.r8.startup.profile.ExternalStartupMethod;
+import com.android.tools.r8.startup.utils.StartupTestingUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+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;
+
+/** Regression test for b/284334258. */
+@RunWith(Parameterized.class)
+public class ForceInlineAfterVerticalClassMergingStartupTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    // Create a startup profile with classes A and B to allow that A is merged into B. The method
+    // B.<init>() is marked as a startup method, but A.<init>() is not, despite being called from
+    // B.<init>().
+    Collection<ExternalStartupItem> startupProfile =
+        ImmutableList.of(
+            ExternalStartupClass.builder()
+                .setClassReference(Reference.classFromClass(A.class))
+                .build(),
+            ExternalStartupClass.builder()
+                .setClassReference(Reference.classFromClass(B.class))
+                .build(),
+            ExternalStartupMethod.builder()
+                .setMethodReference(MethodReferenceUtils.instanceConstructor(B.class))
+                .build());
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(A.class))
+        .apply(testBuilder -> StartupTestingUtils.addStartupProfile(testBuilder, startupProfile))
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("B");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new B());
+    }
+  }
+
+  static class A {}
+
+  static class B extends A {
+
+    @Override
+    public String toString() {
+      return "B";
+    }
+  }
+}