Partial identity transformation from Java bytecode to Java bytecode.

This change minimally enables writing out Java bytecode from Java bytecode
inputs for a simple hello world program. Follow-up work will extend the support
to the full program structure and producing code via our IR.

Bug: 65390962
Change-Id: I978c7400f9a21323dc0c5e95fc55272300839878
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 5f43849..6c4b9bc 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -81,6 +81,8 @@
     if (outputPath == null) {
       return new IgnoreContentsOutputSink();
     } else {
+      // TODO(zerny): Calling getInternalOptions here is incorrect since any modifications by an
+      // options consumer will not be visible to the sink.
       return FileSystemOutputSink.create(outputPath, getInternalOptions());
     }
   }
diff --git a/src/main/java/com/android/tools/r8/OutputSink.java b/src/main/java/com/android/tools/r8/OutputSink.java
index 8ffb792..a73391a 100644
--- a/src/main/java/com/android/tools/r8/OutputSink.java
+++ b/src/main/java/com/android/tools/r8/OutputSink.java
@@ -32,7 +32,8 @@
    * gives the current file count.
    * <p>
    * Files are not necessarily generated in order and files might be written concurrently. However,
-   * for each fileId only one file is ever written.
+   * for each fileId only one file is ever written. If this method is called, the other writeDexFile
+   * and writeClassFile methods will not be called.
    */
   void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) throws IOException;
 
@@ -46,13 +47,28 @@
    * primaryClassName only one file is ever written.
    * <p>
    * This method is only invoked by D8 and only if compiling each class into its own dex file, e.g.,
-   * for incremental compilation. If this method is called, the other writeDexFile method will
-   * not be called.
+   * for incremental compilation. If this method is called, the other writeDexFile and
+   * writeClassFile methods will not be called.
    */
   void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
       throws IOException;
 
   /**
+   * Write a Java classfile that contains the class primaryClassName and its companion classes.
+   * <p>
+   * This is equivalent to writing out the file com/foo/bar/Test.class given a primaryClassName of
+   * com.foo.bar.Test.
+   * <p>
+   * There is no guaranteed order and files might be written concurrently. However, for each
+   * primaryClassName only one file is ever written.
+   * <p>
+   * This method is only invoked by R8 and only if compiling to Java bytecode. If this method is
+   * called, the other writeDexFile and writeClassFile methods will not be called.
+   */
+  void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
+      throws IOException;
+
+  /**
    * Provides the raw bytes that would be generated for the <code>-printusage</code> flag.
    * <p>
    * This method is only invoked by R8 and only if R8 is instructed to generate printusage
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 5255fc5..d9066bd 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.optimize.EnumOrdinalMapCollector;
 import com.android.tools.r8.ir.optimize.SwitchMapCollector;
+import com.android.tools.r8.jar.CfApplicationWriter;
 import com.android.tools.r8.naming.Minifier;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.SourceFileRewriter;
@@ -92,11 +93,14 @@
       throws ExecutionException, DexOverflowException {
     try {
       Marker marker = getMarker(options);
-      new ApplicationWriter(
-          application, options, marker, deadCode, namingLens, proguardSeedsData)
-          .write(outputSink, executorService);
+      if (options.outputClassFiles) {
+        new CfApplicationWriter(application, options).write(outputSink, executorService);
+      } else {
+        new ApplicationWriter(application, options, marker, deadCode, namingLens, proguardSeedsData)
+            .write(outputSink, executorService);
+      }
     } catch (IOException e) {
-      throw new RuntimeException("Cannot write dex application", e);
+      throw new RuntimeException("Cannot write application", e);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index b64053a..ad1af10 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -25,6 +25,7 @@
   private final ProgramResource.Kind originKind;
   private DexEncodedArray staticValues = SENTINEL_NOT_YET_COMPUTED;
   private final Collection<DexProgramClass> synthesizedFrom;
+  private int classFileVersion = -1;
 
   public DexProgramClass(
       DexType type,
@@ -296,4 +297,13 @@
   public DexProgramClass get() {
     return this;
   }
+
+  public void setClassFileVersion(int classFileVersion) {
+    this.classFileVersion = classFileVersion;
+  }
+
+  public int getClassFileVersion() {
+    assert classFileVersion != -1;
+    return classFileVersion;
+  }
 }
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 001480f..9742df7 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -101,6 +101,7 @@
     private final ReparseContext context = new ReparseContext();
 
     // DexClass data.
+    private int version;
     private DexType type;
     private DexAccessFlags accessFlags;
     private DexType superType;
@@ -174,6 +175,7 @@
     @Override
     public void visit(int version, int access, String name, String signature, String superName,
         String[] interfaces) {
+      this.version = version;
       accessFlags = createAccessFlags(access);
       // Unset the (in dex) non-existent ACC_SUPER flag on the class.
       assert Constants.ACC_SYNCHRONIZED == Opcodes.ACC_SUPER;
@@ -263,6 +265,9 @@
       if (classKind == ClassKind.PROGRAM) {
         context.owner = clazz.asProgramClass();
       }
+      if (clazz.isProgramClass()) {
+        clazz.asProgramClass().setClassFileVersion(version);
+      }
       classConsumer.accept(clazz);
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index 5988ae5..e842028 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -27,6 +27,11 @@
 
 public class JarCode extends Code {
 
+  // TODO(zerny): Write via the IR.
+  public void writeTo(MethodVisitor visitor) {
+    node.accept(visitor);
+  }
+
   public static class ReparseContext {
 
     // This will hold the content of the whole class. Once all the methods of the class are swapped
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 063bee1..dda8031 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
@@ -581,7 +581,23 @@
     }
 
     printMethod(code, "Optimized IR (SSA)");
+    finalizeIR(method, code, feedback);
+  }
 
+  private void finalizeIR(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    if (options.outputClassFiles) {
+      finalizeToCf(method, code, feedback);
+    } else {
+      finalizeToDex(method, code, feedback);
+    }
+  }
+
+  private void finalizeToCf(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    assert method.getCode().isJarCode();
+    // TODO(zerny): Actually convert IR back to Java bytecode.
+  }
+
+  private void finalizeToDex(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     // Perform register allocation.
     RegisterAllocator registerAllocator = performRegisterAllocation(code, method);
     method.setCode(code, registerAllocator, options);
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
new file mode 100644
index 0000000..4662cd0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.jar;
+
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+
+import com.android.tools.r8.OutputSink;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.DexAccessFlags;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.InternalOptions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import jdk.internal.org.objectweb.asm.Type;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+
+public class CfApplicationWriter {
+  private final DexApplication application;
+  private final InternalOptions options;
+
+  public CfApplicationWriter(DexApplication application, InternalOptions options) {
+    this.application = application;
+    this.options = options;
+  }
+
+  public void write(OutputSink outputSink, ExecutorService executor) throws IOException {
+    application.timing.begin("CfApplicationWriter.write");
+    try {
+      writeApplication(outputSink, executor);
+    } finally {
+      application.timing.end();
+    }
+  }
+
+  private void writeApplication(OutputSink outputSink, ExecutorService executor)
+      throws IOException {
+    for (DexProgramClass clazz : application.classes()) {
+      if (clazz.getSynthesizedFrom().isEmpty()) {
+        writeClass(clazz, outputSink);
+      } else {
+        throw new Unimplemented("No support for synthetics in the Java bytecode backend.");
+      }
+    }
+  }
+
+  private void writeClass(DexProgramClass clazz, OutputSink outputSink) throws IOException {
+    ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+    writer.visitSource(clazz.sourceFile.toString(), null);
+    int version = clazz.getClassFileVersion();
+    int access = classAndInterfaceAccessFlags(clazz.accessFlags);
+    String desc = clazz.type.toDescriptorString();
+    String name = internalName(clazz.type);
+    String signature = null; // TODO(zerny): Support generic signatures.
+    String superName =
+        clazz.type == options.itemFactory.objectType ? null : internalName(clazz.superType);
+    String[] interfaces = new String[clazz.interfaces.values.length];
+    for (int i = 0; i < clazz.interfaces.values.length; i++) {
+      interfaces[i] = internalName(clazz.interfaces.values[i]);
+    }
+    writer.visit(version, access, name, signature, superName, interfaces);
+    // TODO(zerny): Methods and fields.
+    for (DexEncodedMethod method : clazz.directMethods()) {
+      writeMethod(method, writer);
+    }
+    outputSink.writeClassFile(writer.toByteArray(), Collections.singleton(desc), desc);
+  }
+
+  private void writeMethod(DexEncodedMethod method, ClassWriter writer) {
+    int access = method.accessFlags.get();
+    String name = method.method.name.toString();
+    String desc = method.descriptor();
+    String signature = null; // TODO(zerny): Support generic signatures.
+    String[] exceptions = null;
+    MethodVisitor visitor = writer.visitMethod(access, name, desc, signature, exceptions);
+    method.getCode().asJarCode().writeTo(visitor);
+  }
+
+  private static int classAndInterfaceAccessFlags(DexAccessFlags accessFlags) {
+    // TODO(zerny): Refactor access flags to account for the union of both DEX and Java flags.
+    int access = accessFlags.get();
+    access |= ACC_SUPER;
+    return access;
+  }
+
+  private static String internalName(DexType type) {
+    return Type.getType(type.toDescriptorString()).getInternalName();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index b082d90..24f5c08 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -600,6 +600,10 @@
       return addProgramResources(Kind.CLASS, Resource.fromBytes(origin, data));
     }
 
+    public Builder addClassProgramData(Origin origin, byte[] data, Set<String> classDescriptors) {
+      return addProgramResources(Kind.CLASS, Resource.fromBytes(origin, data, classDescriptors));
+    }
+
     /**
      * Set dead-code data.
      */
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
index 9144284..a4f2236 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
@@ -4,7 +4,10 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.OutputSink;
+import com.android.tools.r8.Resource.Origin;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -13,6 +16,7 @@
   private final AndroidApp.Builder builder = AndroidApp.builder();
   private final TreeMap<String, DescriptorsWithContents> dexFilesWithPrimary = new TreeMap<>();
   private final TreeMap<Integer, DescriptorsWithContents> dexFilesWithId = new TreeMap<>();
+  private final List<DescriptorsWithContents> classFiles = new ArrayList<>();
   private boolean closed = false;
 
   public AndroidAppOutputSink(OutputSink forwardTo) {
@@ -26,6 +30,7 @@
   @Override
   public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId)
       throws IOException {
+    assert dexFilesWithPrimary.isEmpty() && classFiles.isEmpty();
     // Sort the files by id so that their order is deterministic. Some tests depend on this.
     dexFilesWithId.put(fileId, new DescriptorsWithContents(classDescriptors, contents));
     super.writeDexFile(contents, classDescriptors, fileId);
@@ -35,6 +40,7 @@
   public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors,
       String primaryClassName)
       throws IOException {
+    assert dexFilesWithId.isEmpty() && classFiles.isEmpty();
     // Sort the files by their name for good measure.
     dexFilesWithPrimary
         .put(primaryClassName, new DescriptorsWithContents(classDescriptors, contents));
@@ -42,6 +48,14 @@
   }
 
   @Override
+  public synchronized void writeClassFile(
+      byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException {
+    assert dexFilesWithPrimary.isEmpty() && dexFilesWithId.isEmpty();
+    classFiles.add(new DescriptorsWithContents(classDescriptors, contents));
+    super.writeClassFile(contents, classDescriptors, primaryClassName);
+  }
+
+  @Override
   public void writePrintUsedInformation(byte[] contents) throws IOException {
     builder.setDeadCode(contents);
     super.writePrintUsedInformation(contents);
@@ -68,9 +82,18 @@
   @Override
   public void close() throws IOException {
     assert !closed;
-    assert dexFilesWithId.isEmpty() || dexFilesWithPrimary.isEmpty();
-    dexFilesWithPrimary.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v));
-    dexFilesWithId.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors));
+    if (!dexFilesWithPrimary.isEmpty()) {
+      assert dexFilesWithId.isEmpty() && classFiles.isEmpty();
+      dexFilesWithPrimary.forEach(
+          (v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v));
+    } else if (!dexFilesWithId.isEmpty()) {
+      assert dexFilesWithPrimary.isEmpty() && classFiles.isEmpty();
+      dexFilesWithId.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors));
+    } else if (!classFiles.isEmpty()) {
+      assert dexFilesWithPrimary.isEmpty() && dexFilesWithId.isEmpty();
+      classFiles.forEach(
+          d -> builder.addClassProgramData(Origin.unknown(), d.contents, d.descriptors));
+    }
     closed = true;
     super.close();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
index 56e0ea1..6c1090a 100644
--- a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
+
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -43,7 +46,18 @@
   @Override
   public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
       throws IOException {
-    Path target = outputDirectory.resolve(getOutputFileName(primaryClassName));
+    writeFileFromDescriptor(contents, primaryClassName, DEX_EXTENSION);
+  }
+
+  @Override
+  public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
+      throws IOException {
+    writeFileFromDescriptor(contents, primaryClassName, CLASS_EXTENSION);
+  }
+
+  private void writeFileFromDescriptor(byte[] contents, String descriptor, String extension)
+      throws IOException {
+    Path target = outputDirectory.resolve(getOutputFileName(descriptor, extension));
     Files.createDirectories(target.getParent());
     writeToFile(target, null, contents);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
index c0aef0a..1ca0cf9 100644
--- a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
@@ -28,13 +28,13 @@
   }
 
   String getOutputFileName(int index) {
+    assert !options.outputClassFiles;
     return index == 0 ? "classes.dex" : ("classes" + (index + 1) + FileUtils.DEX_EXTENSION);
   }
 
-  String getOutputFileName(String classDescriptor) throws IOException {
+  String getOutputFileName(String classDescriptor, String extension) throws IOException {
     assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor);
-    return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor)
-        + FileUtils.DEX_EXTENSION;
+    return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + extension;
   }
 
 
diff --git a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
index 3ac0e2b..f176329 100644
--- a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
@@ -33,6 +33,12 @@
   }
 
   @Override
+  public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
+      throws IOException {
+    forwardTo.writeClassFile(contents, classDescriptors, primaryClassName);
+  }
+
+  @Override
   public void writePrintUsedInformation(byte[] contents) throws IOException {
     forwardTo.writePrintUsedInformation(contents);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
index 8dbf3bc..384c7d5 100644
--- a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
@@ -20,6 +20,12 @@
   }
 
   @Override
+  public void writeClassFile(
+      byte[] contents, Set<String> classDescriptors, String primaryClassName) {
+    // Intentionally left empty.
+  }
+
+  @Override
   public void writePrintUsedInformation(byte[] contents) {
     // Intentionally left empty.
   }
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 75f89be..a04e28f 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -51,6 +51,8 @@
 
   public boolean printTimes = false;
 
+  public boolean outputClassFiles = false;
+
   // Optimization-related flags. These should conform to -dontoptimize.
   public boolean skipDebugLineNumberOpt = false;
   public boolean skipClassMerging = true;
diff --git a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
index d290b56..c1ad6c93 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
+
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -31,7 +34,13 @@
   @Override
   public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
       throws IOException {
-    writeToZipFile(getOutputFileName(primaryClassName), contents);
+    writeToZipFile(getOutputFileName(primaryClassName, DEX_EXTENSION), contents);
+  }
+
+  @Override
+  public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
+      throws IOException {
+    writeToZipFile(getOutputFileName(primaryClassName, CLASS_EXTENSION), contents);
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index f1ebcbb..96d9e8b 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.TestCondition.match;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
@@ -42,6 +43,11 @@
     DX, JAVAC, JAVAC_ALL, JAVAC_NONE
   }
 
+  enum Output {
+    DEX,
+    CF
+  }
+
   private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
 
   // For local testing on a specific Art version(s) change this set. e.g. to
@@ -65,7 +71,7 @@
                   TestCondition.runtimes(Version.V6_0_1, Version.V5_1_1, Version.V4_4_4)))
           .build();
 
-  @Parameters(name = "{0}_{1}_{2}_{3}")
+  @Parameters(name = "{0}_{1}_{2}_{3}_{5}")
   public static Collection<String[]> data() {
     String[] tests = {
         "arithmetic.Arithmetic",
@@ -115,6 +121,10 @@
         "switchmaps.Switches",
     };
 
+    String[] javaBytecodeTests = {
+      "hello.Hello",
+    };
+
     List<String[]> fullTestList = new ArrayList<>(tests.length * 2);
     for (String test : tests) {
       fullTestList.add(makeTest(Input.JAVAC, CompilerUnderTest.D8, CompilationMode.DEBUG, test));
@@ -130,13 +140,26 @@
           test));
       fullTestList.add(makeTest(Input.DX, CompilerUnderTest.R8, CompilationMode.RELEASE, test));
     }
+    // TODO(zerny): Once all tests pass create the java tests in the main test loop.
+    for (String test : javaBytecodeTests) {
+      fullTestList.add(
+          makeTest(Input.JAVAC_ALL, CompilerUnderTest.R8, CompilationMode.DEBUG, test, Output.CF));
+      fullTestList.add(
+          makeTest(
+              Input.JAVAC_ALL, CompilerUnderTest.R8, CompilationMode.RELEASE, test, Output.CF));
+    }
     return fullTestList;
   }
 
   private static String[] makeTest(
       Input input, CompilerUnderTest compiler, CompilationMode mode, String clazz) {
+    return makeTest(input, compiler, mode, clazz, Output.DEX);
+  }
+
+  private static String[] makeTest(
+      Input input, CompilerUnderTest compiler, CompilationMode mode, String clazz, Output output) {
     String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
-    return new String[]{pkg, input.name(), compiler.name(), mode.name(), clazz};
+    return new String[] {pkg, input.name(), compiler.name(), mode.name(), clazz, output.name()};
   }
 
   @Rule
@@ -147,18 +170,21 @@
   private final CompilationMode mode;
   private final String pkg;
   private final String mainClass;
+  private final Output output;
 
   public R8RunExamplesTest(
       String pkg,
       String input,
       String compiler,
       String mode,
-      String mainClass) {
+      String mainClass,
+      String output) {
     this.pkg = pkg;
     this.input = Input.valueOf(input);
     this.compiler = CompilerUnderTest.valueOf(compiler);
     this.mode = CompilationMode.valueOf(mode);
     this.mainClass = mainClass;
+    this.output = Output.valueOf(output);
   }
 
   private Path getOutputFile() {
@@ -192,15 +218,19 @@
     return input == Input.DX ? DexTool.DX : DexTool.NONE;
   }
 
+  private Path getOutputPath() {
+    return temp.getRoot().toPath().resolve("out.jar");
+  }
+
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
   @Before
   public void compile()
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
-    Path out = temp.getRoot().toPath();
     switch (compiler) {
       case D8: {
+        assertTrue(output == Output.DEX);
         ToolHelper.runD8(D8Command.builder()
             .addProgramFiles(getInputFile())
             .setOutputPath(getOutputFile())
@@ -213,7 +243,8 @@
             .addProgramFiles(getInputFile())
             .setOutputPath(getOutputFile())
             .setMode(mode)
-            .build());
+            .build(),
+            options -> options.outputClassFiles = (output == Output.CF));
         break;
       }
       default:
@@ -236,6 +267,20 @@
       fail("JVM failed for: " + mainClass);
     }
 
+    if (output == Output.CF) {
+      ToolHelper.ProcessResult result =
+          ToolHelper.runJava(ImmutableList.of(generated.toString()), mainClass);
+      if (result.exitCode != 0) {
+        System.err.println(result.stderr);
+        fail("JVM failed on compiled output for: " + mainClass);
+      }
+      assertEquals(
+          "JavaC/JVM and " + compiler.name() + "/JVM output differ",
+          javaResult.stdout,
+          result.stdout);
+      return;
+    }
+
     DexVm vm = ToolHelper.getDexVm();
     TestCondition condition = failingRun.get(mainClass);
     if (condition != null && condition.test(getTool(), compiler, vm.getVersion(), mode)) {
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 663901f..9baed48 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -21,6 +21,7 @@
 import java.io.StringReader;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 public class JasminBuilder {
@@ -45,6 +46,10 @@
       return name + ".j";
     }
 
+    public String getDescriptor() {
+      return "L" + name + ";";
+    }
+
     public MethodSignature addVirtualMethod(
         String name,
         List<String> argumentTypes,
@@ -185,7 +190,8 @@
           return clazz.getSourceFile();
         }
       };
-      builder.addClassProgramData(origin, compile(clazz));
+      builder.addClassProgramData(
+          origin, compile(clazz), Collections.singleton(clazz.getDescriptor()));
     }
     return builder.build();
   }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index 6962f97..67decf2 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.CompilationException;
 import com.android.tools.r8.R8;
+import com.android.tools.r8.Resource;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.dex.ApplicationReader;
@@ -27,10 +28,13 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
 import jasmin.ClassFile;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -48,20 +52,37 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  public static String getPathFromDescriptor(String classDescriptor) {
+    assert classDescriptor.startsWith("L");
+    assert classDescriptor.endsWith(";");
+    return classDescriptor.substring(1, classDescriptor.length() - 1) + ".class";
+  }
+
   protected ProcessResult runOnJavaRaw(JasminBuilder builder, String main) throws Exception {
-    File out = temp.newFolder("classes");
-    for (ClassBuilder clazz : builder.getClasses()) {
-      ClassFile file = new ClassFile();
-      file.readJasmin(new StringReader(clazz.toString()), clazz.name, false);
-      Path path = out.toPath().resolve(clazz.name + ".class");
+    return runOnJavaRaw(builder.build(), main);
+  }
+
+  protected ProcessResult runOnJavaRaw(AndroidApp app, String main) throws Exception {
+    File out = temp.newFolder();
+    for (Resource clazz : app.getClassProgramResources()) {
+      assert clazz.getClassDescriptors().size() == 1;
+      String desc = clazz.getClassDescriptors().iterator().next();
+      Path path = out.toPath().resolve(getPathFromDescriptor(desc));
       Files.createDirectories(path.getParent());
-      file.write(new FileOutputStream(path.toFile()));
+      try (InputStream input = clazz.getStream();
+          OutputStream output = Files.newOutputStream(path)) {
+        ByteStreams.copy(input, output);
+      }
     }
     return ToolHelper.runJava(ImmutableList.of(out.getPath()), main);
   }
 
   protected String runOnJava(JasminBuilder builder, String main) throws Exception {
-    ProcessResult result = runOnJavaRaw(builder, main);
+    return runOnJava(builder.build(), main);
+  }
+
+  protected String runOnJava(AndroidApp app, String main) throws Exception {
+    ProcessResult result = runOnJavaRaw(app, main);
     if (result.exitCode != 0) {
       System.out.println("Std out:");
       System.out.println(result.stdout);