Add test and internal testing option for cf pass through mode

Bug: 154201250
Bug: 152753721
Change-Id: If92521a0821cd08d8fa8ab6378afa122c8c16943
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 484c3e1..7999ea7 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -434,4 +434,9 @@
         ? OptionalBool.of(appInfo().withLiveness().isSubtype(subtype, supertype))
         : OptionalBool.unknown();
   }
+
+  public boolean isCfByteCodePassThrough(DexEncodedMethod method) {
+    return options.testing.cfByteCodePassThrough != null
+        && options.testing.cfByteCodePassThrough.test(method);
+  }
 }
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 b68b839..7a21ff1 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -183,7 +183,7 @@
     // Don't add parameter information if the code already has full debug information.
     // Note: This fast path can cause a method to loose its parameter info, if the debug info turned
     // out to be invalid during IR building.
-    if (appView.options().debug) {
+    if (appView.options().debug || appView.isCfByteCodePassThrough(method)) {
       return false;
     }
     assert localVariables.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 31fd3c0..acb21d7 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
@@ -1585,6 +1585,11 @@
       timing.end();
     }
 
+    if (appView.isCfByteCodePassThrough(method)) {
+      // If the code is pass trough, do not finalize by overwriting the existing code.
+      return timing;
+    }
+
     printMethod(code, "Optimized IR (SSA)", previous);
     timing.begin("Finalize IR");
     finalizeIR(code, feedback, timing);
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 62aa29a..e4b20c6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -73,6 +73,7 @@
 import java.util.function.BiConsumer;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import org.objectweb.asm.Opcodes;
 
@@ -1186,6 +1187,8 @@
     }
 
     public Consumer<ProgramMethod> callSiteOptimizationInfoInspector = null;
+
+    public Predicate<DexEncodedMethod> cfByteCodePassThrough = null;
   }
 
   @VisibleForTesting
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 8cdce08..d62402b 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -338,7 +338,9 @@
                 optimizeDexCodePositions(
                     method, appView, kotlinRemapper, mappedPositions, identityMapping);
               }
-            } else if (code.isCfCode() && doesContainPositions(code.asCfCode())) {
+            } else if (code.isCfCode()
+                && doesContainPositions(code.asCfCode())
+                && !appView.isCfByteCodePassThrough(method)) {
               optimizeCfCodePositions(method, kotlinRemapper, mappedPositions, appView);
             }
           }
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
index d881974..2ae20c9 100644
--- a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -352,18 +352,18 @@
     return descriptorList;
   }
 
-  private static byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
+  public static byte[] getClassAsBytes(ClassFileResourceProvider inputJar, String descriptor)
       throws Exception {
     return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
   }
 
-  private static String asmToString(byte[] clazz) {
+  public 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) {
+  public static void printAsm(PrintWriter pw, byte[] clazz) {
     new ClassReader(clazz).accept(new TraceClassVisitor(null, new ASMifierSorted(), pw), 0);
   }
 
diff --git a/src/test/java/com/android/tools/r8/code/PassThroughTest.java b/src/test/java/com/android/tools/r8/code/PassThroughTest.java
new file mode 100644
index 0000000..bb60719
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/code/PassThroughTest.java
@@ -0,0 +1,160 @@
+// Copyright (c) 2020, 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.code;
+
+import static junit.framework.Assert.assertSame;
+
+import com.android.tools.r8.ArchiveClassFileProvider;
+import com.android.tools.r8.CfFrontendExamplesTest;
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.DirectoryClassFileProvider;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PassThroughTest extends TestBase {
+
+  private final String EXPECTED = StringUtils.lines("0", "foo", "0");
+
+  private final TestParameters parameters;
+  private final boolean keepDebug;
+
+  @Parameters(name = "{0}, keep-debug: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withCfRuntimes().build(), BooleanUtils.values());
+  }
+
+  public PassThroughTest(TestParameters parameters, boolean keepDebug) {
+    this.parameters = parameters;
+    this.keepDebug = keepDebug;
+  }
+
+  @Test
+  public void testJmv() throws Exception {
+    CodeInspector inspector =
+        testForJvm()
+            .addProgramClasses(Main.class)
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutput(EXPECTED)
+            .inspector();
+    // Check that reading the same input is actual matches.
+    ClassFileResourceProvider original =
+        DirectoryClassFileProvider.fromDirectory(ToolHelper.getClassPathForTests());
+    verifyInstructionsForMainMatchingExpectation(original, true, true);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Path outputJar = temp.newFile("output.jar").toPath();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .ifTrue(keepDebug, TestShrinkerBuilder::addKeepAllAttributes)
+        .compile()
+        .writeToZip(outputJar)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED);
+    verifyInstructionsForMainMatchingExpectation(
+        new ArchiveClassFileProvider(outputJar), keepDebug, false);
+  }
+
+  @Test
+  public void testR8ByteCodePassThrough() throws Exception {
+    Path outputJar = temp.newFile("output.jar").toPath();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .ifTrue(keepDebug, TestShrinkerBuilder::addKeepAllAttributes)
+        .addOptionsModification(
+            internalOptions ->
+                internalOptions.testing.cfByteCodePassThrough =
+                    method -> method.method.name.toString().equals("main"))
+        .compile()
+        .writeToZip(outputJar)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED);
+    verifyInstructionsForMainMatchingExpectation(
+        new ArchiveClassFileProvider(outputJar), keepDebug, true);
+  }
+
+  private void verifyInstructionsForMainMatchingExpectation(
+      ClassFileResourceProvider actual, boolean checkDebug, boolean expectation) throws Exception {
+    ClassFileResourceProvider original =
+        DirectoryClassFileProvider.fromDirectory(ToolHelper.getClassPathForTests());
+    String descriptor = DescriptorUtils.javaTypeToDescriptor(Main.class.getTypeName());
+    byte[] expectedBytes = CfFrontendExamplesTest.getClassAsBytes(original, descriptor);
+    byte[] actualBytes = CfFrontendExamplesTest.getClassAsBytes(actual, descriptor);
+    if (!Arrays.equals(expectedBytes, actualBytes)) {
+      String expectedString = CfFrontendExamplesTest.asmToString(expectedBytes);
+      String actualString = CfFrontendExamplesTest.asmToString(actualBytes);
+      verifyInstructionsForMainMatchingExpectation(
+          getMethodInstructions(expectedString),
+          getMethodInstructions(actualString),
+          checkDebug,
+          expectation);
+    }
+  }
+
+  private String getMethodInstructions(String asm) {
+    int methodIndexStart =
+        asm.indexOf(
+            "methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, \"main\","
+                + " \"([Ljava/lang/String;)V\", null, null);");
+    int methodIndexEnd = asm.indexOf("}", methodIndexStart);
+    return asm.substring(methodIndexStart, methodIndexEnd);
+  }
+
+  private void verifyInstructionsForMainMatchingExpectation(
+      String originalInstructions,
+      String actualInstructions,
+      boolean checkDebug,
+      boolean expectation) {
+    if (!checkDebug) {
+      originalInstructions =
+          StringUtils.splitLines(originalInstructions).stream()
+              .filter(this::isNotDebugInstruction)
+              .map(instr -> instr + StringUtils.LINE_SEPARATOR)
+              .collect(Collectors.joining());
+    }
+    assertSame(expectation, actualInstructions.equals(originalInstructions));
+  }
+
+  private boolean isNotDebugInstruction(String instruction) {
+    return !(instruction.startsWith("methodVisitor.visitLocalVariable")
+        || instruction.startsWith("methodVisitor.visitLabel")
+        || instruction.startsWith("Label")
+        || instruction.startsWith("methodVisitor.visitLineNumber"));
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      int i = 0;
+      System.out.println(i);
+      int j = 0;
+      String foo = "foo";
+      // Keep the false to have R8 remove it.
+      if (false) {
+        System.out.println(foo);
+      }
+      System.out.println(foo);
+      System.out.println(j);
+    }
+  }
+}