Merge commit 'e98da634de940e3d0098e0467d08af80c532139d' into dev-release

Change-Id: I5a07ae67971916c550577b6e84a60da721e8ba3f
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
index 15092b9..ab1c629 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
@@ -301,8 +301,7 @@
     if (paramsValue.isAlwaysNull(appView)) {
       elementValues = Collections.emptyList();
     } else {
-      ArrayValues arrayValues =
-          ValueUtils.computeSingleUseArrayValues(paramsValue, formatInvoke, code);
+      ArrayValues arrayValues = ValueUtils.computeSingleUseArrayValues(paramsValue, formatInvoke);
       if (arrayValues == null) {
         return instructionIterator;
       }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 200fbc6..1fcb8e6 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -474,6 +474,7 @@
       return new ConsequentRootSet(
           neverInlineDueToSingleCaller,
           neverClassInline,
+          noHorizontalClassMerging,
           dependentMinimumKeepInfo,
           dependentKeepClassCompatRule,
           Lists.newArrayList(delayedRootSetActionItems),
@@ -1793,6 +1794,7 @@
 
     final Set<DexMethod> neverInlineDueToSingleCaller;
     final Set<DexType> neverClassInline;
+    final Set<DexType> noHorizontalClassMerging;
     private final DependentMinimumKeepInfoCollection dependentMinimumKeepInfo;
     final Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule;
     final List<DelayedRootSetActionItem> delayedRootSetActionItems;
@@ -1801,12 +1803,14 @@
     RootSetBase(
         Set<DexMethod> neverInlineDueToSingleCaller,
         Set<DexType> neverClassInline,
+        Set<DexType> noHorizontalClassMerging,
         DependentMinimumKeepInfoCollection dependentMinimumKeepInfo,
         Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule,
         List<DelayedRootSetActionItem> delayedRootSetActionItems,
         ProgramMethodMap<ProgramMethod> pendingMethodMoveInverse) {
       this.neverInlineDueToSingleCaller = neverInlineDueToSingleCaller;
       this.neverClassInline = neverClassInline;
+      this.noHorizontalClassMerging = noHorizontalClassMerging;
       this.dependentMinimumKeepInfo = dependentMinimumKeepInfo;
       this.dependentKeepClassCompatRule = dependentKeepClassCompatRule;
       this.delayedRootSetActionItems = delayedRootSetActionItems;
@@ -1833,7 +1837,6 @@
     public final PredicateSet<DexType> alwaysClassInline;
     public final Set<DexType> noUnusedInterfaceRemoval;
     public final Set<DexType> noVerticalClassMerging;
-    public final Set<DexType> noHorizontalClassMerging;
     public final Set<DexMember<?, ?>> neverPropagateValue;
     public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
     public final Set<DexMember<?, ?>> identifierNameStrings;
@@ -1863,6 +1866,7 @@
       super(
           neverInlineDueToSingleCaller,
           neverClassInline,
+          noHorizontalClassMerging,
           dependentMinimumKeepInfo,
           dependentKeepClassCompatRule,
           delayedRootSetActionItems,
@@ -1876,7 +1880,6 @@
       this.alwaysClassInline = alwaysClassInline;
       this.noUnusedInterfaceRemoval = noUnusedInterfaceRemoval;
       this.noVerticalClassMerging = noVerticalClassMerging;
-      this.noHorizontalClassMerging = noHorizontalClassMerging;
       this.neverPropagateValue = neverPropagateValue;
       this.mayHaveSideEffects = mayHaveSideEffects;
       this.identifierNameStrings = Collections.unmodifiableSet(identifierNameStrings);
@@ -1909,6 +1912,7 @@
     void addConsequentRootSet(ConsequentRootSet consequentRootSet) {
       neverInlineDueToSingleCaller.addAll(consequentRootSet.neverInlineDueToSingleCaller);
       neverClassInline.addAll(consequentRootSet.neverClassInline);
+      noHorizontalClassMerging.addAll(consequentRootSet.noHorizontalClassMerging);
       consequentRootSet.dependentKeepClassCompatRule.forEach(
           (type, rules) ->
               dependentKeepClassCompatRule
@@ -2235,6 +2239,7 @@
     ConsequentRootSet(
         Set<DexMethod> neverInlineDueToSingleCaller,
         Set<DexType> neverClassInline,
+        Set<DexType> noHorizontalClassMerging,
         DependentMinimumKeepInfoCollection dependentMinimumKeepInfo,
         Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule,
         List<DelayedRootSetActionItem> delayedRootSetActionItems,
@@ -2242,6 +2247,7 @@
       super(
           neverInlineDueToSingleCaller,
           neverClassInline,
+          noHorizontalClassMerging,
           dependentMinimumKeepInfo,
           dependentKeepClassCompatRule,
           delayedRootSetActionItems,
diff --git a/src/main/java/com/android/tools/r8/utils/DominatorChecker.java b/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
new file mode 100644
index 0000000..46c6716
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DominatorChecker.java
@@ -0,0 +1,175 @@
+// 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.utils;
+
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Given a subgraph defined by sourceBlock / destBlock, where sourceBlock dominates destBlock,
+ * allows querying whether other blocks within this subgraph dominate destBlock.
+ */
+public interface DominatorChecker {
+  boolean check(BasicBlock targetBlock);
+
+  DominatorChecker TRUE_CHECKER = targetBlock -> true;
+  DominatorChecker FALSE_CHECKER = targetBlock -> false;
+
+  class PrecomputedDominatorChecker implements DominatorChecker {
+    private final Set<BasicBlock> dominators;
+
+    public PrecomputedDominatorChecker(Set<BasicBlock> dominators) {
+      this.dominators = dominators;
+    }
+
+    @Override
+    public boolean check(BasicBlock targetBlock) {
+      return dominators.contains(targetBlock);
+    }
+  }
+
+  class TraversingDominatorChecker implements DominatorChecker {
+    private final BasicBlock sourceBlock;
+    private final BasicBlock destBlock;
+    private final Set<BasicBlock> knownDominators;
+    private final ArrayDeque<BasicBlock> workQueue = new ArrayDeque<>();
+    private final Set<BasicBlock> visited;
+    private BasicBlock prevTargetBlock;
+
+    private TraversingDominatorChecker(
+        BasicBlock sourceBlock, BasicBlock destBlock, Set<BasicBlock> knownDominators) {
+      this.sourceBlock = sourceBlock;
+      this.destBlock = destBlock;
+      this.knownDominators = knownDominators;
+      this.visited = Sets.newIdentityHashSet();
+      prevTargetBlock = destBlock;
+    }
+
+    @Override
+    public boolean check(BasicBlock targetBlock) {
+      assert prevTargetBlock != null : "DominatorChecker cannot be used after returning false.";
+      Set<BasicBlock> knownDominators = this.knownDominators;
+      if (knownDominators.contains(targetBlock)) {
+        return true;
+      }
+      // See if a block on the same linear path has already been checked.
+      BasicBlock firstSplittingBlock = targetBlock;
+      if (firstSplittingBlock.hasUniqueSuccessor()) {
+        do {
+          // knownDominators prevents firstSplittingBlock from being destBlock.
+          assert firstSplittingBlock != destBlock;
+          firstSplittingBlock = firstSplittingBlock.getUniqueSuccessor();
+        } while (firstSplittingBlock.hasUniqueSuccessor());
+
+        if (knownDominators.contains(firstSplittingBlock)) {
+          knownDominators.add(targetBlock);
+          return true;
+        }
+      }
+
+      boolean ret;
+      // Since we know the previously checked block is a dominator, narrow the check by using it for
+      // either sourceBlock or destBlock.
+      if (visited.contains(targetBlock)) {
+        visited.clear();
+        ret =
+            checkWithTraversal(prevTargetBlock, destBlock, firstSplittingBlock, visited, workQueue);
+        prevTargetBlock = firstSplittingBlock;
+      } else {
+        ret = checkWithTraversal(sourceBlock, prevTargetBlock, targetBlock, visited, workQueue);
+        prevTargetBlock = targetBlock;
+      }
+      if (ret) {
+        knownDominators.add(targetBlock);
+        if (firstSplittingBlock != targetBlock) {
+          knownDominators.add(firstSplittingBlock);
+        }
+      } else {
+        prevTargetBlock = null;
+      }
+      return ret;
+    }
+
+    private static boolean checkWithTraversal(
+        BasicBlock sourceBlock,
+        BasicBlock destBlock,
+        BasicBlock targetBlock,
+        Set<BasicBlock> visited,
+        ArrayDeque<BasicBlock> workQueue) {
+      assert workQueue.isEmpty();
+
+      visited.add(targetBlock);
+
+      workQueue.addAll(destBlock.getPredecessors());
+      do {
+        BasicBlock curBlock = workQueue.removeLast();
+        if (!visited.add(curBlock)) {
+          continue;
+        }
+        if (curBlock == sourceBlock) {
+          // There is a path from sourceBlock -> destBlock that does not go through block.
+          return false;
+        }
+        assert !curBlock.isEntry() : "sourceBlock did not dominate destBlock";
+        workQueue.addAll(curBlock.getPredecessors());
+      } while (!workQueue.isEmpty());
+
+      return true;
+    }
+  }
+
+  static DominatorChecker create(BasicBlock sourceBlock, BasicBlock destBlock) {
+    // Fast-path: blocks are the same.
+    // As of Nov 2023: in Chrome for String.format() optimization, this covers 77% of cases.
+    if (sourceBlock == destBlock) {
+      return new PrecomputedDominatorChecker(Collections.singleton(sourceBlock));
+    }
+
+    // Shrink the subgraph by moving sourceBlock forward to the first block with multiple
+    // successors.
+    Set<BasicBlock> headAndTailDominators = Sets.newIdentityHashSet();
+    headAndTailDominators.add(sourceBlock);
+    while (sourceBlock.hasUniqueSuccessor()) {
+      sourceBlock = sourceBlock.getUniqueSuccessor();
+      if (!headAndTailDominators.add(sourceBlock)) {
+        // Hit an infinite loop. Code would not verify in this case.
+        assert false;
+        return FALSE_CHECKER;
+      }
+      if (sourceBlock == destBlock) {
+        // As of Nov 2023: in Chrome for String.format() optimization, a linear path from
+        // source->dest was 14% of cases.
+        return new PrecomputedDominatorChecker(headAndTailDominators);
+      }
+    }
+    if (sourceBlock.getSuccessors().isEmpty()) {
+      return FALSE_CHECKER;
+    }
+
+    // Shrink the subgraph by moving destBlock back to the first block with multiple predecessors.
+    headAndTailDominators.add(destBlock);
+    while (destBlock.hasUniquePredecessor()) {
+      destBlock = destBlock.getUniquePredecessor();
+      if (!headAndTailDominators.add(destBlock)) {
+        if (sourceBlock == destBlock) {
+          // This normally happens when moving sourceBlock forwards, but when moving destBlock
+          // backwards when sourceBlock has multiple successors.
+          return new PrecomputedDominatorChecker(headAndTailDominators);
+        }
+        // Hit an infinite loop. Code would not verify in this case.
+        assert false;
+        return FALSE_CHECKER;
+      }
+    }
+
+    if (destBlock.isEntry()) {
+      return FALSE_CHECKER;
+    }
+
+    return new TraversingDominatorChecker(sourceBlock, destBlock, headAndTailDominators);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ValueUtils.java b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
index dcb72ad..509816c 100644
--- a/src/main/java/com/android/tools/r8/utils/ValueUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
@@ -8,27 +8,18 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewArrayFilled;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Value;
-import com.google.common.collect.HashMultiset;
-import com.google.common.collect.Multiset;
-import com.google.common.collect.Sets;
-import java.util.ArrayDeque;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 public class ValueUtils {
   // We allocate an array of this size, so guard against it getting too big.
   private static int MAX_ARRAY_SIZE = 100000;
-  private static boolean DEBUG =
-      System.getProperty("com.android.tools.r8.debug.computeSingleUseArrayValues") != null;
 
   @SuppressWarnings("ReferenceEquality")
   public static boolean isStringBuilder(Value value, DexItemFactory dexItemFactory) {
@@ -94,163 +85,6 @@
     }
   }
 
-  private static BasicBlock findNextUnseen(
-      int startIdx, List<BasicBlock> predecessors, Set<BasicBlock> seen) {
-    int size = predecessors.size();
-    for (int i = startIdx; i < size; ++i) {
-      BasicBlock next = predecessors.get(i);
-      if (!seen.contains(next)) {
-        return next;
-      }
-    }
-    return null;
-  }
-
-  private static void debugLog(IRCode code, String message) {
-    System.err.println(message + " method=" + code.context().getReference());
-  }
-
-  /**
-   * Returns all dominator blocks of destBlock between (and including) it and sourceBlock. Returns
-   * null if sourceBlock is not a dominator of destBlock. Returns null if the algorithm is taking
-   * too long.
-   */
-  private static Set<BasicBlock> computeSimpleCaseDominatorBlocks(
-      BasicBlock sourceBlock, BasicBlock destBlock, IRCode code) {
-    // Fast-path: blocks are the same.
-    // As of Nov 2023 in Chrome for String.format() optimization, this is hit 77% of the time .
-    if (destBlock == sourceBlock) {
-      if (DEBUG) {
-        debugLog(code, "computeSimpleCaseDominatorBlocks: SAME BLOCK");
-      }
-      return Collections.singleton(sourceBlock);
-    }
-
-    // Fast-path: Linear path from source -> dest.
-    // As of Nov 2023 in Chrome for String.format() optimization, this is hit 14% of the time .
-    BasicBlock curBlock = destBlock;
-    List<BasicBlock> curPredecessors;
-    Set<BasicBlock> ret = Sets.newIdentityHashSet();
-    while (true) {
-      curPredecessors = curBlock.getPredecessors();
-      ret.add(curBlock);
-      if (curBlock == sourceBlock) {
-        if (DEBUG) {
-          debugLog(code, "computeSimpleCaseDominatorBlocks: LINEAR PATH");
-        }
-        return ret;
-      }
-
-      BasicBlock nextBlock = findNextUnseen(0, curPredecessors, ret);
-      if (nextBlock == null) {
-        debugLog(code, "computeSimpleCaseDominatorBlocks: Not a dominator.");
-        return null;
-      }
-      if (findNextUnseen(curPredecessors.indexOf(nextBlock) + 1, curPredecessors, ret) != null) {
-        // Multiple predecessors.
-        break;
-      }
-
-      curBlock = nextBlock;
-    }
-
-    // Algorithm: Traverse all predecessor paths between curBlock and sourceBlock, tracking the
-    // number of times each block is visited.
-    // Returns blocks with a "visit count" equal to the number of paths.
-    // This algorithm has worst case exponential complexity (for a fully connected graph).
-    // Rather than falling back to a DominatorTree for complex cases, this currently just returns
-    // an empty set when the number of paths is not small. Thus, it should not be used when false
-    // negatives are not acceptable.
-    //
-    // As of Nov 2023 in Chrome for String.format() optimization, the totalPathCounts were:
-    // 2 * 12, 3 * 6, 4 * 2, 5 * 1, 12 * 1, 22 * 1, 24 * 1, 35 * 4,
-    // MAX_PATH_COUNT * 2 (even with MAX_PATH_COUNT=3200)
-    final int MAX_PATH_COUNT = 36;
-
-    // TODO(agrieve): Lower MAX_PATH_COUNT and use DominatorTree for complicated cases. Or...
-    //     Algorithm vNext:
-    //     Track the fraction of paths that reach each node. Doing so would be ~linear for graphs
-    //     without cycles. E.g.:
-    //       DestNodeValue=1
-    //       NodeValue=SUM(block.NodeValue / block.numSuccessors for block in successors)
-    //       Dominators=(b for b in blocks if b.NodeValue == 1)
-    //     To choose which block to visit next, choose any where all predecessors have already been
-    //     visited. If no block has all predecessors visited, then an unvisited block is from a
-    //     cycle (they cannot be from outside of the sourceBlock->destBlock subgraph so long as
-    //     sourceBlock dominates destBlock).
-    //     To deal with cycles (clearly not linear time now):
-    //     * Find all blocks that participate in a cycle by doing a full traversal starting from
-    //       each candidate until one is found. Store the set of edges that do not reach sourceBlock
-    //       (except through the cycle).
-    //     * When computing the value of a block whose successors participate in a cycle:
-    //       NodeValue=SUM(block.NodeValue / effectiveNumSuccessors for block in successors)
-    //       where effectiveNumSuccessors=SUM(1 for s in successors if (b->s) not in cycleOnlyEdges)
-
-    Multiset<BasicBlock> blockCounts = HashMultiset.create();
-    int totalPathCount = 0;
-
-    // Should never need to re-visit initial single-track nodes.
-    Set<BasicBlock> seen = Sets.newIdentityHashSet();
-    seen.addAll(ret);
-    ArrayDeque<BasicBlock> pathStack = new ArrayDeque<>();
-
-    pathStack.add(curBlock);
-    pathStack.add(curPredecessors.get(0));
-    while (true) {
-      curBlock = pathStack.getLast();
-      curPredecessors = curBlock.getPredecessors();
-      if (curBlock == sourceBlock) {
-        // Add every block for every connected path.
-        blockCounts.addAll(pathStack);
-        totalPathCount += 1;
-        if (totalPathCount > MAX_PATH_COUNT) {
-          if (DEBUG) {
-            debugLog(code, "computeSimpleCaseDominatorBlocks: Reached MAX_PATH_COUNT");
-          }
-          return null;
-        }
-      } else if (!seen.contains(curBlock)) {
-        if (curPredecessors.isEmpty()) {
-          // Finding the entry block means the sourceBlock is not a dominator.
-          if (DEBUG) {
-            debugLog(code, "computeSimpleCaseDominatorBlocks: sourceBlock not a dominator");
-          }
-          return null;
-        }
-        // Going deeper.
-        BasicBlock nextBlock = findNextUnseen(0, curPredecessors, seen);
-        if (nextBlock != null) {
-          seen.add(curBlock);
-          pathStack.add(nextBlock);
-          continue;
-        }
-      } else {
-        seen.remove(curBlock);
-      }
-      // Popping.
-      pathStack.removeLast();
-      List<BasicBlock> prevPredecessors = pathStack.getLast().getPredecessors();
-      int nextBlockIdx = prevPredecessors.indexOf(curBlock) + 1;
-      BasicBlock nextBlock = findNextUnseen(nextBlockIdx, prevPredecessors, seen);
-      if (nextBlock != null) {
-        pathStack.add(nextBlock);
-      } else if (pathStack.size() == 1) {
-        break;
-      }
-    }
-
-    for (var entry : blockCounts.entrySet()) {
-      if (entry.getCount() == totalPathCount) {
-        ret.add(entry.getElement());
-      }
-    }
-    if (DEBUG) {
-      debugLog(code, "computeSimpleCaseDominatorBlocks: PATH COUNT " + totalPathCount);
-    }
-
-    return ret;
-  }
-
   /**
    * Attempts to determine all values for the given array. This will work only when:
    *
@@ -260,7 +94,10 @@
    *   * When users are in different blocks, their order is hard to know.
    * 2) The array size is a constant.
    * 3) All array-put instructions have constant and unique indices.
-   *   * Indices must be unique because order is hard to know when multiple blocks are concerned.
+   *   * With the exception of array-puts that are in the same block as singleUser, in which case
+   *     non-unique puts are allowed.
+   *   * Indices must be unique in other blocks because order is hard to know when multiple blocks
+   *     are concerned (and reassignment is rare anyways).
    * 4) The array-put instructions are guaranteed to be executed before singleUser.
    * </pre>
    *
@@ -268,8 +105,7 @@
    * @param singleUser The only non-array-put user, or null to auto-detect.
    * @return The computed array values, or null if they could not be determined.
    */
-  public static ArrayValues computeSingleUseArrayValues(
-      Value arrayValue, Instruction singleUser, IRCode code) {
+  public static ArrayValues computeSingleUseArrayValues(Value arrayValue, Instruction singleUser) {
     assert singleUser == null || arrayValue.uniqueUsers().contains(singleUser);
     TypeElement arrayType = arrayValue.getType();
     if (!arrayType.isArrayType() || arrayValue.hasDebugUsers() || arrayValue.isPhi()) {
@@ -309,15 +145,8 @@
       }
     }
 
-    // Ensure that all paths from new-array-empty to |usage| contain all array-put instructions.
-    Set<BasicBlock> dominatorBlocks =
-        computeSimpleCaseDominatorBlocks(definition.getBlock(), singleUser.getBlock(), code);
-    if (dominatorBlocks == null) {
-      return null;
-    }
-    BasicBlock usageBlock = singleUser.getBlock();
-
     ArrayPut[] arrayPutsByIndex = new ArrayPut[arraySize];
+    BasicBlock usageBlock = singleUser.getBlock();
     for (Instruction user : arrayValue.uniqueUsers()) {
       ArrayPut arrayPut = user.asArrayPut();
       if (arrayPut == null || arrayPut.array() != arrayValue || arrayPut.value() == arrayValue) {
@@ -327,22 +156,26 @@
         // Found a second non-array-put user.
         return null;
       }
-      int index = arrayPut.indexIfConstAndInBounds(arraySize);
-      if (index < 0) {
-        return null;
-      }
-      if (arrayPut.getBlock() == usageBlock) {
-        // Process these later.
+      // Process same-block instructions later.
+      if (user.getBlock() == usageBlock) {
         continue;
-      } else if (!dominatorBlocks.contains(arrayPut.getBlock())) {
-        return null;
       }
+      int index = arrayPut.indexIfConstAndInBounds(arraySize);
       // We do not know what order blocks are in, so do not allow re-assignment.
-      if (arrayPutsByIndex[index] != null) {
+      if (index < 0 || arrayPutsByIndex[index] != null) {
         return null;
       }
       arrayPutsByIndex[index] = arrayPut;
     }
+
+    // Ensure that all paths from new-array-empty to |usage| contain all array-put instructions.
+    DominatorChecker dominatorChecker = DominatorChecker.create(definition.getBlock(), usageBlock);
+    for (Instruction user : arrayValue.uniqueUsers()) {
+      if (!dominatorChecker.check(user.getBlock())) {
+        return null;
+      }
+    }
+
     boolean seenSingleUser = false;
     for (Instruction inst : usageBlock.getInstructions()) {
       if (inst == singleUser) {
@@ -357,7 +190,10 @@
         // Found an array-put after the array was used. This is too uncommon of a thing to support.
         return null;
       }
-      int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
+      int index = arrayPut.indexIfConstAndInBounds(arraySize);
+      if (index < 0) {
+        return null;
+      }
       // We can allow reassignment at this point since we are visiting in order.
       arrayPutsByIndex[index] = arrayPut;
     }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java b/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
index 5089061..b710916 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
@@ -16,6 +16,7 @@
 
 package com.android.build.shrinker;
 
+import static com.android.ide.common.resources.ResourcesUtil.resourceNameToFieldName;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.lang.Character.isDigit;
 
@@ -122,7 +123,7 @@
                                 haveSlash |= c == '/';
                                 formatting |= c == '%';
                                 justName =
-                                        justName && !(c == '.' || c == ':' || c == '%' || c == '/');
+                                        justName && !(c == ':' || c == '%' || c == '/');
                             }
 
                             Stream<Resource> reachable = Streams.concat(
@@ -170,7 +171,7 @@
         // Check for a simple prefix match, e.g. as in
         // getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
         return resourceStore.getResources().stream()
-                .filter(resource -> resource.name.startsWith(string));
+                .filter(resource -> resource.name.startsWith(resourceNameToFieldName(string)));
     }
 
     private Stream<Resource> possibleFormatting(String string) {
@@ -191,7 +192,7 @@
         // Try to pick out the resource name pieces; if we can find the
         // resource type unambiguously; if not, just match on names
         int slash = string.indexOf('/');
-        String name = string.substring(slash + 1);
+        String name = resourceNameToFieldName(string.substring(slash + 1));
         if (name.isEmpty() || !names.contains(name)) {
             return Stream.empty();
         }
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java
new file mode 100644
index 0000000..1e28ef0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java
@@ -0,0 +1,77 @@
+// 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.androidresources;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+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 ResourceNameWithDotsRegressionTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean debug;
+
+  @Parameter(2)
+  public boolean optimized;
+
+  @Parameters(name = "{0}, debug: {1}, optimized: {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addStringValue("foo.bar", "the foobar string")
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    AndroidTestResource testResources = getTestResources(temp);
+    Path resourcesClass = AndroidResourceTestingUtils.resourcesClassAsDex(temp);
+    byte[] withAnroidResourcesReference =
+        AndroidResourceTestingUtils.transformResourcesReferences(FooBar.class);
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClassFileData(withAnroidResourcesReference)
+        .addRunClasspathFiles(resourcesClass)
+        .addAndroidResources(testResources)
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "foo.bar");
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        Resources resources = new Resources();
+        resources.getIdentifier("foo.bar", "string", "mypackage");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/Resources.java b/src/test/java/com/android/tools/r8/androidresources/Resources.java
index ef01a99..e702f2a 100644
--- a/src/test/java/com/android/tools/r8/androidresources/Resources.java
+++ b/src/test/java/com/android/tools/r8/androidresources/Resources.java
@@ -13,4 +13,8 @@
   public String getString(int id) {
     return GET_STRING_VALUE;
   }
+
+  public int getIdentifier(String name, String defType, String defPackage) {
+    return 42;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java
new file mode 100644
index 0000000..15f1e9c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/InvokeSpecialToImmediateInterfaceTest.java
@@ -0,0 +1,129 @@
+// 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.graph.invokespecial;
+
+import static com.android.tools.r8.utils.DescriptorUtils.getBinaryNameFromJavaType;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+
+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.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import java.io.IOException;
+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 InvokeSpecialToImmediateInterfaceTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addProgramClasses(I.class, Main.class)
+        .addProgramClassFileData(getClassWithTransformedInvoked())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class, Main.class)
+        .addProgramClassFileData(getClassWithTransformedInvoked())
+        .release()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/313065227): Should succeed.
+        .applyIf(
+            parameters.getDexRuntimeVersion().isEqualToOneOf(Version.V5_1_1, Version.V6_0_1),
+            runResult -> runResult.assertFailureWithErrorThatMatches(containsString("SIGSEGV")),
+            runResult -> runResult.assertFailureWithErrorThatThrows(NoSuchMethodError.class));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Box<R8TestCompileResult> compileResult = new Box<>();
+
+    // TODO(b/313065227): Should succeed.
+    AssertUtils.assertFailsCompilationIf(
+        parameters.isCfRuntime(),
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClasses(I.class, Main.class)
+                .addProgramClassFileData(getClassWithTransformedInvoked())
+                .addKeepMainRule(Main.class)
+                .setMinApi(parameters)
+                .compile()
+                .apply(compileResult::set));
+
+    if (!compileResult.isSet()) {
+      assertTrue(parameters.isCfRuntime());
+      return;
+    }
+
+    // TODO(b/313065227): Should succeed.
+    compileResult
+        .get()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  private byte[] getClassWithTransformedInvoked() throws IOException {
+    return transformer(A.class)
+        .transformMethodInsnInMethod(
+            "bar",
+            (opcode, owner, name, descriptor, isInterface, continuation) -> {
+              assertEquals(INVOKEVIRTUAL, opcode);
+              assertEquals(owner, getBinaryNameFromJavaType(A.class.getTypeName()));
+              continuation.visitMethodInsn(
+                  INVOKESPECIAL,
+                  getBinaryNameFromJavaType(A.class.getTypeName()),
+                  name,
+                  descriptor,
+                  isInterface);
+            })
+        .transform();
+  }
+
+  public interface I {
+
+    default void foo() {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  public static class A implements I {
+
+    public void bar() {
+      foo(); // Will be rewritten to invoke-special A.foo()
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new A().bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
new file mode 100644
index 0000000..ee1748d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
@@ -0,0 +1,139 @@
+// 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.keepanno.doctests;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
+import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
+import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
+import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConditionalMethodRulesAndHorizontalMergingTest extends TestBase {
+
+  static final String EXPECTED =
+      StringUtils.lines("on Base", "on Sub", "intField = 42", "stringField = Hello!");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ConditionalMethodRulesAndHorizontalMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(TestClass.class)
+        // The following rules are what was generated by the keepanno extraction for the doc tests.
+        .addKeepRules(
+            "-if class " + typeName(MyHiddenMethodCaller.class) + " {",
+            "  void callHiddenMethod(" + typeName(BaseClass.class) + ");",
+            "}",
+            "-keepclassmembers class * extends " + typeName(BaseClass.class) + " {",
+            "  *** hiddenMethod();",
+            "}",
+            "-if class " + typeName(MyHiddenMethodCaller.class) + " {",
+            "  void callHiddenMethod(" + typeName(BaseClass.class) + ");",
+            "}",
+            "-keepclassmembers class " + typeName(BaseClass.class) + " {",
+            "  *** hiddenMethod();",
+            "}",
+            "-if class " + typeName(MyFieldValuePrinter.class) + " {",
+            "  void printFieldValues(" + typeName(PrintableFieldInterface.class) + ");",
+            "}",
+            "-keepclassmembers class * extends " + typeName(PrintableFieldInterface.class) + " {",
+            "  *** *;",
+            "}"
+            // Removed the rule variant on the base interface as it does not have fields.
+            )
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  public List<Class<?>> getExampleClasses() {
+    return ImmutableList.of(Example1.class, Example2.class);
+  }
+
+  static class TestClass {
+    public static void main(String[] args) throws Exception {
+      Example1.run();
+      Example2.run();
+    }
+  }
+
+  static class Example1 {
+
+    static class BaseClass {
+      void hiddenMethod() {
+        System.out.println("on Base");
+      }
+    }
+
+    static class SubClass extends BaseClass {
+      void hiddenMethod() {
+        System.out.println("on Sub");
+      }
+    }
+
+    public static class MyHiddenMethodCaller {
+      public void callHiddenMethod(BaseClass base) throws Exception {
+        base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
+      }
+    }
+
+    static void run() throws Exception {
+      new MyHiddenMethodCaller().callHiddenMethod(new BaseClass());
+      new MyHiddenMethodCaller().callHiddenMethod(new SubClass());
+    }
+  }
+
+  static class Example2 {
+
+    interface PrintableFieldInterface {}
+
+    static class ClassWithFields implements PrintableFieldInterface {
+      final int intField = 42;
+      String stringField = "Hello!";
+    }
+
+    public static class MyFieldValuePrinter {
+      public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+        for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+          System.out.println(field.getName() + " = " + field.get(objectWithFields));
+        }
+      }
+    }
+
+    static void run() throws Exception {
+      new MyFieldValuePrinter().printFieldValues(new ClassWithFields());
+    }
+  }
+}
diff --git a/tools/compiledump.py b/tools/compiledump.py
index ce09db8..3abe651 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -95,6 +95,21 @@
         'Disable Java assertions when running the compiler (default enabled)',
         default=False,
         action='store_true')
+    parser.add_argument(
+        '--enable-test-assertions',
+        '--enable_test_assertions',
+        help=
+        'Enable additional test assertions when running the compiler (default disabled)',
+        default=False,
+        action='store_true')
+    parser.add_argument(
+        '--java-opts',
+        '--java-opts',
+        '-J',
+        metavar='<JVM argument(s)>',
+        default=[],
+        action='append',
+        help='Additional options to pass to JVM invocation')
     parser.add_argument('--classfile',
                         help='Run with classfile output',
                         default=False,
@@ -559,6 +574,7 @@
             cmd.append('-Xmx' + args.xmx)
         if not args.disable_assertions:
             cmd.append('-ea')
+        if args.enable_test_assertions:
             cmd.append('-Dcom.android.tools.r8.enableTestAssertions=1')
         if args.print_times:
             cmd.append('-Dcom.android.tools.r8.printtimes=1')
@@ -567,6 +583,7 @@
         if hasattr(args, 'properties'):
             cmd.extend(args.properties)
         cmd.extend(determine_properties(build_properties))
+        cmd.extend(args.java_opts)
         cmd.extend(['-cp', '%s:%s' % (temp, jar)])
         if compiler == 'd8':
             prepare_d8_wrapper(jar, temp, jdkhome)
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index c06e878..964d1dc 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -743,6 +743,7 @@
         'properties': app.compiler_properties,
         'disable_desugared_lib': False,
         'print_times': options.print_times,
+        'java_opts': [],
     })
 
     app_jar = os.path.join(
diff --git a/tools/tag_versions.py b/tools/tag_versions.py
index 243625a..6f62ff7 100755
--- a/tools/tag_versions.py
+++ b/tools/tag_versions.py
@@ -125,7 +125,8 @@
         ]).decode('utf-8')
         version = output.split(' ')[0]
         run(args, ['git', 'tag', '-f', tag, '-m', tag, '%s^{}' % version])
-        run(args, ['git', 'push', 'origin', tag])
+        run(args, ['git', 'push', '-o', 'push-justification=b/313360935',
+                   'origin', tag])
 
 
 def tag_r8_branch(branch, args):
@@ -145,7 +146,8 @@
         result = get_tag_info_on_origin(version)
         if not result:
             run(args, ['git', 'tag', '-a', version, '-m', version, hash])
-            run(args, ['git', 'push', 'origin', version])
+            run(args, ['git', 'push',  '-o', 'push-justification=b/313360935',
+                       'origin', version])
     if args.dry_run:
         print('Dry run complete. None of the above have been executed.')