Merge "Apply graph lense to DexValueMethodType and DexValueType in LensCodeRewriter"
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 8ab2c1a..5bed061 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -6,7 +6,6 @@
 import static com.android.tools.r8.ir.code.IRCode.INSTRUCTION_NUMBER_DELTA;
 
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DebugLocalInfo.PrintLevel;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -468,7 +467,7 @@
         return instruction;
       }
     }
-    throw new Unreachable();
+    return null;
   }
 
   public void clearUserInfo() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java b/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java
new file mode 100644
index 0000000..8db07f6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/CanonicalPositions.java
@@ -0,0 +1,75 @@
+// 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.code;
+
+import com.android.tools.r8.graph.DexMethod;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Maintains a set of canonical positions. Also supports appending a new caller at the end of the
+ * caller chain of a Position.
+ */
+public class CanonicalPositions {
+  private final Position callerPosition;
+  private final boolean preserveCaller;
+  private final Map<Position, Position> canonicalPositions;
+  private final Position preamblePosition;
+
+  /**
+   * For callerPosition and preserveCaller see canonicalizeCallerPosition. initialCapacity will be
+   * passed to the HashMap constructor.
+   */
+  public CanonicalPositions(
+      Position callerPosition, boolean preserveCaller, int initialCapacity, DexMethod method) {
+    canonicalPositions = new HashMap<>(initialCapacity);
+    this.preserveCaller = preserveCaller;
+    this.callerPosition = callerPosition;
+    if (callerPosition != null) {
+      canonicalPositions.put(callerPosition, callerPosition);
+    }
+    preamblePosition =
+        callerPosition == null
+            ? Position.synthetic(0, method, null)
+            : new Position(0, null, method, callerPosition);
+    canonicalPositions.put(preamblePosition, preamblePosition);
+  }
+
+  public Position getPreamblePosition() {
+    return preamblePosition;
+  }
+
+  /**
+   * Update the internal set if this is the first occurence of the position's value and return
+   * canonical instance of position.
+   */
+  public Position getCanonical(Position position) {
+    Position canonical = canonicalPositions.putIfAbsent(position, position);
+    return canonical != null ? canonical : position;
+  }
+
+  /**
+   * Append callerPosition (supplied in constructor) to the end of caller's caller chain and return
+   * the canonical instance. Always returns null if preserveCaller (also supplied in constructor) is
+   * false.
+   */
+  public Position canonicalizeCallerPosition(Position caller) {
+    if (!preserveCaller) {
+      return null;
+    }
+
+    if (caller == null) {
+      return callerPosition;
+    }
+    if (caller.callerPosition == null && callerPosition == null) {
+      return getCanonical(caller);
+    }
+    Position callerOfCaller = canonicalizeCallerPosition(caller.callerPosition);
+    return getCanonical(
+        caller.isNone()
+            ? Position.noneWithMethod(caller.method, callerOfCaller)
+            : new Position(caller.line, caller.file, caller.method, callerOfCaller));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index b59794b..e0c47da 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -40,6 +40,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.ir.code.CanonicalPositions;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.ValueType;
@@ -68,25 +69,20 @@
   private Instruction currentDexInstruction = null;
 
   private Position currentPosition = null;
-  private Map<Position, Position> canonicalPositions = null;
+  private final CanonicalPositions canonicalPositions;
 
   private final List<ValueType> argumentTypes;
 
   private List<DexDebugEntry> debugEntries = null;
   // In case of inlining the position of the invoke in the caller.
-  private final Position callerPosition;
   private final DexMethod method;
-  private final boolean preserveCaller;
-  private final Position preamblePosition;
 
   public DexSourceCode(
       DexCode code, DexEncodedMethod method, Position callerPosition, boolean preserveCaller) {
     this.code = code;
     this.proto = method.method.proto;
     this.accessFlags = method.accessFlags;
-    this.callerPosition = callerPosition;
     this.method = method.method;
-    this.preserveCaller = preserveCaller;
 
     argumentTypes = computeArgumentTypes();
     DexDebugInfo info = code.getDebugInfo();
@@ -94,18 +90,11 @@
       debugEntries = info.computeEntries(method.method);
     }
     canonicalPositions =
-        new HashMap<>(
-            1
-                + (callerPosition == null ? 0 : 1)
-                + (debugEntries == null ? 0 : debugEntries.size()));
-    if (callerPosition != null) {
-      canonicalPositions.put(callerPosition, callerPosition);
-    }
-    preamblePosition =
-        callerPosition == null
-            ? Position.synthetic(0, this.method, null)
-            : new Position(0, null, this.method, callerPosition);
-    canonicalPositions.put(preamblePosition, preamblePosition);
+        new CanonicalPositions(
+            callerPosition,
+            preserveCaller,
+            1 + (callerPosition == null ? 0 : 1) + (debugEntries == null ? 0 : debugEntries.size()),
+            this.method);
   }
 
   @Override
@@ -148,7 +137,7 @@
 
   @Override
   public void buildPrelude(IRBuilder builder) {
-    currentPosition = preamblePosition;
+    currentPosition = canonicalPositions.getPreamblePosition();
     if (code.incomingRegisterSize == 0) {
       return;
     }
@@ -197,7 +186,9 @@
   @Override
   public Position getDebugPositionAtOffset(int offset) {
     DexDebugEntry entry = getDebugEntryAtOffset(offset);
-    return entry == null ? preamblePosition : getCanonicalPositionAppendCaller(entry);
+    return entry == null
+        ? canonicalPositions.getPreamblePosition()
+        : getCanonicalPositionAppendCaller(entry);
   }
 
   @Override
@@ -250,7 +241,7 @@
     int offset = instructionOffset(instructionIndex);
     DexDebugEntry entry = getDebugEntryAtOffset(offset);
     if (entry == null) {
-      currentPosition = preamblePosition;
+      currentPosition = canonicalPositions.getPreamblePosition();
     } else {
       currentPosition = getCanonicalPositionAppendCaller(entry);
       if (entry.lineEntry && entry.address == offset) {
@@ -259,39 +250,17 @@
     }
   }
 
-  private Position getCanonicalPosition(Position position) {
-    Position canonical = canonicalPositions.putIfAbsent(position, position);
-    return canonical != null ? canonical : position;
-  }
-
-  private Position canonicalizeCallerPosition(Position caller) {
-    // We are not supposed to get here from getCanonicalPositionAppendCaller if !preserveCaller.
-    assert preserveCaller;
-
-    if (caller == null) {
-      return callerPosition;
-    }
-    if (caller.callerPosition == null && callerPosition == null) {
-      return getCanonicalPosition(caller);
-    }
-    Position callerOfCaller = canonicalizeCallerPosition(caller.callerPosition);
-    return getCanonicalPosition(
-        caller.isNone()
-            ? Position.noneWithMethod(caller.method, callerOfCaller)
-            : new Position(caller.line, caller.file, caller.method, callerOfCaller));
-  }
-
   private Position getCanonicalPositionAppendCaller(DexDebugEntry entry) {
     // If this instruction has already been inlined then this.method must be the outermost caller.
     assert entry.callerPosition == null
         || entry.callerPosition.getOutermostCaller().method == method;
 
-    return getCanonicalPosition(
+    return canonicalPositions.getCanonical(
         new Position(
             entry.line,
             entry.sourceFile,
             entry.method,
-            preserveCaller ? canonicalizeCallerPosition(entry.callerPosition) : null));
+            canonicalPositions.canonicalizeCallerPosition(entry.callerPosition)));
   }
 
   @Override
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 63843ee..d5f923e 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
@@ -46,6 +46,7 @@
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.RedundantFieldLoadElimination;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
@@ -104,6 +105,8 @@
   private final Devirtualizer devirtualizer;
   private final CovariantReturnTypeAnnotationTransformer covariantReturnTypeAnnotationTransformer;
 
+  private final boolean enableWholeProgramOptimizations;
+
   private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
   private DexString highestSortingString;
 
@@ -134,6 +137,7 @@
         options.processCovariantReturnTypeAnnotations
             ? new CovariantReturnTypeAnnotationTransformer(this, appInfo.dexItemFactory)
             : null;
+    this.enableWholeProgramOptimizations = enableWholeProgramOptimizations;
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasLiveness();
       this.nonNullTracker = new NonNullTracker();
@@ -695,6 +699,7 @@
     codeRewriter.rewriteSwitch(code);
     codeRewriter.processMethodsNeverReturningNormally(code);
     codeRewriter.simplifyIf(code, typeEnvironment);
+    new RedundantFieldLoadElimination(appInfo, code, enableWholeProgramOptimizations).run();
 
     if (options.testing.invertConditionals) {
       invertConditionalsForTesting(code);
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
new file mode 100644
index 0000000..dee935e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -0,0 +1,222 @@
+// 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.optimize;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Eliminate redundant field loads.
+ *
+ * <p>Simple algorithm that goes through all blocks in one pass in dominator order and propagates
+ * active field sets across control-flow edges where the target has only one predecessor.
+ */
+// TODO(ager): Evaluate speed/size for computing active field sets in a fixed-point computation.
+public class RedundantFieldLoadElimination {
+
+  private final AppInfo appInfo;
+  private final DexEncodedMethod method;
+  private final IRCode code;
+  private final boolean enableWholeProgramOptimizations;
+  private final DominatorTree dominatorTree;
+
+  // Maps keeping track of fields that have an already loaded value at basic block entry.
+  private final HashMap<BasicBlock, HashMap<FieldAndObject, Instruction>>
+      activeInstanceFieldsAtEntry = new HashMap<>();
+  private final HashMap<BasicBlock, HashMap<DexField, Instruction>> activeStaticFieldsAtEntry =
+      new HashMap<>();
+
+  // Maps keeping track of fields with already loaded values for the current block during
+  // elimination.
+  private HashMap<FieldAndObject, Instruction> activeInstanceFields;
+  private HashMap<DexField, Instruction> activeStaticFields;
+
+  public RedundantFieldLoadElimination(
+      AppInfo appInfo, IRCode code, boolean enableWholeProgramOptimizations) {
+    this.appInfo = appInfo;
+    this.method = code.method;
+    this.code = code;
+    this.enableWholeProgramOptimizations = enableWholeProgramOptimizations;
+    dominatorTree = new DominatorTree(code);
+  }
+
+  private static class FieldAndObject {
+    private final DexField field;
+    private final Value object;
+
+    private FieldAndObject(DexField field, Value receiver) {
+      this.field = field;
+      this.object = receiver;
+    }
+
+    @Override
+    public int hashCode() {
+      return field.hashCode() * 7 + object.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof FieldAndObject)) {
+        return false;
+      }
+      FieldAndObject o = (FieldAndObject) other;
+      return o.object == object && o.field == field;
+    }
+  }
+
+  private boolean couldBeVolatile(DexField field) {
+    if (!enableWholeProgramOptimizations && field.getHolder() != method.method.getHolder()) {
+      return true;
+    }
+    DexEncodedField definition = appInfo.definitionFor(field);
+    return definition == null || definition.accessFlags.isVolatile();
+  }
+
+  public void run() {
+    for (BasicBlock block : dominatorTree.getSortedBlocks()) {
+      activeInstanceFields =
+          activeInstanceFieldsAtEntry.containsKey(block)
+              ? activeInstanceFieldsAtEntry.get(block)
+              : new HashMap<>();
+      activeStaticFields =
+          activeStaticFieldsAtEntry.containsKey(block)
+              ? activeStaticFieldsAtEntry.get(block)
+              : new HashMap<>();
+      InstructionListIterator it = block.listIterator();
+      while (it.hasNext()) {
+        Instruction instruction = it.next();
+        if (instruction.isFieldInstruction()) {
+          DexField field = instruction.asFieldInstruction().getField();
+          if (instruction.isInstancePut() || instruction.isStaticPut()) {
+            killActiveFields(instruction.asFieldInstruction());
+          } else if (couldBeVolatile(field)) {
+            assert instruction.isInstanceGet() || instruction.isStaticGet();
+            killAllActiveFields();
+          } else {
+            assert instruction.isInstanceGet() || instruction.isStaticGet();
+            assert !couldBeVolatile(field);
+            if (instruction.isInstanceGet() && !instruction.outValue().hasLocalInfo()) {
+              Value object = instruction.asInstanceGet().object();
+              FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+              if (activeInstanceFields.containsKey(fieldAndObject)) {
+                Instruction active = activeInstanceFields.get(fieldAndObject);
+                eliminateRedundantRead(it, instruction, active);
+              } else {
+                activeInstanceFields.put(fieldAndObject, instruction);
+              }
+            } else if (instruction.isStaticGet() && !instruction.outValue().hasLocalInfo()) {
+              if (activeStaticFields.containsKey(field)) {
+                Instruction active = activeStaticFields.get(field);
+                eliminateRedundantRead(it, instruction, active);
+              } else {
+                // A field get on a different class can cause <clinit> to run and change static
+                // field values.
+                killActiveFields(instruction.asFieldInstruction());
+                activeStaticFields.put(field, instruction);
+              }
+            }
+          }
+        }
+        if (instruction.isMonitor() || instruction.isInvokeMethod()) {
+          killAllActiveFields();
+        }
+      }
+      propagateActiveFieldsFrom(block);
+    }
+    assert code.isConsistentSSA();
+  }
+
+  private void propagateActiveFieldsFrom(BasicBlock block) {
+    for (BasicBlock successor : block.getSuccessors()) {
+      // Allow propagation across exceptional edges, just be careful not to propagate if the
+      // throwing instruction is a field instruction.
+      if (successor.getPredecessors().size() == 1) {
+        if (block.hasCatchSuccessor(successor)) {
+          Instruction exceptionalExit = block.exceptionalExit();
+          if (exceptionalExit != null && exceptionalExit.isFieldInstruction()) {
+            killActiveFieldsForExceptionalExit(exceptionalExit.asFieldInstruction());
+          }
+        }
+        assert !activeInstanceFieldsAtEntry.containsKey(successor);
+        activeInstanceFieldsAtEntry.put(successor, new HashMap<>(activeInstanceFields));
+        assert !activeStaticFieldsAtEntry.containsKey(successor);
+        activeStaticFieldsAtEntry.put(successor, new HashMap<>(activeStaticFields));
+      }
+    }
+  }
+
+  private void killAllActiveFields() {
+    activeInstanceFields.clear();
+    activeStaticFields.clear();
+  }
+
+  private void killActiveFields(FieldInstruction instruction) {
+    DexField field = instruction.getField();
+    if (instruction.isInstancePut()) {
+      // Remove all the field/object pairs that refer to this field to make sure
+      // that we are conservative.
+      List<FieldAndObject> keysToRemove = new ArrayList<>();
+      for (FieldAndObject key : activeInstanceFields.keySet()) {
+        if (key.field == field) {
+          keysToRemove.add(key);
+        }
+      }
+      keysToRemove.forEach((k) -> activeInstanceFields.remove(k));
+    } else if (instruction.isInstanceGet()) {
+      Value object = instruction.asInstanceGet().object();
+      FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+      activeInstanceFields.remove(fieldAndObject);
+    } else if (instruction.isStaticPut()) {
+      if (field.clazz != code.method.method.holder) {
+        // Accessing a static field on a different object could cause <clinit> to run which
+        // could modify any static field on any other object.
+        activeStaticFields.clear();
+      } else {
+        activeStaticFields.remove(field);
+      }
+    } else if (instruction.isStaticGet()) {
+      if (field.clazz != code.method.method.holder) {
+        // Accessing a static field on a different object could cause <clinit> to run which
+        // could modify any static field on any other object.
+        activeStaticFields.clear();
+      }
+    }
+  }
+
+  // If a field get instruction throws an exception it did not have an effect on the
+  // value of the field. Therefore, when propagating across exceptional edges for a
+  // field get instruction we have to exclude that field from the set of known
+  // field values.
+  private void killActiveFieldsForExceptionalExit(FieldInstruction instruction) {
+    DexField field = instruction.getField();
+    if (instruction.isInstanceGet()) {
+      Value object = instruction.asInstanceGet().object();
+      FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+      activeInstanceFields.remove(fieldAndObject);
+    } else if (instruction.isStaticGet()) {
+      activeStaticFields.remove(field);
+    }
+  }
+
+  private void eliminateRedundantRead(
+      InstructionListIterator it, Instruction redundant, Instruction active) {
+    redundant.outValue().replaceUsers(active.outValue());
+    it.removeOrReplaceByDebugLocalRead();
+    active.outValue().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+  }
+}
diff --git a/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
new file mode 100644
index 0000000..781d2b4
--- /dev/null
+++ b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
@@ -0,0 +1,57 @@
+// 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 uninitializedfinal;
+
+// Test that leaks an instance before its final field has been initialized to a thread that
+// reads that field. This tests that redundant field load elimination does not eliminate
+// field reads (even of final fields) that cross a monitor operation.
+public class UninitializedFinalFieldLeak {
+
+  public static class PollingThread extends Thread {
+    public int result = 0;
+    UninitializedFinalFieldLeak f;
+
+    PollingThread(UninitializedFinalFieldLeak f) {
+      this.f = f;
+    }
+
+    // Read the field a number of times. Then lock on the object to await field initialization.
+    public void run() {
+      result += f.i;
+      result += f.i;
+      result += f.i;
+      f.threadReadsDone = true;
+      synchronized (f) {
+        result += f.i;
+      }
+      // The right result is 42. Reading the uninitialized 0 three times and then
+      // reading the initialized value. It is safe to remove the two redundant loads
+      // before the monitor operation.
+      System.out.println(result);
+    }
+  }
+
+  public final int i;
+  public volatile boolean threadReadsDone = false;
+
+  public UninitializedFinalFieldLeak() throws InterruptedException {
+    // Leak the object to a thread and start the thread with the lock on the object taken.
+    // Then allow the other thread to run and read the uninitialized field.
+    // Finally, initialize the field and release the lock.
+    PollingThread t = new PollingThread(this);
+    synchronized (this) {
+      t.start();
+      while (!threadReadsDone) {
+        Thread.yield();
+      }
+      i = 42;
+    }
+    t.join();
+  }
+
+  public static void main(String[] args) throws InterruptedException {
+    new UninitializedFinalFieldLeak();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 36f7594..221d427 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -45,9 +45,11 @@
         "instancevariable.InstanceVariable",
         "instanceofstring.InstanceofString",
         "invoke.Invoke",
+        "invokeempty.InvokeEmpty",
         "jumbostring.JumboString",
         "loadconst.LoadConst",
         "loop.UdpServer",
+        "nestedtrycatches.NestedTryCatches",
         "newarray.NewArray",
         "regalloc.RegAlloc",
         "returns.Returns",
@@ -58,9 +60,7 @@
         "throwing.Throwing",
         "trivial.Trivial",
         "trycatch.TryCatch",
-        "nestedtrycatches.NestedTryCatches",
         "trycatchmany.TryCatchMany",
-        "invokeempty.InvokeEmpty",
         "regress.Regress",
         "regress2.Regress2",
         "regress_37726195.Regress",
@@ -82,6 +82,7 @@
         "enclosingmethod_proguarded.Main",
         "interfaceinlining.Main",
         "switchmaps.Switches",
+        "uninitializedfinal.UninitializedFinalFieldLeak",
     };
 
     List<String[]> fullTestList = new ArrayList<>(tests.length * 2);
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 7b4f199..8adddfd 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -166,6 +166,17 @@
   }
 
   /**
+   * Copy test classes to the specified directory.
+   */
+  protected void copyTestClasses(Path dest, Class... classes) throws IOException {
+    for (Class clazz : classes) {
+      Path path = dest.resolve(clazz.getCanonicalName().replace('.', '/') + ".class");
+      Files.createDirectories(path.getParent());
+      Files.copy(ToolHelper.getClassFileForTestClass(clazz), path);
+    }
+  }
+
+  /**
    * Create a temporary JAR file containing the specified test classes.
    */
   protected Path jarTestClasses(Class... classes) throws IOException {
@@ -518,7 +529,11 @@
   }
 
   protected ProcessResult runOnJavaRaw(String main, byte[]... classes) throws IOException {
-    Path file = writeToZip(Arrays.asList(classes));
+    return runOnJavaRaw(main, Arrays.asList(classes));
+  }
+
+  protected ProcessResult runOnJavaRaw(String main, List<byte[]> classes) throws IOException {
+    Path file = writeToZip(classes);
     return ToolHelper.runJavaNoVerify(file, main);
   }
 
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 200a1e7..2667ce2 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -745,10 +745,6 @@
     return parts;
   }
 
-  public static Path getPackageDirectoryForTestClass(Class clazz) {
-    return getPackageDirectoryForTestPackage(clazz.getPackage());
-  }
-
   public static List<Path> getClassFilesForTestPackage(Package pkg) throws IOException {
     Path dir = ToolHelper.getPackageDirectoryForTestPackage(pkg);
     return Files.walk(dir)
@@ -762,6 +758,11 @@
         Paths.get("", parts.toArray(new String[parts.size() - 1])));
   }
 
+  public static Path getFileNameForTestClass(Class clazz) {
+    List<String> parts = getNamePartsForTestClass(clazz);
+    return Paths.get("", parts.toArray(new String[parts.size() - 1]));
+  }
+
   public static String getJarEntryForTestClass(Class clazz) {
     List<String> parts = getNamePartsForTestClass(clazz);
     return String.join("/", parts);
diff --git a/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTest.java b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTest.java
new file mode 100644
index 0000000..68277dd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTest.java
@@ -0,0 +1,41 @@
+// 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.debug;
+
+public class ArrayDimensionGreaterThanSevenTest {
+
+  public static float foo(int x) {
+    try {
+      float[] fs1 = new float[] {42f};
+      float[][] fs2 = new float[][] {fs1};
+      float[][][] fs3 = new float[][][] {fs2};
+      float[][][][] fs4 = new float[][][][] {fs3};
+      float[][][][][] fs5 = new float[][][][][] {fs4};
+      float[][][][][][] fs6 = new float[][][][][][] {fs5};
+      float[][][][][][][] fs7 = new float[][][][][][][] {fs6};
+      float[][][][][][][][] fs8 = new float[][][][][][][][] {fs7};
+      while (x-- > 0) {
+        try {
+          fs8 = x == 0 ? fs8 : null;
+          fs7 = x == 1 ? fs8[1] : fs8[0];
+          fs6 = x == 2 ? fs7[1] : fs7[0];
+          fs5 = x == 3 ? fs6[1] : fs6[0];
+          fs4 = x == 4 ? fs5[1] : fs5[0];
+          fs3 = x == 5 ? fs4[1] : fs4[0];
+          fs2 = x == 6 ? fs3[1] : fs3[0];
+          fs1 = x == 7 ? fs2[1] : fs2[0];
+        } catch (NullPointerException e) {
+          System.out.println("null pointer");
+        }
+      }
+    } catch (RuntimeException e) {
+      return -1f;
+    }
+    return 42;
+  }
+
+  public static void main(String[] args) {
+    System.out.println(foo(args.length + 1));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestDump.java b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestDump.java
new file mode 100644
index 0000000..5cd652d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestDump.java
@@ -0,0 +1,590 @@
+package com.android.tools.r8.debug;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class ArrayDimensionGreaterThanSevenTestDump implements Opcodes {
+
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(
+        V1_8,
+        ACC_PUBLIC + ACC_SUPER,
+        "com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTest",
+        null,
+        "java/lang/Object",
+        null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "foo", "(I)F", null, null);
+      mv.visitCode();
+      Label l0 = new Label();
+      Label l1 = new Label();
+      Label l2 = new Label();
+      mv.visitTryCatchBlock(l0, l1, l2, "java/lang/NullPointerException");
+      Label l3 = new Label();
+      Label l4 = new Label();
+      Label l5 = new Label();
+      mv.visitTryCatchBlock(l3, l4, l5, "java/lang/RuntimeException");
+      mv.visitLabel(l3);
+      mv.visitInsn(ICONST_1);
+      mv.visitIntInsn(NEWARRAY, T_FLOAT);
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitLdcInsn(new Float("42.0"));
+      mv.visitInsn(FASTORE);
+      mv.visitVarInsn(ASTORE, 1);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 2);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 2);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 3);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[[[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 3);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 4);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[[[[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 4);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 5);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[[[[[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 5);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 6);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[[[[[[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 6);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 7);
+      mv.visitInsn(ICONST_1);
+      mv.visitTypeInsn(ANEWARRAY, "[[[[[[[F");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_0);
+      mv.visitVarInsn(ALOAD, 7);
+      mv.visitInsn(AASTORE);
+      mv.visitVarInsn(ASTORE, 8);
+      Label l6 = new Label();
+      mv.visitLabel(l6);
+      // mv.visitFrame(Opcodes.F_FULL, 9, new Object[] {Opcodes.INTEGER, "[F", "[[F", "[[[F",
+      // "[[[[F", "[[[[[F", "[[[[[[F", "[[[[[[[F", "[[[[[[[[F"}, 0, new Object[] {});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitIincInsn(0, -1);
+      mv.visitJumpInsn(IFLE, l4);
+      mv.visitLabel(l0);
+      mv.visitVarInsn(ILOAD, 0);
+      Label l7 = new Label();
+      mv.visitJumpInsn(IFNE, l7);
+      mv.visitVarInsn(ALOAD, 8);
+      Label l8 = new Label();
+      mv.visitJumpInsn(GOTO, l8);
+      mv.visitLabel(l7);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitInsn(ACONST_NULL);
+      mv.visitTypeInsn(CHECKCAST, "[[[[[[[[F");
+      mv.visitLabel(l8);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[[[[[[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[[[[[[[F"});
+      mv.visitVarInsn(ASTORE, 8);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitInsn(ICONST_1);
+      Label l9 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l9);
+      mv.visitVarInsn(ALOAD, 8);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l10 = new Label();
+      mv.visitJumpInsn(GOTO, l10);
+      mv.visitLabel(l9);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 8);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l10);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[[[[[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[[[[[[F"});
+      mv.visitVarInsn(ASTORE, 7);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitInsn(ICONST_2);
+      Label l11 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l11);
+      mv.visitVarInsn(ALOAD, 7);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l12 = new Label();
+      mv.visitJumpInsn(GOTO, l12);
+      mv.visitLabel(l11);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 7);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l12);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[[[[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[[[[[F"});
+      mv.visitVarInsn(ASTORE, 6);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitInsn(ICONST_3);
+      Label l13 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l13);
+      mv.visitVarInsn(ALOAD, 6);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l14 = new Label();
+      mv.visitJumpInsn(GOTO, l14);
+      mv.visitLabel(l13);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 6);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l14);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[[[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[[[[F"});
+      mv.visitVarInsn(ASTORE, 5);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitInsn(ICONST_4);
+      Label l15 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l15);
+      mv.visitVarInsn(ALOAD, 5);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l16 = new Label();
+      mv.visitJumpInsn(GOTO, l16);
+      mv.visitLabel(l15);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 5);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l16);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[[[F"});
+      mv.visitVarInsn(ASTORE, 4);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitInsn(ICONST_5);
+      Label l17 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l17);
+      mv.visitVarInsn(ALOAD, 4);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l18 = new Label();
+      mv.visitJumpInsn(GOTO, l18);
+      mv.visitLabel(l17);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 4);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l18);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[[F"});
+      mv.visitVarInsn(ASTORE, 3);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitIntInsn(BIPUSH, 6);
+      Label l19 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l19);
+      mv.visitVarInsn(ALOAD, 3);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l20 = new Label();
+      mv.visitJumpInsn(GOTO, l20);
+      mv.visitLabel(l19);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 3);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l20);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[[F"});
+      mv.visitVarInsn(ASTORE, 2);
+      mv.visitVarInsn(ILOAD, 0);
+      mv.visitIntInsn(BIPUSH, 7);
+      Label l21 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l21);
+      mv.visitVarInsn(ALOAD, 2);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(AALOAD);
+      Label l22 = new Label();
+      mv.visitJumpInsn(GOTO, l22);
+      mv.visitLabel(l21);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          0,
+          new Object[] {});
+      mv.visitVarInsn(ALOAD, 2);
+      mv.visitInsn(ICONST_0);
+      mv.visitInsn(AALOAD);
+      mv.visitLabel(l22);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"[F"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"[F"});
+      mv.visitVarInsn(ASTORE, 1);
+      mv.visitLabel(l1);
+      mv.visitJumpInsn(GOTO, l6);
+      mv.visitLabel(l2);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]
+      // {"java/lang/NullPointerException"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          9,
+          new Object[] {
+            Opcodes.INTEGER,
+            "[F",
+            "[[F",
+            "[[[F",
+            "[[[[F",
+            "[[[[[F",
+            "[[[[[[F",
+            "[[[[[[[F",
+            "[[[[[[[[F"
+          },
+          1,
+          new Object[] {"java/lang/NullPointerException"});
+      mv.visitVarInsn(ASTORE, 9);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitLdcInsn("null pointer");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      mv.visitJumpInsn(GOTO, l6);
+      mv.visitLabel(l4);
+      // mv.visitFrame(Opcodes.F_FULL, 1, new Object[] {Opcodes.INTEGER}, 0, new Object[] {});
+      mv.visitFrame(Opcodes.F_NEW, 1, new Object[] {Opcodes.INTEGER}, 0, new Object[] {});
+      Label l23 = new Label();
+      mv.visitJumpInsn(GOTO, l23);
+      mv.visitLabel(l5);
+      // mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/RuntimeException"});
+      mv.visitFrame(
+          Opcodes.F_NEW,
+          1,
+          new Object[] {Opcodes.INTEGER},
+          1,
+          new Object[] {"java/lang/RuntimeException"});
+      mv.visitVarInsn(ASTORE, 1);
+      mv.visitLdcInsn(new Float("-1.0"));
+      mv.visitInsn(FRETURN);
+      mv.visitLabel(l23);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFrame(Opcodes.F_NEW, 1, new Object[] {Opcodes.INTEGER}, 0, new Object[] {});
+      mv.visitLdcInsn(new Float("42.0"));
+      mv.visitInsn(FRETURN);
+      mv.visitMaxs(4, 10);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+      mv.visitCode();
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ARRAYLENGTH);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(IADD);
+      mv.visitMethodInsn(
+          INVOKESTATIC,
+          "com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTest",
+          "foo",
+          "(I)F",
+          false);
+      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(F)V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(3, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java
new file mode 100644
index 0000000..7866e1b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ArrayDimensionGreaterThanSevenTestRunner.java
@@ -0,0 +1,82 @@
+// 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.debug;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class ArrayDimensionGreaterThanSevenTestRunner extends DebugTestBase {
+
+  private static final Class CLASS = ArrayDimensionGreaterThanSevenTest.class;
+  private static final String NAME = CLASS.getCanonicalName();
+
+  private DebugTestConfig getR8CfConfig(String s, Consumer<InternalOptions> optionsConsumer)
+      throws IOException, com.android.tools.r8.CompilationFailedException {
+    Path cfOut = temp.getRoot().toPath().resolve(s);
+    ToolHelper.runR8(
+        R8Command.builder()
+            .addClassProgramData(ToolHelper.getClassAsBytes(CLASS), Origin.unknown())
+            .setMode(CompilationMode.DEBUG)
+            .setOutput(cfOut, OutputMode.ClassFile)
+            .build(),
+        optionsConsumer);
+    return new CfDebugTestConfig(cfOut);
+  }
+
+  private Stream<DebuggeeState> createStream(DebugTestConfig config) throws Exception {
+    return streamDebugTest(config, NAME, ANDROID_FILTER);
+  }
+
+  @Test
+  @Ignore("b/111296969")
+  // Once R8 does not use expanded frames this can be enabled again.
+  public void test() throws Exception {
+    DebugTestConfig cfConfig = new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
+    DebugTestConfig d8Config = new D8DebugTestConfig().compileAndAddClasses(temp, CLASS);
+    DebugTestConfig r8JarConfig =
+        getR8CfConfig("r8jar.jar", options -> options.enableCfFrontend = false);
+    DebugTestConfig r8CfConfig =
+        getR8CfConfig("r8cf.jar", options -> options.enableCfFrontend = true);
+    new DebugStreamComparator()
+        .add("CF", createStream(cfConfig))
+        .add("R8/CF", createStream(r8CfConfig))
+        .add("R8/Jar", createStream(r8JarConfig))
+        .add("D8", createStream(d8Config))
+        .compare();
+  }
+
+  @Test
+  // Verify that ASM fails when using expanded frames directly.
+  // See b/111296969
+  public void runTestOnAsmDump() throws Exception {
+    Path out = temp.getRoot().toPath().resolve("out.jar");
+    ArchiveConsumer consumer = new ArchiveConsumer(out);
+    consumer.accept(
+        ArrayDimensionGreaterThanSevenTestDump.dump(),
+        DescriptorUtils.javaTypeToDescriptor(NAME),
+        null);
+    consumer.finished(null);
+    ProcessResult result = ToolHelper.runJava(out, NAME);
+    assertEquals("Expected ASM to fail when using visitFrame(F_NEW, ...)", 1, result.exitCode);
+    assertThat(result.stderr, containsString("java.lang.NoClassDefFoundError: F"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java b/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
index eb85a7c..3a83e30 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
@@ -214,7 +214,8 @@
       try {
         if (done) {
           assertTrue(
-              "Not all streams completed at the same time",
+              "Not all streams completed at the same time. "
+                  + "Set 'DebugTestBase.DEBUG_TEST = true' to aid in diagnosing the issue.",
               states.stream().allMatch(Objects::isNull));
           return;
         } else {
diff --git a/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java b/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
index bda9ac5..6877617 100644
--- a/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
@@ -10,11 +10,22 @@
 
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.graph.invokesuper2.C0;
+import com.android.tools.r8.graph.invokesuper2.C1;
+import com.android.tools.r8.graph.invokesuper2.C2;
+import com.android.tools.r8.graph.invokesuper2.I0;
+import com.android.tools.r8.graph.invokesuper2.I1;
+import com.android.tools.r8.graph.invokesuper2.I2;
+import com.android.tools.r8.graph.invokesuper2.I3;
+import com.android.tools.r8.graph.invokesuper2.I4;
+import com.android.tools.r8.graph.invokesuper2.Main;
 import com.android.tools.r8.smali.SmaliBuilder;
 import com.android.tools.r8.smali.SmaliTestBase;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
 import java.util.Collections;
 import org.junit.Test;
 
@@ -200,6 +211,63 @@
     AndroidApp processedApp = processApplication(application);
     assertEquals("42", runArt(processedApp));
   }
+
+  @Test
+  public void testLookupSuperTarget() throws Exception {
+    String pkg = Main.class.getPackage().getName().replace('.', '/');
+
+    AndroidApp.Builder builder = AndroidApp.builder();
+    for (Class clazz : new Class[]{
+        I0.class, I1.class, I2.class, I3.class, I4.class,
+        C0.class, C1.class, C2.class,
+        Main.class}) {
+      builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
+      // At least java.lang.Object is needed as interface method lookup have special handling
+      // of methods on java.lang.Object.
+      builder.addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    }
+    AndroidApp application = builder.build();
+    AppInfo appInfo = getAppInfo(application);
+    DexItemFactory factory = appInfo.dexItemFactory;
+
+    DexType i0 = factory.createType("L" + pkg + "/I0;");
+    DexType i1 = factory.createType("L" + pkg + "/I1;");
+    DexType i2 = factory.createType("L" + pkg + "/I2;");
+    DexType i3 = factory.createType("L" + pkg + "/I3;");
+    DexType i4 = factory.createType("L" + pkg + "/I4;");
+    DexType c0 = factory.createType("L" + pkg + "/C0;");
+    DexType c1 = factory.createType("L" + pkg + "/C1;");
+    DexType c2 = factory.createType("L" + pkg + "/C2;");
+
+    DexProto mProto = factory.createProto(factory.intType);
+    DexString m = factory.createString("m");
+    DexMethod mOnC0 = factory.createMethod(c0, mProto, m);
+    DexMethod mOnC1 = factory.createMethod(c1, mProto, m);
+    DexMethod mOnI0 = factory.createMethod(i0, mProto, m);
+    DexMethod mOnI1 = factory.createMethod(i1, mProto, m);
+    DexMethod mOnI2 = factory.createMethod(i2, mProto, m);
+    DexMethod mOnI3 = factory.createMethod(i3, mProto, m);
+    DexMethod mOnI4 = factory.createMethod(i4, mProto, m);
+
+    assertEquals(mOnI0, appInfo.lookupSuperTarget(mOnC0, c1).method);
+    assertEquals(mOnI1, appInfo.lookupSuperTarget(mOnI1, c1).method);
+    assertEquals(mOnI2, appInfo.lookupSuperTarget(mOnI2, c1).method);
+
+    assertEquals(mOnI0, appInfo.lookupSuperTarget(mOnC1, c2).method);
+    assertEquals(mOnI1, appInfo.lookupSuperTarget(mOnI3, c2).method);
+    assertEquals(mOnI2, appInfo.lookupSuperTarget(mOnI4, c2).method);
+
+    // Copy classes to run on the Java VM.
+    Path out = temp.newFolder().toPath();
+    copyTestClasses(out, I0.class, I1.class, I2.class, I3.class, I4.class);
+    copyTestClasses(out, C0.class, C1.class, C2.class, Main.class);
+    ProcessResult result = ToolHelper.runJava(out, Main.class.getCanonicalName());
+    assertEquals(0, result.exitCode);
+
+    // Process the application and expect the same result on Art.
+    AndroidApp processedApp = processApplication(application);
+    assertEquals(result.stdout, runArt(processedApp, Main.class.getCanonicalName()));
+  }
 }
 
 
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/C0.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/C0.java
new file mode 100644
index 0000000..8e92fb4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/C0.java
@@ -0,0 +1,8 @@
+// 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.graph.invokesuper2;
+
+public class C0  implements I0 {
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/C1.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/C1.java
new file mode 100644
index 0000000..ff98da9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/C1.java
@@ -0,0 +1,17 @@
+// 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.graph.invokesuper2;
+
+public class C1 extends C0 implements I1, I2 {
+  public int m() {
+    // super.m() becomes: invokespecial com/android/tools/r8/graph/invokesuper2/C0.m()I
+    System.out.println(super.m());
+    // I1.super.m() becomes: invokespecial com/android/tools/r8/graph/invokesuper2/I1.m:()I
+    System.out.println(I1.super.m());
+    // I2.super.m() becomes: invokespecial com/android/tools/r8/graph/invokesuper2/I2.m:()I
+    System.out.println(I2.super.m());
+    return 3;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/C2.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/C2.java
new file mode 100644
index 0000000..abdf7ff
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/C2.java
@@ -0,0 +1,17 @@
+// 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.graph.invokesuper2;
+
+public class C2 extends C0 implements I3, I4 {
+  public int m() {
+    // super.m() becomes: invokespecial com/android/tools/r8/graph/invokesuper2/C0.m()I
+    System.out.println(super.m());
+    // I1.super.m() becomes: invokespecial com/android/tools/r8/graph/invokesuper2/I3.m:()I
+    System.out.println(I3.super.m());
+    // I2.super.m() becomes: invokespecial com/android/tools/r8/graph/invokesuper2/I4.m:()I
+    System.out.println(I4.super.m());
+    return 3;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/I0.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/I0.java
new file mode 100644
index 0000000..02b59a6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/I0.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.graph.invokesuper2;
+
+public interface I0 {
+  default int m() {
+    return 0;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/I1.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/I1.java
new file mode 100644
index 0000000..0593d90
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/I1.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.graph.invokesuper2;
+
+public interface I1 extends I0 {
+  default int m() {
+    return 1;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/I2.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/I2.java
new file mode 100644
index 0000000..22956c3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/I2.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.graph.invokesuper2;
+
+public interface I2 extends I0 {
+  default int m() {
+    return 2;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/I3.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/I3.java
new file mode 100644
index 0000000..e64154e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/I3.java
@@ -0,0 +1,8 @@
+// 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.graph.invokesuper2;
+
+public interface I3 extends I1 {
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/I4.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/I4.java
new file mode 100644
index 0000000..ddc5ad0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/I4.java
@@ -0,0 +1,8 @@
+// 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.graph.invokesuper2;
+
+public interface I4 extends I2 {
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokesuper2/Main.java b/src/test/java/com/android/tools/r8/graph/invokesuper2/Main.java
new file mode 100644
index 0000000..6a2a3b9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokesuper2/Main.java
@@ -0,0 +1,13 @@
+// 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.graph.invokesuper2;
+
+public class Main {
+
+  public static void main(String[] args) {
+    System.out.println(new C1().m());
+    System.out.println(new C2().m());
+  }
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index 82381dd..9c15ce7 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -353,12 +353,16 @@
     return out.toByteArray();
   }
 
-  public List<byte[]> buildClasses() throws Exception {
-    List<byte[]> result = new ArrayList<>();
+  public ImmutableList.Builder<byte[]> buildClasses(ImmutableList.Builder<byte[]> builder)
+      throws Exception {
     for (ClassBuilder clazz : classes) {
-      result.add(compile(clazz));
+      builder.add(compile(clazz));
     }
-    return result;
+    return builder;
+  }
+
+  public List<byte[]> buildClasses() throws Exception {
+    return buildClasses(ImmutableList.builder()).build();
   }
 
   public AndroidApp build() throws Exception {
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
new file mode 100644
index 0000000..90fba5f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java
@@ -0,0 +1,342 @@
+// 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.regress.b111250398;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.code.Iget;
+import com.android.tools.r8.code.IgetObject;
+import com.android.tools.r8.code.Sget;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexField;
+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.FieldSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import org.junit.Test;
+
+// Copy of javax.inject.Provider.
+interface Provider<T> {
+  T get();
+}
+
+// Copy of dagger.internal.SingleClass.
+final class SingleCheck<T> implements Provider<T> {
+  private static final Object UNINITIALIZED = new Object();
+
+  private volatile Provider<T> provider;
+  private volatile Object instance = UNINITIALIZED;
+
+  private SingleCheck(Provider<T> provider) {
+    assert provider != null;
+    this.provider = provider;
+  }
+
+  @SuppressWarnings("unchecked") // cast only happens when result comes from the delegate provider
+  @Override
+  public T get() {
+    Object local = instance;
+    if (local == UNINITIALIZED) {
+      // provider is volatile and might become null after the check, so retrieve the provider first
+      Provider<T> providerReference = provider;
+      if (providerReference == null) {
+        // The provider was null, so the instance must already be set
+        local = instance;
+      } else {
+        local = providerReference.get();
+        instance = local;
+
+        // Null out the reference to the provider. We are never going to need it again, so we can
+        // make it eligible for GC.
+        provider = null;
+      }
+    }
+    return (T) local;
+  }
+
+  // This method is not relevant for the test.
+  /*
+  public static <P extends Provider<T>, T> Provider<T> provider(P provider) {
+    // If a scoped @Binds delegates to a scoped binding, don't cache the value again.
+    if (provider instanceof SingleCheck || provider instanceof DoubleCheck) {
+      return provider;
+    }
+    return new SingleCheck<T>(checkNotNull(provider));
+  }
+  */
+}
+
+// Several field gets on non-volatile and volatile fields on the same class.
+class A {
+  int t;
+  int f;
+  static int sf;
+  volatile int v;
+  static volatile int sv;
+
+  public void mf() {
+    t = f;
+    t = f;
+    t = f;
+    t = f;
+    t = f;
+  }
+
+  public void msf() {
+    t = sf;
+    t = sf;
+    t = sf;
+    t = sf;
+    t = sf;
+  }
+
+  public void mv() {
+    t = v;
+    t = v;
+    t = v;
+    t = v;
+    t = v;
+  }
+
+  public void msv() {
+    t = sv;
+    t = sv;
+    t = sv;
+    t = sv;
+    t = sv;
+  }
+}
+
+// Several field gets on non-volatile and volatile fields on different class.
+class B {
+  int t;
+
+  public void mf(A a) {
+    t = a.f;
+    t = a.f;
+    t = a.f;
+    t = a.f;
+    t = a.f;
+  }
+
+  public void msf() {
+    t = A.sf;
+    t = A.sf;
+    t = A.sf;
+    t = A.sf;
+    t = A.sf;
+  }
+
+  public void mv(A a) {
+    t = a.v;
+    t = a.v;
+    t = a.v;
+    t = a.v;
+    t = a.v;
+  }
+
+  public void msv() {
+    t = A.sv;
+    t = A.sv;
+    t = A.sv;
+    t = A.sv;
+    t = A.sv;
+  }
+}
+
+// Modified sample from http://tutorials.jenkov.com/java-concurrency/volatile.html.
+class C {
+  private int years;
+  private int months;
+  private volatile int days;
+
+  public int totalDays() {
+    int total = this.days;
+    total += months * 30;
+    total += years * 365;
+    return total;
+  }
+
+  public int totalDaysTimes2() {
+    int total = this.days;
+    total += months * 30;
+    total += years * 365;
+    total += this.days;
+    total += months * 30;
+    total += years * 365;
+    return total;
+  }
+
+  public int totalDaysTimes3() {
+    int total = this.days;
+    total += months * 30;
+    total += years * 365;
+    total += this.days;
+    total += months * 30;
+    total += years * 365;
+    total += this.days;
+    total += months * 30;
+    total += years * 365;
+    return total;
+  }
+
+  public void update(int years, int months, int days){
+    this.years  = years;
+    this.months = months;
+    this.days   = days;
+  }
+}
+
+public class B111250398 extends TestBase {
+
+  private void releaseMode(InternalOptions options) {
+    options.debug = false;
+  }
+
+  private long countIget(DexCode code, DexField field) {
+    return Arrays.stream(code.instructions)
+        .filter(instruction -> instruction instanceof Iget)
+        .map(instruction -> (Iget) instruction)
+        .filter(get -> get.getField() == field)
+        .count();
+  }
+
+  private long countSget(DexCode code, DexField field) {
+    return Arrays.stream(code.instructions)
+        .filter(instruction -> instruction instanceof Sget)
+        .map(instruction -> (Sget) instruction)
+        .filter(get -> get.getField() == field)
+        .count();
+  }
+
+  private long countIgetObject(MethodSubject method, FieldSubject field) {
+    return Arrays.stream(method.getMethod().getCode().asDexCode().instructions)
+        .filter(instruction -> instruction instanceof IgetObject)
+        .map(instruction -> (IgetObject) instruction)
+        .filter(get -> get.getField() == field.getField().field)
+        .count();
+  }
+
+  private void check(DexInspector inspector, int mfOnBGets, int msfOnBGets) {
+    ClassSubject classA = inspector.clazz(A.class);
+    assertThat(classA, isPresent());
+    MethodSubject mfOnA = classA.method("void", "mf", ImmutableList.of());
+    assertThat(mfOnA, isPresent());
+    MethodSubject msfOnA = classA.method("void", "msf", ImmutableList.of());
+    assertThat(msfOnA, isPresent());
+    MethodSubject mvOnA = classA.method("void", "mv", ImmutableList.of());
+    assertThat(mvOnA, isPresent());
+    MethodSubject msvOnA = classA.method("void", "msv", ImmutableList.of());
+    assertThat(msvOnA, isPresent());
+    FieldSubject fOnA = classA.field("int", "f");
+    assertThat(fOnA, isPresent());
+    FieldSubject sfOnA = classA.field("int", "sf");
+    assertThat(sfOnA, isPresent());
+    FieldSubject vOnA = classA.field("int", "v");
+    assertThat(vOnA, isPresent());
+    FieldSubject svOnA = classA.field("int", "sv");
+    assertThat(svOnA, isPresent());
+    ClassSubject classB = inspector.clazz(B.class);
+    assertThat(classB, isPresent());
+    MethodSubject mfOnB = classB.method("void", "mf", ImmutableList.of(classA.getOriginalName()));
+    assertThat(mfOnB, isPresent());
+    MethodSubject msfOnB = classB.method("void", "msf", ImmutableList.of());
+    assertThat(msfOnB, isPresent());
+    MethodSubject mvOnB = classB.method("void", "mv", ImmutableList.of(classA.getOriginalName()));
+    assertThat(mvOnB, isPresent());
+    MethodSubject msvOnB = classB.method("void", "msv", ImmutableList.of());
+    assertThat(msvOnB, isPresent());
+    // Field load of volatile fields are never eliminated.
+    assertEquals(5, countIget(mvOnA.getMethod().getCode().asDexCode(), vOnA.getField().field));
+    assertEquals(5, countSget(msvOnA.getMethod().getCode().asDexCode(), svOnA.getField().field));
+    assertEquals(5, countIget(mvOnB.getMethod().getCode().asDexCode(), vOnA.getField().field));
+    assertEquals(5, countSget(msvOnB.getMethod().getCode().asDexCode(), svOnA.getField().field));
+    // For fields on the same class both separate compilation (D8) and whole program
+    // 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));
+    // 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,
+        countIget(mfOnB.getMethod().getCode().asDexCode(), fOnA.getField().field));
+    assertEquals(msfOnBGets,
+        countSget(msfOnB.getMethod().getCode().asDexCode(), sfOnA.getField().field));
+  }
+
+  @Test
+  public void testSeparateCompilation() throws Exception {
+    DexInspector inspector =
+        new DexInspector(compileWithD8(readClasses(A.class, B.class), this::releaseMode));
+    check(inspector, 5, 5);
+  }
+
+  @Test
+  public void testWholeProgram() throws Exception {
+    DexInspector inspector =
+        new DexInspector(compileWithR8(readClasses(A.class, B.class), this::releaseMode));
+    // The reason for getting two Igets in B.mf is that the first Iget inserts a NonNull
+    // instruction which creates a new value for the remaining Igets.
+    check(inspector, 2, 1);
+  }
+
+  private void checkMixed(AndroidApp app) throws Exception{
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject classC = inspector.clazz(C.class);
+    assertThat(classC, isPresent());
+    MethodSubject totalDays = classC.method("int", "totalDays", ImmutableList.of());
+    assertThat(totalDays, isPresent());
+    MethodSubject totalDaysTimes2 = classC.method("int", "totalDaysTimes2", ImmutableList.of());
+    assertThat(totalDaysTimes2, isPresent());
+    MethodSubject totalDaysTimes3 = classC.method("int", "totalDaysTimes3", ImmutableList.of());
+    assertThat(totalDaysTimes3, isPresent());
+    FieldSubject years = classC.field("int", "years");
+    assertThat(years, isPresent());
+    FieldSubject months = classC.field("int", "months");
+    assertThat(months, isPresent());
+    FieldSubject days = classC.field("int", "days");
+    assertThat(days, isPresent());
+
+
+    for (FieldSubject field : new FieldSubject[]{years, months, days}) {
+      assertEquals(1,
+          countIget(totalDays.getMethod().getCode().asDexCode(), field.getField().field));
+      assertEquals(2,
+          countIget(totalDaysTimes2.getMethod().getCode().asDexCode(), field.getField().field));
+      assertEquals(3,
+          countIget(totalDaysTimes3.getMethod().getCode().asDexCode(), field.getField().field));
+    }
+  }
+
+  @Test
+  public void testMixedVolatileNonVolatile() throws Exception {
+    AndroidApp app = readClasses(C.class);
+    checkMixed(compileWithD8(app, this::releaseMode));
+    checkMixed(compileWithR8(app, this::releaseMode));
+  }
+
+  private void checkDaggerSingleProviderGet(AndroidApp app) throws Exception {
+    DexInspector inspector = new DexInspector(app);
+    MethodSubject get =
+        inspector.clazz(SingleCheck.class).method("java.lang.Object", "get", ImmutableList.of());
+    assertThat(get, isPresent());
+    FieldSubject instance =
+        inspector.clazz(SingleCheck.class).field("java.lang.Object", "instance");
+    assertEquals(2, countIgetObject(get, instance));
+  }
+
+  @Test
+  public void testDaggerSingleProvider() throws Exception {
+    AndroidApp app = readClasses(Provider.class, SingleCheck.class);
+    checkDaggerSingleProviderGet(compileWithD8(app, this::releaseMode));
+    checkDaggerSingleProviderGet(compileWithR8(app, this::releaseMode));
+  }
+}