Generalize escape analysis

This CL generalizes the escape analysis in such a way that it is now possible to customize which escape instructions for a given value that are considered legitimate. This is achieved by passing a configuration object to the escape analysis.

Change-Id: I07d9f5b20d2cff11625997294c8e9fe0be95cdba
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java
new file mode 100644
index 0000000..162120b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java
@@ -0,0 +1,27 @@
+// 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.escape;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.code.Instruction;
+
+public class DefaultEscapeAnalysisConfiguration implements EscapeAnalysisConfiguration {
+
+  private static final DefaultEscapeAnalysisConfiguration INSTANCE =
+      new DefaultEscapeAnalysisConfiguration();
+
+  private DefaultEscapeAnalysisConfiguration() {}
+
+  public static DefaultEscapeAnalysisConfiguration getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public boolean isLegitimateEscapeRoute(
+      AppView<?> appView, Instruction escapeRoute, DexMethod context) {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
index 685a251..84aeadc 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
@@ -3,30 +3,90 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.analysis.escape;
 
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.Box;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Escape analysis that collects instructions where the value of interest can escape from the given
  * method, i.e., can be stored outside, passed as arguments, etc.
  *
- * Note: at this point, the analysis always returns a non-empty list for values that are defined by
- * a new-instance IR, since they could escape via the corresponding direct call to `<init>()`.
+ * <p>Note: at this point, the analysis always returns a non-empty list for values that are defined
+ * by a new-instance IR, since they could escape via the corresponding direct call to `<init>()`.
  */
 public class EscapeAnalysis {
 
+  private final AppView<?> appView;
+  private final EscapeAnalysisConfiguration configuration;
+
+  // Data structure to ensure termination in case of cycles in the data flow graph.
+  private final Set<Value> trackedValues = Sets.newIdentityHashSet();
+
+  // Worklist containing values that are alias of the value of interest.
+  private final Deque<Value> valuesToTrack = new ArrayDeque<>();
+
+  public EscapeAnalysis(AppView<?> appView) {
+    this(appView, DefaultEscapeAnalysisConfiguration.getInstance());
+  }
+
+  public EscapeAnalysis(AppView<?> appView, EscapeAnalysisConfiguration configuration) {
+    this.appView = appView;
+    this.configuration = configuration;
+  }
+
+  /**
+   * Main entry point for running the escape analysis. This method is used to determine if
+   * `valueOfInterest` escapes from the given method. Returns true as soon as the value is known to
+   * escape, hence it is more efficient to use this method over {@link #computeEscapeRoutes(IRCode,
+   * Value)}.
+   */
+  public boolean isEscaping(IRCode code, Value valueOfInterest) {
+    Box<Instruction> firstEscapeRoute = new Box<>();
+    run(
+        code,
+        valueOfInterest,
+        escapeRoute -> {
+          firstEscapeRoute.set(escapeRoute);
+          return true;
+        });
+    return firstEscapeRoute.isSet();
+  }
+
+  /**
+   * Main entry point for running the escape analysis. This method is used to determine if
+   * `valueOfInterest` escapes from the given method. Returns the set of instructions from which the
+   * value may escape. Clients that only need to know if the value escapes or not should use {@link
+   * #isEscaping(IRCode, Value)} for better performance.
+   */
+  public Set<Instruction> computeEscapeRoutes(IRCode code, Value valueOfInterest) {
+    ImmutableSet.Builder<Instruction> escapeRoutes = ImmutableSet.builder();
+    run(
+        code,
+        valueOfInterest,
+        escapeRoute -> {
+          escapeRoutes.add(escapeRoute);
+          return false;
+        });
+    return escapeRoutes.build();
+  }
+
   // Returns the set of instructions where the value of interest can escape from the code.
-  public static Set<Instruction> escape(IRCode code, Value valueOfInterest) {
+  private void run(IRCode code, Value valueOfInterest, Predicate<Instruction> stoppingCriterion) {
+    assert valueOfInterest.getTypeLattice().isReference();
+    assert trackedValues.isEmpty();
+    assert valuesToTrack.isEmpty();
+
     // Sanity check: value (or its containing block) belongs to the code.
     BasicBlock block =
         valueOfInterest.isPhi()
@@ -35,51 +95,77 @@
     assert code.blocks.contains(block);
     List<Value> arguments = code.collectArguments();
 
-    ImmutableSet.Builder<Instruction> builder = ImmutableSet.builder();
-    Set<Value> trackedValues = Sets.newIdentityHashSet();
-    Deque<Value> valuesToTrack = new ArrayDeque<>();
-    valuesToTrack.push(valueOfInterest);
+    addToWorklist(valueOfInterest);
+
     while (!valuesToTrack.isEmpty()) {
-      Value v = valuesToTrack.poll();
+      Value alias = valuesToTrack.poll();
+      assert alias != null;
+
       // Make sure we are not tracking values over and over again.
-      if (!trackedValues.add(v)) {
-        continue;
-      }
-      v.uniquePhiUsers().forEach(valuesToTrack::push);
-      for (Instruction user : v.uniqueUsers()) {
-        // Users in the same block need one more filtering.
-        if (user.getBlock() == block) {
-          // When the value of interest has the definition
-          if (!valueOfInterest.isPhi()) {
-            List<Instruction> instructions = block.getInstructions();
-            // Make sure we're not considering instructions prior to the value of interest.
-            if (instructions.indexOf(user) < instructions.indexOf(valueOfInterest.definition)) {
-              continue;
-            }
-          }
-        }
-        if (isDirectlyEscaping(user, code.method, arguments)) {
-          builder.add(user);
-          continue;
-        }
-        // Track aliased value.
-        if (user.couldIntroduceAnAlias()) {
-          Value outValue = user.outValue();
-          assert outValue != null;
-          valuesToTrack.push(outValue);
-        }
-        // Track propagated values through which the value of interest can escape indirectly.
-        Value propagatedValue = getPropagatedSubject(v, user);
-        if (propagatedValue != null && propagatedValue != v) {
-          valuesToTrack.push(propagatedValue);
-        }
+      assert trackedValues.contains(alias);
+
+      boolean stopped =
+          processValue(valueOfInterest, alias, block, code, arguments, stoppingCriterion);
+      if (stopped) {
+        break;
       }
     }
-    return builder.build();
+
+    trackedValues.clear();
+    valuesToTrack.clear();
   }
 
-  private static boolean isDirectlyEscaping(
-      Instruction instr, DexEncodedMethod invocationContext, List<Value> arguments) {
+  private boolean processValue(
+      Value root,
+      Value alias,
+      BasicBlock block,
+      IRCode code,
+      List<Value> arguments,
+      Predicate<Instruction> stoppingCriterion) {
+    for (Value phi : alias.uniquePhiUsers()) {
+      addToWorklist(phi);
+    }
+    for (Instruction user : alias.uniqueUsers()) {
+      // Users in the same block need one more filtering.
+      if (user.getBlock() == block) {
+        // When the value of interest has the definition
+        if (!root.isPhi()) {
+          List<Instruction> instructions = block.getInstructions();
+          // Make sure we're not considering instructions prior to the value of interest.
+          if (instructions.indexOf(user) < instructions.indexOf(root.definition)) {
+            continue;
+          }
+        }
+      }
+      if (!configuration.isLegitimateEscapeRoute(appView, user, code.method.method)
+          && isDirectlyEscaping(user, code.method.method, arguments)) {
+        if (stoppingCriterion.test(user)) {
+          return true;
+        }
+      }
+      // Track aliased value.
+      if (user.couldIntroduceAnAlias()) {
+        Value outValue = user.outValue();
+        assert outValue != null;
+        addToWorklist(outValue);
+      }
+      // Track propagated values through which the value of interest can escape indirectly.
+      Value propagatedValue = getPropagatedSubject(alias, user);
+      if (propagatedValue != null && propagatedValue != alias) {
+        addToWorklist(propagatedValue);
+      }
+    }
+    return false;
+  }
+
+  private void addToWorklist(Value value) {
+    assert value != null;
+    if (trackedValues.add(value)) {
+      valuesToTrack.push(value);
+    }
+  }
+
+  private boolean isDirectlyEscaping(Instruction instr, DexMethod context, List<Value> arguments) {
     // As return value.
     if (instr.isReturn()) {
       return true;
@@ -96,7 +182,7 @@
     if (instr.isInvokeMethod()) {
       DexMethod invokedMethod = instr.asInvokeMethod().getInvokedMethod();
       // Filter out the recursion with exactly same arguments.
-      if (invokedMethod == invocationContext.method) {
+      if (invokedMethod == context) {
         return !instr.inValues().equals(arguments);
       }
       return true;
@@ -108,6 +194,10 @@
     return false;
   }
 
+  protected boolean isValueOfInterestOrAlias(Value value) {
+    return trackedValues.contains(value);
+  }
+
   private static Value getPropagatedSubject(Value src, Instruction instr) {
     // We may need to bind array index if we want to track array-get precisely:
     //  array-put arr, idx1, x
@@ -116,12 +206,13 @@
     // For now, we don't distinguish such cases, which is conservative.
     if (instr.isArrayGet()) {
       return instr.asArrayGet().dest();
-    } else if (instr.isArrayPut()) {
+    }
+    if (instr.isArrayPut()) {
       return instr.asArrayPut().array();
-    } else if (instr.isInstancePut()) {
+    }
+    if (instr.isInstancePut()) {
       return instr.asInstancePut().object();
     }
     return null;
   }
-
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java
new file mode 100644
index 0000000..6443dd9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java
@@ -0,0 +1,14 @@
+// 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.escape;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.code.Instruction;
+
+public interface EscapeAnalysisConfiguration {
+
+  boolean isLegitimateEscapeRoute(AppView<?> appView, Instruction escapeRoute, DexMethod context);
+}
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 334bfb2..b2d7276 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
@@ -3157,8 +3157,7 @@
     Long2ReferenceMap<List<ConstNumber>> constantsByValue = new Long2ReferenceOpenHashMap<>();
 
     // Initialize `constantsByValue`.
-    Iterable<Instruction> instructions = code::instructionIterator;
-    for (Instruction instruction : instructions) {
+    for (Instruction instruction : code.instructions()) {
       if (instruction.isConstNumber()) {
         ConstNumber constNumber = instruction.asConstNumber();
         if (constNumber.outValue().hasLocalInfo()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 9718df9..069c729 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -11,15 +11,14 @@
 import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.computeClassName;
 import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
 
-import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.BasicBlock.ThrowingInfo;
 import com.android.tools.r8.ir.code.ConstClass;
@@ -33,8 +32,6 @@
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.ReflectionOptimizer.ClassNameComputationInfo;
-import com.google.common.annotations.VisibleForTesting;
-import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
@@ -231,10 +228,12 @@
       // b/120138731: Filter out local uses, which are likely one-time name computation. In such
       // case, the result of this optimization can lead to a regression if the corresponding class
       // is in a deep package hierarchy.
-      if (!appView.options().testing.forceNameReflectionOptimization
-          && !hasPotentialReadOutside(
-              appView.appInfo(), code.method, EscapeAnalysis.escape(code, out))) {
-        continue;
+      if (!appView.options().testing.forceNameReflectionOptimization) {
+        EscapeAnalysis escapeAnalysis =
+            new EscapeAnalysis(appView, StringOptimizerEscapeAnalysisConfiguration.getInstance());
+        if (escapeAnalysis.isEscaping(code, out)) {
+          continue;
+        }
       }
 
       assert invoke.inValues().size() == 1;
@@ -351,36 +350,6 @@
     }
   }
 
-  @VisibleForTesting
-  public static boolean hasPotentialReadOutside(
-      AppInfo appInfo, DexEncodedMethod invocationContext, Set<Instruction> escapingInstructions) {
-    for (Instruction instr : escapingInstructions) {
-      if (instr.isReturn() || instr.isThrow() || instr.isStaticPut()) {
-        return true;
-      }
-      if (instr.isInvokeMethod()) {
-        DexMethod invokedMethod = instr.asInvokeMethod().getInvokedMethod();
-        DexClass holder = appInfo.definitionFor(invokedMethod.holder);
-        // For most cases, library call is not interesting, e.g.,
-        // System.out.println(...), String.valueOf(...), etc.
-        // If it's too broad, we can introduce black-list.
-        if (holder == null || holder.isNotProgramClass()) {
-          continue;
-        }
-        // Heuristic: if the call target has the same method name, it could be still local.
-        if (invokedMethod.name == invocationContext.method.name) {
-          continue;
-        }
-        // Add more cases to filter out, if any.
-        return true;
-      }
-      if (instr.isArrayPut()) {
-        return instr.asArrayPut().array().isArgument();
-      }
-    }
-    return false;
-  }
-
   // String#valueOf(null) -> "null"
   // String#valueOf(String s) -> s
   // str.toString() -> str
@@ -441,4 +410,44 @@
     }
   }
 
+  public static class StringOptimizerEscapeAnalysisConfiguration
+      implements EscapeAnalysisConfiguration {
+
+    private static final StringOptimizerEscapeAnalysisConfiguration INSTANCE =
+        new StringOptimizerEscapeAnalysisConfiguration();
+
+    private StringOptimizerEscapeAnalysisConfiguration() {}
+
+    public static StringOptimizerEscapeAnalysisConfiguration getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isLegitimateEscapeRoute(
+        AppView<?> appView, Instruction escapeRoute, DexMethod context) {
+      if (escapeRoute.isReturn() || escapeRoute.isThrow() || escapeRoute.isStaticPut()) {
+        return false;
+      }
+      if (escapeRoute.isInvokeMethod()) {
+        DexMethod invokedMethod = escapeRoute.asInvokeMethod().getInvokedMethod();
+        DexClass holder = appView.definitionFor(invokedMethod.holder);
+        // For most cases, library call is not interesting, e.g.,
+        // System.out.println(...), String.valueOf(...), etc.
+        // If it's too broad, we can introduce black-list.
+        if (holder == null || holder.isNotProgramClass()) {
+          return true;
+        }
+        // Heuristic: if the call target has the same method name, it could be still local.
+        if (invokedMethod.name == context.name) {
+          return true;
+        }
+        // Add more cases to filter out, if any.
+        return false;
+      }
+      if (escapeRoute.isArrayPut()) {
+        return !escapeRoute.asArrayPut().array().isArgument();
+      }
+      return true;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/Box.java b/src/main/java/com/android/tools/r8/utils/Box.java
new file mode 100644
index 0000000..fd2b68d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/Box.java
@@ -0,0 +1,18 @@
+// 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.utils;
+
+public class Box<T> {
+
+  private T value;
+
+  public void set(T value) {
+    this.value = value;
+  }
+
+  public boolean isSet() {
+    return value != null;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/AnalysisTestBase.java b/src/test/java/com/android/tools/r8/ir/analysis/AnalysisTestBase.java
index b53d969..b2acadc 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/AnalysisTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/AnalysisTestBase.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.utils.AndroidApp;
@@ -26,7 +27,7 @@
   private final String className;
   private final InternalOptions options = new InternalOptions();
 
-  public AppInfo appInfo;
+  public AppView<?> appView;
 
   public AnalysisTestBase(Class<?> clazz) throws Exception {
     this.app = testForD8().release().addProgramClasses(clazz).compile().app;
@@ -45,9 +46,12 @@
 
   @Before
   public void setup() throws Exception {
-    appInfo =
-        new AppInfo(
-            new ApplicationReader(app, options, new Timing("AnalysisTestBase.appReader")).read());
+    appView =
+        AppView.createForR8(
+            new AppInfo(
+                new ApplicationReader(app, options, new Timing("AnalysisTestBase.appReader"))
+                    .read()),
+            options);
   }
 
   public void buildAndCheckIR(String methodName, Consumer<IRCode> irInspector) throws Exception {
@@ -60,8 +64,7 @@
   public static <T extends Instruction> T getMatchingInstruction(
       IRCode code, Predicate<Instruction> predicate) {
     Instruction result = null;
-    Iterable<Instruction> instructions = code::instructionIterator;
-    for (Instruction instruction : instructions) {
+    for (Instruction instruction : code.instructions()) {
       if (predicate.test(instruction)) {
         if (result != null) {
           fail();
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisForNameReflectionTest.java b/src/test/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisForNameReflectionTest.java
index 3f4353a..de19967 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisForNameReflectionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisForNameReflectionTest.java
@@ -3,12 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.analysis.escape;
 
-import static junit.framework.TestCase.assertFalse;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.analysis.AnalysisTestBase;
 import com.android.tools.r8.ir.code.IRCode;
@@ -28,145 +28,141 @@
 public class EscapeAnalysisForNameReflectionTest extends AnalysisTestBase {
 
   public EscapeAnalysisForNameReflectionTest() throws Exception {
-    super(TestClass.class.getTypeName(),
-        TestClass.class, Helper.class, NamingInterface.class);
+    super(TestClass.class.getTypeName(), TestClass.class, Helper.class, NamingInterface.class);
+  }
+
+  private static Predicate<Instruction> invokesMethodWithName(String name) {
+    return instruction ->
+        instruction.isInvokeMethod()
+            && instruction.asInvokeMethod().getInvokedMethod().name.toString().equals(name);
   }
 
   @Test
   public void testEscapeViaReturn() throws Exception {
-    buildAndCheckIR("escapeViaReturn", checkEscapingName(appInfo, true, instr -> {
-      return instr.isReturn();
-    }));
+    buildAndCheckIR("escapeViaReturn", checkEscapingName(true, Instruction::isReturn));
   }
 
   @Test
   public void testEscapeViaThrow() throws Exception {
-    buildAndCheckIR("escapeViaThrow", code -> {
-      NewInstance e = getMatchingInstruction(code, Instruction::isNewInstance);
-      assertNotNull(e);
-      Value v = e.outValue();
-      assertNotNull(v);
-      Set<Instruction> escapingInstructions = EscapeAnalysis.escape(code, v);
-      assertTrue(
-          StringOptimizer.hasPotentialReadOutside(appInfo, code.method, escapingInstructions));
-      assertTrue(escapingInstructions.stream().allMatch(instr -> {
-        return instr.isThrow()
-            || (instr.isInvokeDirect()
-                && instr.asInvokeDirect().getInvokedMethod().name.toString().equals("<init>"));
-      }));
-    });
+    buildAndCheckIR(
+        "escapeViaThrow",
+        code -> {
+          NewInstance e = getMatchingInstruction(code, Instruction::isNewInstance);
+          assertNotNull(e);
+          Value v = e.outValue();
+          assertNotNull(v);
+          EscapeAnalysis escapeAnalysis =
+              new EscapeAnalysis(
+                  appView,
+                  StringOptimizer.StringOptimizerEscapeAnalysisConfiguration.getInstance());
+          Set<Instruction> escapeRoutes = escapeAnalysis.computeEscapeRoutes(code, v);
+          assertEquals(1, escapeRoutes.size());
+          assertTrue(
+              escapeRoutes.stream()
+                  .allMatch(
+                      instr ->
+                          instr.isThrow()
+                              || (instr.isInvokeDirect()
+                                  && instr
+                                      .asInvokeDirect()
+                                      .getInvokedMethod()
+                                      .name
+                                      .toString()
+                                      .equals("<init>"))));
+        });
   }
 
   @Test
   public void testEscapeViaStaticPut() throws Exception {
-    buildAndCheckIR("escapeViaStaticPut", checkEscapingName(appInfo, true, instr -> {
-      return instr.isStaticPut();
-    }));
+    buildAndCheckIR("escapeViaStaticPut", checkEscapingName(true, Instruction::isStaticPut));
   }
 
   @Test
   public void testEscapeViaInstancePut() throws Exception {
-    buildAndCheckIR("escapeViaInstancePut", checkEscapingName(appInfo, true, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString()
-              .equals("namingInterfaceConsumer");
-    }));
+    buildAndCheckIR(
+        "escapeViaInstancePut",
+        checkEscapingName(true, invokesMethodWithName("namingInterfaceConsumer")));
   }
 
   @Test
   public void testEscapeViaArrayPut() throws Exception {
-    buildAndCheckIR("escapeViaArrayPut", checkEscapingName(appInfo, true, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString()
-              .equals("namingInterfacesConsumer");
-    }));
+    buildAndCheckIR(
+        "escapeViaArrayPut",
+        checkEscapingName(true, invokesMethodWithName("namingInterfacesConsumer")));
   }
 
   @Test
   public void testEscapeViaArrayArgumentPut() throws Exception {
-    buildAndCheckIR("escapeViaArrayArgumentPut", checkEscapingName(appInfo, true, instr -> {
-      return instr.isArrayPut();
-    }));
+    buildAndCheckIR("escapeViaArrayArgumentPut", checkEscapingName(true, Instruction::isArrayPut));
   }
 
   @Test
   public void testEscapeViaListPut() throws Exception {
-    buildAndCheckIR("escapeViaListPut", checkEscapingName(appInfo, false, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("add");
-    }));
+    buildAndCheckIR(
+        "escapeViaListPut",
+        checkEscapingName(
+            false,
+            instr -> {
+              throw new Unreachable();
+            }));
   }
 
   @Test
   public void testEscapeViaListArgumentPut() throws Exception {
-    buildAndCheckIR("escapeViaListArgumentPut", checkEscapingName(appInfo, false, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("add");
-    }));
+    buildAndCheckIR(
+        "escapeViaListArgumentPut",
+        checkEscapingName(
+            false,
+            instr -> {
+              throw new Unreachable();
+            }));
   }
 
   @Test
   public void testEscapeViaArrayGet() throws Exception {
-    buildAndCheckIR("escapeViaArrayGet", checkEscapingName(appInfo, true, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString()
-              .equals("namingInterfaceConsumer");
-    }));
+    buildAndCheckIR(
+        "escapeViaArrayGet",
+        checkEscapingName(true, invokesMethodWithName("namingInterfaceConsumer")));
   }
 
   @Test
   public void testHandlePhiAndAlias() throws Exception {
-    buildAndCheckIR("handlePhiAndAlias", checkEscapingName(appInfo, true, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("stringConsumer");
-    }));
+    buildAndCheckIR(
+        "handlePhiAndAlias", checkEscapingName(true, invokesMethodWithName("stringConsumer")));
   }
 
   @Test
   public void testToString() throws Exception {
-    buildAndCheckIR("toString", checkEscapingName(appInfo, false, instr -> {
-      return instr.isInvokeMethod()
-          && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("toString");
-    }));
+    buildAndCheckIR("toString", checkEscapingName(true, Instruction::isReturn));
   }
 
   @Test
   public void testEscapeViaRecursion() throws Exception {
-    buildAndCheckIR("escapeViaRecursion", checkEscapingName(appInfo, false, instr -> {
-      return instr.isInvokeStatic();
-    }));
+    buildAndCheckIR("escapeViaRecursion", checkEscapingName(true, Instruction::isReturn));
   }
 
   @Test
   public void testEscapeViaLoopAndBoxing() throws Exception {
-    buildAndCheckIR("escapeViaLoopAndBoxing", checkEscapingName(appInfo, true, instr -> {
-      return instr.isReturn()
-          || (instr.isInvokeMethod()
-              && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("toString"));
-    }));
+    buildAndCheckIR(
+        "escapeViaLoopAndBoxing",
+        checkEscapingName(
+            true, instr -> instr.isReturn() || invokesMethodWithName("toString").test(instr)));
   }
 
-  private static Consumer<IRCode> checkEscapingName(
-      AppInfo appInfo,
-      boolean expectedHeuristicResult,
-      Predicate<Instruction> instructionTester) {
+  private Consumer<IRCode> checkEscapingName(
+      boolean expectedHeuristicResult, Predicate<Instruction> instructionTester) {
+    assert instructionTester != null;
     return code -> {
       InvokeVirtual simpleNameCall = getSimpleNameCall(code);
       assertNotNull(simpleNameCall);
       Value v = simpleNameCall.outValue();
       assertNotNull(v);
-      Set<Instruction> escapingInstructions = EscapeAnalysis.escape(code, v);
-      assertEquals(
-          expectedHeuristicResult,
-          StringOptimizer.hasPotentialReadOutside(appInfo, code.method, escapingInstructions));
-      if (instructionTester == null) {
-        // Implicitly expecting the absence of escaping points.
-        assertTrue(escapingInstructions.isEmpty());
-      } else {
-        // Otherwise, test all escaping instructions.
-        assertFalse(escapingInstructions.isEmpty());
-        assertTrue(escapingInstructions.stream().allMatch(instructionTester));
-      }
+      EscapeAnalysis escapeAnalysis =
+          new EscapeAnalysis(
+              appView, StringOptimizer.StringOptimizerEscapeAnalysisConfiguration.getInstance());
+      Set<Instruction> escapeRoutes = escapeAnalysis.computeEscapeRoutes(code, v);
+      assertNotEquals(expectedHeuristicResult, escapeRoutes.isEmpty());
+      assertTrue(escapeRoutes.stream().allMatch(instructionTester));
     };
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/ArrayTypeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/ArrayTypeTest.java
index 8b28c5d..2d50834 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/ArrayTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/ArrayTypeTest.java
@@ -9,7 +9,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.ir.analysis.AnalysisTestBase;
 import com.android.tools.r8.ir.code.ArrayGet;
 import com.android.tools.r8.ir.code.ArrayPut;
@@ -47,8 +46,7 @@
 
   private static Consumer<IRCode> arrayTestInspector() {
     return code -> {
-      Iterable<Instruction> instructions = code::instructionIterator;
-      for (Instruction instruction : instructions) {
+      for (Instruction instruction : code.instructions()) {
         if (instruction.isArrayGet() || instruction.isArrayPut()) {
           Value array, value;
           if (instruction.isArrayGet()) {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
index 798f1b0..06ea239 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
@@ -84,8 +84,7 @@
   private static Consumer<IRCode> testInspector(
       TypeLatticeElement expectedType, int expectedNumberOfConstNumberInstructions) {
     return code -> {
-      Iterable<Instruction> instructions = code::instructionIterator;
-      for (Instruction instruction : instructions) {
+      for (Instruction instruction : code.instructions()) {
         if (instruction.isConstNumber()) {
           ConstNumber constNumberInstruction = instruction.asConstNumber();
           assertEquals(expectedType, constNumberInstruction.outValue().getTypeLattice());
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/UnconstrainedPrimitiveTypeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/UnconstrainedPrimitiveTypeTest.java
index 12c1329..ffa586b 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/UnconstrainedPrimitiveTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/UnconstrainedPrimitiveTypeTest.java
@@ -104,8 +104,7 @@
   private static Consumer<IRCode> testInspector(
       TypeLatticeElement expectedType, int expectedNumberOfConstNumberInstructions) {
     return code -> {
-      Iterable<Instruction> instructions = code::instructionIterator;
-      for (Instruction instruction : instructions) {
+      for (Instruction instruction : code.instructions()) {
         if (instruction.isConstNumber()) {
           ConstNumber constNumberInstruction = instruction.asConstNumber();
           assertEquals(expectedType, constNumberInstruction.outValue().getTypeLattice());