Support unused records

Bug: 199360923
Change-Id: Ic43632d34cd955ca428a6dd15f7e893e662cd7fa
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 4050961..08e18ca4 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -331,7 +331,6 @@
     // as there was a dex resource.
     private boolean hasReadProgramResourceFromCf = false;
     private boolean hasReadProgramResourceFromDex = false;
-    private boolean hasReadProgramRecord = false;
 
     ClassReader(ExecutorService executorService, List<Future<?>> futures) {
       this.executorService = executorService;
@@ -340,7 +339,9 @@
 
     public DexApplicationReadFlags getDexApplicationReadFlags() {
       return new DexApplicationReadFlags(
-          hasReadProgramResourceFromDex, hasReadProgramResourceFromCf, hasReadProgramRecord);
+          hasReadProgramResourceFromDex,
+          hasReadProgramResourceFromCf,
+          application.hasReadRecordReferenceFromProgramClass());
     }
 
     private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
@@ -382,15 +383,7 @@
       }
       hasReadProgramResourceFromCf = true;
       JarClassFileReader<DexProgramClass> reader =
-          new JarClassFileReader<>(
-              application,
-              clazz -> {
-                classes.add(clazz);
-                if (clazz.isRecord()) {
-                  hasReadProgramRecord = true;
-                }
-              },
-              PROGRAM);
+          new JarClassFileReader<>(application, classes::add, PROGRAM);
       // Read classes in parallel.
       for (ProgramResource input : classSources) {
         futures.add(
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
index d5089a7..bb3bcf8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
@@ -9,15 +9,15 @@
 
   private final boolean hasReadProgramClassFromDex;
   private final boolean hasReadProgramClassFromCf;
-  private final boolean hasReadProgramRecord;
+  private final boolean hasReadRecordReferenceFromProgramClass;
 
   public DexApplicationReadFlags(
       boolean hasReadProgramClassFromDex,
       boolean hasReadProgramClassFromCf,
-      boolean hasReadProgramRecord) {
+      boolean hasReadRecordReferenceFromProgramClass) {
     this.hasReadProgramClassFromDex = hasReadProgramClassFromDex;
     this.hasReadProgramClassFromCf = hasReadProgramClassFromCf;
-    this.hasReadProgramRecord = hasReadProgramRecord;
+    this.hasReadRecordReferenceFromProgramClass = hasReadRecordReferenceFromProgramClass;
   }
 
   public boolean hasReadProgramClassFromCf() {
@@ -28,7 +28,7 @@
     return hasReadProgramClassFromDex;
   }
 
-  public boolean hasReadProgramRecord() {
-    return hasReadProgramRecord;
+  public boolean hasReadRecordReferenceFromProgramClass() {
+    return hasReadRecordReferenceFromProgramClass;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
index 67e7b5b..047be58 100644
--- a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
+import com.android.tools.r8.ir.desugar.records.RecordRewriter;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
@@ -26,6 +27,8 @@
   private final ConcurrentHashMap<String, DexString> stringCache = new ConcurrentHashMap<>();
   private final Map<String, String> typeDescriptorMap;
 
+  private boolean hasReadRecordReferenceFromProgramClass = false;
+
   public JarApplicationReader(InternalOptions options) {
     this.options = options;
     typeDescriptorMap = ApplicationReaderMap.getDescriptorMap(options);
@@ -149,4 +152,24 @@
   public Type getReturnType(final String methodDescriptor) {
     return getAsmType(DescriptorUtils.getReturnTypeDescriptor(methodDescriptor));
   }
+
+  public void setHasReadRecordReferenceFromProgramClass() {
+    hasReadRecordReferenceFromProgramClass = true;
+  }
+
+  public boolean hasReadRecordReferenceFromProgramClass() {
+    return hasReadRecordReferenceFromProgramClass;
+  }
+
+  public void checkFieldForRecord(DexField dexField) {
+    if (options.shouldDesugarRecords() && RecordRewriter.refersToRecord(dexField, getFactory())) {
+      setHasReadRecordReferenceFromProgramClass();
+    }
+  }
+
+  public void checkMethodForRecord(DexMethod dexMethod) {
+    if (options.shouldDesugarRecords() && RecordRewriter.refersToRecord(dexMethod, getFactory())) {
+      setHasReadRecordReferenceFromProgramClass();
+    }
+  }
 }
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 4c6c165..296f792 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -512,9 +512,13 @@
     }
 
     private void checkRecord() {
+      if (!application.options.shouldDesugarRecords()) {
+        return;
+      }
       if (!accessFlags.isRecord()) {
         return;
       }
+      application.setHasReadRecordReferenceFromProgramClass();
       // TODO(b/169645628): Change this logic if we start stripping the record components.
       // Another approach would be to mark a bit in fields that are record components instead.
       String message = "Records are expected to have one record component per instance field.";
@@ -661,6 +665,7 @@
     public void visitEnd() {
       FieldAccessFlags flags = createFieldAccessFlags(access);
       DexField dexField = parent.application.getField(parent.type, name, desc);
+      parent.application.checkFieldForRecord(dexField);
       Wrapper<DexField> signature = FieldSignatureEquivalence.get().wrap(dexField);
       if (parent.fieldSignatures.add(signature)) {
         DexAnnotationSet annotationSet =
@@ -878,6 +883,7 @@
     @Override
     public void visitEnd() {
       InternalOptions options = parent.application.options;
+      parent.application.checkMethodForRecord(method);
       if (!flags.isAbstract() && !flags.isNative() && classRequiresCode()) {
         code = new LazyCfCode(method, parent.origin, parent.context, parent.application);
       }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
index 7fa0410..1f16c1b 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
@@ -137,21 +137,21 @@
     assert !instruction.isInitClass();
     if (instruction.isInvoke()) {
       CfInvoke cfInvoke = instruction.asInvoke();
-      if (refersToRecord(cfInvoke.getMethod())) {
+      if (refersToRecord(cfInvoke.getMethod(), factory)) {
         ensureRecordClass(eventConsumer);
       }
       return;
     }
     if (instruction.isFieldInstruction()) {
       CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
-      if (refersToRecord(fieldInstruction.getField())) {
+      if (refersToRecord(fieldInstruction.getField(), factory)) {
         ensureRecordClass(eventConsumer);
       }
       return;
     }
     if (instruction.isTypeInstruction()) {
       CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
-      if (refersToRecord(typeInstruction.getType())) {
+      if (refersToRecord(typeInstruction.getType(), factory)) {
         ensureRecordClass(eventConsumer);
       }
       return;
@@ -452,35 +452,35 @@
     }
   }
 
-  private boolean refersToRecord(DexField field) {
-    assert !refersToRecord(field.holder) : "The java.lang.Record class has no fields.";
-    return refersToRecord(field.type);
+  public static boolean refersToRecord(DexField field, DexItemFactory factory) {
+    assert !refersToRecord(field.holder, factory) : "The java.lang.Record class has no fields.";
+    return refersToRecord(field.type, factory);
   }
 
-  private boolean refersToRecord(DexMethod method) {
-    if (refersToRecord(method.holder)) {
+  public static boolean refersToRecord(DexMethod method, DexItemFactory factory) {
+    if (refersToRecord(method.holder, factory)) {
       return true;
     }
-    return refersToRecord(method.proto);
+    return refersToRecord(method.proto, factory);
   }
 
-  private boolean refersToRecord(DexProto proto) {
-    if (refersToRecord(proto.returnType)) {
+  private static boolean refersToRecord(DexProto proto, DexItemFactory factory) {
+    if (refersToRecord(proto.returnType, factory)) {
       return true;
     }
-    return refersToRecord(proto.parameters.values);
+    return refersToRecord(proto.parameters.values, factory);
   }
 
-  private boolean refersToRecord(DexType[] types) {
+  private static boolean refersToRecord(DexType[] types, DexItemFactory factory) {
     for (DexType type : types) {
-      if (refersToRecord(type)) {
+      if (refersToRecord(type, factory)) {
         return true;
       }
     }
     return false;
   }
 
-  private boolean refersToRecord(DexType type) {
+  private static boolean refersToRecord(DexType type, DexItemFactory factory) {
     return type == factory.recordType;
   }
 
@@ -600,7 +600,7 @@
 
   @Override
   public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
-    if (appView.appInfo().app().getFlags().hasReadProgramRecord()) {
+    if (appView.appInfo().app().getFlags().hasReadRecordReferenceFromProgramClass()) {
       ensureRecordClass(eventConsumer);
     }
   }
diff --git a/src/test/examplesJava16/records/UnusedRecordField.java b/src/test/examplesJava16/records/UnusedRecordField.java
new file mode 100644
index 0000000..7a21412
--- /dev/null
+++ b/src/test/examplesJava16/records/UnusedRecordField.java
@@ -0,0 +1,18 @@
+// 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 records;
+
+public class UnusedRecordField {
+
+  Record unusedInstanceField;
+
+  void printHello() {
+    System.out.println("Hello!");
+  }
+
+  public static void main(String[] args) {
+    new UnusedRecordField().printHello();
+  }
+}
diff --git a/src/test/examplesJava16/records/UnusedRecordMethod.java b/src/test/examplesJava16/records/UnusedRecordMethod.java
new file mode 100644
index 0000000..31a044f
--- /dev/null
+++ b/src/test/examplesJava16/records/UnusedRecordMethod.java
@@ -0,0 +1,20 @@
+// 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 records;
+
+public class UnusedRecordMethod {
+
+  Record unusedInstanceMethod(Record unused) {
+    return null;
+  }
+
+  void printHello() {
+    System.out.println("Hello!");
+  }
+
+  public static void main(String[] args) {
+    new UnusedRecordMethod().printHello();
+  }
+}
diff --git a/src/test/examplesJava16/records/UnusedRecordReflection.java b/src/test/examplesJava16/records/UnusedRecordReflection.java
new file mode 100644
index 0000000..f99cc52
--- /dev/null
+++ b/src/test/examplesJava16/records/UnusedRecordReflection.java
@@ -0,0 +1,42 @@
+// 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 records;
+
+import java.lang.reflect.Method;
+
+public class UnusedRecordReflection {
+
+  Record instanceField;
+
+  Record method(int i, Record unused, int j) {
+    return null;
+  }
+
+  Object reflectiveGetField() {
+    try {
+      return this.getClass().getDeclaredField("instanceField").get(this);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  Object reflectiveCallMethod() {
+    try {
+      for (Method declaredMethod : this.getClass().getDeclaredMethods()) {
+        if (declaredMethod.getName().equals("method")) {
+          return declaredMethod.invoke(this, 0, null, 1);
+        }
+      }
+      throw new RuntimeException("Unreachable");
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.println(new UnusedRecordReflection().reflectiveGetField());
+    System.out.println(new UnusedRecordReflection().reflectiveCallMethod());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java
new file mode 100644
index 0000000..bb1fa84
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2021, 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.desugar.records;
+
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordFieldTest extends TestBase {
+
+  private static final String RECORD_NAME = "UnusedRecordField";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("Hello!");
+
+  private final TestParameters parameters;
+
+  public UnusedRecordFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForR8(parameters.getBackend())
+          .addProgramClassFileData(PROGRAM_DATA)
+          .setMinApi(parameters.getApiLevel())
+          .addKeepRules("-keep class records.UnusedRecordField { *; }")
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .enableJVMPreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules("-keep class records.UnusedRecordField { *; }")
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java
new file mode 100644
index 0000000..9edf909
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2021, 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.desugar.records;
+
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordMethodTest extends TestBase {
+
+  private static final String RECORD_NAME = "UnusedRecordMethod";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("Hello!");
+
+  private final TestParameters parameters;
+
+  public UnusedRecordMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForR8(parameters.getBackend())
+          .addProgramClassFileData(PROGRAM_DATA)
+          .setMinApi(parameters.getApiLevel())
+          .addKeepRules("-keep class records.UnusedRecordMethod { *; }")
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .enableJVMPreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules("-keep class records.UnusedRecordMethod { *; }")
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java
new file mode 100644
index 0000000..a3dc24e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2021, 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.desugar.records;
+
+import static com.android.tools.r8.utils.InternalOptions.TestingOptions;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordReflectionTest extends TestBase {
+
+  private static final String RECORD_NAME = "UnusedRecordReflection";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("null", "null");
+
+  private final TestParameters parameters;
+
+  public UnusedRecordReflectionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk16).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk16())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForR8(parameters.getBackend())
+          .addProgramClassFileData(PROGRAM_DATA)
+          .setMinApi(parameters.getApiLevel())
+          .addKeepRules("-keep class records.UnusedRecordReflection { *; }")
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .enableJVMPreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules("-keep class records.UnusedRecordReflection { *; }")
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .compile()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}