Split RecordDesugaring class to prepare partial desugaring

Bug: b/357021427
Change-Id: I34f440497ed767b765825809cca6eba9248a938b
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
index 9b9989a..4b2ab5a 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
@@ -36,7 +36,7 @@
 import com.android.tools.r8.graph.ThrowExceptionCode;
 import com.android.tools.r8.ir.conversion.PrimaryD8L8IRConverter;
 import com.android.tools.r8.ir.desugar.TypeRewriter;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaring;
+import com.android.tools.r8.ir.desugar.records.RecordTagSynthesizer;
 import com.android.tools.r8.ir.desugar.varhandle.VarHandleDesugaring;
 import com.android.tools.r8.ir.desugar.varhandle.VarHandleDesugaringEventConsumer;
 import com.android.tools.r8.jar.CfApplicationWriter;
@@ -182,7 +182,7 @@
 
     List<ProgramMethod> methodsToProcess = new ArrayList<>();
     // Add global synthetic class for records.
-    RecordDesugaring.ensureRecordClassHelper(
+    RecordTagSynthesizer.ensureRecordClassHelper(
         appView,
         synthesizingContext,
         recordTagClass -> recordTagClass.programMethods().forEach(methodsToProcess::add),
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 74f9c92..5dcdfdb 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -45,8 +45,8 @@
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryAmender;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaring;
 import com.android.tools.r8.ir.desugar.records.RecordFieldValuesRewriter;
+import com.android.tools.r8.ir.desugar.records.RecordInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.varhandle.VarHandleDesugaring;
 import com.android.tools.r8.ir.optimize.AssertionsRewriter;
 import com.android.tools.r8.ir.optimize.Inliner;
@@ -328,7 +328,7 @@
         EnumUnboxingCfMethods.registerSynthesizedCodeReferences(appView.dexItemFactory());
       }
       if (options.shouldDesugarRecords()) {
-        RecordDesugaring.registerSynthesizedCodeReferences(appView.dexItemFactory());
+        RecordInstructionDesugaring.registerSynthesizedCodeReferences(appView.dexItemFactory());
       }
       if (options.shouldDesugarVarHandle()) {
         VarHandleDesugaring.registerSynthesizedCodeReferences(appView.dexItemFactory());
diff --git a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
index d6c5bdf..f07c75f 100644
--- a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaring;
+import com.android.tools.r8.ir.desugar.records.RecordInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.varhandle.VarHandleDesugaring;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -162,14 +162,15 @@
   }
 
   public void checkFieldForRecord(DexField dexField, ClassKind<?> classKind) {
-    if (options.shouldDesugarRecords() && RecordDesugaring.refersToRecord(dexField, getFactory())) {
+    if (options.shouldDesugarRecords()
+        && RecordInstructionDesugaring.refersToRecord(dexField, getFactory())) {
       addRecordWitness(dexField.getHolderType(), classKind);
     }
   }
 
   public void checkMethodForRecord(DexMethod dexMethod, ClassKind<?> classKind) {
     if (options.shouldDesugarRecords()
-        && RecordDesugaring.refersToRecord(dexMethod, getFactory())) {
+        && RecordInstructionDesugaring.refersToRecord(dexMethod, getFactory())) {
       addRecordWitness(dexMethod.getHolderType(), classKind);
     }
   }
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 fc40466..8789699 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
@@ -30,7 +30,7 @@
 import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.nest.D8NestBasedAccessDesugaring;
 import com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaring;
+import com.android.tools.r8.ir.desugar.records.RecordInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.stringconcat.StringConcatInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.twr.TwrInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.typeswitch.TypeSwitchDesugaring;
@@ -59,7 +59,7 @@
   private final List<CfInstructionDesugaring> yieldingDesugarings = new ArrayList<>();
 
   private final NestBasedAccessDesugaring nestBasedAccessDesugaring;
-  private final RecordDesugaring recordRewriter;
+  private final RecordInstructionDesugaring recordRewriter;
   private final DesugaredLibraryRetargeter desugaredLibraryRetargeter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final DesugaredLibraryAPIConverter desugaredLibraryAPIConverter;
@@ -111,7 +111,7 @@
     if (appView.options().enableTypeSwitchDesugaring) {
       desugarings.add(new TypeSwitchDesugaring(appView));
     }
-    recordRewriter = RecordDesugaring.create(appView);
+    recordRewriter = RecordInstructionDesugaring.create(appView);
     if (recordRewriter != null) {
       desugarings.add(recordRewriter);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
index 71527c8..4939de8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
@@ -4,581 +4,36 @@
 
 package com.android.tools.r8.ir.desugar.records;
 
-import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Dup;
-import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Swap;
-import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.isInvokeDynamicOnRecord;
-import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.parseInvokeDynamicOnRecord;
+import static com.android.tools.r8.ir.desugar.records.RecordTagSynthesizer.ensureRecordClass;
 
-import com.android.tools.r8.cf.code.CfConstClass;
-import com.android.tools.r8.cf.code.CfConstString;
-import com.android.tools.r8.cf.code.CfDexItemBasedConstString;
-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.CfInvokeDynamic;
-import com.android.tools.r8.cf.code.CfStackInstruction;
-import com.android.tools.r8.cf.code.CfTypeInstruction;
 import com.android.tools.r8.contexts.CompilationContext.ClassSynthesisDesugaringContext;
-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.errors.MissingGlobalSyntheticsConsumerDiagnostic;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexApplicationReadFlags;
 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.MethodAccessFlags;
-import com.android.tools.r8.graph.ProgramDefinition;
-import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
-import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaring;
 import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.DesugarDescription;
-import com.android.tools.r8.ir.desugar.LocalStackAllocator;
-import com.android.tools.r8.ir.desugar.ProgramAdditions;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordClassSynthesizerDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordInstructionDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.RecordInvokeDynamic;
-import com.android.tools.r8.ir.synthetic.CallObjectInitCfCodeProvider;
-import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordEqualsCfCodeProvider;
-import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordGetFieldsAsObjectsCfCodeProvider;
-import com.android.tools.r8.ir.synthetic.SyntheticCfCodeProvider;
-import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
-import java.util.function.BiFunction;
-import org.objectweb.asm.Opcodes;
 
-public class RecordDesugaring
-    implements CfInstructionDesugaring, CfClassSynthesizerDesugaring, CfPostProcessingDesugaring {
+public class RecordDesugaring implements CfClassSynthesizerDesugaring, CfPostProcessingDesugaring {
 
   private final AppView<?> appView;
   private final DexItemFactory factory;
-  private final DexProto recordToStringHelperProto;
-  private final DexProto recordHashCodeHelperProto;
-
-  public static final String GET_FIELDS_AS_OBJECTS_METHOD_NAME = "$record$getFieldsAsObjects";
-  public static final String EQUALS_RECORD_METHOD_NAME = "$record$equals";
 
   public static RecordDesugaring create(AppView<?> appView) {
     return appView.options().shouldDesugarRecords() ? new RecordDesugaring(appView) : null;
   }
 
-  public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
-    RecordCfMethods.registerSynthesizedCodeReferences(factory);
-    RecordGetFieldsAsObjectsCfCodeProvider.registerSynthesizedCodeReferences(factory);
-    RecordEqualsCfCodeProvider.registerSynthesizedCodeReferences(factory);
-  }
-
   private RecordDesugaring(AppView<?> appView) {
     this.appView = appView;
     factory = appView.dexItemFactory();
-    recordToStringHelperProto =
-        factory.createProto(
-            factory.stringType, factory.objectArrayType, factory.classType, factory.stringType);
-    recordHashCodeHelperProto =
-        factory.createProto(factory.intType, factory.classType, factory.objectArrayType);
-  }
-
-  @Override
-  public void prepare(
-      ProgramMethod method,
-      CfInstructionDesugaringEventConsumer eventConsumer,
-      ProgramAdditions programAdditions) {
-    CfCode cfCode = method.getDefinition().getCode().asCfCode();
-    for (CfInstruction instruction : cfCode.getInstructions()) {
-      if (instruction.isInvokeDynamic() && compute(instruction, method).needsDesugaring()) {
-        prepareInvokeDynamicOnRecord(
-            instruction.asInvokeDynamic(), programAdditions, method, eventConsumer);
-      }
-    }
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private void prepareInvokeDynamicOnRecord(
-      CfInvokeDynamic invokeDynamic,
-      ProgramAdditions programAdditions,
-      ProgramMethod context,
-      RecordInstructionDesugaringEventConsumer eventConsumer) {
-    RecordInvokeDynamic recordInvokeDynamic =
-        parseInvokeDynamicOnRecord(invokeDynamic.getCallSite(), appView);
-    if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName
-        || recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
-      ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions, context, eventConsumer);
-      return;
-    }
-    if (recordInvokeDynamic.getMethodName() == factory.equalsMethodName) {
-      ensureEqualsRecord(recordInvokeDynamic, programAdditions, context, eventConsumer);
-      return;
-    }
-    throw new Unreachable("Invoke dynamic needs record desugaring but could not be desugared.");
-  }
-
-  @Override
-  public void scan(
-      ProgramMethod programMethod, CfInstructionDesugaringEventConsumer eventConsumer) {
-    CfCode cfCode = programMethod.getDefinition().getCode().asCfCode();
-    for (CfInstruction instruction : cfCode.getInstructions()) {
-      scanInstruction(instruction, eventConsumer, programMethod);
-    }
-  }
-
-  // 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,
-      ProgramMethod context) {
-    assert !instruction.isInitClass();
-    if (instruction.isInvoke()) {
-      CfInvoke cfInvoke = instruction.asInvoke();
-      if (refersToRecord(cfInvoke.getMethod(), factory)) {
-        ensureRecordClass(eventConsumer, context);
-      }
-      return;
-    }
-    if (instruction.isFieldInstruction()) {
-      CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
-      if (refersToRecord(fieldInstruction.getField(), factory)) {
-        ensureRecordClass(eventConsumer, context);
-      }
-      return;
-    }
-    if (instruction.isTypeInstruction()) {
-      CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
-      if (refersToRecord(typeInstruction.getType(), factory)) {
-        ensureRecordClass(eventConsumer, context);
-      }
-      return;
-    }
-    // TODO(b/179146128): Analyse MethodHandle and MethodType.
-  }
-
-  @Override
-  @SuppressWarnings("ReferenceEquality")
-  public DesugarDescription compute(CfInstruction instruction, ProgramMethod context) {
-    if (instruction.isInvokeDynamic()) {
-      if (needsDesugaring(instruction.asInvokeDynamic(), context)) {
-        return desugarInvokeDynamic(instruction);
-      } else {
-        return DesugarDescription.nothing();
-      }
-    }
-    if (instruction.isInvoke()) {
-      CfInvoke cfInvoke = instruction.asInvoke();
-      if (needsDesugaring(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()))) {
-        DexMethod newMethod =
-            rewriteMethod(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()));
-        assert newMethod != cfInvoke.getMethod();
-        return desugarInvoke(cfInvoke, newMethod);
-      } else {
-        return DesugarDescription.nothing();
-      }
-    }
-    return DesugarDescription.nothing();
-  }
-
-  private DesugarDescription desugarInvokeDynamic(CfInstruction instruction) {
-    return DesugarDescription.builder()
-        .setDesugarRewrite(
-            (position,
-                freshLocalProvider,
-                localStackAllocator,
-                desugaringInfo,
-                eventConsumer,
-                context,
-                methodProcessingContext,
-                desugaringCollection,
-                dexItemFactory) ->
-                desugarInvokeDynamicOnRecord(
-                    instruction.asInvokeDynamic(),
-                    localStackAllocator,
-                    eventConsumer,
-                    context,
-                    methodProcessingContext))
-        .build();
-  }
-
-  private DesugarDescription desugarInvoke(CfInvoke invoke, DexMethod newMethod) {
-    return DesugarDescription.builder()
-        .setDesugarRewrite(
-            (position,
-                freshLocalProvider,
-                localStackAllocator,
-                desugaringInfo,
-                eventConsumer,
-                context,
-                methodProcessingContext,
-                desugaringCollection,
-                dexItemFactory) ->
-                Collections.singletonList(
-                    new CfInvoke(invoke.getOpcode(), newMethod, invoke.isInterface())))
-        .build();
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private List<CfInstruction> desugarInvokeDynamicOnRecord(
-      CfInvokeDynamic invokeDynamic,
-      LocalStackAllocator localStackAllocator,
-      CfInstructionDesugaringEventConsumer eventConsumer,
-      ProgramMethod context,
-      MethodProcessingContext methodProcessingContext) {
-    RecordInvokeDynamic recordInvokeDynamic =
-        parseInvokeDynamicOnRecord(invokeDynamic, appView, context);
-    if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName) {
-      return desugarInvokeRecordToString(
-          recordInvokeDynamic,
-          localStackAllocator,
-          eventConsumer,
-          context,
-          methodProcessingContext);
-    }
-    if (recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
-      return desugarInvokeRecordHashCode(
-          recordInvokeDynamic,
-          localStackAllocator,
-          eventConsumer,
-          context,
-          methodProcessingContext);
-    }
-    if (recordInvokeDynamic.getMethodName() == factory.equalsMethodName) {
-      return desugarInvokeRecordEquals(recordInvokeDynamic);
-    }
-    throw new Unreachable("Invoke dynamic needs record desugaring but could not be desugared.");
-  }
-
-  private ProgramMethod synthesizeEqualsRecordMethod(
-      DexProgramClass clazz, DexMethod getFieldsAsObjects, DexMethod method) {
-    return synthesizeMethod(
-        clazz, new RecordEqualsCfCodeProvider(appView, clazz.type, getFieldsAsObjects), method);
-  }
-
-  private ProgramMethod synthesizeGetFieldsAsObjectsMethod(
-      DexProgramClass clazz, DexField[] fields, DexMethod method) {
-    return synthesizeMethod(
-        clazz,
-        new RecordGetFieldsAsObjectsCfCodeProvider(appView, factory.recordTagType, fields),
-        method);
-  }
-
-  private ProgramMethod synthesizeMethod(
-      DexProgramClass clazz, SyntheticCfCodeProvider provider, DexMethod method) {
-    MethodAccessFlags methodAccessFlags =
-        MethodAccessFlags.fromSharedAccessFlags(
-            Constants.ACC_SYNTHETIC | Constants.ACC_PRIVATE, false);
-    DexEncodedMethod encodedMethod =
-        DexEncodedMethod.syntheticBuilder()
-            .setMethod(method)
-            .setAccessFlags(methodAccessFlags)
-            // Will be traced by the enqueuer.
-            .disableAndroidApiLevelCheck()
-            .build();
-    ProgramMethod result = new ProgramMethod(clazz, encodedMethod);
-    result.setCode(provider.generateCfCode(), appView);
-    return result;
-  }
-
-  private DexMethod ensureEqualsRecord(
-      RecordInvokeDynamic recordInvokeDynamic,
-      ProgramAdditions programAdditions,
-      ProgramMethod context,
-      RecordInstructionDesugaringEventConsumer eventConsumer) {
-    DexMethod getFieldsAsObjects =
-        ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions, context, eventConsumer);
-    DexProgramClass clazz = recordInvokeDynamic.getRecordClass();
-    DexMethod method = equalsRecordMethod(clazz.type);
-    assert clazz.lookupProgramMethod(method) == null;
-    ProgramMethod equalsHelperMethod =
-        programAdditions.ensureMethod(
-            method, () -> synthesizeEqualsRecordMethod(clazz, getFieldsAsObjects, method));
-    eventConsumer.acceptRecordEqualsHelperMethod(equalsHelperMethod, context);
-    return method;
-  }
-
-  private DexMethod ensureGetFieldsAsObjects(
-      RecordInvokeDynamic recordInvokeDynamic,
-      ProgramAdditions programAdditions,
-      ProgramMethod context,
-      RecordInstructionDesugaringEventConsumer eventConsumer) {
-    DexProgramClass clazz = recordInvokeDynamic.getRecordClass();
-    DexMethod method = getFieldsAsObjectsMethod(clazz.type);
-    assert clazz.lookupProgramMethod(method) == null;
-    ProgramMethod getFieldsAsObjectsHelperMethod =
-        programAdditions.ensureMethod(
-            method,
-            () ->
-                synthesizeGetFieldsAsObjectsMethod(clazz, recordInvokeDynamic.getFields(), method));
-    eventConsumer.acceptRecordGetFieldsAsObjectsHelperMethod(
-        getFieldsAsObjectsHelperMethod, context);
-    return method;
-  }
-
-  private DexMethod getFieldsAsObjectsMethod(DexType holder) {
-    return factory.createMethod(
-        holder, factory.createProto(factory.objectArrayType), GET_FIELDS_AS_OBJECTS_METHOD_NAME);
-  }
-
-  private DexMethod equalsRecordMethod(DexType holder) {
-    return factory.createMethod(
-        holder,
-        factory.createProto(factory.booleanType, factory.objectType),
-        EQUALS_RECORD_METHOD_NAME);
-  }
-
-  private ProgramMethod synthesizeRecordHelper(
-      DexProto helperProto,
-      BiFunction<DexItemFactory, DexMethod, CfCode> codeGenerator,
-      MethodProcessingContext methodProcessingContext) {
-    return appView
-        .getSyntheticItems()
-        .createMethod(
-            kinds -> kinds.RECORD_HELPER,
-            methodProcessingContext.createUniqueContext(),
-            appView,
-            builder ->
-                builder
-                    .setProto(helperProto)
-                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
-                    .setCode(methodSig -> codeGenerator.apply(appView.dexItemFactory(), methodSig))
-                    .disableAndroidApiLevelCheck());
-  }
-
-  private List<CfInstruction> desugarInvokeRecordHashCode(
-      RecordInvokeDynamic recordInvokeDynamic,
-      LocalStackAllocator localStackAllocator,
-      RecordInstructionDesugaringEventConsumer eventConsumer,
-      ProgramMethod context,
-      MethodProcessingContext methodProcessingContext) {
-    localStackAllocator.allocateLocalStack(1);
-    DexMethod getFieldsAsObjects = getFieldsAsObjectsMethod(recordInvokeDynamic.getRecordType());
-    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(getFieldsAsObjects) != null;
-    ArrayList<CfInstruction> instructions = new ArrayList<>();
-    instructions.add(new CfStackInstruction(Dup));
-    instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.objectMembers.getClass, false));
-    instructions.add(new CfStackInstruction(Swap));
-    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
-    ProgramMethod programMethod =
-        synthesizeRecordHelper(
-            recordHashCodeHelperProto,
-            RecordCfMethods::RecordMethods_hashCode,
-            methodProcessingContext);
-    eventConsumer.acceptRecordHashCodeHelperMethod(programMethod, context);
-    instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
-    return instructions;
-  }
-
-  private List<CfInstruction> desugarInvokeRecordEquals(RecordInvokeDynamic recordInvokeDynamic) {
-    DexMethod equalsRecord = equalsRecordMethod(recordInvokeDynamic.getRecordType());
-    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(equalsRecord) != null;
-    return Collections.singletonList(new CfInvoke(Opcodes.INVOKESPECIAL, equalsRecord, false));
-  }
-
-  private List<CfInstruction> desugarInvokeRecordToString(
-      RecordInvokeDynamic recordInvokeDynamic,
-      LocalStackAllocator localStackAllocator,
-      RecordInstructionDesugaringEventConsumer eventConsumer,
-      ProgramMethod context,
-      MethodProcessingContext methodProcessingContext) {
-    localStackAllocator.allocateLocalStack(2);
-    DexMethod getFieldsAsObjects = getFieldsAsObjectsMethod(recordInvokeDynamic.getRecordType());
-    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(getFieldsAsObjects)
-        != null;
-    ArrayList<CfInstruction> instructions = new ArrayList<>();
-    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
-    instructions.add(new CfConstClass(recordInvokeDynamic.getRecordType(), true));
-    if (appView.enableWholeProgramOptimizations()) {
-      instructions.add(
-          new CfDexItemBasedConstString(
-              recordInvokeDynamic.getRecordType(),
-              recordInvokeDynamic.computeRecordFieldNamesComputationInfo()));
-    } else {
-      instructions.add(new CfConstString(recordInvokeDynamic.getFieldNames()));
-    }
-    ProgramMethod programMethod =
-        synthesizeRecordHelper(
-            recordToStringHelperProto,
-            RecordCfMethods::RecordMethods_toString,
-            methodProcessingContext);
-    eventConsumer.acceptRecordToStringHelperMethod(programMethod, context);
-    instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
-    return instructions;
-  }
-
-  private void ensureRecordClass(
-      RecordInstructionDesugaringEventConsumer eventConsumer, ProgramMethod context) {
-    internalEnsureRecordClass(eventConsumer, null, eventConsumer, ImmutableList.of(context));
-  }
-
-  private void ensureRecordClass(
-      RecordClassSynthesizerDesugaringEventConsumer eventConsumer,
-      Collection<DexProgramClass> recordClasses) {
-    internalEnsureRecordClass(eventConsumer, eventConsumer, null, recordClasses);
-  }
-
-  /**
-   * If java.lang.Record is referenced from a class' supertype or a program method/field signature,
-   * then the global synthetic is generated upfront of the compilation to avoid confusing D8/R8.
-   *
-   * <p>However, if java.lang.Record is referenced only from an instruction, for example, the code
-   * contains "x instance of java.lang.Record" but no record type is present, then the global
-   * synthetic is generated during instruction desugaring scanning.
-   */
-  private DexProgramClass internalEnsureRecordClass(
-      RecordDesugaringEventConsumer eventConsumer,
-      RecordClassSynthesizerDesugaringEventConsumer recordClassSynthesizerDesugaringEventConsumer,
-      RecordInstructionDesugaringEventConsumer recordInstructionDesugaringEventConsumer,
-      Collection<? extends ProgramDefinition> contexts) {
-    DexItemFactory factory = appView.dexItemFactory();
-    checkRecordTagNotPresent(factory);
-    return ensureRecordClassHelper(
-        appView,
-        contexts,
-        eventConsumer,
-        recordClassSynthesizerDesugaringEventConsumer,
-        recordInstructionDesugaringEventConsumer);
-  }
-
-  public static DexProgramClass ensureRecordClassHelper(
-      AppView<?> appView,
-      Collection<? extends ProgramDefinition> contexts,
-      RecordDesugaringEventConsumer eventConsumer,
-      RecordClassSynthesizerDesugaringEventConsumer recordClassSynthesizerDesugaringEventConsumer,
-      RecordInstructionDesugaringEventConsumer recordInstructionDesugaringEventConsumer) {
-    return appView
-        .getSyntheticItems()
-        .ensureGlobalClass(
-            () -> new MissingGlobalSyntheticsConsumerDiagnostic("Record desugaring"),
-            kinds -> kinds.RECORD_TAG,
-            appView.dexItemFactory().recordType,
-            contexts,
-            appView,
-            builder -> {
-              DexEncodedMethod init = synthesizeRecordInitMethod(appView);
-              builder.setAbstract().setDirectMethods(ImmutableList.of(init));
-            },
-            eventConsumer::acceptRecordClass,
-            clazz -> {
-              if (recordClassSynthesizerDesugaringEventConsumer != null) {
-                for (ProgramDefinition context : contexts) {
-                  recordClassSynthesizerDesugaringEventConsumer.acceptRecordClassContext(
-                      clazz, context.asClass());
-                }
-              }
-              if (recordInstructionDesugaringEventConsumer != null) {
-                for (ProgramDefinition context : contexts) {
-                  recordInstructionDesugaringEventConsumer.acceptRecordClassContext(
-                      clazz, context.asMethod());
-                }
-              }
-            });
-  }
-
-  private void checkRecordTagNotPresent(DexItemFactory factory) {
-    DexClass r8RecordClass =
-        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordTagType);
-    if (r8RecordClass != null && r8RecordClass.isProgramClass()) {
-      appView
-          .options()
-          .reporter
-          .error(
-              "D8/R8 is compiling a mix of desugared and non desugared input using"
-                  + " java.lang.Record, but the application reader did not import correctly "
-                  + factory.recordTagType);
-    }
-  }
-
-  public static boolean refersToRecord(DexField field, DexItemFactory factory) {
-    assert !refersToRecord(field.holder, factory) : "The java.lang.Record class has no fields.";
-    return refersToRecord(field.type, factory);
-  }
-
-  public static boolean refersToRecord(DexMethod method, DexItemFactory factory) {
-    if (refersToRecord(method.holder, factory)) {
-      return true;
-    }
-    return refersToRecord(method.proto, factory);
-  }
-
-  private static boolean refersToRecord(DexProto proto, DexItemFactory factory) {
-    if (refersToRecord(proto.returnType, factory)) {
-      return true;
-    }
-    return refersToRecord(proto.parameters.values, factory);
-  }
-
-  private static boolean refersToRecord(DexType[] types, DexItemFactory factory) {
-    for (DexType type : types) {
-      if (refersToRecord(type, factory)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private static boolean refersToRecord(DexType type, DexItemFactory factory) {
-    return type == factory.recordType;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private boolean needsDesugaring(DexMethod method, boolean isSuper) {
-    return rewriteMethod(method, isSuper) != method;
-  }
-
-  private boolean needsDesugaring(CfInvokeDynamic invokeDynamic, ProgramMethod context) {
-    return isInvokeDynamicOnRecord(invokeDynamic, appView, context);
-  }
-
-  @SuppressWarnings({"ConstantConditions", "ReferenceEquality"})
-  private DexMethod rewriteMethod(DexMethod method, boolean isSuper) {
-    if (!(method == factory.recordMembers.equals
-        || method == factory.recordMembers.hashCode
-        || method == factory.recordMembers.toString)) {
-      return method;
-    }
-    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.hashCode;
-  }
-
-  private static DexEncodedMethod synthesizeRecordInitMethod(AppView<?> appView) {
-    MethodAccessFlags methodAccessFlags =
-        MethodAccessFlags.fromSharedAccessFlags(
-            Constants.ACC_SYNTHETIC | Constants.ACC_PROTECTED, true);
-    return DexEncodedMethod.syntheticBuilder()
-        .setMethod(appView.dexItemFactory().recordMembers.constructor)
-        .setAccessFlags(methodAccessFlags)
-        .setCode(
-            new CallObjectInitCfCodeProvider(appView, appView.dexItemFactory().recordTagType)
-                .generateCfCode())
-        // Will be traced by the enqueuer.
-        .disableAndroidApiLevelCheck()
-        .build();
   }
 
   @Override
@@ -599,7 +54,7 @@
         assert dexClass.isProgramClass();
         classes.add(dexClass.asProgramClass());
       }
-      ensureRecordClass(eventConsumer, classes);
+      ensureRecordClass(eventConsumer, classes, appView);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordInstructionDesugaring.java
new file mode 100644
index 0000000..d4bb7ec
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordInstructionDesugaring.java
@@ -0,0 +1,471 @@
+// Copyright (c) 2024, 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.records;
+
+import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Dup;
+import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Swap;
+import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.isInvokeDynamicOnRecord;
+import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.parseInvokeDynamicOnRecord;
+import static com.android.tools.r8.ir.desugar.records.RecordTagSynthesizer.ensureRecordClass;
+
+import com.android.tools.r8.cf.code.CfConstClass;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfDexItemBasedConstString;
+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.CfInvokeDynamic;
+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.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+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.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.DesugarDescription;
+import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.ir.desugar.ProgramAdditions;
+import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.RecordInvokeDynamic;
+import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordEqualsCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordGetFieldsAsObjectsCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.SyntheticCfCodeProvider;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+import org.objectweb.asm.Opcodes;
+
+public class RecordInstructionDesugaring implements CfInstructionDesugaring {
+
+  private final AppView<?> appView;
+  private final DexItemFactory factory;
+  private final DexProto recordToStringHelperProto;
+  private final DexProto recordHashCodeHelperProto;
+
+  public static final String GET_FIELDS_AS_OBJECTS_METHOD_NAME = "$record$getFieldsAsObjects";
+  public static final String EQUALS_RECORD_METHOD_NAME = "$record$equals";
+
+  public static RecordInstructionDesugaring create(AppView<?> appView) {
+    return appView.options().shouldDesugarRecords()
+        ? new RecordInstructionDesugaring(appView)
+        : null;
+  }
+
+  public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
+    RecordCfMethods.registerSynthesizedCodeReferences(factory);
+    RecordGetFieldsAsObjectsCfCodeProvider.registerSynthesizedCodeReferences(factory);
+    RecordEqualsCfCodeProvider.registerSynthesizedCodeReferences(factory);
+  }
+
+  private RecordInstructionDesugaring(AppView<?> appView) {
+    this.appView = appView;
+    factory = appView.dexItemFactory();
+    recordToStringHelperProto =
+        factory.createProto(
+            factory.stringType, factory.objectArrayType, factory.classType, factory.stringType);
+    recordHashCodeHelperProto =
+        factory.createProto(factory.intType, factory.classType, factory.objectArrayType);
+  }
+
+  @Override
+  public void prepare(
+      ProgramMethod method,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramAdditions programAdditions) {
+    CfCode cfCode = method.getDefinition().getCode().asCfCode();
+    for (CfInstruction instruction : cfCode.getInstructions()) {
+      if (instruction.isInvokeDynamic() && compute(instruction, method).needsDesugaring()) {
+        prepareInvokeDynamicOnRecord(
+            instruction.asInvokeDynamic(), programAdditions, method, eventConsumer);
+      }
+    }
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private void prepareInvokeDynamicOnRecord(
+      CfInvokeDynamic invokeDynamic,
+      ProgramAdditions programAdditions,
+      ProgramMethod context,
+      RecordInstructionDesugaringEventConsumer eventConsumer) {
+    RecordInvokeDynamic recordInvokeDynamic =
+        parseInvokeDynamicOnRecord(invokeDynamic.getCallSite(), appView);
+    if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName
+        || recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
+      ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions, context, eventConsumer);
+      return;
+    }
+    if (recordInvokeDynamic.getMethodName() == factory.equalsMethodName) {
+      ensureEqualsRecord(recordInvokeDynamic, programAdditions, context, eventConsumer);
+      return;
+    }
+    throw new Unreachable("Invoke dynamic needs record desugaring but could not be desugared.");
+  }
+
+  @Override
+  public void scan(
+      ProgramMethod programMethod, CfInstructionDesugaringEventConsumer eventConsumer) {
+    CfCode cfCode = programMethod.getDefinition().getCode().asCfCode();
+    for (CfInstruction instruction : cfCode.getInstructions()) {
+      scanInstruction(instruction, eventConsumer, programMethod);
+    }
+  }
+
+  // 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,
+      ProgramMethod context) {
+    assert !instruction.isInitClass();
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      if (refersToRecord(cfInvoke.getMethod(), factory)) {
+        ensureRecordClass(eventConsumer, context, appView);
+      }
+      return;
+    }
+    if (instruction.isFieldInstruction()) {
+      CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
+      if (refersToRecord(fieldInstruction.getField(), factory)) {
+        ensureRecordClass(eventConsumer, context, appView);
+      }
+      return;
+    }
+    if (instruction.isTypeInstruction()) {
+      CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
+      if (refersToRecord(typeInstruction.getType(), factory)) {
+        ensureRecordClass(eventConsumer, context, appView);
+      }
+      return;
+    }
+    // TODO(b/179146128): Analyse MethodHandle and MethodType.
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public DesugarDescription compute(CfInstruction instruction, ProgramMethod context) {
+    if (instruction.isInvokeDynamic()) {
+      if (needsDesugaring(instruction.asInvokeDynamic(), context)) {
+        return desugarInvokeDynamic(instruction);
+      } else {
+        return DesugarDescription.nothing();
+      }
+    }
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      if (needsDesugaring(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()))) {
+        DexMethod newMethod =
+            rewriteMethod(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()));
+        assert newMethod != cfInvoke.getMethod();
+        return desugarInvoke(cfInvoke, newMethod);
+      } else {
+        return DesugarDescription.nothing();
+      }
+    }
+    return DesugarDescription.nothing();
+  }
+
+  private DesugarDescription desugarInvokeDynamic(CfInstruction instruction) {
+    return DesugarDescription.builder()
+        .setDesugarRewrite(
+            (position,
+                freshLocalProvider,
+                localStackAllocator,
+                desugaringInfo,
+                eventConsumer,
+                context,
+                methodProcessingContext,
+                desugaringCollection,
+                dexItemFactory) ->
+                desugarInvokeDynamicOnRecord(
+                    instruction.asInvokeDynamic(),
+                    localStackAllocator,
+                    eventConsumer,
+                    context,
+                    methodProcessingContext))
+        .build();
+  }
+
+  private DesugarDescription desugarInvoke(CfInvoke invoke, DexMethod newMethod) {
+    return DesugarDescription.builder()
+        .setDesugarRewrite(
+            (position,
+                freshLocalProvider,
+                localStackAllocator,
+                desugaringInfo,
+                eventConsumer,
+                context,
+                methodProcessingContext,
+                desugaringCollection,
+                dexItemFactory) ->
+                Collections.singletonList(
+                    new CfInvoke(invoke.getOpcode(), newMethod, invoke.isInterface())))
+        .build();
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private List<CfInstruction> desugarInvokeDynamicOnRecord(
+      CfInvokeDynamic invokeDynamic,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext) {
+    RecordInvokeDynamic recordInvokeDynamic =
+        parseInvokeDynamicOnRecord(invokeDynamic, appView, context);
+    if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName) {
+      return desugarInvokeRecordToString(
+          recordInvokeDynamic,
+          localStackAllocator,
+          eventConsumer,
+          context,
+          methodProcessingContext);
+    }
+    if (recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
+      return desugarInvokeRecordHashCode(
+          recordInvokeDynamic,
+          localStackAllocator,
+          eventConsumer,
+          context,
+          methodProcessingContext);
+    }
+    if (recordInvokeDynamic.getMethodName() == factory.equalsMethodName) {
+      return desugarInvokeRecordEquals(recordInvokeDynamic);
+    }
+    throw new Unreachable("Invoke dynamic needs record desugaring but could not be desugared.");
+  }
+
+  private ProgramMethod synthesizeEqualsRecordMethod(
+      DexProgramClass clazz, DexMethod getFieldsAsObjects, DexMethod method) {
+    return synthesizeMethod(
+        clazz, new RecordEqualsCfCodeProvider(appView, clazz.type, getFieldsAsObjects), method);
+  }
+
+  private ProgramMethod synthesizeGetFieldsAsObjectsMethod(
+      DexProgramClass clazz, DexField[] fields, DexMethod method) {
+    return synthesizeMethod(
+        clazz,
+        new RecordGetFieldsAsObjectsCfCodeProvider(appView, factory.recordTagType, fields),
+        method);
+  }
+
+  private ProgramMethod synthesizeMethod(
+      DexProgramClass clazz, SyntheticCfCodeProvider provider, DexMethod method) {
+    MethodAccessFlags methodAccessFlags =
+        MethodAccessFlags.fromSharedAccessFlags(
+            Constants.ACC_SYNTHETIC | Constants.ACC_PRIVATE, false);
+    DexEncodedMethod encodedMethod =
+        DexEncodedMethod.syntheticBuilder()
+            .setMethod(method)
+            .setAccessFlags(methodAccessFlags)
+            // Will be traced by the enqueuer.
+            .disableAndroidApiLevelCheck()
+            .build();
+    ProgramMethod result = new ProgramMethod(clazz, encodedMethod);
+    result.setCode(provider.generateCfCode(), appView);
+    return result;
+  }
+
+  private void ensureEqualsRecord(
+      RecordInvokeDynamic recordInvokeDynamic,
+      ProgramAdditions programAdditions,
+      ProgramMethod context,
+      RecordInstructionDesugaringEventConsumer eventConsumer) {
+    DexMethod getFieldsAsObjects =
+        ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions, context, eventConsumer);
+    DexProgramClass clazz = recordInvokeDynamic.getRecordClass();
+    DexMethod method = equalsRecordMethod(clazz.type);
+    assert clazz.lookupProgramMethod(method) == null;
+    ProgramMethod equalsHelperMethod =
+        programAdditions.ensureMethod(
+            method, () -> synthesizeEqualsRecordMethod(clazz, getFieldsAsObjects, method));
+    eventConsumer.acceptRecordEqualsHelperMethod(equalsHelperMethod, context);
+  }
+
+  private DexMethod ensureGetFieldsAsObjects(
+      RecordInvokeDynamic recordInvokeDynamic,
+      ProgramAdditions programAdditions,
+      ProgramMethod context,
+      RecordInstructionDesugaringEventConsumer eventConsumer) {
+    DexProgramClass clazz = recordInvokeDynamic.getRecordClass();
+    DexMethod method = getFieldsAsObjectsMethod(clazz.type);
+    assert clazz.lookupProgramMethod(method) == null;
+    ProgramMethod getFieldsAsObjectsHelperMethod =
+        programAdditions.ensureMethod(
+            method,
+            () ->
+                synthesizeGetFieldsAsObjectsMethod(clazz, recordInvokeDynamic.getFields(), method));
+    eventConsumer.acceptRecordGetFieldsAsObjectsHelperMethod(
+        getFieldsAsObjectsHelperMethod, context);
+    return method;
+  }
+
+  private DexMethod getFieldsAsObjectsMethod(DexType holder) {
+    return factory.createMethod(
+        holder, factory.createProto(factory.objectArrayType), GET_FIELDS_AS_OBJECTS_METHOD_NAME);
+  }
+
+  private DexMethod equalsRecordMethod(DexType holder) {
+    return factory.createMethod(
+        holder,
+        factory.createProto(factory.booleanType, factory.objectType),
+        EQUALS_RECORD_METHOD_NAME);
+  }
+
+  private ProgramMethod synthesizeRecordHelper(
+      DexProto helperProto,
+      BiFunction<DexItemFactory, DexMethod, CfCode> codeGenerator,
+      MethodProcessingContext methodProcessingContext) {
+    return appView
+        .getSyntheticItems()
+        .createMethod(
+            kinds -> kinds.RECORD_HELPER,
+            methodProcessingContext.createUniqueContext(),
+            appView,
+            builder ->
+                builder
+                    .setProto(helperProto)
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setCode(methodSig -> codeGenerator.apply(appView.dexItemFactory(), methodSig))
+                    .disableAndroidApiLevelCheck());
+  }
+
+  private List<CfInstruction> desugarInvokeRecordHashCode(
+      RecordInvokeDynamic recordInvokeDynamic,
+      LocalStackAllocator localStackAllocator,
+      RecordInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext) {
+    localStackAllocator.allocateLocalStack(1);
+    DexMethod getFieldsAsObjects = getFieldsAsObjectsMethod(recordInvokeDynamic.getRecordType());
+    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(getFieldsAsObjects) != null;
+    ArrayList<CfInstruction> instructions = new ArrayList<>();
+    instructions.add(new CfStackInstruction(Dup));
+    instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.objectMembers.getClass, false));
+    instructions.add(new CfStackInstruction(Swap));
+    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
+    ProgramMethod programMethod =
+        synthesizeRecordHelper(
+            recordHashCodeHelperProto,
+            RecordCfMethods::RecordMethods_hashCode,
+            methodProcessingContext);
+    eventConsumer.acceptRecordHashCodeHelperMethod(programMethod, context);
+    instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
+    return instructions;
+  }
+
+  private List<CfInstruction> desugarInvokeRecordEquals(RecordInvokeDynamic recordInvokeDynamic) {
+    DexMethod equalsRecord = equalsRecordMethod(recordInvokeDynamic.getRecordType());
+    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(equalsRecord) != null;
+    return Collections.singletonList(new CfInvoke(Opcodes.INVOKESPECIAL, equalsRecord, false));
+  }
+
+  private List<CfInstruction> desugarInvokeRecordToString(
+      RecordInvokeDynamic recordInvokeDynamic,
+      LocalStackAllocator localStackAllocator,
+      RecordInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext) {
+    localStackAllocator.allocateLocalStack(2);
+    DexMethod getFieldsAsObjects = getFieldsAsObjectsMethod(recordInvokeDynamic.getRecordType());
+    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(getFieldsAsObjects) != null;
+    ArrayList<CfInstruction> instructions = new ArrayList<>();
+    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
+    instructions.add(new CfConstClass(recordInvokeDynamic.getRecordType(), true));
+    if (appView.enableWholeProgramOptimizations()) {
+      instructions.add(
+          new CfDexItemBasedConstString(
+              recordInvokeDynamic.getRecordType(),
+              recordInvokeDynamic.computeRecordFieldNamesComputationInfo()));
+    } else {
+      instructions.add(new CfConstString(recordInvokeDynamic.getFieldNames()));
+    }
+    ProgramMethod programMethod =
+        synthesizeRecordHelper(
+            recordToStringHelperProto,
+            RecordCfMethods::RecordMethods_toString,
+            methodProcessingContext);
+    eventConsumer.acceptRecordToStringHelperMethod(programMethod, context);
+    instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
+    return instructions;
+  }
+
+  public static boolean refersToRecord(DexField field, DexItemFactory factory) {
+    assert !refersToRecord(field.holder, factory) : "The java.lang.Record class has no fields.";
+    return refersToRecord(field.type, factory);
+  }
+
+  public static boolean refersToRecord(DexMethod method, DexItemFactory factory) {
+    if (refersToRecord(method.holder, factory)) {
+      return true;
+    }
+    return refersToRecord(method.proto, factory);
+  }
+
+  private static boolean refersToRecord(DexProto proto, DexItemFactory factory) {
+    if (refersToRecord(proto.returnType, factory)) {
+      return true;
+    }
+    return refersToRecord(proto.parameters.values, factory);
+  }
+
+  private static boolean refersToRecord(DexType[] types, DexItemFactory factory) {
+    for (DexType type : types) {
+      if (refersToRecord(type, factory)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private static boolean refersToRecord(DexType type, DexItemFactory factory) {
+    return type == factory.recordType;
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private boolean needsDesugaring(DexMethod method, boolean isSuper) {
+    return rewriteMethod(method, isSuper) != method;
+  }
+
+  private boolean needsDesugaring(CfInvokeDynamic invokeDynamic, ProgramMethod context) {
+    return isInvokeDynamicOnRecord(invokeDynamic, appView, context);
+  }
+
+  @SuppressWarnings({"ConstantConditions", "ReferenceEquality"})
+  private DexMethod rewriteMethod(DexMethod method, boolean isSuper) {
+    if (!(method == factory.recordMembers.equals
+        || method == factory.recordMembers.hashCode
+        || method == factory.recordMembers.toString)) {
+      return method;
+    }
+    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.hashCode;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordTagSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordTagSynthesizer.java
new file mode 100644
index 0000000..95b67e5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordTagSynthesizer.java
@@ -0,0 +1,127 @@
+// Copyright (c) 2024, 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.records;
+
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordClassSynthesizerDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.synthetic.CallObjectInitCfCodeProvider;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+public class RecordTagSynthesizer {
+
+  static void ensureRecordClass(
+      RecordInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      AppView<?> appView) {
+    internalEnsureRecordClass(
+        eventConsumer, null, eventConsumer, ImmutableList.of(context), appView);
+  }
+
+  static void ensureRecordClass(
+      RecordClassSynthesizerDesugaringEventConsumer eventConsumer,
+      Collection<DexProgramClass> recordClasses,
+      AppView<?> appView) {
+    internalEnsureRecordClass(eventConsumer, eventConsumer, null, recordClasses, appView);
+  }
+
+  /**
+   * If java.lang.Record is referenced from a class' supertype or a program method/field signature,
+   * then the global synthetic is generated upfront of the compilation to avoid confusing D8/R8.
+   *
+   * <p>However, if java.lang.Record is referenced only from an instruction, for example, the code
+   * contains "x instance of java.lang.Record" but no record type is present, then the global
+   * synthetic is generated during instruction desugaring scanning.
+   */
+  private static void internalEnsureRecordClass(
+      RecordDesugaringEventConsumer eventConsumer,
+      RecordClassSynthesizerDesugaringEventConsumer recordClassSynthesizerDesugaringEventConsumer,
+      RecordInstructionDesugaringEventConsumer recordInstructionDesugaringEventConsumer,
+      Collection<? extends ProgramDefinition> contexts,
+      AppView<?> appView) {
+    checkRecordTagNotPresent(appView);
+    ensureRecordClassHelper(
+        appView,
+        contexts,
+        eventConsumer,
+        recordClassSynthesizerDesugaringEventConsumer,
+        recordInstructionDesugaringEventConsumer);
+  }
+
+  public static void ensureRecordClassHelper(
+      AppView<?> appView,
+      Collection<? extends ProgramDefinition> contexts,
+      RecordDesugaringEventConsumer eventConsumer,
+      RecordClassSynthesizerDesugaringEventConsumer recordClassSynthesizerDesugaringEventConsumer,
+      RecordInstructionDesugaringEventConsumer recordInstructionDesugaringEventConsumer) {
+    appView
+        .getSyntheticItems()
+        .ensureGlobalClass(
+            () -> new MissingGlobalSyntheticsConsumerDiagnostic("Record desugaring"),
+            kinds -> kinds.RECORD_TAG,
+            appView.dexItemFactory().recordType,
+            contexts,
+            appView,
+            builder -> {
+              DexEncodedMethod init = synthesizeRecordInitMethod(appView);
+              builder.setAbstract().setDirectMethods(ImmutableList.of(init));
+            },
+            eventConsumer::acceptRecordClass,
+            clazz -> {
+              if (recordClassSynthesizerDesugaringEventConsumer != null) {
+                for (ProgramDefinition context : contexts) {
+                  recordClassSynthesizerDesugaringEventConsumer.acceptRecordClassContext(
+                      clazz, context.asClass());
+                }
+              }
+              if (recordInstructionDesugaringEventConsumer != null) {
+                for (ProgramDefinition context : contexts) {
+                  recordInstructionDesugaringEventConsumer.acceptRecordClassContext(
+                      clazz, context.asMethod());
+                }
+              }
+            });
+  }
+
+  private static void checkRecordTagNotPresent(AppView<?> appView) {
+    DexItemFactory factory = appView.dexItemFactory();
+    DexClass r8RecordClass =
+        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordTagType);
+    if (r8RecordClass != null && r8RecordClass.isProgramClass()) {
+      appView
+          .options()
+          .reporter
+          .error(
+              "D8/R8 is compiling a mix of desugared and non desugared input using"
+                  + " java.lang.Record, but the application reader did not import correctly "
+                  + factory.recordTagType);
+    }
+  }
+
+  private static DexEncodedMethod synthesizeRecordInitMethod(AppView<?> appView) {
+    MethodAccessFlags methodAccessFlags =
+        MethodAccessFlags.fromSharedAccessFlags(
+            Constants.ACC_SYNTHETIC | Constants.ACC_PROTECTED, true);
+    return DexEncodedMethod.syntheticBuilder()
+        .setMethod(appView.dexItemFactory().recordMembers.constructor)
+        .setAccessFlags(methodAccessFlags)
+        .setCode(
+            new CallObjectInitCfCodeProvider(appView, appView.dexItemFactory().recordTagType)
+                .generateCfCode())
+        // Will be traced by the enqueuer.
+        .disableAndroidApiLevelCheck()
+        .build();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
index f0cd1ab..020371a 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.profile.art.completeness;
 
-import static com.android.tools.r8.ir.desugar.records.RecordDesugaring.EQUALS_RECORD_METHOD_NAME;
-import static com.android.tools.r8.ir.desugar.records.RecordDesugaring.GET_FIELDS_AS_OBJECTS_METHOD_NAME;
+import static com.android.tools.r8.ir.desugar.records.RecordInstructionDesugaring.EQUALS_RECORD_METHOD_NAME;
+import static com.android.tools.r8.ir.desugar.records.RecordInstructionDesugaring.GET_FIELDS_AS_OBJECTS_METHOD_NAME;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.ifThen;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;