Redundant switch case elimination

Change-Id: I60a1ef2bb7849ef82b7be6dd66b7ccfd239d3aa3
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
new file mode 100644
index 0000000..fce5100
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
@@ -0,0 +1,207 @@
+// Copyright (c) 2019, 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.analysis.equivalence;
+
+import static com.google.common.base.Predicates.not;
+import static com.google.common.base.Predicates.or;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.DexItemBasedConstString;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Set;
+
+/**
+ * Analysis that can be used to determine if the behavior of one basic block is subsumed by the
+ * behavior of another basic block.
+ *
+ * <p>Example: If the behavior of a switch case is subsumed by the behavior of the switch's default
+ * target, then the switch case can simply be removed from the switch instruction.
+ */
+public class BasicBlockBehavioralSubsumption {
+
+  private final AppView<?> appView;
+  private final DexType context;
+
+  public BasicBlockBehavioralSubsumption(AppView<?> appView, DexType context) {
+    this.appView = appView;
+    this.context = context;
+  }
+
+  public boolean isSubsumedBy(BasicBlock block, BasicBlock other) {
+    return isSubsumedBy(block.iterator(), other.iterator(), null);
+  }
+
+  private boolean isSubsumedBy(
+      InstructionIterator iterator, InstructionIterator otherIterator, Set<BasicBlock> visited) {
+    // Skip over block-local instructions (i.e., instructions that define values that are not used
+    // outside the block itself) that do not have side effects.
+    Instruction instruction =
+        iterator.nextUntil(
+            or(
+                not(this::isBlockLocalInstructionWithoutSideEffects),
+                Instruction::isJumpInstruction));
+
+    // If the instruction defines a value with non-local usages, then we would need a dominator
+    // analysis to verify that all these non-local usages can in fact be replaced by a value
+    // defined in `other`. We just give up in this case.
+    if (definesValueWithNonLocalUsages(instruction)) {
+      return false;
+    }
+
+    // Skip over non-throwing instructions in the other block.
+    Instruction otherInstruction =
+        otherIterator.nextUntil(
+            or(this::instructionMayHaveSideEffects, Instruction::isJumpInstruction));
+    assert otherInstruction != null;
+
+    if (instruction.isGoto()) {
+      BasicBlock targetBlock = instruction.asGoto().getTarget();
+      if (otherInstruction.isGoto()) {
+        BasicBlock otherTargetBlock = otherInstruction.asGoto().getTarget();
+        // If the other instruction is also a goto instruction, which targets the same block, then
+        // we are done.
+        if (targetBlock == otherTargetBlock) {
+          return true;
+        }
+        // Otherwise we continue the search from the two successor blocks.
+        otherIterator = otherTargetBlock.iterator();
+      } else {
+        // Move the cursor backwards to ensure that the recursive call will revisit the current
+        // instruction.
+        otherIterator.previous();
+      }
+      if (visited == null) {
+        visited = SetUtils.newIdentityHashSet(instruction.getBlock());
+      }
+      if (visited.add(targetBlock)) {
+        return isSubsumedBy(targetBlock.iterator(), otherIterator, visited);
+      }
+      // Guard against cycles in the control flow graph.
+      return false;
+    }
+
+    // If the current instruction is not a goto instruction, but the other instruction is, then
+    // we continue the search from the target of the other goto instruction.
+    if (otherInstruction.isGoto()) {
+      otherIterator = otherInstruction.asGoto().getTarget().iterator();
+      otherInstruction =
+          otherIterator.nextUntil(
+              or(this::instructionMayHaveSideEffects, Instruction::isJumpInstruction));
+    }
+
+    if (instruction.isReturn()) {
+      Return returnInstruction = instruction.asReturn();
+      if (otherInstruction.isReturn()) {
+        Return otherReturnInstruction = otherInstruction.asReturn();
+        if (returnInstruction.isReturnVoid()) {
+          assert otherReturnInstruction.isReturnVoid();
+          return true;
+        }
+        return valuesAreIdentical(
+            otherReturnInstruction.returnValue(), returnInstruction.returnValue());
+      }
+      return false;
+    }
+
+    return false;
+  }
+
+  private boolean isBlockLocalInstructionWithoutSideEffects(Instruction instruction) {
+    return definesBlockLocalValue(instruction) && !instructionMayHaveSideEffects(instruction);
+  }
+
+  private boolean definesBlockLocalValue(Instruction instruction) {
+    return !definesValueWithNonLocalUsages(instruction);
+  }
+
+  private boolean definesValueWithNonLocalUsages(Instruction instruction) {
+    if (instruction.hasOutValue()) {
+      Value outValue = instruction.outValue();
+      if (outValue.numberOfPhiUsers() > 0) {
+        return true;
+      }
+      for (Instruction user : outValue.uniqueUsers()) {
+        if (user.getBlock() != instruction.getBlock()) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private boolean instructionMayHaveSideEffects(Instruction instruction) {
+    return instruction.instructionMayHaveSideEffects(appView, context);
+  }
+
+  private boolean valuesAreIdentical(Value value, Value other) {
+    value = value.getAliasedValue();
+    other = other.getAliasedValue();
+    if (value == other) {
+      return true;
+    }
+    if (value.isPhi() || other.isPhi()) {
+      return false;
+    }
+    return instructionsDefineIdenticalValues(value.definition, other.definition);
+  }
+
+  private boolean instructionsDefineIdenticalValues(Instruction instruction, Instruction other) {
+    assert instruction.hasOutValue();
+    assert other.hasOutValue();
+
+    Value outValue = instruction.outValue();
+    Value otherOutValue = other.outValue();
+    if (!outValue.getTypeLattice().equals(otherOutValue.getTypeLattice())) {
+      return false;
+    }
+
+    if (instruction.isConstClass()) {
+      if (!other.isConstClass()) {
+        return false;
+      }
+      ConstClass constClassInstruction = instruction.asConstClass();
+      ConstClass otherConstClassInstruction = other.asConstClass();
+      return constClassInstruction.getValue() == otherConstClassInstruction.getValue();
+    }
+
+    if (instruction.isConstNumber()) {
+      if (!other.isConstNumber()) {
+        return false;
+      }
+      ConstNumber constNumberInstruction = instruction.asConstNumber();
+      ConstNumber otherConstNumberInstruction = other.asConstNumber();
+      return constNumberInstruction.getRawValue() == otherConstNumberInstruction.getRawValue();
+    }
+
+    if (instruction.isConstString()) {
+      if (!other.isConstString()) {
+        return false;
+      }
+      ConstString constStringInstruction = instruction.asConstString();
+      ConstString otherConstStringInstruction = other.asConstString();
+      return constStringInstruction.getValue() == otherConstStringInstruction.getValue();
+    }
+
+    if (instruction.isDexItemBasedConstString()) {
+      if (!other.isDexItemBasedConstString()) {
+        return false;
+      }
+      DexItemBasedConstString constStringInstruction = instruction.asDexItemBasedConstString();
+      DexItemBasedConstString otherConstStringInstruction = other.asDexItemBasedConstString();
+      return constStringInstruction.getItem() == otherConstStringInstruction.getItem();
+    }
+
+    return false;
+  }
+}
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 8194593..af2aeba 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
@@ -31,6 +31,7 @@
 import com.android.tools.r8.graph.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.graph.ParameterUsagesInfo.ParameterUsageBuilder;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
+import com.android.tools.r8.ir.analysis.equivalence.BasicBlockBehavioralSubsumption;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
@@ -940,6 +941,8 @@
       IRCode code, Switch theSwitch, InstructionListIterator iterator) {
     BasicBlock defaultTarget = theSwitch.fallthroughBlock();
     SwitchCaseEliminator eliminator = null;
+    BasicBlockBehavioralSubsumption behavioralSubsumption =
+        new BasicBlockBehavioralSubsumption(appView, code.method.method.holder);
 
     // Compute the set of switch cases that can be removed.
     for (int i = 0; i < theSwitch.numberOfKeys(); i++) {
@@ -947,8 +950,8 @@
 
       // This switch case can be removed if the behavior of the target block is equivalent to the
       // behavior of the default block, or if the switch case is unreachable.
-      if (basicBlockCanBeReplacedBy(code, targetBlock, defaultTarget)
-          || switchCaseIsUnreachable(theSwitch, i)) {
+      if (switchCaseIsUnreachable(theSwitch, i)
+          || behavioralSubsumption.isSubsumedBy(targetBlock, defaultTarget)) {
         if (eliminator == null) {
           eliminator = new SwitchCaseEliminator(theSwitch, iterator);
         }
@@ -961,11 +964,6 @@
     return eliminator;
   }
 
-  private boolean basicBlockCanBeReplacedBy(IRCode code, BasicBlock block, BasicBlock replacement) {
-    // TODO(b/132420434): TBD.
-    return false;
-  }
-
   private boolean switchCaseIsUnreachable(Switch theSwitch, int index) {
     Value switchValue = theSwitch.value();
     return switchValue.hasValueRange()
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index f910740..c6e8bea 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -10,6 +10,12 @@
 
 public class SetUtils {
 
+  public static <T> Set<T> newIdentityHashSet(T element) {
+    Set<T> result = Sets.newIdentityHashSet();
+    result.add(element);
+    return result;
+  }
+
   public static <T> Set<T> newIdentityHashSet(Collection<T> c) {
     Set<T> result = Sets.newIdentityHashSet();
     result.addAll(c);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/SwitchCaseRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/SwitchCaseRemovalTest.java
index 3dbf05f..74f9f43 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/SwitchCaseRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/SwitchCaseRemovalTest.java
@@ -71,14 +71,10 @@
       MethodSubject methodSubject = classSubject.uniqueMethodWithName("testSwitchCaseRemoval");
       assertThat(methodSubject, isPresent());
       assertEquals(
-          parameters.isCfRuntime() ? 2 : 1,
-          methodSubject.streamInstructions().filter(InstructionSubject::isConstNull).count());
+          1, methodSubject.streamInstructions().filter(InstructionSubject::isConstNull).count());
       assertEquals(
-          parameters.isCfRuntime() ? 3 : 2,
-          methodSubject.streamInstructions().filter(InstructionSubject::isReturnObject).count());
-      verifyUniqueSwitchHasExactCases(
-          methodSubject.buildIR(),
-          parameters.isCfRuntime() ? ImmutableSet.of(0, 1) : ImmutableSet.of(0));
+          2, methodSubject.streamInstructions().filter(InstructionSubject::isReturnObject).count());
+      verifyUniqueSwitchHasExactCases(methodSubject.buildIR(), ImmutableSet.of(0));
     }
 
     {
diff --git a/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java b/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java
index 12562ac..37c3bc3 100644
--- a/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/switches/SwitchRewritingTest.java
@@ -149,10 +149,12 @@
         "    return-void"
     );
 
-    Consumer<InternalOptions> optionsConsumer = options -> {
-      options.verbose = true;
-      options.printTimes = true;
-    };
+    Consumer<InternalOptions> optionsConsumer =
+        options -> {
+          options.testing.enableDeadSwitchCaseElimination = false;
+          options.verbose = true;
+          options.printTimes = true;
+        };
     AndroidApp originalApplication = buildApplication(builder);
     AndroidApp processedApplication = processApplication(originalApplication, optionsConsumer);
     DexEncodedMethod method = getMethod(processedApplication, signature);