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.