Support Record dex merge

Bug: 169645628
Change-Id: I0e023e2008bb441e6fcc1848834661ddea35ea43
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index cd78995..26bbc5e 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.InstructionFactory;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ApplicationReaderMap;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexAnnotation;
@@ -79,6 +80,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
@@ -992,9 +994,12 @@
   private void populateTypes() {
     DexSection dexSection = lookupSection(Constants.TYPE_TYPE_ID_ITEM);
     assert verifyOrderOfTypeIds(dexSection);
+    Map<DexType, DexType> typeMap = ApplicationReaderMap.getTypeMap(options);
     indexedItems.initializeTypes(dexSection.length);
     for (int i = 0; i < dexSection.length; i++) {
-      indexedItems.setType(i, typeAt(i));
+      DexType type = typeAt(i);
+      DexType actualType = typeMap.getOrDefault(type, type);
+      indexedItems.setType(i, actualType);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/ApplicationReaderMap.java b/src/main/java/com/android/tools/r8/graph/ApplicationReaderMap.java
new file mode 100644
index 0000000..82b9d7f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ApplicationReaderMap.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.graph;
+
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+public class ApplicationReaderMap {
+
+  public static Map<String, String> getDescriptorMap(InternalOptions options) {
+    ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+    if (options.shouldDesugarRecords()) {
+      builder.put(DexItemFactory.recordTagDescriptorString, DexItemFactory.recordDescriptorString);
+    }
+    return builder.build();
+  }
+
+  public static Map<DexType, DexType> getTypeMap(InternalOptions options) {
+    DexItemFactory factory = options.dexItemFactory();
+    ImmutableMap.Builder<DexType, DexType> builder = ImmutableMap.builder();
+    getDescriptorMap(options)
+        .forEach(
+            (k, v) -> {
+              builder.put(factory.createType(k), factory.createType(v));
+            });
+    return builder.build();
+  }
+}
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 1339701..8ba291b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -64,6 +64,8 @@
 
   public static final String throwableDescriptorString = "Ljava/lang/Throwable;";
   public static final String dalvikAnnotationSignatureString = "Ldalvik/annotation/Signature;";
+  public static final String recordTagDescriptorString = "Lcom/android/tools/r8/RecordTag;";
+  public static final String recordDescriptorString = "Ljava/lang/Record;";
 
   /** Set of types that may be synthesized during compilation. */
   private final Set<DexType> possibleCompilerSynthesizedTypes = Sets.newIdentityHashSet();
@@ -215,8 +217,8 @@
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
   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 recordDescriptor = createString(recordDescriptorString);
+  public final DexString recordTagDescriptor = createString(recordTagDescriptorString);
   public final DexString objectArrayDescriptor = createString("[Ljava/lang/Object;");
   public final DexString classDescriptor = createString("Ljava/lang/Class;");
   public final DexString classLoaderDescriptor = createString("Ljava/lang/ClassLoader;");
@@ -348,7 +350,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 recordTagType = createStaticallyKnownType(recordTagDescriptor);
   public final DexType objectArrayType = createStaticallyKnownType(objectArrayDescriptor);
   public final DexType classArrayType = createStaticallyKnownType(classArrayDescriptor);
   public final DexType enumType = createStaticallyKnownType(enumDescriptor);
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 75b592b..a38770a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -315,6 +315,14 @@
     return isDoubleType() || isLongType();
   }
 
+  public boolean isSynthesizedTypeAllowedDuplication() {
+    // If we are desugaring Records, then the r8Record type is mapped back to java.lang.Record, and
+    // java.lang.Record can be duplicated.
+    // If we are not desugaring Records, then the r8Record type can be duplicated instead.
+    return descriptor.toString().equals(DexItemFactory.recordDescriptorString)
+        || descriptor.toString().equals(DexItemFactory.recordTagDescriptorString);
+  }
+
   public boolean isLegacySynthesizedTypeAllowedDuplication() {
     return oldSynthesizedName(toSourceString());
   }
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 8b5977f..67e7b5b 100644
--- a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import org.objectweb.asm.Type;
 
@@ -23,9 +24,11 @@
   private final ConcurrentHashMap<String, Type> asmObjectTypeCache = new ConcurrentHashMap<>();
   private final ConcurrentHashMap<String, Type> asmTypeCache = new ConcurrentHashMap<>();
   private final ConcurrentHashMap<String, DexString> stringCache = new ConcurrentHashMap<>();
+  private final Map<String, String> typeDescriptorMap;
 
   public JarApplicationReader(InternalOptions options) {
     this.options = options;
+    typeDescriptorMap = ApplicationReaderMap.getDescriptorMap(options);
   }
 
   public Type getAsmObjectType(String name) {
@@ -55,7 +58,8 @@
 
   public DexType getTypeFromDescriptor(String desc) {
     assert isValidDescriptor(desc);
-    return options.itemFactory.createType(getString(desc));
+    String actualDesc = typeDescriptorMap.getOrDefault(desc, desc);
+    return options.itemFactory.createType(getString(actualDesc));
   }
 
   public DexTypeList getTypeListFromNames(String[] names) {
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
index d94a747..469f119 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
@@ -191,7 +191,7 @@
             null,
             true);
     encodedMethod.setCode(
-        new RecordGetFieldsAsObjectsCfCodeProvider(appView, factory.r8RecordType, fields)
+        new RecordGetFieldsAsObjectsCfCodeProvider(appView, factory.recordTagType, fields)
             .generateCfCode(),
         appView);
     return new ProgramMethod(clazz, encodedMethod);
@@ -316,7 +316,6 @@
 
   @Override
   public boolean needsDesugaring(DexProgramClass clazz) {
-    assert clazz.isRecord() || clazz.superType != factory.recordType;
     return clazz.isRecord();
   }
 
@@ -460,6 +459,17 @@
 
   private DexProgramClass synthesizeR8Record() {
     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.toString());
+    }
     DexClass recordClass =
         appView.appInfo().definitionForWithoutExistenceAssert(factory.recordType);
     if (recordClass != null && recordClass.isProgramClass()) {
@@ -520,7 +530,7 @@
             null,
             true);
     init.setCode(
-        new CallObjectInitCfCodeProvider(appView, factory.r8RecordType).generateCfCode(), appView);
+        new CallObjectInitCfCodeProvider(appView, factory.recordTagType).generateCfCode(), appView);
     return init;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java b/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
index 83fa84e..e809317 100644
--- a/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
@@ -45,7 +45,7 @@
 
   private DexString getRenaming(DexType type) {
     if (type == factory.recordType) {
-      return factory.r8RecordType.descriptor;
+      return factory.recordTagType.descriptor;
     }
     return null;
   }
@@ -77,7 +77,7 @@
   @Override
   public DexString lookupDescriptorForJavaTypeName(String typeName) {
     if (typeName.equals(factory.recordType.toSourceString())) {
-      return factory.r8RecordType.descriptor;
+      return factory.recordTagType.descriptor;
     }
     return namingLens.lookupDescriptorForJavaTypeName(typeName);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
index fff5425..8c3e592 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -62,10 +62,7 @@
     // All other conflicts are reported as a fatal error.
     return (DexProgramClass a, DexProgramClass b) -> {
       assert a.type == b.type;
-      if (a.originatesFromDexResource()
-          && b.originatesFromDexResource()
-          && a.accessFlags.isSynthetic()
-          && b.accessFlags.isSynthetic()) {
+      if (a.accessFlags.isSynthetic() && b.accessFlags.isSynthetic()) {
         return mergeClasses(reporter, a, b);
       }
       throw reportDuplicateTypes(reporter, a, b);
@@ -82,7 +79,8 @@
 
   private static DexProgramClass mergeClasses(
       Reporter reporter, DexProgramClass a, DexProgramClass b) {
-    if (a.type.isLegacySynthesizedTypeAllowedDuplication()) {
+    if (a.type.isLegacySynthesizedTypeAllowedDuplication()
+        || a.type.isSynthesizedTypeAllowedDuplication()) {
       assert assertEqualClasses(a, b);
       return a;
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
new file mode 100644
index 0000000..bd08a25
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.records;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.InternalOptions.TestingOptions;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RecordMergeTest extends TestBase {
+
+  private static final String RECORD_NAME_1 = "RecordWithMembers";
+  private static final byte[][] PROGRAM_DATA_1 = RecordTestUtils.getProgramData(RECORD_NAME_1);
+  private static final String MAIN_TYPE_1 = RecordTestUtils.getMainType(RECORD_NAME_1);
+  private static final String EXPECTED_RESULT_1 =
+      StringUtils.lines(
+          "BobX", "43", "BobX", "43", "FelixX", "-1", "FelixX", "-1", "print", "Bob43", "extra");
+
+  private static final String RECORD_NAME_2 = "SimpleRecord";
+  private static final byte[][] PROGRAM_DATA_2 = RecordTestUtils.getProgramData(RECORD_NAME_2);
+  private static final String MAIN_TYPE_2 = RecordTestUtils.getMainType(RECORD_NAME_2);
+  private static final String EXPECTED_RESULT_2 =
+      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42");
+
+  private final TestParameters parameters;
+
+  public RecordMergeTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters()
+            .withCustomRuntime(CfRuntime.getCheckedInJdk15())
+            .withDexRuntimes()
+            .withAllApiLevelsAlsoForCf()
+            .build());
+  }
+
+  @Test
+  public void testMergeDesugaredInputs() throws Exception {
+    Path output1 =
+        testForD8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA_1)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+            .compile()
+            .writeToZip();
+    Path output2 =
+        testForD8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA_2)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+            .compile()
+            .writeToZip();
+    D8TestCompileResult result =
+        testForD8(parameters.getBackend())
+            .addProgramFiles(output1, output2)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .compile();
+    result.run(parameters.getRuntime(), MAIN_TYPE_1).assertSuccessWithOutput(EXPECTED_RESULT_1);
+    result.run(parameters.getRuntime(), MAIN_TYPE_2).assertSuccessWithOutput(EXPECTED_RESULT_2);
+  }
+
+  @Test
+  public void testMergeDesugaredAndNonDesugaredInputs() throws Exception {
+    Path output1 =
+        testForD8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA_1)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+            .compile()
+            .writeToZip();
+    D8TestCompileResult result =
+        testForD8(parameters.getBackend())
+            .addProgramFiles(output1)
+            .addProgramClassFileData(PROGRAM_DATA_2)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+            .compile();
+    result.run(parameters.getRuntime(), MAIN_TYPE_1).assertSuccessWithOutput(EXPECTED_RESULT_1);
+    result.run(parameters.getRuntime(), MAIN_TYPE_2).assertSuccessWithOutput(EXPECTED_RESULT_2);
+  }
+}