Compiler APIs for global synthetic classes.

Bug: b/208788455
Change-Id: I960be943dfeaf6675fb87261c0468b5368833f9e
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 9884112..7f74668 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AssertionConfigurationWithDefault;
 import com.android.tools.r8.utils.DumpInputFlags;
+import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramProvider;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
 import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
@@ -82,6 +83,9 @@
   public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> {
 
     private boolean intermediate = false;
+    private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
+    private List<GlobalSyntheticsResourceProvider> globalSyntheticsResourceProviders =
+        new ArrayList<>();
     private DesugarGraphConsumer desugarGraphConsumer = null;
     private StringConsumer desugaredLibraryKeepRuleConsumer = null;
     private String synthesizedClassPrefix = "";
@@ -144,6 +148,16 @@
     /**
      * Indicate if compilation is to intermediate results, i.e., intended for later merging.
      *
+     * <p>When compiling to intermediate mode, the compiler will avoid sharing of synthetic items,
+     * and instead annotate them as synthetics for possible later merging. For global synthetics,
+     * the compiler will emit these to a separate consumer (see {@code GlobalSyntheticsConsumer}
+     * with the expectation that a later build step will consume them again as part of a
+     * non-intermediate build (see {@code GlobalSyntheticsResourceProvider}. Synthetic items
+     * typically come from the desugaring of various language features, such as lambdas and default
+     * interface methods. Global synthetics are non-local in that many compilation units may
+     * reference the same synthetic. For example, desugaring records requires a global tag to
+     * distinguish the class of all records.
+     *
      * <p>Intermediate mode is implied if compiling results to a "file-per-class-file".
      */
     public Builder setIntermediate(boolean value) {
@@ -152,6 +166,43 @@
     }
 
     /**
+     * Set a consumer for receiving the global synthetic content for the given compilation.
+     *
+     * <p>Note: this consumer is ignored if the compilation is not an "intermediate mode"
+     * compilation.
+     */
+    public Builder setGlobalSyntheticsConsumer(GlobalSyntheticsConsumer globalSyntheticsConsumer) {
+      this.globalSyntheticsConsumer = globalSyntheticsConsumer;
+      return self();
+    }
+
+    /** Add global synthetics resource providers. */
+    public Builder addGlobalSyntheticsResourceProviders(
+        GlobalSyntheticsResourceProvider... providers) {
+      return addGlobalSyntheticsResourceProviders(Arrays.asList(providers));
+    }
+
+    /** Add global synthetics resource providers. */
+    public Builder addGlobalSyntheticsResourceProviders(
+        Collection<GlobalSyntheticsResourceProvider> providers) {
+      providers.forEach(globalSyntheticsResourceProviders::add);
+      return self();
+    }
+
+    /** Add global synthetics resource files. */
+    public Builder addGlobalSyntheticsFiles(Path... files) {
+      return addGlobalSyntheticsFiles(Arrays.asList(files));
+    }
+
+    /** Add global synthetics resource files. */
+    public Builder addGlobalSyntheticsFiles(Collection<Path> files) {
+      for (Path file : files) {
+        addGlobalSyntheticsResourceProviders(new GlobalSyntheticsResourceFile(file));
+      }
+      return self();
+    }
+
+    /**
      * Set a consumer for receiving the keep rules to use when compiling the desugared library for
      * the program being compiled in this compilation.
      *
@@ -294,6 +345,11 @@
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules =
           ProguardConfigurationParser.parse(mainDexRules, factory, getReporter());
 
+      if (!globalSyntheticsResourceProviders.isEmpty()) {
+        addProgramResourceProvider(
+            new InternalGlobalSyntheticsProgramProvider(globalSyntheticsResourceProviders));
+      }
+
       return new D8Command(
           getAppBuilder().build(),
           getMode(),
@@ -303,6 +359,7 @@
           getReporter(),
           getDesugaringState(),
           intermediate,
+          intermediate ? globalSyntheticsConsumer : null,
           isOptimizeMultidexForLinearAlloc(),
           getIncludeClassesChecksum(),
           getDexClassChecksumFilter(),
@@ -326,6 +383,7 @@
   static final String USAGE_MESSAGE = D8CommandParser.USAGE_MESSAGE;
 
   private final boolean intermediate;
+  private final GlobalSyntheticsConsumer globalSyntheticsConsumer;
   private final DesugarGraphConsumer desugarGraphConsumer;
   private final StringConsumer desugaredLibraryKeepRuleConsumer;
   private final DesugaredLibrarySpecification desugaredLibrarySpecification;
@@ -385,6 +443,7 @@
       Reporter diagnosticsHandler,
       DesugarState enableDesugaring,
       boolean intermediate,
+      GlobalSyntheticsConsumer globalSyntheticsConsumer,
       boolean optimizeMultidexForLinearAlloc,
       boolean encodeChecksum,
       BiPredicate<String, Long> dexClassChecksumFilter,
@@ -420,6 +479,7 @@
         mapIdProvider,
         null);
     this.intermediate = intermediate;
+    this.globalSyntheticsConsumer = globalSyntheticsConsumer;
     this.desugarGraphConsumer = desugarGraphConsumer;
     this.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
     this.desugaredLibrarySpecification = desugaredLibrarySpecification;
@@ -434,6 +494,7 @@
   private D8Command(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
     intermediate = false;
+    globalSyntheticsConsumer = null;
     desugarGraphConsumer = null;
     desugaredLibraryKeepRuleConsumer = null;
     desugaredLibrarySpecification = null;
@@ -468,6 +529,7 @@
     internal.setMinApiLevel(AndroidApiLevel.getAndroidApiLevel(getMinApiLevel()));
     internal.intermediate = intermediate;
     internal.retainCompileTimeAnnotations = intermediate;
+    internal.setGlobalSyntheticsConsumer(globalSyntheticsConsumer);
     internal.desugarGraphConsumer = desugarGraphConsumer;
     internal.mainDexKeepRules = mainDexKeepRules;
     internal.lineNumberOptimization = LineNumberOptimization.OFF;
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java
new file mode 100644
index 0000000..ea8ad11
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2022, 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;
+
+/**
+ * Consumer receiving the data representing global synthetics for the program.
+ *
+ * <p>Global synthetic information is only produced as part of D8 intermediate builds (e.g., for
+ * incremental compilation.) The global synthetic information represents desugaring content that may
+ * be duplicated among many intermediate-mode builds and will need to be merged to ensure a valid
+ * final program (i.e., a program that does not contain any duplicate definitions).
+ *
+ * <p>The data obtained for global synthetics must be passed to the subsequent compilation unit that
+ * builds a non-intermediate output. That compilation output can then be packaged as a final
+ * application. It is valid to merge just the globals in such a final step. See {@code
+ * GlobalSyntheticsResourceProvider}.
+ */
+@Keep
+public interface GlobalSyntheticsConsumer {
+
+  /**
+   * Callback to receive the data representing the global synthetics for the program.
+   *
+   * <p>The encoding of the global synthetics is compiler internal and may vary between compiler
+   * versions. The data received here is thus only valid as inputs to the same compiler version.
+   *
+   * @param bytes Opaque encoding of the global synthetics for the program.
+   */
+  void accept(byte[] bytes);
+}
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceFile.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceFile.java
new file mode 100644
index 0000000..598a074
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceFile.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2022, 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;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class GlobalSyntheticsResourceFile implements GlobalSyntheticsResourceProvider {
+
+  private final Path file;
+  private final Origin origin;
+
+  public GlobalSyntheticsResourceFile(Path file) {
+    this.file = file;
+    this.origin = new PathOrigin(file);
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public InputStream getByteStream() throws ResourceException {
+    try {
+      return Files.newInputStream(file);
+    } catch (IOException e) {
+      throw new ResourceException(origin, e);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceProvider.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceProvider.java
new file mode 100644
index 0000000..4cfdbb1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceProvider.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2022, 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;
+
+import com.android.tools.r8.origin.Origin;
+import java.io.InputStream;
+
+/**
+ * Interface to provide global synthetic information to the compiler.
+ *
+ * <p>The global synthetic information can only be obtained by consuming it from a previous
+ * compilation unit for the same compiler version. See {@code GlobalSyntheticsConsumer}.
+ */
+@Keep
+public interface GlobalSyntheticsResourceProvider {
+
+  /** Get the origin of the global synthetics resource. */
+  Origin getOrigin();
+
+  /** Get the bytes of the global synthetics resource. */
+  InputStream getByteStream() throws ResourceException;
+}
diff --git a/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java b/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java
index 5725955..15bf742 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.androidapi;
 
+import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DefaultInstanceInitializerCode;
@@ -233,7 +234,8 @@
     appView
         .appInfo()
         .getSyntheticItems()
-        .ensureFixedClassFromType(
+        .ensureGlobalClass(
+            () -> new MissingGlobalSyntheticsConsumerDiagnostic("API stubbing"),
             kinds -> kinds.API_MODEL_STUB,
             libraryClass.getType(),
             appView,
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index e7f4ad0..1e62a10 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.debuginfo.DebugRepresentation;
 import com.android.tools.r8.debuginfo.DebugRepresentation.DebugRepresentationPredicate;
 import com.android.tools.r8.dex.FileWriter.ByteBufferResult;
+import com.android.tools.r8.dex.VirtualFile.FilePerInputClassDistributor;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.features.FeatureSplitConfiguration.DataResourceProvidersAndConsumer;
 import com.android.tools.r8.graph.AppServices;
@@ -51,6 +52,7 @@
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramConsumer.InternalGlobalSyntheticsDexConsumer;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OriginalSourceFiles;
 import com.android.tools.r8.utils.PredicateUtils;
@@ -85,8 +87,10 @@
   private final Predicate<DexType> isTypeMissing;
   public List<Marker> markers;
   public List<DexString> markerStrings;
+  public Set<VirtualFile> globalSyntheticFiles;
 
   public DexIndexedConsumer programConsumer;
+  public InternalGlobalSyntheticsDexConsumer globalsSyntheticsConsumer;
 
   private static class SortAnnotations extends MixedSectionCollection {
 
@@ -179,20 +183,47 @@
 
   private List<VirtualFile> distribute(ExecutorService executorService)
       throws ExecutionException, IOException {
+    Collection<DexProgramClass> classes = appView.appInfo().classes();
+    Collection<DexProgramClass> globalSynthetics = new ArrayList<>();
+    if (appView.options().intermediate && appView.options().hasGlobalSyntheticsConsumer()) {
+      Collection<DexProgramClass> allClasses = classes;
+      classes = new ArrayList<>(allClasses.size());
+      for (DexProgramClass clazz : allClasses) {
+        if (appView.getSyntheticItems().isGlobalSyntheticClass(clazz)) {
+          globalSynthetics.add(clazz);
+        } else {
+          classes.add(clazz);
+        }
+      }
+    }
+
     // Distribute classes into dex files.
     VirtualFile.Distributor distributor;
     if (options.isGeneratingDexFilePerClassFile()) {
-      distributor = new VirtualFile.FilePerInputClassDistributor(this,
-          options.getDexFilePerClassFileConsumer().combineSyntheticClassesWithPrimaryClass());
+      distributor =
+          new VirtualFile.FilePerInputClassDistributor(
+              this,
+              classes,
+              options.getDexFilePerClassFileConsumer().combineSyntheticClassesWithPrimaryClass());
     } else if (!options.canUseMultidex()
         && options.mainDexKeepRules.isEmpty()
         && appView.appInfo().getMainDexInfo().isEmpty()
         && options.enableMainDexListCheck) {
-      distributor = new VirtualFile.MonoDexDistributor(this, options);
+      distributor = new VirtualFile.MonoDexDistributor(this, classes, options);
     } else {
-      distributor = new VirtualFile.FillFilesDistributor(this, options, executorService);
+      distributor = new VirtualFile.FillFilesDistributor(this, classes, options, executorService);
     }
-    return distributor.run();
+
+    List<VirtualFile> virtualFiles = distributor.run();
+    if (!globalSynthetics.isEmpty()) {
+      List<VirtualFile> files =
+          new FilePerInputClassDistributor(this, globalSynthetics, false).run();
+      globalSyntheticFiles = new HashSet<>(files);
+      virtualFiles.addAll(globalSyntheticFiles);
+      globalsSyntheticsConsumer =
+          new InternalGlobalSyntheticsDexConsumer(options.getGlobalSyntheticsConsumer());
+    }
+    return virtualFiles;
   }
 
   /**
@@ -330,6 +361,9 @@
                 executorService);
         merger.add(timings);
         merger.end();
+        if (globalsSyntheticsConsumer != null) {
+          globalsSyntheticsConsumer.finished(options.reporter);
+        }
       }
 
       // A consumer can manage the generated keep rules.
@@ -460,7 +494,11 @@
 
     ProgramConsumer consumer;
     ByteBufferProvider byteBufferProvider;
-    if (programConsumer != null) {
+
+    if (globalSyntheticFiles != null && globalSyntheticFiles.contains(virtualFile)) {
+      consumer = globalsSyntheticsConsumer;
+      byteBufferProvider = globalsSyntheticsConsumer;
+    } else if (programConsumer != null) {
       consumer = programConsumer;
       byteBufferProvider = programConsumer;
     } else if (virtualFile.getPrimaryClassDescriptor() != null) {
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 7d88ac7..827f43bd 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -307,6 +307,7 @@
     }
 
     public abstract List<VirtualFile> run() throws ExecutionException, IOException;
+
   }
 
   /**
@@ -316,11 +317,15 @@
    * may then be distributed in several individual virtual files.
    */
   public static class FilePerInputClassDistributor extends Distributor {
+    private final Collection<DexProgramClass> classes;
     private final boolean combineSyntheticClassesWithPrimaryClass;
 
-    FilePerInputClassDistributor(ApplicationWriter writer,
+    FilePerInputClassDistributor(
+        ApplicationWriter writer,
+        Collection<DexProgramClass> classes,
         boolean combineSyntheticClassesWithPrimaryClass) {
       super(writer);
+      this.classes = classes;
       this.combineSyntheticClassesWithPrimaryClass = combineSyntheticClassesWithPrimaryClass;
     }
 
@@ -329,7 +334,7 @@
       HashMap<DexProgramClass, VirtualFile> files = new HashMap<>();
       Collection<DexProgramClass> synthetics = new ArrayList<>();
       // Assign dedicated virtual files for all program classes.
-      for (DexProgramClass clazz : appView.appInfo().classes()) {
+      for (DexProgramClass clazz : classes) {
         // TODO(b/181636450): Simplify this making use of the assumption that synthetics are never
         //  duplicated.
         if (!combineSyntheticClassesWithPrimaryClass
@@ -370,9 +375,11 @@
     protected final VirtualFile mainDexFile;
     protected final InternalOptions options;
 
-    DistributorBase(ApplicationWriter writer, InternalOptions options) {
+    DistributorBase(
+        ApplicationWriter writer, Collection<DexProgramClass> classes, InternalOptions options) {
       super(writer);
       this.options = options;
+      this.classes = SetUtils.newIdentityHashSet(classes);
 
       // Create the primary dex file. The distribution will add more if needed.
       mainDexFile = new VirtualFile(0, writer.appView, writer.namingLens);
@@ -380,7 +387,6 @@
       virtualFiles.add(mainDexFile);
       addMarkers(mainDexFile);
 
-      classes = SetUtils.newIdentityHashSet(appView.appInfo().classes());
       originalNames =
           computeOriginalNameMapping(
               classes, appView.graphLens(), appView.appInfo().app().getProguardMap());
@@ -513,9 +519,12 @@
   public static class FillFilesDistributor extends DistributorBase {
     private final ExecutorService executorService;
 
-    FillFilesDistributor(ApplicationWriter writer, InternalOptions options,
+    FillFilesDistributor(
+        ApplicationWriter writer,
+        Collection<DexProgramClass> classes,
+        InternalOptions options,
         ExecutorService executorService) {
-      super(writer, options);
+      super(writer, classes, options);
       this.executorService = executorService;
     }
 
@@ -576,8 +585,9 @@
   }
 
   public static class MonoDexDistributor extends DistributorBase {
-    MonoDexDistributor(ApplicationWriter writer, InternalOptions options) {
-      super(writer, options);
+    MonoDexDistributor(
+        ApplicationWriter writer, Collection<DexProgramClass> classes, InternalOptions options) {
+      super(writer, classes, options);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/errors/MissingGlobalSyntheticsConsumerDiagnostic.java b/src/main/java/com/android/tools/r8/errors/MissingGlobalSyntheticsConsumerDiagnostic.java
new file mode 100644
index 0000000..ff99d05
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/MissingGlobalSyntheticsConsumerDiagnostic.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2022, 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.errors;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+public class MissingGlobalSyntheticsConsumerDiagnostic implements DesugarDiagnostic {
+
+  private final String generatingReason;
+
+  public MissingGlobalSyntheticsConsumerDiagnostic(String generatingReason) {
+    this.generatingReason = generatingReason;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return Origin.unknown();
+  }
+
+  @Override
+  public Position getPosition() {
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return "Invalid build configuration. "
+        + "Attempt to create a global synthetic for '"
+        + generatingReason
+        + "' without a global-synthetics consumer.";
+  }
+}
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 72884e9..810874b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -337,14 +337,6 @@
     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/ir/desugar/records/RecordDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
index a08a5ba..6a84f09 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
@@ -22,6 +22,7 @@
 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;
@@ -373,7 +374,8 @@
     checkRecordTagNotPresent(factory);
     appView
         .getSyntheticItems()
-        .ensureFixedClassFromType(
+        .ensureGlobalClass(
+            () -> new MissingGlobalSyntheticsConsumerDiagnostic("Record desugaring"),
             kinds -> kinds.RECORD_TAG,
             factory.recordType,
             appView,
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 11f0a2c..7f8d075 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -48,6 +48,7 @@
 import com.android.tools.r8.utils.AsmUtils;
 import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramConsumer.InternalGlobalSyntheticsCfConsumer;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OriginalSourceFiles;
 import com.android.tools.r8.utils.PredicateUtils;
@@ -57,6 +58,7 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
@@ -149,36 +151,67 @@
       sourceFileEnvironment = ApplicationWriter.createSourceFileEnvironment(proguardMapId);
     }
     LensCodeRewriterUtils rewriter = new LensCodeRewriterUtils(appView);
-    for (DexProgramClass clazz : application.classes()) {
-      assert SyntheticNaming.verifyNotInternalSynthetic(clazz.getType());
-      try {
-        writeClass(clazz, consumer, rewriter, markerString, sourceFileEnvironment);
-      } catch (ClassTooLargeException e) {
-        throw appView
-            .options()
-            .reporter
-            .fatalError(
-                new ConstantPoolOverflowDiagnostic(
-                    clazz.getOrigin(),
-                    Reference.classFromBinaryName(e.getClassName()),
-                    e.getConstantPoolCount()));
-      } catch (MethodTooLargeException e) {
-        throw appView
-            .options()
-            .reporter
-            .fatalError(
-                new CodeSizeOverflowDiagnostic(
-                    clazz.getOrigin(),
-                    Reference.methodFromDescriptor(
-                        Reference.classFromBinaryName(e.getClassName()).getDescriptor(),
-                        e.getMethodName(),
-                        e.getDescriptor()),
-                    e.getCodeSize()));
+    Collection<DexProgramClass> classes = application.classes();
+    Collection<DexProgramClass> globalSyntheticClasses = new ArrayList<>();
+    if (options.intermediate && options.hasGlobalSyntheticsConsumer()) {
+      Collection<DexProgramClass> allClasses = classes;
+      classes = new ArrayList<>(allClasses.size());
+      for (DexProgramClass clazz : allClasses) {
+        if (appView.getSyntheticItems().isGlobalSyntheticClass(clazz)) {
+          globalSyntheticClasses.add(clazz);
+        } else {
+          classes.add(clazz);
+        }
       }
     }
+    for (DexProgramClass clazz : classes) {
+      writeClassCatchingErrors(clazz, consumer, rewriter, markerString, sourceFileEnvironment);
+    }
+    if (!globalSyntheticClasses.isEmpty()) {
+      InternalGlobalSyntheticsCfConsumer globalsConsumer =
+          new InternalGlobalSyntheticsCfConsumer(options.getGlobalSyntheticsConsumer());
+      for (DexProgramClass clazz : globalSyntheticClasses) {
+        writeClassCatchingErrors(
+            clazz, globalsConsumer, rewriter, markerString, sourceFileEnvironment);
+      }
+      globalsConsumer.finished(options.reporter);
+    }
     ApplicationWriter.supplyAdditionalConsumers(application, appView, namingLens, options);
   }
 
+  private void writeClassCatchingErrors(
+      DexProgramClass clazz,
+      ClassFileConsumer consumer,
+      LensCodeRewriterUtils rewriter,
+      Optional<String> markerString,
+      SourceFileEnvironment sourceFileEnvironment) {
+    assert SyntheticNaming.verifyNotInternalSynthetic(clazz.getType());
+    try {
+      writeClass(clazz, consumer, rewriter, markerString, sourceFileEnvironment);
+    } catch (ClassTooLargeException e) {
+      throw appView
+          .options()
+          .reporter
+          .fatalError(
+              new ConstantPoolOverflowDiagnostic(
+                  clazz.getOrigin(),
+                  Reference.classFromBinaryName(e.getClassName()),
+                  e.getConstantPoolCount()));
+    } catch (MethodTooLargeException e) {
+      throw appView
+          .options()
+          .reporter
+          .fatalError(
+              new CodeSizeOverflowDiagnostic(
+                  clazz.getOrigin(),
+                  Reference.methodFromDescriptor(
+                      Reference.classFromBinaryName(e.getClassName()).getDescriptor(),
+                      e.getMethodName(),
+                      e.getDescriptor()),
+                  e.getCodeSize()));
+    }
+  }
+
   private void writeClass(
       DexProgramClass clazz,
       ClassFileConsumer consumer,
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 48ff52a..cee58c4 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.contexts.CompilationContext.UniqueContext;
+import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
@@ -278,6 +279,32 @@
     return isSyntheticClass(clazz.type);
   }
 
+  public boolean isGlobalSyntheticClass(DexProgramClass clazz) {
+    SyntheticDefinition<?, ?, ?> definition = pending.nonLegacyDefinitions.get(clazz.type);
+    if (definition != null) {
+      return definition.getKind().isGlobal();
+    }
+    return isGlobalReferences(committed.getNonLegacyClasses().get(clazz.type));
+  }
+
+  private static boolean isGlobalReferences(List<SyntheticProgramClassReference> references) {
+    if (references == null) {
+      return false;
+    }
+    if (references.size() == 1 && references.get(0).getKind().isGlobal()) {
+      return true;
+    }
+    assert verifyNoGlobals(references);
+    return false;
+  }
+
+  private static boolean verifyNoGlobals(List<SyntheticProgramClassReference> references) {
+    for (SyntheticProgramClassReference reference : references) {
+      assert !reference.getKind().isGlobal();
+    }
+    return true;
+  }
+
   public boolean isSyntheticOfKind(DexType type, SyntheticKindSelector kindSelector) {
     SyntheticKind kind = kindSelector.select(naming);
     return pending.containsTypeOfKind(type, kind) || committed.containsTypeOfKind(type, kind);
@@ -769,14 +796,20 @@
     }
   }
 
-  public DexProgramClass ensureFixedClassFromType(
+  public DexProgramClass ensureGlobalClass(
+      Supplier<MissingGlobalSyntheticsConsumerDiagnostic> diagnosticSupplier,
       SyntheticKindSelector kindSelector,
-      DexType contextType,
+      DexType globalType,
       AppView<?> appView,
       Consumer<SyntheticProgramClassBuilder> fn,
       Consumer<DexProgramClass> onCreationConsumer) {
     SyntheticKind kind = kindSelector.select(naming);
-    SynthesizingContext outerContext = SynthesizingContext.fromType(contextType);
+    assert kind.isGlobal();
+    if (appView.options().intermediate && !appView.options().hasGlobalSyntheticsConsumer()) {
+      appView.reporter().fatalError(diagnosticSupplier.get());
+    }
+    // A global type is its own context.
+    SynthesizingContext outerContext = SynthesizingContext.fromType(globalType);
     return internalEnsureFixedProgramClass(kind, fn, onCreationConsumer, outerContext, appView);
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 20bde1a..ef26890 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -31,6 +31,7 @@
   public static final String KT_EXTENSION = ".kt";
   public static final String MODULE_INFO_CLASS = "module-info.class";
   public static final String MODULES_PREFIX = "/modules";
+  public static final String GLOBAL_SYNTHETIC_EXTENSION = ".global";
 
   public static final boolean isAndroid =
       System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik");
diff --git a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
new file mode 100644
index 0000000..3945a6e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2022, 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.utils;
+
+import static com.android.tools.r8.utils.FileUtils.GLOBAL_SYNTHETIC_EXTENSION;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.Version;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public abstract class InternalGlobalSyntheticsProgramConsumer {
+
+  public static final String COMPILER_INFO_ENTRY_NAME = "compilerinfo";
+  public static final String OUTPUT_KIND_ENTRY_NAME = "kind";
+
+  private final GlobalSyntheticsConsumer consumer;
+  private final List<Pair<String, byte[]>> content = new ArrayList<>();
+
+  public InternalGlobalSyntheticsProgramConsumer(GlobalSyntheticsConsumer consumer) {
+    this.consumer = consumer;
+  }
+
+  public abstract Kind getKind();
+
+  synchronized void addGlobalSynthetic(String descriptor, byte[] data) {
+    add(getGlobalSyntheticFileName(descriptor), data);
+  }
+
+  private void add(String entryName, byte[] data) {
+    content.add(new Pair<>(entryName, data));
+  }
+
+  public void finished(DiagnosticsHandler handler) {
+    // Add meta information.
+    add(COMPILER_INFO_ENTRY_NAME, Version.getVersionString().getBytes(StandardCharsets.UTF_8));
+    add(OUTPUT_KIND_ENTRY_NAME, getKind().toString().getBytes(StandardCharsets.UTF_8));
+
+    // Size estimate to avoid reallocation of the byte output array.
+    final int zipHeaderOverhead = 500;
+    final int zipEntryOverhead = 200;
+    int estimatedZipSize =
+        zipHeaderOverhead
+            + ListUtils.fold(
+                content,
+                0,
+                (acc, pair) ->
+                    acc + pair.getFirst().length() + pair.getSecond().length + zipEntryOverhead);
+    ByteArrayOutputStream baos = new ByteArrayOutputStream(estimatedZipSize);
+    try (ZipOutputStream stream = new ZipOutputStream(baos)) {
+      for (Pair<String, byte[]> pair : content) {
+        ZipUtils.writeToZipStream(stream, pair.getFirst(), pair.getSecond(), ZipEntry.STORED);
+        // Clear out the bytes to avoid three copies when converting the boas.
+        pair.setSecond(null);
+      }
+    } catch (IOException e) {
+      handler.error(new ExceptionDiagnostic(e));
+    }
+    byte[] bytes = baos.toByteArray();
+    consumer.accept(bytes);
+  }
+
+  private static String getGlobalSyntheticFileName(String descriptor) {
+    assert descriptor != null && DescriptorUtils.isClassDescriptor(descriptor);
+    return DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor)
+        + GLOBAL_SYNTHETIC_EXTENSION;
+  }
+
+  public static class InternalGlobalSyntheticsDexConsumer
+      extends InternalGlobalSyntheticsProgramConsumer implements DexFilePerClassFileConsumer {
+
+    public InternalGlobalSyntheticsDexConsumer(GlobalSyntheticsConsumer consumer) {
+      super(consumer);
+    }
+
+    @Override
+    public Kind getKind() {
+      return Kind.DEX;
+    }
+
+    @Override
+    public void accept(
+        String primaryClassDescriptor,
+        ByteDataView data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      addGlobalSynthetic(primaryClassDescriptor, data.copyByteData());
+    }
+
+    @Override
+    public boolean combineSyntheticClassesWithPrimaryClass() {
+      return false;
+    }
+  }
+
+  public static class InternalGlobalSyntheticsCfConsumer
+      extends InternalGlobalSyntheticsProgramConsumer implements ClassFileConsumer {
+
+    public InternalGlobalSyntheticsCfConsumer(GlobalSyntheticsConsumer consumer) {
+      super(consumer);
+    }
+
+    @Override
+    public Kind getKind() {
+      return Kind.CF;
+    }
+
+    @Override
+    public void accept(ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+      addGlobalSynthetic(descriptor, data.copyByteData());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java
new file mode 100644
index 0000000..6b2d828
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2022, 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.utils;
+
+import com.android.tools.r8.GlobalSyntheticsResourceProvider;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.Version;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class InternalGlobalSyntheticsProgramProvider implements ProgramResourceProvider {
+
+  private final List<GlobalSyntheticsResourceProvider> providers;
+  private List<ProgramResource> resources = null;
+
+  public InternalGlobalSyntheticsProgramProvider(List<GlobalSyntheticsResourceProvider> providers) {
+    this.providers = providers;
+  }
+
+  @Override
+  public Collection<ProgramResource> getProgramResources() throws ResourceException {
+    if (resources == null) {
+      ensureResources();
+    }
+    return resources;
+  }
+
+  private synchronized void ensureResources() throws ResourceException {
+    if (resources != null) {
+      return;
+    }
+    List<ProgramResource> resources = new ArrayList<>();
+    Set<String> seen = new HashSet<>();
+    for (GlobalSyntheticsResourceProvider provider : providers) {
+      List<Function<Kind, ProgramResource>> delayedResouces = new ArrayList<>();
+      Kind providerKind = null;
+      try (ZipInputStream stream = new ZipInputStream(provider.getByteStream())) {
+        ZipEntry entry;
+        while (null != (entry = stream.getNextEntry())) {
+          String name = entry.getName();
+          if (name.equals(InternalGlobalSyntheticsProgramConsumer.OUTPUT_KIND_ENTRY_NAME)) {
+            providerKind =
+                Kind.valueOf(new String(ByteStreams.toByteArray(stream), StandardCharsets.UTF_8));
+          } else if (name.equals(
+              InternalGlobalSyntheticsProgramConsumer.COMPILER_INFO_ENTRY_NAME)) {
+            String version = new String(ByteStreams.toByteArray(stream), StandardCharsets.UTF_8);
+            if (!Version.getVersionString().equals(version)) {
+              throw new ResourceException(
+                  provider.getOrigin(),
+                  "Outdated or inconsistent global synthetics information."
+                      + "\nGlobal synthetics information version: "
+                      + version
+                      + "\nCompiler version: "
+                      + Version.getVersionString());
+            }
+          } else if (name.endsWith(FileUtils.GLOBAL_SYNTHETIC_EXTENSION) && seen.add(name)) {
+            ArchiveEntryOrigin origin = new ArchiveEntryOrigin(name, provider.getOrigin());
+            String descriptor = guessTypeDescriptor(name);
+            byte[] bytes = ByteStreams.toByteArray(stream);
+            Set<String> descriptors = Collections.singleton(descriptor);
+            delayedResouces.add(
+                kind -> OneShotByteResource.create(kind, origin, bytes, descriptors));
+          }
+        }
+      } catch (IOException e) {
+        throw new ResourceException(provider.getOrigin(), e);
+      }
+      if (providerKind == null) {
+        throw new ResourceException(
+            provider.getOrigin(),
+            "Invalid global synthetics provider does not specify its content kind.");
+      }
+      for (Function<Kind, ProgramResource> fn : delayedResouces) {
+        resources.add(fn.apply(providerKind));
+      }
+    }
+    this.resources = resources;
+  }
+
+  private String guessTypeDescriptor(String name) {
+    String noExt = name.substring(0, name.length() - FileUtils.GLOBAL_SYNTHETIC_EXTENSION.length());
+    String classExt = noExt + FileUtils.CLASS_EXTENSION;
+    return DescriptorUtils.guessTypeDescriptor(classExt);
+  }
+}
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 dc0a966..c11f715 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.GlobalSyntheticsConsumer;
 import com.android.tools.r8.MapIdProvider;
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.SourceFileProvider;
@@ -164,6 +165,8 @@
   // TODO(zerny): Make this private-final once we have full program-consumer support.
   public ProgramConsumer programConsumer = null;
 
+  private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
+
   public DataResourceConsumer dataResourceConsumer;
   public FeatureSplitConfiguration featureSplitConfiguration;
   public StartupConfiguration startupConfiguration;
@@ -444,6 +447,18 @@
     throw new UnsupportedOperationException("Cannot find internal output mode.");
   }
 
+  public boolean hasGlobalSyntheticsConsumer() {
+    return globalSyntheticsConsumer != null;
+  }
+
+  public GlobalSyntheticsConsumer getGlobalSyntheticsConsumer() {
+    return globalSyntheticsConsumer;
+  }
+
+  public void setGlobalSyntheticsConsumer(GlobalSyntheticsConsumer globalSyntheticsConsumer) {
+    this.globalSyntheticsConsumer = globalSyntheticsConsumer;
+  }
+
   public boolean isAndroidPlatform() {
     return minApiLevel == ANDROID_PLATFORM;
   }
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 8c3e592..a5bbfcb 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -79,8 +79,7 @@
 
   private static DexProgramClass mergeClasses(
       Reporter reporter, DexProgramClass a, DexProgramClass b) {
-    if (a.type.isLegacySynthesizedTypeAllowedDuplication()
-        || a.type.isSynthesizedTypeAllowedDuplication()) {
+    if (a.type.isLegacySynthesizedTypeAllowedDuplication()) {
       assert assertEqualClasses(a, b);
       return a;
     }
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index dd19c1e..37f2a5f 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -29,6 +29,11 @@
     JDK9("jdk9", 53),
     JDK10("jdk10", 54),
     JDK11("jdk11", 55),
+    JDK12("jdk12", 56),
+    JDK13("jdk13", 57),
+    JDK14("jdk14", 58),
+    JDK15("jdk15", 59),
+    JDK16("jdk16", 60),
     JDK17("jdk17", 61),
     JDK18("jdk18", 62),
     ;
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
index 5ec9cbc..501370c 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.compilerapi.assertionconfiguration.AssertionConfigurationTest;
 import com.android.tools.r8.compilerapi.desugardependencies.DesugarDependenciesTest;
+import com.android.tools.r8.compilerapi.globalsynthetics.GlobalSyntheticsTest;
 import com.android.tools.r8.compilerapi.inputdependencies.InputDependenciesTest;
 import com.android.tools.r8.compilerapi.mapid.CustomMapIdTest;
 import com.android.tools.r8.compilerapi.mockdata.MockClass;
@@ -39,7 +40,7 @@
           DesugarDependenciesTest.ApiTest.class);
 
   private static final List<Class<? extends CompilerApiTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
-      ImmutableList.of();
+      ImmutableList.of(GlobalSyntheticsTest.ApiTest.class);
 
   private final TemporaryFolder temp;
 
diff --git a/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java b/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java
new file mode 100644
index 0000000..edc42c2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2022, 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.compilerapi.globalsynthetics;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.GlobalSyntheticsResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.compilerapi.CompilerApiTest;
+import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
+import com.android.tools.r8.origin.Origin;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+public class GlobalSyntheticsTest extends CompilerApiTestRunner {
+
+  public GlobalSyntheticsTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<? extends CompilerApiTest> binaryTestClass() {
+    return ApiTest.class;
+  }
+
+  @Test
+  public void testGlobalSynthetics() throws Exception {
+    new ApiTest(ApiTest.PARAMETERS).run();
+  }
+
+  public static class ApiTest extends CompilerApiTest {
+
+    public ApiTest(Object parameters) {
+      super(parameters);
+    }
+
+    public void run() throws Exception {
+      GlobalSyntheticsResourceProvider provider =
+          new GlobalSyntheticsResourceProvider() {
+            @Override
+            public Origin getOrigin() {
+              return Origin.unknown();
+            }
+
+            @Override
+            public InputStream getByteStream() throws ResourceException {
+              throw new IllegalStateException();
+            }
+          };
+      List<GlobalSyntheticsResourceProvider> providers = new ArrayList<>();
+      // Don't actually add the provider as we don't have any bytes to return.
+      if (false) {
+        providers.add(provider);
+      }
+      D8.run(
+          D8Command.builder()
+              .addClassProgramData(getBytesForClass(getMockClass()), Origin.unknown())
+              .addLibraryFiles(getJava8RuntimeJar())
+              .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+              .setIntermediate(true)
+              .addGlobalSyntheticsFiles()
+              .addGlobalSyntheticsFiles(new ArrayList<>())
+              .addGlobalSyntheticsResourceProviders()
+              .addGlobalSyntheticsResourceProviders(providers)
+              .setGlobalSyntheticsConsumer(
+                  new GlobalSyntheticsConsumer() {
+                    @Override
+                    public void accept(byte[] bytes) {
+                      // Nothing is actually received here as MockClass does not give rise to
+                      // globals.
+                    }
+                  })
+              .build());
+    }
+
+    @Test
+    public void testGlobalSynthetics() throws Exception {
+      run();
+    }
+  }
+}
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
index 8401196..cf3c955 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
@@ -4,15 +4,25 @@
 
 package com.android.tools.r8.desugar.records;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
 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.BooleanUtils;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
+import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsConsumerAndProvider;
 import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
-import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,66 +44,93 @@
       StringUtils.lines("Jane Doe", "42", "Jane Doe", "42");
 
   private final TestParameters parameters;
-  private final boolean intermediate;
 
-  public RecordMergeTest(TestParameters parameters, boolean intermediate) {
+  public RecordMergeTest(TestParameters parameters) {
     this.parameters = parameters;
-    this.intermediate = intermediate;
   }
 
-  @Parameterized.Parameters(name = "{0}, intermediate: {1}")
-  public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
-    return buildParameters(
-        getTestParameters()
-            .withCustomRuntime(CfRuntime.getCheckedInJdk17())
-            .withDexRuntimes()
-            .withAllApiLevelsAlsoForCf()
-            .build(),
-        BooleanUtils.values());
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  @Test
+  public void testFailureWithoutGlobalSyntheticsConsumer() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClassFileData(PROGRAM_DATA_1)
+                .setMinApi(parameters.getApiLevel())
+                .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+                .setIntermediate(true)
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics
+                            .assertOnlyErrors()
+                            .assertErrorsMatch(
+                                diagnosticType(MissingGlobalSyntheticsConsumerDiagnostic.class))));
   }
 
   @Test
   public void testMergeDesugaredInputs() throws Exception {
+    GlobalSyntheticsConsumerAndProvider globals1 = new GlobalSyntheticsConsumerAndProvider();
     Path output1 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_1)
             .setMinApi(parameters.getApiLevel())
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .setIntermediate(intermediate)
+            .setIntermediate(true)
+            .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals1))
             .compile()
+            .inspect(this::assertDoesNotHaveRecordTag)
             .writeToZip();
+
+    GlobalSyntheticsConsumerAndProvider globals2 = new GlobalSyntheticsConsumerAndProvider();
     Path output2 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_2)
             .setMinApi(parameters.getApiLevel())
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .setIntermediate(intermediate)
+            .setIntermediate(true)
+            .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals2))
             .compile()
+            .inspect(this::assertDoesNotHaveRecordTag)
             .writeToZip();
+
+    assertTrue(globals1.hasBytes());
+    assertTrue(globals2.hasBytes());
+
     D8TestCompileResult result =
         testForD8(parameters.getBackend())
             .addProgramFiles(output1, output2)
+            .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals1, globals2))
             .setMinApi(parameters.getApiLevel())
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .compile();
+            .compile()
+            .inspect(this::assertHasRecordTag);
+
     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 {
+    GlobalSyntheticsConsumerAndProvider globals1 = new GlobalSyntheticsConsumerAndProvider();
     Path output1 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_1)
             .setMinApi(parameters.getApiLevel())
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .setIntermediate(intermediate)
+            .setIntermediate(true)
+            .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals1))
             .compile()
             .writeToZip();
+
     D8TestCompileResult result =
         testForD8(parameters.getBackend())
             .addProgramFiles(output1)
+            .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals1))
             .addProgramClassFileData(PROGRAM_DATA_2)
             .setMinApi(parameters.getApiLevel())
             .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
@@ -101,4 +138,48 @@
     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 testMergeNonIntermediates() throws Exception {
+    Path output1 =
+        testForD8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA_1)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .compile()
+            .inspect(this::assertHasRecordTag)
+            .writeToZip();
+
+    Path output2 =
+        testForD8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA_2)
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+            .compile()
+            .inspect(this::assertHasRecordTag)
+            .writeToZip();
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramFiles(output1, output2)
+                .setMinApi(parameters.getApiLevel())
+                .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics
+                            .assertOnlyErrors()
+                            .assertErrorsMatch(diagnosticType(DuplicateTypesDiagnostic.class))));
+  }
+
+  private void assertHasRecordTag(CodeInspector inspector) {
+    // Note: this should be asserting on record tag.
+    assertThat(inspector.clazz("java.lang.Record"), isPresent());
+  }
+
+  private void assertDoesNotHaveRecordTag(CodeInspector inspector) {
+    // Note: this should be asserting on record tag.
+    assertThat(inspector.clazz("java.lang.Record"), isAbsent());
+  }
 }
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 56dab1e..0a8a585 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
@@ -4,16 +4,20 @@
 
 package com.android.tools.r8.desugar.records;
 
+import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.GlobalSyntheticsConsumer;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsConsumerAndProvider;
+import com.android.tools.r8.utils.AndroidApiLevel;
 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.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -35,23 +39,32 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk17).
+    // TODO(b/174431251): Remove once jdk14 or above is added to default parameters.
     return buildParameters(
         getTestParameters()
             .withCustomRuntime(CfRuntime.getCheckedInJdk17())
-            .withDexRuntimes()
+            .withAllRuntimes()
             .withAllApiLevelsAlsoForCf()
             .build());
   }
 
+  private boolean isCfWithNativeRecordSupport() {
+    return parameters.isCfRuntime()
+        && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK14)
+        && parameters.getApiLevel().equals(AndroidApiLevel.B);
+  }
+
   @Test
-  public void testD8AndJvm() throws Exception {
-    if (parameters.isCfRuntime()) {
-      testForJvm()
-          .addProgramClassFileData(PROGRAM_DATA)
-          .run(parameters.getRuntime(), MAIN_TYPE)
-          .assertSuccessWithOutput(EXPECTED_RESULT);
-    }
+  public void testReference() throws Exception {
+    assumeTrue(isCfWithNativeRecordSupport());
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
     testForD8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         .setMinApi(parameters.getApiLevel())
@@ -66,10 +79,12 @@
 
   @Test
   public void testD8Intermediate() throws Exception {
-    Assume.assumeTrue(parameters.isDexRuntime());
-    Path path = compileIntermediate();
+    assumeTrue(parameters.isDexRuntime());
+    GlobalSyntheticsConsumerAndProvider globals = new GlobalSyntheticsConsumerAndProvider();
+    Path path = compileIntermediate(globals);
     testForD8()
         .addProgramFiles(path)
+        .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals))
         .setMinApi(parameters.getApiLevel())
         .setIncludeClassesChecksum(true)
         .run(parameters.getRuntime(), MAIN_TYPE)
@@ -78,11 +93,13 @@
 
   @Test
   public void testD8IntermediateNoDesugaringInStep2() throws Exception {
-    Assume.assumeTrue(parameters.isDexRuntime());
-    Path path = compileIntermediate();
+    assumeTrue(parameters.isDexRuntime());
+    GlobalSyntheticsConsumerAndProvider globals = new GlobalSyntheticsConsumerAndProvider();
+    Path path = compileIntermediate(globals);
     // In Android Studio they disable desugaring at this point to improve build speed.
     testForD8()
         .addProgramFiles(path)
+        .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals))
         .setMinApi(parameters.getApiLevel())
         .setIncludeClassesChecksum(true)
         .disableDesugaring()
@@ -90,19 +107,22 @@
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
-  private Path compileIntermediate() throws Exception {
+  private Path compileIntermediate(GlobalSyntheticsConsumer globalSyntheticsConsumer)
+      throws Exception {
     return testForD8(Backend.DEX)
         .addProgramClassFileData(PROGRAM_DATA)
         .setMinApi(parameters.getApiLevel())
         .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
         .setIntermediate(true)
         .setIncludeClassesChecksum(true)
+        .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globalSyntheticsConsumer))
         .compile()
         .writeToZip();
   }
 
   @Test
   public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || isCfWithNativeRecordSupport());
     R8FullTestBuilder builder =
         testForR8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA)
@@ -129,6 +149,7 @@
 
   @Test
   public void testR8NoMinification() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || isCfWithNativeRecordSupport());
     R8FullTestBuilder builder =
         testForR8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA)
diff --git a/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsConsumerAndProvider.java b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsConsumerAndProvider.java
new file mode 100644
index 0000000..57f5622
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsConsumerAndProvider.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2022, 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.synthesis.globals;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.GlobalSyntheticsResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.origin.Origin;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class GlobalSyntheticsConsumerAndProvider
+    implements GlobalSyntheticsConsumer, GlobalSyntheticsResourceProvider {
+
+  private byte[] bytes;
+
+  @Override
+  public void accept(byte[] bytes) {
+    assertNull(this.bytes);
+    assertNotNull(bytes);
+    this.bytes = bytes;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return Origin.unknown();
+  }
+
+  @Override
+  public InputStream getByteStream() throws ResourceException {
+    return new ByteArrayInputStream(bytes);
+  }
+
+  public boolean hasBytes() {
+    return true;
+  }
+}