CF backend: Compute uninitialized types in stackmaps

Add new type CfFrame.Uninitialized representing either an uninitialized
reference from a NewInstruction or from the `this` argument in an
instance initializer.

Construct a mapping from each NewInstance instruction to the Invokes
that initialize the reference, and construct the list of Invokes that
initialize the uninitialized `this`.

When emitting a stack frame involving values with definitions pointing
to NewInstruction or `this` Argument, follow predecessor pointers from
the current block to see if the reference is initialized at this point.

Since most code will have the NewInstance/Argument and the initializing
Invoke in the same block, there's no need to cache the calls to
findAllocator().

Also assert in CfBuilder that stack is empty between all basic blocks.

Change-Id: I949f951c9e7ec3a48162f0f8375faf0ba820d045
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 39d916a..181ebac 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -16,6 +16,9 @@
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
 import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfFrame.Uninitialized;
+import com.android.tools.r8.cf.code.CfFrame.UninitializedNew;
+import com.android.tools.r8.cf.code.CfFrame.UninitializedThis;
 import com.android.tools.r8.cf.code.CfGoto;
 import com.android.tools.r8.cf.code.CfIf;
 import com.android.tools.r8.cf.code.CfIfCmp;
@@ -240,7 +243,17 @@
     StringBuilder builder = new StringBuilder("frame: [");
     String separator = "";
     for (Entry<DexType> entry : frame.getLocals().int2ReferenceEntrySet()) {
-      builder.append(separator).append(entry.getIntKey()).append(':').append(entry.getValue());
+      builder.append(separator).append(entry.getIntKey()).append(':');
+      Uninitialized allocator = frame.getAllocators().get(entry.getIntKey());
+      if (allocator == null) {
+        builder.append(entry.getValue());
+      } else if (allocator instanceof UninitializedThis) {
+        builder.append("uninitialized this");
+      } else {
+        builder
+            .append("uninitialized ")
+            .append(getLabel(((UninitializedNew) allocator).getLabel()));
+      }
       separator = ", ";
     }
     builder.append("] ");
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index 2818e44..b5e759c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -17,11 +17,44 @@
 
 public class CfFrame extends CfInstruction {
 
+  public abstract static class Uninitialized {
+    abstract Object getAsmLabel();
+  }
+
+  public static class UninitializedNew extends Uninitialized {
+    private final CfLabel label;
+
+    public UninitializedNew(CfLabel label) {
+      this.label = label;
+    }
+
+    @Override
+    Object getAsmLabel() {
+      return label.getLabel();
+    }
+
+    public CfLabel getLabel() {
+      return label;
+    }
+  }
+
+  public static class UninitializedThis extends Uninitialized {
+    @Override
+    Object getAsmLabel() {
+      return Opcodes.UNINITIALIZED_THIS;
+    }
+  }
+
   private final Int2ReferenceSortedMap<DexType> locals;
+  private final Int2ReferenceSortedMap<Uninitialized> allocators;
   private final List<DexType> stack;
 
-  public CfFrame(Int2ReferenceSortedMap<DexType> locals, List<DexType> stack) {
+  public CfFrame(
+      Int2ReferenceSortedMap<DexType> locals,
+      Int2ReferenceSortedMap<Uninitialized> allocators,
+      List<DexType> stack) {
     this.locals = locals;
+    this.allocators = allocators;
     this.stack = stack;
   }
 
@@ -29,6 +62,10 @@
     return locals;
   }
 
+  public Int2ReferenceSortedMap<Uninitialized> getAllocators() {
+    return allocators;
+  }
+
   public List<DexType> getStack() {
     return stack;
   }
@@ -88,7 +125,8 @@
     int localIndex = 0;
     for (int i = 0; i <= maxRegister; i++) {
       DexType type = locals.get(i);
-      Object typeOpcode = getType(type, lens);
+      Uninitialized allocator = allocators.get(i);
+      Object typeOpcode = allocator == null ? getType(type, lens) : allocator.getAsmLabel();
       localsTypes[localIndex++] = typeOpcode;
       if (type != null && isWide(type)) {
         i++;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 7c04392..8dd9162 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -7,6 +7,9 @@
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfFrame.Uninitialized;
+import com.android.tools.r8.cf.code.CfFrame.UninitializedNew;
+import com.android.tools.r8.cf.code.CfFrame.UninitializedThis;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfPosition;
@@ -28,8 +31,10 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.JumpInstruction;
 import com.android.tools.r8.ir.code.Load;
+import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StackValue;
 import com.android.tools.r8.ir.code.Store;
@@ -42,9 +47,11 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -77,6 +84,10 @@
 
   private AppInfoWithSubtyping appInfo;
 
+  private Map<NewInstance, List<InvokeDirect>> initializers;
+  private List<InvokeDirect> thisInitializers;
+  private Map<NewInstance, CfLabel> newInstanceLabels;
+
   // Internal abstraction of the stack values and height.
   private static class Stack {
     int maxHeight = 0;
@@ -110,6 +121,7 @@
 
   public CfCode build(
       CodeRewriter rewriter, InternalOptions options, AppInfoWithSubtyping appInfo) {
+    computeInitializers();
     types = new TypeVerificationHelper(code, factory, appInfo).computeVerificationTypes();
     splitExceptionalBlocks();
     LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, types);
@@ -133,6 +145,39 @@
     return resolvedField == null ? field : resolvedField.field;
   }
 
+  private void computeInitializers() {
+    assert initializers == null;
+    assert thisInitializers == null;
+    initializers = new HashMap<>();
+    for (BasicBlock block : code.blocks) {
+      for (Instruction insn : block.getInstructions()) {
+        if (insn.isNewInstance()) {
+          initializers.put(insn.asNewInstance(), computeInitializers(insn.outValue()));
+        } else if (insn.isArgument() && method.isInstanceInitializer()) {
+          if (insn.outValue().isThis()) {
+            // By JVM8 §4.10.1.9 (invokespecial), a this() or super() call in a constructor
+            // changes the type of `this` from uninitializedThis
+            // to the type of the class of the <init> method.
+            thisInitializers = computeInitializers(insn.outValue());
+          }
+        }
+      }
+    }
+    assert !(method.isInstanceInitializer() && thisInitializers == null);
+  }
+
+  private List<InvokeDirect> computeInitializers(Value value) {
+    List<InvokeDirect> initializers = new ArrayList<>();
+    for (Instruction user : value.uniqueUsers()) {
+      if (user instanceof InvokeDirect
+          && user.inValues().get(0) == value
+          && user.asInvokeDirect().getInvokedMethod().name == factory.constructorMethodName) {
+        initializers.add(user.asInvokeDirect());
+      }
+    }
+    return initializers;
+  }
+
   // Split all blocks with throwing instructions and exceptional edges such that any non-throwing
   // instructions that might define values prior to the throwing exception are excluded from the
   // try-catch range. Failure to do so will result in code that does not verify on the JVM.
@@ -203,6 +248,7 @@
     List<CfTryCatch> tryCatchRanges = new ArrayList<>();
     labels = new HashMap<>(code.blocks.size());
     emittedLabels = new HashSet<>(code.blocks.size());
+    newInstanceLabels = new HashMap<>(initializers.size());
     instructions = new ArrayList<>();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     BasicBlock block = blockIterator.next();
@@ -211,6 +257,7 @@
     BasicBlock pendingFrame = null;
     boolean previousFallthrough = false;
     do {
+      assert stack.isEmpty();
       CatchHandlers<BasicBlock> handlers = block.getCatchHandlers();
       if (!tryCatchHandlers.equals(handlers)) {
         if (!tryCatchHandlers.isEmpty()) {
@@ -230,7 +277,6 @@
       // If previousBlock is fallthrough, then it is counted in getPredecessors().size(), but
       // we only want to set a pendingFrame if we have a predecessor which is not previousBlock.
       if (block.getPredecessors().size() > (previousFallthrough ? 1 : 0)) {
-        assert stack.isEmpty();
         pendingFrame = block;
         emitLabel(getLabel(block));
       }
@@ -318,6 +364,9 @@
           pendingLocalChanges = true;
         }
       } else {
+        if (instruction.isNewInstance()) {
+          newInstanceLabels.put(instruction.asNewInstance(), ensureLabel());
+        }
         updatePositionAndLocals(instruction);
         instruction.buildCf(this);
       }
@@ -400,9 +449,11 @@
 
     Collection<Value> locals = registerAllocator.getLocalsAtBlockEntry(block);
     Int2ReferenceSortedMap<DexType> mapping = new Int2ReferenceAVLTreeMap<>();
+    Int2ReferenceSortedMap<Uninitialized> allocators = new Int2ReferenceAVLTreeMap<>();
 
     for (Value local : locals) {
       DexType type;
+      Uninitialized allocator = null;
       switch (local.outType()) {
         case INT:
           type = factory.intType;
@@ -418,14 +469,64 @@
           break;
         case OBJECT:
           type = types.get(local);
+          allocator = findAllocator(block, local);
           break;
         default:
           throw new Unreachable(
               "Unexpected local type: " + local.outType() + " for local: " + local);
       }
       mapping.put(getLocalRegister(local), type);
+      if (allocator != null) {
+        allocators.put(getLocalRegister(local), allocator);
+      }
     }
-    instructions.add(new CfFrame(mapping, stackTypes));
+    instructions.add(new CfFrame(mapping, allocators, stackTypes));
+  }
+
+  private Uninitialized findAllocator(BasicBlock liveBlock, Value value) {
+    Instruction definition = value.definition;
+    while (definition != null && (definition.isStore() || definition.isLoad())) {
+      definition = definition.inValues().get(0).definition;
+    }
+    if (definition == null) {
+      return null;
+    }
+    Uninitialized res;
+    if (definition.isNewInstance()) {
+      res = new UninitializedNew(newInstanceLabels.get(definition.asNewInstance()));
+    } else if (definition.isArgument()
+        && method.isInstanceInitializer()
+        && definition.outValue().isThis()) {
+      res = new UninitializedThis();
+    } else {
+      return null;
+    }
+    BasicBlock definitionBlock = definition.getBlock();
+    Set<BasicBlock> visited = new HashSet<>();
+    Deque<BasicBlock> toVisit = new ArrayDeque<>();
+    List<InvokeDirect> valueInitializers =
+        definition.isArgument() ? thisInitializers : initializers.get(definition.asNewInstance());
+    for (InvokeDirect initializer : valueInitializers) {
+      BasicBlock initializerBlock = initializer.getBlock();
+      if (initializerBlock == liveBlock) {
+        return res;
+      }
+      if (initializerBlock != definitionBlock && visited.add(initializerBlock)) {
+        toVisit.addLast(initializerBlock);
+      }
+    }
+    while (!toVisit.isEmpty()) {
+      BasicBlock block = toVisit.removeLast();
+      for (BasicBlock predecessor : block.getPredecessors()) {
+        if (predecessor == liveBlock) {
+          return res;
+        }
+        if (predecessor != definitionBlock && visited.add(predecessor)) {
+          toVisit.addLast(predecessor);
+        }
+      }
+    }
+    return null;
   }
 
   private void emitLabel(CfLabel label) {
diff --git a/src/test/java/com/android/tools/r8/cf/UninitializedInFrameDump.java b/src/test/java/com/android/tools/r8/cf/UninitializedInFrameDump.java
new file mode 100644
index 0000000..bac896e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/UninitializedInFrameDump.java
@@ -0,0 +1,179 @@
+// 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.cf;
+
+import java.util.*;
+import org.objectweb.asm.*;
+
+public class UninitializedInFrameDump implements Opcodes {
+
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(
+        V1_8,
+        ACC_PUBLIC + ACC_SUPER,
+        "com/android/tools/r8/cf/UninitializedInFrameTest",
+        null,
+        "java/lang/Object",
+        null);
+
+    // The constructor UninitializedInFrameTest(int i) has been modified
+    // to add a jump back to the entry block.
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(I)V", null, null);
+      mv.visitCode();
+      Label l = new Label(); // Added
+      mv.visitLabel(l); // Added
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ILOAD, 1);
+      mv.visitInsn(ICONST_1);
+      mv.visitInsn(ISUB);
+      mv.visitInsn(DUP); // Added
+      mv.visitIntInsn(BIPUSH, 42);
+      Label l0 = new Label();
+      mv.visitJumpInsn(IF_ICMPLT, l0);
+      // mv.visitInsn(ICONST_1);
+      mv.visitVarInsn(ISTORE, 1); // Added
+      mv.visitInsn(POP); // Added
+      mv.visitJumpInsn(GOTO, l); // Added
+      Label l1 = new Label();
+      mv.visitJumpInsn(GOTO, l1);
+      mv.visitLabel(l0);
+      mv.visitInsn(POP); // Added
+      mv.visitInsn(ICONST_0);
+      mv.visitLabel(l1);
+      mv.visitMethodInsn(
+          INVOKESPECIAL,
+          "com/android/tools/r8/cf/UninitializedInFrameTest",
+          "<init>",
+          "(Z)V",
+          false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(-1, -1);
+      // mv.visitMaxs(3, 2);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PRIVATE, "<init>", "(Z)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.visitMaxs(1, 2);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ARRAYLENGTH);
+      Label l0 = new Label();
+      mv.visitJumpInsn(IFEQ, l0);
+      mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
+      mv.visitInsn(DUP);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ARRAYLENGTH);
+      mv.visitIntInsn(BIPUSH, 42);
+      Label l1 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l1);
+      // mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
+      // mv.visitInsn(DUP);
+      mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V", false);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/Throwable;)V", false);
+      mv.visitVarInsn(ASTORE, 1);
+      // At this point, stack is empty.
+      Label l2 = new Label();
+      mv.visitJumpInsn(GOTO, l2);
+      mv.visitLabel(l1);
+      // At this point, stack contains two copies of uninitialized RuntimeException.
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      // mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
+      // mv.visitInsn(DUP);
+      mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
+      mv.visitLdcInsn("You supplied ");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ARRAYLENGTH);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(I)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ARRAYLENGTH);
+      mv.visitInsn(ICONST_1);
+      Label l3 = new Label();
+      mv.visitJumpInsn(IF_ICMPNE, l3);
+      mv.visitLdcInsn(" arg");
+      Label l4 = new Label();
+      mv.visitJumpInsn(GOTO, l4);
+      mv.visitLabel(l3);
+      // At this point, stack contains two copies of uninitialized RuntimeException.
+      // Note that asmifier seems to produce incorrect labels for the uninitialized type.
+      // mv.visitFrame(Opcodes.F_FULL, 1, new Object[] {"[Ljava/lang/String;"}, 3, new Object[] {l1,
+      // l1, "java/lang/StringBuilder"});
+      mv.visitLdcInsn(" args");
+      mv.visitLabel(l4);
+      // At this point, stack contains two copies of uninitialized RuntimeException.
+      // mv.visitFrame(Opcodes.F_FULL, 1, new Object[] {"[Ljava/lang/String;"}, 4, new Object[] {l1,
+      // l1, "java/lang/StringBuilder", "java/lang/String"});
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V", false);
+      mv.visitVarInsn(ASTORE, 1);
+      mv.visitLabel(l2);
+      // At this point, stack is empty, and local 1 contains an initialized RuntimeException.
+      // mv.visitFrame(Opcodes.F_APPEND,1, new Object[] {"java/lang/RuntimeException"}, 0, null);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ARRAYLENGTH);
+      mv.visitInsn(ICONST_2);
+      mv.visitInsn(IREM);
+      Label l5 = new Label();
+      mv.visitJumpInsn(IFNE, l5);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false);
+      mv.visitLabel(l5);
+      // mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitInsn(ATHROW);
+      mv.visitLabel(l0);
+      // mv.visitFrame(Opcodes.F_CHOP,1, null, 0, null);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(-1, -1);
+      // mv.visitMaxs(5, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/UninitializedInFrameTest.java b/src/test/java/com/android/tools/r8/cf/UninitializedInFrameTest.java
new file mode 100644
index 0000000..4479796
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/UninitializedInFrameTest.java
@@ -0,0 +1,54 @@
+// 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.cf;
+
+public class UninitializedInFrameTest {
+  int v;
+
+  public UninitializedInFrameTest(int i) {
+    // In UninitializedInFrameDump, this method is changed to:
+    //     while (i-1 >= 42) {i = i-1;} this(i-1 >= 42);
+    // ...which is invalid in Java source since this() must be the first statement.
+    // Put "i-1 >= 42" in the code here to aid the manual editing in UninitializedInFrameDump.
+    this(i - 1 >= 42);
+  }
+
+  public UninitializedInFrameTest(boolean b) {
+    v = b ? 42 : 0;
+    System.out.println(this);
+    // Add an InvokeDirect that has 'this' as argument to ensure we don't consider it to be
+    // an initialization for 'this'.
+    if (!b) {
+      throw new AssertionError(this);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "Hello world! " + v;
+  }
+
+  public static void main(String[] args) {
+    try {
+      new UninitializedInFrameTest(true);
+      new UninitializedInFrameTest(45);
+    } catch (AssertionError e) {
+    }
+    if (args.length != 0) {
+      RuntimeException e;
+      if (args.length == 42) {
+        e = new RuntimeException(new IllegalArgumentException());
+      } else {
+        e =
+            new RuntimeException(
+                "You supplied " + args.length + (args.length == 1 ? " arg" : " args"));
+      }
+      if (args.length % 2 == 0) {
+        System.out.println(e);
+      }
+      throw e;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/UninitializedInFrameTestRunner.java b/src/test/java/com/android/tools/r8/cf/UninitializedInFrameTestRunner.java
new file mode 100644
index 0000000..f433a9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/UninitializedInFrameTestRunner.java
@@ -0,0 +1,64 @@
+// 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.cf;
+
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class UninitializedInFrameTestRunner {
+  static final Class CLASS = UninitializedInFrameTest.class;
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void test() throws Exception {
+    test(ToolHelper.getClassAsBytes(CLASS));
+  }
+
+  @Test
+  public void testDump() throws Exception {
+    test(UninitializedInFrameDump.dump());
+  }
+
+  private void test(byte[] clazz) throws CompilationFailedException, IOException {
+    Path input = temp.getRoot().toPath().resolve("input.jar");
+    Path output = temp.getRoot().toPath().resolve("output.jar");
+
+    ArchiveConsumer inputConsumer = new ArchiveConsumer(input);
+    inputConsumer.accept(clazz, DescriptorUtils.javaTypeToDescriptor(CLASS.getName()), null);
+    inputConsumer.finished(null);
+    ProcessResult runInput = ToolHelper.runJava(input, CLASS.getCanonicalName());
+    if (runInput.exitCode != 0) {
+      System.out.println(runInput);
+    }
+    Assert.assertEquals(0, runInput.exitCode);
+
+    R8.run(
+        R8Command.builder()
+            .setMode(CompilationMode.DEBUG)
+            .addClassProgramData(clazz, Origin.unknown())
+            .addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()))
+            .setProgramConsumer(new ArchiveConsumer(output))
+            .build());
+    ProcessResult runOutput = ToolHelper.runJava(output, CLASS.getCanonicalName());
+    if (runOutput.exitCode != 0) {
+      System.out.println(runOutput);
+    }
+    Assert.assertEquals(runInput.toString(), runOutput.toString());
+  }
+}