Split throw catch rewritings from CodeRewriter

Bug: b/284304606
Change-Id: I538c7715b59c872d3c2346990439393beb86c7fc
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index aa7b371..cba30d9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.conversion.passes.CommonSubexpressionElimination;
 import com.android.tools.r8.ir.conversion.passes.ParentConstructorHoistingCodeRewriter;
 import com.android.tools.r8.ir.conversion.passes.SplitBranch;
+import com.android.tools.r8.ir.conversion.passes.ThrowCatchOptimizer;
 import com.android.tools.r8.ir.conversion.passes.TrivialCheckCastAndInstanceOfRemover;
 import com.android.tools.r8.ir.conversion.passes.TrivialPhiSimplifier;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
@@ -770,7 +771,7 @@
     new SparseConditionalConstantPropagation(appView, code).run();
     timing.end();
     timing.begin("Rewrite always throwing instructions");
-    codeRewriter.optimizeAlwaysThrowingInstructions(code);
+    new ThrowCatchOptimizer(appView).optimizeAlwaysThrowingInstructions(code);
     timing.end();
     timing.begin("Simplify control flow");
     if (new BranchSimplifier(appView).simplifyBranches(code)) {
@@ -801,7 +802,7 @@
 
     if (!isDebugMode) {
       timing.begin("Rewrite throw NPE");
-      codeRewriter.rewriteThrowNullPointerException(code);
+      new ThrowCatchOptimizer(appView).rewriteThrowNullPointerException(code);
       timing.end();
       previous = printMethod(code, "IR after rewrite throw null (SSA)", previous);
     }
@@ -933,7 +934,7 @@
     }
 
     timing.begin("Redundant catch/rethrow elimination");
-    codeRewriter.optimizeRedundantCatchRethrowInstructions(code);
+    new ThrowCatchOptimizer(appView).optimizeRedundantCatchRethrowInstructions(code);
     timing.end();
     previous = printMethod(code, "IR after redundant catch/rethrow elimination (SSA)", previous);
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
new file mode 100644
index 0000000..8b170c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
@@ -0,0 +1,456 @@
+// 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.ir.conversion.passes;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
+import com.android.tools.r8.ir.code.Goto;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.InstanceFieldInstruction;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Instruction.SideEffectAssumption;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import java.util.BitSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+public class ThrowCatchOptimizer {
+
+  private final AppView<?> appView;
+  private final DexItemFactory dexItemFactory;
+
+  public ThrowCatchOptimizer(AppView<?> appView) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  // Rewrite 'throw new NullPointerException()' to 'throw null'.
+  public void rewriteThrowNullPointerException(IRCode code) {
+    boolean shouldRemoveUnreachableBlocks = false;
+    for (BasicBlock block : code.blocks) {
+      InstructionListIterator it = block.listIterator(code);
+      while (it.hasNext()) {
+        Instruction instruction = it.next();
+
+        // Check for the patterns 'if (x == null) throw null' and
+        // 'if (x == null) throw new NullPointerException()'.
+        if (instruction.isIf()) {
+          if (appView
+              .dexItemFactory()
+              .objectsMethods
+              .isRequireNonNullMethod(code.method().getReference())) {
+            continue;
+          }
+
+          If ifInstruction = instruction.asIf();
+          if (!ifInstruction.isZeroTest()) {
+            continue;
+          }
+
+          Value value = ifInstruction.lhs();
+          if (!value.getType().isReferenceType()) {
+            assert value.getType().isPrimitiveType();
+            continue;
+          }
+
+          BasicBlock valueIsNullTarget = ifInstruction.targetFromCondition(0);
+          if (valueIsNullTarget.getPredecessors().size() != 1
+              || !valueIsNullTarget.exit().isThrow()) {
+            continue;
+          }
+
+          Throw throwInstruction = valueIsNullTarget.exit().asThrow();
+          Value exceptionValue = throwInstruction.exception().getAliasedValue();
+          if (!exceptionValue.isConstZero()
+              && exceptionValue.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
+            NewInstance newInstance = exceptionValue.definition.asNewInstance();
+            if (newInstance.clazz != dexItemFactory.npeType) {
+              continue;
+            }
+            if (newInstance.outValue().numberOfAllUsers() != 2) {
+              continue; // Could be mutated before it is thrown.
+            }
+            InvokeDirect constructorCall = newInstance.getUniqueConstructorInvoke(dexItemFactory);
+            if (constructorCall == null) {
+              continue;
+            }
+            if (constructorCall.getInvokedMethod() != dexItemFactory.npeMethods.init) {
+              continue;
+            }
+          } else if (!exceptionValue.isConstZero()) {
+            continue;
+          }
+
+          boolean canDetachValueIsNullTarget = true;
+          for (Instruction i : valueIsNullTarget.instructionsBefore(throwInstruction)) {
+            if (!i.isBlockLocalInstructionWithoutSideEffects(appView, code.context())) {
+              canDetachValueIsNullTarget = false;
+              break;
+            }
+          }
+          if (!canDetachValueIsNullTarget) {
+            continue;
+          }
+
+          insertNotNullCheck(
+              block,
+              it,
+              ifInstruction,
+              ifInstruction.targetFromCondition(1),
+              valueIsNullTarget,
+              throwInstruction.getPosition());
+          shouldRemoveUnreachableBlocks = true;
+        }
+
+        // Check for 'new-instance NullPointerException' with 2 users, not declaring a local and
+        // not ending the scope of any locals.
+        if (instruction.isNewInstance()
+            && instruction.asNewInstance().clazz == dexItemFactory.npeType
+            && instruction.outValue().numberOfAllUsers() == 2
+            && !instruction.outValue().hasLocalInfo()
+            && instruction.getDebugValues().isEmpty()) {
+          if (it.hasNext()) {
+            Instruction instruction2 = it.next();
+            // Check for 'invoke NullPointerException.init() not ending the scope of any locals
+            // and with the result of the first instruction as the argument. Also check that
+            // the two first instructions have the same position.
+            if (instruction2.isInvokeDirect() && instruction2.getDebugValues().isEmpty()) {
+              InvokeDirect invokeDirect = instruction2.asInvokeDirect();
+              if (invokeDirect.getInvokedMethod() == dexItemFactory.npeMethods.init
+                  && invokeDirect.getReceiver() == instruction.outValue()
+                  && invokeDirect.arguments().size() == 1
+                  && invokeDirect.getPosition() == instruction.getPosition()) {
+                if (it.hasNext()) {
+                  Instruction instruction3 = it.next();
+                  // Finally check that the last instruction is a throw of the initialized
+                  // exception object and replace with 'throw null' if so.
+                  if (instruction3.isThrow()
+                      && instruction3.asThrow().exception() == instruction.outValue()) {
+                    // Create const 0 with null type and a throw using that value.
+                    Instruction nullPointer = code.createConstNull();
+                    Instruction throwInstruction = new Throw(nullPointer.outValue());
+                    // Preserve positions: we have checked that the first two original instructions
+                    // have the same position.
+                    assert instruction.getPosition() == instruction2.getPosition();
+                    nullPointer.setPosition(instruction.getPosition());
+                    throwInstruction.setPosition(instruction3.getPosition());
+                    // Copy debug values from original throw to new throw to correctly end scope
+                    // of locals.
+                    instruction3.moveDebugValues(throwInstruction);
+                    // Remove the three original instructions.
+                    it.remove();
+                    it.previous();
+                    it.remove();
+                    it.previous();
+                    it.remove();
+                    // Replace them with 'const 0' and 'throw'.
+                    it.add(nullPointer);
+                    it.add(throwInstruction);
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    if (shouldRemoveUnreachableBlocks) {
+      Set<Value> affectedValues = code.removeUnreachableBlocks();
+      if (!affectedValues.isEmpty()) {
+        new TypeAnalysis(appView).narrowing(affectedValues);
+      }
+    }
+    assert code.isConsistentSSA(appView);
+  }
+
+  // Find all instructions that always throw, split the block after each such instruction and follow
+  // it with a block throwing a null value (which should result in NPE). Note that this throw is not
+  // expected to be ever reached, but is intended to satisfy verifier.
+  public void optimizeAlwaysThrowingInstructions(IRCode code) {
+    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
+    ListIterator<BasicBlock> blockIterator = code.listIterator();
+    ProgramMethod context = code.context();
+    boolean hasUnlinkedCatchHandlers = false;
+    // For cyclic phis we sometimes do not propagate the dynamic upper type after rewritings.
+    // The inValue.isAlwaysNull(appView) check below will not recompute the dynamic type of phi's
+    // so we recompute all phis here if they are always null.
+    AppView<AppInfoWithClassHierarchy> appViewWithClassHierarchy =
+        appView.hasClassHierarchy() ? appView.withClassHierarchy() : null;
+    if (appViewWithClassHierarchy != null) {
+      code.blocks.forEach(
+          block ->
+              block
+                  .getPhis()
+                  .forEach(
+                      phi -> {
+                        if (!phi.getType().isDefinitelyNull()) {
+                          TypeElement dynamicUpperBoundType =
+                              phi.getDynamicUpperBoundType(appViewWithClassHierarchy);
+                          if (dynamicUpperBoundType.isDefinitelyNull()) {
+                            affectedValues.add(phi);
+                            phi.setType(dynamicUpperBoundType);
+                          }
+                        }
+                      }));
+    }
+    while (blockIterator.hasNext()) {
+      BasicBlock block = blockIterator.next();
+      if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) {
+        continue;
+      }
+      if (blocksToRemove.contains(block)) {
+        continue;
+      }
+      InstructionListIterator instructionIterator = block.listIterator(code);
+      while (instructionIterator.hasNext()) {
+        Instruction instruction = instructionIterator.next();
+        if (instruction.throwsOnNullInput()) {
+          Value inValue = instruction.getNonNullInput();
+          if (inValue.isAlwaysNull(appView)) {
+            // Insert `throw null` after the instruction if it is not guaranteed to throw an NPE.
+            if (instruction.isAssume()) {
+              // If this assume is in a block with catch handlers, then the out-value can have
+              // usages in the catch handler if the block's throwing instruction comes after the
+              // assume instruction. In this case, the catch handler is also guaranteed to be dead,
+              // so we detach it from the current block.
+              if (block.hasCatchHandlers()
+                  && block.isInstructionBeforeThrowingInstruction(instruction)) {
+                for (CatchHandler<BasicBlock> catchHandler : block.getCatchHandlers()) {
+                  catchHandler.getTarget().unlinkCatchHandler();
+                }
+                hasUnlinkedCatchHandlers = true;
+              }
+            } else if (instruction.isInstanceFieldInstruction()) {
+              InstanceFieldInstruction instanceFieldInstruction =
+                  instruction.asInstanceFieldInstruction();
+              if (instanceFieldInstruction.instructionInstanceCanThrow(
+                  appView, context, SideEffectAssumption.RECEIVER_NOT_NULL)) {
+                instructionIterator.next();
+              }
+            } else if (instruction.isInvokeMethodWithReceiver()) {
+              InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
+              SideEffectAssumption assumption =
+                  SideEffectAssumption.RECEIVER_NOT_NULL.join(
+                      SideEffectAssumption.INVOKED_METHOD_DOES_NOT_HAVE_SIDE_EFFECTS);
+              if (invoke.instructionMayHaveSideEffects(appView, context, assumption)) {
+                instructionIterator.next();
+              }
+            }
+            instructionIterator.replaceCurrentInstructionWithThrowNull(
+                appView, code, blockIterator, blocksToRemove, affectedValues);
+            continue;
+          }
+        }
+
+        if (!instruction.isInvokeMethod()) {
+          continue;
+        }
+
+        InvokeMethod invoke = instruction.asInvokeMethod();
+        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
+        if (singleTarget == null) {
+          continue;
+        }
+
+        MethodOptimizationInfo optimizationInfo =
+            singleTarget.getDefinition().getOptimizationInfo();
+
+        // If the invoke instruction is a null check, we can remove it.
+        boolean isNullCheck = false;
+        if (optimizationInfo.hasNonNullParamOrThrow()) {
+          BitSet nonNullParamOrThrow = optimizationInfo.getNonNullParamOrThrow();
+          for (int i = 0; i < invoke.arguments().size(); i++) {
+            Value argument = invoke.arguments().get(i);
+            if (argument.isAlwaysNull(appView) && nonNullParamOrThrow.get(i)) {
+              isNullCheck = true;
+              break;
+            }
+          }
+        }
+        // If the invoke instruction never returns normally, we can insert a throw null instruction
+        // after the invoke.
+        if (isNullCheck || optimizationInfo.neverReturnsNormally()) {
+          instructionIterator.setInsertionPosition(invoke.getPosition());
+          instructionIterator.next();
+          instructionIterator.replaceCurrentInstructionWithThrowNull(
+              appView, code, blockIterator, blocksToRemove, affectedValues);
+          instructionIterator.unsetInsertionPosition();
+        }
+      }
+    }
+    code.removeBlocks(blocksToRemove);
+    if (hasUnlinkedCatchHandlers) {
+      affectedValues.addAll(code.removeUnreachableBlocks());
+    }
+    assert code.getUnreachableBlocks().isEmpty();
+    if (!affectedValues.isEmpty()) {
+      new TypeAnalysis(appView).narrowing(affectedValues);
+    }
+    assert code.isConsistentSSA(appView);
+  }
+
+  // Find any case where we have a catch followed immediately and only by a rethrow. This is extra
+  // code doing what the JVM does automatically and can be safely elided.
+  public void optimizeRedundantCatchRethrowInstructions(IRCode code) {
+    ListIterator<BasicBlock> blockIterator = code.listIterator();
+    boolean hasUnlinkedCatchHandlers = false;
+    while (blockIterator.hasNext()) {
+      BasicBlock blockWithHandlers = blockIterator.next();
+      if (blockWithHandlers.hasCatchHandlers()) {
+        boolean allHandlersAreTrivial = true;
+        for (CatchHandler<BasicBlock> handler : blockWithHandlers.getCatchHandlers()) {
+          if (!isSingleHandlerTrivial(handler.target, code)) {
+            allHandlersAreTrivial = false;
+            break;
+          }
+        }
+        // We need to ensure all handlers are trivial to unlink, since if one is non-trivial, and
+        // its guard is a parent type to a trivial one, removing the trivial catch will result in
+        // us hitting the non-trivial catch. This could be avoided by more sophisticated type
+        // analysis.
+        if (allHandlersAreTrivial) {
+          hasUnlinkedCatchHandlers = true;
+          for (CatchHandler<BasicBlock> handler : blockWithHandlers.getCatchHandlers()) {
+            handler.getTarget().unlinkCatchHandler();
+          }
+        }
+      }
+    }
+    if (hasUnlinkedCatchHandlers) {
+      code.removeUnreachableBlocks();
+    }
+  }
+
+  private boolean isSingleHandlerTrivial(BasicBlock firstBlock, IRCode code) {
+    InstructionListIterator instructionIterator = firstBlock.listIterator(code);
+    Instruction instruction = instructionIterator.next();
+    if (!instruction.isMoveException()) {
+      // A catch handler which doesn't use its exception is not going to be a trivial rethrow.
+      return false;
+    }
+    Value exceptionValue = instruction.outValue();
+    if (!isPotentialTrivialRethrowValue(exceptionValue)) {
+      return false;
+    }
+    while (instructionIterator.hasNext()) {
+      instruction = instructionIterator.next();
+      BasicBlock currentBlock = instruction.getBlock();
+      if (instruction.isGoto()) {
+        BasicBlock nextBlock = instruction.asGoto().getTarget();
+        int predecessorIndex = nextBlock.getPredecessors().indexOf(currentBlock);
+        Value phiAliasOfExceptionValue = null;
+        for (Phi phi : nextBlock.getPhis()) {
+          Value operand = phi.getOperand(predecessorIndex);
+          if (exceptionValue == operand) {
+            phiAliasOfExceptionValue = phi;
+            break;
+          }
+        }
+        if (phiAliasOfExceptionValue != null) {
+          if (!isPotentialTrivialRethrowValue(phiAliasOfExceptionValue)) {
+            return false;
+          }
+          exceptionValue = phiAliasOfExceptionValue;
+        }
+        instructionIterator = nextBlock.listIterator(code);
+      } else if (instruction.isThrow()) {
+        List<Value> throwValues = instruction.inValues();
+        assert throwValues.size() == 1;
+        if (throwValues.get(0) != exceptionValue) {
+          return false;
+        }
+        CatchHandlers<BasicBlock> currentHandlers = currentBlock.getCatchHandlers();
+        if (!currentHandlers.isEmpty()) {
+          // This is the case where our trivial catch handler has catch handler(s). For now, we
+          // will only treat our block as trivial if all its catch handlers are also trivial.
+          // Note: it is possible that we could "bridge" a trivial handler, where we take the
+          // handlers of the handler and bring them up to replace the trivial handler. Example:
+          //   catch (Throwable t) {
+          //     try { throw t; } catch(Throwable abc) { foo(abc); }
+          //   }
+          // could turn into:
+          //   catch (Throwable abc) {
+          //     foo(abc);
+          //   }
+          // However this gets significantly harder when you have to consider non-matching guard
+          // types.
+          for (CatchHandler<BasicBlock> handler : currentHandlers) {
+            if (!isSingleHandlerTrivial(handler.getTarget(), code)) {
+              return false;
+            }
+          }
+        }
+        return true;
+      } else {
+        // Any other instructions in the catch handler means it's not trivial, and thus we can't
+        // elide.
+        return false;
+      }
+    }
+    throw new Unreachable("Triviality check should always return before the loop terminates");
+  }
+
+  private boolean isPotentialTrivialRethrowValue(Value exceptionValue) {
+    if (exceptionValue.hasDebugUsers()) {
+      return false;
+    }
+    if (exceptionValue.hasUsers()) {
+      if (exceptionValue.hasPhiUsers()
+          || !exceptionValue.hasSingleUniqueUser()
+          || !exceptionValue.singleUniqueUser().isThrow()) {
+        return false;
+      }
+    } else if (exceptionValue.numberOfPhiUsers() != 1) {
+      return false;
+    }
+    return true;
+  }
+
+  private void insertNotNullCheck(
+      BasicBlock block,
+      InstructionListIterator iterator,
+      If theIf,
+      BasicBlock target,
+      BasicBlock deadTarget,
+      Position position) {
+    deadTarget.unlinkSinglePredecessorSiblingsAllowed();
+    assert theIf == block.exit();
+    iterator.previous();
+    Instruction instruction;
+    DexMethod getClassMethod = appView.dexItemFactory().objectMembers.getClass;
+    instruction = new InvokeVirtual(getClassMethod, null, ImmutableList.of(theIf.lhs()));
+    instruction.setPosition(position);
+    iterator.add(instruction);
+    iterator.next();
+    iterator.replaceCurrentInstruction(new Goto());
+    assert block.exit().isGoto();
+    assert block.exit().asGoto().getTarget() == target;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 4ad6534..3a60f68 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -14,7 +14,6 @@
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
@@ -34,8 +33,6 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.Binop;
-import com.android.tools.r8.ir.code.CatchHandlers;
-import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
 import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
@@ -43,30 +40,23 @@
 import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.DexItemBasedConstString;
 import com.android.tools.r8.ir.code.DominatorTree;
-import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.IfType;
-import com.android.tools.r8.ir.code.InstanceFieldInstruction;
 import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.Instruction.SideEffectAssumption;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InstructionOrPhi;
 import com.android.tools.r8.ir.code.Invoke;
-import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeInterface;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Move;
-import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.ir.code.StaticGet;
-import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
@@ -88,7 +78,6 @@
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
-import java.util.BitSet;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
@@ -173,146 +162,6 @@
     assert Streams.stream(code.instructions()).noneMatch(Instruction::isAssume);
   }
 
-  // Rewrite 'throw new NullPointerException()' to 'throw null'.
-  public void rewriteThrowNullPointerException(IRCode code) {
-    boolean shouldRemoveUnreachableBlocks = false;
-    for (BasicBlock block : code.blocks) {
-      InstructionListIterator it = block.listIterator(code);
-      while (it.hasNext()) {
-        Instruction instruction = it.next();
-
-        // Check for the patterns 'if (x == null) throw null' and
-        // 'if (x == null) throw new NullPointerException()'.
-        if (instruction.isIf()) {
-          if (appView
-              .dexItemFactory()
-              .objectsMethods
-              .isRequireNonNullMethod(code.method().getReference())) {
-            continue;
-          }
-
-          If ifInstruction = instruction.asIf();
-          if (!ifInstruction.isZeroTest()) {
-            continue;
-          }
-
-          Value value = ifInstruction.lhs();
-          if (!value.getType().isReferenceType()) {
-            assert value.getType().isPrimitiveType();
-            continue;
-          }
-
-          BasicBlock valueIsNullTarget = ifInstruction.targetFromCondition(0);
-          if (valueIsNullTarget.getPredecessors().size() != 1
-              || !valueIsNullTarget.exit().isThrow()) {
-            continue;
-          }
-
-          Throw throwInstruction = valueIsNullTarget.exit().asThrow();
-          Value exceptionValue = throwInstruction.exception().getAliasedValue();
-          if (!exceptionValue.isConstZero()
-              && exceptionValue.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
-            NewInstance newInstance = exceptionValue.definition.asNewInstance();
-            if (newInstance.clazz != dexItemFactory.npeType) {
-              continue;
-            }
-            if (newInstance.outValue().numberOfAllUsers() != 2) {
-              continue; // Could be mutated before it is thrown.
-            }
-            InvokeDirect constructorCall = newInstance.getUniqueConstructorInvoke(dexItemFactory);
-            if (constructorCall == null) {
-              continue;
-            }
-            if (constructorCall.getInvokedMethod() != dexItemFactory.npeMethods.init) {
-              continue;
-            }
-          } else if (!exceptionValue.isConstZero()) {
-            continue;
-          }
-
-          boolean canDetachValueIsNullTarget = true;
-          for (Instruction i : valueIsNullTarget.instructionsBefore(throwInstruction)) {
-            if (!i.isBlockLocalInstructionWithoutSideEffects(appView, code.context())) {
-              canDetachValueIsNullTarget = false;
-              break;
-            }
-          }
-          if (!canDetachValueIsNullTarget) {
-            continue;
-          }
-
-          insertNotNullCheck(
-              block,
-              it,
-              ifInstruction,
-              ifInstruction.targetFromCondition(1),
-              valueIsNullTarget,
-              throwInstruction.getPosition());
-          shouldRemoveUnreachableBlocks = true;
-        }
-
-        // Check for 'new-instance NullPointerException' with 2 users, not declaring a local and
-        // not ending the scope of any locals.
-        if (instruction.isNewInstance()
-            && instruction.asNewInstance().clazz == dexItemFactory.npeType
-            && instruction.outValue().numberOfAllUsers() == 2
-            && !instruction.outValue().hasLocalInfo()
-            && instruction.getDebugValues().isEmpty()) {
-          if (it.hasNext()) {
-            Instruction instruction2 = it.next();
-            // Check for 'invoke NullPointerException.init() not ending the scope of any locals
-            // and with the result of the first instruction as the argument. Also check that
-            // the two first instructions have the same position.
-            if (instruction2.isInvokeDirect()
-                && instruction2.getDebugValues().isEmpty()) {
-              InvokeDirect invokeDirect = instruction2.asInvokeDirect();
-              if (invokeDirect.getInvokedMethod() == dexItemFactory.npeMethods.init
-                  && invokeDirect.getReceiver() == instruction.outValue()
-                  && invokeDirect.arguments().size() == 1
-                  && invokeDirect.getPosition() == instruction.getPosition()) {
-                if (it.hasNext()) {
-                  Instruction instruction3 = it.next();
-                  // Finally check that the last instruction is a throw of the initialized
-                  // exception object and replace with 'throw null' if so.
-                  if (instruction3.isThrow()
-                      && instruction3.asThrow().exception() == instruction.outValue()) {
-                    // Create const 0 with null type and a throw using that value.
-                    Instruction nullPointer = code.createConstNull();
-                    Instruction throwInstruction = new Throw(nullPointer.outValue());
-                    // Preserve positions: we have checked that the first two original instructions
-                    // have the same position.
-                    assert instruction.getPosition() == instruction2.getPosition();
-                    nullPointer.setPosition(instruction.getPosition());
-                    throwInstruction.setPosition(instruction3.getPosition());
-                    // Copy debug values from original throw to new throw to correctly end scope
-                    // of locals.
-                    instruction3.moveDebugValues(throwInstruction);
-                    // Remove the three original instructions.
-                    it.remove();
-                    it.previous();
-                    it.remove();
-                    it.previous();
-                    it.remove();
-                    // Replace them with 'const 0' and 'throw'.
-                    it.add(nullPointer);
-                    it.add(throwInstruction);
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-    if (shouldRemoveUnreachableBlocks) {
-      Set<Value> affectedValues = code.removeUnreachableBlocks();
-      if (!affectedValues.isEmpty()) {
-        new TypeAnalysis(appView).narrowing(affectedValues);
-      }
-    }
-    assert code.isConsistentSSA(appView);
-  }
-
   private boolean checkArgumentType(InvokeMethod invoke, int argumentIndex) {
     // TODO(sgjesse): Insert cast if required.
     TypeElement returnType =
@@ -1237,271 +1086,6 @@
     return changed;
   }
 
-  private boolean isPotentialTrivialRethrowValue(Value exceptionValue) {
-    if (exceptionValue.hasDebugUsers()) {
-      return false;
-    }
-    if (exceptionValue.hasUsers()) {
-      if (exceptionValue.hasPhiUsers()
-          || !exceptionValue.hasSingleUniqueUser()
-          || !exceptionValue.singleUniqueUser().isThrow()) {
-        return false;
-      }
-    } else if (exceptionValue.numberOfPhiUsers() != 1) {
-      return false;
-    }
-    return true;
-  }
-
-  private boolean isSingleHandlerTrivial(BasicBlock firstBlock, IRCode code) {
-    InstructionListIterator instructionIterator = firstBlock.listIterator(code);
-    Instruction instruction = instructionIterator.next();
-    if (!instruction.isMoveException()) {
-      // A catch handler which doesn't use its exception is not going to be a trivial rethrow.
-      return false;
-    }
-    Value exceptionValue = instruction.outValue();
-    if (!isPotentialTrivialRethrowValue(exceptionValue)) {
-      return false;
-    }
-    while (instructionIterator.hasNext()) {
-      instruction = instructionIterator.next();
-      BasicBlock currentBlock = instruction.getBlock();
-      if (instruction.isGoto()) {
-        BasicBlock nextBlock = instruction.asGoto().getTarget();
-        int predecessorIndex = nextBlock.getPredecessors().indexOf(currentBlock);
-        Value phiAliasOfExceptionValue = null;
-        for (Phi phi : nextBlock.getPhis()) {
-          Value operand = phi.getOperand(predecessorIndex);
-          if (exceptionValue == operand) {
-            phiAliasOfExceptionValue = phi;
-            break;
-          }
-        }
-        if (phiAliasOfExceptionValue != null) {
-          if (!isPotentialTrivialRethrowValue(phiAliasOfExceptionValue)) {
-            return false;
-          }
-          exceptionValue = phiAliasOfExceptionValue;
-        }
-        instructionIterator = nextBlock.listIterator(code);
-      } else if (instruction.isThrow()) {
-        List<Value> throwValues = instruction.inValues();
-        assert throwValues.size() == 1;
-        if (throwValues.get(0) != exceptionValue) {
-          return false;
-        }
-        CatchHandlers<BasicBlock> currentHandlers = currentBlock.getCatchHandlers();
-        if (!currentHandlers.isEmpty()) {
-          // This is the case where our trivial catch handler has catch handler(s). For now, we
-          // will only treat our block as trivial if all its catch handlers are also trivial.
-          // Note: it is possible that we could "bridge" a trivial handler, where we take the
-          // handlers of the handler and bring them up to replace the trivial handler. Example:
-          //   catch (Throwable t) {
-          //     try { throw t; } catch(Throwable abc) { foo(abc); }
-          //   }
-          // could turn into:
-          //   catch (Throwable abc) {
-          //     foo(abc);
-          //   }
-          // However this gets significantly harder when you have to consider non-matching guard
-          // types.
-          for (CatchHandler<BasicBlock> handler : currentHandlers) {
-            if (!isSingleHandlerTrivial(handler.getTarget(), code)) {
-              return false;
-            }
-          }
-        }
-        return true;
-      } else {
-        // Any other instructions in the catch handler means it's not trivial, and thus we can't
-        // elide.
-        return false;
-      }
-    }
-    throw new Unreachable("Triviality check should always return before the loop terminates");
-  }
-
-  // Find any case where we have a catch followed immediately and only by a rethrow. This is extra
-  // code doing what the JVM does automatically and can be safely elided.
-  public void optimizeRedundantCatchRethrowInstructions(IRCode code) {
-    ListIterator<BasicBlock> blockIterator = code.listIterator();
-    boolean hasUnlinkedCatchHandlers = false;
-    while (blockIterator.hasNext()) {
-      BasicBlock blockWithHandlers = blockIterator.next();
-      if (blockWithHandlers.hasCatchHandlers()) {
-        boolean allHandlersAreTrivial = true;
-        for (CatchHandler<BasicBlock> handler : blockWithHandlers.getCatchHandlers()) {
-          if (!isSingleHandlerTrivial(handler.target, code)) {
-            allHandlersAreTrivial = false;
-            break;
-          }
-        }
-        // We need to ensure all handlers are trivial to unlink, since if one is non-trivial, and
-        // its guard is a parent type to a trivial one, removing the trivial catch will result in
-        // us hitting the non-trivial catch. This could be avoided by more sophisticated type
-        // analysis.
-        if (allHandlersAreTrivial) {
-          hasUnlinkedCatchHandlers = true;
-          for (CatchHandler<BasicBlock> handler : blockWithHandlers.getCatchHandlers()) {
-            handler.getTarget().unlinkCatchHandler();
-          }
-        }
-      }
-    }
-    if (hasUnlinkedCatchHandlers) {
-      code.removeUnreachableBlocks();
-    }
-  }
-
-  // Find all instructions that always throw, split the block after each such instruction and follow
-  // it with a block throwing a null value (which should result in NPE). Note that this throw is not
-  // expected to be ever reached, but is intended to satisfy verifier.
-  public void optimizeAlwaysThrowingInstructions(IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
-    Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
-    ListIterator<BasicBlock> blockIterator = code.listIterator();
-    ProgramMethod context = code.context();
-    boolean hasUnlinkedCatchHandlers = false;
-    // For cyclic phis we sometimes do not propagate the dynamic upper type after rewritings.
-    // The inValue.isAlwaysNull(appView) check below will not recompute the dynamic type of phi's
-    // so we recompute all phis here if they are always null.
-    AppView<AppInfoWithClassHierarchy> appViewWithClassHierarchy =
-        appView.hasClassHierarchy() ? appView.withClassHierarchy() : null;
-    if (appViewWithClassHierarchy != null) {
-      code.blocks.forEach(
-          block ->
-              block
-                  .getPhis()
-                  .forEach(
-                      phi -> {
-                        if (!phi.getType().isDefinitelyNull()) {
-                          TypeElement dynamicUpperBoundType =
-                              phi.getDynamicUpperBoundType(appViewWithClassHierarchy);
-                          if (dynamicUpperBoundType.isDefinitelyNull()) {
-                            affectedValues.add(phi);
-                            phi.setType(dynamicUpperBoundType);
-                          }
-                        }
-                      }));
-    }
-    while (blockIterator.hasNext()) {
-      BasicBlock block = blockIterator.next();
-      if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) {
-        continue;
-      }
-      if (blocksToRemove.contains(block)) {
-        continue;
-      }
-      InstructionListIterator instructionIterator = block.listIterator(code);
-      while (instructionIterator.hasNext()) {
-        Instruction instruction = instructionIterator.next();
-        if (instruction.throwsOnNullInput()) {
-          Value inValue = instruction.getNonNullInput();
-          if (inValue.isAlwaysNull(appView)) {
-            // Insert `throw null` after the instruction if it is not guaranteed to throw an NPE.
-            if (instruction.isAssume()) {
-              // If this assume is in a block with catch handlers, then the out-value can have
-              // usages in the catch handler if the block's throwing instruction comes after the
-              // assume instruction. In this case, the catch handler is also guaranteed to be dead,
-              // so we detach it from the current block.
-              if (block.hasCatchHandlers()
-                  && block.isInstructionBeforeThrowingInstruction(instruction)) {
-                for (CatchHandler<BasicBlock> catchHandler : block.getCatchHandlers()) {
-                  catchHandler.getTarget().unlinkCatchHandler();
-                }
-                hasUnlinkedCatchHandlers = true;
-              }
-            } else if (instruction.isInstanceFieldInstruction()) {
-              InstanceFieldInstruction instanceFieldInstruction =
-                  instruction.asInstanceFieldInstruction();
-              if (instanceFieldInstruction.instructionInstanceCanThrow(
-                  appView, context, SideEffectAssumption.RECEIVER_NOT_NULL)) {
-                instructionIterator.next();
-              }
-            } else if (instruction.isInvokeMethodWithReceiver()) {
-              InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
-              SideEffectAssumption assumption =
-                  SideEffectAssumption.RECEIVER_NOT_NULL.join(
-                      SideEffectAssumption.INVOKED_METHOD_DOES_NOT_HAVE_SIDE_EFFECTS);
-              if (invoke.instructionMayHaveSideEffects(appView, context, assumption)) {
-                instructionIterator.next();
-              }
-            }
-            instructionIterator.replaceCurrentInstructionWithThrowNull(
-                appView, code, blockIterator, blocksToRemove, affectedValues);
-            continue;
-          }
-        }
-
-        if (!instruction.isInvokeMethod()) {
-          continue;
-        }
-
-        InvokeMethod invoke = instruction.asInvokeMethod();
-        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
-        if (singleTarget == null) {
-          continue;
-        }
-
-        MethodOptimizationInfo optimizationInfo =
-            singleTarget.getDefinition().getOptimizationInfo();
-
-        // If the invoke instruction is a null check, we can remove it.
-        boolean isNullCheck = false;
-        if (optimizationInfo.hasNonNullParamOrThrow()) {
-          BitSet nonNullParamOrThrow = optimizationInfo.getNonNullParamOrThrow();
-          for (int i = 0; i < invoke.arguments().size(); i++) {
-            Value argument = invoke.arguments().get(i);
-            if (argument.isAlwaysNull(appView) && nonNullParamOrThrow.get(i)) {
-              isNullCheck = true;
-              break;
-            }
-          }
-        }
-        // If the invoke instruction never returns normally, we can insert a throw null instruction
-        // after the invoke.
-        if (isNullCheck || optimizationInfo.neverReturnsNormally()) {
-          instructionIterator.setInsertionPosition(invoke.getPosition());
-          instructionIterator.next();
-          instructionIterator.replaceCurrentInstructionWithThrowNull(
-              appView, code, blockIterator, blocksToRemove, affectedValues);
-          instructionIterator.unsetInsertionPosition();
-        }
-      }
-    }
-    code.removeBlocks(blocksToRemove);
-    if (hasUnlinkedCatchHandlers) {
-      affectedValues.addAll(code.removeUnreachableBlocks());
-    }
-    assert code.getUnreachableBlocks().isEmpty();
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
-    assert code.isConsistentSSA(appView);
-  }
-
-  private void insertNotNullCheck(
-      BasicBlock block,
-      InstructionListIterator iterator,
-      If theIf,
-      BasicBlock target,
-      BasicBlock deadTarget,
-      Position position) {
-    deadTarget.unlinkSinglePredecessorSiblingsAllowed();
-    assert theIf == block.exit();
-    iterator.previous();
-    Instruction instruction;
-    DexMethod getClassMethod = appView.dexItemFactory().objectMembers.getClass;
-    instruction = new InvokeVirtual(getClassMethod, null, ImmutableList.of(theIf.lhs()));
-    instruction.setPosition(position);
-    iterator.add(instruction);
-    iterator.next();
-    iterator.replaceCurrentInstruction(new Goto());
-    assert block.exit().isGoto();
-    assert block.exit().asGoto().getTarget() == target;
-  }
-
   /**
    * Remove moves that are not actually used by instructions in exiting paths. These moves can arise
    * due to debug local info needing a particular value and the live-interval for it then moves it
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
index 650aa8b..1c0d3de 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.conversion.IRToCfFinalizer;
 import com.android.tools.r8.ir.conversion.IRToDexFinalizer;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.conversion.passes.ThrowCatchOptimizer;
 import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfo;
 import com.android.tools.r8.shaking.Enqueuer.FieldAccessKind;
 import com.android.tools.r8.shaking.Enqueuer.FieldAccessMetadata;
@@ -269,7 +270,7 @@
     rewriter.rewriteCode(ir, initializedClassesWithContexts, prunedFields);
 
     // Run dead code elimination.
-    rewriter.getCodeRewriter().optimizeAlwaysThrowingInstructions(ir);
+    new ThrowCatchOptimizer(appView).optimizeAlwaysThrowingInstructions(ir);
     rewriter.getDeadCodeRemover().run(ir, Timing.empty());
 
     // Finalize to class files or dex.