Merge "Decouple method pools from ClassAndMemberPublicizer"
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 584b06d..f0ae9e0 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -618,6 +618,13 @@
     internal.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
     internal.dataResourceConsumer = internal.programConsumer.getDataResourceConsumer();
 
+    // Default is to remove Java assertion code as Dalvik and Art does not reliable support
+    // Java assertions. When generation class file output always keep the Java assertions code.
+    assert internal.disableAssertions;
+    if (internal.isGeneratingClassFiles()) {
+      internal.disableAssertions = false;
+    }
+
     // EXPERIMENTAL flags.
     assert !internal.forceProguardCompatibility;
     internal.forceProguardCompatibility = forceProguardCompatibility;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 3586d6d..9a44dde 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -634,6 +634,7 @@
     // class inliner, null value indicates that the method is not eligible.
     private ClassInlinerEligibility classInlinerEligibility = null;
     private TrivialInitializer trivialInitializerInfo = null;
+    private boolean initializerEnablingJavaAssertions = false;
     private ParameterUsagesInfo parametersUsages = null;
     private BitSet kotlinNotNullParamHints = null;
 
@@ -705,6 +706,14 @@
       return this.trivialInitializerInfo;
     }
 
+    private void setInitializerEnablingJavaAssertions() {
+      this.initializerEnablingJavaAssertions = true;
+    }
+
+    public boolean isInitializerEnablingJavaAssertions() {
+      return initializerEnablingJavaAssertions;
+    }
+
     public long getReturnedConstant() {
       assert returnsConstant();
       return returnedConstant;
@@ -754,6 +763,10 @@
       forceInline = true;
     }
 
+    private void unsetForceInline() {
+      forceInline = false;
+    }
+
     private void markPublicized() {
       publicized = true;
     }
@@ -831,10 +844,18 @@
     ensureMutableOI().setTrivialInitializer(info);
   }
 
+  synchronized public void setInitializerEnablingJavaAssertions() {
+    ensureMutableOI().setInitializerEnablingJavaAssertions();
+  }
+
   synchronized public void markForceInline() {
     ensureMutableOI().markForceInline();
   }
 
+  public synchronized void unsetForceInline() {
+    ensureMutableOI().unsetForceInline();
+  }
+
   synchronized public void markPublicized() {
     ensureMutableOI().markPublicized();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Monitor.java b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
index 6b3c89c..3b1b2d1 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Monitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
@@ -34,6 +34,14 @@
     return inValues.get(0);
   }
 
+  public boolean isEnter() {
+    return type == Type.ENTER;
+  }
+
+  public boolean isExit() {
+    return type == Type.EXIT;
+  }
+
   @Override
   public void buildDex(DexBuilder builder) {
     // If the monitor object is an argument, we use the argument register for all the monitor
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 650757e..382dd64 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.conversion;
 
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -15,16 +16,23 @@
 import com.android.tools.r8.graph.GraphLense.GraphLenseLookupResult;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
+import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -56,7 +64,7 @@
     this.shuffle = options.testing.irOrdering;
   }
 
-  private static class Node {
+  public static class Node {
 
     public final DexEncodedMethod method;
     private int invokeCount = 0;
@@ -68,7 +76,7 @@
     // Incoming calls to this method.
     private final Set<Node> callers = new LinkedHashSet<>();
 
-    private Node(DexEncodedMethod method) {
+    public Node(DexEncodedMethod method) {
       this.method = method;
     }
 
@@ -76,19 +84,20 @@
       return method.accessFlags.isBridge();
     }
 
-    private void addCallee(Node method) {
+    public void addCallee(Node method) {
       callees.add(method);
+      method.callers.add(this);
     }
 
-    private void addCaller(Node method) {
-      callers.add(method);
+    public boolean hasCallee(Node method) {
+      return callees.contains(method);
     }
 
     boolean isSelfRecursive() {
       return isSelfRecursive;
     }
 
-    boolean isLeaf() {
+    public boolean isLeaf() {
       return callees.isEmpty();
     }
 
@@ -96,7 +105,7 @@
     public String toString() {
       StringBuilder builder = new StringBuilder();
       builder.append("MethodNode for: ");
-      builder.append(method.qualifiedName());
+      builder.append(method.toSourceString());
       builder.append(" (");
       builder.append(callees.size());
       builder.append(" callees, ");
@@ -114,7 +123,7 @@
         builder.append("Callees:\n");
         for (Node call : callees) {
           builder.append("  ");
-          builder.append(call.method.qualifiedName());
+          builder.append(call.method.toSourceString());
           builder.append("\n");
         }
       }
@@ -122,7 +131,7 @@
         builder.append("Callers:\n");
         for (Node caller : callers) {
           builder.append("  ");
-          builder.append(caller.method.qualifiedName());
+          builder.append(caller.method.toSourceString());
           builder.append("\n");
         }
       }
@@ -136,8 +145,12 @@
   private final Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
   private final Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
 
-  public static CallGraph build(DexApplication application, AppInfoWithLiveness appInfo,
-      GraphLense graphLense, InternalOptions options) {
+  public static CallGraph build(
+      DexApplication application,
+      AppInfoWithLiveness appInfo,
+      GraphLense graphLense,
+      InternalOptions options,
+      Timing timing) {
     CallGraph graph = new CallGraph(options);
     DexClass[] classes = application.classes().toArray(new DexClass[application.classes().size()]);
     Arrays.sort(classes, (DexClass a, DexClass b) -> a.type.slowCompareTo(b.type));
@@ -149,8 +162,13 @@
       }
     }
     assert allMethodsExists(application, graph);
-    graph.breakCycles();
-    assert graph.breakCycles() == 0;  // This time the cycles should be gone.
+
+    timing.begin("Cycle elimination");
+    CycleEliminator cycleEliminator = new CycleEliminator(graph.nodes.values(), options);
+    cycleEliminator.breakCycles();
+    timing.end();
+    assert cycleEliminator.breakCycles() == 0; // This time the cycles should be gone.
+
     graph.fillCallSiteSets(appInfo);
     return graph;
   }
@@ -196,9 +214,10 @@
 
   /**
    * Extract the next set of leaves (nodes with an call (outgoing) degree of 0) if any.
-   * <p>
-   * All nodes in the graph are extracted if called repeatedly until null is returned.
-   * Please note that there are no cycles in this graph (see {@link #breakCycles}).
+   *
+   * <p>All nodes in the graph are extracted if called repeatedly until null is returned. Please
+   * note that there are no cycles in this graph (see {@link CycleEliminator#breakCycles}).
+   *
    * <p>
    */
   private Set<DexEncodedMethod> extractLeaves() {
@@ -215,48 +234,197 @@
         .collect(Collectors.toCollection(LinkedHashSet::new)));
   }
 
-  private int traverse(Node node, Set<Node> stack, Set<Node> marked) {
-    int numberOfCycles = 0;
-    if (!marked.contains(node)) {
-      assert !stack.contains(node);
-      stack.add(node);
-      ArrayList<Node> toBeRemoved = null;
-      // Sort the callees before calling traverse recursively.
-      // This will ensure cycles are broken the same way across
-      // multiple invocations of the R8 compiler.
+  public static class CycleEliminator {
+
+    public static final String CYCLIC_FORCE_INLINING_MESSAGE =
+        "Unable to satisfy force inlining constraints due to cyclic force inlining";
+
+    private static class CallEdge {
+
+      private final Node caller;
+      private final Node callee;
+
+      public CallEdge(Node caller, Node callee) {
+        this.caller = caller;
+        this.callee = callee;
+      }
+    }
+
+    private final Collection<Node> nodes;
+    private final InternalOptions options;
+
+    // DFS stack.
+    private Deque<Node> stack = new ArrayDeque<>();
+
+    // Set of nodes on the DFS stack.
+    private Set<Node> stackSet = Sets.newIdentityHashSet();
+
+    // Set of nodes that have been visited entirely.
+    private Set<Node> marked = Sets.newIdentityHashSet();
+
+    private int numberOfCycles = 0;
+
+    public CycleEliminator(Collection<Node> nodes, InternalOptions options) {
+      this.options = options;
+
+      // Call to reorderNodes must happen after assigning options.
+      this.nodes =
+          options.testing.nondeterministicCycleElimination
+              ? reorderNodes(new ArrayList<>(nodes))
+              : nodes;
+    }
+
+    public int breakCycles() {
+      // Break cycles in this call graph by removing edges causing cycles.
+      for (Node node : nodes) {
+        traverse(node);
+      }
+      int result = numberOfCycles;
+      reset();
+      return result;
+    }
+
+    private void reset() {
+      assert stack.isEmpty();
+      assert stackSet.isEmpty();
+      marked.clear();
+      numberOfCycles = 0;
+    }
+
+    private void traverse(Node node) {
+      if (marked.contains(node)) {
+        // Already visited all nodes that can be reached from this node.
+        return;
+      }
+
+      push(node);
+
+      // Sort the callees before calling traverse recursively. This will ensure cycles are broken
+      // the same way across multiple invocations of the R8 compiler.
       Node[] callees = node.callees.toArray(new Node[node.callees.size()]);
       Arrays.sort(callees, (Node a, Node b) -> a.method.method.slowCompareTo(b.method.method));
+      if (options.testing.nondeterministicCycleElimination) {
+        reorderNodes(Arrays.asList(callees));
+      }
+
       for (Node callee : callees) {
-        if (stack.contains(callee)) {
-          if (toBeRemoved == null) {
-            toBeRemoved = new ArrayList<>();
+        if (stackSet.contains(callee)) {
+          // Found a cycle that needs to be eliminated.
+          numberOfCycles++;
+
+          if (edgeRemovalIsSafe(node, callee)) {
+            // Break the cycle by removing the edge node->callee.
+            callee.callers.remove(node);
+            node.callees.remove(callee);
+
+            if (Log.ENABLED) {
+              Log.info(
+                  CallGraph.class,
+                  "Removed call edge from method '%s' to '%s'",
+                  node.method.toSourceString(),
+                  callee.method.toSourceString());
+            }
+          } else {
+            // The cycle has a method that is marked as force inline.
+            LinkedList<Node> cycle = extractCycle(callee);
+
+            if (Log.ENABLED) {
+              Log.info(
+                  CallGraph.class, "Extracted cycle to find an edge that can safely be removed");
+            }
+
+            // Break the cycle by finding an edge that can be removed without breaking force
+            // inlining. If that is not possible, this call fails with a compilation error.
+            CallEdge edge = findCallEdgeForRemoval(cycle);
+
+            // The edge will be null if this cycle has already been eliminated as a result of
+            // another cycle elimination.
+            if (edge != null) {
+              assert edgeRemovalIsSafe(edge.caller, edge.callee);
+
+              // Break the cycle by removing the edge caller->callee.
+              edge.caller.callees.remove(edge.callee);
+              edge.callee.callers.remove(edge.caller);
+
+              if (Log.ENABLED) {
+                Log.info(
+                    CallGraph.class,
+                    "Removed call edge from force inlined method '%s' to '%s' to ensure that "
+                        + "force inlining will succeed",
+                    node.method.toSourceString(),
+                    callee.method.toSourceString());
+              }
+            }
+
+            // Recover the stack.
+            recoverStack(cycle);
           }
-          // We have a cycle; break it by removing node->callee.
-          toBeRemoved.add(callee);
-          callee.callers.remove(node);
         } else {
-          numberOfCycles += traverse(callee, stack, marked);
+          traverse(callee);
         }
       }
-      if (toBeRemoved != null) {
-        numberOfCycles += toBeRemoved.size();
-        node.callees.removeAll(toBeRemoved);
-      }
-      stack.remove(node);
+      pop(node);
       marked.add(node);
     }
-    return numberOfCycles;
-  }
 
-  private int breakCycles() {
-    // Break cycles in this call graph by removing edges causing cycles.
-    int numberOfCycles = 0;
-    Set<Node> stack = Sets.newIdentityHashSet();
-    Set<Node> marked = Sets.newIdentityHashSet();
-    for (Node node : nodes.values()) {
-      numberOfCycles += traverse(node, stack, marked);
+    private void push(Node node) {
+      stack.push(node);
+      boolean changed = stackSet.add(node);
+      assert changed;
     }
-    return numberOfCycles;
+
+    private void pop(Node node) {
+      Node popped = stack.pop();
+      assert popped == node;
+      boolean changed = stackSet.remove(node);
+      assert changed;
+    }
+
+    private LinkedList<Node> extractCycle(Node entry) {
+      LinkedList<Node> cycle = new LinkedList<>();
+      do {
+        assert !stack.isEmpty();
+        cycle.add(stack.pop());
+      } while (cycle.getLast() != entry);
+      return cycle;
+    }
+
+    private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) {
+      Node callee = extractedCycle.getLast();
+      for (Node caller : extractedCycle) {
+        if (!caller.callees.contains(callee)) {
+          // No need to break any edges since this cycle has already been broken previously.
+          assert !callee.callers.contains(caller);
+          return null;
+        }
+        if (edgeRemovalIsSafe(caller, callee)) {
+          return new CallEdge(caller, callee);
+        }
+        callee = caller;
+      }
+      throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE);
+    }
+
+    private static boolean edgeRemovalIsSafe(Node caller, Node callee) {
+      // All call edges where the callee is a method that should be force inlined must be kept,
+      // to guarantee that the IR converter will process the callee before the caller.
+      return !callee.method.getOptimizationInfo().forceInline();
+    }
+
+    private void recoverStack(LinkedList<Node> extractedCycle) {
+      Iterator<Node> descendingIt = extractedCycle.descendingIterator();
+      while (descendingIt.hasNext()) {
+        stack.push(descendingIt.next());
+      }
+    }
+
+    private Collection<Node> reorderNodes(List<Node> nodes) {
+      assert options.testing.nondeterministicCycleElimination;
+      if (!InternalOptions.DETERMINISTIC_DEBUGGING) {
+        Collections.shuffle(nodes);
+      }
+      return nodes;
+    }
   }
 
   synchronized private Node ensureMethodNode(DexEncodedMethod method) {
@@ -268,7 +436,6 @@
     assert callee != null;
     if (caller != callee) {
       caller.addCallee(callee);
-      callee.addCaller(caller);
     } else {
       caller.isSelfRecursive = true;
     }
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 3ea97e4..33531da 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
@@ -108,6 +108,7 @@
   private final boolean enableWholeProgramOptimizations;
 
   private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
+  private final OptimizationFeedback simpleOptimizationFeedback = new OptimizationFeedbackSimple();
   private DexString highestSortingString;
 
   private IRConverter(
@@ -357,19 +358,34 @@
       ExecutorService executor) throws ExecutionException {
     List<Future<?>> futures = new ArrayList<>();
     for (DexProgramClass clazz : classes) {
-      futures.add(executor.submit(() -> clazz.forEachMethod(this::convertMethodToDex)));
+      futures.add(executor.submit(() -> convertMethodsToDex(clazz)));
     }
     ThreadUtils.awaitFutures(futures);
   }
 
-  void convertMethodToDex(DexEncodedMethod method) {
+  private void convertMethodsToDex(DexProgramClass clazz) {
+    // When converting all methods on a class always convert <clinit> first.
+    for (DexEncodedMethod method : clazz.directMethods()) {
+      if (method.isClassInitializer()) {
+        convertMethodToDex(method);
+        break;
+      }
+    }
+    clazz.forEachMethod(method -> {
+      if (!method.isClassInitializer()) {
+        convertMethodToDex(method);
+      }
+    });
+  }
+
+  private void convertMethodToDex(DexEncodedMethod method) {
     assert options.isGeneratingDex();
     if (method.getCode() != null) {
       boolean matchesMethodFilter = options.methodMatchesFilter(method);
       if (matchesMethodFilter) {
         if (!(options.passthroughDexCode && method.getCode().isDexCode())) {
           // We do not process in call graph order, so anything could be a leaf.
-          rewriteCode(method, ignoreOptimizationFeedback, x -> true, CallSiteInformation.empty(),
+          rewriteCode(method, simpleOptimizationFeedback, x -> true, CallSiteInformation.empty(),
               Outliner::noProcessing);
         }
         updateHighestSortingStrings(method);
@@ -401,8 +417,8 @@
     OptimizationFeedback directFeedback = new OptimizationFeedbackDirect();
     {
       timing.begin("Build call graph");
-      CallGraph callGraph = CallGraph
-          .build(application, appInfo.withLiveness(), graphLense, options);
+      CallGraph callGraph =
+          CallGraph.build(application, appInfo.withLiveness(), graphLense, options, timing);
       timing.end();
       timing.begin("IR conversion phase 1");
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler =
@@ -668,7 +684,7 @@
       codeRewriter.removeSwitchMaps(code);
     }
     if (options.disableAssertions) {
-      codeRewriter.disableAssertions(code);
+      codeRewriter.disableAssertions(appInfo, method, code, feedback);
     }
     if (options.enableNonNullTracking && nonNullTracker != null) {
       nonNullTracker.addNonNull(code);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index c26aecf..49bfac8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -21,6 +21,7 @@
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
   void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info);
+  void setInitializerEnablingJavaAssertions(DexEncodedMethod method);
   void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo);
   void setKotlinNotNullParamHints(DexEncodedMethod method, BitSet hints);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index 2b1a6db..1d7a2d6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -60,6 +60,11 @@
   }
 
   @Override
+  public void setInitializerEnablingJavaAssertions(DexEncodedMethod method) {
+    method.setInitializerEnablingJavaAssertions();
+  }
+
+  @Override
   public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
     method.setParameterUsages(parameterUsagesInfo);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index a547b36..2fb2969 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -44,6 +44,10 @@
   }
 
   @Override
+  public void setInitializerEnablingJavaAssertions(DexEncodedMethod method) {
+  }
+
+  @Override
   public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
new file mode 100644
index 0000000..b31f28a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.ParameterUsagesInfo;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.BitSet;
+
+public class OptimizationFeedbackSimple implements OptimizationFeedback {
+
+  @Override
+  public void methodReturnsArgument(DexEncodedMethod method, int argument) {
+    // Ignored.
+  }
+
+  @Override
+  public void methodReturnsConstant(DexEncodedMethod method, long value) {
+    // Ignored.
+  }
+
+  @Override
+  public void methodNeverReturnsNull(DexEncodedMethod method) {
+    // Ignored.
+  }
+
+  @Override
+  public void methodNeverReturnsNormally(DexEncodedMethod method) {
+    // Ignored.
+  }
+
+  @Override
+  public void markProcessed(DexEncodedMethod method, Constraint state) {
+    // Just as processed, don't provide any inlining constraints.
+    method.markProcessed(Constraint.NEVER);
+  }
+
+  @Override
+  public void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
+    // Ignored.
+  }
+
+  @Override
+  public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
+    // Ignored.
+  }
+
+  @Override
+  public void setClassInlinerEligibility(
+      DexEncodedMethod method, ClassInlinerEligibility eligibility) {
+    // Ignored.
+  }
+
+  @Override
+  public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
+    // Ignored.
+  }
+
+  @Override
+  public void setInitializerEnablingJavaAssertions(DexEncodedMethod method) {
+    method.setInitializerEnablingJavaAssertions();
+  }
+
+  @Override
+  public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
+    // Ignored.
+  }
+
+  @Override
+  public void setKotlinNotNullParamHints(DexEncodedMethod method, BitSet hints) {
+    // Ignored.
+  }
+}
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 94e8147..d87a508 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
@@ -1265,7 +1265,29 @@
    * }
    * </pre>
    */
-  public void disableAssertions(IRCode code) {
+  public void disableAssertions(
+      AppInfo appInfo, DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    if (method.isClassInitializer()) {
+      if (!hasJavacClinitAssertionCode(code)) {
+        return;
+      }
+      // Mark the clinit as having code to turn on assertions.
+      feedback.setInitializerEnablingJavaAssertions(method);
+    } else {
+      // If the clinit of this class did not have have code to turn on assertions don't try to
+      // remove assertion code from the method.
+      DexClass clazz = appInfo.definitionFor(method.method.holder);
+      if (clazz == null) {
+        return;
+      }
+      DexEncodedMethod clinit = clazz.getClassInitializer();
+      if (clinit == null
+          || !clinit.isProcessed()
+          || !clinit.getOptimizationInfo().isInitializerEnablingJavaAssertions()) {
+        return;
+      }
+    }
+
     InstructionIterator iterator = code.instructionIterator();
     while (iterator.hasNext()) {
       Instruction current = iterator.next();
@@ -1288,6 +1310,78 @@
     }
   }
 
+  private boolean isClassDesiredAssertionStatusInvoke(Instruction instruction) {
+    return instruction.isInvokeMethod()
+    && instruction.asInvokeMethod().getInvokedMethod()
+        == dexItemFactory.classMethods.desiredAssertionStatus;
+  }
+
+  private boolean isAssertionsDisabledFieldPut(Instruction instruction) {
+    return instruction.isStaticPut()
+        && instruction.asStaticPut().getField().name == dexItemFactory.assertionsDisabled;
+  }
+
+  private boolean isNotDebugInstruction(Instruction instruction) {
+    return !instruction.isDebugInstruction();
+  }
+
+  private Value blockWithSingleConstNumberAndGoto(BasicBlock block) {
+    InstructionIterator iterator  = block.iterator();
+    Instruction constNumber = iterator.nextUntil(this::isNotDebugInstruction);
+    if (constNumber == null || !constNumber.isConstNumber()) {
+      return null;
+    }
+    Instruction exit = iterator.nextUntil(this::isNotDebugInstruction);
+    return exit != null && exit.isGoto() ? constNumber.outValue() : null;
+  }
+
+  private Value blockWithAssertionsDisabledFieldPut(BasicBlock block) {
+    InstructionIterator iterator  = block.iterator();
+    Instruction fieldPut = iterator.nextUntil(this::isNotDebugInstruction);
+    return fieldPut != null
+        && isAssertionsDisabledFieldPut(fieldPut) ? fieldPut.inValues().get(0) : null;
+  }
+
+  private boolean hasJavacClinitAssertionCode(IRCode code) {
+    InstructionIterator iterator = code.instructionIterator();
+    Instruction current = iterator.nextUntil(this::isClassDesiredAssertionStatusInvoke);
+    if (current == null) {
+      return false;
+    }
+
+    Value DesiredAssertionStatus = current.outValue();
+    assert iterator.hasNext();
+    current = iterator.next();
+    if (!current.isIf()
+        || !current.asIf().isZeroTest()
+        || current.asIf().inValues().get(0) != DesiredAssertionStatus) {
+      return false;
+    }
+
+    If theIf = current.asIf();
+    BasicBlock trueTarget = theIf.getTrueTarget();
+    BasicBlock falseTarget = theIf.fallthroughBlock();
+    if (trueTarget == falseTarget) {
+      return false;
+    }
+
+    Value trueValue = blockWithSingleConstNumberAndGoto(trueTarget);
+    Value falseValue = blockWithSingleConstNumberAndGoto(falseTarget);
+    if (trueValue == null
+        || falseValue == null
+        || (trueTarget.exit().asGoto().getTarget() != falseTarget.exit().asGoto().getTarget())) {
+      return false;
+    }
+
+    BasicBlock target = trueTarget.exit().asGoto().getTarget();
+    Value storeValue = blockWithAssertionsDisabledFieldPut(target);
+    return storeValue != null
+        && storeValue.isPhi()
+        && storeValue.asPhi().getOperands().size() == 2
+        && storeValue.asPhi().getOperands().contains(trueValue)
+        && storeValue.asPhi().getOperands().contains(falseValue);
+  }
+
   // Check if the static put is a constant derived from the class holding the method.
   // This checks for java.lang.Class.getName and java.lang.Class.getSimpleName.
   private boolean isClassNameConstant(DexEncodedMethod method, StaticPut put) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
index dee935e..905d4a8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -132,7 +132,8 @@
             }
           }
         }
-        if (instruction.isMonitor() || instruction.isInvokeMethod()) {
+        if ((instruction.isMonitor() && instruction.asMonitor().isEnter())
+            || instruction.isInvokeMethod()) {
           killAllActiveFields();
         }
       }
diff --git a/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java b/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
index b53e1bc..50571f3 100644
--- a/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
@@ -106,7 +106,8 @@
     assert accessFlags.isPrivate();
 
     if (appView.getDexItemFactory().isConstructor(encodedMethod.method)) {
-      // TODO(b/72211928)
+      accessFlags.unsetPrivate();
+      accessFlags.setPublic();
       return false;
     }
 
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 3909dfd..93a80b6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -447,6 +447,7 @@
     public boolean placeExceptionalBlocksLast = false;
     public boolean dontCreateMarkerInD8 = false;
     public boolean forceJumboStringProcessing = false;
+    public boolean nondeterministicCycleElimination = false;
     public Set<Inliner.Reason> validInliningReasons = null;
     public boolean suppressExperimentalCfBackendWarning = false;
   }
diff --git a/src/test/examples/classmerging/CallGraphCycleTest.java b/src/test/examples/classmerging/CallGraphCycleTest.java
new file mode 100644
index 0000000..40347d6
--- /dev/null
+++ b/src/test/examples/classmerging/CallGraphCycleTest.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public class CallGraphCycleTest {
+
+  public static void main(String[] args) {
+    new B(true);
+  }
+
+  public static class A {
+
+    public A(boolean instantiateB) {
+      if (instantiateB) {
+        new B(false);
+      }
+      System.out.println("A(" + instantiateB + ")");
+    }
+  }
+
+  public static class B extends A {
+
+    public B(boolean instantiateBinA) {
+      super(instantiateBinA);
+      System.out.println("B(" + instantiateBinA + ")");
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 4ba014e..37a3010 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -7,6 +7,9 @@
 -keep public class classmerging.Test {
   public static void main(...);
 }
+-keep public class classmerging.CallGraphCycleTest {
+  public static void main(...);
+}
 -keep public class classmerging.ClassWithNativeMethodTest {
   public static void main(...);
 }
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationTestBase.java b/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationTestBase.java
new file mode 100644
index 0000000..d88f3c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationTestBase.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2018, 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.accessrelaxation;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPublic;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+abstract class AccessRelaxationTestBase extends TestBase {
+
+  static R8Command.Builder loadProgramFiles(Iterable<Class> classes) {
+    R8Command.Builder builder = R8Command.builder();
+    for (Class clazz : classes) {
+      builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
+    }
+    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
+    return builder;
+  }
+
+  static R8Command.Builder loadProgramFiles(Package p, Class... classes) throws Exception {
+    R8Command.Builder builder = R8Command.builder();
+    builder.addProgramFiles(ToolHelper.getClassFilesForTestPackage(p));
+    for (Class clazz : classes) {
+      builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
+    }
+    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
+    return builder;
+  }
+
+  void compareJvmAndArt(AndroidApp app, Class mainClass) throws Exception {
+    // Run on Jvm.
+    String jvmOutput = runOnJava(mainClass);
+
+    // Run on Art to check generated code against verifier.
+    String artOutput = runOnArt(app, mainClass);
+
+    String adjustedArtOutput = artOutput.replace(
+        "java.lang.IncompatibleClassChangeError", "java.lang.IllegalAccessError");
+    assertEquals(jvmOutput, adjustedArtOutput);
+  }
+
+  static void assertPublic(DexInspector dexInspector, Class clazz, MethodSignature signature) {
+    ClassSubject classSubject = dexInspector.clazz(clazz);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method(signature);
+    assertThat(methodSubject, isPublic());
+  }
+
+  static void assertNotPublic(DexInspector dexInspector, Class clazz, MethodSignature signature) {
+    ClassSubject classSubject = dexInspector.clazz(clazz);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method(signature);
+    assertThat(methodSubject, not(isPublic()));
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java
new file mode 100644
index 0000000..01d5621
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/ConstructorRelaxationTest.java
@@ -0,0 +1,198 @@
+// Copyright (c) 2018, 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.accessrelaxation;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import java.util.List;
+import org.junit.Test;
+
+class L1 {
+  private final String x;
+
+  private L1(String x) {
+    this.x = x;
+  }
+
+  private L1() {
+    this("private_x");
+  }
+
+  static L1 create() {
+    return new L1();
+  }
+
+  L1(int i) {
+    this(String.valueOf(i));
+  }
+
+  @Override
+  public String toString() {
+    return x;
+  }
+}
+
+class L2_1 extends L1 {
+  private String y;
+
+  private L2_1() {
+    this(21);
+    this.y = "private_L2_1_y";
+  }
+
+  L2_1(int i) {
+    super(i);
+    this.y = "L2_1_y";
+  }
+
+  private L2_1(String y) {
+    this(21);
+    this.y = y;
+  }
+
+  static L2_1 create(String y) {
+    return new L2_1(y);
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + "_" + y;
+  }
+}
+
+class L2_2 extends L1 {
+  private String y;
+
+  private L2_2(int i) {
+    super(i);
+    this.y = "private_L2_2_y";
+  }
+
+  L2_2(String y) {
+    this(22);
+    this.y = y;
+  }
+
+  static L2_1 create() {
+    return new L2_1(22);
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + "_" + y;
+  }
+}
+
+class L3_1 extends L2_1 {
+  private final String z;
+
+  private L3_1(int i) {
+    this(String.valueOf(i));
+  }
+
+  private L3_1(String z) {
+    super(31);
+    this.z = z;
+  }
+
+  static L3_1 create(int i) {
+    return new L3_1(i);
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + "_" + z;
+  }
+}
+
+class L3_2 extends L2_2 {
+  private String z;
+
+  private L3_2() {
+    super("private_L3_2_y");
+    this.z = "private_L3_2_z";
+  }
+
+  private L3_2(int i) {
+    super(String.valueOf(i));
+    this.z = "private_L3_2_z" + "_" + i;
+  }
+
+  L3_2(String z) {
+    this(32);
+    this.z = z;
+  }
+
+  static L3_2 create(String z) {
+    return new L3_2(z);
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + "_" + z;
+  }
+}
+
+class CtorTestMain {
+  public static void main(String[] args) {
+    System.out.println(L1.create());
+    System.out.println(L2_1.create("main_y"));
+    System.out.println(L2_2.create());
+    System.out.println(L3_1.create(41));
+    System.out.println(L3_2.create("main_z"));
+  }
+}
+
+public final class ConstructorRelaxationTest extends AccessRelaxationTestBase {
+  private static final String INIT= "<init>";
+  private static final List<Class> CLASSES =
+      ImmutableList.of(L1.class, L2_1.class, L2_2.class, L3_1.class, L3_2.class);
+
+  @Test
+  public void test() throws Exception {
+    Class mainClass = CtorTestMain.class;
+    R8Command.Builder builder =
+        loadProgramFiles(Iterables.concat(CLASSES, ImmutableList.of(mainClass)));
+    builder.addProguardConfiguration(
+        ImmutableList.of(
+            "-keep class " + mainClass.getCanonicalName() + "{",
+            "  public static void main(java.lang.String[]);",
+            "}",
+            "",
+            "-keep class *.L* {",
+            "  <init>(...);",
+            "}",
+            "",
+            "-dontobfuscate",
+            "-allowaccessmodification"
+        ),
+        Origin.unknown());
+
+    AndroidApp app = ToolHelper.runR8(builder.build(), options -> {
+      options.enableInlining = false;
+      options.enableClassMerging = false;
+    });
+    compareJvmAndArt(app, mainClass);
+
+    DexInspector dexInspector = new DexInspector(app);
+    for (Class clazz : CLASSES) {
+      ClassSubject classSubject = dexInspector.clazz(clazz);
+      assertThat(classSubject, isPresent());
+      classSubject.getDexClass().forEachMethod(m -> {
+        assertTrue(!m.isInstanceInitializer() || m.isPublicMethod());
+      });
+    }
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
similarity index 69%
rename from src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationTest.java
rename to src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
index 53a8c31..d5190e6 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -4,17 +4,8 @@
 
 package com.android.tools.r8.accessrelaxation;
 
-import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
-import static com.android.tools.r8.utils.DexInspectorMatchers.isPublic;
-import static org.hamcrest.CoreMatchers.not;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-
-import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
-import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.accessrelaxation.privateinstance.Base;
 import com.android.tools.r8.accessrelaxation.privateinstance.Sub1;
 import com.android.tools.r8.accessrelaxation.privateinstance.Sub2;
@@ -27,55 +18,12 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
-import com.android.tools.r8.utils.DexInspector.ClassSubject;
-import com.android.tools.r8.utils.DexInspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
-@RunWith(VmTestRunner.class)
-public class AccessRelaxationTest extends TestBase {
+public final class NonConstructorRelaxationTest extends AccessRelaxationTestBase {
   private static final String STRING = "java.lang.String";
 
-  private static R8Command.Builder loadProgramFiles(Package p, Class... classes) throws Exception {
-    R8Command.Builder builder = R8Command.builder();
-    builder.addProgramFiles(ToolHelper.getClassFilesForTestPackage(p));
-    for (Class clazz : classes) {
-      builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
-    }
-    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
-    return builder;
-  }
-
-  private void compareJvmAndArt(AndroidApp app, Class mainClass) throws Exception {
-    // Run on Jvm.
-    String jvmOutput = runOnJava(mainClass);
-
-    // Run on Art to check generated code against verifier.
-    String artOutput = runOnArt(app, mainClass);
-
-    String adjustedArtOutput = artOutput.replace(
-        "java.lang.IncompatibleClassChangeError", "java.lang.IllegalAccessError");
-    assertEquals(jvmOutput, adjustedArtOutput);
-  }
-
-  private static void assertPublic(
-      DexInspector dexInspector, Class clazz, MethodSignature signature) {
-    ClassSubject classSubject = dexInspector.clazz(clazz);
-    assertThat(classSubject, isPresent());
-    MethodSubject methodSubject = classSubject.method(signature);
-    assertThat(methodSubject, isPublic());
-  }
-
-  private static void assertNotPublic(
-      DexInspector dexInspector, Class clazz, MethodSignature signature) {
-    ClassSubject classSubject = dexInspector.clazz(clazz);
-    assertThat(classSubject, isPresent());
-    MethodSubject methodSubject = classSubject.method(signature);
-    assertThat(methodSubject, not(isPublic()));
-  }
-
   @Test
   public void testStaticMethodRelaxation() throws Exception {
     Class mainClass = C.class;
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index b6489b0..7635f0c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -70,6 +70,7 @@
     options.enableClassMerging = true;
     options.enableClassInlining = false;
     options.enableMinification = false;
+    options.testing.nondeterministicCycleElimination = true;
   }
 
   private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
@@ -115,6 +116,29 @@
     }
   }
 
+  // This test has a cycle in the call graph consisting of the methods A.<init> and B.<init>.
+  // When nondeterministicCycleElimination is enabled, we shuffle the nodes in the call graph
+  // before the cycle elimination. Therefore, it is nondeterministic if the cycle detector will
+  // detect the cycle upon the call edge A.<init> to B.<init>, or B.<init> to A.<init>. In order
+  // increase the likelihood that this test encounters both orderings, the test is repeated 5 times.
+  // Assuming that the chance of observing one of the two orderings is 50%, this test therefore has
+  // approximately 3% chance of succeeding although there is an issue.
+  @Test
+  public void testCallGraphCycle() throws Exception {
+    String main = "classmerging.CallGraphCycleTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("CallGraphCycleTest.class"),
+          CF_DIR.resolve("CallGraphCycleTest$A.class"),
+          CF_DIR.resolve("CallGraphCycleTest$B.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of("classmerging.CallGraphCycleTest", "classmerging.CallGraphCycleTest$B");
+    for (int i = 0; i < 5; i++) {
+      runTest(main, programFiles, preservedClassNames::contains);
+    }
+  }
+
   @Test
   public void testConflictInGeneratedName() throws Exception {
     String main = "classmerging.ConflictInGeneratedNameTest";
diff --git a/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java b/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java
new file mode 100644
index 0000000..56699e3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java
@@ -0,0 +1,214 @@
+// Copyright (c) 2018, 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.callgraph;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.errors.CompilationError;
+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.ir.conversion.CallGraph.CycleEliminator;
+import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class CycleEliminationTest extends TestBase {
+
+  private static class Configuration {
+
+    final Collection<Node> nodes;
+    final Set<Node> forceInline;
+    final BooleanSupplier test;
+
+    Configuration(Collection<Node> nodes, Set<Node> forceInline, BooleanSupplier test) {
+      this.nodes = nodes;
+      this.forceInline = forceInline;
+      this.test = test;
+    }
+  }
+
+  private DexItemFactory dexItemFactory = new DexItemFactory();
+
+  @Rule public final ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void testSimpleCycle() {
+    Node method = createNode("n1");
+    Node forceInlinedMethod = createForceInlinedNode("n2");
+
+    Iterable<Collection<Node>> orderings =
+        ImmutableList.of(
+            ImmutableList.of(method, forceInlinedMethod),
+            ImmutableList.of(forceInlinedMethod, method));
+
+    for (Collection<Node> nodes : orderings) {
+      // Create a cycle between the two nodes.
+      method.addCallee(forceInlinedMethod);
+      forceInlinedMethod.addCallee(method);
+
+      // Check that the cycle eliminator finds the cycle.
+      CycleEliminator cycleEliminator = new CycleEliminator(nodes, new InternalOptions());
+      assertEquals(1, cycleEliminator.breakCycles());
+
+      // The edge from method to forceInlinedMethod should be removed to ensure that force inlining
+      // will work.
+      assertTrue(forceInlinedMethod.isLeaf());
+
+      // Check that the cycle eliminator agrees that there are no more cycles left.
+      assertEquals(0, cycleEliminator.breakCycles());
+    }
+  }
+
+  @Test
+  public void testSimpleCycleWithCyclicForceInlining() {
+    Node method = createForceInlinedNode("n1");
+    Node forceInlinedMethod = createForceInlinedNode("n2");
+
+    // Create a cycle between the two nodes.
+    method.addCallee(forceInlinedMethod);
+    forceInlinedMethod.addCallee(method);
+
+    CycleEliminator cycleEliminator =
+        new CycleEliminator(ImmutableList.of(method, forceInlinedMethod), new InternalOptions());
+
+    exception.expect(CompilationError.class);
+    exception.expectMessage(CycleEliminator.CYCLIC_FORCE_INLINING_MESSAGE);
+
+    // Should throw because force inlining will fail.
+    cycleEliminator.breakCycles();
+  }
+
+  @Test
+  public void testGraphWithNestedCycles() {
+    Node n1 = createNode("n1");
+    Node n2 = createNode("n2");
+    Node n3 = createNode("n3");
+
+    BooleanSupplier canInlineN1 =
+        () -> {
+          // The node n1 should be force inlined into n2 and n3, so these edges must be kept.
+          assertTrue(n2.hasCallee(n1));
+          assertTrue(n3.hasCallee(n1));
+          // Furthermore, the edge from n1 to n2 must be removed.
+          assertFalse(n1.hasCallee(n2));
+          return true;
+        };
+
+    BooleanSupplier canInlineN3 =
+        () -> {
+          // The node n3 should be force inlined into n2, so this edge must be kept.
+          assertTrue(n2.hasCallee(n3));
+          // Furthermore, one of the edges n1 -> n2 and n3 -> n1 must be removed.
+          assertFalse(n1.hasCallee(n2) && n3.hasCallee(n1));
+          return true;
+        };
+
+    BooleanSupplier canInlineN1N3 =
+        () -> {
+          // The edge n1 -> n2 must be removed.
+          assertFalse(n1.hasCallee(n2));
+          // Check that both can be force inlined.
+          return canInlineN1.getAsBoolean() && canInlineN3.getAsBoolean();
+        };
+
+    List<Collection<Node>> orderings =
+        ImmutableList.of(
+            ImmutableList.of(n1, n2, n3),
+            ImmutableList.of(n1, n3, n2),
+            ImmutableList.of(n2, n1, n3),
+            ImmutableList.of(n2, n3, n1),
+            ImmutableList.of(n3, n1, n2),
+            ImmutableList.of(n3, n2, n1));
+
+    List<Configuration> configurations = new ArrayList<>();
+    // All orderings, where no methods are marked as force inline.
+    orderings
+        .stream()
+        .map(ordering -> new Configuration(ordering, ImmutableSet.of(), null))
+        .forEach(configurations::add);
+    // All orderings, where n1 is marked as force inline
+    // (the configuration where n2 is marked as force inline is symmetric).
+    orderings
+        .stream()
+        .map(ordering -> new Configuration(ordering, ImmutableSet.of(n1), canInlineN1))
+        .forEach(configurations::add);
+    // All orderings, where n3 is marked as force inline.
+    orderings
+        .stream()
+        .map(ordering -> new Configuration(ordering, ImmutableSet.of(n3), canInlineN3))
+        .forEach(configurations::add);
+    // All orderings, where n1 and n3 are marked as force inline.
+    orderings
+        .stream()
+        .map(ordering -> new Configuration(ordering, ImmutableSet.of(n1, n3), canInlineN1N3))
+        .forEach(configurations::add);
+
+    for (Configuration configuration : configurations) {
+      // Create a cycle between the three nodes.
+      n1.addCallee(n2);
+      n2.addCallee(n3);
+      n3.addCallee(n1);
+
+      // Create a cycle in the graph between node n1 and n2.
+      n2.addCallee(n1);
+
+      for (Node node : configuration.nodes) {
+        if (configuration.forceInline.contains(node)) {
+          node.method.markForceInline();
+        } else {
+          node.method.unsetForceInline();
+        }
+      }
+
+      // Check that the cycle eliminator finds the cycles.
+      CycleEliminator cycleEliminator =
+          new CycleEliminator(configuration.nodes, new InternalOptions());
+      int numberOfCycles = cycleEliminator.breakCycles();
+      if (numberOfCycles == 1) {
+        // If only one cycle was removed, then it must be the edge from n1 -> n2 that was removed.
+        assertTrue(n1.isLeaf());
+      } else {
+        // Check that the cycle eliminator found both cycles.
+        assertEquals(2, numberOfCycles);
+      }
+
+      // Check that the cycle eliminator agrees that there are no more cycles left.
+      assertEquals(0, cycleEliminator.breakCycles());
+
+      // Check that force inlining is guaranteed to succeed.
+      if (configuration.test != null) {
+        assertTrue(configuration.test.getAsBoolean());
+      }
+    }
+  }
+
+  private Node createNode(String methodName) {
+    DexMethod signature =
+        dexItemFactory.createMethod(
+            dexItemFactory.objectType,
+            dexItemFactory.createProto(dexItemFactory.voidType),
+            methodName);
+    return new Node(new DexEncodedMethod(signature, null, null, null, null));
+  }
+
+  private Node createForceInlinedNode(String methodName) {
+    Node node = createNode(methodName);
+    node.method.markForceInline();
+    return node;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java b/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
index c929eaf..878cb7c 100644
--- a/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
+++ b/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.DexInstructionSubject;
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
@@ -82,7 +83,8 @@
     assertTrue(insn.isInvoke());
     assertTrue(((InvokeInstructionSubject) insn)
         .invokedMethod().name.toString().equals("throwNpe"));
-    assertTrue(nextInstruction(instructions).isConst4());
+    insn = nextInstruction(instructions);
+    assertTrue(insn instanceof DexInstructionSubject && ((DexInstructionSubject) insn).isConst4());
     assertTrue(nextInstruction(instructions).isThrow());
     assertFalse(instructions.hasNext());
 
@@ -105,7 +107,8 @@
       assertTrue(((InvokeInstructionSubject) insn)
           .invokedMethod().name.toString().equals("throwNpe"));
     }
-    assertTrue(nextInstruction(instructions).isConst4());
+    insn = nextInstruction(instructions);
+    assertTrue(insn instanceof DexInstructionSubject && ((DexInstructionSubject) insn).isConst4());
     assertTrue(nextInstruction(instructions).isThrow());
     assertFalse(instructions.hasNext());
 
@@ -119,7 +122,8 @@
     assertTrue(insn.isInvoke());
     assertTrue(((InvokeInstructionSubject) insn)
         .invokedMethod().name.toString().equals("innerNotReachable"));
-    assertTrue(nextInstruction(instructions).isConst4());
+    insn = nextInstruction(instructions);
+    assertTrue(insn instanceof DexInstructionSubject && ((DexInstructionSubject) insn).isConst4());
     assertTrue(nextInstruction(instructions).isThrow());
     assertFalse(instructions.hasNext());
   }
diff --git a/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java b/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java
index 90fba5f..84fc5c4 100644
--- a/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java
+++ b/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java
@@ -91,6 +91,16 @@
     t = f;
   }
 
+  public void mfWithMonitor() {
+    t = f;
+    synchronized (this) {
+      t = f;
+      t = f;
+    }
+    t = f;
+    t = f;
+  }
+
   public void msf() {
     t = sf;
     t = sf;
@@ -231,6 +241,8 @@
     assertThat(classA, isPresent());
     MethodSubject mfOnA = classA.method("void", "mf", ImmutableList.of());
     assertThat(mfOnA, isPresent());
+    MethodSubject mfWithMonitorOnA = classA.method("void", "mfWithMonitor", ImmutableList.of());
+    assertThat(mfWithMonitorOnA, isPresent());
     MethodSubject msfOnA = classA.method("void", "msf", ImmutableList.of());
     assertThat(msfOnA, isPresent());
     MethodSubject mvOnA = classA.method("void", "mv", ImmutableList.of());
@@ -264,6 +276,13 @@
     // compilation (R8) will eliminate field loads on non-volatile fields.
     assertEquals(1, countIget(mfOnA.getMethod().getCode().asDexCode(), fOnA.getField().field));
     assertEquals(1, countSget(msfOnA.getMethod().getCode().asDexCode(), sfOnA.getField().field));
+    // TODO(111380066). This could be 2 in stead of 4, but right now the optimization tracks the
+    // combined set of fields for all successors, and for synchronized code all blocks have
+    // exceptional edges for ensuring monitor exit causing the active load to be invalidated for
+    // both normal and exceptional successors.
+    assertEquals(4,
+        countIget(mfWithMonitorOnA.getMethod().getCode().asDexCode(), fOnA.getField().field));
+
     // For fields on other class both separate compilation (D8) and whole program
     // compilation (R8) will differ in the eliminated field loads of non-volatile fields.
     assertEquals(mfOnBGets,
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/ChromuimAssertionHookMock.java b/src/test/java/com/android/tools/r8/rewrite/assertions/ChromuimAssertionHookMock.java
new file mode 100644
index 0000000..772ef09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/ChromuimAssertionHookMock.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2018, 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.rewrite.assertions;
+
+public class ChromuimAssertionHookMock {
+  public static void assertFailureHandler(AssertionError assertion) {
+    System.out.println("Got AssertionError " + assertion);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java b/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java
index cc56790..49fe85e 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java
@@ -16,7 +16,9 @@
   }
 
   int getX() {
+    System.out.println("1");
     assert condition();
+    System.out.println("2");
     return x;
   }
 
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java b/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
index 3c724c0..6574e7b 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
@@ -4,17 +4,139 @@
 
 package com.android.tools.r8.rewrite.assertions;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+import java.util.function.Function;
 import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+// This ASM class visitor has been adapted from
+// https://chromium.googlesource.com/chromium/src/+/164e81fcd0828b40f5496e9025349ea728cde7f5/build/android/bytecode/java/org/chromium/bytecode/AssertionEnablerClassAdapter.java
+// See b/110887293.
+
+/**
+ * An ClassVisitor for replacing Java ASSERT statements with a function by modifying Java bytecode.
+ *
+ * We do this in two steps, first step is to enable assert.
+ * Following bytecode is generated for each class with ASSERT statements:
+ * 0: ldc #8 // class CLASSNAME
+ * 2: invokevirtual #9 // Method java/lang/Class.desiredAssertionStatus:()Z
+ * 5: ifne 12
+ * 8: iconst_1
+ * 9: goto 13
+ * 12: iconst_0
+ * 13: putstatic #2 // Field $assertionsDisabled:Z
+ * Replaces line #13 to the following:
+ * 13: pop
+ * Consequently, $assertionsDisabled is assigned the default value FALSE.
+ * This is done in the first if statement in overridden visitFieldInsn. We do this per per-assert.
+ *
+ * Second step is to replace assert statement with a function:
+ * The followed instructions are generated by a java assert statement:
+ * getstatic     #3     // Field $assertionsDisabled:Z
+ * ifne          118    // Jump to instruction as if assertion if not enabled
+ * ...
+ * ifne          19
+ * new           #4     // class java/lang/AssertionError
+ * dup
+ * ldc           #5     // String (don't have this line if no assert message given)
+ * invokespecial #6     // Method java/lang/AssertionError.
+ * athrow
+ * Replace athrow with:
+ * invokestatic  #7     // Method org/chromium/base/JavaExceptionReporter.assertFailureHandler
+ * goto          118
+ * JavaExceptionReporter.assertFailureHandler is a function that handles the AssertionError,
+ * 118 is the instruction to execute as if assertion if not enabled.
+ */
+class AssertionEnablerClassAdapter extends ClassVisitor {
+  AssertionEnablerClassAdapter(ClassVisitor visitor) {
+    super(Opcodes.ASM6, visitor);
+  }
+
+  @Override
+  public MethodVisitor visitMethod(final int access, final String name, String desc,
+      String signature, String[] exceptions) {
+    return new RewriteAssertMethodVisitor(
+        Opcodes.ASM5, super.visitMethod(access, name, desc, signature, exceptions));
+  }
+
+  static class RewriteAssertMethodVisitor extends MethodVisitor {
+    static final String ASSERTION_DISABLED_NAME = "$assertionsDisabled";
+    static final String INSERT_INSTRUCTION_NAME = "assertFailureHandler";
+    static final String INSERT_INSTRUCTION_DESC =
+        Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType("java/lang/AssertionError"));
+    static final boolean INSERT_INSTRUCTION_ITF = false;
+
+    boolean mStartLoadingAssert;
+    Label mGotoLabel;
+
+    public RewriteAssertMethodVisitor(int api, MethodVisitor mv) {
+      super(api, mv);
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      if (opcode == Opcodes.PUTSTATIC && name.equals(ASSERTION_DISABLED_NAME)) {
+        super.visitInsn(Opcodes.POP); // enable assert
+      } else if (opcode == Opcodes.GETSTATIC && name.equals(ASSERTION_DISABLED_NAME)) {
+        mStartLoadingAssert = true;
+        super.visitFieldInsn(opcode, owner, name, desc);
+      } else {
+        super.visitFieldInsn(opcode, owner, name, desc);
+      }
+    }
+
+    @Override
+    public void visitJumpInsn(int opcode, Label label) {
+      if (mStartLoadingAssert && opcode == Opcodes.IFNE && mGotoLabel == null) {
+        mGotoLabel = label;
+      }
+      super.visitJumpInsn(opcode, label);
+    }
+
+    @Override
+    public void visitInsn(int opcode) {
+      if (!mStartLoadingAssert || opcode != Opcodes.ATHROW) {
+        super.visitInsn(opcode);
+      } else {
+        super.visitMethodInsn(
+            Opcodes.INVOKESTATIC,
+            ChromuimAssertionHookMock.class.getCanonicalName().replace('.', '/'),
+            INSERT_INSTRUCTION_NAME,
+            INSERT_INSTRUCTION_DESC,
+            INSERT_INSTRUCTION_ITF);
+        super.visitJumpInsn(Opcodes.GOTO, mGotoLabel);
+        mStartLoadingAssert = false;
+        mGotoLabel = null;
+      }
+    }
+  }
+}
 
 public class RemoveAssertionsTest extends TestBase {
 
@@ -38,4 +160,75 @@
         clazz.method(new MethodSignature(Constants.CLASS_INITIALIZER_NAME, "void", new String[]{}));
     assertTrue(!clinit.isPresent());
   }
+
+  private Path buildTestToCf(Consumer<InternalOptions> consumer) throws Exception {
+    Path outputJar = temp.getRoot().toPath().resolve("output.jar");
+    R8Command command =
+        ToolHelper.prepareR8CommandBuilder(readClasses(ClassWithAssertions.class))
+            .setMode(CompilationMode.DEBUG)
+            .setOutput(outputJar, OutputMode.ClassFile)
+            .build();
+    ToolHelper.runR8(command, consumer);
+    return outputJar;
+  }
+
+  @Test
+  public void testCfOutput() throws Exception {
+    String main = ClassWithAssertions.class.getCanonicalName();
+    ProcessResult result;
+    // Assertion is hit.
+    result = ToolHelper.runJava(buildTestToCf(options -> {}), "-ea", main, "0");
+    assertEquals(1, result.exitCode);
+    assertEquals("1\n".replace("\n", System.lineSeparator()), result.stdout);
+    // Assertion is not hit.
+    result = ToolHelper.runJava(buildTestToCf(options -> {}), "-ea", main, "1");
+    assertEquals(0, result.exitCode);
+    assertEquals("1\n2\n".replace("\n", System.lineSeparator()), result.stdout);
+    // Assertion is hit, but removed.
+    result = ToolHelper.runJava(
+        buildTestToCf(
+            options -> options.disableAssertions = true), "-ea", main, "0");
+    assertEquals(0, result.exitCode);
+    assertEquals("1\n2\n".replace("\n", System.lineSeparator()), result.stdout);
+  }
+
+  private byte[] identity(byte[] classBytes) {
+    return classBytes;
+  }
+
+  private byte[] chromiumAssertionEnabler(byte[] classBytes) {
+    ClassWriter writer = new ClassWriter(0);
+    new ClassReader(classBytes).accept(new AssertionEnablerClassAdapter(writer), 0);
+    return writer.toByteArray();
+  }
+
+  private AndroidApp runRegress110887293(Function<byte[], byte[]> rewriter) throws Exception {
+    return ToolHelper.runR8(
+        R8Command.builder()
+            .addClassProgramData(
+                rewriter.apply(ToolHelper.getClassAsBytes(ClassWithAssertions.class)),
+                Origin.unknown())
+            .addClassProgramData(
+                ToolHelper.getClassAsBytes(ChromuimAssertionHookMock.class), Origin.unknown())
+            .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+            .setMode(CompilationMode.DEBUG)
+            .build());
+  }
+
+  @Test
+  public void regress110887293() throws Exception {
+    AndroidApp app;
+    // Assertions removed for default assertion code.
+    app = runRegress110887293(this::identity);
+    assertEquals("1\n2\n", runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "0"));
+    assertEquals("1\n2\n", runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "1"));
+    // Assertions not removed when default assertion code is not present.
+    app = runRegress110887293(this::chromiumAssertionEnabler);
+    assertEquals(
+        "1\nGot AssertionError java.lang.AssertionError\n2\n".replace("\n", System.lineSeparator()),
+        runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "0"));
+    assertEquals(
+        "1\n2\n".replace("\n", System.lineSeparator()),
+        runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "1"));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index 955d1ee..059a24a 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -54,6 +54,7 @@
 import com.android.tools.r8.code.SputWide;
 import com.android.tools.r8.code.Throw;
 import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexAnnotationSet;
@@ -81,6 +82,7 @@
 import com.android.tools.r8.naming.signature.GenericSignatureAction;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
 import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.utils.DexInspector.FieldAccessInstructionSubject;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -106,8 +108,6 @@
   private final ClassNameMapper mapping;
   private final BiMap<String, String> originalToObfuscatedMapping;
 
-  private final InstructionSubjectFactory factory = new InstructionSubjectFactory();
-
   public static MethodSignature MAIN =
       new MethodSignature("main", "void", new String[]{"java.lang.String[]"});
 
@@ -304,18 +304,18 @@
     return obfuscatedType;
   }
 
-  public abstract class Subject {
+  public abstract static class Subject {
 
     public abstract boolean isPresent();
     public abstract boolean isRenamed();
   }
 
-  public abstract class AnnotationSubject extends Subject {
+  public abstract static class AnnotationSubject extends Subject {
 
     public abstract DexEncodedAnnotation getAnnotation();
   }
 
-  public class FoundAnnotationSubject extends AnnotationSubject {
+  public static class FoundAnnotationSubject extends AnnotationSubject {
 
     private final DexAnnotation annotation;
 
@@ -339,7 +339,7 @@
     }
   }
 
-  public class AbsentAnnotationSubject extends AnnotationSubject {
+  public static class AbsentAnnotationSubject extends AnnotationSubject {
 
     @Override
     public boolean isPresent() {
@@ -357,8 +357,7 @@
     }
   }
 
-
-  public abstract class ClassSubject extends Subject {
+  public abstract static class ClassSubject extends Subject {
 
     public abstract void forAllMethods(Consumer<FoundMethodSubject> inspection);
 
@@ -737,7 +736,7 @@
     }
   }
 
-  public abstract class MemberSubject extends Subject {
+  public abstract static class MemberSubject extends Subject {
 
     public abstract boolean isPublic();
 
@@ -760,7 +759,7 @@
     }
   }
 
-  public abstract class MethodSubject extends MemberSubject {
+  public abstract static class MethodSubject extends MemberSubject {
 
     public abstract boolean isAbstract();
 
@@ -970,7 +969,7 @@
 
     @Override
     public Iterator<InstructionSubject> iterateInstructions() {
-      return new InstructionIterator(this);
+      return createInstructionIterator(this);
     }
 
     @Override
@@ -985,7 +984,7 @@
     }
   }
 
-  public abstract class FieldSubject extends MemberSubject {
+  public abstract static class FieldSubject extends MemberSubject {
     public abstract boolean hasExplicitStaticValue();
 
     public abstract DexEncodedField getField();
@@ -999,7 +998,7 @@
     public abstract String getFinalSignatureAttribute();
   }
 
-  public class AbsentFieldSubject extends FieldSubject {
+  public static class AbsentFieldSubject extends FieldSubject {
 
     @Override
     public boolean isPublic() {
@@ -1192,91 +1191,139 @@
     }
   }
 
-  private class InstructionSubjectFactory {
+  public interface InstructionSubject {
+    boolean isFieldAccess();
 
-    InstructionSubject create(Instruction instruction) {
-      if (isInvoke(instruction)) {
-        return new InvokeInstructionSubject(this, instruction);
-      } else if (isFieldAccess(instruction)) {
-        return new FieldAccessInstructionSubject(this, instruction);
-      } else {
-        return new InstructionSubject(this, instruction);
-      }
+    boolean isInvokeVirtual();
+
+    boolean isInvokeInterface();
+
+    boolean isInvokeDirect();
+
+    boolean isInvokeSuper();
+
+    boolean isInvokeStatic();
+
+    boolean isNop();
+
+    boolean isConstString();
+
+    boolean isConstString(String value);
+
+    boolean isGoto();
+
+    boolean isIfNez();
+
+    boolean isIfEqz();
+
+    boolean isReturnVoid();
+
+    boolean isThrow();
+
+    default boolean isInvoke() {
+      return isInvokeVirtual()
+          || isInvokeInterface()
+          || isInvokeDirect()
+          || isInvokeSuper()
+          || isInvokeStatic();
+    }
+  }
+
+  public InstructionSubject createInstructionSubject(Instruction instruction) {
+    DexInstructionSubject dexInst = new DexInstructionSubject(instruction);
+    if (dexInst.isInvoke()) {
+      return new InvokeDexInstructionSubject(instruction);
+    } else if (dexInst.isFieldAccess()) {
+      return new FieldAccessDexInstructionSubject(instruction);
+    } else {
+      return dexInst;
+    }
+  }
+
+  public class DexInstructionSubject implements InstructionSubject {
+    protected Instruction instruction;
+
+    public DexInstructionSubject(Instruction instruction) {
+      this.instruction = instruction;
     }
 
-    boolean isInvoke(Instruction instruction) {
-      return isInvokeVirtual(instruction)
-          || isInvokeInterface(instruction)
-          || isInvokeDirect(instruction)
-          || isInvokeSuper(instruction)
-          || isInvokeStatic(instruction);
+    @Override
+    public boolean isFieldAccess() {
+      return isInstanceGet() || isInstancePut() || isStaticGet() || isStaticSet();
     }
 
-    boolean isInvokeVirtual(Instruction instruction) {
+    @Override
+    public boolean isInvokeVirtual() {
       return instruction instanceof InvokeVirtual || instruction instanceof InvokeVirtualRange;
     }
 
-    boolean isInvokeInterface(Instruction instruction) {
+    @Override
+    public boolean isInvokeInterface() {
       return instruction instanceof InvokeInterface || instruction instanceof InvokeInterfaceRange;
     }
 
-    boolean isInvokeDirect(Instruction instruction) {
+    @Override
+    public boolean isInvokeDirect() {
       return instruction instanceof InvokeDirect || instruction instanceof InvokeDirectRange;
     }
 
-    boolean isInvokeSuper(Instruction instruction) {
+    @Override
+    public boolean isInvokeSuper() {
       return instruction instanceof InvokeSuper || instruction instanceof InvokeSuperRange;
     }
 
-    boolean isInvokeStatic(Instruction instruction) {
+    @Override
+    public boolean isInvokeStatic() {
       return instruction instanceof InvokeStatic || instruction instanceof InvokeStaticRange;
     }
 
-    boolean isNop(Instruction instruction) {
+    @Override
+    public boolean isNop() {
       return instruction instanceof Nop;
     }
 
-    boolean isGoto(Instruction instruction) {
-      return instruction instanceof Goto;
-    }
-
-    boolean isReturnVoid(Instruction instruction) {
-      return instruction instanceof ReturnVoid;
-    }
-
-    boolean isConst4(Instruction instruction) {
-      return instruction instanceof Const4;
-    }
-
-    boolean isThrow(Instruction instruction) {
-      return instruction instanceof Throw;
-    }
-
-    boolean isConstString(Instruction instruction) {
+    @Override
+    public boolean isConstString() {
       return instruction instanceof ConstString;
     }
 
-    boolean isConstString(Instruction instruction, String value) {
+    @Override
+    public boolean isConstString(String value) {
       return instruction instanceof ConstString
           && ((ConstString) instruction).BBBB.toSourceString().equals(value);
     }
 
-    boolean isIfNez(Instruction instruction) {
+    @Override
+    public boolean isGoto() {
+
+      return instruction instanceof Goto;
+    }
+
+    @Override
+    public boolean isIfNez() {
       return instruction instanceof IfNez;
     }
 
-    boolean isIfEqz(Instruction instruction) {
+    @Override
+    public boolean isIfEqz() {
       return instruction instanceof IfEqz;
     }
 
-    boolean isFieldAccess(Instruction instruction) {
-      return isInstanceGet(instruction)
-          || isInstancePut(instruction)
-          || isStaticGet(instruction)
-          || isStaticSet(instruction);
+    @Override
+    public boolean isReturnVoid() {
+      return instruction instanceof ReturnVoid;
     }
 
-    boolean isInstanceGet(Instruction instruction) {
+    @Override
+    public boolean isThrow() {
+      return instruction instanceof Throw;
+    }
+
+    public boolean isConst4() {
+      return instruction instanceof Const4;
+    }
+
+    public boolean isInstanceGet() {
       return instruction instanceof Iget
           || instruction instanceof IgetBoolean
           || instruction instanceof IgetByte
@@ -1286,7 +1333,7 @@
           || instruction instanceof IgetObject;
     }
 
-    boolean isInstancePut(Instruction instruction) {
+    public boolean isInstancePut() {
       return instruction instanceof Iput
           || instruction instanceof IputBoolean
           || instruction instanceof IputByte
@@ -1296,7 +1343,7 @@
           || instruction instanceof IputObject;
     }
 
-    boolean isStaticGet(Instruction instruction) {
+    public boolean isStaticGet() {
       return instruction instanceof Sget
           || instruction instanceof SgetBoolean
           || instruction instanceof SgetByte
@@ -1306,7 +1353,7 @@
           || instruction instanceof SgetObject;
     }
 
-    boolean isStaticSet(Instruction instruction) {
+    public boolean isStaticSet() {
       return instruction instanceof Sput
           || instruction instanceof SputBoolean
           || instruction instanceof SputByte
@@ -1317,89 +1364,21 @@
     }
   }
 
-  public class InstructionSubject {
+  public interface InvokeInstructionSubject extends InstructionSubject {
+    TypeSubject holder();
 
-    protected final InstructionSubjectFactory factory;
-    protected final Instruction instruction;
-
-    protected InstructionSubject(InstructionSubjectFactory factory, Instruction instruction) {
-      this.factory = factory;
-      this.instruction = instruction;
-    }
-
-    public boolean isInvoke() {
-      return factory.isInvoke(instruction);
-    }
-
-    public boolean isFieldAccess() {
-      return factory.isFieldAccess(instruction);
-    }
-
-    public boolean isInvokeVirtual() {
-      return factory.isInvokeVirtual(instruction);
-    }
-
-    public boolean isInvokeInterface() {
-      return factory.isInvokeInterface(instruction);
-    }
-
-    public boolean isInvokeDirect() {
-      return factory.isInvokeDirect(instruction);
-    }
-
-    public boolean isInvokeSuper() {
-      return factory.isInvokeSuper(instruction);
-    }
-
-    public boolean isInvokeStatic() {
-      return factory.isInvokeStatic(instruction);
-    }
-
-    boolean isFieldAccess(Instruction instruction) {
-      return factory.isFieldAccess(instruction);
-    }
-
-    public boolean isNop() {
-      return factory.isNop(instruction);
-    }
-
-    public boolean isConstString() {
-      return factory.isConstString(instruction);
-    }
-
-    public boolean isConstString(String value) {
-      return factory.isConstString(instruction, value);
-    }
-
-    public boolean isGoto() {
-      return factory.isGoto(instruction);
-    }
-
-    public boolean isIfNez() {
-      return factory.isIfNez(instruction);
-    }
-
-    public boolean isIfEqz() {
-      return factory.isIfEqz(instruction);
-    }
-
-    public boolean isReturnVoid() {
-      return factory.isReturnVoid(instruction);
-    }
-
-    public boolean isConst4() {
-      return factory.isConst4(instruction);
-    }
-
-    public boolean isThrow() {
-      return factory.isThrow(instruction);
-    }
+    DexMethod invokedMethod();
   }
 
-  public class InvokeInstructionSubject extends InstructionSubject {
+  public interface FieldAccessInstructionSubject extends InstructionSubject {
+    TypeSubject holder();
+  }
 
-    InvokeInstructionSubject(InstructionSubjectFactory factory, Instruction instruction) {
-      super(factory, instruction);
+  public class InvokeDexInstructionSubject extends DexInstructionSubject
+      implements InvokeInstructionSubject {
+
+    public InvokeDexInstructionSubject(Instruction instruction) {
+      super(instruction);
       assert isInvoke();
     }
 
@@ -1408,156 +1387,41 @@
     }
 
     public DexMethod invokedMethod() {
-      if (instruction instanceof InvokeVirtual) {
-        return ((InvokeVirtual) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeVirtualRange) {
-        return ((InvokeVirtualRange) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeInterface) {
-        return ((InvokeInterface) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeInterfaceRange) {
-        return ((InvokeInterfaceRange) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeDirect) {
-        return ((InvokeDirect) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeDirectRange) {
-        return ((InvokeDirectRange) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeSuper) {
-        return ((InvokeSuper) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeSuperRange) {
-        return ((InvokeSuperRange) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeDirect) {
-        return ((InvokeDirect) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeDirectRange) {
-        return ((InvokeDirectRange) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeStatic) {
-        return ((InvokeStatic) instruction).getMethod();
-      }
-      if (instruction instanceof InvokeStaticRange) {
-        return ((InvokeStaticRange) instruction).getMethod();
-      }
-      assert false;
-      return null;
+      return instruction.getMethod();
     }
   }
 
-  public class FieldAccessInstructionSubject extends InstructionSubject {
+  public class FieldAccessDexInstructionSubject extends DexInstructionSubject
+      implements FieldAccessInstructionSubject {
 
-    FieldAccessInstructionSubject(InstructionSubjectFactory factory, Instruction instruction) {
-      super(factory, instruction);
+    public FieldAccessDexInstructionSubject(Instruction instruction) {
+      super(instruction);
       assert isFieldAccess();
     }
 
     public TypeSubject holder() {
-      return new TypeSubject(accessedField().getHolder());
-    }
-
-    public DexField accessedField() {
-      if (instruction instanceof Iget) {
-        return ((Iget) instruction).getField();
-      }
-      if (instruction instanceof IgetBoolean) {
-        return ((IgetBoolean) instruction).getField();
-      }
-      if (instruction instanceof IgetByte) {
-        return ((IgetByte) instruction).getField();
-      }
-      if (instruction instanceof IgetShort) {
-        return ((IgetShort) instruction).getField();
-      }
-      if (instruction instanceof IgetChar) {
-        return ((IgetChar) instruction).getField();
-      }
-      if (instruction instanceof IgetWide) {
-        return ((IgetWide) instruction).getField();
-      }
-      if (instruction instanceof IgetObject) {
-        return ((IgetObject) instruction).getField();
-      }
-      if (instruction instanceof Iput) {
-        return ((Iput) instruction).getField();
-      }
-      if (instruction instanceof IputBoolean) {
-        return ((IputBoolean) instruction).getField();
-      }
-      if (instruction instanceof IputByte) {
-        return ((IputByte) instruction).getField();
-      }
-      if (instruction instanceof IputShort) {
-        return ((IputShort) instruction).getField();
-      }
-      if (instruction instanceof IputChar) {
-        return ((IputChar) instruction).getField();
-      }
-      if (instruction instanceof IputWide) {
-        return ((IputWide) instruction).getField();
-      }
-      if (instruction instanceof IputObject) {
-        return ((IputObject) instruction).getField();
-      }
-      if (instruction instanceof Sget) {
-        return ((Sget) instruction).getField();
-      }
-      if (instruction instanceof SgetBoolean) {
-        return ((SgetBoolean) instruction).getField();
-      }
-      if (instruction instanceof SgetByte) {
-        return ((SgetByte) instruction).getField();
-      }
-      if (instruction instanceof SgetShort) {
-        return ((SgetShort) instruction).getField();
-      }
-      if (instruction instanceof SgetChar) {
-        return ((SgetChar) instruction).getField();
-      }
-      if (instruction instanceof SgetWide) {
-        return ((SgetWide) instruction).getField();
-      }
-      if (instruction instanceof SgetObject) {
-        return ((SgetObject) instruction).getField();
-      }
-      if (instruction instanceof Sput) {
-        return ((Sput) instruction).getField();
-      }
-      if (instruction instanceof SputBoolean) {
-        return ((SputBoolean) instruction).getField();
-      }
-      if (instruction instanceof SputByte) {
-        return ((SputByte) instruction).getField();
-      }
-      if (instruction instanceof SputShort) {
-        return ((SputShort) instruction).getField();
-      }
-      if (instruction instanceof SputChar) {
-        return ((SputChar) instruction).getField();
-      }
-      if (instruction instanceof SputWide) {
-        return ((SputWide) instruction).getField();
-      }
-      if (instruction instanceof SputObject) {
-        return ((SputObject) instruction).getField();
-      }
-      assert false;
-      return null;
+      return new TypeSubject(instruction.getField().getHolder());
     }
   }
 
-  private class InstructionIterator implements Iterator<InstructionSubject> {
+  private interface InstructionIterator extends Iterator<InstructionSubject> {}
+
+  private InstructionIterator createInstructionIterator(MethodSubject method) {
+    Code code = method.getMethod().getCode();
+    assert code != null && code.isDexCode();
+    return new DexInstructionIterator(method);
+  }
+
+  private class DexInstructionIterator implements InstructionIterator {
 
     private final DexCode code;
     private int index;
 
-    InstructionIterator(MethodSubject method) {
+    DexInstructionIterator(MethodSubject method) {
       assert method.isPresent();
-      this.code = method.getMethod().getCode().asDexCode();
+      Code code = method.getMethod().getCode();
+      assert code != null && code.isDexCode();
+      this.code = code.asDexCode();
       this.index = 0;
     }
 
@@ -1571,7 +1435,7 @@
       if (index == code.instructions.length) {
         throw new NoSuchElementException();
       }
-      return factory.create(code.instructions[index++]);
+      return createInstructionSubject(code.instructions[index++]);
     }
   }
 
@@ -1582,7 +1446,7 @@
     private InstructionSubject pendingNext = null;
 
     FilteredInstructionIterator(MethodSubject method, Predicate<InstructionSubject> predicate) {
-      this.iterator = new InstructionIterator(method);
+      this.iterator = createInstructionIterator(method);
       this.predicate = predicate;
       hasNext();
     }