Add CF frontend for testing

- Add InternalOptions.enableCfFrontend to enable CF frontend for testing
- Add InternalOptions.skipIR to skip IRConverter for testing
- Add CfFrontendExamplesTest to test CF frontend without involving IR
- Add CfExtended to handle SWAP and DUP[2][_X1|_X2]
- Add CfIinc to handle IINC
- Add CfTryCatch constructor accepting lists of guards and targets
- Add InternalOptions.DETERMINISTIC_DEBUGGING for testing

Change-Id: Iae03784aabb3f7b48da712902dfb122185c4a4c3
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 181ebac..8763e64 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfExtended;
 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;
@@ -22,6 +23,7 @@
 import com.android.tools.r8.cf.code.CfGoto;
 import com.android.tools.r8.cf.code.CfIf;
 import com.android.tools.r8.cf.code.CfIfCmp;
+import com.android.tools.r8.cf.code.CfIinc;
 import com.android.tools.r8.cf.code.CfInstanceOf;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
@@ -152,6 +154,13 @@
     print("nop");
   }
 
+  public void print(CfExtended instruction) {
+    switch (instruction.getOpcode()) {
+      default:
+        print("???");
+    }
+  }
+
   public void print(CfThrow insn) {
     print("athrow");
   }
@@ -401,6 +410,15 @@
     printPrefixed(store.getType(), "store", store.getLocalIndex());
   }
 
+  public void print(CfIinc instruction) {
+    indent();
+    builder
+        .append("iinc ")
+        .append(instruction.getLocalIndex())
+        .append(' ')
+        .append(instruction.getIncrement());
+  }
+
   private void printPrefixed(ValueType type, String instruction, int local) {
     indent();
     builder.append(typePrefix(type)).append(instruction).append(' ').append(local);
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfExtended.java b/src/main/java/com/android/tools/r8/cf/code/CfExtended.java
new file mode 100644
index 0000000..c75f0e3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfExtended.java
@@ -0,0 +1,31 @@
+// 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.code;
+
+import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.naming.NamingLens;
+import org.objectweb.asm.MethodVisitor;
+
+public class CfExtended extends CfInstruction {
+
+  private final int opcode;
+
+  public CfExtended(int opcode) {
+    this.opcode = opcode;
+  }
+
+  @Override
+  public void write(MethodVisitor visitor, NamingLens lens) {
+    visitor.visitInsn(opcode);
+  }
+
+  @Override
+  public void print(CfPrinter printer) {
+    printer.print(this);
+  }
+
+  public int getOpcode() {
+    return opcode;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
new file mode 100644
index 0000000..6db7250
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
@@ -0,0 +1,37 @@
+// 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.code;
+
+import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.naming.NamingLens;
+import org.objectweb.asm.MethodVisitor;
+
+public class CfIinc extends CfInstruction {
+
+  private final int var;
+  private final int increment;
+
+  public CfIinc(int var, int increment) {
+    this.var = var;
+    this.increment = increment;
+  }
+
+  @Override
+  public void write(MethodVisitor visitor, NamingLens lens) {
+    visitor.visitIincInsn(var, increment);
+  }
+
+  @Override
+  public void print(CfPrinter printer) {
+    printer.print(this);
+  }
+
+  public int getLocalIndex() {
+    return var;
+  }
+
+  public int getIncrement() {
+    return increment;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
index a0d1f6a..6dceda0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
@@ -16,17 +16,23 @@
   public final List<DexType> guards;
   public final List<CfLabel> targets;
 
-  public CfTryCatch(
+  public CfTryCatch(CfLabel start, CfLabel end, List<DexType> guards, List<CfLabel> targets) {
+    this.start = start;
+    this.end = end;
+    this.guards = guards;
+    this.targets = targets;
+  }
+
+  public static CfTryCatch fromBuilder(
       CfLabel start,
       CfLabel end,
       CatchHandlers<BasicBlock> handlers,
       CfBuilder builder) {
-    this.start = start;
-    this.end = end;
-    guards = handlers.getGuards();
-    targets = new ArrayList<>(handlers.getAllTargets().size());
+    List<DexType> guards = handlers.getGuards();
+    ArrayList<CfLabel> targets = new ArrayList<>(handlers.getAllTargets().size());
     for (BasicBlock block : handlers.getAllTargets()) {
       targets.add(builder.getLabel(block));
     }
+    return new CfTryCatch(start, end, guards, targets);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 61cd5a7..ab1eca5 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -45,6 +45,11 @@
       this.start = start;
     }
 
+    public LocalVariableInfo(int index, DebugLocalInfo local, CfLabel start, CfLabel end) {
+      this(index, local, start);
+      setEnd(end);
+    }
+
     public void setEnd(CfLabel end) {
       assert this.end == null;
       assert end != null;
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 20949c3..b5a3fc2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableSet;
@@ -66,7 +67,9 @@
   // code, but this non-determinism exists even with the same order of classes since we
   // may process classes concurrently and fail-fast on the first error.
   private <T> boolean reorderClasses(List<T> classes) {
-    Collections.shuffle(classes);
+    if (!InternalOptions.DETERMINISTIC_DEBUGGING) {
+      Collections.shuffle(classes);
+    }
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 32cc10b..b18e916 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -3,14 +3,58 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
 import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
 import static org.objectweb.asm.Opcodes.ACC_DEPRECATED;
 import static org.objectweb.asm.Opcodes.ASM6;
 
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.cf.code.CfArrayLength;
+import com.android.tools.r8.cf.code.CfArrayLoad;
+import com.android.tools.r8.cf.code.CfArrayStore;
+import com.android.tools.r8.cf.code.CfBinop;
+import com.android.tools.r8.cf.code.CfCheckCast;
+import com.android.tools.r8.cf.code.CfConstClass;
+import com.android.tools.r8.cf.code.CfConstMethodHandle;
+import com.android.tools.r8.cf.code.CfConstMethodType;
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfExtended;
+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;
+import com.android.tools.r8.cf.code.CfIinc;
+import com.android.tools.r8.cf.code.CfInstanceOf;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfMonitor;
+import com.android.tools.r8.cf.code.CfMultiANewArray;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfNewArray;
+import com.android.tools.r8.cf.code.CfNop;
+import com.android.tools.r8.cf.code.CfPop;
+import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.cf.code.CfStore;
+import com.android.tools.r8.cf.code.CfSwitch;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.cf.code.CfUnop;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.CfCode.LocalVariableInfo;
 import com.android.tools.r8.graph.DexValue.DexValueAnnotation;
 import com.android.tools.r8.graph.DexValue.DexValueArray;
 import com.android.tools.r8.graph.DexValue.DexValueBoolean;
@@ -26,12 +70,22 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.DexValue.DexValueType;
 import com.android.tools.r8.graph.JarCode.ReparseContext;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.Monitor;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -40,8 +94,10 @@
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.TypePath;
 
@@ -64,8 +120,12 @@
 
   public void read(Origin origin, ClassKind classKind, InputStream input) throws IOException {
     ClassReader reader = new ClassReader(input);
-    reader.accept(new CreateDexClassVisitor(
-        origin, classKind, reader.b, application, classConsumer), SKIP_FRAMES);
+    int flags = SKIP_FRAMES;
+    if (application.options.enableCfFrontend) {
+      flags = EXPAND_FRAMES;
+    }
+    reader.accept(
+        new CreateDexClassVisitor(origin, classKind, reader.b, application, classConsumer), flags);
   }
 
   private static int cleanAccessFlags(int access) {
@@ -210,6 +270,9 @@
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String desc, String signature, String[] exceptions) {
+      if (application.options.enableCfFrontend) {
+        return new CfCreateMethodVisitor(access, name, desc, signature, exceptions, this);
+      }
       return new CreateMethodVisitor(access, name, desc, signature, exceptions, this);
     }
 
@@ -396,7 +459,7 @@
     private final int access;
     private final String name;
     private final String desc;
-    private final CreateDexClassVisitor parent;
+    final CreateDexClassVisitor parent;
     private final int parameterCount;
     private List<DexAnnotation> annotations = null;
     private DexValue defaultAnnotation = null;
@@ -404,6 +467,9 @@
     private List<List<DexAnnotation>> parameterAnnotations = null;
     private List<DexValue> parameterNames = null;
     private List<DexValue> parameterFlags = null;
+    final DexMethod method;
+    final MethodAccessFlags flags;
+    Code code = null;
 
     public CreateMethodVisitor(int access, String name, String desc, String signature,
         String[] exceptions, CreateDexClassVisitor parent) {
@@ -412,6 +478,8 @@
       this.name = name;
       this.desc = desc;
       this.parent = parent;
+      this.method = parent.application.getMethod(parent.type, name, desc);
+      this.flags = createMethodAccessFlags(name, access);
       parameterCount = JarApplicationReader.getArgumentCount(desc);
       if (exceptions != null && exceptions.length > 0) {
         DexValue[] values = new DexValue[exceptions.length];
@@ -511,15 +579,17 @@
     }
 
     @Override
-    public void visitEnd() {
-      DexMethod method = parent.application.getMethod(parent.type, name, desc);
-      MethodAccessFlags flags = createMethodAccessFlags(name, access);
-      Code code = null;
-      if (!flags.isAbstract()
-          && !flags.isNative()
-          && parent.classKind == ClassKind.PROGRAM) {
+    public void visitCode() {
+      assert !flags.isAbstract() && !flags.isNative();
+      if (parent.classKind == ClassKind.PROGRAM) {
         code = new JarCode(method, parent.origin, parent.context, parent.application);
       }
+    }
+
+    @Override
+    public void visitEnd() {
+      assert flags.isAbstract() || flags.isNative() || parent.classKind != ClassKind.PROGRAM
+          || code != null;
       DexAnnotationSetRefList parameterAnnotationSets;
       if (parameterAnnotations == null) {
         parameterAnnotationSets = DexAnnotationSetRefList.empty();
@@ -566,6 +636,651 @@
     }
   }
 
+  private static class CfCreateMethodVisitor extends CreateMethodVisitor {
+
+    private final JarApplicationReader application;
+    private final DexItemFactory factory;
+    private int maxStack;
+    private int maxLocals;
+    private List<CfInstruction> instructions;
+    private List<CfTryCatch> tryCatchRanges;
+    private List<LocalVariableInfo> localVariables;
+    private Map<Label, CfLabel> labelMap;
+
+    public CfCreateMethodVisitor(
+        int access,
+        String name,
+        String desc,
+        String signature,
+        String[] exceptions,
+        CreateDexClassVisitor parent) {
+      super(access, name, desc, signature, exceptions, parent);
+      this.application = parent.application;
+      this.factory = application.getFactory();
+    }
+
+    @Override
+    public void visitCode() {
+      assert !flags.isAbstract() && !flags.isNative();
+      maxStack = 0;
+      maxLocals = 0;
+      instructions = new ArrayList<>();
+      tryCatchRanges = new ArrayList<>();
+      localVariables = new ArrayList<>();
+      labelMap = new IdentityHashMap<>();
+    }
+
+    @Override
+    public void visitEnd() {
+      if (!flags.isAbstract() && !flags.isNative() && parent.classKind == ClassKind.PROGRAM) {
+        code =
+            new CfCode(method, maxStack, maxLocals, instructions, tryCatchRanges, localVariables);
+      }
+      super.visitEnd();
+    }
+
+    @Override
+    public void visitFrame(
+        int type, int nLocal, Object[] localTypes, int nStack, Object[] stackTypes) {
+      Int2ReferenceSortedMap<DexType> dexLocals = new Int2ReferenceAVLTreeMap<>();
+      Int2ReferenceSortedMap<Uninitialized> allocators = new Int2ReferenceAVLTreeMap<>();
+      parseLocals(nLocal, localTypes, dexLocals, allocators);
+      List<DexType> dexStack = parseStack(nStack, stackTypes);
+      instructions.add(new CfFrame(dexLocals, allocators, dexStack));
+    }
+
+    private void parseLocals(
+        int nLocal,
+        Object[] localTypes,
+        Int2ReferenceSortedMap<DexType> dexLocals,
+        Int2ReferenceSortedMap<Uninitialized> allocators) {
+      int i = 0;
+      for (int j = 0; j < nLocal; j++) {
+        Object localType = localTypes[j];
+        if (localType instanceof Label) {
+          CfLabel cfLabel = getLabel((Label) localType);
+          allocators.put(i++, new UninitializedNew(cfLabel));
+        } else if (localType == Opcodes.UNINITIALIZED_THIS) {
+          allocators.put(i++, new UninitializedThis());
+        } else {
+          DexType dexType = parseAsmType(localType);
+          dexLocals.put(i++, dexType);
+          if (dexType == factory.longType || dexType == factory.doubleType) {
+            i++;
+          }
+        }
+      }
+    }
+
+    private CfLabel getLabel(Label label) {
+      return labelMap.computeIfAbsent(label, l -> new CfLabel());
+    }
+
+    private List<DexType> parseStack(int nStack, Object[] stackTypes) {
+      List<DexType> dexStack = new ArrayList<>(nStack);
+      for (int i = 0; i < nStack; i++) {
+        Object stackType = stackTypes[i];
+        if (stackType instanceof Label || stackType == Opcodes.UNINITIALIZED_THIS) {
+          throw new Unimplemented("Uninitialized values not supported in stack slots");
+        }
+        DexType dexType = parseAsmType(stackType);
+        dexStack.add(dexType);
+      }
+      return dexStack;
+    }
+
+    private DexType parseAsmType(Object local) {
+      if (local == null || local == Opcodes.TOP) {
+        return null;
+      } else if (local == Opcodes.INTEGER) {
+        return factory.intType;
+      } else if (local == Opcodes.FLOAT) {
+        return factory.floatType;
+      } else if (local == Opcodes.LONG) {
+        return factory.longType;
+      } else if (local == Opcodes.DOUBLE) {
+        return factory.doubleType;
+      } else if (local == Opcodes.NULL) {
+        return DexItemFactory.nullValueType;
+      } else if (local instanceof String) {
+        return createTypeFromInternalType((String) local);
+      } else {
+        throw new Unreachable("Unexpected ASM type: " + local);
+      }
+    }
+
+    private DexType createTypeFromInternalType(String local) {
+      assert local.indexOf('.') == -1;
+      return factory.createType("L" + local + ";");
+    }
+
+    @Override
+    public void visitInsn(int opcode) {
+      switch (opcode) {
+        case Opcodes.NOP:
+          instructions.add(new CfNop());
+          break;
+        case Opcodes.ACONST_NULL:
+          instructions.add(new CfConstNull());
+          break;
+        case Opcodes.ICONST_M1:
+        case Opcodes.ICONST_0:
+        case Opcodes.ICONST_1:
+        case Opcodes.ICONST_2:
+        case Opcodes.ICONST_3:
+        case Opcodes.ICONST_4:
+        case Opcodes.ICONST_5:
+          instructions.add(new CfConstNumber(opcode - Opcodes.ICONST_0, ValueType.INT));
+          break;
+        case Opcodes.LCONST_0:
+        case Opcodes.LCONST_1:
+          instructions.add(new CfConstNumber(opcode - Opcodes.LCONST_0, ValueType.LONG));
+          break;
+        case Opcodes.FCONST_0:
+        case Opcodes.FCONST_1:
+        case Opcodes.FCONST_2:
+          instructions.add(
+              new CfConstNumber(
+                  Float.floatToRawIntBits(opcode - Opcodes.FCONST_0), ValueType.FLOAT));
+          break;
+        case Opcodes.DCONST_0:
+        case Opcodes.DCONST_1:
+          instructions.add(
+              new CfConstNumber(
+                  Double.doubleToRawLongBits(opcode - Opcodes.DCONST_0), ValueType.DOUBLE));
+          break;
+        case Opcodes.IALOAD:
+        case Opcodes.LALOAD:
+        case Opcodes.FALOAD:
+        case Opcodes.DALOAD:
+        case Opcodes.AALOAD:
+        case Opcodes.BALOAD:
+        case Opcodes.CALOAD:
+        case Opcodes.SALOAD:
+          instructions.add(new CfArrayLoad(getMemberTypeForOpcode(opcode)));
+          break;
+        case Opcodes.IASTORE:
+        case Opcodes.LASTORE:
+        case Opcodes.FASTORE:
+        case Opcodes.DASTORE:
+        case Opcodes.AASTORE:
+        case Opcodes.BASTORE:
+        case Opcodes.CASTORE:
+        case Opcodes.SASTORE:
+          instructions.add(new CfArrayStore(getMemberTypeForOpcode(opcode)));
+          break;
+        case Opcodes.POP:
+          instructions.add(new CfPop(ValueType.INT_OR_FLOAT_OR_NULL));
+          break;
+        case Opcodes.POP2:
+          instructions.add(new CfPop(ValueType.LONG_OR_DOUBLE));
+          break;
+        case Opcodes.DUP:
+        case Opcodes.DUP_X1:
+        case Opcodes.DUP_X2:
+        case Opcodes.DUP2:
+        case Opcodes.DUP2_X1:
+        case Opcodes.DUP2_X2:
+        case Opcodes.SWAP:
+          // TODO(mathiasr): Merge with POP/POP2; rename into CfStackInstruction with enum field.
+          instructions.add(new CfExtended(opcode));
+          break;
+        case Opcodes.IADD:
+        case Opcodes.LADD:
+        case Opcodes.FADD:
+        case Opcodes.DADD:
+        case Opcodes.ISUB:
+        case Opcodes.LSUB:
+        case Opcodes.FSUB:
+        case Opcodes.DSUB:
+        case Opcodes.IMUL:
+        case Opcodes.LMUL:
+        case Opcodes.FMUL:
+        case Opcodes.DMUL:
+        case Opcodes.IDIV:
+        case Opcodes.LDIV:
+        case Opcodes.FDIV:
+        case Opcodes.DDIV:
+        case Opcodes.IREM:
+        case Opcodes.LREM:
+        case Opcodes.FREM:
+        case Opcodes.DREM:
+          instructions.add(new CfBinop(opcode));
+          break;
+        case Opcodes.INEG:
+        case Opcodes.LNEG:
+        case Opcodes.FNEG:
+        case Opcodes.DNEG:
+          instructions.add(new CfUnop(opcode));
+          break;
+        case Opcodes.ISHL:
+        case Opcodes.LSHL:
+        case Opcodes.ISHR:
+        case Opcodes.LSHR:
+        case Opcodes.IUSHR:
+        case Opcodes.LUSHR:
+        case Opcodes.IAND:
+        case Opcodes.LAND:
+        case Opcodes.IOR:
+        case Opcodes.LOR:
+        case Opcodes.IXOR:
+        case Opcodes.LXOR:
+          instructions.add(new CfBinop(opcode));
+          break;
+        case Opcodes.I2L:
+        case Opcodes.I2F:
+        case Opcodes.I2D:
+        case Opcodes.L2I:
+        case Opcodes.L2F:
+        case Opcodes.L2D:
+        case Opcodes.F2I:
+        case Opcodes.F2L:
+        case Opcodes.F2D:
+        case Opcodes.D2I:
+        case Opcodes.D2L:
+        case Opcodes.D2F:
+        case Opcodes.I2B:
+        case Opcodes.I2C:
+        case Opcodes.I2S:
+          instructions.add(new CfUnop(opcode));
+          break;
+        case Opcodes.LCMP:
+        case Opcodes.FCMPL:
+        case Opcodes.FCMPG:
+        case Opcodes.DCMPL:
+        case Opcodes.DCMPG:
+          instructions.add(new CfBinop(opcode));
+          break;
+        case Opcodes.IRETURN:
+          instructions.add(new CfReturn(ValueType.INT));
+          break;
+        case Opcodes.LRETURN:
+          instructions.add(new CfReturn(ValueType.LONG));
+          break;
+        case Opcodes.FRETURN:
+          instructions.add(new CfReturn(ValueType.FLOAT));
+          break;
+        case Opcodes.DRETURN:
+          instructions.add(new CfReturn(ValueType.DOUBLE));
+          break;
+        case Opcodes.ARETURN:
+          instructions.add(new CfReturn(ValueType.OBJECT));
+          break;
+        case Opcodes.RETURN:
+          instructions.add(new CfReturnVoid());
+          break;
+        case Opcodes.ARRAYLENGTH:
+          instructions.add(new CfArrayLength());
+          break;
+        case Opcodes.ATHROW:
+          instructions.add(new CfThrow());
+          break;
+        case Opcodes.MONITORENTER:
+          instructions.add(new CfMonitor(Monitor.Type.ENTER));
+          break;
+        case Opcodes.MONITOREXIT:
+          instructions.add(new CfMonitor(Monitor.Type.EXIT));
+          break;
+        default:
+          throw new Unreachable("Unknown instruction");
+      }
+    }
+
+    private DexType opType(int opcode, DexItemFactory factory) {
+      switch (opcode) {
+        case Opcodes.IADD:
+        case Opcodes.ISUB:
+        case Opcodes.IMUL:
+        case Opcodes.IDIV:
+        case Opcodes.IREM:
+        case Opcodes.INEG:
+        case Opcodes.ISHL:
+        case Opcodes.ISHR:
+        case Opcodes.IUSHR:
+          return factory.intType;
+        case Opcodes.LADD:
+        case Opcodes.LSUB:
+        case Opcodes.LMUL:
+        case Opcodes.LDIV:
+        case Opcodes.LREM:
+        case Opcodes.LNEG:
+        case Opcodes.LSHL:
+        case Opcodes.LSHR:
+        case Opcodes.LUSHR:
+          return factory.longType;
+        case Opcodes.FADD:
+        case Opcodes.FSUB:
+        case Opcodes.FMUL:
+        case Opcodes.FDIV:
+        case Opcodes.FREM:
+        case Opcodes.FNEG:
+          return factory.floatType;
+        case Opcodes.DADD:
+        case Opcodes.DSUB:
+        case Opcodes.DMUL:
+        case Opcodes.DDIV:
+        case Opcodes.DREM:
+        case Opcodes.DNEG:
+          return factory.doubleType;
+        default:
+          throw new Unreachable("Unexpected opcode " + opcode);
+      }
+    }
+
+    private static MemberType getMemberTypeForOpcode(int opcode) {
+      switch (opcode) {
+        case Opcodes.IALOAD:
+        case Opcodes.IASTORE:
+          return MemberType.INT;
+        case Opcodes.FALOAD:
+        case Opcodes.FASTORE:
+          return MemberType.FLOAT;
+        case Opcodes.LALOAD:
+        case Opcodes.LASTORE:
+          return MemberType.LONG;
+        case Opcodes.DALOAD:
+        case Opcodes.DASTORE:
+          return MemberType.DOUBLE;
+        case Opcodes.AALOAD:
+        case Opcodes.AASTORE:
+          return MemberType.OBJECT;
+        case Opcodes.BALOAD:
+        case Opcodes.BASTORE:
+          return MemberType.BOOLEAN; // TODO: Distinguish byte and boolean.
+        case Opcodes.CALOAD:
+        case Opcodes.CASTORE:
+          return MemberType.CHAR;
+        case Opcodes.SALOAD:
+        case Opcodes.SASTORE:
+          return MemberType.SHORT;
+        default:
+          throw new Unreachable("Unexpected array opcode " + opcode);
+      }
+    }
+
+    @Override
+    public void visitIntInsn(int opcode, int operand) {
+      switch (opcode) {
+        case Opcodes.SIPUSH:
+        case Opcodes.BIPUSH:
+          instructions.add(new CfConstNumber(operand, ValueType.INT));
+          break;
+        case Opcodes.NEWARRAY:
+          instructions.add(
+              new CfNewArray(factory.createArrayType(1, arrayTypeDesc(operand, factory))));
+          break;
+        default:
+          throw new Unreachable("Unexpected int opcode " + opcode);
+      }
+    }
+
+    private static DexType arrayTypeDesc(int arrayTypeCode, DexItemFactory factory) {
+      switch (arrayTypeCode) {
+        case Opcodes.T_BOOLEAN:
+          return factory.booleanType;
+        case Opcodes.T_CHAR:
+          return factory.charType;
+        case Opcodes.T_FLOAT:
+          return factory.floatType;
+        case Opcodes.T_DOUBLE:
+          return factory.doubleType;
+        case Opcodes.T_BYTE:
+          return factory.byteType;
+        case Opcodes.T_SHORT:
+          return factory.shortType;
+        case Opcodes.T_INT:
+          return factory.intType;
+        case Opcodes.T_LONG:
+          return factory.longType;
+        default:
+          throw new Unreachable("Unexpected array-type code " + arrayTypeCode);
+      }
+    }
+
+    @Override
+    public void visitVarInsn(int opcode, int var) {
+      ValueType type;
+      switch (opcode) {
+        case Opcodes.ILOAD:
+        case Opcodes.ISTORE:
+          type = ValueType.INT;
+          break;
+        case Opcodes.FLOAD:
+        case Opcodes.FSTORE:
+          type = ValueType.FLOAT;
+          break;
+        case Opcodes.LLOAD:
+        case Opcodes.LSTORE:
+          type = ValueType.LONG;
+          break;
+        case Opcodes.DLOAD:
+        case Opcodes.DSTORE:
+          type = ValueType.DOUBLE;
+          break;
+        case Opcodes.ALOAD:
+        case Opcodes.ASTORE:
+          type = ValueType.OBJECT;
+          break;
+        case Opcodes.RET:
+          throw new Unreachable("RET should be handled by the ASM jsr inliner");
+        default:
+          throw new Unreachable("Unexpected VarInsn opcode: " + opcode);
+      }
+      if (Opcodes.ILOAD <= opcode && opcode <= Opcodes.ALOAD) {
+        instructions.add(new CfLoad(type, var));
+      } else {
+        instructions.add(new CfStore(type, var));
+      }
+    }
+
+    @Override
+    public void visitTypeInsn(int opcode, String typeName) {
+      DexType type = createTypeFromInternalType(typeName);
+      switch (opcode) {
+        case Opcodes.NEW:
+          instructions.add(new CfNew(type));
+          break;
+        case Opcodes.ANEWARRAY:
+          instructions.add(new CfNewArray(factory.createArrayType(1, type)));
+          break;
+        case Opcodes.CHECKCAST:
+          instructions.add(new CfCheckCast(type));
+          break;
+        case Opcodes.INSTANCEOF:
+          instructions.add(new CfInstanceOf(type));
+          break;
+        default:
+          throw new Unreachable("Unexpected TypeInsn opcode: " + opcode);
+      }
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      DexField field =
+          factory.createField(createTypeFromInternalType(owner), factory.createType(desc), name);
+      // TODO(mathiasr): Don't require CfFieldInstruction::declaringField. It is needed for proper
+      // renaming in the backend, but it is not available here in the frontend.
+      instructions.add(new CfFieldInstruction(opcode, field, field));
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+      visitMethodInsn(opcode, owner, name, desc, false);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      DexMethod method = application.getMethod(owner, name, desc);
+      instructions.add(new CfInvoke(opcode, method, itf));
+    }
+
+    @Override
+    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+      DexCallSite callSite =
+          DexCallSite.fromAsmInvokeDynamic(application, parent.type, name, desc, bsm, bsmArgs);
+      instructions.add(new CfInvokeDynamic(callSite));
+    }
+
+    @Override
+    public void visitJumpInsn(int opcode, Label label) {
+      CfLabel target = getLabel(label);
+      if (Opcodes.IFEQ <= opcode && opcode <= Opcodes.IF_ACMPNE) {
+        if (opcode <= Opcodes.IFLE) {
+          // IFEQ, IFNE, IFLT, IFGE, IFGT, or IFLE.
+          instructions.add(new CfIf(ifType(opcode), ValueType.INT, target));
+        } else {
+          // IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, or
+          // IF_ACMPNE.
+          ValueType valueType;
+          if (opcode <= Opcodes.IF_ICMPLE) {
+            valueType = ValueType.INT;
+          } else {
+            valueType = ValueType.OBJECT;
+          }
+          instructions.add(new CfIfCmp(ifType(opcode), valueType, target));
+        }
+      } else {
+        // GOTO, JSR, IFNULL or IFNONNULL.
+        switch (opcode) {
+          case Opcodes.GOTO:
+            instructions.add(new CfGoto(target));
+            break;
+          case Opcodes.IFNULL:
+          case Opcodes.IFNONNULL:
+            If.Type type = opcode == Opcodes.IFNULL ? If.Type.EQ : If.Type.NE;
+            instructions.add(new CfIf(type, ValueType.OBJECT, target));
+            break;
+          case Opcodes.JSR:
+            throw new Unreachable("JSR should be handled by the ASM jsr inliner");
+          default:
+            throw new Unreachable("Unexpected JumpInsn opcode: " + opcode);
+        }
+      }
+    }
+
+    private static If.Type ifType(int opcode) {
+      switch (opcode) {
+        case Opcodes.IFEQ:
+        case Opcodes.IF_ICMPEQ:
+        case Opcodes.IF_ACMPEQ:
+          return If.Type.EQ;
+        case Opcodes.IFNE:
+        case Opcodes.IF_ICMPNE:
+        case Opcodes.IF_ACMPNE:
+          return If.Type.NE;
+        case Opcodes.IFLT:
+        case Opcodes.IF_ICMPLT:
+          return If.Type.LT;
+        case Opcodes.IFGE:
+        case Opcodes.IF_ICMPGE:
+          return If.Type.GE;
+        case Opcodes.IFGT:
+        case Opcodes.IF_ICMPGT:
+          return If.Type.GT;
+        case Opcodes.IFLE:
+        case Opcodes.IF_ICMPLE:
+          return If.Type.LE;
+        default:
+          throw new Unreachable("Unexpected If instruction opcode: " + opcode);
+      }
+    }
+
+    @Override
+    public void visitLabel(Label label) {
+      instructions.add(getLabel(label));
+    }
+
+    @Override
+    public void visitLdcInsn(Object cst) {
+      if (cst instanceof Type) {
+        Type type = (Type) cst;
+        if (type.getSort() == Type.METHOD) {
+          DexProto proto = application.getProto(type.getDescriptor());
+          instructions.add(new CfConstMethodType(proto));
+        } else {
+          instructions.add(new CfConstClass(factory.createType(type.getDescriptor())));
+        }
+      } else if (cst instanceof String) {
+        instructions.add(new CfConstString(factory.createString((String) cst)));
+      } else if (cst instanceof Long) {
+        instructions.add(new CfConstNumber((Long) cst, ValueType.LONG));
+      } else if (cst instanceof Double) {
+        long l = Double.doubleToRawLongBits((Double) cst);
+        instructions.add(new CfConstNumber(l, ValueType.DOUBLE));
+      } else if (cst instanceof Integer) {
+        instructions.add(new CfConstNumber((Integer) cst, ValueType.INT));
+      } else if (cst instanceof Float) {
+        long i = Float.floatToRawIntBits((Float) cst);
+        instructions.add(new CfConstNumber(i, ValueType.FLOAT));
+      } else if (cst instanceof Handle) {
+        instructions.add(
+            new CfConstMethodHandle(
+                DexMethodHandle.fromAsmHandle((Handle) cst, application, parent.type)));
+      } else {
+        throw new CompilationError("Unsupported constant: " + cst.toString());
+      }
+    }
+
+    @Override
+    public void visitIincInsn(int var, int increment) {
+      instructions.add(new CfIinc(var, increment));
+    }
+
+    @Override
+    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
+      // TODO(mathiasr): Support emitting table switches in CfSwitch.
+      throw new Unimplemented("Table switches not supported in CF backend");
+    }
+
+    @Override
+    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+      ArrayList<CfLabel> targets = new ArrayList<>(labels.length);
+      for (Label label : labels) {
+        targets.add(getLabel(label));
+      }
+      instructions.add(new CfSwitch(getLabel(dflt), keys, targets));
+    }
+
+    @Override
+    public void visitMultiANewArrayInsn(String desc, int dims) {
+      instructions.add(new CfMultiANewArray(factory.createType(desc), dims));
+    }
+
+    @Override
+    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+      List<DexType> guards =
+          Collections.singletonList(
+              type == null ? DexItemFactory.catchAllType : createTypeFromInternalType(type));
+      List<CfLabel> targets = Collections.singletonList(getLabel(handler));
+      tryCatchRanges.add(new CfTryCatch(getLabel(start), getLabel(end), guards, targets));
+    }
+
+    @Override
+    public void visitLocalVariable(
+        String name, String desc, String signature, Label start, Label end, int index) {
+      DebugLocalInfo debugLocalInfo =
+          new DebugLocalInfo(
+              factory.createString(name),
+              factory.createType(desc),
+              signature == null ? null : factory.createString(signature));
+      localVariables.add(
+          new LocalVariableInfo(index, debugLocalInfo, getLabel(start), getLabel(end)));
+    }
+
+    @Override
+    public void visitLineNumber(int line, Label start) {
+      instructions.add(new CfPosition(getLabel(start), new Position(line, null, method, null)));
+    }
+
+    @Override
+    public void visitMaxs(int maxStack, int maxLocals) {
+      assert maxStack >= 0;
+      assert maxLocals >= 0;
+      this.maxStack = maxStack;
+      this.maxLocals = maxLocals;
+    }
+  }
+
   private static class CreateAnnotationVisitor extends AnnotationVisitor {
 
     private final JarApplicationReader application;
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 d945dea..6ba1d71 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
@@ -263,7 +263,8 @@
         if (!tryCatchHandlers.isEmpty()) {
           // Close try-catch and save the range.
           CfLabel tryCatchEnd = getLabel(block);
-          tryCatchRanges.add(new CfTryCatch(tryCatchStart, tryCatchEnd, tryCatchHandlers, this));
+          tryCatchRanges.add(
+              CfTryCatch.fromBuilder(tryCatchStart, tryCatchEnd, tryCatchHandlers, this));
           emitLabel(tryCatchEnd);
         }
         if (!handlers.isEmpty()) {
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 2b6ad47..f44bc25 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
@@ -568,6 +568,10 @@
       Log.debug(getClass(), "Original code for %s:\n%s",
           method.toSourceString(), logCode(options, method));
     }
+    if (options.skipIR) {
+      feedback.markProcessed(method, Constraint.NEVER);
+      return;
+    }
     IRCode code = method.buildIR(options);
     if (code == null) {
       feedback.markProcessed(method, Constraint.NEVER);
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9fd675e..c5ba5cc 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -30,6 +30,10 @@
 
 public class InternalOptions {
 
+  // Set to true to run compilation in a single thread and without randomly shuffling the input.
+  // This makes life easier when running R8 in a debugger.
+  public static final boolean DETERMINISTIC_DEBUGGING = false;
+
   public enum LineNumberOptimization {
     OFF,
     ON,
@@ -94,7 +98,7 @@
   public boolean enableValuePropagation = true;
 
   // Number of threads to use while processing the dex files.
-  public int numberOfThreads = ThreadUtils.NOT_SPECIFIED;
+  public int numberOfThreads = DETERMINISTIC_DEBUGGING ? 1 : ThreadUtils.NOT_SPECIFIED;
   // Print smali disassembly.
   public boolean useSmaliSyntax = false;
   // Verbose output.
@@ -189,6 +193,10 @@
   public boolean enableMinification = true;
   public boolean disableAssertions = true;
   public boolean debugKeepRules = false;
+  // Read input classes into CfCode format (instead of JarCode).
+  public boolean enableCfFrontend = false;
+  // Don't convert Code objects to IRCode.
+  public boolean skipIR = false;
 
   public boolean debug = false;
   public final TestingOptions testing = new TestingOptions();
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
new file mode 100644
index 0000000..ece865e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -0,0 +1,244 @@
+// 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;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+import static com.google.common.io.ByteStreams.toByteArray;
+import static org.junit.Assert.assertEquals;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.util.ASMifier;
+import org.objectweb.asm.util.TraceClassVisitor;
+
+@RunWith(Parameterized.class)
+public class CfFrontendExamplesTest extends TestBase {
+
+  static final Collection<Object[]> TESTS = Arrays.asList(
+    makeTest("arithmetic.Arithmetic"),
+    makeTest("arrayaccess.ArrayAccess"),
+    makeTest("barray.BArray"),
+    makeTest("bridge.BridgeMethod"),
+    makeTest("cse.CommonSubexpressionElimination"),
+    makeTest("constants.Constants"),
+    makeTest("controlflow.ControlFlow"),
+    makeTest("conversions.Conversions"),
+    makeTest("floating_point_annotations.FloatingPointValuedAnnotationTest"),
+    makeTest("filledarray.FilledArray"),
+    makeTest("hello.Hello"),
+    makeTest("ifstatements.IfStatements"),
+    makeTest("instancevariable.InstanceVariable"),
+    makeTest("instanceofstring.InstanceofString"),
+    makeTest("invoke.Invoke"),
+    makeTest("jumbostring.JumboString"),
+    makeTest("loadconst.LoadConst"),
+    makeTest("loop.UdpServer"),
+    makeTest("newarray.NewArray"),
+    makeTest("regalloc.RegAlloc"),
+    makeTest("returns.Returns"),
+    makeTest("staticfield.StaticField"),
+    makeTest("stringbuilding.StringBuilding"),
+    makeTest("throwing.Throwing"),
+    makeTest("trivial.Trivial"),
+    makeTest("trycatch.TryCatch"),
+    makeTest("nestedtrycatches.NestedTryCatches"),
+    makeTest("trycatchmany.TryCatchMany"),
+    makeTest("invokeempty.InvokeEmpty"),
+    makeTest("regress.Regress"),
+    makeTest("regress2.Regress2"),
+    makeTest("regress_37726195.Regress"),
+    makeTest("regress_37658666.Regress", CfFrontendExamplesTest::compareRegress37658666),
+    makeTest("regress_37875803.Regress"),
+    makeTest("regress_37955340.Regress"),
+    // TODO(mathiasr): This test fails because we remove ASM's java.lang.Synthetic annotations.
+    // makeTest("regress_62300145.Regress"),
+    makeTest("regress_64881691.Regress"),
+    makeTest("regress_65104300.Regress"),
+    makeTest("regress_70703087.Test"),
+    makeTest("regress_70736958.Test"),
+    // TODO(mathiasr): Support uninitialized values in stack slots in CfFrame.
+    // makeTest("regress_70737019.Test"),
+    makeTest("regress_72361252.Test"),
+    makeTest("memberrebinding3.Memberrebinding"),
+    makeTest("minification.Minification"),
+    makeTest("enclosingmethod.Main"),
+    makeTest("enclosingmethod_proguarded.Main"),
+    makeTest("interfaceinlining.Main")
+    // TODO(mathiasr): These fail because we add a zero initializer to an field with no initializer.
+    // makeTest("sync.Sync"),
+    // makeTest("memberrebinding2.Memberrebinding"),
+    // TODO(mathiasr): Support emitting table switches in CfSwitch.
+    // makeTest("switches.Switches"),
+    // makeTest("switchmaps.Switches"),
+  );
+
+  private static Object[] makeTest(String className) {
+    return makeTest(className, null);
+  }
+
+  private static Object[] makeTest(String className, BiConsumer<byte[], byte[]> comparator) {
+    return new Object[] {className, comparator};
+  }
+
+  @Parameters(name = "{0}")
+  public static Collection<Object[]> data() {
+    return TESTS;
+  }
+
+  private static void compareRegress37658666(byte[] expectedBytes, byte[] actualBytes) {
+    // javac emits LDC(-0.0f) instead of the shorter FCONST_0 FNEG emitted by CfConstNumber.
+    String ldc = "mv.visitLdcInsn(new Float(\"-0.0\"));";
+    String constNeg = "mv.visitInsn(FCONST_0);\nmv.visitInsn(FNEG);";
+    assertEquals(
+        asmToString(expectedBytes).replace(ldc, constNeg),
+        asmToString(actualBytes));
+  }
+
+  private final Path inputJar;
+  private final BiConsumer<byte[], byte[]> comparator;
+
+  public CfFrontendExamplesTest(String clazz, BiConsumer<byte[], byte[]> comparator) {
+    this.comparator = comparator;
+    String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
+    String suffix = "_debuginfo_all";
+    inputJar = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, pkg + suffix + JAR_EXTENSION);
+  }
+
+  @Test
+  public void test() throws Exception {
+    Path outputJar = temp.getRoot().toPath().resolve("output.jar");
+    R8Command command =
+        R8Command.builder()
+            .addProgramFiles(inputJar)
+            .setMode(CompilationMode.DEBUG)
+            .setOutput(outputJar, OutputMode.ClassFile)
+            .build();
+    ToolHelper.runR8(
+        command,
+        options -> {
+          options.skipIR = true;
+          options.enableCfFrontend = true;
+        });
+    ArchiveClassFileProvider expected = new ArchiveClassFileProvider(inputJar);
+    ArchiveClassFileProvider actual = new ArchiveClassFileProvider(outputJar);
+    assertEquals(getSortedDescriptorList(expected), getSortedDescriptorList(actual));
+    for (String descriptor : expected.getClassDescriptors()) {
+      byte[] expectedBytes = getClassAsBytes(expected, descriptor);
+      byte[] actualBytes = getClassAsBytes(actual, descriptor);
+      if (comparator != null) {
+        comparator.accept(expectedBytes, actualBytes);
+      } else if (!Arrays.equals(expectedBytes, actualBytes)) {
+        assertEquals(
+            "Class " + descriptor + " differs",
+            asmToString(expectedBytes),
+            asmToString(actualBytes));
+      }
+    }
+  }
+
+  private static List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) {
+    ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors());
+    Collections.sort(descriptorList);
+    return descriptorList;
+  }
+
+  private static byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
+      throws Exception {
+    return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
+  }
+
+  private static String asmToString(byte[] clazz) {
+    StringWriter stringWriter = new StringWriter();
+    printAsm(new PrintWriter(stringWriter), clazz);
+    return stringWriter.toString();
+  }
+
+  private static void printAsm(PrintWriter pw, byte[] clazz) {
+    new ClassReader(clazz).accept(new TraceClassVisitor(null, new ASMifierSorted(), pw), 0);
+  }
+
+  /** Sort methods and fields in the output of ASMifier to make diffing possible. */
+  private static class ASMifierSorted extends ASMifier {
+    private static class Part implements Comparable<Part> {
+
+      private final String key;
+      private final int start;
+      private final int end;
+
+      Part(String key, int start, int end) {
+        this.key = key;
+        this.start = start;
+        this.end = end;
+      }
+
+      @Override
+      public int compareTo(Part part) {
+        int i = key.compareTo(part.key);
+        return i != 0 ? i : Integer.compare(start, part.start);
+      }
+    }
+
+    private final List<Part> parts = new ArrayList<>();
+
+    ASMifierSorted() {
+      super(Opcodes.ASM6, "cw", 0);
+    }
+
+    @Override
+    public ASMifier visitField(
+        int access, String name, String desc, String signature, Object value) {
+      init();
+      int i = text.size();
+      ASMifier res = super.visitField(access, name, desc, signature, value);
+      parts.add(new Part((String) text.get(i), i, text.size()));
+      return res;
+    }
+
+    @Override
+    public ASMifier visitMethod(
+        int access, String name, String desc, String signature, String[] exceptions) {
+      init();
+      int i = text.size();
+      ASMifier res = super.visitMethod(access, name, desc, signature, exceptions);
+      parts.add(new Part((String) text.get(i), i, text.size()));
+      return res;
+    }
+
+    private void init() {
+      if (parts.isEmpty()) {
+        parts.add(new Part("", 0, text.size()));
+      }
+    }
+
+    @Override
+    public void print(PrintWriter pw) {
+      init();
+      int end = parts.get(parts.size() - 1).end;
+      Collections.sort(parts);
+      parts.add(new Part("", end, text.size()));
+      ArrayList<Object> tmp = new ArrayList<>(text);
+      text.clear();
+      for (Part part : parts) {
+        for (int i = part.start; i < part.end; i++) {
+          text.add(tmp.get(i));
+        }
+      }
+      super.print(pw);
+    }
+  }
+}