Basic D8 Record support

Bug: 169645628
Change-Id: I5835fd436682a2fc713e5f5008413585a7937bce
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 8e44ee9..cdbcdd8 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.kotlin.KotlinMetadataRewriter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.PrefixRewritingNamingLens;
+import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
 import com.android.tools.r8.origin.CommandLineOrigin;
 import com.android.tools.r8.origin.Origin;
@@ -259,26 +260,22 @@
       Marker.checkCompatibleDesugaredLibrary(markers, options.reporter);
 
       InspectorImpl.runInspections(options.outputInspections, appView.appInfo().classes());
+      NamingLens namingLens = NamingLens.getIdentityLens();
+      if (appView.rewritePrefix.isRewriting()) {
+        namingLens = PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView, namingLens);
+      }
+      if (appView.options().shouldDesugarRecords()) {
+        namingLens = RecordRewritingNamingLens.createRecordRewritingNamingLens(appView, namingLens);
+      }
       if (options.isGeneratingClassFiles()) {
         // TODO(b/158159959): Move this out so it is shared for both CF and DEX pipelines.
         SyntheticFinalization.finalize(appView);
-        new CfApplicationWriter(
-                appView,
-                marker,
-                GraphLens.getIdentityLens(),
-                appView.rewritePrefix.isRewriting()
-                    ? PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView)
-                    : NamingLens.getIdentityLens(),
-                null)
+        new CfApplicationWriter(appView, marker, GraphLens.getIdentityLens(), namingLens, null)
             .write(options.getClassFileConsumer());
       } else {
-        NamingLens namingLens;
         if (!hasDexResources || !hasClassResources || !appView.rewritePrefix.isRewriting()) {
           // All inputs are either dex or cf, or there is nothing to rewrite.
-          namingLens =
-              hasDexResources
-                  ? NamingLens.getIdentityLens()
-                  : PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
+          namingLens = hasDexResources ? NamingLens.getIdentityLens() : namingLens;
           new GenericSignatureRewriter(appView, namingLens)
               .run(appView.appInfo().classes(), executor);
           new KotlinMetadataRewriter(appView, namingLens).runForD8(executor);
@@ -290,7 +287,13 @@
           timing.begin("Rewrite non-dex inputs");
           DexApplication app =
               rewriteNonDexInputs(
-                  appView, inputApp, options, executor, timing, appView.appInfo().app());
+                  appView,
+                  inputApp,
+                  options,
+                  executor,
+                  timing,
+                  appView.appInfo().app(),
+                  namingLens);
           timing.end();
           appView.setAppInfo(
               new AppInfo(
@@ -339,7 +342,8 @@
       InternalOptions options,
       ExecutorService executor,
       Timing timing,
-      DexApplication app)
+      DexApplication app,
+      NamingLens desugaringLens)
       throws IOException, ExecutionException {
     // TODO(b/154575955): Remove the naming lens in D8.
     appView
@@ -365,17 +369,15 @@
             appView.appInfo().getSyntheticItems().commit(cfApp),
             appView.appInfo().getMainDexInfo()));
     ConvertedCfFiles convertedCfFiles = new ConvertedCfFiles();
-    NamingLens prefixRewritingNamingLens =
-        PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
-    new GenericSignatureRewriter(appView, prefixRewritingNamingLens)
+    new GenericSignatureRewriter(appView, desugaringLens)
         .run(appView.appInfo().classes(), executor);
-    new KotlinMetadataRewriter(appView, prefixRewritingNamingLens).runForD8(executor);
+    new KotlinMetadataRewriter(appView, desugaringLens).runForD8(executor);
     new ApplicationWriter(
             appView,
             null,
             GraphLens.getIdentityLens(),
             InitClassLens.getDefault(),
-            prefixRewritingNamingLens,
+            desugaringLens,
             null,
             convertedCfFiles)
         .write(executor);
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 09f14ce..9862f27 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -59,6 +59,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.Monitor;
@@ -396,10 +397,14 @@
     builder.append(callSite.methodName);
     builder.append(callSite.methodProto.toDescriptorString());
     if (callSite.bootstrapArgs.size() > 1) {
-      DexMethodHandle handle = callSite.bootstrapArgs.get(1).asDexValueMethodHandle().getValue();
-      builder.append(", handle:");
-      builder.append(handle.toSourceString());
-      builder.append(", itf: ").append(handle.isInterface);
+      DexValue.DexValueMethodHandle dexValueMethodHandle =
+          callSite.bootstrapArgs.get(1).asDexValueMethodHandle();
+      if (dexValueMethodHandle != null) {
+        DexMethodHandle handle = dexValueMethodHandle.getValue();
+        builder.append(", handle:");
+        builder.append(handle.toSourceString());
+        builder.append(", itf: ").append(handle.isInterface);
+      }
     }
     builder.append(", bsm:");
     appendMethod(bootstrapMethod.asMethod());
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
index 277f2e2..1e25ca7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
@@ -26,7 +26,7 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
-public class CfCheckCast extends CfInstruction {
+public class CfCheckCast extends CfInstruction implements CfTypeInstruction {
 
   private final DexType type;
 
@@ -34,11 +34,27 @@
     this.type = type;
   }
 
+  @Override
+  public CfTypeInstruction asTypeInstruction() {
+    return this;
+  }
+
+  @Override
+  public boolean isTypeInstruction() {
+    return true;
+  }
+
+  @Override
   public DexType getType() {
     return type;
   }
 
   @Override
+  public CfInstruction withType(DexType newType) {
+    return new CfCheckCast(newType);
+  }
+
+  @Override
   public int getCompareToId() {
     return Opcodes.CHECKCAST;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
index 863dce0..a6a0933 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
@@ -26,7 +26,7 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Type;
 
-public class CfConstClass extends CfInstruction {
+public class CfConstClass extends CfInstruction implements CfTypeInstruction {
 
   private final DexType type;
 
@@ -45,11 +45,27 @@
     return type.acceptCompareTo(((CfConstClass) other).type, visitor);
   }
 
+  @Override
+  public CfTypeInstruction asTypeInstruction() {
+    return this;
+  }
+
+  @Override
+  public boolean isTypeInstruction() {
+    return true;
+  }
+
+  @Override
   public DexType getType() {
     return type;
   }
 
   @Override
+  public CfInstruction withType(DexType newType) {
+    return new CfConstClass(newType);
+  }
+
+  @Override
   public void write(
       AppView<?> appView,
       ProgramMethod context,
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
index 815500d..0b8240a 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
@@ -55,6 +55,11 @@
   }
 
   @Override
+  public boolean isInitClass() {
+    return true;
+  }
+
+  @Override
   public void write(
       AppView<?> appView,
       ProgramMethod context,
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
index 3cc1f4b..b4d6d43 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
@@ -25,7 +25,7 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
-public class CfInstanceOf extends CfInstruction {
+public class CfInstanceOf extends CfInstruction implements CfTypeInstruction {
 
   private final DexType type;
 
@@ -33,11 +33,27 @@
     this.type = type;
   }
 
+  @Override
+  public CfTypeInstruction asTypeInstruction() {
+    return this;
+  }
+
+  @Override
+  public boolean isTypeInstruction() {
+    return true;
+  }
+
+  @Override
   public DexType getType() {
     return type;
   }
 
   @Override
+  public CfInstruction withType(DexType newType) {
+    return new CfInstanceOf(newType);
+  }
+
+  @Override
   public int getCompareToId() {
     return Opcodes.INSTANCEOF;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
index c69ba09..1009080 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
@@ -221,6 +221,18 @@
     return false;
   }
 
+  public CfTypeInstruction asTypeInstruction() {
+    return null;
+  }
+
+  public boolean isTypeInstruction() {
+    return false;
+  }
+
+  public boolean isInitClass() {
+    return false;
+  }
+
   public CfDexItemBasedConstString asDexItemBasedConstString() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
index bac5536..682c857 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
@@ -27,7 +27,7 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
-public class CfMultiANewArray extends CfInstruction {
+public class CfMultiANewArray extends CfInstruction implements CfTypeInstruction {
 
   private final DexType type;
   private final int dimensions;
@@ -41,10 +41,26 @@
     this.dimensions = dimensions;
   }
 
+  @Override
+  public CfTypeInstruction asTypeInstruction() {
+    return this;
+  }
+
+  @Override
+  public boolean isTypeInstruction() {
+    return true;
+  }
+
+  @Override
   public DexType getType() {
     return type;
   }
 
+  @Override
+  public CfInstruction withType(DexType newType) {
+    return new CfMultiANewArray(newType, dimensions);
+  }
+
   public int getDimensions() {
     return dimensions;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNew.java b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
index 7140404..53fc40c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNew.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
@@ -26,7 +26,7 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
-public class CfNew extends CfInstruction {
+public class CfNew extends CfInstruction implements CfTypeInstruction {
 
   private final DexType type;
 
@@ -34,11 +34,27 @@
     this.type = type;
   }
 
+  @Override
+  public CfTypeInstruction asTypeInstruction() {
+    return this;
+  }
+
+  @Override
+  public boolean isTypeInstruction() {
+    return true;
+  }
+
+  @Override
   public DexType getType() {
     return type;
   }
 
   @Override
+  public CfInstruction withType(DexType newType) {
+    return new CfNew(newType);
+  }
+
+  @Override
   public int getCompareToId() {
     return Opcodes.NEW;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
index 78200b7..ad50c17 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
@@ -28,7 +28,7 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
-public class CfNewArray extends CfInstruction {
+public class CfNewArray extends CfInstruction implements CfTypeInstruction {
 
   private final DexType type;
 
@@ -37,11 +37,27 @@
     this.type = type;
   }
 
+  @Override
+  public CfTypeInstruction asTypeInstruction() {
+    return this;
+  }
+
+  @Override
+  public boolean isTypeInstruction() {
+    return true;
+  }
+
+  @Override
   public DexType getType() {
     return type;
   }
 
   @Override
+  public CfInstruction withType(DexType newType) {
+    return new CfNewArray(newType);
+  }
+
+  @Override
   public int getCompareToId() {
     return type.isPrimitiveArrayType() ? Opcodes.NEWARRAY : Opcodes.ANEWARRAY;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfTypeInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfTypeInstruction.java
new file mode 100644
index 0000000..1762c7a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfTypeInstruction.java
@@ -0,0 +1,14 @@
+// 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.cf.code;
+
+import com.android.tools.r8.graph.DexType;
+
+public interface CfTypeInstruction {
+
+  DexType getType();
+
+  CfInstruction withType(DexType newType);
+}
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
index abeb404..be553ac6 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
@@ -95,7 +95,7 @@
   public int getAsDexAccessFlags() {
     // We unset the super flag here, as it is meaningless in DEX. Furthermore, we add missing
     // abstract to interfaces to work around a javac bug when generating package-info classes.
-    int flags = materialize() & ~Constants.ACC_SUPER;
+    int flags = materialize() & ~Constants.ACC_SUPER & ~Constants.ACC_RECORD;
     if (isInterface()) {
       return flags | Constants.ACC_ABSTRACT;
     }
@@ -186,6 +186,10 @@
     set(Constants.ACC_RECORD);
   }
 
+  public void unsetRecord() {
+    unset(Constants.ACC_RECORD);
+  }
+
   public boolean isSuper() {
     return isSet(Constants.ACC_SUPER);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index d5ea16e..d82db4a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -199,6 +199,8 @@
       return self();
     }
 
+    public abstract void addProgramClassPotentiallyOverridingNonProgramClass(DexProgramClass clazz);
+
     public synchronized T addProgramClasses(Collection<DexProgramClass> classes) {
       programClasses.addAll(classes);
       return self();
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 04aff56..2ce742a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -216,6 +216,7 @@
   public final DexString stringArrayDescriptor = createString("[Ljava/lang/String;");
   public final DexString objectDescriptor = createString("Ljava/lang/Object;");
   public final DexString recordDescriptor = createString("Ljava/lang/Record;");
+  public final DexString r8RecordDescriptor = createString("Lcom/android/tools/r8/RecordTag;");
   public final DexString objectArrayDescriptor = createString("[Ljava/lang/Object;");
   public final DexString classDescriptor = createString("Ljava/lang/Class;");
   public final DexString classLoaderDescriptor = createString("Ljava/lang/ClassLoader;");
@@ -347,6 +348,7 @@
   public final DexType stringArrayType = createStaticallyKnownType(stringArrayDescriptor);
   public final DexType objectType = createStaticallyKnownType(objectDescriptor);
   public final DexType recordType = createStaticallyKnownType(recordDescriptor);
+  public final DexType r8RecordType = createStaticallyKnownType(r8RecordDescriptor);
   public final DexType objectArrayType = createStaticallyKnownType(objectArrayDescriptor);
   public final DexType classArrayType = createStaticallyKnownType(classArrayDescriptor);
   public final DexType enumType = createStaticallyKnownType(enumDescriptor);
@@ -510,6 +512,7 @@
   public final LongMembers longMembers = new LongMembers();
   public final ObjectsMethods objectsMethods = new ObjectsMethods();
   public final ObjectMembers objectMembers = new ObjectMembers();
+  public final RecordMembers recordMembers = new RecordMembers();
   public final ShortMembers shortMembers = new ShortMembers();
   public final StringMembers stringMembers = new StringMembers();
   public final DoubleMembers doubleMembers = new DoubleMembers();
@@ -1204,6 +1207,14 @@
     }
   }
 
+  public class RecordMembers {
+    public final DexMethod init = createMethod(recordType, createProto(voidType), "<init>");
+    public final DexMethod equals =
+        createMethod(recordType, createProto(booleanType, objectType), "equals");
+    public final DexMethod hashCode = createMethod(recordType, createProto(intType), "hashCode");
+    public final DexMethod toString = createMethod(recordType, createProto(stringType), "toString");
+  }
+
   public class ObjectMembers {
 
     /**
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index 86377b3..0f7b951 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -213,6 +213,37 @@
     }
 
     @Override
+    public void addProgramClassPotentiallyOverridingNonProgramClass(DexProgramClass clazz) {
+      addProgramClass(clazz);
+      if (containsType(clazz.type, libraryClasses)) {
+        replaceLibraryClasses(withoutType(clazz.type, libraryClasses));
+        return;
+      }
+      if (containsType(clazz.type, classpathClasses)) {
+        replaceClasspathClasses(withoutType(clazz.type, classpathClasses));
+      }
+    }
+
+    private boolean containsType(DexType type, List<? extends DexClass> classes) {
+      for (DexClass clazz : classes) {
+        if (clazz.type == type) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    private <T extends DexClass> ImmutableList<T> withoutType(DexType type, List<T> classes) {
+      ImmutableList.Builder<T> builder = ImmutableList.builder();
+      for (T clazz : classes) {
+        if (clazz.type != type) {
+          builder.add(clazz);
+        }
+      }
+      return builder.build();
+    }
+
+    @Override
     Builder self() {
       return this;
     }
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 35b4574..d8815c7 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -500,7 +500,7 @@
         return;
       }
       // TODO(b/169645628): Support records in all compilation.
-      if (!application.options.canUseRecords()) {
+      if (!application.options.enableExperimentalRecordDesugaring()) {
         throw new CompilationError("Records are not supported", origin);
       }
       // TODO(b/169645628): Change this logic if we start stripping the record components.
diff --git a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
index 7005e06..509a10d 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -226,6 +226,13 @@
     }
 
     @Override
+    public void addProgramClassPotentiallyOverridingNonProgramClass(DexProgramClass clazz) {
+      addProgramClass(clazz);
+      classpathClasses.clearType(clazz.type);
+      libraryClasses.clearType(clazz.type);
+    }
+
+    @Override
     public LazyLoadedDexApplication build() {
       return new LazyLoadedDexApplication(
           proguardMap,
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
index 6d79c9c..8f83dca 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer.D8CfClassDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer.D8CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -50,6 +52,11 @@
       ClassConverterResult.Builder resultBuilder, ExecutorService executorService)
       throws ExecutionException {
     List<DexProgramClass> classes = appView.appInfo().classes();
+
+    D8CfClassDesugaringEventConsumer classDesugaringEventConsumer =
+        CfClassDesugaringEventConsumer.createForD8(methodProcessor);
+    converter.desugarClassesForD8(classes, classDesugaringEventConsumer, executorService);
+
     while (!classes.isEmpty()) {
       Set<DexType> seenNestHosts = Sets.newIdentityHashSet();
       List<DexProgramClass> deferred = new ArrayList<>(classes.size() / 2);
@@ -65,18 +72,19 @@
         }
       }
 
-      // Process the wave and wait for all IR processing to complete.
-      D8CfInstructionDesugaringEventConsumer desugaringEventConsumer =
+      D8CfInstructionDesugaringEventConsumer instructionDesugaringEventConsumer =
           CfInstructionDesugaringEventConsumer.createForD8(methodProcessor);
+
+      // Process the wave and wait for all IR processing to complete.
       methodProcessor.newWave();
       ThreadUtils.processItems(
-          wave, clazz -> convertClass(clazz, desugaringEventConsumer), executorService);
+          wave, clazz -> convertClass(clazz, instructionDesugaringEventConsumer), executorService);
       methodProcessor.awaitMethodProcessing();
 
       // Finalize the desugaring of the processed classes. This may require processing (and
       // reprocessing) of some methods.
       List<ProgramMethod> needsProcessing =
-          desugaringEventConsumer.finalizeDesugaring(appView, resultBuilder);
+          instructionDesugaringEventConsumer.finalizeDesugaring(appView, resultBuilder);
       if (!needsProcessing.isEmpty()) {
         // Create a new processor context to ensure unique method processing contexts.
         methodProcessor.newWave();
@@ -90,13 +98,13 @@
               if (definition.isProcessed()) {
                 definition.markNotProcessed();
               }
-              methodProcessor.processMethod(method, desugaringEventConsumer);
+              methodProcessor.processMethod(method, instructionDesugaringEventConsumer);
             },
             executorService);
 
         // Verify there is nothing to finalize once method processing finishes.
         methodProcessor.awaitMethodProcessing();
-        assert desugaringEventConsumer.verifyNothingToFinalize();
+        assert instructionDesugaringEventConsumer.verifyNothingToFinalize();
       }
 
       classes = deferred;
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 8e5c11c..54b0a66 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
@@ -42,6 +42,8 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer.D8CfClassDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer.D8CfInstructionDesugaringEventConsumer;
@@ -125,7 +127,8 @@
   private final Timing timing;
   private final Outliner outliner;
   private final ClassInitializerDefaultsOptimization classInitializerDefaultsOptimization;
-  private final CfInstructionDesugaringCollection desugaring;
+  private final CfClassDesugaringCollection classDesugaring;
+  private final CfInstructionDesugaringCollection instructionDesugaring;
   private final FieldAccessAnalysis fieldAccessAnalysis;
   private final LibraryMethodOverrideAnalysis libraryMethodOverrideAnalysis;
   private final StringOptimizer stringOptimizer;
@@ -217,7 +220,8 @@
       // - nest based access desugaring,
       // - invoke-special desugaring.
       assert options.desugarState.isOn();
-      this.desugaring = CfInstructionDesugaringCollection.create(appView);
+      this.instructionDesugaring = CfInstructionDesugaringCollection.create(appView);
+      this.classDesugaring = instructionDesugaring.createClassDesugaringCollection();
       this.desugaredLibraryRetargeter =
           options.desugaredLibraryConfiguration.getRetargetCoreLibMember().isEmpty()
               ? null
@@ -249,10 +253,11 @@
       this.assumeInserter = null;
       return;
     }
-    this.desugaring =
+    this.instructionDesugaring =
         appView.enableWholeProgramOptimizations()
             ? CfInstructionDesugaringCollection.empty()
             : CfInstructionDesugaringCollection.create(appView);
+    this.classDesugaring = instructionDesugaring.createClassDesugaringCollection();
     this.interfaceMethodRewriter =
         options.isInterfaceMethodDesugaringEnabled()
             ? new InterfaceMethodRewriter(appView, this)
@@ -349,7 +354,7 @@
   private void synthesizeBridgesForNestBasedAccessesOnClasspath(
       D8MethodProcessor methodProcessor, ExecutorService executorService)
       throws ExecutionException {
-    desugaring.withD8NestBasedAccessDesugaring(
+    instructionDesugaring.withD8NestBasedAccessDesugaring(
         d8NestBasedAccessDesugaring ->
             d8NestBasedAccessDesugaring.synthesizeBridgesForNestBasedAccessesOnClasspath(
                 methodProcessor, executorService));
@@ -357,7 +362,7 @@
   }
 
   private void reportNestDesugarDependencies() {
-    desugaring.withD8NestBasedAccessDesugaring(
+    instructionDesugaring.withD8NestBasedAccessDesugaring(
         D8NestBasedAccessDesugaring::reportDesugarDependencies);
   }
 
@@ -460,6 +465,26 @@
         appView, classConverterResult.getForcefullyMovedLambdaMethods());
   }
 
+  public void desugarClassesForD8(
+      List<DexProgramClass> classes,
+      D8CfClassDesugaringEventConsumer desugaringEventConsumer,
+      ExecutorService executorService)
+      throws ExecutionException {
+    if (classDesugaring.isEmpty()) {
+      return;
+    }
+    // Currently the classes can be processed in any order and do not require to be sorted.
+    ThreadUtils.processItems(
+        classes, clazz -> desugarClassForD8(clazz, desugaringEventConsumer), executorService);
+  }
+
+  public void desugarClassForD8(
+      DexProgramClass clazz, D8CfClassDesugaringEventConsumer desugaringEventConsumer) {
+    if (classDesugaring.needsDesugaring(clazz)) {
+      classDesugaring.desugar(clazz, desugaringEventConsumer);
+    }
+  }
+
   void convertMethods(
       DexProgramClass clazz,
       D8CfInstructionDesugaringEventConsumer desugaringEventConsumer,
@@ -545,7 +570,7 @@
     if (!options.cfToCfDesugar) {
       return true;
     }
-    if (desugaring.needsDesugaring(method)) {
+    if (instructionDesugaring.needsDesugaring(method)) {
       return true;
     }
     if (desugaredLibraryAPIConverter != null
@@ -621,7 +646,7 @@
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService)
       throws ExecutionException {
     // Desugaring happens in the enqueuer.
-    assert desugaring.isEmpty();
+    assert instructionDesugaring.isEmpty();
 
     DexApplication application = appView.appInfo().app();
 
@@ -1110,13 +1135,15 @@
       ProgramMethod method,
       CfInstructionDesugaringEventConsumer desugaringEventConsumer,
       MethodProcessingContext methodProcessingContext) {
-    if (options.desugarState.isOff()
-        || !method.getDefinition().getCode().isCfCode()
-        || !desugaring.needsDesugaring(method)) {
+    if (options.desugarState.isOff() || !method.getDefinition().getCode().isCfCode()) {
       return false;
     }
-    desugaring.desugar(method, methodProcessingContext, desugaringEventConsumer);
-    return true;
+    instructionDesugaring.scan(method, desugaringEventConsumer);
+    if (instructionDesugaring.needsDesugaring(method)) {
+      instructionDesugaring.desugar(method, methodProcessingContext, desugaringEventConsumer);
+      return true;
+    }
+    return false;
   }
 
   // TODO(b/140766440): Convert all sub steps an implementer of CodeOptimization
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaring.java
new file mode 100644
index 0000000..a885344
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaring.java
@@ -0,0 +1,16 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.graph.DexProgramClass;
+
+/** Interface for desugaring a class. */
+public interface CfClassDesugaring {
+
+  void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer);
+
+  /** Returns true if the given class needs desugaring. */
+  boolean needsDesugaring(DexProgramClass clazz);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringCollection.java
new file mode 100644
index 0000000..9b7c709
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringCollection.java
@@ -0,0 +1,58 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.graph.DexProgramClass;
+
+/** Interface for desugaring a class. */
+public abstract class CfClassDesugaringCollection {
+
+  public abstract void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer);
+
+  /** Returns true if the given class needs desugaring. */
+  public abstract boolean needsDesugaring(DexProgramClass clazz);
+
+  public abstract boolean isEmpty();
+
+  public static class NonEmptyCfClassDesugaringCollection extends CfClassDesugaringCollection {
+    private final RecordRewriter recordRewriter;
+
+    NonEmptyCfClassDesugaringCollection(RecordRewriter recordRewriter) {
+      this.recordRewriter = recordRewriter;
+    }
+
+    @Override
+    public void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer) {
+      recordRewriter.desugar(clazz, eventConsumer);
+    }
+
+    @Override
+    public boolean needsDesugaring(DexProgramClass clazz) {
+      return recordRewriter.needsDesugaring(clazz);
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return false;
+    }
+  }
+
+  public static class EmptyCfClassDesugaringCollection extends CfClassDesugaringCollection {
+    @Override
+    public void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public boolean needsDesugaring(DexProgramClass clazz) {
+      return false;
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringEventConsumer.java
new file mode 100644
index 0000000..d5acaa7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringEventConsumer.java
@@ -0,0 +1,31 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.ir.conversion.D8MethodProcessor;
+
+public abstract class CfClassDesugaringEventConsumer implements RecordDesugaringEventConsumer {
+
+  public static D8CfClassDesugaringEventConsumer createForD8(D8MethodProcessor methodProcessor) {
+    return new D8CfClassDesugaringEventConsumer(methodProcessor);
+  }
+
+  public static class D8CfClassDesugaringEventConsumer extends CfClassDesugaringEventConsumer {
+
+    private final D8MethodProcessor methodProcessor;
+
+    public D8CfClassDesugaringEventConsumer(D8MethodProcessor methodProcessor) {
+      this.methodProcessor = methodProcessor;
+    }
+
+    @Override
+    public void acceptRecordClass(DexProgramClass recordClass) {
+      methodProcessor.scheduleDesugaredMethodsForProcessing(recordClass.programMethods());
+    }
+  }
+
+  // TODO(b/): Implement R8CfClassDesugaringEventConsumer
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
index 21ac7ae..3fb9329 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.nest.D8NestBasedAccessDesugaring;
 import com.android.tools.r8.utils.ThrowingConsumer;
+import java.util.function.Consumer;
 
 /**
  * Abstracts a collection of low-level desugarings (i.e., mappings from class-file instructions to
@@ -34,6 +35,9 @@
     return EmptyCfInstructionDesugaringCollection.getInstance();
   }
 
+  public abstract void scan(
+      ProgramMethod method, CfInstructionDesugaringEventConsumer eventConsumer);
+
   /** Desugars the instructions in the given method. */
   public abstract void desugar(
       ProgramMethod method,
@@ -44,9 +48,13 @@
     return false;
   }
 
+  public abstract CfClassDesugaringCollection createClassDesugaringCollection();
+
   /** Returns true if the given method needs desugaring. */
   public abstract boolean needsDesugaring(ProgramMethod method);
 
   public abstract <T extends Throwable> void withD8NestBasedAccessDesugaring(
       ThrowingConsumer<D8NestBasedAccessDesugaring, T> consumer) throws T;
+
+  public abstract void withRecordRewriter(Consumer<RecordRewriter> consumer);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index d765537..a95569e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -39,6 +39,7 @@
         InvokeSpecialToSelfDesugaringEventConsumer,
         LambdaDesugaringEventConsumer,
         NestBasedAccessDesugaringEventConsumer,
+        RecordDesugaringEventConsumer,
         TwrCloseResourceDesugaringEventConsumer {
 
   public static D8CfInstructionDesugaringEventConsumer createForD8(
@@ -58,6 +59,11 @@
     return new CfInstructionDesugaringEventConsumer() {
 
       @Override
+      public void acceptRecordClass(DexProgramClass recordClass) {
+        assert false;
+      }
+
+      @Override
       public void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context) {
         assert false;
       }
@@ -121,6 +127,11 @@
     }
 
     @Override
+    public void acceptRecordClass(DexProgramClass recordClass) {
+      methodProcessor.scheduleDesugaredMethodsForProcessing(recordClass.programMethods());
+    }
+
+    @Override
     public void acceptLambdaClass(LambdaClass lambdaClass, ProgramMethod context) {
       synchronized (synthesizedLambdaClasses) {
         synthesizedLambdaClasses.add(lambdaClass);
@@ -225,6 +236,12 @@
     }
 
     @Override
+    public void acceptRecordClass(DexProgramClass recordClass) {
+      // This is called each time an instruction or a class is found to require the record class.
+      assert false : "TODO(b/179146128): To be implemented";
+    }
+
+    @Override
     public void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context) {
       // Intentionally empty. The backported method will be hit by the tracing in R8 as if it was
       // present in the input code, and thus nothing needs to be done.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java
index 60b3537..c8b6147 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java
@@ -6,8 +6,10 @@
 
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection.EmptyCfClassDesugaringCollection;
 import com.android.tools.r8.ir.desugar.nest.D8NestBasedAccessDesugaring;
 import com.android.tools.r8.utils.ThrowingConsumer;
+import java.util.function.Consumer;
 
 public class EmptyCfInstructionDesugaringCollection extends CfInstructionDesugaringCollection {
 
@@ -22,6 +24,11 @@
   }
 
   @Override
+  public void scan(ProgramMethod method, CfInstructionDesugaringEventConsumer eventConsumer) {
+    // Intentionally empty.
+  }
+
+  @Override
   public void desugar(
       ProgramMethod method,
       MethodProcessingContext methodProcessingContext,
@@ -35,6 +42,11 @@
   }
 
   @Override
+  public CfClassDesugaringCollection createClassDesugaringCollection() {
+    return new EmptyCfClassDesugaringCollection();
+  }
+
+  @Override
   public boolean needsDesugaring(ProgramMethod method) {
     return false;
   }
@@ -44,4 +56,9 @@
       ThrowingConsumer<D8NestBasedAccessDesugaring, T> consumer) {
     // Intentionally empty.
   }
+
+  @Override
+  public void withRecordRewriter(Consumer<RecordRewriter> consumer) {
+    // Intentionally empty.
+  }
 }
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 5815053..1865244 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
@@ -12,6 +12,8 @@
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection.EmptyCfClassDesugaringCollection;
+import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection.NonEmptyCfClassDesugaringCollection;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
 import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.nest.D8NestBasedAccessDesugaring;
@@ -28,6 +30,7 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class NonEmptyCfInstructionDesugaringCollection extends CfInstructionDesugaringCollection {
 
@@ -35,6 +38,7 @@
   private final List<CfInstructionDesugaring> desugarings = new ArrayList<>();
 
   private final NestBasedAccessDesugaring nestBasedAccessDesugaring;
+  private final RecordRewriter recordRewriter;
 
   NonEmptyCfInstructionDesugaringCollection(AppView<?> appView) {
     this.appView = appView;
@@ -54,6 +58,11 @@
     if (nestBasedAccessDesugaring != null) {
       desugarings.add(nestBasedAccessDesugaring);
     }
+    this.recordRewriter = RecordRewriter.create(appView);
+    if (recordRewriter != null) {
+      assert !appView.enableWholeProgramOptimizations() : "To be implemented";
+      desugarings.add(recordRewriter);
+    }
   }
 
   // TODO(b/145775365): special constructor for cf-to-cf compilations with desugaring disabled.
@@ -62,6 +71,7 @@
       AppView<?> appView, InvokeSpecialToSelfDesugaring invokeSpecialToSelfDesugaring) {
     this.appView = appView;
     this.nestBasedAccessDesugaring = null;
+    this.recordRewriter = null;
     desugarings.add(invokeSpecialToSelfDesugaring);
   }
 
@@ -72,13 +82,8 @@
         appView, new InvokeSpecialToSelfDesugaring(appView));
   }
 
-  @Override
-  public void desugar(
-      ProgramMethod method,
-      MethodProcessingContext methodProcessingContext,
-      CfInstructionDesugaringEventConsumer eventConsumer) {
-    Code code = method.getDefinition().getCode();
-    if (!code.isCfCode()) {
+  private void ensureCfCode(ProgramMethod method) {
+    if (!method.getDefinition().getCode().isCfCode()) {
       appView
           .options()
           .reporter
@@ -87,10 +92,24 @@
                   "Unsupported attempt to desugar non-CF code",
                   method.getOrigin(),
                   method.getPosition()));
-      return;
     }
+  }
 
-    CfCode cfCode = code.asCfCode();
+  @Override
+  public void scan(ProgramMethod method, CfInstructionDesugaringEventConsumer eventConsumer) {
+    ensureCfCode(method);
+    if (recordRewriter != null) {
+      recordRewriter.scan(method, eventConsumer);
+    }
+  }
+
+  @Override
+  public void desugar(
+      ProgramMethod method,
+      MethodProcessingContext methodProcessingContext,
+      CfInstructionDesugaringEventConsumer eventConsumer) {
+    ensureCfCode(method);
+    CfCode cfCode = method.getDefinition().getCode().asCfCode();
 
     // Tracking of temporary locals used for instruction desugaring. The desugaring of each
     // instruction is assumed to use locals only for the duration of the instruction, such that any
@@ -135,6 +154,14 @@
     }
   }
 
+  @Override
+  public CfClassDesugaringCollection createClassDesugaringCollection() {
+    if (recordRewriter == null) {
+      return new EmptyCfClassDesugaringCollection();
+    }
+    return new NonEmptyCfClassDesugaringCollection(recordRewriter);
+  }
+
   private Collection<CfInstruction> desugarInstruction(
       CfInstruction instruction,
       FreshLocalProvider freshLocalProvider,
@@ -220,4 +247,11 @@
       consumer.accept((D8NestBasedAccessDesugaring) nestBasedAccessDesugaring);
     }
   }
+
+  @Override
+  public void withRecordRewriter(Consumer<RecordRewriter> consumer) {
+    if (recordRewriter != null) {
+      consumer.accept(recordRewriter);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/RecordDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/RecordDesugaringEventConsumer.java
new file mode 100644
index 0000000..19d9ae3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/RecordDesugaringEventConsumer.java
@@ -0,0 +1,12 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.graph.DexProgramClass;
+
+public interface RecordDesugaringEventConsumer {
+
+  void acceptRecordClass(DexProgramClass recordClass);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
new file mode 100644
index 0000000..67e2c90
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
@@ -0,0 +1,272 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfTypeInstruction;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.synthetic.CallObjectInitCfCodeProvider;
+import com.android.tools.r8.synthesis.SyntheticNaming;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Collections;
+
+public class RecordRewriter implements CfInstructionDesugaring, CfClassDesugaring {
+
+  private final AppView<?> appView;
+  private final DexItemFactory factory;
+
+  public static RecordRewriter create(AppView<?> appView) {
+    return appView.options().shouldDesugarRecords() ? new RecordRewriter(appView) : null;
+  }
+
+  private RecordRewriter(AppView<?> appView) {
+    this.appView = appView;
+    factory = appView.dexItemFactory();
+  }
+
+  public void scan(
+      ProgramMethod programMethod, CfInstructionDesugaringEventConsumer eventConsumer) {
+    CfCode cfCode = programMethod.getDefinition().getCode().asCfCode();
+    for (CfInstruction instruction : cfCode.getInstructions()) {
+      scanInstruction(instruction, eventConsumer);
+    }
+  }
+
+  // The record rewriter scans the cf instructions to figure out if the record class needs to
+  // be added in the output. the analysis cannot be done in desugarInstruction because the analysis
+  // does not rewrite any instruction, and desugarInstruction is expected to rewrite at least one
+  // instruction for assertions to be valid.
+  private void scanInstruction(
+      CfInstruction instruction, CfInstructionDesugaringEventConsumer eventConsumer) {
+    assert !instruction.isInitClass();
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      if (refersToRecord(cfInvoke.getMethod())) {
+        requiresRecordClass(eventConsumer);
+      }
+      return;
+    }
+    if (instruction.isFieldInstruction()) {
+      CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
+      if (refersToRecord(fieldInstruction.getField())) {
+        requiresRecordClass(eventConsumer);
+      }
+      return;
+    }
+    if (instruction.isTypeInstruction()) {
+      CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
+      if (refersToRecord(typeInstruction.getType())) {
+        requiresRecordClass(eventConsumer);
+      }
+      return;
+    }
+    // TODO(b/179146128): Analyse MethodHandle and MethodType.
+  }
+
+  @Override
+  public Collection<CfInstruction> desugarInstruction(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext) {
+
+    // TODO(b/179146128): This is a temporary work-around to test desugaring of records
+    // without rewriting the record invoke-custom. This should be removed when the record support
+    // is complete.
+    if (instruction.isInvokeDynamic()
+        && context.getHolder().superType == factory.recordType
+        && (context.getReference().match(factory.recordMembers.toString)
+            || context.getReference().match(factory.recordMembers.hashCode)
+            || context.getReference().match(factory.recordMembers.equals))) {
+      requiresRecordClass(eventConsumer);
+      CfInstruction constant =
+          context.getReference().match(factory.recordMembers.toString)
+              ? new CfConstNull()
+              : new CfConstNumber(0, ValueType.INT);
+      return ImmutableList.of(new CfStackInstruction(CfStackInstruction.Opcode.Pop), constant);
+    }
+
+    CfInstruction desugaredInstruction = desugarInstruction(instruction, context);
+    return desugaredInstruction == null ? null : Collections.singletonList(desugaredInstruction);
+  }
+
+  private CfInstruction desugarInstruction(CfInstruction instruction, ProgramMethod context) {
+    assert !instruction.isInitClass();
+    // TODO(b/179146128): Rewrite record invoke-dynamic here.
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      DexMethod newMethod =
+          rewriteMethod(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()));
+      if (newMethod != cfInvoke.getMethod()) {
+        return new CfInvoke(cfInvoke.getOpcode(), newMethod, cfInvoke.isInterface());
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    assert !instruction.isInitClass();
+    // TODO(b/179146128): This is a temporary work-around to test desugaring of records
+    // without rewriting the record invoke-custom. This should be removed when the record support
+    // is complete.
+    if (instruction.isInvokeDynamic()
+        && context.getHolder().superType == factory.recordType
+        && (context.getName() == factory.toStringMethodName
+            || context.getName() == factory.hashCodeMethodName
+            || context.getName() == factory.equalsMethodName)) {
+      return true;
+    }
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      return needsDesugaring(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()));
+    }
+    return false;
+  }
+
+  private void requiresRecordClass(RecordDesugaringEventConsumer eventConsumer) {
+    DexProgramClass recordClass = synthesizeR8Record();
+    if (recordClass != null) {
+      eventConsumer.acceptRecordClass(recordClass);
+    }
+  }
+
+  @Override
+  public boolean needsDesugaring(DexProgramClass clazz) {
+    assert clazz.isRecord() || clazz.superType != factory.recordType;
+    return clazz.isRecord();
+  }
+
+  @Override
+  public void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer) {
+    if (clazz.isRecord()) {
+      assert clazz.superType == factory.recordType;
+      requiresRecordClass(eventConsumer);
+      clazz.accessFlags.unsetRecord();
+    }
+  }
+
+  private boolean refersToRecord(DexField field) {
+    assert !refersToRecord(field.holder) : "The java.lang.Record class has no fields.";
+    return refersToRecord(field.type);
+  }
+
+  private boolean refersToRecord(DexMethod method) {
+    if (refersToRecord(method.holder)) {
+      return true;
+    }
+    return refersToRecord(method.proto);
+  }
+
+  private boolean refersToRecord(DexProto proto) {
+    if (refersToRecord(proto.returnType)) {
+      return true;
+    }
+    return refersToRecord(proto.parameters.values);
+  }
+
+  private boolean refersToRecord(DexType[] types) {
+    for (DexType type : types) {
+      if (refersToRecord(type)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean refersToRecord(DexType type) {
+    return type == factory.recordType;
+  }
+
+  private boolean needsDesugaring(DexMethod method, boolean isSuper) {
+    return rewriteMethod(method, isSuper) != method;
+  }
+
+  @SuppressWarnings("ConstantConditions")
+  private DexMethod rewriteMethod(DexMethod method, boolean isSuper) {
+    if (method.holder != factory.recordType || method.isInstanceInitializer(factory)) {
+      return method;
+    }
+    assert method == factory.recordMembers.equals
+        || method == factory.recordMembers.hashCode
+        || method == factory.recordMembers.toString;
+    if (isSuper) {
+      // TODO(b/179146128): Support rewriting invoke-super to a Record method.
+      throw new CompilationError("Rewrite invoke-super to abstract method error.");
+    }
+    if (method == factory.recordMembers.equals) {
+      return factory.objectMembers.equals;
+    }
+    if (method == factory.recordMembers.toString) {
+      return factory.objectMembers.toString;
+    }
+    assert method == factory.recordMembers.hashCode;
+    return factory.objectMembers.toString;
+  }
+
+  private DexProgramClass synthesizeR8Record() {
+    DexItemFactory factory = appView.dexItemFactory();
+    DexClass recordClass =
+        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordType);
+    if (recordClass != null && recordClass.isProgramClass()) {
+      return null;
+    }
+    assert recordClass == null || recordClass.isLibraryClass();
+    DexEncodedMethod init = synthesizeRecordInitMethod();
+    // TODO(b/179146128): We may want to remove here the class from the library classes if present
+    //  in cf to cf.
+    return appView
+        .getSyntheticItems()
+        .createFixedClassFromType(
+            SyntheticNaming.SyntheticKind.RECORD_TAG,
+            factory.recordType,
+            factory,
+            builder -> builder.setAbstract().setDirectMethods(Collections.singletonList(init)));
+  }
+
+  private DexEncodedMethod synthesizeRecordInitMethod() {
+    MethodAccessFlags methodAccessFlags =
+        MethodAccessFlags.fromSharedAccessFlags(
+            Constants.ACC_SYNTHETIC | Constants.ACC_PROTECTED, true);
+    DexEncodedMethod init =
+        new DexEncodedMethod(
+            factory.recordMembers.init,
+            methodAccessFlags,
+            MethodTypeSignature.noSignature(),
+            DexAnnotationSet.empty(),
+            ParameterAnnotationsList.empty(),
+            null,
+            true);
+    init.setCode(
+        new CallObjectInitCfCodeProvider(appView, factory.r8RecordType).generateCfCode(), appView);
+    return init;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/CallObjectInitCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/CallObjectInitCfCodeProvider.java
new file mode 100644
index 0000000..77462a4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/CallObjectInitCfCodeProvider.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2019, 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.ir.synthetic;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.ValueType;
+import java.util.ArrayList;
+import java.util.List;
+import org.objectweb.asm.Opcodes;
+
+public class CallObjectInitCfCodeProvider extends SyntheticCfCodeProvider {
+
+  public CallObjectInitCfCodeProvider(AppView<?> appView, DexType holder) {
+    super(appView, holder);
+  }
+
+  @Override
+  public CfCode generateCfCode() {
+    DexItemFactory factory = appView.dexItemFactory();
+    List<CfInstruction> instructions = new ArrayList<>();
+    instructions.add(new CfLoad(ValueType.OBJECT, 0));
+    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, factory.objectMembers.constructor, false));
+    instructions.add(new CfReturnVoid());
+    return standardCfCodeFromInstructions(instructions);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java b/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
new file mode 100644
index 0000000..83fa84e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2019, 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.naming;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.naming.NamingLens.NonIdentityNamingLens;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.IdentityHashMap;
+
+// Naming lens for rewriting java.lang.Record to the internal RecordTag type.
+public class RecordRewritingNamingLens extends NonIdentityNamingLens {
+
+  final NamingLens namingLens;
+  private final DexItemFactory factory;
+
+  public static NamingLens createRecordRewritingNamingLens(AppView<?> appView) {
+    return createRecordRewritingNamingLens(appView, NamingLens.getIdentityLens());
+  }
+
+  public static NamingLens createRecordRewritingNamingLens(
+      AppView<?> appView, NamingLens namingLens) {
+    if (!appView.options().shouldDesugarRecords()) {
+      return namingLens;
+    }
+    return new RecordRewritingNamingLens(namingLens, appView);
+  }
+
+  public RecordRewritingNamingLens(NamingLens namingLens, AppView<?> appView) {
+    super(appView.dexItemFactory(), new IdentityHashMap<>());
+    this.namingLens = namingLens;
+    factory = appView.dexItemFactory();
+  }
+
+  private boolean isRenamed(DexType type) {
+    return getRenaming(type) != null;
+  }
+
+  private DexString getRenaming(DexType type) {
+    if (type == factory.recordType) {
+      return factory.r8RecordType.descriptor;
+    }
+    return null;
+  }
+
+  @Override
+  protected DexString internalLookupClassDescriptor(DexType type) {
+    DexString renaming = getRenaming(type);
+    return renaming != null ? renaming : namingLens.lookupDescriptor(type);
+  }
+
+  @Override
+  public DexString lookupInnerName(InnerClassAttribute attribute, InternalOptions options) {
+    assert !isRenamed(attribute.getInner());
+    return namingLens.lookupInnerName(attribute, options);
+  }
+
+  @Override
+  public DexString lookupName(DexMethod method) {
+    // Record rewriting does not influence method name.
+    return namingLens.lookupName(method);
+  }
+
+  @Override
+  public DexString lookupName(DexField field) {
+    // Record rewriting does not influence field name.
+    return namingLens.lookupName(field);
+  }
+
+  @Override
+  public DexString lookupDescriptorForJavaTypeName(String typeName) {
+    if (typeName.equals(factory.recordType.toSourceString())) {
+      return factory.r8RecordType.descriptor;
+    }
+    return namingLens.lookupDescriptorForJavaTypeName(typeName);
+  }
+
+  @Override
+  public String lookupPackageName(String packageName) {
+    return namingLens.lookupPackageName(packageName);
+  }
+
+  @Override
+  public boolean verifyRenamingConsistentWithResolution(DexMethod item) {
+    return namingLens.verifyRenamingConsistentWithResolution(item);
+  }
+}
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 3e7f5bc..68bca58 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -44,6 +44,10 @@
         context.getContextType(), context.getContextType(), context.getOrigin());
   }
 
+  static SynthesizingContext fromType(DexType type) {
+    return new SynthesizingContext(type, type, Origin.unknown());
+  }
+
   static SynthesizingContext fromNonSyntheticInputContext(ProgramDefinition context) {
     // A context that is itself non-synthetic is the single context, thus both the input context
     // and synthesizing context coincide.
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
index 01e6c75..126355f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -34,6 +34,7 @@
   private final DexType type;
   private final Origin origin;
 
+  private boolean isAbstract = false;
   private Kind originKind;
   private DexType superType;
   private DexTypeList interfaces = DexTypeList.empty();
@@ -70,6 +71,11 @@
     return self();
   }
 
+  public B setAbstract() {
+    isAbstract = true;
+    return self();
+  }
+
   public B setOriginKind(Kind originKind) {
     this.originKind = originKind;
     return self();
@@ -107,9 +113,10 @@
   }
 
   public C build() {
+    int flag = isAbstract ? Constants.ACC_ABSTRACT : Constants.ACC_FINAL;
     ClassAccessFlags accessFlags =
         ClassAccessFlags.fromSharedAccessFlags(
-            Constants.ACC_FINAL | Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
+            flag | Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
     DexString sourceFile = null;
     NestHostClassAttribute nestHost = null;
     List<NestMemberClassAttribute> nestMembers = Collections.emptyList();
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 2b24294..b2cf60e 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -338,7 +338,8 @@
     // Check that a context is never itself synthetic class.
     committed.forEachNonLegacyItem(
         item -> {
-          assert isNotSyntheticType(item.getContext().getSynthesizingContextType());
+          assert isNotSyntheticType(item.getContext().getSynthesizingContextType())
+              || item.getKind().allowSyntheticContext();
         });
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index f63d186..65c53d5 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -150,6 +150,7 @@
   @Override
   public DexClass definitionFor(DexType type, Function<DexType, DexClass> baseDefinitionFor) {
     DexClass clazz = null;
+    SyntheticKind kind = null;
     LegacySyntheticDefinition legacyItem = pending.legacyClasses.get(type);
     if (legacyItem != null) {
       clazz = legacyItem.getDefinition();
@@ -157,12 +158,15 @@
       SyntheticDefinition<?, ?, ?> item = pending.nonLegacyDefinitions.get(type);
       if (item != null) {
         clazz = item.getHolder();
+        kind = item.getKind();
         assert clazz.isProgramClass() == item.isProgramDefinition();
         assert clazz.isClasspathClass() == item.isClasspathDefinition();
       }
     }
     if (clazz != null) {
+      assert legacyItem != null || kind != null;
       assert baseDefinitionFor.apply(type) == null
+              || (kind != null && kind.mayOverridesNonProgramType)
           : "Pending synthetic definition also present in the active program: " + type;
       return clazz;
     }
@@ -386,6 +390,23 @@
     return clazz;
   }
 
+  public DexProgramClass createFixedClassFromType(
+      SyntheticKind kind,
+      DexType contextType,
+      DexItemFactory factory,
+      Consumer<SyntheticProgramClassBuilder> fn) {
+    // Obtain the outer synthesizing context in the case the context itself is synthetic.
+    // This is to ensure a flat input-type -> synthetic-item mapping.
+    SynthesizingContext outerContext = SynthesizingContext.fromType(contextType);
+    DexType type = SyntheticNaming.createFixedType(kind, outerContext, factory);
+    SyntheticProgramClassBuilder classBuilder =
+        new SyntheticProgramClassBuilder(type, outerContext, factory);
+    fn.accept(classBuilder);
+    DexProgramClass clazz = classBuilder.build();
+    addPendingDefinition(new SyntheticProgramClassDefinition(kind, outerContext, clazz));
+    return clazz;
+  }
+
   public DexClasspathClass createFixedClasspathClass(
       SyntheticKind kind, DexClasspathClass context, DexItemFactory factory) {
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
@@ -476,7 +497,12 @@
         if (!removedClasses.contains(definition.getHolder().getType())) {
           if (definition.isProgramDefinition()) {
             committedProgramTypesBuilder.add(definition.getHolder().getType());
-            appBuilder.addProgramClass(definition.asProgramDefinition().getHolder());
+            if (definition.getKind().mayOverridesNonProgramType) {
+              appBuilder.addProgramClassPotentiallyOverridingNonProgramClass(
+                  definition.asProgramDefinition().getHolder());
+            } else {
+              appBuilder.addProgramClass(definition.asProgramDefinition().getHolder());
+            }
           } else if (appBuilder.isDirect()) {
             assert definition.isClasspathDefinition();
             appBuilder.asDirect().addClasspathClass(definition.asClasspathDefinition().getHolder());
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 1b80f41..c3a9d9c 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -23,6 +23,7 @@
    */
   public enum SyntheticKind {
     // Class synthetics.
+    RECORD_TAG("", false, true, true),
     COMPANION_CLASS("CompanionClass", false),
     LAMBDA("Lambda", false),
     INIT_TYPE_ARGUMENT("-IA", false, true),
@@ -41,6 +42,7 @@
     public final String descriptor;
     public final boolean isSingleSyntheticMethod;
     public final boolean isFixedSuffixSynthetic;
+    public final boolean mayOverridesNonProgramType;
 
     SyntheticKind(String descriptor, boolean isSingleSyntheticMethod) {
       this(descriptor, isSingleSyntheticMethod, false);
@@ -48,9 +50,22 @@
 
     SyntheticKind(
         String descriptor, boolean isSingleSyntheticMethod, boolean isFixedSuffixSynthetic) {
+      this(descriptor, isSingleSyntheticMethod, isFixedSuffixSynthetic, false);
+    }
+
+    SyntheticKind(
+        String descriptor,
+        boolean isSingleSyntheticMethod,
+        boolean isFixedSuffixSynthetic,
+        boolean mayOverridesNonProgramType) {
       this.descriptor = descriptor;
       this.isSingleSyntheticMethod = isSingleSyntheticMethod;
       this.isFixedSuffixSynthetic = isFixedSuffixSynthetic;
+      this.mayOverridesNonProgramType = mayOverridesNonProgramType;
+    }
+
+    public boolean allowSyntheticContext() {
+      return this == RECORD_TAG;
     }
 
     public static SyntheticKind fromDescriptor(String descriptor) {
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
index 3c7b99b..c885d9e 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassMap.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -115,6 +116,21 @@
   }
 
   /**
+   * Clears the type so if a class with the given type was present, it cannot be found anymore. This
+   * has to be run at a join point, concurrent accesses may be confused.
+   */
+  public void clearType(DexType type) {
+    if (classes.containsKey(type)) {
+      classes.remove(type);
+    }
+    ClassProvider<T> provider = classProvider.get();
+    if (provider == null) {
+      return;
+    }
+    classProvider.set(provider.without(ImmutableSet.of(type)));
+  }
+
+  /**
    * Returns all classes from the collection. The collection must be force-loaded.
    */
   public List<T> getAllClasses() {
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
index c048529..af272b8 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -70,6 +71,10 @@
     return new PreloadedClassProvider<>(classKind, builder.build());
   }
 
+  public FilteringClassProvider<T> without(Set<DexType> filteredTypes) {
+    return new FilteringClassProvider(classKind, this, filteredTypes);
+  }
+
   /** Create class provider for preloaded classes. */
   public static <T extends DexClass> ClassProvider<T> combine(
       ClassKind<T> classKind, List<ClassProvider<T>> providers) {
@@ -145,6 +150,47 @@
     }
   }
 
+  /** Class provider which ignores a list of filtered classes */
+  private static class FilteringClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final ClassProvider<T> provider;
+    private final Set<DexType> filteredOut;
+
+    FilteringClassProvider(
+        ClassKind<T> classKind, ClassProvider<T> provider, Set<DexType> filteredOut) {
+      super(classKind);
+      assert !(provider instanceof FilteringClassProvider) : "Nested Filtering class providers";
+      this.provider = provider;
+      this.filteredOut = filteredOut;
+    }
+
+    @Override
+    public FilteringClassProvider<T> without(Set<DexType> filteredTypes) {
+      ImmutableSet<DexType> newSet =
+          ImmutableSet.<DexType>builder().addAll(filteredOut).addAll(filteredTypes).build();
+      return new FilteringClassProvider(getClassKind(), provider, newSet);
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      if (filteredOut.contains(type)) {
+        return;
+      }
+      provider.collectClass(type, classConsumer);
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      Collection<DexType> dexTypes = provider.collectTypes();
+      dexTypes.removeAll(filteredOut);
+      return dexTypes;
+    }
+
+    @Override
+    public String toString() {
+      return provider + " without " + filteredOut;
+    }
+  }
+
   private static class CombinedClassProvider<T extends DexClass> extends ClassProvider<T> {
     private final List<ClassProvider<T>> providers;
 
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 59d3b10..f2587df 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -490,9 +490,16 @@
     return !canUseNestBasedAccess();
   }
 
-  public boolean canUseRecords() {
-    // TODO(b/169645628): Replace by true when records are supported.
-    return testing.canUseRecords;
+  public boolean enableExperimentalRecordDesugaring() {
+    // TODO(b/169645628): Remove when records are supported.
+    return testing.enableExperimentalRecordDesugaring;
+  }
+
+  public boolean shouldDesugarRecords() {
+    if (!enableExperimentalRecordDesugaring()) {
+      return false;
+    }
+    return desugarState.isOn() && !canUseRecords();
   }
 
   public Set<String> extensiveLoggingFilter = getExtensiveLoggingFilter();
@@ -1294,7 +1301,7 @@
     public boolean enableSwitchToIfRewriting = true;
     public boolean enableEnumUnboxingDebugLogs = false;
     public boolean forceRedundantConstNumberRemoval = false;
-    public boolean canUseRecords = false;
+    public boolean enableExperimentalRecordDesugaring = false;
     public boolean invertConditionals = false;
     public boolean placeExceptionalBlocksLast = false;
     public boolean dontCreateMarkerInD8 = false;
@@ -1455,6 +1462,10 @@
     return !isDesugaring();
   }
 
+  public boolean canUseRecords() {
+    return !isDesugaring();
+  }
+
   public boolean canLeaveStaticInterfaceMethodInvokes() {
     return !isDesugaring() || hasMinApi(AndroidApiLevel.L);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
index c68ec40..2e97d1e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
@@ -57,7 +57,7 @@
             .addKeepMainRule(MAIN_TYPE)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.canUseRecords = true)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
             .compile()
             .writeToZip();
     RecordTestUtils.assertRecordsAreRecords(output);
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
index 811d24a..5fce5e2 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
@@ -57,7 +57,7 @@
             .addKeepMainRule(MAIN_TYPE)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.canUseRecords = true)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
             .compile()
             .writeToZip();
     RecordTestUtils.assertRecordsAreRecords(output);
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
index fc7a247..071af85 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
@@ -70,7 +70,7 @@
             .addKeepMainRule(MAIN_TYPE)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.canUseRecords = true)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
             .compile()
             .writeToZip();
     RecordTestUtils.assertRecordsAreRecords(output);
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
index eca975d..15684fe 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
@@ -67,7 +67,7 @@
             .addKeepMainRule(MAIN_TYPE)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.canUseRecords = true)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
             .compile()
             .writeToZip();
     RecordTestUtils.assertRecordsAreRecords(output);
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java b/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
index 3a0a306..7589764 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
@@ -99,7 +99,8 @@
   }
 
   public static void assertRecordsAreRecords(Path output) throws IOException {
-    CodeInspector inspector = new CodeInspector(output, opt -> opt.testing.canUseRecords = true);
+    CodeInspector inspector =
+        new CodeInspector(output, opt -> opt.testing.enableExperimentalRecordDesugaring = true);
     for (FoundClassSubject clazz : inspector.allClasses()) {
       if (clazz.getDexProgramClass().superType.toString().equals("java.lang.Record")) {
         assertTrue(clazz.getDexProgramClass().isRecord());
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
index e6d1444..610b061 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
@@ -61,7 +61,7 @@
             .addKeepMainRule(MAIN_TYPE)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.canUseRecords = true)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
             .compile()
             .writeToZip();
     RecordTestUtils.assertRecordsAreRecords(output);
diff --git a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
index ff540d1..d5d2b1a 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.List;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -36,20 +37,37 @@
   public static List<Object[]> data() {
     // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
     return buildParameters(
-        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk15())
+            .withDexRuntimes()
+            .withAllApiLevels()
+            .build());
   }
 
   @Test
-  public void testJvm() throws Exception {
-    testForJvm()
+  public void testD8AndJvm() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClassFileData(PROGRAM_DATA)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      // TODO(b/179146128): Add a test for D8 cf to cf.
+      return;
+    }
+    testForD8()
         .addProgramClassFileData(PROGRAM_DATA)
-        .enablePreview()
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+        .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
   @Test
   public void testR8Cf() throws Exception {
+    Assume.assumeTrue(parameters.isCfRuntime());
     Path output =
         testForR8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA)
@@ -58,7 +76,7 @@
             .addKeepMainRule(MAIN_TYPE)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.canUseRecords = true)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
             .compile()
             .writeToZip();
     RecordTestUtils.assertRecordsAreRecords(output);