Custom conversion stubs

Change-Id: Ib2d67cf6c7e2a8cfb019c7913f96da47596a5075
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
index cd8d6b7..14c62be 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
@@ -57,6 +57,13 @@
     return field;
   }
 
+  public CfFieldInstruction withField(DexField newField) {
+    if (field == newField) {
+      return this;
+    }
+    return new CfFieldInstruction(opcode, newField, newField);
+  }
+
   public int getOpcode() {
     return opcode;
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 625430b..3237f29 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -77,6 +77,13 @@
     return method;
   }
 
+  public CfInvoke withMethod(DexMethod newMethod) {
+    if (method == newMethod) {
+      return this;
+    }
+    return new CfInvoke(opcode, newMethod, itf);
+  }
+
   public int getOpcode() {
     return opcode;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 9cc0945..af0ba94 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -1419,7 +1419,7 @@
     return new Builder(false);
   }
 
-  private static Builder builder(DexEncodedMethod from) {
+  public static Builder builder(DexEncodedMethod from) {
     return new Builder(from.isD8R8Synthesized(), from);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
index 5d49406..888707ff 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
@@ -53,9 +53,9 @@
       throws ExecutionException {
     List<DexProgramClass> classes = appView.appInfo().classes();
 
-      CfClassSynthesizerDesugaringEventConsumer classSynthesizerEventConsumer =
-          new CfClassSynthesizerDesugaringEventConsumer();
-      converter.classSynthesisDesugaring(executorService, classSynthesizerEventConsumer);
+    CfClassSynthesizerDesugaringEventConsumer classSynthesizerEventConsumer =
+        new CfClassSynthesizerDesugaringEventConsumer();
+    converter.classSynthesisDesugaring(executorService, classSynthesizerEventConsumer);
     if (!classSynthesizerEventConsumer.getSynthesizedClasses().isEmpty()) {
       classes =
           ImmutableList.<DexProgramClass>builder()
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java
index 3f60ffe..feb1b51 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java
@@ -4,8 +4,10 @@
 
 package com.android.tools.r8.ir.desugar;
 
+import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeterSynthesizerEventConsumer.DesugaredLibraryRetargeterL8SynthesizerEventConsumer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryCustomConversionEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer;
 import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizerEventConsumer.L8ProgramEmulatedInterfaceSynthesizerEventConsumer;
 import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer;
@@ -16,6 +18,7 @@
     implements L8ProgramEmulatedInterfaceSynthesizerEventConsumer,
         DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer,
         DesugaredLibraryRetargeterL8SynthesizerEventConsumer,
+        DesugaredLibraryCustomConversionEventConsumer,
         RecordDesugaringEventConsumer {
 
   private Set<DexProgramClass> synthesizedClasses = Sets.newConcurrentHashSet();
@@ -40,6 +43,11 @@
     synthesizedClasses.add(clazz);
   }
 
+  @Override
+  public void acceptCustomConversionClasspathClass(DexClasspathClass clazz) {
+    // Intentionally empty.
+  }
+
   public Set<DexProgramClass> getSynthesizedClasses() {
     return synthesizedClasses;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfCodeTypeRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/CfCodeTypeRewriter.java
new file mode 100644
index 0000000..8aa124c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfCodeTypeRewriter.java
@@ -0,0 +1,131 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar;
+
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfTypeInstruction;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.Code;
+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.DexType;
+import com.android.tools.r8.utils.ListUtils;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Rewrites all references to a type in a CfCode instance to another type.
+ *
+ * <p>The implementation is minimal for the needs of DesugaredLibraryCustomConversionRewriter.
+ */
+public class CfCodeTypeRewriter {
+
+  private final DexItemFactory factory;
+
+  public CfCodeTypeRewriter(DexItemFactory factory) {
+    this.factory = factory;
+  }
+
+  public DexEncodedMethod rewriteCfCode(
+      DexEncodedMethod method, Map<DexType, DexType> replacement) {
+    if (method.getCode() == null) {
+      assert false;
+      return null;
+    }
+    return DexEncodedMethod.builder(method)
+        .setMethod(rewriteMethod(method.getReference(), replacement))
+        .setCode(rewriteCfCode(method.getCode(), replacement))
+        .build();
+  }
+
+  private Code rewriteCfCode(Code code, Map<DexType, DexType> replacement) {
+    assert code.isCfCode();
+    CfCode cfCode = code.asCfCode();
+    List<CfInstruction> desugaredInstructions =
+        ListUtils.map(
+            cfCode.getInstructions(),
+            instruction -> rewriteCfInstruction(instruction, replacement));
+    cfCode.setInstructions(desugaredInstructions);
+    return cfCode;
+  }
+
+  private CfInstruction rewriteCfInstruction(
+      CfInstruction instruction, Map<DexType, DexType> replacement) {
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      DexMethod replacementMethod = rewriteMethod(cfInvoke.getMethod(), replacement);
+      return cfInvoke.withMethod(replacementMethod);
+    }
+    if (instruction.isTypeInstruction()) {
+      CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
+      return typeInstruction.withType(rewriteType(typeInstruction.getType(), replacement));
+    }
+    if (instruction.isFieldInstruction()) {
+      CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
+      DexField field = rewriteField(fieldInstruction.getField(), replacement);
+      return fieldInstruction.withField(field);
+    }
+    if (instruction.isFrame()) {
+      CfFrame cfFrame = instruction.asFrame();
+      return rewriteFrame(cfFrame, replacement);
+    }
+    assert !instruction.isInvokeDynamic();
+    return instruction;
+  }
+
+  private CfFrame rewriteFrame(CfFrame cfFrame, Map<DexType, DexType> replacement) {
+    Int2ReferenceAVLTreeMap<FrameType> newLocals = new Int2ReferenceAVLTreeMap<>();
+    cfFrame
+        .getLocals()
+        .forEach((i, frameType) -> newLocals.put(i, rewriteFrameType(frameType, replacement)));
+    Deque<FrameType> newStack = new ArrayDeque<>();
+    cfFrame
+        .getStack()
+        .forEach(frameType -> newStack.addLast(rewriteFrameType(frameType, replacement)));
+    return new CfFrame(newLocals, newStack);
+  }
+
+  private FrameType rewriteFrameType(FrameType frameType, Map<DexType, DexType> replacement) {
+    if (frameType.isInitialized()) {
+      return FrameType.initialized(rewriteType(frameType.getInitializedType(), replacement));
+    }
+    if (frameType.isUninitializedNew()) {
+      FrameType.uninitializedNew(
+          frameType.getUninitializedLabel(),
+          rewriteType(frameType.getUninitializedNewType(), replacement));
+    }
+    return frameType;
+  }
+
+  private DexType rewriteType(DexType type, Map<DexType, DexType> replacement) {
+    return replacement.getOrDefault(type, type);
+  }
+
+  private DexField rewriteField(DexField field, Map<DexType, DexType> replacement) {
+    DexType newHolder = rewriteType(field.holder, replacement);
+    DexType newType = rewriteType(field.type, replacement);
+    return factory.createField(newHolder, newType, field.name);
+  }
+
+  private DexMethod rewriteMethod(DexMethod reference, Map<DexType, DexType> replacement) {
+    DexType newHolder = rewriteType(reference.holder, replacement);
+    DexType newReturnType = rewriteType(reference.getReturnType(), replacement);
+    DexType[] newParameters = new DexType[reference.getArity()];
+    for (int i = 0; i < reference.getArity(); i++) {
+      newParameters[i] = rewriteType(reference.getParameter(i), replacement);
+    }
+    return factory.createMethod(
+        newHolder, factory.createProto(newReturnType, newParameters), reference.getName());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 2493dd3..39d072a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -100,6 +100,11 @@
       }
 
       @Override
+      public void acceptCustomConversionClasspathClass(DexClasspathClass clazz) {
+        assert false;
+      }
+
+      @Override
       public void acceptAPIConversion(ProgramMethod method) {
         assert false;
       }
@@ -282,6 +287,11 @@
     }
 
     @Override
+    public void acceptCustomConversionClasspathClass(DexClasspathClass clazz) {
+      // Intentionally empty.
+    }
+
+    @Override
     public void acceptAPIConversion(ProgramMethod method) {
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
     }
@@ -436,6 +446,11 @@
     }
 
     @Override
+    public void acceptCustomConversionClasspathClass(DexClasspathClass clazz) {
+      additions.addLiveClasspathClass(clazz);
+    }
+
+    @Override
     public void acceptAPIConversion(ProgramMethod method) {
       // Intentionally empty. The method will be hit by tracing if required.
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java
index 8f5178e..486915a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java
@@ -118,6 +118,11 @@
     public void acceptWrapperClasspathClass(DexClasspathClass clazz) {
       // Intentionally empty.
     }
+
+    @Override
+    public void acceptCustomConversionClasspathClass(DexClasspathClass clazz) {
+      // Intentionally empty.
+    }
   }
 
   public static class R8PostProcessingDesugaringEventConsumer
@@ -191,5 +196,10 @@
     public void acceptWrapperClasspathClass(DexClasspathClass clazz) {
       additions.addLiveClasspathClass(clazz);
     }
+
+    @Override
+    public void acceptCustomConversionClasspathClass(DexClasspathClass clazz) {
+      additions.addLiveClasspathClass(clazz);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryCustomConversionRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryCustomConversionRewriter.java
new file mode 100644
index 0000000..2a800f8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryCustomConversionRewriter.java
@@ -0,0 +1,160 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryAPIConverter.vivifiedTypeFor;
+import static com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizer.conversionEncodedMethod;
+
+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.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.CfCodeTypeRewriter;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryCustomConversionEventConsumer;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * L8 specific pass, rewrites or generates custom conversions.
+ *
+ * <p>In normal set-ups, custom conversions are provided with the types: type <-> rewrittenType.
+ * However the compiler uses internally the types: vivifiedType <-> type during compilation. This
+ * pass rewrites the custom conversions from type <-> rewrittenType to vivifiedType <-> type so the
+ * compilation can look-up and find the methods.
+ *
+ * <p>In broken set-ups, where the input is missing, instead generates the custom conversions on the
+ * classpath since they are assumed to be present in L8.
+ */
+public class DesugaredLibraryCustomConversionRewriter implements CfClassSynthesizerDesugaring {
+
+  private final AppView<?> appView;
+  private final DesugaredLibraryWrapperSynthesizer synthesizer;
+  private final DexString libraryPrefix;
+
+  public DesugaredLibraryCustomConversionRewriter(
+      AppView<?> appView, DesugaredLibraryWrapperSynthesizer synthesizer) {
+    assert appView.options().isDesugaredLibraryCompilation();
+    this.appView = appView;
+    this.synthesizer = synthesizer;
+    this.libraryPrefix =
+        appView
+            .dexItemFactory()
+            .createString(
+                "L"
+                    + appView
+                        .options()
+                        .desugaredLibraryConfiguration
+                        .getSynthesizedLibraryClassesPackagePrefix());
+  }
+
+  @Override
+  public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
+    Map<DexProgramClass, Set<DexEncodedMethod>> methodsToProcessPerClass =
+        readConversionMethodsToConvert(eventConsumer);
+    methodsToProcessPerClass.forEach(
+        (conversionHolder, conversionsMethods) -> {
+          Set<DexEncodedMethod> convertedMethods = convertMethods(conversionsMethods);
+          conversionHolder.getMethodCollection().removeMethods(conversionsMethods);
+          conversionHolder.addDirectMethods(convertedMethods);
+        });
+  }
+
+  private Map<DexProgramClass, Set<DexEncodedMethod>> readConversionMethodsToConvert(
+      DesugaredLibraryCustomConversionEventConsumer eventConsumer) {
+    Map<DexProgramClass, Set<DexEncodedMethod>> methodsToProcessPerClass = new IdentityHashMap<>();
+    appView
+        .options()
+        .desugaredLibraryConfiguration
+        .getCustomConversions()
+        .forEach(
+            (convertedType, conversionHolder) -> {
+              DexClass dexClass = appView.definitionFor(conversionHolder);
+              if (dexClass == null || dexClass.isNotProgramClass()) {
+                generateMissingConversion(eventConsumer, convertedType, conversionHolder);
+                return;
+              }
+              DexProgramClass conversionProgramClass = dexClass.asProgramClass();
+              DexType rewrittenType = appView.rewritePrefix.rewrittenType(convertedType, appView);
+              Set<DexEncodedMethod> methods =
+                  methodsToProcessPerClass.computeIfAbsent(
+                      conversionProgramClass, ignored -> Sets.newIdentityHashSet());
+              methods.add(
+                  conversionEncodedMethod(
+                      conversionProgramClass,
+                      rewrittenType,
+                      convertedType,
+                      appView.dexItemFactory()));
+              methods.add(
+                  conversionEncodedMethod(
+                      conversionProgramClass,
+                      convertedType,
+                      rewrittenType,
+                      appView.dexItemFactory()));
+            });
+    return methodsToProcessPerClass;
+  }
+
+  private void generateMissingConversion(
+      DesugaredLibraryCustomConversionEventConsumer eventConsumer,
+      DexType convertedType,
+      DexType conversionHolder) {
+    appView
+        .options()
+        .reporter
+        .warning(
+            "Missing custom conversion "
+                + conversionHolder
+                + " from L8 compilation: Cannot convert "
+                + convertedType);
+    DexType vivifiedType = vivifiedTypeFor(convertedType, appView);
+    synthesizer.ensureClasspathCustomConversion(
+        vivifiedType, convertedType, eventConsumer, conversionHolder);
+    synthesizer.ensureClasspathCustomConversion(
+        convertedType, vivifiedType, eventConsumer, conversionHolder);
+  }
+
+  private Set<DexEncodedMethod> convertMethods(Set<DexEncodedMethod> conversionsMethods) {
+    Set<DexEncodedMethod> newMethods = Sets.newIdentityHashSet();
+    CfCodeTypeRewriter cfCodeTypeRewriter = new CfCodeTypeRewriter(appView.dexItemFactory());
+    for (DexEncodedMethod conversionMethod : conversionsMethods) {
+      Map<DexType, DexType> replacement = computeReplacement(conversionMethod.getReference());
+      newMethods.add(cfCodeTypeRewriter.rewriteCfCode(conversionMethod, replacement));
+    }
+    return newMethods;
+  }
+
+  private Map<DexType, DexType> computeReplacement(DexMethod reference) {
+    assert reference.getArity() == 1;
+    DexType argumentType = reference.getArgumentType(0, true);
+    if (isLibraryPrefixed(argumentType)) {
+      return ImmutableMap.of(
+          argumentType,
+          convertDesugaredLibraryToVivifiedType(argumentType, reference.getReturnType()));
+    }
+    assert isLibraryPrefixed(reference.getReturnType());
+    return ImmutableMap.of(
+        reference.getReturnType(),
+        convertDesugaredLibraryToVivifiedType(reference.getReturnType(), argumentType));
+  }
+
+  private boolean isLibraryPrefixed(DexType argumentType) {
+    return argumentType.getDescriptor().startsWith(libraryPrefix);
+  }
+
+  private DexType convertDesugaredLibraryToVivifiedType(DexType type, DexType oppositeType) {
+    if (!isLibraryPrefixed(type)) {
+      return type;
+    }
+    return vivifiedTypeFor(oppositeType, appView);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java
index 126d020..a28f92a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -26,6 +27,7 @@
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
 import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryClasspathWrapperSynthesizeEventConsumer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryCustomConversionEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer;
 import com.android.tools.r8.ir.synthetic.DesugaredLibraryAPIConversionCfCodeProvider.APIConverterConstructorCfCodeProvider;
 import com.android.tools.r8.ir.synthetic.DesugaredLibraryAPIConversionCfCodeProvider.APIConverterThrowRuntimeExceptionCfCodeProvider;
@@ -124,13 +126,28 @@
       DexType srcType,
       DexType destType,
       DesugaredLibraryClasspathWrapperSynthesizeEventConsumer eventConsumer) {
-    DexMethod customConversion = getCustomConversion(type, srcType, destType);
-    if (customConversion != null) {
-      return customConversion;
+    if (hasCustomConversion(type)) {
+      return getCustomConversion(type, srcType, destType, eventConsumer);
     }
     assert canGenerateWrapper(type) : type;
-    DexClass clazz = getValidClassToWrap(type);
-    WrapperConversions wrapperConversions = ensureWrappers(clazz, eventConsumer);
+    WrapperConversions wrapperConversions =
+        ensureWrappers(getValidClassToWrap(type), eventConsumer);
+    return extractConversion(type, srcType, destType, wrapperConversions);
+  }
+
+  public DexMethod getExistingProgramConversionMethod(
+      DexType type, DexType srcType, DexType destType) {
+    if (hasCustomConversion(type)) {
+      return getExistingProgramCustomConversion(type, srcType, destType);
+    }
+    assert canGenerateWrapper(type) : type;
+    WrapperConversions wrapperConversions =
+        getExistingProgramWrapperConversions(getValidClassToWrap(type));
+    return extractConversion(type, srcType, destType, wrapperConversions);
+  }
+
+  private DexMethod extractConversion(
+      DexType type, DexType srcType, DexType destType, WrapperConversions wrapperConversions) {
     DexMethod conversion =
         type == srcType
             ? wrapperConversions.getConversion()
@@ -140,32 +157,80 @@
     return conversion;
   }
 
-  public DexMethod getExistingProgramConversionMethod(
-      DexType type, DexType srcType, DexType destType) {
-    DexMethod customConversion = getCustomConversion(type, srcType, destType);
-    if (customConversion != null) {
-      return customConversion;
-    }
-    WrapperConversions wrapperConversions =
-        getExistingProgramWrapperConversions(getValidClassToWrap(type));
-    DexMethod conversion =
-        type == srcType
-            ? wrapperConversions.getConversion()
-            : wrapperConversions.getVivifiedConversion();
-    assert srcType == conversion.getArgumentType(0, true);
-    return conversion;
+  private boolean hasCustomConversion(DexType type) {
+    return appView.options().desugaredLibraryConfiguration.getCustomConversions().get(type) != null;
   }
 
-  private DexMethod getCustomConversion(DexType type, DexType srcType, DexType destType) {
-    // ConversionType holds the methods "rewrittenType convert(type)" and the other way around.
-    // But everything is going to be rewritten, so we need to use vivifiedType and type".
+  private DexMethod getCustomConversion(
+      DexType type,
+      DexType srcType,
+      DexType destType,
+      DesugaredLibraryClasspathWrapperSynthesizeEventConsumer eventConsumer) {
+    assert hasCustomConversion(type);
     DexType conversionHolder =
         appView.options().desugaredLibraryConfiguration.getCustomConversions().get(type);
-    if (conversionHolder != null) {
-      return factory.createMethod(
-          conversionHolder, factory.createProto(destType, srcType), factory.convertMethodName);
+    assert conversionHolder != null;
+    DexClass dexClass = appView.definitionFor(conversionHolder);
+    if (dexClass != null && dexClass.isProgramClass()) {
+      return getExistingProgramCustomConversion(type, srcType, destType);
     }
-    return null;
+    return ensureClasspathCustomConversion(srcType, destType, eventConsumer, conversionHolder)
+        .getReference();
+  }
+
+  public DexClassAndMethod ensureClasspathCustomConversion(
+      DexType srcType,
+      DexType destType,
+      DesugaredLibraryCustomConversionEventConsumer eventConsumer,
+      DexType conversionHolder) {
+    return appView
+        .getSyntheticItems()
+        .ensureFixedClasspathClassFromTypeMethod(
+            factory.convertMethodName,
+            factory.createProto(destType, srcType),
+            SyntheticKind.CUSTOM_CONVERSION,
+            conversionHolder,
+            appView,
+            ignored -> {},
+            eventConsumer::acceptCustomConversionClasspathClass,
+            methodBuilder ->
+                methodBuilder
+                    .setAccessFlags(
+                        MethodAccessFlags.fromSharedAccessFlags(
+                            Constants.ACC_PUBLIC | Constants.ACC_STATIC, false))
+                    .setCode(null));
+  }
+
+  private DexMethod getExistingProgramCustomConversion(
+      DexType type, DexType srcType, DexType destType) {
+    assert hasCustomConversion(type);
+    DexType conversionHolder =
+        appView.options().desugaredLibraryConfiguration.getCustomConversions().get(type);
+    assert conversionHolder != null;
+    DexClass dexClass = appView.definitionFor(conversionHolder);
+    assert dexClass != null;
+    // Note: DexClass can unfortunately be a non program class on broken set-ups.
+    DexMethod conversionMethod =
+        conversionMethod(dexClass.type, srcType, destType, appView.dexItemFactory());
+    assert conversionEncodedMethod(dexClass, srcType, destType, appView.dexItemFactory())
+            .getReference()
+        == conversionMethod;
+    return conversionMethod;
+  }
+
+  public static DexEncodedMethod conversionEncodedMethod(
+      DexClass conversionHolder, DexType srcType, DexType destType, DexItemFactory factory) {
+    DexMethod method = conversionMethod(conversionHolder.type, srcType, destType, factory);
+    DexEncodedMethod conversionMethod = conversionHolder.lookupDirectMethod(method);
+    assert conversionMethod != null;
+    return conversionMethod;
+  }
+
+  private static DexMethod conversionMethod(
+      DexType conversionHolder, DexType srcType, DexType destType, DexItemFactory factory) {
+    DexProto proto = factory.createProto(destType, srcType);
+    DexMethod method = factory.createMethod(conversionHolder, proto, factory.convertMethodName);
+    return method;
   }
 
   private boolean canConvert(DexType type) {
@@ -276,10 +341,12 @@
   }
 
   private DexMethod getConversion(DexClass wrapper, DexType returnType, DexType argType) {
-    DexMethod convertMethod =
-        factory.createMethod(
-            wrapper.type, factory.createProto(returnType, argType), factory.convertMethodName);
-    return wrapper.lookupDirectMethod(convertMethod).getReference();
+    DexMethod conversionMethod =
+        conversionMethod(wrapper.type, argType, returnType, appView.dexItemFactory());
+    assert conversionEncodedMethod(wrapper, argType, returnType, appView.dexItemFactory())
+            .getReference()
+        == conversionMethod;
+    return conversionMethod;
   }
 
   private DexEncodedField getWrapperUniqueEncodedField(DexClass wrapper) {
@@ -630,6 +697,7 @@
   @Override
   public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
     DesugaredLibraryConfiguration conf = appView.options().desugaredLibraryConfiguration;
+    new DesugaredLibraryCustomConversionRewriter(appView, this).synthesizeClasses(eventConsumer);
     List<DexProgramClass> validClassesToWrap = new ArrayList<>();
     for (DexType type : conf.getWrapperConversions()) {
       assert !conf.getCustomConversions().containsKey(type);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizerEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizerEventConsumer.java
index 212ad66..a4a894d 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizerEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizerEventConsumer.java
@@ -15,7 +15,13 @@
     void acceptWrapperProgramClass(DexProgramClass clazz);
   }
 
-  interface DesugaredLibraryClasspathWrapperSynthesizeEventConsumer {
+  interface DesugaredLibraryCustomConversionEventConsumer {
+
+    void acceptCustomConversionClasspathClass(DexClasspathClass clazz);
+  }
+
+  interface DesugaredLibraryClasspathWrapperSynthesizeEventConsumer
+      extends DesugaredLibraryCustomConversionEventConsumer {
 
     void acceptWrapperClasspathClass(DexClasspathClass clazz);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index 7dc9de3..f136195 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -165,7 +165,6 @@
     DesugaredLibraryConfiguration config = options.desugaredLibraryConfiguration;
     BiConsumer<DexType, DexType> registerEntry = registerMapEntry(appInfo);
     config.getEmulateLibraryInterface().forEach(registerEntry);
-    config.getCustomConversions().forEach(registerEntry);
     config.getRetargetCoreLibMember().forEach((method, types) -> types.forEach(registerEntry));
   }
 
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 474d1c3..9e362ed 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -644,31 +644,86 @@
       AppView<?> appView,
       Consumer<SyntheticClasspathClassBuilder> classConsumer,
       Consumer<DexClasspathClass> onCreationConsumer) {
+    // Obtain the outer synthesizing context in the case the context itself is synthetic.
+    // This is to ensure a flat input-type -> synthetic-item mapping.
     SynthesizingContext outerContext = SynthesizingContext.fromNonSyntheticInputContext(context);
     DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
+    return internalEnsureDexClasspathClass(
+        kind, classConsumer, onCreationConsumer, outerContext, type, appView);
+  }
+
+  private DexClasspathClass internalEnsureDexClasspathClass(
+      SyntheticKind kind,
+      Consumer<SyntheticClasspathClassBuilder> classConsumer,
+      Consumer<DexClasspathClass> onCreationConsumer,
+      SynthesizingContext outerContext,
+      DexType type,
+      AppView<?> appView) {
     DexClass dexClass = appView.appInfo().definitionForWithoutExistenceAssert(type);
     if (dexClass != null) {
       assert dexClass.isClasspathClass();
       return dexClass.asClasspathClass();
     }
-    synchronized (context) {
+    synchronized (type) {
       dexClass = appView.appInfo().definitionForWithoutExistenceAssert(type);
       if (dexClass != null) {
         assert dexClass.isClasspathClass();
         return dexClass.asClasspathClass();
       }
-      // Obtain the outer synthesizing context in the case the context itself is synthetic.
-      // This is to ensure a flat input-type -> synthetic-item mapping.
-      SyntheticClasspathClassBuilder classBuilder =
-          new SyntheticClasspathClassBuilder(type, kind, outerContext, appView.dexItemFactory());
-      classConsumer.accept(classBuilder);
-      DexClasspathClass clazz = classBuilder.build();
-      addPendingDefinition(new SyntheticClasspathClassDefinition(kind, outerContext, clazz));
+      DexClasspathClass clazz =
+          internalCreateClasspathClass(
+              kind, classConsumer, outerContext, type, appView.dexItemFactory());
       onCreationConsumer.accept(clazz);
       return clazz;
     }
   }
 
+  private DexClasspathClass internalCreateClasspathClass(
+      SyntheticKind kind,
+      Consumer<SyntheticClasspathClassBuilder> fn,
+      SynthesizingContext outerContext,
+      DexType type,
+      DexItemFactory factory) {
+    SyntheticClasspathClassBuilder classBuilder =
+        new SyntheticClasspathClassBuilder(type, kind, outerContext, factory);
+    fn.accept(classBuilder);
+      DexClasspathClass clazz = classBuilder.build();
+      addPendingDefinition(new SyntheticClasspathClassDefinition(kind, outerContext, clazz));
+      return clazz;
+    }
+
+  public DexClasspathClass ensureFixedClasspathClassFromType(
+      SyntheticKind kind,
+      DexType contextType,
+      AppView<?> appView,
+      Consumer<SyntheticClasspathClassBuilder> classConsumer,
+      Consumer<DexClasspathClass> onCreationConsumer) {
+    SynthesizingContext outerContext = SynthesizingContext.fromType(contextType);
+    DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
+    return internalEnsureDexClasspathClass(
+        kind, classConsumer, onCreationConsumer, outerContext, type, appView);
+  }
+
+  public DexClassAndMethod ensureFixedClasspathClassFromTypeMethod(
+      DexString methodName,
+      DexProto methodProto,
+      SyntheticKind kind,
+      DexType contextType,
+      AppView<?> appView,
+      Consumer<SyntheticClasspathClassBuilder> classConsumer,
+      Consumer<DexClasspathClass> onCreationConsumer,
+      Consumer<SyntheticMethodBuilder> buildMethodCallback) {
+    DexClasspathClass clazz =
+        ensureFixedClasspathClassFromType(
+            kind, contextType, appView, classConsumer, onCreationConsumer);
+    DexMethod methodReference =
+        appView.dexItemFactory().createMethod(clazz.getType(), methodProto, methodName);
+    DexEncodedMethod methodDefinition =
+        internalEnsureMethod(
+            methodReference, clazz, kind, appView, buildMethodCallback, emptyConsumer());
+    return DexClassAndMethod.create(clazz, methodDefinition);
+  }
+
   public DexClassAndMethod ensureFixedClasspathClassMethod(
       DexString methodName,
       DexProto methodProto,
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
index 0e45bd8..567b595 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
@@ -157,6 +157,10 @@
   }
 
   private Code getCodeObject(DexMethod methodSignature) {
+    if (codeGenerator == null) {
+      // The codeGenerator may be null on classpath classes.
+      return null;
+    }
     return codeGenerator.generate(methodSignature);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 0a05c0f..872fdee 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -28,6 +28,7 @@
     ENUM_UNBOXING_LOCAL_UTILITY_CLASS("$EnumUnboxingLocalUtility", 24, false, true),
     ENUM_UNBOXING_SHARED_UTILITY_CLASS("$EnumUnboxingSharedUtility", 25, false, true),
     RECORD_TAG("", 1, false, true, true),
+    CUSTOM_CONVERSION("", 31, false, true),
     COMPANION_CLASS("$-CC", 2, false, true),
     EMULATED_INTERFACE_CLASS("$-EL", 3, false, true),
     RETARGET_CLASS("RetargetClass", 20, false, true),