Fix Record compilation

- Remove irrelevant warnings
- Do not report invalid dependency edges

Bug: b/270927174
Change-Id: Ib9fb2ac55004eccb4e7d62e0b36a4bfd2c464606
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index d48cdca..de49aea 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,7 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
+import com.android.tools.r8.DesugarGraphConsumer;
+import com.android.tools.r8.origin.GlobalSyntheticOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexInfo;
@@ -171,11 +172,43 @@
     }
     DexClass definition = definitionFor(type);
     if (definition != null && !definition.isLibraryClass() && !dependent.isLibraryClass()) {
-      InterfaceMethodRewriter.reportDependencyEdge(dependent, definition, this);
+      reportDependencyEdge(dependent, definition);
     }
     return definition;
   }
 
+  public void reportDependencyEdge(DexClass dependent, DexClass dependency) {
+    assert !dependent.isLibraryClass();
+    assert !dependency.isLibraryClass();
+    DesugarGraphConsumer consumer = options().desugarGraphConsumer;
+    if (consumer == null) {
+      return;
+    }
+    Origin dependencyOrigin = dependency.getOrigin();
+    java.util.Collection<DexType> dependents =
+        getSyntheticItems().getSynthesizingContextTypes(dependent.getType());
+    if (dependents.isEmpty()) {
+      reportDependencyEdge(consumer, dependencyOrigin, dependent);
+    } else {
+      for (DexType type : dependents) {
+        reportDependencyEdge(consumer, dependencyOrigin, definitionFor(type));
+      }
+    }
+  }
+
+  private void reportDependencyEdge(
+      DesugarGraphConsumer consumer, Origin dependencyOrigin, DexClass clazz) {
+    Origin dependentOrigin = clazz.getOrigin();
+    if (dependencyOrigin == GlobalSyntheticOrigin.instance()
+        || dependentOrigin == GlobalSyntheticOrigin.instance()) {
+      // D8/R8 does not report edges to synthetic classes that D8/R8 generates.
+      return;
+    }
+    if (dependentOrigin != dependencyOrigin) {
+      consumer.accept(dependentOrigin, dependencyOrigin);
+    }
+  }
+
   public DexProgramClass unsafeDirectProgramTypeLookup(DexType type) {
     return app.programDefinitionFor(type);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index 54837a8..957ab7a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -100,13 +100,24 @@
     if (appView.options().enableTryWithResourcesDesugaring()) {
       desugarings.add(new TwrInstructionDesugaring(appView));
     }
+    recordRewriter = RecordDesugaring.create(appView);
+    if (recordRewriter != null) {
+      desugarings.add(recordRewriter);
+    }
+    StringConcatInstructionDesugaring stringConcatDesugaring =
+        new StringConcatInstructionDesugaring(appView);
+    desugarings.add(stringConcatDesugaring);
+    LambdaInstructionDesugaring lambdaDesugaring = new LambdaInstructionDesugaring(appView);
+    desugarings.add(lambdaDesugaring);
     interfaceMethodRewriter =
         InterfaceMethodRewriter.create(
             appView,
             SetUtils.newImmutableSetExcludingNullItems(
                 alwaysThrowingInstructionDesugaring,
                 backportedMethodRewriter,
-                desugaredLibraryRetargeter));
+                desugaredLibraryRetargeter),
+            SetUtils.newImmutableSetExcludingNullItems(
+                lambdaDesugaring, stringConcatDesugaring, recordRewriter));
     if (interfaceMethodRewriter != null) {
       desugarings.add(interfaceMethodRewriter);
     }
@@ -123,7 +134,6 @@
     if (desugaredLibraryAPIConverter != null) {
       desugarings.add(desugaredLibraryAPIConverter);
     }
-    desugarings.add(new LambdaInstructionDesugaring(appView));
     desugarings.add(new ConstantDynamicInstructionDesugaring(appView));
     desugarings.add(new InvokeSpecialToSelfDesugaring(appView));
     if (appView.options().isGeneratingClassFiles()) {
@@ -131,7 +141,6 @@
       assert nestBasedAccessDesugaring != null;
       desugarings.add(new InvokeToPrivateRewriter());
     }
-    desugarings.add(new StringConcatInstructionDesugaring(appView));
     desugarings.add(new BufferCovariantReturnTypeRewriter(appView));
     if (backportedMethodRewriter.hasBackports()) {
       desugarings.add(backportedMethodRewriter);
@@ -139,10 +148,6 @@
     if (nestBasedAccessDesugaring != null) {
       desugarings.add(nestBasedAccessDesugaring);
     }
-    this.recordRewriter = RecordDesugaring.create(appView);
-    if (recordRewriter != null) {
-      desugarings.add(recordRewriter);
-    }
     yieldingDesugarings.add(new UnrepresentableInDexInstructionRemover(appView));
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index e91925d..8d69d58 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -9,10 +9,10 @@
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.InterfaceMethodDesugaringMode.NONE;
 import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.getInterfaceMethodDesugaringMode;
 
-import com.android.tools.r8.DesugarGraphConsumer;
 import com.android.tools.r8.cf.CfVersion;
 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.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
@@ -44,8 +44,6 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineDesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.icce.AlwaysThrowingInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.InterfaceMethodDesugaringMode;
-import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
-import com.android.tools.r8.ir.desugar.stringconcat.StringConcatInstructionDesugaring;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.MethodPosition;
@@ -106,8 +104,9 @@
   // Caches default interface method info for already processed interfaces.
   private final Map<DexType, DefaultMethodsHelper.Collection> cache = new ConcurrentHashMap<>();
 
-  // This is used to filter out double desugaring on backported methods.
-  private final Set<CfInstructionDesugaring> precedingDesugarings;
+  // This is used to filter out double desugaring.
+  private final Set<CfInstructionDesugaring> precedingDesugaringsForInvoke;
+  private final Set<CfInstructionDesugaring> precedingDesugaringsForInvokeDynamic;
 
   /** Defines a minor variation in desugaring. */
   public enum Flavor {
@@ -118,21 +117,29 @@
   }
 
   public static InterfaceMethodRewriter create(
-      AppView<?> appView, Set<CfInstructionDesugaring> precedingDesugarings) {
+      AppView<?> appView,
+      Set<CfInstructionDesugaring> precedingDesugaringsForInvoke,
+      Set<CfInstructionDesugaring> precedingDesugaringsForInvokeDynamic) {
     InterfaceMethodDesugaringMode desugaringMode =
         getInterfaceMethodDesugaringMode(appView.options());
     if (desugaringMode == NONE) {
       return null;
     }
-    return new InterfaceMethodRewriter(appView, precedingDesugarings, desugaringMode);
+    return new InterfaceMethodRewriter(
+        appView,
+        precedingDesugaringsForInvoke,
+        precedingDesugaringsForInvokeDynamic,
+        desugaringMode);
   }
 
-  public InterfaceMethodRewriter(
+  private InterfaceMethodRewriter(
       AppView<?> appView,
-      Set<CfInstructionDesugaring> precedingDesugarings,
+      Set<CfInstructionDesugaring> precedingDesugaringsForInvoke,
+      Set<CfInstructionDesugaring> precedingDesugaringsForInvokeDynamic,
       InterfaceMethodDesugaringMode desugaringMode) {
     this.appView = appView;
-    this.precedingDesugarings = precedingDesugarings;
+    this.precedingDesugaringsForInvoke = precedingDesugaringsForInvoke;
+    this.precedingDesugaringsForInvokeDynamic = precedingDesugaringsForInvokeDynamic;
     this.options = appView.options();
     this.factory = appView.dexItemFactory();
     assert desugaringMode == EMULATED_INTERFACE_ONLY || desugaringMode == ALL;
@@ -201,7 +208,13 @@
 
   private boolean isAlreadyDesugared(CfInvoke invoke, ProgramMethod context) {
     return Iterables.any(
-        precedingDesugarings, desugaring -> desugaring.needsDesugaring(invoke, context));
+        precedingDesugaringsForInvoke, desugaring -> desugaring.needsDesugaring(invoke, context));
+  }
+
+  private boolean isAlreadyDesugared(CfInvokeDynamic invoke, ProgramMethod context) {
+    return Iterables.any(
+        precedingDesugaringsForInvokeDynamic,
+        desugaring -> desugaring.needsDesugaring(invoke, context));
   }
 
   @Override
@@ -222,9 +235,7 @@
     CfCode code = context.getDefinition().getCode().asCfCode();
     for (CfInstruction instruction : code.getInstructions()) {
       if (instruction.isInvokeDynamic()
-          && !LambdaInstructionDesugaring.isLambdaInvoke(instruction, context, appView)
-          && !StringConcatInstructionDesugaring.isStringConcatInvoke(
-              instruction, appView.dexItemFactory())) {
+          && !isAlreadyDesugared(instruction.asInvokeDynamic(), context)) {
         reportInterfaceMethodHandleCallSite(instruction.asInvokeDynamic().getCallSite(), context);
       }
       computeDescription(instruction, context).scan();
@@ -927,7 +938,7 @@
     // At this point we likely have a non-library type that may depend on default method information
     // from its interfaces and the dependency should be reported.
     if (implementing.isProgramClass() && !definedInterface.isLibraryClass()) {
-      reportDependencyEdge(implementing.asProgramClass(), definedInterface, appView.appInfo());
+      appView.appInfo().reportDependencyEdge(implementing.asProgramClass(), definedInterface);
     }
 
     // Merge information from all superinterfaces.
@@ -961,31 +972,4 @@
     MethodPosition position = new MethodPosition(method.asMethodReference());
     options.warningMissingTypeForDesugar(origin, position, missing, method);
   }
-
-  public static void reportDependencyEdge(
-      DexClass dependent, DexClass dependency, AppInfo appInfo) {
-    assert !dependent.isLibraryClass();
-    assert !dependency.isLibraryClass();
-    DesugarGraphConsumer consumer = appInfo.app().options.desugarGraphConsumer;
-    if (consumer != null) {
-      Origin dependencyOrigin = dependency.getOrigin();
-      java.util.Collection<DexType> dependents =
-          appInfo.getSyntheticItems().getSynthesizingContextTypes(dependent.getType());
-      if (dependents.isEmpty()) {
-        reportDependencyEdge(consumer, dependencyOrigin, dependent);
-      } else {
-        for (DexType type : dependents) {
-          reportDependencyEdge(consumer, dependencyOrigin, appInfo.definitionFor(type));
-        }
-      }
-    }
-  }
-
-  private static void reportDependencyEdge(
-      DesugarGraphConsumer consumer, Origin dependencyOrigin, DexClass clazz) {
-    Origin dependentOrigin = clazz.getOrigin();
-    if (dependentOrigin != dependencyOrigin) {
-      consumer.accept(dependentOrigin, dependencyOrigin);
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
index bde95b8..f3445c2 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.ir.desugar.nest;
 
-import static com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter.reportDependencyEdge;
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ClasspathMethod;
 import com.android.tools.r8.graph.DexClass;
@@ -45,8 +43,8 @@
           DexClass hostClass = nest.getHostClass();
           for (DexClass memberClass : nest.getMembers()) {
             if (hostClass.isProgramClass() || memberClass.isProgramClass()) {
-              reportDependencyEdge(hostClass, memberClass, appView.appInfo());
-              reportDependencyEdge(memberClass, hostClass, appView.appInfo());
+              appView.appInfo().reportDependencyEdge(hostClass, memberClass);
+              appView.appInfo().reportDependencyEdge(memberClass, hostClass);
             }
           }
         },
diff --git a/src/main/java/com/android/tools/r8/origin/GlobalSyntheticOrigin.java b/src/main/java/com/android/tools/r8/origin/GlobalSyntheticOrigin.java
new file mode 100644
index 0000000..cec0daf
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/origin/GlobalSyntheticOrigin.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2023, 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.origin;
+
+public class GlobalSyntheticOrigin extends Origin {
+
+  private static final Origin INSTANCE = new GlobalSyntheticOrigin(Origin.root());
+
+  public static Origin instance() {
+    return INSTANCE;
+  }
+
+  protected GlobalSyntheticOrigin(Origin parent) {
+    super(parent);
+  }
+
+  @Override
+  public String part() {
+    return "<synthetic>";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
index 8c2a3bf..91d352f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.origin.GlobalSyntheticOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.MainDexInfo;
 import java.util.Comparator;
@@ -55,7 +56,7 @@
   static SynthesizingContext fromType(DexType type) {
     // This method should only be used for synthesizing from a non-program context!
     // Thus we have no origin info and place the context in the "base" feature.
-    return new SynthesizingContext(type, type, Origin.unknown(), FeatureSplit.BASE);
+    return new SynthesizingContext(type, type, GlobalSyntheticOrigin.instance(), FeatureSplit.BASE);
   }
 
   static SynthesizingContext fromNonSyntheticInputContext(
diff --git a/src/test/examplesJava17/records/RecordInterface.java b/src/test/examplesJava17/records/RecordInterface.java
new file mode 100644
index 0000000..8fc1e56
--- /dev/null
+++ b/src/test/examplesJava17/records/RecordInterface.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2023, 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 RecordInterface {
+
+  interface Human {
+    default void printHuman() {
+      System.out.println("Human");
+    }
+  }
+
+  record Person(String name, int age) implements Human {}
+
+  public static void main(String[] args) {
+    Person janeDoe = new Person("Jane Doe", 42);
+    janeDoe.printHuman();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/graph/DesugarGraphTestConsumer.java b/src/test/java/com/android/tools/r8/desugar/graph/DesugarGraphTestConsumer.java
index d80d81e..541bfea 100644
--- a/src/test/java/com/android/tools/r8/desugar/graph/DesugarGraphTestConsumer.java
+++ b/src/test/java/com/android/tools/r8/desugar/graph/DesugarGraphTestConsumer.java
@@ -4,9 +4,11 @@
 package com.android.tools.r8.desugar.graph;
 
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.DesugarGraphConsumer;
+import com.android.tools.r8.origin.GlobalSyntheticOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
@@ -96,6 +98,12 @@
 
   @Override
   public synchronized void accept(Origin dependent, Origin dependency) {
+    // D8/R8 should not report edges synthetic origin.
+    assertNotEquals(dependent, GlobalSyntheticOrigin.instance());
+    assertNotEquals(dependency, GlobalSyntheticOrigin.instance());
+    // D8/R8 may report edges to unknown origin, but that is typically *not* what should be done.
+    assertNotEquals(dependency, Origin.unknown());
+    assertNotEquals(dependent, Origin.unknown());
     assertFalse(finished);
     dependents.computeIfAbsent(dependency, s -> new HashSet<>()).add(dependent);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java
new file mode 100644
index 0000000..5efd3b2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInterfaceTest.java
@@ -0,0 +1,165 @@
+// Copyright (c) 2023, 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 org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.desugar.graph.DesugarGraphTestConsumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsTestingConsumer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RecordInterfaceTest extends TestBase {
+
+  private static final String RECORD_NAME = "RecordInterface";
+  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("Human");
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  private boolean isCfRuntimeWithNativeRecordSupport() {
+    return parameters.isCfRuntime()
+        && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK14)
+        && parameters.getApiLevel().equals(AndroidApiLevel.B);
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(isCfRuntimeWithNativeRecordSupport());
+    testForJvm(parameters)
+        .addProgramClassFileData(PROGRAM_DATA)
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testD8Intermediate() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    DesugarGraphTestConsumer consumer = new DesugarGraphTestConsumer();
+    GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer();
+    Path path = compileIntermediate(globals);
+    testForD8()
+        .addProgramFiles(path)
+        .apply(
+            b ->
+                b.getBuilder()
+                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
+        .apply(b -> b.getBuilder().setDesugarGraphConsumer(consumer))
+        .setMinApi(parameters)
+        .setIncludeClassesChecksum(true)
+        .compile()
+        .assertNoMessages()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+    assertNoEdgeToRecord(consumer);
+  }
+
+  @Test
+  public void testD8IntermediateNoDesugaringInStep2() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    DesugarGraphTestConsumer consumer = new DesugarGraphTestConsumer();
+    GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer();
+    Path path = compileIntermediate(globals);
+    testForD8()
+        .addProgramFiles(path)
+        .apply(
+            b ->
+                b.getBuilder()
+                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
+        .apply(b -> b.getBuilder().setDesugarGraphConsumer(consumer))
+        .setMinApi(parameters)
+        .setIncludeClassesChecksum(true)
+        // In Android Studio they disable desugaring at this point to improve build speed.
+        .disableDesugaring()
+        .compile()
+        .assertNoMessages()
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+    assertNoEdgeToRecord(consumer);
+  }
+
+  private Path compileIntermediate(GlobalSyntheticsConsumer globalSyntheticsConsumer)
+      throws Exception {
+    Origin fake = new PathOrigin(Paths.get("origin"));
+    DesugarGraphTestConsumer consumer = new DesugarGraphTestConsumer();
+    Path intermediate =
+        testForD8(Backend.DEX)
+            .apply(
+                b -> {
+                  // We avoid unknown origin here since they are not allowed when using a Graph
+                  // consumer.
+                  for (byte[] programDatum : PROGRAM_DATA) {
+                    b.getBuilder().addClassProgramData(programDatum, fake);
+                  }
+                })
+            .setMinApi(parameters)
+            .setIntermediate(true)
+            .setIncludeClassesChecksum(true)
+            .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globalSyntheticsConsumer))
+            .apply(b -> b.getBuilder().setDesugarGraphConsumer(consumer))
+            .compile()
+            .assertNoMessages()
+            .writeToZip();
+    assertNoEdgeToRecord(consumer);
+    return intermediate;
+  }
+
+  private void assertNoEdgeToRecord(DesugarGraphTestConsumer consumer) {
+    assertEquals(0, consumer.totalEdgeCount());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || isCfRuntimeWithNativeRecordSupport());
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .setMinApi(parameters)
+            .addKeepMainRule(MAIN_TYPE);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}