Merge commit '6f637c5b276c284fdc71100061af5a2c9e11cae9' into dev-release
diff --git a/README.md b/README.md
index 1621acb..d4f6295 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,46 @@
 - R8 is a java program shrinking and minification tool that converts java byte
   code to optimized dex code.
 
-D8 is a replacement for the DX dexer and R8 is a replacement for
-the [Proguard](https://www.guardsquare.com/en/proguard) shrinking and
+D8 is a replacement for the DX dexer and R8 is an alternative to
+the [ProGuard](https://www.guardsquare.com/en/proguard) shrinking and
 minification tool.
 
-## Downloading and building
+## Obtaining prebuilts
+
+There are several places to obtain a prebuilt version without building it
+yourself.
+
+The stable release versions shipped with Android Studio are available from
+the Google Maven repository, see
+https://maven.google.com/web/index.html#com.android.tools:r8.
+
+Our [CI](https://ci.chromium.org/p/r8/g/main/console) builds for each commit and
+stores all build artifacts in Google Cloud Storage in the bucket r8-releases.
+
+To obtain a prebuild from the CI for a specifc version (including both
+stable and `-dev` versions), download from the following URL:
+
+    https://storage.googleapis.com/r8-releases/raw/<version>/r8lib.jar
+
+To obtain a prebuild from the CI for a specifc main branch hash, download from the
+following URL:
+
+    https://storage.googleapis.com/r8-releases/raw/main/<hash>/r8lib.jar
+
+The prebuilt JARs have been processed by R8, and for each build the corresponding
+mapping file is located together with the prebuild under the name `r8lib.jar.map`.
+
+The Google Cloud Storage bucket r8-releases can also be used as a simple
+Maven repository using the following in a Gradle build file:
+
+    maven {
+        url = uri("https://storage.googleapis.com/r8-releases/raw")
+    }
+
+See [Running D8](#running-d8) and [Running R8](#running-r8) below on how to invoke
+D8 and R8 using the obtained `r8lib.jar` in place of `build/libs/r8.jar`.
+
+## Downloading source and building
 
 The R8 project uses [`depot_tools`](https://www.chromium.org/developers/how-tos/install-depot-tools)
 from the chromium project to manage dependencies. Install `depot_tools` and add it to
@@ -30,9 +65,10 @@
 a version of gradle to use for building on the first run. This will produce
 a jar file: `build/libs/r8.jar` which contains both R8 and D8.
 
-## Running D8
+## <a name="running-d8"></a>Running D8
 
 The D8 dexer has a simple command-line interface with only a few options.
+D8 consumes class files and produce DEX.
 
 The most important option is whether to build in debug or release mode.  Debug
 is the default mode and includes debugging information in the resulting dex
@@ -54,19 +90,28 @@
 The full set of D8 options can be obtained by running the command line tool with
 the `--help` option.
 
-## Running R8
+## <a name="running-r8"></a>Running R8
 
-R8 is a [Proguard](https://www.guardsquare.com/en/proguard) replacement for
-whole-program optimization, shrinking and minification. R8 uses the Proguard
+R8 is a [ProGuard](https://www.guardsquare.com/en/proguard) replacement for
+whole-program optimization, shrinking and minification. R8 uses the ProGuard
 keep rule format for specifying the entry points for an application.
 
-Typical invocations of R8 to produce optimized dex file(s) in the out directory:
+R8 consumes class files and can output either DEX for Android apps or class files
+for Java apps.
 
-    $ java -jar build/libs/r8.jar --release --output out --pg-conf proguard.cfg input.jar
+Typical invocations of R8 to produce optimized DEX file(s) in the `out` directory:
+
+    $ java -cp build/libs/r8.jar com.android.tools.r8.R8 --release --output out --pg-conf proguard.cfg input.jar
+
+ The default is to produce DEX. To produce class files pass the option `--classfile`. This invocation will provide optimized Java classfiles in `output.jar`:
+
+    $ java -cp build/libs/r8.jar com.android.tools.r8.R8 --classfile --release --output output.jar --pg-conf proguard.cfg input.jar
 
 The full set of R8 options can be obtained by running the command line tool with
 the `--help` option.
 
+R8 is not command line compatible with ProGuard, for instance keep rules cannot be passed on the command line, but have to be passed in a file using the `--pg-conf` option.
+
 ## Testing
 
 Typical steps to run tests:
diff --git a/build.gradle b/build.gradle
index 9f4cf29..e614057 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2238,9 +2238,6 @@
         systemProperty 'slow_tests', project.property('slow_tests')
     }
 
-    if (project.hasProperty('force_32_bit_art')) {
-        systemProperty 'force_32_bit_art', project.property('force_32_bit_art')
-    }
 
     if (project.hasProperty('desugar_jdk_json_dir')) {
         systemProperty 'desugar_jdk_json_dir', project.property('desugar_jdk_json_dir')
diff --git a/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java b/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java
index 3ac81cd..d1b949b 100644
--- a/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java
+++ b/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java
@@ -35,12 +35,14 @@
   static Map<String, String> getWrapConvertOwnerMap() {
     HashMap<String, String> map = new HashMap<>();
     for (String theEnum : ENUM_WRAP_CONVERT_OWNER) {
-      map.put(theEnum, theEnum + "$EnumConversion");
-      map.put(withJavaPrefix(theEnum), theEnum + "$EnumConversion");
+      String theEnumJava = withJavaPrefix(theEnum);
+      map.put(theEnum, theEnumJava + "$EnumConversion");
+      map.put(theEnumJava, theEnumJava + "$EnumConversion");
     }
     for (String owner : WRAP_CONVERT_OWNER) {
-      map.put(withJavaPrefix(owner), owner + "$Wrapper");
-      map.put(owner, owner + "$VivifiedWrapper");
+      String ownerJava = withJavaPrefix(owner);
+      map.put(ownerJava, ownerJava + "$Wrapper");
+      map.put(owner, ownerJava + "$VivifiedWrapper");
     }
     return map;
   }
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index f461b56..ff1df31 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -861,7 +861,6 @@
         '  "test_options": ['
         '    "--dex_vm=7.0.0",'
         '    "--all_tests",'
-        '    "--force-32-bit-art",'
         '    "--tool=r8",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
@@ -899,7 +898,6 @@
         '  "test_options": ['
         '    "--dex_vm=7.0.0",'
         '    "--all_tests",'
-        '    "--force-32-bit-art",'
         '    "--tool=r8",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 40ea46a..32abb36 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -302,7 +302,7 @@
 r8_tester_with_default("linux-android-6.0.1",
     ["--dex_vm=6.0.1", "--all_tests"])
 r8_tester_with_default("linux-android-7.0.0",
-    ["--dex_vm=7.0.0", "--all_tests", "--force-32-bit-art"])
+    ["--dex_vm=7.0.0", "--all_tests"])
 r8_tester_with_default("linux-android-8.1.0",
     ["--dex_vm=8.1.0", "--all_tests"])
 r8_tester_with_default("linux-android-9.0.0",
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index ff649f7..137cb8b 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -696,11 +696,8 @@
     internal.encodeChecksums = getIncludeClassesChecksum();
     internal.dexClassChecksumFilter = getDexClassChecksumFilter();
     internal.enableInheritanceClassInDexDistributor = isOptimizeMultidexForLinearAlloc();
-    internal.setDesugaredLibrarySpecification(desugaredLibrarySpecification);
-    internal.synthesizedClassPrefix =
-        synthesizedClassPrefix.isEmpty()
-            ? System.getProperty("com.android.tools.r8.synthesizedClassPrefix", "")
-            : synthesizedClassPrefix;
+
+    internal.configureDesugaredLibrary(desugaredLibrarySpecification, synthesizedClassPrefix);
     internal.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
 
     if (!enableMissingLibraryApiModeling) {
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 1edde59..6eb01b6 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -211,10 +211,9 @@
     assert internal.enableInheritanceClassInDexDistributor;
     internal.enableInheritanceClassInDexDistributor = false;
 
-    assert desugaredLibrarySpecification != null;
-    internal.setDesugaredLibrarySpecification(desugaredLibrarySpecification);
-    internal.synthesizedClassPrefix =
-        desugaredLibrarySpecification.getSynthesizedLibraryClassesPackagePrefix();
+    internal.configureDesugaredLibrary(
+        desugaredLibrarySpecification,
+        desugaredLibrarySpecification.getSynthesizedLibraryClassesPackagePrefix());
 
     // Default is to remove all javac generated assertion code when generating dex.
     assert internal.assertionsConfiguration == null;
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 62a6e97..edec018 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -1144,11 +1144,7 @@
 
     internal.enableInheritanceClassInDexDistributor = isOptimizeMultidexForLinearAlloc();
 
-    internal.setDesugaredLibrarySpecification(desugaredLibrarySpecification);
-    internal.synthesizedClassPrefix =
-        synthesizedClassPrefix.isEmpty()
-            ? System.getProperty("com.android.tools.r8.synthesizedClassPrefix", "")
-            : synthesizedClassPrefix;
+    internal.configureDesugaredLibrary(desugaredLibrarySpecification, synthesizedClassPrefix);
     boolean l8Shrinking = !internal.synthesizedClassPrefix.isEmpty();
     // TODO(b/214382176): Enable all the time.
     internal.loadAllClassDefinitions = l8Shrinking;
diff --git a/src/main/java/com/android/tools/r8/dex/CompatByteBuffer.java b/src/main/java/com/android/tools/r8/dex/CompatByteBuffer.java
index 0d36470..533a5f5 100644
--- a/src/main/java/com/android/tools/r8/dex/CompatByteBuffer.java
+++ b/src/main/java/com/android/tools/r8/dex/CompatByteBuffer.java
@@ -7,6 +7,7 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
 
 /**
  * In JDK 9 ByteBuffer ByteBuffer.position(int) started overriding Buffer Buffer.position(int) along
@@ -26,6 +27,10 @@
     return new CompatByteBuffer(ByteBuffer.wrap(bytes));
   }
 
+  public static CompatByteBuffer wrapOrNull(byte[] bytes) {
+    return bytes == null ? null : wrap(bytes);
+  }
+
   private Buffer asBuffer() {
     return buffer;
   }
@@ -136,4 +141,34 @@
   public void put(byte[] bytes) {
     asByteBuffer().put(bytes);
   }
+
+  // ----------------------------------------------------------------------------------------------
+  // Additional custom methods
+  // ----------------------------------------------------------------------------------------------
+
+  public int getUShort() {
+    return buffer.getShort() & 0xffff;
+  }
+
+  public byte[] getBytesOfUByteSize() {
+    int length = getUShort();
+    byte[] data = new byte[length];
+    get(data);
+    return data;
+  }
+
+  public String getUTFOfUByteSize() {
+    return new String(getBytesOfUByteSize(), StandardCharsets.UTF_8);
+  }
+
+  public byte[] getBytesOfIntSize() {
+    int length = buffer.getInt();
+    byte[] data = new byte[length];
+    get(data);
+    return data;
+  }
+
+  public String getUTFOfIntSize() {
+    return new String(getBytesOfIntSize(), StandardCharsets.UTF_8);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 5ebfb20..7a7b8d6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -771,7 +771,7 @@
   }
 
   public boolean validateUnboxedEnumsHaveBeenPruned() {
-    for (DexType unboxedEnum : unboxedEnums.getUnboxedEnums()) {
+    for (DexType unboxedEnum : unboxedEnums.computeAllUnboxedEnums()) {
       assert appInfo.definitionForWithoutExistenceAssert(unboxedEnum) == null
           : "Enum " + unboxedEnum + " has been unboxed but is still in the program.";
       assert appInfo().withLiveness().wasPruned(unboxedEnum)
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 4d6f31a..cfa8347 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1992,8 +1992,17 @@
     }
 
     public boolean isEnumField(DexEncodedField staticField, DexType enumType) {
+      return isEnumField(staticField, enumType, ImmutableSet.of());
+    }
+
+    // In some case, the enum field may be respecialized to an enum subtype. In this case, one
+    // can pass the encoded field as well as the field with the super enum type for the checks.
+    public boolean isEnumField(
+        DexEncodedField staticField, DexType enumType, Set<DexType> subtypes) {
       assert staticField.isStatic();
-      return staticField.getType() == enumType && staticField.isEnum() && staticField.isFinal();
+      return (staticField.getType() == enumType || subtypes.contains(staticField.getType()))
+          && staticField.isEnum()
+          && staticField.isFinal();
     }
 
     public boolean isValuesFieldCandidate(DexEncodedField staticField, DexType enumType) {
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 45936b7..97dfaf5 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -482,7 +482,7 @@
           (clazz, allocationSitesForClass) -> {
             DexType type = lens.lookupType(clazz.type);
             if (type.isPrimitiveType()) {
-              assert !objectAllocationInfos.hasInstantiatedStrictSubtype(clazz);
+              assert clazz.isEnum();
               return;
             }
             DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
diff --git a/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
index 0e85802..a736fe9 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/AppliedGraphLens.java
@@ -167,7 +167,7 @@
   }
 
   @Override
-  public boolean isContextFreeForMethods() {
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
index f63985a..1635380 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/ClearCodeRewritingGraphLens.java
@@ -67,12 +67,12 @@
 
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     throw new Unreachable();
   }
 
   @Override
-  public boolean isContextFreeForMethods() {
-    return getIdentityLens().isContextFreeForMethods();
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
index 1f8f516..bf57958 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/DefaultNonIdentityGraphLens.java
@@ -22,8 +22,11 @@
   }
 
   @Override
-  public boolean isContextFreeForMethods() {
-    return getPrevious().isContextFreeForMethods();
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    if (this == codeLens) {
+      return true;
+    }
+    return getPrevious().isContextFreeForMethods(codeLens);
   }
 
   // Class lookup APIs.
@@ -67,7 +70,7 @@
 
   @Override
   protected MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     return previous;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
index 6f292cb..d9f9c79 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -146,6 +145,14 @@
     return original;
   }
 
+  public final DexReference getRenamedReference(
+      DexReference originalReference, GraphLens codeLens) {
+    return originalReference.apply(
+        clazz -> lookupType(clazz, codeLens),
+        field -> getRenamedFieldSignature(field, codeLens),
+        method -> getRenamedMethodSignature(method, codeLens));
+  }
+
   public final DexField getRenamedFieldSignature(DexField originalField) {
     return getRenamedFieldSignature(originalField, null);
   }
@@ -238,12 +245,6 @@
 
   public abstract DexType lookupType(DexType type, GraphLens applied);
 
-  @Deprecated
-  public final DexMethod lookupMethod(DexMethod method) {
-    assert verifyIsContextFreeForMethod(method);
-    return lookupMethod(method, null, null).getReference();
-  }
-
   public final MethodLookupResult lookupInvokeDirect(DexMethod method, ProgramMethod context) {
     return lookupMethod(method, context.getReference(), InvokeType.DIRECT);
   }
@@ -359,10 +360,6 @@
     FieldLookupResult lookupField(FieldLookupResult previous);
   }
 
-  public DexReference lookupReference(DexReference reference) {
-    return reference.apply(this::lookupType, this::lookupField, this::lookupMethod);
-  }
-
   // The method lookupMethod() maps a pair INVOKE=(method signature, invoke type) to a new pair
   // INVOKE'=(method signature', invoke type'). This mapping can be context sensitive, meaning that
   // the result INVOKE' depends on where the invocation INVOKE is in the program. This is, for
@@ -373,10 +370,11 @@
   // is context insensitive, it is safe to invoke lookupMethod() without a context (or to pass null
   // as context). Trying to invoke a context sensitive graph lens without a context will lead to
   // an assertion error.
-  public abstract boolean isContextFreeForMethods();
+  public abstract boolean isContextFreeForMethods(GraphLens codeLens);
 
-  public boolean verifyIsContextFreeForMethod(DexMethod method) {
-    return isContextFreeForMethods();
+  public boolean verifyIsContextFreeForMethod(DexMethod method, GraphLens codeLens) {
+    assert isContextFreeForMethods(codeLens);
+    return true;
   }
 
   public static GraphLens getIdentityLens() {
@@ -454,13 +452,10 @@
     return this;
   }
 
-  public <T extends DexDefinition> boolean assertDefinitionsNotModified(Iterable<T> definitions) {
-    for (DexDefinition definition : definitions) {
-      DexReference reference = definition.getReference();
-      // We allow changes to bridge methods as these get retargeted even if they are kept.
-      boolean isBridge =
-          definition.isDexEncodedMethod() && definition.asDexEncodedMethod().accessFlags.isBridge();
-      assert isBridge || lookupReference(reference) == reference;
+  public boolean assertFieldsNotModified(Iterable<DexEncodedField> fields) {
+    for (DexEncodedField field : fields) {
+      DexField reference = field.getReference();
+      assert getRenamedFieldSignature(reference) == reference;
     }
     return true;
   }
@@ -642,5 +637,4 @@
 
     return true;
   }
-
 }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
index ce9aca7..2d6b49e 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/IdentityGraphLens.java
@@ -107,7 +107,7 @@
   }
 
   @Override
-  public boolean isContextFreeForMethods() {
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
index 7c921ca..ee3efbe 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLens.java
@@ -191,7 +191,7 @@
 
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     if (previous.hasReboundReference()) {
       // TODO(sgjesse): Should we always do interface to virtual mapping? Is it a performance win
       //  that only subclasses which are known to need it actually do it?
@@ -307,13 +307,17 @@
   }
 
   @Override
-  public boolean isContextFreeForMethods() {
-    return getPrevious().isContextFreeForMethods();
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    return getPrevious().isContextFreeForMethods(codeLens);
   }
 
   @Override
-  public boolean verifyIsContextFreeForMethod(DexMethod method) {
-    assert getPrevious().verifyIsContextFreeForMethod(method);
+  public boolean verifyIsContextFreeForMethod(DexMethod method, GraphLens codeLens) {
+    assert codeLens == this
+        || getPrevious().verifyIsContextFreeForMethod(getPreviousMethodSignature(method), codeLens);
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
index fb0c944..8eaab04 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NonIdentityGraphLens.java
@@ -155,13 +155,18 @@
         getPreviousMethodSignature(context),
         type,
         codeLens,
-        previous -> continuation.lookupMethod(internalDescribeLookupMethod(previous, context)));
+        previous ->
+            continuation.lookupMethod(internalDescribeLookupMethod(previous, context, codeLens)));
   }
 
   protected abstract FieldLookupResult internalDescribeLookupField(FieldLookupResult previous);
 
+  /**
+   * The codeLens is only needed for assertions that call other lens methods, it should not
+   * influence the lookup itself.
+   */
   protected abstract MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context);
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens);
 
   protected abstract DexType internalDescribeLookupClassType(DexType previous);
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 72e96a7..cd2586e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.FieldLookupResult;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
@@ -67,9 +68,9 @@
    */
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     List<ExtraParameter> extraParameters = methodExtraParameters.get(previous.getReference());
-    MethodLookupResult lookup = super.internalDescribeLookupMethod(previous, context);
+    MethodLookupResult lookup = super.internalDescribeLookupMethod(previous, context, codeLens);
     if (extraParameters == null) {
       return lookup;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
index e57052f..0f40f03 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
@@ -4,12 +4,15 @@
 
 package com.android.tools.r8.ir.analysis.fieldvalueanalysis;
 
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
 
 public abstract class StaticFieldValues {
@@ -47,6 +50,16 @@
       return new Builder();
     }
 
+    public EnumStaticFieldValues rewrittenWithLens(
+        AppView<AppInfoWithLiveness> appView, GraphLens lens, GraphLens codeLens) {
+      ImmutableMap.Builder<DexField, ObjectState> builder = ImmutableMap.builder();
+      enumAbstractValues.forEach(
+          (field, state) ->
+              builder.put(
+                  lens.lookupField(field), state.rewrittenWithLens(appView, lens, codeLens)));
+      return new EnumStaticFieldValues(builder.build());
+    }
+
     public static class Builder extends StaticFieldValues.Builder {
       private final ImmutableMap.Builder<DexField, ObjectState> enumObjectStateBuilder =
           ImmutableMap.builder();
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
index c521bf5..5f07431 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
@@ -58,6 +58,10 @@
     return inValues.get(VALUE_INDEX);
   }
 
+  public void replacePutValue(Value newValue) {
+    replaceValue(VALUE_INDEX, newValue);
+  }
+
   @Override
   public MemberType getMemberType() {
     return type;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 68073e7..7db097a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -196,7 +196,7 @@
 
     if (serviceLoaderRewriter != null) {
       processSimpleSynthesizeMethods(
-          serviceLoaderRewriter.getServiceLoadMethods(), executorService);
+          serviceLoaderRewriter.getSynthesizedServiceLoadMethods(), executorService);
     }
 
     if (instanceInitializerOutliner != null) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/TypeRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/TypeRewriter.java
index c8c5159..a529491 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/TypeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/TypeRewriter.java
@@ -15,7 +15,7 @@
 public abstract class TypeRewriter {
 
   public static TypeRewriter empty() {
-    return new EmptyPrefixRewritingMapper();
+    return new EmptyTypeRewriter();
   }
 
   public abstract void rewriteType(DexType type, DexType rewrittenType);
@@ -44,12 +44,12 @@
 
   public abstract void forAllRewrittenTypes(Consumer<DexType> consumer);
 
-  public static class MachineDesugarPrefixRewritingMapper extends TypeRewriter {
+  public static class MachineTypeRewriter extends TypeRewriter {
 
     private final Map<DexType, DexType> rewriteType;
     private final Map<DexType, DexType> rewriteDerivedTypeOnly;
 
-    public MachineDesugarPrefixRewritingMapper(MachineDesugaredLibrarySpecification specification) {
+    public MachineTypeRewriter(MachineDesugaredLibrarySpecification specification) {
       this.rewriteType = new ConcurrentHashMap<>(specification.getRewriteType());
       rewriteDerivedTypeOnly = specification.getRewriteDerivedTypeOnly();
     }
@@ -99,7 +99,7 @@
     }
   }
 
-  public static class EmptyPrefixRewritingMapper extends TypeRewriter {
+  public static class EmptyTypeRewriter extends TypeRewriter {
 
     @Override
     public DexType rewrittenType(DexType type, AppView<?> appView) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
index 31baed9..985eb29 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexApplication;
 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.DexReference;
 import com.android.tools.r8.graph.DexType;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.specificationconversion.LibraryValidator;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SemanticVersion;
 import com.android.tools.r8.utils.StringUtils;
@@ -261,4 +263,14 @@
   public boolean includesJDK11Methods() {
     return getLeadingVersionNumber() >= 2;
   }
+
+  public MachineDesugaredLibrarySpecification withPostPrefix(
+      DexItemFactory factory, String postPrefix) {
+    String oldPrefix = topLevelFlags.getSynthesizedLibraryClassesPackagePrefix();
+    String newPrefix = oldPrefix + DescriptorUtils.getPackageBinaryNameFromJavaType(postPrefix);
+    return new MachineDesugaredLibrarySpecification(
+        libraryCompilation,
+        topLevelFlags.withPostPrefix(postPrefix),
+        rewritingFlags.withPostPrefix(factory, oldPrefix, newPrefix));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
index 6d3f489..a30322f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
@@ -6,8 +6,11 @@
 
 import com.android.tools.r8.errors.CompilationError;
 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.DexProto;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.google.common.collect.ImmutableMap;
@@ -232,6 +235,109 @@
         && legacyBackport.isEmpty();
   }
 
+  public MachineRewritingFlags withPostPrefix(
+      DexItemFactory factory, String oldPrefix, String newPrefix) {
+    return new MachineRewritingFlags(
+        typeMapWithPostPrefix(rewriteType, factory, oldPrefix, newPrefix),
+        maintainType,
+        typeMapWithPostPrefix(rewriteDerivedTypeOnly, factory, oldPrefix, newPrefix),
+        staticFieldRetarget,
+        covariantRetarget,
+        staticRetarget,
+        nonEmulatedVirtualRetarget,
+        emulatedVirtualRetarget,
+        emulatedVirtualRetargetThroughEmulatedInterface,
+        apiGenericTypesConversion,
+        emulatedInterfacesWithPostPrefix(factory, oldPrefix, newPrefix),
+        wrappers,
+        legacyBackport,
+        dontRetarget,
+        customConversionsWithPostPrefix(factory, oldPrefix, newPrefix),
+        neverOutlineApi,
+        amendLibraryMethod,
+        amendLibraryField);
+  }
+
+  private Map<DexType, CustomConversionDescriptor> customConversionsWithPostPrefix(
+      DexItemFactory factory, String oldPrefix, String newPrefix) {
+    ImmutableMap.Builder<DexType, CustomConversionDescriptor> builder = ImmutableMap.builder();
+    customConversions.forEach(
+        (k, v) ->
+            builder.put(
+                k,
+                new CustomConversionDescriptor(
+                    methodWithPostPrefix(v.getTo(), factory, oldPrefix, newPrefix),
+                    methodWithPostPrefix(v.getFrom(), factory, oldPrefix, newPrefix))));
+    return builder.build();
+  }
+
+  private Map<DexType, EmulatedInterfaceDescriptor> emulatedInterfacesWithPostPrefix(
+      DexItemFactory factory, String oldPrefix, String newPrefix) {
+    ImmutableMap.Builder<DexType, EmulatedInterfaceDescriptor> builder = ImmutableMap.builder();
+    emulatedInterfaces.forEach(
+        (k, v) -> builder.put(k, descriptorWithPostPrefix(v, factory, oldPrefix, newPrefix)));
+    return builder.build();
+  }
+
+  private EmulatedInterfaceDescriptor descriptorWithPostPrefix(
+      EmulatedInterfaceDescriptor descriptor,
+      DexItemFactory factory,
+      String oldPrefix,
+      String newPrefix) {
+    DexType rewritten =
+        typeWithPostPrefix(descriptor.getRewrittenType(), factory, oldPrefix, newPrefix);
+    Map<DexMethod, EmulatedDispatchMethodDescriptor> newDescriptors = new IdentityHashMap<>();
+    descriptor
+        .getEmulatedMethods()
+        .forEach(
+            (method, descr) -> {
+              assert descr.getInterfaceMethod().getMethod().getHolderType()
+                  == descriptor.getRewrittenType();
+              newDescriptors.put(
+                  method,
+                  new EmulatedDispatchMethodDescriptor(
+                      new DerivedMethod(
+                          descr.getInterfaceMethod().getMethod().withHolder(rewritten, factory),
+                          descr.getInterfaceMethod().getMachineHolderKind()),
+                      descr.getEmulatedDispatchMethod(),
+                      descr.getForwardingMethod(),
+                      descr.getDispatchCases()));
+            });
+    return new EmulatedInterfaceDescriptor(rewritten, newDescriptors);
+  }
+
+  private Map<DexType, DexType> typeMapWithPostPrefix(
+      Map<DexType, DexType> map, DexItemFactory factory, String oldPrefix, String newPrefix) {
+    ImmutableMap.Builder<DexType, DexType> builder = ImmutableMap.builder();
+    map.forEach((k, v) -> builder.put(k, typeWithPostPrefix(v, factory, oldPrefix, newPrefix)));
+    return builder.build();
+  }
+
+  private DexMethod methodWithPostPrefix(
+      DexMethod method, DexItemFactory factory, String oldPrefix, String newPrefix) {
+    return factory.createMethod(
+        method.getHolderType(),
+        protoWithPostPrefix(method.getProto(), factory, oldPrefix, newPrefix),
+        method.getName());
+  }
+
+  private DexProto protoWithPostPrefix(
+      DexProto proto, DexItemFactory factory, String oldPrefix, String newPrefix) {
+    DexType[] values = proto.getParameters().values;
+    DexType[] newValues = new DexType[values.length];
+    for (int i = 0; i < values.length; i++) {
+      newValues[i] = typeWithPostPrefix(values[i], factory, oldPrefix, newPrefix);
+    }
+    return factory.createProto(
+        typeWithPostPrefix(proto.getReturnType(), factory, oldPrefix, newPrefix),
+        DexTypeList.create(newValues));
+  }
+
+  private DexType typeWithPostPrefix(
+      DexType type, DexItemFactory factory, String oldPrefix, String newPrefix) {
+    return factory.createType(type.toDescriptorString().replace(oldPrefix, newPrefix));
+  }
+
   public static class Builder {
 
     Builder() {}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineTopLevelFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineTopLevelFlags.java
index c5c1b4b..f944e8e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineTopLevelFlags.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineTopLevelFlags.java
@@ -5,7 +5,9 @@
 package com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification;
 
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
 import java.util.List;
 
 public class MachineTopLevelFlags {
@@ -71,4 +73,25 @@
   public String getExtraKeepRulesConcatenated() {
     return String.join("\n", extraKeepRules);
   }
+
+  public MachineTopLevelFlags withPostPrefix(String postPrefix) {
+    assert postPrefix.endsWith(String.valueOf(DescriptorUtils.JAVA_PACKAGE_SEPARATOR));
+    String prefix =
+        DescriptorUtils.getJavaTypeFromBinaryName(synthesizedLibraryClassesPackagePrefix);
+    String cleanPostPrefix = DescriptorUtils.getJavaTypeFromBinaryName(postPrefix);
+    String newPrefix = prefix + cleanPostPrefix;
+    String newPrefixWithSlash = DescriptorUtils.getPackageBinaryNameFromJavaType(newPrefix);
+    List<String> newKeepRules = new ArrayList<>(extraKeepRules.size());
+    for (String kr : extraKeepRules) {
+      // TODO(b/278046666): Consider changing the ProguardRuleParser to avoid invalid replacements.
+      newKeepRules.add(kr.replace(prefix, newPrefix));
+    }
+    return new MachineTopLevelFlags(
+        requiredCompilationAPILevel,
+        newPrefixWithSlash,
+        identifier,
+        jsonSource,
+        supportAllCallbacksFromLibrary,
+        newKeepRules);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index a2572eb..03245b3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -523,6 +523,7 @@
     //       ...
     //     }
     //   }
+    // TODO(b/278679664): Relax requirement (3) when targeting DEX.
     Value thisValue = inlinee.entryBlock().entry().asArgument().outValue();
 
     List<InvokeDirect> initCallsOnThis = new ArrayList<>();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 534b5a9..86d1998 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -6,10 +6,11 @@
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.ServiceLoaderMethods;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
@@ -24,9 +25,11 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.desugar.ServiceLoaderSourceCode;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
@@ -63,24 +66,34 @@
 public class ServiceLoaderRewriter {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final List<ProgramMethod> serviceLoadMethods = new ArrayList<>();
   private final AndroidApiLevelCompute apiLevelCompute;
+  private final Reporter reporter;
+  private final ServiceLoaderMethods serviceLoaderMethods;
+
+  private final List<ProgramMethod> synthesizedServiceLoadMethods = new ArrayList<>();
 
   public ServiceLoaderRewriter(
       AppView<AppInfoWithLiveness> appView, AndroidApiLevelCompute apiLevelCompute) {
     this.appView = appView;
     this.apiLevelCompute = apiLevelCompute;
+    serviceLoaderMethods = appView.dexItemFactory().serviceLoaderMethods;
+    reporter = shouldReportWhyAreYouNotInliningServiceLoaderLoad() ? appView.reporter() : null;
   }
 
-  public List<ProgramMethod> getServiceLoadMethods() {
-    return serviceLoadMethods;
+  private boolean shouldReportWhyAreYouNotInliningServiceLoaderLoad() {
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    return appInfo.isWhyAreYouNotInliningMethod(serviceLoaderMethods.load)
+        || appInfo.isWhyAreYouNotInliningMethod(serviceLoaderMethods.loadWithClassLoader);
+  }
+
+  public List<ProgramMethod> getSynthesizedServiceLoadMethods() {
+    return synthesizedServiceLoadMethods;
   }
 
   public void rewrite(
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    DexItemFactory factory = appView.dexItemFactory();
     InstructionListIterator instructionIterator = code.instructionListIterator();
     // Create a map from service type to loader methods local to this context since two
     // service loader calls to the same type in different methods and in the same wave can race.
@@ -89,54 +102,82 @@
       Instruction instruction = instructionIterator.next();
 
       // Check if instruction is an invoke static on the desired form of ServiceLoader.load.
-      if (!instruction.isInvokeStatic()
-          || instruction.asInvokeStatic().getInvokedMethod()
-              != factory.serviceLoaderMethods.loadWithClassLoader) {
+      if (!instruction.isInvokeStatic()) {
         continue;
       }
 
       InvokeStatic serviceLoaderLoad = instruction.asInvokeStatic();
-      Value serviceLoaderLoadOut = serviceLoaderLoad.outValue();
-      if (serviceLoaderLoadOut.numberOfAllUsers() != 1 || serviceLoaderLoadOut.hasPhiUsers()) {
-        continue;
-      }
-
-      // Check that the only user is a call to iterator().
-      if (!serviceLoaderLoadOut.singleUniqueUser().isInvokeVirtual()
-          || serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual().getInvokedMethod()
-              != factory.serviceLoaderMethods.iterator) {
+      DexMethod invokedMethod = serviceLoaderLoad.getInvokedMethod();
+      if (!serviceLoaderMethods.isLoadMethod(invokedMethod)) {
         continue;
       }
 
       // Check that the first argument is a const class.
       Value argument = serviceLoaderLoad.inValues().get(0).getAliasedValue();
       if (argument.isPhi() || !argument.definition.isConstClass()) {
+        report(code.origin, null, "The service loader type could not be determined");
         continue;
       }
 
       ConstClass constClass = argument.getConstInstruction().asConstClass();
 
+      if (invokedMethod != serviceLoaderMethods.loadWithClassLoader) {
+        report(
+            code.origin,
+            constClass.getType(),
+            "Inlining is only support for `java.util.ServiceLoader.load(java.lang.Class,"
+                + " java.lang.ClassLoader)`");
+        continue;
+      }
+
+      String invalidUserMessage =
+          "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
+              + " java.lang.ServiceLoader.iterator()`";
+      Value serviceLoaderLoadOut = serviceLoaderLoad.outValue();
+      if (serviceLoaderLoadOut.numberOfAllUsers() != 1 || serviceLoaderLoadOut.hasPhiUsers()) {
+        report(code.origin, constClass.getType(), invalidUserMessage);
+        continue;
+      }
+
+      // Check that the only user is a call to iterator().
+      if (!serviceLoaderLoadOut.singleUniqueUser().isInvokeVirtual()
+          || serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual().getInvokedMethod()
+              != serviceLoaderMethods.iterator) {
+        report(code.origin, constClass.getType(), invalidUserMessage + ", but found other usages");
+        continue;
+      }
+
       // Check that the service is not kept.
       if (appView.appInfo().isPinnedWithDefinitionLookup(constClass.getValue())) {
+        report(code.origin, constClass.getType(), "The service loader type is kept");
         continue;
       }
 
       // Check that the service is configured in the META-INF/services.
-      if (!appView.appServices().allServiceTypes().contains(constClass.getValue())) {
+      AppServices appServices = appView.appServices();
+      if (!appServices.allServiceTypes().contains(constClass.getValue())) {
         // Error already reported in the Enqueuer.
         continue;
       }
 
       // Check that we are not service loading anything from a feature into base.
-      if (appView
-          .appServices()
-          .hasServiceImplementationsInFeature(appView, constClass.getValue())) {
+      if (appServices.hasServiceImplementationsInFeature(appView, constClass.getValue())) {
+        report(
+            code.origin,
+            constClass.getType(),
+            "The service loader type has implementations in a feature split");
         continue;
       }
 
       // Check that ClassLoader used is the ClassLoader defined for the service configuration
       // that we are instantiating or NULL.
       if (serviceLoaderLoad.inValues().get(1).isPhi()) {
+        report(
+            code.origin,
+            constClass.getType(),
+            "The java.lang.ClassLoader argument must be defined locally as null or "
+                + constClass.getType()
+                + ".class.getClassLoader()");
         continue;
       }
       InvokeVirtual classLoaderInvoke =
@@ -154,19 +195,28 @@
                           .getValue()
                       == constClass.getValue());
       if (!isGetClassLoaderOnConstClassOrNull) {
+        report(
+            code.origin,
+            constClass.getType(),
+            "The java.lang.ClassLoader argument must be defined locally as null or "
+                + constClass.getType()
+                + ".class.getClassLoader()");
         continue;
       }
 
-      List<DexType> dexTypes =
-          appView.appServices().serviceImplementationsFor(constClass.getValue());
+      List<DexType> dexTypes = appServices.serviceImplementationsFor(constClass.getValue());
       List<DexClass> classes = new ArrayList<>(dexTypes.size());
       boolean seenNull = false;
       for (DexType serviceImpl : dexTypes) {
-        DexClass serviceImplClazz = appView.definitionFor(serviceImpl);
-        if (serviceImplClazz == null) {
+        DexClass serviceImplementation = appView.definitionFor(serviceImpl);
+        if (serviceImplementation == null) {
+          report(
+              code.origin,
+              constClass.getType(),
+              "Unable to find definition for service implementation " + serviceImpl.getTypeName());
           seenNull = true;
         }
-        classes.add(serviceImplClazz);
+        classes.add(serviceImplementation);
       }
 
       if (seenNull) {
@@ -193,6 +243,20 @@
     }
   }
 
+  private void report(Origin origin, DexType serviceLoaderType, String message) {
+    if (reporter != null) {
+      reporter.info(
+          new ServiceLoaderRewriterDiagnostic(
+              origin,
+              "Could not inline ServiceLoader.load"
+                  + (serviceLoaderType == null
+                      ? ""
+                      : (" of type " + serviceLoaderType.getTypeName()))
+                  + ": "
+                  + message));
+    }
+  }
+
   private DexEncodedMethod createSynthesizedMethod(
       DexType serviceType,
       List<DexClass> classes,
@@ -218,8 +282,8 @@
                             m ->
                                 ServiceLoaderSourceCode.generate(
                                     serviceType, classes, appView.dexItemFactory())));
-    synchronized (serviceLoadMethods) {
-      serviceLoadMethods.add(method);
+    synchronized (synthesizedServiceLoadMethods) {
+      synthesizedServiceLoadMethods.add(method);
     }
     methodProcessor
         .getEventConsumer()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriterDiagnostic.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriterDiagnostic.java
new file mode 100644
index 0000000..ce17cc0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriterDiagnostic.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2023, 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.optimize;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class ServiceLoaderRewriterDiagnostic implements Diagnostic {
+
+  private final Origin origin;
+  private final String message;
+
+  ServiceLoaderRewriterDiagnostic(Origin origin, String message) {
+    this.origin = origin;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
index 4a91dc0..6672c87 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import java.util.ArrayList;
 import java.util.List;
@@ -21,13 +22,20 @@
 
 public class EnumDataMap {
   private final ImmutableMap<DexType, EnumData> map;
+  private final ImmutableMap<DexType, DexType> subEnumToSuperEnumMap;
 
   public static EnumDataMap empty() {
-    return new EnumDataMap(ImmutableMap.of());
+    return new EnumDataMap(ImmutableMap.of(), ImmutableMap.of());
   }
 
-  public EnumDataMap(ImmutableMap<DexType, EnumData> map) {
+  public EnumDataMap(
+      ImmutableMap<DexType, EnumData> map, ImmutableMap<DexType, DexType> subEnumToSuperEnumMap) {
     this.map = map;
+    this.subEnumToSuperEnumMap = subEnumToSuperEnumMap;
+  }
+
+  public boolean hasAnyEnumsWithSubtypes() {
+    return !subEnumToSuperEnumMap.isEmpty();
   }
 
   public void checkEnumsUnboxed(AppView<AppInfoWithLiveness> appView) {
@@ -47,47 +55,75 @@
     }
   }
 
-  public boolean isUnboxedEnum(DexProgramClass clazz) {
-    return isUnboxedEnum(clazz.getType());
+  public boolean isAssignableTo(DexType subtype, DexType superType) {
+    assert superType != null;
+    assert subtype != null;
+    if (superType == subtype) {
+      return true;
+    }
+    return superType == subEnumToSuperEnumMap.get(subtype);
   }
 
-  public boolean isUnboxedEnum(DexType type) {
-    return map.containsKey(type);
+  public DexType representativeType(DexType type) {
+    return subEnumToSuperEnumMap.getOrDefault(type, type);
   }
 
   public boolean isEmpty() {
     return map.isEmpty();
   }
 
-  public EnumData get(DexProgramClass enumClass) {
-    EnumData enumData = map.get(enumClass.getType());
+  public boolean isSuperUnboxedEnum(DexType type) {
+    return map.containsKey(type);
+  }
+
+  public boolean isUnboxedEnum(DexProgramClass clazz) {
+    return isUnboxedEnum(clazz.getType());
+  }
+
+  public boolean isUnboxedEnum(DexType type) {
+    return map.containsKey(representativeType(type));
+  }
+
+  private EnumData get(DexType type) {
+    EnumData enumData = map.get(representativeType(type));
     assert enumData != null;
     return enumData;
   }
 
-  public Set<DexType> getUnboxedEnums() {
+  public EnumData get(DexProgramClass enumClass) {
+    return get(enumClass.getType());
+  }
+
+  public Set<DexType> getUnboxedSuperEnums() {
     return map.keySet();
   }
 
+  public Set<DexType> computeAllUnboxedEnums() {
+    Set<DexType> items = Sets.newIdentityHashSet();
+    items.addAll(map.keySet());
+    items.addAll(subEnumToSuperEnumMap.keySet());
+    return items;
+  }
+
   public EnumInstanceFieldKnownData getInstanceFieldData(
       DexType enumType, DexField enumInstanceField) {
-    assert map.containsKey(enumType);
-    return map.get(enumType).getInstanceFieldData(enumInstanceField);
+    assert isUnboxedEnum(enumType);
+    return get(enumType).getInstanceFieldData(enumInstanceField);
   }
 
   public boolean hasUnboxedValueFor(DexField enumStaticField) {
-    return isUnboxedEnum(enumStaticField.holder)
-        && map.get(enumStaticField.holder).hasUnboxedValueFor(enumStaticField);
+    return isUnboxedEnum(enumStaticField.getHolderType())
+        && get(enumStaticField.getHolderType()).hasUnboxedValueFor(enumStaticField);
   }
 
   public int getUnboxedValue(DexField enumStaticField) {
-    assert map.containsKey(enumStaticField.holder);
-    return map.get(enumStaticField.holder).getUnboxedValue(enumStaticField);
+    assert isUnboxedEnum(enumStaticField.getHolderType());
+    return get(enumStaticField.getHolderType()).getUnboxedValue(enumStaticField);
   }
 
   public int getValuesSize(DexType enumType) {
-    assert map.containsKey(enumType);
-    return map.get(enumType).getValuesSize();
+    assert isUnboxedEnum(enumType);
+    return get(enumType).getValuesSize();
   }
 
   public int getMaxValuesSize() {
@@ -101,8 +137,8 @@
   }
 
   public boolean matchesValuesField(DexField staticField) {
-    assert map.containsKey(staticField.holder);
-    return map.get(staticField.holder).matchesValuesField(staticField);
+    assert isUnboxedEnum(staticField.getHolderType());
+    return get(staticField.getHolderType()).matchesValuesField(staticField);
   }
 
   public static class EnumData {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index c147322..ee9878a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -116,9 +116,11 @@
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.util.ArrayList;
@@ -683,8 +685,8 @@
 
     GraphLens previousLens = appView.graphLens();
     ImmutableSet<DexType> enumsToUnbox = enumUnboxingCandidatesInfo.candidates();
-    ImmutableSet<DexProgramClass> enumClassesToUnbox =
-        enumUnboxingCandidatesInfo.candidateClasses();
+    ImmutableMap<DexProgramClass, Set<DexProgramClass>> enumClassesToUnbox =
+        enumUnboxingCandidatesInfo.candidateClassesWithSubclasses();
     LongLivedProgramMethodSetBuilder<ProgramMethodSet> dependencies =
         enumUnboxingCandidatesInfo.allMethodDependencies();
     enumUnboxingCandidatesInfo.clear();
@@ -693,7 +695,7 @@
 
     EnumUnboxingUtilityClasses utilityClasses =
         EnumUnboxingUtilityClasses.builder(appView)
-            .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox, enumDataMap)
+            .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox.keySet(), enumDataMap)
             .build(converter, executorService);
 
     // Fixup the application.
@@ -729,7 +731,8 @@
         .merge(
             methodsDependingOnLibraryModelisation
                 .rewrittenWithLens(appView)
-                .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()));
+                .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()))
+        .addAll(treeFixerResult.getDispatchMethods(), appView.graphLens());
     methodsDependingOnLibraryModelisation.clear();
 
     updateOptimizationInfos(executorService, feedback, treeFixerResult, previousLens);
@@ -806,15 +809,18 @@
       debugLogs.keySet().forEach(enumUnboxingCandidatesInfo::removeCandidate);
       reportEnumsAnalysis();
     }
-    assert enumDataMap.getUnboxedEnums().size() == enumUnboxingCandidatesInfo.candidates().size();
+    assert enumDataMap.getUnboxedSuperEnums().size()
+        == enumUnboxingCandidatesInfo.candidates().size();
     return enumDataMap;
   }
 
   private EnumDataMap analyzeEnumInstances() {
+    ImmutableMap.Builder<DexType, DexType> enumSubtypes = ImmutableMap.builder();
     ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder();
-    enumUnboxingCandidatesInfo.forEachCandidateAndRequiredInstanceFieldData(
-        (enumClass, instanceFields) -> {
-          EnumData data = buildData(enumClass, instanceFields);
+    enumUnboxingCandidatesInfo.forEachCandidateInfo(
+        info -> {
+          DexProgramClass enumClass = info.getEnumClass();
+          EnumData data = buildData(enumClass, info.getRequiredInstanceFieldData());
           if (data == null) {
             // Reason is already reported at this point.
             enumUnboxingCandidatesInfo.removeCandidate(enumClass);
@@ -822,10 +828,17 @@
           }
           if (!debugLogEnabled || !debugLogs.containsKey(enumClass.getType())) {
             builder.put(enumClass.type, data);
+            if (data.valuesTypes != null) {
+              for (DexType value : data.valuesTypes.values()) {
+                if (value != enumClass.type) {
+                  enumSubtypes.put(value, enumClass.type);
+                }
+              }
+            }
           }
         });
     staticFieldValuesMap.clear();
-    return new EnumDataMap(builder.build());
+    return new EnumDataMap(builder.build(), enumSubtypes.build());
   }
 
   private EnumData buildData(DexProgramClass enumClass, Set<DexField> instanceFields) {
@@ -854,13 +867,21 @@
       return null;
     }
 
+    enumStaticFieldValues =
+        enumStaticFieldValues.rewrittenWithLens(appView, appView.graphLens(), appView.codeLens());
+    Set<DexType> enumSubtypes = enumUnboxingCandidatesInfo.getSubtypes(enumClass.getType());
+
     // Step 1: We iterate over the field to find direct enum instance information and the values
     // fields.
     for (DexEncodedField staticField : enumClass.staticFields()) {
-      if (factory.enumMembers.isEnumField(staticField, enumClass.type)) {
+      // The field might be specialized while the data was recorded without the specialization.
+      if (factory.enumMembers.isEnumField(staticField, enumClass.type, enumSubtypes)) {
         ObjectState enumState =
             enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference());
         if (enumState == null) {
+          assert enumStaticFieldValues.getObjectStateForPossiblyPinnedField(
+                  staticField.getReference().withType(enumClass.type, factory))
+              == null;
           if (staticField.getOptimizationInfo().isDead()) {
             // We don't care about unused field data.
             continue;
@@ -957,7 +978,7 @@
 
     return new EnumData(
         instanceFieldsData,
-        isEnumWithSubtypes ? valueTypes : null,
+        isEnumWithSubtypes ? valueTypes : Int2ReferenceMaps.emptyMap(),
         unboxedValues.build(),
         valuesField.build(),
         valuesContents == null ? EnumData.INVALID_VALUES_SIZE : valuesContents.getEnumValuesSize());
@@ -1051,26 +1072,44 @@
   }
 
   private void analyzeInitializers() {
-    enumUnboxingCandidatesInfo.forEachCandidate(
-        enumClass -> {
-          for (DexEncodedMethod directMethod : enumClass.directMethods()) {
-            if (directMethod.isInstanceInitializer()) {
-              if (directMethod
-                  .getOptimizationInfo()
-                  .getContextInsensitiveInstanceInitializerInfo()
-                  .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
-                if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) {
-                  break;
-                }
-              }
+    enumUnboxingCandidatesInfo.forEachCandidateInfo(
+        (info) -> {
+          DexProgramClass enumClass = info.getEnumClass();
+          if (!instanceInitializersAllowUnboxing(enumClass)) {
+            if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) {
+              return;
             }
           }
           if (enumClass.classInitializationMayHaveSideEffects(appView)) {
-            markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
+            if (markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass)) {
+              return;
+            }
+          }
+          for (DexProgramClass subclass : info.getSubclasses()) {
+            if (!instanceInitializersAllowUnboxing(subclass)) {
+              if (markEnumAsUnboxable(Reason.INVALID_SUBTYPE_INIT, enumClass)) {
+                return;
+              }
+            }
+            if (subclass.hasClassInitializer()) {
+              if (markEnumAsUnboxable(Reason.SUBTYPE_CLINIT, enumClass)) {
+                return;
+              }
+            }
           }
         });
   }
 
+  private boolean instanceInitializersAllowUnboxing(DexProgramClass clazz) {
+    return !Iterables.any(
+        clazz.programInstanceInitializers(),
+        instanceInitializer ->
+            instanceInitializer
+                .getOptimizationInfo()
+                .getContextInsensitiveInstanceInitializerInfo()
+                .mayHaveOtherSideEffectsThanInstanceFieldAssignments());
+  }
+
   private Reason instructionAllowEnumUnboxing(
       Instruction instruction, IRCode code, DexProgramClass enumClass, Value enumValue) {
     ProgramMethod context = code.context();
@@ -1142,6 +1181,18 @@
     return Reason.ELIGIBLE;
   }
 
+  private boolean isAssignableToArray(Value value, ClassTypeElement arrayBaseType) {
+    TypeElement valueType = value.getType();
+    if (valueType.isNullType()) {
+      return true;
+    }
+    TypeElement valueBaseType =
+        valueType.isArrayType() ? valueType.asArrayType().getBaseType() : valueType;
+    assert valueBaseType.isClassType();
+    return enumUnboxingCandidatesInfo.isAssignableTo(
+        valueBaseType.asClassType().getClassType(), arrayBaseType.getClassType());
+  }
+
   private Reason analyzeArrayPutUser(
       ArrayPut arrayPut,
       IRCode code,
@@ -1157,14 +1208,7 @@
     assert arrayType.isArrayType();
     assert arrayType.asArrayType().getBaseType().isClassType();
     ClassTypeElement arrayBaseType = arrayType.asArrayType().getBaseType().asClassType();
-    TypeElement valueBaseType = arrayPut.value().getType();
-    if (valueBaseType.isArrayType()) {
-      assert valueBaseType.asArrayType().getBaseType().isClassType();
-      assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
-      valueBaseType = valueBaseType.asArrayType().getBaseType();
-    }
-    if (arrayBaseType.equalUpToNullability(valueBaseType)
-        && arrayBaseType.getClassType() == enumClass.type) {
+    if (isAssignableToArray(arrayPut.value(), arrayBaseType)) {
       return Reason.ELIGIBLE;
     }
     return Reason.INVALID_ARRAY_PUT;
@@ -1191,13 +1235,7 @@
     }
 
     for (Value value : invokeNewArray.inValues()) {
-      TypeElement valueBaseType = value.getType();
-      if (valueBaseType.isArrayType()) {
-        assert valueBaseType.asArrayType().getBaseType().isClassType();
-        assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
-        valueBaseType = valueBaseType.asArrayType().getBaseType();
-      }
-      if (!arrayBaseType.equalUpToNullability(valueBaseType)) {
+      if (!isAssignableToArray(value, arrayBaseType)) {
         return Reason.INVALID_INVOKE_NEW_ARRAY;
       }
     }
@@ -1305,7 +1343,9 @@
       if (targetHolder.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
         // The enum instance initializer is only allowed to be called from an initializer of the
         // enum itself.
-        if (code.context().getHolder() != targetHolder || !code.method().isInitializer()) {
+        if (getEnumUnboxingCandidateOrNull(code.context().getHolder().getType())
+                != getEnumUnboxingCandidateOrNull(targetHolder.getType())
+            || !context.getDefinition().isInitializer()) {
           return Reason.INVALID_INIT;
         }
         if (code.method().isInstanceInitializer() && !invoke.getFirstArgument().isThis()) {
@@ -1340,7 +1380,8 @@
       // enum's type.
       for (int i = 0; i < singleTarget.getParameters().size(); i++) {
         if (invoke.getArgumentForParameter(i) == enumValue
-            && singleTarget.getParameter(i).toBaseType(factory) != enumClass.getType()) {
+            && !enumUnboxingCandidatesInfo.isAssignableTo(
+                singleTarget.getParameter(i).toBaseType(factory), enumClass.getType())) {
           return new IllegalInvokeWithImpreciseParameterTypeReason(singleTargetReference);
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index abc0fde..e12cdc8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -72,11 +71,12 @@
   }
 
   private void setEnumSubclassesOnCandidates() {
-    enumToUnboxCandidates.forEachCandidate(
-        candidate ->
-            enumToUnboxCandidates.setEnumSubclasses(
-                candidate.getType(),
-                enumSubclasses.getOrDefault(candidate.getType(), ImmutableSet.of())));
+    enumToUnboxCandidates.forEachCandidateInfo(
+        info -> {
+          DexType type = info.getEnumClass().getType();
+          enumToUnboxCandidates.setEnumSubclasses(
+              type, enumSubclasses.getOrDefault(type, ImmutableSet.of()));
+        });
   }
 
   private void removeIneligibleCandidates() {
@@ -132,14 +132,6 @@
       }
       result = false;
     }
-    // TODO(b/271385332): Support subEnums with static members (JDK16+).
-    if (!clazz.staticFields().isEmpty()
-        || !Iterables.isEmpty(clazz.directMethods(DexEncodedMethod::isStatic))) {
-      if (!enumUnboxer.reportFailure(clazz.superType, Reason.SUBENUM_STATIC_MEMBER)) {
-        return false;
-      }
-      result = false;
-    }
     return result;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
index bfeb83b..3c2fc90 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.enums;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -12,20 +13,23 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 public class EnumUnboxingCandidateInfoCollection {
 
   private final Map<DexType, EnumUnboxingCandidateInfo> enumTypeToInfo = new ConcurrentHashMap<>();
+  private final Map<DexType, DexType> subEnumToSuperEnumMap = new IdentityHashMap<>();
   private final Set<DexMethod> prunedMethods = Sets.newConcurrentHashSet();
 
   public void addCandidate(
@@ -42,8 +46,16 @@
     return !enumTypeToInfo.get(enumType).getSubclasses().isEmpty();
   }
 
+  public Set<DexType> getSubtypes(DexType enumType) {
+    return SetUtils.mapIdentityHashSet(
+        enumTypeToInfo.get(enumType).getSubclasses(), DexClass::getType);
+  }
+
   public void setEnumSubclasses(DexType superEnum, Set<DexProgramClass> subclasses) {
     enumTypeToInfo.get(superEnum).setSubclasses(subclasses);
+    for (DexProgramClass subclass : subclasses) {
+      subEnumToSuperEnumMap.put(subclass.getType(), superEnum);
+    }
   }
 
   public void addPrunedMethod(ProgramMethod method) {
@@ -70,16 +82,27 @@
     return ImmutableSet.copyOf(enumTypeToInfo.keySet());
   }
 
-  public ImmutableSet<DexProgramClass> candidateClasses() {
-    ImmutableSet.Builder<DexProgramClass> builder = ImmutableSet.builder();
+  public ImmutableMap<DexProgramClass, Set<DexProgramClass>> candidateClassesWithSubclasses() {
+    ImmutableMap.Builder<DexProgramClass, Set<DexProgramClass>> builder = ImmutableMap.builder();
     for (EnumUnboxingCandidateInfo info : enumTypeToInfo.values()) {
-      builder.add(info.getEnumClass());
+      builder.put(info.getEnumClass(), info.getSubclasses());
     }
     return builder.build();
   }
 
+  /** Answers true if both enums are identical, or if one inherit from the other. */
+  public boolean isAssignableTo(DexType subtype, DexType superType) {
+    assert superType != null;
+    assert subtype != null;
+    if (superType == subtype) {
+      return true;
+    }
+    return superType == subEnumToSuperEnumMap.get(subtype);
+  }
+
   public DexProgramClass getCandidateClassOrNull(DexType enumType) {
-    EnumUnboxingCandidateInfo info = enumTypeToInfo.get(enumType);
+    DexType superEnum = subEnumToSuperEnumMap.getOrDefault(enumType, enumType);
+    EnumUnboxingCandidateInfo info = enumTypeToInfo.get(superEnum);
     if (info == null) {
       return null;
     }
@@ -120,16 +143,8 @@
     info.addRequiredInstanceFieldData(field);
   }
 
-  public void forEachCandidate(Consumer<DexProgramClass> enumClassConsumer) {
-    enumTypeToInfo.values().forEach(info -> enumClassConsumer.accept(info.enumClass));
-  }
-
-  public void forEachCandidateAndRequiredInstanceFieldData(
-      BiConsumer<DexProgramClass, Set<DexField>> biConsumer) {
-    enumTypeToInfo
-        .values()
-        .forEach(
-            info -> biConsumer.accept(info.getEnumClass(), info.getRequiredInstanceFieldData()));
+  public void forEachCandidateInfo(Consumer<EnumUnboxingCandidateInfo> consumer) {
+    enumTypeToInfo.values().forEach(consumer);
   }
 
   public void clear() {
@@ -143,7 +158,7 @@
     return true;
   }
 
-  private static class EnumUnboxingCandidateInfo {
+  public static class EnumUnboxingCandidateInfo {
 
     private final DexProgramClass enumClass;
     private final LongLivedProgramMethodSetBuilder<ProgramMethodSet> methodDependencies;
@@ -164,6 +179,7 @@
     }
 
     public Set<DexProgramClass> getSubclasses() {
+      assert subclasses != null;
       return subclasses;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index 91e6e67..da8ac44 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -10,6 +10,8 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
@@ -43,10 +45,11 @@
   EnumUnboxingLens(
       AppView<?> appView,
       BidirectionalOneToOneMap<DexField, DexField> fieldMap,
-      BidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod> methodMap,
+      BidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod> renamedSignatures,
       Map<DexType, DexType> typeMap,
+      Map<DexMethod, DexMethod> methodMap,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod) {
-    super(appView, fieldMap, methodMap::getRepresentativeValue, typeMap, methodMap);
+    super(appView, fieldMap, methodMap, typeMap, renamedSignatures);
     assert !appView.unboxedEnums().isEmpty();
     this.abstractValueFactory = appView.abstractValueFactory();
     this.prototypeChangesPerMethod = prototypeChangesPerMethod;
@@ -69,6 +72,69 @@
   }
 
   @Override
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    return !unboxedEnums.hasAnyEnumsWithSubtypes()
+        && getPrevious().isContextFreeForMethods(codeLens);
+  }
+
+  @Override
+  public boolean verifyIsContextFreeForMethod(DexMethod method, GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    assert getPrevious().verifyIsContextFreeForMethod(getPreviousMethodSignature(method), codeLens);
+    DexMethod previous =
+        getPrevious()
+            .lookupMethod(getPreviousMethodSignature(method), null, null, codeLens)
+            .getReference();
+    assert unboxedEnums.representativeType(previous.getHolderType()) == previous.getHolderType();
+    return true;
+  }
+
+  @Override
+  public MethodLookupResult internalDescribeLookupMethod(
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
+    assert context != null || verifyIsContextFreeForMethod(previous.getReference(), codeLens);
+    assert context == null || previous.getType() != null;
+    DexMethod result;
+    if (previous.getType() == InvokeType.SUPER) {
+      assert context != null;
+      DexMethod previousContext = getPreviousMethodSignature(context);
+      DexType superEnum = unboxedEnums.representativeType(previousContext.getHolderType());
+      if (unboxedEnums.isUnboxedEnum(superEnum)) {
+        if (superEnum != previousContext.getHolderType()) {
+          DexMethod reference = previous.getReference();
+          if (reference.getHolderType() != superEnum) {
+            // We are in an enum subtype where super-invokes are rebound differently.
+            reference = reference.withHolder(superEnum, dexItemFactory());
+          }
+          result = newMethodSignatures.getRepresentativeValue(reference);
+        } else {
+          // This is a super-invoke to a library method, not rewritten by the lens.
+          // This is rewritten by the EnumUnboxerRewriter.
+          return previous;
+        }
+      } else {
+        result = methodMap.apply(previous.getReference());
+      }
+    } else {
+      result = methodMap.apply(previous.getReference());
+    }
+    if (result == null) {
+      return previous;
+    }
+    return MethodLookupResult.builder(this)
+        .setReference(result)
+        .setPrototypeChanges(
+            internalDescribePrototypeChanges(previous.getPrototypeChanges(), result))
+        .setType(mapInvocationType(result, previous.getReference(), previous.getType()))
+        .build();
+  }
+
+  @Override
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
       RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
     // Rewrite the single value of the given RewrittenPrototypeDescription if it is referring to an
@@ -133,6 +199,7 @@
         new BidirectionalOneToOneHashMap<>();
     private final MutableBidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod>
         newMethodSignatures = new BidirectionalOneToManyRepresentativeHashMap<>();
+    private final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
 
     private Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
         new IdentityHashMap<>();
@@ -155,18 +222,59 @@
       newFieldSignatures.put(from, to);
     }
 
-    public void move(DexMethod from, DexMethod to, boolean fromStatic, boolean toStatic) {
-      move(from, to, fromStatic, toStatic, Collections.emptyList());
+    private RewrittenPrototypeDescription recordPrototypeChanges(
+        DexMethod from,
+        DexMethod to,
+        boolean fromStatic,
+        boolean toStatic,
+        boolean virtualReceiverAlreadyRemapped,
+        List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
+      assert from != to;
+      RewrittenPrototypeDescription prototypeChanges =
+          computePrototypeChanges(
+              from,
+              to,
+              fromStatic,
+              toStatic,
+              virtualReceiverAlreadyRemapped,
+              extraUnusedNullParameters);
+      prototypeChangesPerMethod.put(to, prototypeChanges);
+      return prototypeChanges;
     }
 
-    public RewrittenPrototypeDescription move(
+    public void moveAndMap(DexMethod from, DexMethod to, boolean fromStatic) {
+      moveAndMap(from, to, fromStatic, true, Collections.emptyList());
+    }
+
+    public RewrittenPrototypeDescription moveVirtual(DexMethod from, DexMethod to) {
+      newMethodSignatures.put(from, to);
+      return recordPrototypeChanges(from, to, false, true, false, Collections.emptyList());
+    }
+
+    public RewrittenPrototypeDescription mapToDispatch(DexMethod from, DexMethod to) {
+      methodMap.put(from, to);
+      return recordPrototypeChanges(from, to, false, true, true, Collections.emptyList());
+    }
+
+    public RewrittenPrototypeDescription moveAndMap(
         DexMethod from,
         DexMethod to,
         boolean fromStatic,
         boolean toStatic,
         List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
-      assert from != to;
       newMethodSignatures.put(from, to);
+      methodMap.put(from, to);
+      return recordPrototypeChanges(
+          from, to, fromStatic, toStatic, false, extraUnusedNullParameters);
+    }
+
+    private RewrittenPrototypeDescription computePrototypeChanges(
+        DexMethod from,
+        DexMethod to,
+        boolean fromStatic,
+        boolean toStatic,
+        boolean virtualReceiverAlreadyRemapped,
+        List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
       int offsetDiff = 0;
       int toOffset = BooleanUtils.intValue(!toStatic);
       ArgumentInfoCollection.Builder builder =
@@ -175,14 +283,21 @@
       if (fromStatic != toStatic) {
         assert toStatic;
         offsetDiff = 1;
-        builder
-            .addArgumentInfo(
-                0,
-                RewrittenTypeInfo.builder()
-                    .setOldType(from.getHolderType())
-                    .setNewType(to.getParameter(0))
-                    .build())
-            .setIsConvertedToStaticMethod();
+        if (!virtualReceiverAlreadyRemapped) {
+          builder
+              .addArgumentInfo(
+                  0,
+                  RewrittenTypeInfo.builder()
+                      .setOldType(from.getHolderType())
+                      .setNewType(to.getParameter(0))
+                      .build())
+              .setIsConvertedToStaticMethod();
+        } else {
+          assert to.getParameter(0).isIntType();
+          assert !fromStatic;
+          assert toStatic;
+          assert from.getArity() == to.getArity() - 1;
+        }
       }
       for (int i = 0; i < from.getParameters().size(); i++) {
         DexType fromType = from.getParameter(i);
@@ -200,11 +315,8 @@
                   .setOldType(from.getReturnType())
                   .setNewType(to.getReturnType())
                   .build();
-      RewrittenPrototypeDescription prototypeChanges =
-          RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build())
-              .withExtraParameters(extraUnusedNullParameters);
-      prototypeChangesPerMethod.put(to, prototypeChanges);
-      return prototypeChanges;
+      return RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build())
+          .withExtraParameters(extraUnusedNullParameters);
     }
 
     void recordCheckNotZeroMethod(
@@ -227,6 +339,7 @@
           newFieldSignatures,
           newMethodSignatures,
           typeMap,
+          methodMap,
           ImmutableMap.copyOf(prototypeChangesPerMethod));
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index c9f6a96..cbbd4b9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.ArrayAccess;
+import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.ConstNumber;
@@ -78,7 +79,7 @@
   }
 
   private LocalEnumUnboxingUtilityClass getLocalUtilityClass(DexType enumType) {
-    return utilityClasses.getLocalUtilityClass(enumType);
+    return utilityClasses.getLocalUtilityClass(unboxedEnumsData.representativeType(enumType));
   }
 
   private SharedEnumUnboxingUtilityClass getSharedUtilityClass() {
@@ -208,14 +209,31 @@
               rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
               continue;
             } else if (invokedMethod.match(factory.enumMembers.toString)) {
-              DexMethod lookupMethod = enumUnboxingLens.lookupMethod(invokedMethod);
-              // If the lookupMethod is different, then a toString method was on the enumType
-              // class, which was moved, and the lens code rewriter will rewrite the invoke to
-              // that method.
-              if (invoke.isInvokeSuper() || lookupMethod == invokedMethod) {
+              DexMethod reboundMethod =
+                  invokedMethod.withHolder(unboxedEnumsData.representativeType(enumType), factory);
+              DexMethod lookupMethod =
+                  enumUnboxingLens
+                      .lookupMethod(
+                          reboundMethod,
+                          context.getReference(),
+                          invoke.getType(),
+                          enumUnboxingLens.getPrevious())
+                      .getReference();
+              // If the SuperEnum had declared a toString() override, then the unboxer moves it to
+              // the local utility class method corresponding to that override.
+              // If a SubEnum had declared a toString() override, then the unboxer records a
+              // synthetic move from SuperEnum.toString() to the dispatch method on the local
+              // utility class.
+              // When they are the same, then there are no overrides of toString().
+              if (lookupMethod == reboundMethod) {
                 rewriteNameMethod(iterator, invoke, enumType, context, eventConsumer);
-                continue;
+              } else {
+                DexClassAndMethod dexClassAndMethod = appView.definitionFor(lookupMethod);
+                assert dexClassAndMethod != null;
+                assert dexClassAndMethod.isProgramMethod();
+                replaceEnumInvoke(iterator, invoke, dexClassAndMethod.asProgramMethod());
               }
+              continue;
             } else if (invokedMethod == factory.objectMembers.getClass) {
               rewriteNullCheck(iterator, invoke, context, eventConsumer);
               continue;
@@ -347,6 +365,14 @@
             arrayAccess = arrayAccess.withMemberType(MemberType.INT);
             iterator.replaceCurrentInstruction(arrayAccess);
             convertedEnums.put(arrayAccess, enumType);
+            if (arrayAccess.isArrayPut()) {
+              ArrayPut arrayPut = arrayAccess.asArrayPut();
+              if (arrayPut.value().getType().isNullType()) {
+                iterator.previous();
+                arrayPut.replacePutValue(iterator.insertConstIntInstruction(code, options, 0));
+                iterator.next();
+              }
+            }
           }
           assert validateArrayAccess(arrayAccess);
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 738ed1e..8d04a32 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -5,16 +5,19 @@
 package com.android.tools.r8.ir.optimize.enums;
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.ir.optimize.enums.EnumUnboxerImpl.ordinalToUnboxedInt;
 
 import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedField.Builder;
 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.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
@@ -49,15 +52,22 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
+import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider.EnumUnboxingMethodDispatchCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider.EnumUnboxingMethodDispatchCfCodeProvider.CfCodeWithLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ImmutableArrayUtils;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -77,56 +87,76 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods;
   private final DexItemFactory factory;
+  // Provides information for each unboxed enum regarding the instance field values of each enum
+  // instance, the original instance class, and the ordinal value.
   private final EnumDataMap enumDataMap;
-  private final Set<DexProgramClass> unboxedEnums;
+  // Maps the superEnum to the enumSubtypes. This is already present in the enumDataMap as DexTypes,
+  // we duplicate that here as DexProgramClasses.
+  private final Map<DexProgramClass, Set<DexProgramClass>> unboxedEnumHierarchy;
   private final EnumUnboxingUtilityClasses utilityClasses;
+  private final ProgramMethodMap<CfCodeWithLens> dispatchMethods = ProgramMethodMap.create();
 
   EnumUnboxingTreeFixer(
       AppView<AppInfoWithLiveness> appView,
       ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods,
       EnumDataMap enumDataMap,
-      Set<DexProgramClass> unboxedEnums,
+      Map<DexProgramClass, Set<DexProgramClass>> unboxedEnums,
       EnumUnboxingUtilityClasses utilityClasses) {
     this.appView = appView;
     this.checkNotNullMethods = checkNotNullMethods;
     this.enumDataMap = enumDataMap;
     this.factory = appView.dexItemFactory();
+    this.unboxedEnumHierarchy = unboxedEnums;
     this.lensBuilder =
-        EnumUnboxingLens.enumUnboxingLensBuilder(appView)
-            .mapUnboxedEnums(enumDataMap.getUnboxedEnums());
-    this.unboxedEnums = unboxedEnums;
+        EnumUnboxingLens.enumUnboxingLensBuilder(appView).mapUnboxedEnums(getUnboxedEnums());
     this.utilityClasses = utilityClasses;
   }
 
+  private Set<DexProgramClass> computeUnboxedEnumClasses() {
+    Set<DexProgramClass> unboxedEnumClasses = Sets.newIdentityHashSet();
+    unboxedEnumHierarchy.forEach(
+        (superEnum, subEnums) -> {
+          unboxedEnumClasses.add(superEnum);
+          unboxedEnumClasses.addAll(subEnums);
+        });
+    return unboxedEnumClasses;
+  }
+
+  private Set<DexType> getUnboxedEnums() {
+    return enumDataMap.computeAllUnboxedEnums();
+  }
+
   Result fixupTypeReferences(IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     PrunedItems.Builder prunedItemsBuilder = PrunedItems.builder();
 
     // We do this before so that we can still perform lookup of definitions.
-    fixupEnumClassInitializers(converter, executorService);
+    fixupSuperEnumClassInitializers(converter, executorService);
 
     // Fix all methods and fields using enums to unbox.
     // TODO(b/191617665): Parallelize this fixup.
     for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (enumDataMap.isUnboxedEnum(clazz)) {
+      if (enumDataMap.isSuperUnboxedEnum(clazz.getType())) {
+
         // Clear the initializers and move the other methods to the new location.
         LocalEnumUnboxingUtilityClass localUtilityClass =
             utilityClasses.getLocalUtilityClass(clazz);
         Collection<DexEncodedField> localUtilityFields =
             createLocalUtilityFields(clazz, localUtilityClass, prunedItemsBuilder);
         Collection<DexEncodedMethod> localUtilityMethods =
-            createLocalUtilityMethods(clazz, localUtilityClass, prunedItemsBuilder);
+            createLocalUtilityMethods(
+                clazz, unboxedEnumHierarchy.get(clazz), localUtilityClass, prunedItemsBuilder);
 
-        // Cleanup old class.
-        clazz.clearInstanceFields();
-        clazz.clearStaticFields();
-        clazz.getMethodCollection().clearDirectMethods();
-        clazz.getMethodCollection().clearVirtualMethods();
+        // Cleanup old classes.
+        cleanUpOldClass(clazz);
+        for (DexProgramClass subEnum : unboxedEnumHierarchy.get(clazz)) {
+          cleanUpOldClass(subEnum);
+        }
 
         // Update members on the local utility class.
         localUtilityClass.getDefinition().setDirectMethods(localUtilityMethods);
         localUtilityClass.getDefinition().setStaticFields(localUtilityFields);
-      } else {
+      } else if (!enumDataMap.isUnboxedEnum(clazz.getType())) {
         clazz.getMethodCollection().replaceMethods(this::fixupEncodedMethod);
         clazz.getFieldCollection().replaceFields(this::fixupEncodedField);
       }
@@ -143,7 +173,22 @@
     BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping =
         duplicateCheckNotNullMethods(converter, executorService);
 
-    return new Result(checkNotNullToCheckNotZeroMapping, lens, prunedItemsBuilder.build());
+    ProgramMethodSet dispatchMethodSet = ProgramMethodSet.create();
+    dispatchMethods.forEach(
+        (method, code) -> {
+          dispatchMethodSet.add(method);
+          code.setCodeLens(lens);
+        });
+
+    return new Result(
+        checkNotNullToCheckNotZeroMapping, dispatchMethodSet, lens, prunedItemsBuilder.build());
+  }
+
+  private void cleanUpOldClass(DexProgramClass clazz) {
+    clazz.clearInstanceFields();
+    clazz.clearStaticFields();
+    clazz.getMethodCollection().clearDirectMethods();
+    clazz.getMethodCollection().clearVirtualMethods();
   }
 
   private BiMap<DexMethod, DexMethod> duplicateCheckNotNullMethods(
@@ -154,10 +199,12 @@
     OneTimeMethodProcessor.Builder methodProcessorBuilder =
         OneTimeMethodProcessor.builder(eventConsumer, processorContext);
 
+    Set<DexProgramClass> unboxedEnumClasses = computeUnboxedEnumClasses();
+
     // Only duplicate checkNotNull() methods that are required for enum unboxing.
     checkNotNullMethods.removeIf(
         (checkNotNullMethod, dependentEnums) ->
-            !SetUtils.containsAnyOf(unboxedEnums, dependentEnums));
+            !SetUtils.containsAnyOf(unboxedEnumClasses, dependentEnums));
 
     // For each checkNotNull() method, synthesize a free flowing static checkNotZero() method that
     // takes an int instead of an Object with the same implementation.
@@ -221,21 +268,20 @@
     return checkNotNullToCheckNotZeroMapping;
   }
 
-
-  private void fixupEnumClassInitializers(IRConverter converter, ExecutorService executorService)
-      throws ExecutionException {
+  private void fixupSuperEnumClassInitializers(
+      IRConverter converter, ExecutorService executorService) throws ExecutionException {
     DexEncodedField ordinalField =
         appView
             .appInfo()
             .resolveField(appView.dexItemFactory().enumMembers.ordinalField)
             .getResolvedField();
     ThreadUtils.processItems(
-        unboxedEnums,
-        unboxedEnum -> fixupEnumClassInitializer(converter, unboxedEnum, ordinalField),
+        unboxedEnumHierarchy.keySet(),
+        unboxedEnum -> fixupSuperEnumClassInitializer(converter, unboxedEnum, ordinalField),
         executorService);
   }
 
-  private void fixupEnumClassInitializer(
+  private void fixupSuperEnumClassInitializer(
       IRConverter converter, DexProgramClass unboxedEnum, DexEncodedField ordinalField) {
     if (!unboxedEnum.hasClassInitializer()) {
       assert unboxedEnum.staticFields().isEmpty();
@@ -275,7 +321,7 @@
           // LocalEnumUtility.class.desiredAssertionStatus() instead of
           // int.class.desiredAssertionStatus().
           ConstClass constClass = instruction.asConstClass();
-          if (constClass.getType() != unboxedEnum.getType()) {
+          if (!enumDataMap.isAssignableTo(constClass.getType(), unboxedEnum.getType())) {
             continue;
           }
 
@@ -307,7 +353,7 @@
         } else if (instruction.isNewInstance()) {
           NewInstance newInstance = instruction.asNewInstance();
           DexType rewrittenType = appView.graphLens().lookupType(newInstance.getType());
-          if (rewrittenType == unboxedEnum.getType()) {
+          if (enumDataMap.isAssignableTo(rewrittenType, unboxedEnum.getType())) {
             InvokeDirect constructorInvoke =
                 newInstance.getUniqueConstructorInvoke(appView.dexItemFactory());
             assert constructorInvoke != null;
@@ -386,16 +432,15 @@
             // the enum unboxing rewriter.
             instructionIterator.replaceCurrentInstruction(
                 new NewUnboxedEnumInstance(
-                    unboxedEnum.getType(),
+                    rewrittenType,
                     ordinal,
                     code.createValue(
-                        ClassTypeElement.create(
-                            unboxedEnum.getType(), definitelyNotNull(), appView))));
+                        ClassTypeElement.create(rewrittenType, definitelyNotNull(), appView))));
           }
         } else if (instruction.isStaticPut()) {
           StaticPut staticPut = instruction.asStaticPut();
           DexField rewrittenField = appView.graphLens().lookupField(staticPut.getField());
-          if (rewrittenField.getHolderType() != unboxedEnum.getType()) {
+          if (!enumDataMap.isAssignableTo(rewrittenField.getHolderType(), unboxedEnum.getType())) {
             continue;
           }
 
@@ -485,8 +530,28 @@
                         }));
   }
 
+  private void processMethod(
+      ProgramMethod method,
+      PrunedItems.Builder prunedItemsBuilder,
+      DexMethodSignatureSet nonPrivateVirtualMethods,
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods) {
+    if (method.getDefinition().isClassInitializer()
+        && enumDataMap.representativeType(method.getHolderType()) != method.getHolderType()) {
+      assert method.getDefinition().getCode().isEmptyVoidMethod();
+      prunedItemsBuilder.addRemovedMethod(method.getReference());
+    } else if (method.getDefinition().isInstanceInitializer()) {
+      prunedItemsBuilder.addRemovedMethod(method.getReference());
+    } else if (method.getDefinition().isNonPrivateVirtualMethod()) {
+      nonPrivateVirtualMethods.add(method.getReference());
+    } else {
+      directMoveAndMap(localUtilityClass, localUtilityMethods, method);
+    }
+  }
+
   private Collection<DexEncodedMethod> createLocalUtilityMethods(
       DexProgramClass unboxedEnum,
+      Set<DexProgramClass> subEnums,
       LocalEnumUnboxingUtilityClass localUtilityClass,
       PrunedItems.Builder prunedItemsBuilder) {
     Map<DexMethod, DexEncodedMethod> localUtilityMethods =
@@ -497,23 +562,202 @@
         .getDefinition()
         .forEachMethod(method -> localUtilityMethods.put(method.getReference(), method));
 
+    // First generate all methods but the ones requiring emulated dispatch.
+    DexMethodSignatureSet nonPrivateVirtualMethods = DexMethodSignatureSet.create();
     unboxedEnum.forEachProgramMethod(
-        method -> {
-          if (method.getDefinition().isInstanceInitializer()) {
-            prunedItemsBuilder.addRemovedMethod(method.getReference());
-          } else {
-            DexEncodedMethod newLocalUtilityMethod =
-                createLocalUtilityMethod(
-                    method,
-                    localUtilityClass,
-                    newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
-            assert !localUtilityMethods.containsKey(newLocalUtilityMethod.getReference());
-            localUtilityMethods.put(newLocalUtilityMethod.getReference(), newLocalUtilityMethod);
-          }
-        });
+        method ->
+            processMethod(
+                method,
+                prunedItemsBuilder,
+                nonPrivateVirtualMethods,
+                localUtilityClass,
+                localUtilityMethods));
+    // Second for each subEnum generate the remaining methods if not already generated.
+    for (DexProgramClass subEnum : subEnums) {
+      subEnum.forEachProgramMethod(
+          method ->
+              processMethod(
+                  method,
+                  prunedItemsBuilder,
+                  nonPrivateVirtualMethods,
+                  localUtilityClass,
+                  localUtilityMethods));
+    }
+
+    // Then analyze each method that may require emulated dispatch.
+    for (DexMethodSignature nonPrivateVirtualMethod : nonPrivateVirtualMethods) {
+      processVirtualMethod(
+          nonPrivateVirtualMethod, unboxedEnum, subEnums, localUtilityClass, localUtilityMethods);
+    }
+
     return localUtilityMethods.values();
   }
 
+  private void processVirtualMethod(
+      DexMethodSignature nonPrivateVirtualMethod,
+      DexProgramClass unboxedEnum,
+      Set<DexProgramClass> subEnums,
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods) {
+    // Emulated dispatch is required if there is a "super method" in the superEnum or above,
+    // and at least one override.
+    DexMethod reference = nonPrivateVirtualMethod.withHolder(unboxedEnum.getType(), factory);
+    ProgramMethodSet subimplementations = ProgramMethodSet.create();
+    for (DexProgramClass subEnum : subEnums) {
+      ProgramMethod subMethod = subEnum.lookupProgramMethod(reference);
+      if (subMethod != null) {
+        subimplementations.add(subMethod);
+      }
+    }
+    DexClassAndMethod superMethod = unboxedEnum.lookupProgramMethod(reference);
+    if (superMethod == null) {
+      assert !subimplementations.isEmpty();
+      superMethod = appView.appInfo().lookupSuperTarget(reference, unboxedEnum, appView);
+      assert superMethod == null || superMethod.getReference() == factory.enumMembers.toString;
+    }
+    if (superMethod == null || subimplementations.isEmpty()) {
+      // No emulated dispatch is required, just move everything.
+      if (superMethod != null) {
+        assert superMethod.isProgramMethod();
+        directMoveAndMap(localUtilityClass, localUtilityMethods, superMethod.asProgramMethod());
+      }
+      for (ProgramMethod override : subimplementations) {
+        directMoveAndMap(localUtilityClass, localUtilityMethods, override);
+      }
+      return;
+    }
+    // These methods require emulated dispatch.
+    emulatedDispatchMoveAndMap(
+        localUtilityClass, localUtilityMethods, superMethod, subimplementations);
+  }
+
+  private void emulatedDispatchMoveAndMap(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      DexClassAndMethod superMethod,
+      ProgramMethodSet subimplementations) {
+    assert !subimplementations.isEmpty();
+    DexMethod superUtilityMethod;
+    if (superMethod.isProgramMethod()) {
+      superUtilityMethod =
+          installLocalUtilityMethod(
+                  localUtilityClass, localUtilityMethods, superMethod.asProgramMethod())
+              .getReference();
+    } else {
+      // All methods but toString() are final or non-virtual.
+      // We could support other cases by setting correctly the superUtilityMethod here.
+      assert superMethod.getReference() == factory.enumMembers.toString;
+      superUtilityMethod = localUtilityClass.computeToStringUtilityMethod(factory);
+    }
+    Map<DexMethod, DexMethod> overrideToUtilityMethods = new IdentityHashMap<>();
+    for (ProgramMethod subMethod : subimplementations) {
+      DexEncodedMethod subEnumLocalUtilityMethod =
+          installLocalUtilityMethod(localUtilityClass, localUtilityMethods, subMethod);
+      overrideToUtilityMethods.put(
+          subMethod.getReference(), subEnumLocalUtilityMethod.getReference());
+    }
+    DexMethod dispatch =
+        installDispatchMethod(
+                localUtilityClass,
+                localUtilityMethods,
+                subimplementations.iterator().next(),
+                superUtilityMethod,
+                overrideToUtilityMethods)
+            .getReference();
+    if (superMethod.isProgramMethod()) {
+      recordEmulatedDispatch(superMethod.getReference(), superUtilityMethod, dispatch);
+    } else {
+      lensBuilder.mapToDispatch(
+          superMethod
+              .getReference()
+              .withHolder(localUtilityClass.getSynthesizingContext().getType(), factory),
+          dispatch);
+    }
+    for (DexMethod override : overrideToUtilityMethods.keySet()) {
+      recordEmulatedDispatch(override, overrideToUtilityMethods.get(override), dispatch);
+    }
+  }
+
+  private void directMoveAndMap(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      ProgramMethod method) {
+    DexEncodedMethod utilityMethod =
+        installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
+    lensBuilder.moveAndMap(
+        method.getReference(), utilityMethod.getReference(), method.getDefinition().isStatic());
+  }
+
+  public void recordEmulatedDispatch(DexMethod from, DexMethod move, DexMethod dispatch) {
+    // Move is used for getRenamedSignature and to remap invoke-super.
+    // Map is used to remap all the other invokes.
+    lensBuilder.moveVirtual(from, move);
+    lensBuilder.mapToDispatch(from, dispatch);
+  }
+
+  private DexEncodedMethod installDispatchMethod(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      ProgramMethod representative,
+      DexMethod superUtilityMethod,
+      Map<DexMethod, DexMethod> map) {
+    assert !map.isEmpty();
+    DexMethod newLocalUtilityMethodReference =
+        factory.createFreshMethodNameWithoutHolder(
+            "_dispatch_" + representative.getName().toString(),
+            fixupProto(factory.prependHolderToProto(representative.getReference())),
+            localUtilityClass.getType(),
+            newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
+    Int2ObjectMap<DexMethod> methodMap = new Int2ObjectArrayMap<>();
+    IdentityHashMap<DexType, DexMethod> typeToMethod = new IdentityHashMap<>();
+    map.forEach(
+        (methodReference, newMethodReference) ->
+            typeToMethod.put(methodReference.getHolderType(), newMethodReference));
+    DexProgramClass unboxedEnum = localUtilityClass.getSynthesizingContext();
+    assert enumDataMap.get(unboxedEnum).valuesTypes != null;
+    enumDataMap
+        .get(unboxedEnum)
+        .valuesTypes
+        .forEach(
+            (i, type) -> {
+              if (typeToMethod.containsKey(type)) {
+                methodMap.put(ordinalToUnboxedInt(i), typeToMethod.get(type));
+              }
+            });
+    CfCodeWithLens codeWithLens =
+        new EnumUnboxingMethodDispatchCfCodeProvider(
+                appView, localUtilityClass.getType(), superUtilityMethod, methodMap)
+            .generateCfCode();
+    DexEncodedMethod newLocalUtilityMethod =
+        DexEncodedMethod.builder()
+            .setMethod(newLocalUtilityMethodReference)
+            .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+            .setCode(codeWithLens)
+            .setClassFileVersion(unboxedEnum.getInitialClassFileVersion())
+            .setApiLevelForDefinition(representative.getDefinition().getApiLevelForDefinition())
+            .setApiLevelForCode(representative.getDefinition().getApiLevelForCode())
+            .build();
+    dispatchMethods.put(
+        newLocalUtilityMethod.asProgramMethod(localUtilityClass.getDefinition()), codeWithLens);
+    assert !localUtilityMethods.containsKey(newLocalUtilityMethodReference);
+    localUtilityMethods.put(newLocalUtilityMethodReference, newLocalUtilityMethod);
+    return newLocalUtilityMethod;
+  }
+
+  private DexEncodedMethod installLocalUtilityMethod(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      ProgramMethod method) {
+    DexEncodedMethod newLocalUtilityMethod =
+        createLocalUtilityMethod(
+            method,
+            localUtilityClass,
+            newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
+    assert !localUtilityMethods.containsKey(newLocalUtilityMethod.getReference());
+    localUtilityMethods.put(newLocalUtilityMethod.getReference(), newLocalUtilityMethod);
+    return newLocalUtilityMethod;
+  }
+
   private DexEncodedMethod createLocalUtilityMethod(
       ProgramMethod method,
       LocalEnumUnboxingUtilityClass localUtilityClass,
@@ -534,9 +778,6 @@
                 localUtilityClass.getType(),
                 availableMethodSignatures);
 
-    // Record the move.
-    lensBuilder.move(methodReference, newMethod, method.getDefinition().isStatic(), true);
-
     return method
         .getDefinition()
         .toTypeSubstitutedMethod(
@@ -581,7 +822,7 @@
         ExtraUnusedNullParameter.computeExtraUnusedNullParameters(method.getReference(), newMethod);
     boolean isStatic = method.isStatic();
     RewrittenPrototypeDescription prototypeChanges =
-        lensBuilder.move(
+        lensBuilder.moveAndMap(
             method.getReference(), newMethod, isStatic, isStatic, extraUnusedNullParameters);
     return method.toTypeSubstitutedMethod(
         newMethod,
@@ -662,14 +903,17 @@
   public static class Result {
 
     private final BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping;
+    private final ProgramMethodSet dispatchMethods;
     private final EnumUnboxingLens lens;
     private final PrunedItems prunedItems;
 
     Result(
         BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping,
+        ProgramMethodSet dispatchMethods,
         EnumUnboxingLens lens,
         PrunedItems prunedItems) {
       this.checkNotNullToCheckNotZeroMapping = checkNotNullToCheckNotZeroMapping;
+      this.dispatchMethods = dispatchMethods;
       this.lens = lens;
       this.prunedItems = prunedItems;
     }
@@ -678,6 +922,10 @@
       return checkNotNullToCheckNotZeroMapping;
     }
 
+    public ProgramMethodSet getDispatchMethods() {
+      return dispatchMethods;
+    }
+
     EnumUnboxingLens getLens() {
       return lens;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
index 4e7e556..a7fdb84 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
@@ -67,24 +68,35 @@
     return method;
   }
 
+  private DexString computeGetInstanceFieldMethodName(DexField field, DexItemFactory factory) {
+    String fieldName = field.getName().toString();
+    if (field.getHolderType() == getSynthesizingContext().getType()) {
+      return factory.createString(
+          "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
+    }
+    assert field == factory.enumMembers.nameField || field == factory.enumMembers.ordinalField;
+    return field.getName();
+  }
+
+  private DexProto computeGetInstanceFieldMethodProto(DexField field, DexItemFactory factory) {
+    return factory.createProto(field.getType(), factory.intType);
+  }
+
+  public DexMethod computeToStringUtilityMethod(DexItemFactory factory) {
+    DexField nameField = factory.enumMembers.nameField;
+    DexString name = computeGetInstanceFieldMethodName(nameField, factory);
+    DexProto proto = computeGetInstanceFieldMethodProto(nameField, factory);
+    return factory.createMethod(getDefinition().getType(), proto, name);
+  }
+
   private ProgramMethod ensureGetInstanceFieldMethod(
       AppView<AppInfoWithLiveness> appView, DexField field) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
-    String fieldName = field.getName().toString();
-    DexString methodName;
-    if (field.getHolderType() == getSynthesizingContext().getType()) {
-      methodName =
-          dexItemFactory.createString(
-              "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
-    } else {
-      assert field == appView.dexItemFactory().enumMembers.nameField
-          || field == appView.dexItemFactory().enumMembers.ordinalField;
-      methodName = field.getName();
-    }
+    DexString methodName = computeGetInstanceFieldMethodName(field, dexItemFactory);
     return internalEnsureMethod(
         appView,
         methodName,
-        dexItemFactory.createProto(field.getType(), dexItemFactory.intType),
+        computeGetInstanceFieldMethodProto(field, dexItemFactory),
         method ->
             new EnumUnboxingCfCodeProvider.EnumUnboxingInstanceFieldCfCodeProvider(
                     appView, getType(), data, field)
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
index bfd215d..56b05f6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
@@ -31,6 +31,8 @@
   public static final Reason NO_INIT = new StringReason("NO_INIT");
   public static final Reason INVALID_INIT = new StringReason("INVALID_INIT");
   public static final Reason INVALID_CLINIT = new StringReason("INVALID_CLINIT");
+  public static final Reason INVALID_SUBTYPE_INIT = new StringReason("INVALID_SUBTYPE_INIT");
+  public static final Reason SUBTYPE_CLINIT = new StringReason("SUBTYPE_CLINIT");
   public static final Reason INVALID_INVOKE = new StringReason("INVALID_INVOKE");
   public static final Reason INVALID_INVOKE_CLASSPATH =
       new StringReason("INVALID_INVOKE_CLASSPATH");
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 8b7bd6b..b1e1a69 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.DefaultInstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.google.common.collect.ImmutableSet;
@@ -91,6 +92,11 @@
   }
 
   @Override
+  public int getMaxRemovedAndroidLogLevel() {
+    return MaximumRemovedAndroidLogLevelRule.NOT_SET;
+  }
+
+  @Override
   public BitSet getNonNullParamOrThrow() {
     return NO_NULL_PARAMETER_OR_THROW_FACTS;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index c4e8e3a..0d42b62 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -40,6 +40,8 @@
 
   public abstract DynamicType getDynamicType();
 
+  public abstract int getMaxRemovedAndroidLogLevel();
+
   public final boolean hasNonNullParamOrThrow() {
     return getNonNullParamOrThrow() != null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index d5127e3..7dcb035 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule;
 import com.android.tools.r8.utils.BitSetUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -77,6 +78,7 @@
   private SimpleInliningConstraint simpleInliningConstraint =
       NeverSimpleInliningConstraint.getInstance();
 
+  private int maxRemovedAndroidLogLevel = MaximumRemovedAndroidLogLevelRule.NOT_SET;
   private BitSet unusedArguments = null;
 
   // To reduce the memory footprint of UpdatableMethodOptimizationInfo, all the boolean fields are
@@ -144,6 +146,7 @@
     nonNullParamOnNormalExits = template.nonNullParamOnNormalExits;
     classInlinerConstraint = template.classInlinerConstraint;
     enumUnboxerMethodClassification = template.enumUnboxerMethodClassification;
+    maxRemovedAndroidLogLevel = template.maxRemovedAndroidLogLevel;
   }
 
   public MutableMethodOptimizationInfo fixup(
@@ -316,6 +319,17 @@
   }
 
   @Override
+  public int getMaxRemovedAndroidLogLevel() {
+    return maxRemovedAndroidLogLevel;
+  }
+
+  public void joinMaxRemovedAndroidLogLevel(int maxRemovedAndroidLogLevel) {
+    this.maxRemovedAndroidLogLevel =
+        MaximumRemovedAndroidLogLevelRule.joinMaxRemovedAndroidLogLevel(
+            this.maxRemovedAndroidLogLevel, maxRemovedAndroidLogLevel);
+  }
+
+  @Override
   public BitSet getNonNullParamOrThrow() {
     return nonNullParamOrThrow;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 3fdb8b4..a0c9863 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -75,6 +75,13 @@
 
   // METHOD OPTIMIZATION INFO.
 
+  public void joinMaxRemovedAndroidLogLevel(ProgramMethod method, int maxRemovedAndroidLogLevel) {
+    method
+        .getDefinition()
+        .getMutableOptimizationInfo()
+        .joinMaxRemovedAndroidLogLevel(maxRemovedAndroidLogLevel);
+  }
+
   @Override
   public void markForceInline(DexEncodedMethod method) {
     // Ignored.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningDiagnostic.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningDiagnostic.java
new file mode 100644
index 0000000..9d0e1d1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningDiagnostic.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2023, 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.optimize.inliner;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@Keep
+public class WhyAreYouNotInliningDiagnostic implements Diagnostic {
+
+  private final Origin origin;
+  private final String message;
+
+  public WhyAreYouNotInliningDiagnostic(Origin origin, String message) {
+    this.origin = origin;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    // We could make this even more precise if we added the current position of the invoke.
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
index 7c3e5da..4bf931b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -21,8 +21,7 @@
   public static WhyAreYouNotInliningReporter createFor(
       ProgramMethod callee, AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
     if (appView.appInfo().isWhyAreYouNotInliningMethod(callee.getReference())) {
-      return new WhyAreYouNotInliningReporterImpl(
-          callee, context, appView.options().testing.whyAreYouNotInliningConsumer);
+      return new WhyAreYouNotInliningReporterImpl(appView, callee, context);
     }
     return NopWhyAreYouNotInliningReporter.getInstance();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
index f5807dd..05e2cb1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
@@ -5,69 +5,72 @@
 package com.android.tools.r8.ir.optimize.inliner;
 
 import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
-import java.io.PrintStream;
 import java.util.Set;
 
 class WhyAreYouNotInliningReporterImpl extends WhyAreYouNotInliningReporter {
 
   private final ProgramMethod callee;
   private final ProgramMethod context;
-  private final PrintStream output;
+  private final Reporter reporter;
 
   private boolean reasonHasBeenReported = false;
 
   WhyAreYouNotInliningReporterImpl(
-      ProgramMethod callee, ProgramMethod context, PrintStream output) {
+      AppView<?> appView, ProgramMethod callee, ProgramMethod context) {
     this.callee = callee;
     this.context = context;
-    this.output = output;
+    reporter = appView.reporter();
   }
 
-  private void print(String reason) {
-    output.print("Method `");
-    output.print(callee.toSourceString());
-    output.print("` was not inlined into `");
-    output.print(context.toSourceString());
+  private void report(String reason) {
+    StringBuilder message = new StringBuilder();
+    message.append("Method `");
+    message.append(callee.toSourceString());
+    message.append("` was not inlined into `");
+    message.append(context.toSourceString());
     if (reason != null) {
-      output.print("`: ");
-      output.println(reason);
+      message.append("`: ");
+      message.append(reason);
     } else {
-      output.println("`.");
+      message.append("`.");
     }
+    reporter.info(new WhyAreYouNotInliningDiagnostic(context.getOrigin(), message.toString()));
     reasonHasBeenReported = true;
   }
 
   private void printWithExceededThreshold(
       String reason, String description, int value, int threshold) {
-    print(reason + " (" + description + ": " + value + ", threshold: " + threshold + ").");
+    report(reason + " (" + description + ": " + value + ", threshold: " + threshold + ").");
   }
 
   @Override
   public void reportCallerNotSameClass() {
-    print("inlinee can only be inlined into methods in the same class.");
+    report("inlinee can only be inlined into methods in the same class.");
   }
 
   @Override
   public void reportCallerNotSameNest() {
-    print("inlinee can only be inlined into methods in the same class (and its nest members).");
+    report("inlinee can only be inlined into methods in the same class (and its nest members).");
   }
 
   @Override
   public void reportCallerNotSamePackage() {
-    print(
+    report(
         "inlinee can only be inlined into methods in the same package "
             + "(declared package private or accesses package private type or member).");
   }
 
   @Override
   public void reportCallerNotSubtype() {
-    print(
+    report(
         "inlinee can only be inlined into methods in the same package and methods in subtypes of "
             + "the inlinee's enclosing class"
             + "(declared protected or accesses protected type or member).");
@@ -75,22 +78,22 @@
 
   @Override
   public void reportCallerHasUnknownApiLevel() {
-    print("computed API level for caller is unknown");
+    report("computed API level for caller is unknown");
   }
 
   @Override
   public void reportClasspathMethod() {
-    print("inlinee is on the classpath.");
+    report("inlinee is on the classpath.");
   }
 
   @Override
   public void reportInaccessible() {
-    print("inlinee is not accessible from the caller context.");
+    report("inlinee is not accessible from the caller context.");
   }
 
   @Override
   public void reportIncorrectArity(int numberOfArguments, int arity) {
-    print(
+    report(
         "number of arguments ("
             + numberOfArguments
             + ") does not match arity of method ("
@@ -100,22 +103,22 @@
 
   @Override
   public void reportInlineeDoesNotHaveCode() {
-    print("inlinee does not have code.");
+    report("inlinee does not have code.");
   }
 
   @Override
   public void reportInlineeNotInliningCandidate() {
-    print("unsupported instruction in inlinee.");
+    report("unsupported instruction in inlinee.");
   }
 
   @Override
   public void reportInlineeNotProcessed() {
-    print("inlinee not processed yet.");
+    report("inlinee not processed yet.");
   }
 
   @Override
   public void reportInlineeNotSimple() {
-    print(
+    report(
         "not inlining due to code size heuristic "
             + "(inlinee may have multiple callers and is not considered trivial).");
   }
@@ -125,10 +128,10 @@
       ComputedApiLevel callerApiLevel, ComputedApiLevel inlineeApiLevel) {
     assert callerApiLevel.isKnownApiLevel();
     if (inlineeApiLevel.isUnknownApiLevel()) {
-      print("computed API level for inlinee is unknown");
+      report("computed API level for inlinee is unknown");
     } else {
       assert inlineeApiLevel.isKnownApiLevel();
-      print(
+      report(
           "computed API level for inlinee ("
               + inlineeApiLevel.asKnownApiLevel().getApiLevel()
               + ") is higher than caller's ("
@@ -139,29 +142,29 @@
 
   @Override
   public void reportInlineeRefersToClassesNotInMainDex() {
-    print(
+    report(
         "inlining could increase the main dex size "
             + "(caller is in main dex and inlinee refers to classes not in main dex).");
   }
 
   @Override
   public void reportInliningAcrossFeatureSplit() {
-    print("cannot inline across feature splits.");
+    report("cannot inline across feature splits.");
   }
 
   @Override
   public void reportInstructionBudgetIsExceeded() {
-    print("caller's instruction budget is exceeded.");
+    report("caller's instruction budget is exceeded.");
   }
 
   @Override
   public void reportInvalidDoubleInliningCandidate() {
-    print("inlinee is invoked more than once and could not be inlined into all call sites.");
+    report("inlinee is invoked more than once and could not be inlined into all call sites.");
   }
 
   @Override
   public void reportInvalidInliningReason(Reason reason, Set<Reason> validInliningReasons) {
-    print(
+    report(
         "not a valid inlining reason (was: "
             + reason
             + ", allowed: one of "
@@ -171,29 +174,29 @@
 
   @Override
   public void reportLibraryMethod() {
-    print("inlinee is a library method.");
+    report("inlinee is a library method.");
   }
 
   @Override
   public void reportMarkedAsNeverInline() {
-    print("method is marked by a -neverinline rule.");
+    report("method is marked by a -neverinline rule.");
   }
 
   @Override
   public void reportMustTriggerClassInitialization() {
-    print(
+    report(
         "cannot guarantee that the enclosing class of the inlinee is guaranteed to be class "
             + "initialized before the first side-effecting instruction in the inlinee.");
   }
 
   @Override
   public void reportNoInliningIntoConstructorsWhenGeneratingClassFiles() {
-    print("inlining into constructors not supported when generating class files.");
+    report("inlining into constructors not supported when generating class files.");
   }
 
   @Override
   public void reportPinned() {
-    print("method is kept by a Proguard configuration rule.");
+    report("method is kept by a Proguard configuration rule.");
   }
 
   @Override
@@ -208,33 +211,33 @@
 
   @Override
   public void reportProcessedConcurrently() {
-    print(
+    report(
         "could lead to nondeterministic output since the inlinee is being optimized concurrently.");
   }
 
   @Override
   public void reportReceiverDefinitelyNull() {
-    print("the receiver is always null at the call site.");
+    report("the receiver is always null at the call site.");
   }
 
   @Override
   public void reportReceiverMaybeNull() {
-    print("the receiver may be null at the call site.");
+    report("the receiver may be null at the call site.");
   }
 
   @Override
   public void reportRecursiveMethod() {
-    print("recursive calls are not inlined.");
+    report("recursive calls are not inlined.");
   }
 
   @Override
   public void reportUnknownTarget() {
-    print("could not find a single target.");
+    report("could not find a single target.");
   }
 
   @Override
   public void reportUnsafeConstructorInliningDueToFinalFieldAssignment(InstancePut instancePut) {
-    print(
+    report(
         "final field `"
             + instancePut.getField()
             + "` must be initialized in a constructor of `"
@@ -244,7 +247,7 @@
 
   @Override
   public void reportUnsafeConstructorInliningDueToIndirectConstructorCall(InvokeDirect invoke) {
-    print(
+    report(
         "must invoke a constructor from the class being instantiated (would invoke `"
             + invoke.getInvokedMethod().toSourceString()
             + "`).");
@@ -252,7 +255,7 @@
 
   @Override
   public void reportUnsafeConstructorInliningDueToUninitializedObjectUse(Instruction user) {
-    print("would lead to use of uninitialized object (user: `" + user.toString() + "`).");
+    report("would lead to use of uninitialized object (user: `" + user.toString() + "`).");
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
index 2cf8495..821561c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
@@ -4,11 +4,19 @@
 
 package com.android.tools.r8.ir.optimize.library;
 
+import static com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule.ASSERT;
+import static com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule.DEBUG;
+import static com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule.ERROR;
+import static com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule.INFO;
+import static com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule.VERBOSE;
+import static com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule.WARN;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 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.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.IRCode;
@@ -16,18 +24,12 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.MaximumRemovedAndroidLogLevelRule;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import java.util.Set;
 
 public class LogMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
-  private static final int VERBOSE = 2;
-  private static final int DEBUG = 3;
-  private static final int INFO = 4;
-  private static final int WARN = 5;
-  private static final int ERROR = 6;
-  private static final int ASSERT = 7;
-
   private final AppView<?> appView;
 
   private final DexType logType;
@@ -92,8 +94,11 @@
 
   public static boolean isEnabled(AppView<?> appView) {
     ProguardConfiguration proguardConfiguration = appView.options().getProguardConfiguration();
-    return proguardConfiguration != null
-        && proguardConfiguration.getMaxRemovedAndroidLogLevel() >= VERBOSE;
+    if (proguardConfiguration == null) {
+      return false;
+    }
+    return proguardConfiguration.getMaxRemovedAndroidLogLevel() >= VERBOSE
+        || proguardConfiguration.hasMaximumRemovedAndroidLogLevelRules();
   }
 
   @Override
@@ -113,13 +118,21 @@
     // Replace Android logging statements like Log.w(...) and Log.IsLoggable(..., WARNING) at or
     // below a certain logging level by false.
     int logLevel = getLogLevel(invoke, singleTarget);
-    int maxRemovedAndroidLogLevel =
-        appView.options().getProguardConfiguration().getMaxRemovedAndroidLogLevel();
+    int maxRemovedAndroidLogLevel = getMaxRemovedAndroidLogLevel(code.context());
     if (VERBOSE <= logLevel && logLevel <= maxRemovedAndroidLogLevel) {
       instructionIterator.replaceCurrentInstructionWithConstFalse(code);
     }
   }
 
+  private int getMaxRemovedAndroidLogLevel(ProgramMethod context) {
+    int globalMaxRemovedAndroidLogLevel =
+        appView.options().getProguardConfiguration().getMaxRemovedAndroidLogLevel();
+    int methodMaxRemovedAndroidLogLevel =
+        context.getOptimizationInfo().getMaxRemovedAndroidLogLevel();
+    return MaximumRemovedAndroidLogLevelRule.joinMaxRemovedAndroidLogLevel(
+        globalMaxRemovedAndroidLogLevel, methodMaxRemovedAndroidLogLevel);
+  }
+
   /**
    * @return The log level of the given invoke if it is a call to an android.util.Log method and the
    *     log level can be determined, otherwise returns -1.
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
index 941248a..af9e41c 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.cf.code.CfLoad;
 import com.android.tools.r8.cf.code.CfNew;
 import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfReturnVoid;
 import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
 import com.android.tools.r8.cf.code.CfStaticFieldRead;
@@ -29,11 +30,13 @@
 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.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import java.util.ArrayList;
 import java.util.List;
 import org.objectweb.asm.Opcodes;
@@ -65,6 +68,85 @@
     }
   }
 
+  public static class EnumUnboxingMethodDispatchCfCodeProvider extends EnumUnboxingCfCodeProvider {
+
+    private final GraphLens codeLens;
+    private final DexMethod superEnumMethod;
+    private final Int2ObjectMap<DexMethod> methodMap;
+
+    public EnumUnboxingMethodDispatchCfCodeProvider(
+        AppView<?> appView,
+        DexType holder,
+        DexMethod superEnumMethod,
+        Int2ObjectMap<DexMethod> methodMap) {
+      super(appView, holder);
+      this.codeLens = appView.codeLens();
+      this.superEnumMethod = superEnumMethod;
+      this.methodMap = methodMap;
+    }
+
+    @Override
+    public CfCodeWithLens generateCfCode() {
+      // TODO(b/167942775): Should use a table-switch for large enums (maybe same threshold in the
+      //  rewriter of switchmaps).
+
+      DexItemFactory factory = appView.dexItemFactory();
+      int returnInvokeSize = superEnumMethod.getParameters().size() + 2;
+      List<CfInstruction> instructions =
+          new ArrayList<>(methodMap.size() * (returnInvokeSize + 5) + returnInvokeSize);
+
+      CfFrame.Builder frameBuilder = CfFrame.builder();
+      for (DexType parameter : superEnumMethod.getParameters()) {
+        frameBuilder.appendLocal(FrameType.initialized(parameter));
+      }
+      methodMap.forEach(
+          (unboxedEnumValue, method) -> {
+            CfLabel dest = new CfLabel();
+            instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
+            instructions.add(new CfConstNumber(unboxedEnumValue, ValueType.INT));
+            instructions.add(new CfIfCmp(IfType.NE, ValueType.INT, dest));
+            addReturnInvoke(instructions, method);
+            instructions.add(dest);
+            instructions.add(frameBuilder.build());
+          });
+
+      addReturnInvoke(instructions, superEnumMethod);
+      return new CfCodeWithLens(getHolder(), defaultMaxStack(), defaultMaxLocals(), instructions);
+    }
+
+    private void addReturnInvoke(List<CfInstruction> instructions, DexMethod method) {
+      int localIndex = 0;
+      for (DexType parameterType : method.getParameters()) {
+        instructions.add(new CfLoad(ValueType.fromDexType(parameterType), localIndex));
+        localIndex += parameterType.getRequiredRegisters();
+      }
+      instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, method, false));
+      instructions.add(
+          method.getReturnType().isVoidType()
+              ? new CfReturnVoid()
+              : new CfReturn(ValueType.fromDexType(method.getReturnType())));
+    }
+
+    public static class CfCodeWithLens extends CfCode {
+      private GraphLens codeLens;
+
+      public void setCodeLens(GraphLens codeLens) {
+        this.codeLens = codeLens;
+      }
+
+      public CfCodeWithLens(
+          DexType originalHolder, int maxStack, int maxLocals, List<CfInstruction> instructions) {
+        super(originalHolder, maxStack, maxLocals, instructions);
+      }
+
+      @Override
+      public GraphLens getCodeLens(AppView<?> appView) {
+        assert codeLens != null;
+        return codeLens;
+      }
+    }
+  }
+
   public static class EnumUnboxingInstanceFieldCfCodeProvider extends EnumUnboxingCfCodeProvider {
 
     private final DexType returnType;
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index 0ee8077..7b17f6d 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -29,7 +29,6 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -40,6 +39,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
 
 public class ClassNameMapper implements ProguardMap {
 
@@ -51,6 +51,7 @@
   public static class Builder extends ProguardMap.Builder {
 
     private boolean buildPreamble = false;
+    private boolean addVersionAsPreamble = false;
     private final List<String> preamble = new ArrayList<>();
     private final Map<String, ClassNamingForNameMapper.Builder> mapping = new HashMap<>();
     private final LinkedHashSet<MapVersionMappingInformation> mapVersions = new LinkedHashSet<>();
@@ -65,13 +66,18 @@
       return classNamingBuilder;
     }
 
-    Builder setBuildPreamble(boolean buildPreamble) {
+    public Builder setBuildPreamble(boolean buildPreamble) {
       this.buildPreamble = buildPreamble;
       return this;
     }
 
+    public Builder setAddVersionAsPreamble(boolean addVersionAsPreamble) {
+      this.addVersionAsPreamble = addVersionAsPreamble;
+      return this;
+    }
+
     @Override
-    void addPreambleLine(String line) {
+    public void addPreambleLine(String line) {
       if (buildPreamble) {
         preamble.add(line);
       }
@@ -93,6 +99,15 @@
     @Override
     public ProguardMap.Builder setCurrentMapVersion(MapVersionMappingInformation mapVersion) {
       mapVersions.add(mapVersion);
+      if (addVersionAsPreamble) {
+        addPreambleLine("# " + mapVersion.serialize());
+      }
+      return this;
+    }
+
+    @Override
+    ProguardMap.Builder addFileName(String originalName, String fileName) {
+      originalSourceFiles.put(originalName, fileName);
       return this;
     }
   }
@@ -148,14 +163,15 @@
       DiagnosticsHandler diagnosticsHandler,
       boolean allowEmptyMappedRanges,
       boolean allowExperimentalMapping,
-      boolean readPreamble)
+      boolean buildPreamble)
       throws IOException {
-    return mapperFromLineReader(
+    return mapperFromLineReaderWithFiltering(
         LineReader.fromBufferedReader(CharSource.wrap(contents).openBufferedStream()),
+        MapVersion.MAP_VERSION_NONE,
         diagnosticsHandler,
         allowEmptyMappedRanges,
         allowExperimentalMapping,
-        readPreamble);
+        builder -> builder.setBuildPreamble(buildPreamble));
   }
 
   private static ClassNameMapper mapperFromBufferedReader(
@@ -170,31 +186,13 @@
       boolean allowExperimentalMapping,
       boolean buildPreamble)
       throws IOException {
-    return mapperFromLineReader(
+    return mapperFromLineReaderWithFiltering(
         LineReader.fromBufferedReader(reader),
+        MapVersion.MAP_VERSION_NONE,
         diagnosticsHandler,
         allowEmptyMappedRanges,
         allowExperimentalMapping,
-        buildPreamble);
-  }
-
-  public static ClassNameMapper mapperFromLineReader(
-      LineReader reader,
-      DiagnosticsHandler diagnosticsHandler,
-      boolean allowEmptyMappedRanges,
-      boolean allowExperimentalMapping,
-      boolean buildPreamble)
-      throws IOException {
-    try (ProguardMapReader proguardReader =
-        new ProguardMapReader(
-            reader,
-            diagnosticsHandler != null ? diagnosticsHandler : new Reporter(),
-            allowEmptyMappedRanges,
-            allowExperimentalMapping)) {
-      ClassNameMapper.Builder builder = ClassNameMapper.builder().setBuildPreamble(buildPreamble);
-      proguardReader.parse(builder);
-      return builder.build();
-    }
+        builder -> builder.setBuildPreamble(buildPreamble));
   }
 
   public static ClassNameMapper mapperFromLineReaderWithFiltering(
@@ -202,7 +200,8 @@
       MapVersion mapVersion,
       DiagnosticsHandler diagnosticsHandler,
       boolean allowEmptyMappedRanges,
-      boolean allowExperimentalMapping)
+      boolean allowExperimentalMapping,
+      Consumer<ClassNameMapper.Builder> builderConsumer)
       throws IOException {
     try (ProguardMapReader proguardReader =
         new ProguardMapReader(
@@ -212,6 +211,7 @@
             allowExperimentalMapping,
             mapVersion)) {
       ClassNameMapper.Builder builder = ClassNameMapper.builder();
+      builderConsumer.accept(builder);
       proguardReader.parse(builder);
       return builder.build();
     }
@@ -239,7 +239,7 @@
     return classNameMappings;
   }
 
-  public Collection<String> getPreamble() {
+  public List<String> getPreamble() {
     return preamble;
   }
 
@@ -350,7 +350,7 @@
   }
 
   public boolean isEmpty() {
-    return classNameMappings.isEmpty();
+    return classNameMappings.isEmpty() && preamble.isEmpty();
   }
 
   public ClassNameMapper sorted() {
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
index dce1aee..05fe545 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
@@ -380,7 +380,7 @@
     StringDiagnostic diagnostic =
         instruction.getPosition().getLine() >= 1
             ? new StringDiagnostic(
-                message, origin, new TextPosition(0L, instruction.getPosition().getLine(), 1))
+                message, origin, new TextPosition(0, instruction.getPosition().getLine(), 1))
             : new StringDiagnostic(message, origin);
     appView.options().reporter.warning(diagnostic);
   }
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMap.java b/src/main/java/com/android/tools/r8/naming/ProguardMap.java
index 38fe729..f51194a 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMap.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMap.java
@@ -17,6 +17,8 @@
 
     abstract Builder setCurrentMapVersion(MapVersionMappingInformation mapVersion);
 
+    abstract Builder addFileName(String originalName, String fileName);
+
     abstract ProguardMap build();
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 7af0129..05681d8 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -264,9 +264,14 @@
         if (!parseMappingInformation(
             info -> {
               assert info.isMapVersionMappingInformation()
-                  || info.isUnknownJsonMappingInformation();
+                  || info.isUnknownJsonMappingInformation()
+                  || info.isPartitionFileNameInformation();
               if (info.isMapVersionMappingInformation()) {
                 mapBuilder.setCurrentMapVersion(info.asMapVersionMappingInformation());
+              } else if (info.isPartitionFileNameInformation()) {
+                info.asPartitionFileNameInformation()
+                    .getTypeNameToFileNameMapping()
+                    .forEach(mapBuilder::addFileName);
               } else if (!seenClassMapping) {
                 mapBuilder.addPreambleLine(line);
               }
diff --git a/src/main/java/com/android/tools/r8/naming/SeedMapper.java b/src/main/java/com/android/tools/r8/naming/SeedMapper.java
index 3acd86b..dd2d150 100644
--- a/src/main/java/com/android/tools/r8/naming/SeedMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/SeedMapper.java
@@ -74,6 +74,12 @@
     }
 
     @Override
+    ProguardMap.Builder addFileName(String originalName, String fileName) {
+      // Do nothing
+      return this;
+    }
+
+    @Override
     SeedMapper build() {
       reporter.failIfPendingErrors();
       return new SeedMapper(ImmutableMap.copyOf(map), mappedToDescriptorNames, reporter);
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
index 029547e..11014f6 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
@@ -17,7 +17,8 @@
       DexDefinitionSupplier definitions,
       GraphLens graphLens,
       NamingLens namingLens) {
-    DexReference rewritten = graphLens.lookupReference(reference);
+    GraphLens nameLens = GraphLens.getIdentityLens();
+    DexReference rewritten = graphLens.getRenamedReference(reference, nameLens);
     if (needsToComputeName()) {
       if (isFieldNameComputationInfo()) {
         return asFieldNameComputationInfo()
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
index 6680142..ffc0a72 100644
--- a/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
@@ -62,6 +62,10 @@
     return false;
   }
 
+  public boolean isPartitionFileNameInformation() {
+    return false;
+  }
+
   public MapVersionMappingInformation asMapVersionMappingInformation() {
     return null;
   }
@@ -102,6 +106,10 @@
     return null;
   }
 
+  public PartitionFileNameInformation asPartitionFileNameInformation() {
+    return null;
+  }
+
   public boolean shouldCompose(MappingInformation existing) {
     return !allowOther(existing);
   }
@@ -176,6 +184,9 @@
       case ResidualSignatureMappingInformation.ID:
         ResidualSignatureMappingInformation.deserialize(version, object, onMappingInfo);
         return;
+      case PartitionFileNameInformation.ID:
+        PartitionFileNameInformation.deserialize(object, onMappingInfo);
+        return;
       default:
         diagnosticsHandler.info(MappingInformationDiagnostics.noHandlerFor(lineNumber, id));
         UnknownJsonMappingInformation.deserialize(id, object, onMappingInfo);
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/PartitionFileNameInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/PartitionFileNameInformation.java
new file mode 100644
index 0000000..9f74d5d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/PartitionFileNameInformation.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming.mappinginformation;
+
+import com.android.tools.r8.naming.MappingComposeException;
+import com.android.tools.r8.naming.mappinginformation.MappingInformation.ReferentialMappingInformation;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class PartitionFileNameInformation extends ReferentialMappingInformation {
+
+  private final Map<String, String> typeNameToFileNameMapping;
+
+  public static final String ID = "partitionSourceFiles";
+  static final String FILE_NAME_MAPPINGS_KEY = "fileNameMappings";
+
+  private PartitionFileNameInformation(Map<String, String> typeNameToFileNameMapping) {
+    this.typeNameToFileNameMapping = typeNameToFileNameMapping;
+  }
+
+  @Override
+  public String getId() {
+    return ID;
+  }
+
+  public Map<String, String> getTypeNameToFileNameMapping() {
+    return typeNameToFileNameMapping;
+  }
+
+  @Override
+  public boolean isPartitionFileNameInformation() {
+    return true;
+  }
+
+  @Override
+  public PartitionFileNameInformation asPartitionFileNameInformation() {
+    return this;
+  }
+
+  @Override
+  public MappingInformation compose(MappingInformation existing) throws MappingComposeException {
+    throw new MappingComposeException("Unable to compose " + ID);
+  }
+
+  @Override
+  public boolean allowOther(MappingInformation information) {
+    return !information.isPartitionFileNameInformation();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public String serialize() {
+    JsonObject object = new JsonObject();
+    object.add(MAPPING_ID_KEY, new JsonPrimitive(ID));
+    JsonObject map = new JsonObject();
+    typeNameToFileNameMapping.forEach(map::addProperty);
+    object.add(FILE_NAME_MAPPINGS_KEY, map);
+    return object.toString();
+  }
+
+  public static void deserialize(JsonObject object, Consumer<MappingInformation> onMappingInfo) {
+    JsonObject mappingsObject = object.getAsJsonObject(FILE_NAME_MAPPINGS_KEY);
+    Builder builder = builder();
+    mappingsObject
+        .entrySet()
+        .forEach(
+            entry ->
+                builder.addClassToFileNameMapping(entry.getKey(), entry.getValue().getAsString()));
+    onMappingInfo.accept(builder.build());
+  }
+
+  public static class Builder {
+
+    private final Map<String, String> typeNameToFileNameMapping = new HashMap<>();
+
+    public Builder addClassToFileNameMapping(String typeName, String fileName) {
+      typeNameToFileNameMapping.put(typeName, fileName);
+      return this;
+    }
+
+    public boolean isEmpty() {
+      return typeNameToFileNameMapping.isEmpty();
+    }
+
+    public PartitionFileNameInformation build() {
+      return new PartitionFileNameInformation(typeNameToFileNameMapping);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index d860861..ca5ae4a 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -56,7 +56,7 @@
   private final MemberRebindingLens.Builder lensBuilder;
 
   public MemberRebindingAnalysis(AppView<AppInfoWithLiveness> appView) {
-    assert appView.graphLens().isContextFreeForMethods();
+    assert appView.graphLens().isContextFreeForMethods(appView.codeLens());
     this.androidApiLevelCompute = appView.apiLevelCompute();
     this.appView = appView;
     this.eventConsumer = MemberRebindingEventConsumer.create(appView);
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 0c6a75a..f8ddbad 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -76,7 +76,7 @@
 
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     assert previous.getReboundReference() == null;
     return MethodLookupResult.builder(this)
         .setReference(previous.getReference())
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 21796c5..d94e65d 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -69,7 +69,7 @@
 
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     Map<DexMethod, DexMethod> methodMap =
         methodMaps.getOrDefault(previous.getType(), Collections.emptyMap());
     DexMethod newMethod = methodMap.get(previous.getReference());
diff --git a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
index 54d66e4..8dee6d5 100644
--- a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
@@ -40,7 +40,7 @@
 
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     if (previous.getType() == InvokeType.DIRECT
         && publicizedMethods.contains(previous.getReference())) {
       assert publicizedMethodIsPresentOnHolder(previous.getReference(), context);
diff --git a/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java b/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java
index 5aedebc..2a7d000 100644
--- a/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/proto/ProtoNormalizerGraphLens.java
@@ -58,7 +58,7 @@
 
   @Override
   protected MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     DexMethod methodSignature = previous.getReference();
     DexMethod newMethodSignature = getNextMethodSignature(methodSignature);
     if (methodSignature == newMethodSignature) {
diff --git a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
index 427b37e..3307010 100644
--- a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
@@ -44,7 +44,7 @@
 
   @Override
   protected MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
     if (methodMap.containsKey(previous.getReference())) {
       DexMethod newReference = previous.getReference();
       do {
diff --git a/src/main/java/com/android/tools/r8/position/TextPosition.java b/src/main/java/com/android/tools/r8/position/TextPosition.java
index 08d13bc..dcbc695 100644
--- a/src/main/java/com/android/tools/r8/position/TextPosition.java
+++ b/src/main/java/com/android/tools/r8/position/TextPosition.java
@@ -17,10 +17,9 @@
    */
   public static final int UNKNOWN_COLUMN = -1;
 
-  /**
-   * Char offset from the start of the text resource.
-   */
+  /** Char offset from the start of the text resource. */
   private final long offset;
+
   private final int line;
   private final int column;
 
@@ -52,6 +51,13 @@
     return offset;
   }
 
+  public int getOffsetAsInt() {
+    if (offset > Integer.MAX_VALUE) {
+      throw new RuntimeException("Expected offset to be an int, but was " + offset);
+    }
+    return (int) offset;
+  }
+
   @Override
   public String toString() {
     return "offset: " + offset + ", line: " + line + ", column: " + column;
diff --git a/src/main/java/com/android/tools/r8/profile/AbstractProfileMethodRule.java b/src/main/java/com/android/tools/r8/profile/AbstractProfileMethodRule.java
index ddb1225..f39ea03 100644
--- a/src/main/java/com/android/tools/r8/profile/AbstractProfileMethodRule.java
+++ b/src/main/java/com/android/tools/r8/profile/AbstractProfileMethodRule.java
@@ -20,6 +20,8 @@
 
     MethodRuleBuilder join(MethodRuleBuilder methodRuleBuilder);
 
+    MethodRuleBuilder setIsStartup();
+
     MethodRuleBuilder setMethod(DexMethod method);
 
     MethodRule build();
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java
index e6dc68a..bc1dfda 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCollection.java
@@ -53,7 +53,12 @@
       clazz.forEachMethod(
           method ->
               artProfileBuilder.addMethodRule(
-                  ArtProfileMethodRule.builder().setMethod(method.getReference()).build()));
+                  ArtProfileMethodRule.builder()
+                      .setMethod(method.getReference())
+                      .acceptMethodRuleInfoBuilder(
+                          methodRuleInfoBuilder ->
+                              methodRuleInfoBuilder.setIsHot().setIsStartup().setIsPostStartup())
+                      .build()));
     }
     return artProfileBuilder.build();
   }
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
index d7fde7d..06464c7 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
@@ -85,7 +85,9 @@
       ProgramDefinition definition,
       Set<CompletenessExceptions> completenessExceptions,
       List<DexReference> missing) {
-    if (completenessExceptions.contains(ALLOW_MISSING_ENUM_UNBOXING_UTILITY_METHODS)) {
+    // TODO(b/274030968): Fix profile for enum unboxing with subtypes.
+    if (appView.options().testing.enableEnumWithSubtypesUnboxing
+        || completenessExceptions.contains(ALLOW_MISSING_ENUM_UNBOXING_UTILITY_METHODS)) {
       DexType contextType = definition.getContextType();
       SyntheticItems syntheticItems = appView.getSyntheticItems();
       if (syntheticItems.isSynthetic(contextType)) {
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRule.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRule.java
index 4b417c9..5f4defc 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRule.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRule.java
@@ -21,6 +21,7 @@
   private final ArtProfileMethodRuleInfoImpl info;
 
   ArtProfileMethodRule(DexMethod method, ArtProfileMethodRuleInfoImpl info) {
+    assert info.getFlags() != 0;
     this.method = method;
     this.info = info;
   }
@@ -141,6 +142,12 @@
     }
 
     @Override
+    public Builder setIsStartup() {
+      methodRuleInfoBuilder.setIsStartup();
+      return this;
+    }
+
+    @Override
     public Builder setMethodReference(MethodReference methodReference) {
       assert dexItemFactory != null;
       return setMethod(MethodReferenceUtils.toDexMethod(methodReference, dexItemFactory));
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRuleInfoImpl.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRuleInfoImpl.java
index d91553e..b86c569 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRuleInfoImpl.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileMethodRuleInfoImpl.java
@@ -4,16 +4,18 @@
 
 package com.android.tools.r8.profile.art;
 
+import com.android.tools.r8.utils.ArrayUtils;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 
 public class ArtProfileMethodRuleInfoImpl implements ArtProfileMethodRuleInfo {
 
-  private static final ArtProfileMethodRuleInfoImpl EMPTY = new ArtProfileMethodRuleInfoImpl(0);
+  private static final ArtProfileMethodRuleInfoImpl[] INSTANCES =
+      ArrayUtils.initialize(new ArtProfileMethodRuleInfoImpl[8], ArtProfileMethodRuleInfoImpl::new);
 
   private final int flags;
 
-  ArtProfileMethodRuleInfoImpl(int flags) {
+  private ArtProfileMethodRuleInfoImpl(int flags) {
     this.flags = flags;
   }
 
@@ -22,7 +24,7 @@
   }
 
   public static ArtProfileMethodRuleInfoImpl empty() {
-    return EMPTY;
+    return INSTANCES[0];
   }
 
   public int getFlags() {
@@ -163,7 +165,9 @@
     }
 
     public ArtProfileMethodRuleInfoImpl build() {
-      return new ArtProfileMethodRuleInfoImpl(flags);
+      assert 0 <= flags;
+      assert flags < INSTANCES.length;
+      return INSTANCES[flags];
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java b/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java
index cb799fd..9d133de 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ConcreteProfileCollectionAdditions.java
@@ -65,14 +65,13 @@
     applyIfContextIsInProfile(context, additionsBuilder -> additionsBuilder.addRule(method));
   }
 
-  public void addMethodIfContextIsInProfile(
-      ProgramMethod method,
-      DexClassAndMethod context,
-      Consumer<AbstractProfileMethodRule.Builder<?, ?>> methodRuleBuilderConsumer) {
+  public void addMethodIfContextIsInProfile(ProgramMethod method, DexClassAndMethod context) {
     if (context.isProgramMethod()) {
       addMethodIfContextIsInProfile(method, context.asProgramMethod());
     } else {
-      accept(additions -> additions.addMethodRule(method, methodRuleBuilderConsumer));
+      accept(
+          additions ->
+              additions.addMethodRule(method, AbstractProfileMethodRule.Builder::setIsStartup));
     }
   }
 
@@ -82,21 +81,18 @@
   }
 
   void applyIfContextIsInProfile(
-      ProgramDefinition context,
-      Consumer<ProfileAdditions<?, ?, ?, ?, ?, ?, ?, ?>> additionsConsumer,
-      Consumer<ProfileAdditionsBuilder> additionsBuilderConsumer) {
+      ProgramDefinition context, Consumer<ProfileAdditionsBuilder> builderConsumer) {
     if (context.isProgramClass()) {
-      applyIfContextIsInProfile(context.asProgramClass(), additionsConsumer);
+      applyIfContextIsInProfile(context.asProgramClass(), builderConsumer);
     } else {
       assert context.isProgramMethod();
-      applyIfContextIsInProfile(context.asProgramMethod(), additionsBuilderConsumer);
+      applyIfContextIsInProfile(context.asProgramMethod(), builderConsumer);
     }
   }
 
   void applyIfContextIsInProfile(
-      DexProgramClass context,
-      Consumer<ProfileAdditions<?, ?, ?, ?, ?, ?, ?, ?>> additionsConsumer) {
-    accept(additions -> additions.applyIfContextIsInProfile(context.getType(), additionsConsumer));
+      DexProgramClass context, Consumer<ProfileAdditionsBuilder> builderConsumer) {
+    accept(additions -> additions.applyIfContextIsInProfile(context.getType(), builderConsumer));
   }
 
   public void applyIfContextIsInProfile(
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java
index 869f88b..bf4b3eb 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileAdditions.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.profile.AbstractProfileClassRule;
 import com.android.tools.r8.profile.AbstractProfileMethodRule;
 import com.android.tools.r8.profile.AbstractProfileRule;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -90,9 +91,29 @@
     this.profile = profile;
   }
 
-  public void applyIfContextIsInProfile(DexType context, Consumer<? super Additions> fn) {
+  public void applyIfContextIsInProfile(
+      DexType context, Consumer<ProfileAdditionsBuilder> builderConsumer) {
     if (profile.containsClassRule(context) || classRuleAdditions.containsKey(context)) {
-      fn.accept(self());
+      builderConsumer.accept(
+          new ProfileAdditionsBuilder() {
+            @Override
+            public ProfileAdditionsBuilder addClassRule(DexType type) {
+              ProfileAdditions.this.addClassRule(type);
+              return this;
+            }
+
+            @Override
+            public ProfileAdditionsBuilder addMethodRule(DexMethod method) {
+              ProfileAdditions.this.addMethodRule(
+                  method, AbstractProfileMethodRule.Builder::setIsStartup);
+              return this;
+            }
+
+            @Override
+            public void removeMovedMethodRule(DexMethod oldMethod, ProgramMethod newMethod) {
+              ProfileAdditions.this.removeMovedMethodRule(oldMethod, newMethod);
+            }
+          });
     }
   }
 
@@ -167,7 +188,7 @@
     return addMethodRule(method.getReference(), methodRuleBuilderConsumer);
   }
 
-  public Additions addMethodRule(
+  private Additions addMethodRule(
       DexMethod method, Consumer<? super MethodRuleBuilder> methodRuleBuilderConsumer) {
     // Create profile rule for method.
     MethodRuleBuilder methodRuleBuilder =
@@ -296,7 +317,8 @@
           // If this assertion fails, that means we have synthetics with multiple
           // synthesizing contexts, which are not guaranteed to be processed before the
           // synthetic itself. In that case this assertion should simply be removed.
-          assert successorMethodRuleBuilder.isGreaterThanOrEqualTo(methodRuleBuilder);
+          assert successorMethodRuleBuilder.isGreaterThanOrEqualTo(methodRuleBuilder)
+              : getGraphString(methodRuleAdditions, method, successor);
           successorMethodRuleBuilder.join(methodRuleBuilder);
           // Note: no need to addIgnoringSeenSet() since the graph will not have cycles. Indeed, it
           // should never be the case that a method m2(), which is synthesized from method context
@@ -305,5 +327,35 @@
         }
       }
     }
+
+    // Return a string representation of the graph for diagnosing b/278524993.
+    private String getGraphString(
+        Map<DexMethod, MethodRuleBuilder> methodRuleAdditions,
+        DexMethod context,
+        DexMethod method) {
+      StringBuilder builder =
+          new StringBuilder("Error at edge: ")
+              .append(context.toSourceString())
+              .append(" -> ")
+              .append(method.toSourceString());
+      Set<DexMethod> nodes =
+          SetUtils.unionIdentityHashSet(predecessors.keySet(), successors.keySet());
+      for (DexMethod node : nodes) {
+        builder
+            .append(System.lineSeparator())
+            .append(System.lineSeparator())
+            .append(node.toSourceString());
+        for (DexMethod predecessor : predecessors.getOrDefault(node, Collections.emptySet())) {
+          builder
+              .append(System.lineSeparator())
+              .append("  <- ")
+              .append(predecessor.toSourceString());
+        }
+        for (DexMethod successor : successors.getOrDefault(node, Collections.emptySet())) {
+          builder.append(System.lineSeparator()).append("  -> ").append(successor.toSourceString());
+        }
+      }
+      return builder.toString();
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingApiReferenceStubberEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingApiReferenceStubberEventConsumer.java
index c5bbcf7..33e4b0f 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingApiReferenceStubberEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingApiReferenceStubberEventConsumer.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.profile.rewriting;
 
-import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
-
 import com.android.tools.r8.androidapi.ApiReferenceStubberEventConsumer;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexLibraryClass;
@@ -47,10 +45,10 @@
       DexProgramClass mockClass, DexLibraryClass libraryClass, DexProgramClass context) {
     collectionAdditions.applyIfContextIsInProfile(
         context,
-        additions ->
-            additions
-                .addClassRule(mockClass)
-                .addMethodRule(mockClass.getProgramClassInitializer(), emptyConsumer()));
+        additionsBuilder ->
+            additionsBuilder
+                .addClassRule(mockClass.getType())
+                .addMethodRule(mockClass.getProgramClassInitializer().getReference()));
     parent.acceptMockedLibraryClassContext(mockClass, libraryClass, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfClassSynthesizerDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfClassSynthesizerDesugaringEventConsumer.java
index c16dd21..b0e127a 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfClassSynthesizerDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfClassSynthesizerDesugaringEventConsumer.java
@@ -84,7 +84,7 @@
   public void acceptRecordClassContext(
       DexProgramClass recordTagClass, DexProgramClass recordClass) {
     additionsCollection.applyIfContextIsInProfile(
-        recordClass, additions -> additions.addClassRule(recordTagClass));
+        recordClass, additionsBuilder -> additionsBuilder.addClassRule(recordTagClass.getType()));
     ProgramMethod recordTagInstanceInitializer = recordTagClass.getProgramDefaultInitializer();
     if (recordTagInstanceInitializer != null) {
       recordClass.forEachProgramInstanceInitializer(
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfPostProcessingDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfPostProcessingDesugaringEventConsumer.java
index 0d5c835..81115bb 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfPostProcessingDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfPostProcessingDesugaringEventConsumer.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.EmulatedDispatchMethodDescriptor;
 import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper;
+import com.android.tools.r8.profile.AbstractProfileMethodRule;
 import com.android.tools.r8.profile.art.ArtProfileOptions;
 import com.android.tools.r8.utils.BooleanBox;
 import java.util.Set;
@@ -80,7 +81,9 @@
   public void acceptDesugaredLibraryRetargeterForwardingMethod(
       ProgramMethod method, EmulatedDispatchMethodDescriptor descriptor) {
     if (options.isIncludingDesugaredLibraryRetargeterForwardingMethodsUnconditionally()) {
-      additionsCollection.accept(additions -> additions.addMethodRule(method, emptyConsumer()));
+      additionsCollection.accept(
+          additions ->
+              additions.addMethodRule(method, AbstractProfileMethodRule.Builder::setIsStartup));
     }
     parent.acceptDesugaredLibraryRetargeterForwardingMethod(method, descriptor);
   }
@@ -109,7 +112,7 @@
   @Override
   public void acceptInterfaceMethodDesugaringForwardingMethod(
       ProgramMethod method, DexClassAndMethod baseMethod) {
-    additionsCollection.addMethodIfContextIsInProfile(method, baseMethod, emptyConsumer());
+    additionsCollection.addMethodIfContextIsInProfile(method, baseMethod);
     parent.acceptInterfaceMethodDesugaringForwardingMethod(method, baseMethod);
   }
 
@@ -128,7 +131,8 @@
           });
       if (seenMethodCausingError.isFalse()) {
         additionsCollection.applyIfContextIsInProfile(
-            method.getHolder(), additions -> additions.addMethodRule(method, emptyConsumer()));
+            method.getHolder(),
+            additionsBuilder -> additionsBuilder.addMethodRule(method.getReference()));
       }
     }
     parent.acceptThrowingMethod(method, errorType, resolutionResult);
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingMemberRebindingEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingMemberRebindingEventConsumer.java
index c7aa6b9..59ca2dc 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingMemberRebindingEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingMemberRebindingEventConsumer.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.profile.rewriting;
 
-import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -37,7 +35,7 @@
   @Override
   public void acceptMemberRebindingBridgeMethod(
       ProgramMethod bridgeMethod, DexClassAndMethod targetMethod) {
-    additionsCollection.addMethodIfContextIsInProfile(bridgeMethod, targetMethod, emptyConsumer());
+    additionsCollection.addMethodIfContextIsInProfile(bridgeMethod, targetMethod);
     parent.acceptMemberRebindingBridgeMethod(bridgeMethod, targetMethod);
   }
 
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingNestBasedAccessDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingNestBasedAccessDesugaringEventConsumer.java
index 65cce7f..7d731fb 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingNestBasedAccessDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingNestBasedAccessDesugaringEventConsumer.java
@@ -4,13 +4,12 @@
 
 package com.android.tools.r8.profile.rewriting;
 
-import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
-
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaringEventConsumer;
+import com.android.tools.r8.profile.AbstractProfileMethodRule;
 
 public class ProfileRewritingNestBasedAccessDesugaringEventConsumer
     implements NestBasedAccessDesugaringEventConsumer {
@@ -48,7 +47,9 @@
     } else {
       additionsCollection.accept(
           additions ->
-              additions.addClassRule(argumentClass).addMethodRule(bridge, emptyConsumer()));
+              additions
+                  .addClassRule(argumentClass)
+                  .addMethodRule(bridge, AbstractProfileMethodRule.Builder::setIsStartup));
     }
     parent.acceptNestConstructorBridge(target, bridge, argumentClass, context);
   }
@@ -56,21 +57,21 @@
   @Override
   public void acceptNestFieldGetBridge(
       ProgramField target, ProgramMethod bridge, DexClassAndMethod context) {
-    additionsCollection.addMethodIfContextIsInProfile(bridge, context, emptyConsumer());
+    additionsCollection.addMethodIfContextIsInProfile(bridge, context);
     parent.acceptNestFieldGetBridge(target, bridge, context);
   }
 
   @Override
   public void acceptNestFieldPutBridge(
       ProgramField target, ProgramMethod bridge, DexClassAndMethod context) {
-    additionsCollection.addMethodIfContextIsInProfile(bridge, context, emptyConsumer());
+    additionsCollection.addMethodIfContextIsInProfile(bridge, context);
     parent.acceptNestFieldPutBridge(target, bridge, context);
   }
 
   @Override
   public void acceptNestMethodBridge(
       ProgramMethod target, ProgramMethod bridge, DexClassAndMethod context) {
-    additionsCollection.addMethodIfContextIsInProfile(bridge, context, emptyConsumer());
+    additionsCollection.addMethodIfContextIsInProfile(bridge, context);
     parent.acceptNestMethodBridge(target, bridge, context);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingVarHandleDesugaringEventConsumerUtils.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingVarHandleDesugaringEventConsumerUtils.java
index c9ff809..05f0187 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingVarHandleDesugaringEventConsumerUtils.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingVarHandleDesugaringEventConsumerUtils.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.profile.rewriting;
 
-import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
-
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.profile.art.ArtProfileOptions;
@@ -20,11 +18,6 @@
     if (options.isIncludingVarHandleClasses()) {
       additionsCollection.applyIfContextIsInProfile(
           context,
-          additions -> {
-            additions.addClassRule(varHandleClass);
-            varHandleClass.forEachProgramMethod(
-                method -> additions.addMethodRule(method, emptyConsumer()));
-          },
           additionsBuilder -> {
             additionsBuilder.addRule(varHandleClass);
             varHandleClass.forEachProgramMethod(additionsBuilder::addRule);
diff --git a/src/main/java/com/android/tools/r8/profile/startup/profile/StartupProfileMethodRule.java b/src/main/java/com/android/tools/r8/profile/startup/profile/StartupProfileMethodRule.java
index e16699f..9c2afae 100644
--- a/src/main/java/com/android/tools/r8/profile/startup/profile/StartupProfileMethodRule.java
+++ b/src/main/java/com/android/tools/r8/profile/startup/profile/StartupProfileMethodRule.java
@@ -105,6 +105,12 @@
     }
 
     @Override
+    public Builder setIsStartup() {
+      // Intentionally empty, startup profile rules do not have any flags.
+      return this;
+    }
+
+    @Override
     public Builder setMethod(DexMethod method) {
       this.method = method;
       return this;
diff --git a/src/main/java/com/android/tools/r8/retrace/PartitionedToProguardMappingConverter.java b/src/main/java/com/android/tools/r8/retrace/PartitionedToProguardMappingConverter.java
index b6181dc..4a4d593 100644
--- a/src/main/java/com/android/tools/r8/retrace/PartitionedToProguardMappingConverter.java
+++ b/src/main/java/com/android/tools/r8/retrace/PartitionedToProguardMappingConverter.java
@@ -7,13 +7,15 @@
 import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.Finishable;
 import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.dex.CompatByteBuffer;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.LineReader;
 import com.android.tools.r8.naming.MapVersion;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal;
+import com.android.tools.r8.retrace.internal.MetadataAdditionalInfo;
 import com.android.tools.r8.retrace.internal.ProguardMapReaderWithFiltering.ProguardMapReaderWithFilteringInputBuffer;
-import com.android.tools.r8.retrace.internal.RetracePartitionException;
 import com.android.tools.r8.utils.ChainableStringConsumer;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -39,29 +41,40 @@
   public void run() throws RetracePartitionException {
     MappingPartitionMetadataInternal metadataInternal =
         MappingPartitionMetadataInternal.deserialize(
-            metadata, MapVersion.MAP_VERSION_UNKNOWN, diagnosticsHandler);
+            CompatByteBuffer.wrapOrNull(metadata),
+            MapVersion.MAP_VERSION_UNKNOWN,
+            diagnosticsHandler);
     if (!metadataInternal.canGetPartitionKeys()) {
       throw new RetracePartitionException("Cannot obtain all partition keys from metadata");
     }
-    // TODO(b/274893426): Account for preamble.
-    ClassNameMapper classNameMapper = ClassNameMapper.builder().build();
+    ProguardMapWriter consumer = new ProguardMapWriter(this.consumer, diagnosticsHandler);
+    if (metadataInternal.canGetAdditionalInfo()) {
+      MetadataAdditionalInfo additionalInfo = metadataInternal.getAdditionalInfo();
+      if (additionalInfo.hasPreamble()) {
+        additionalInfo.getPreamble().forEach(line -> consumer.accept(line).accept("\n"));
+      }
+    }
     for (String partitionKey : metadataInternal.getPartitionKeys()) {
       LineReader reader =
           new ProguardMapReaderWithFilteringInputBuffer(
               new ByteArrayInputStream(partitionSupplier.get(partitionKey)), alwaysTrue(), true);
       try {
-        classNameMapper =
-            ClassNameMapper.mapperFromLineReaderWithFiltering(
-                    reader, metadataInternal.getMapVersion(), diagnosticsHandler, true, true)
-                .combine(classNameMapper);
+        ClassNameMapper.mapperFromLineReaderWithFiltering(
+                reader,
+                metadataInternal.getMapVersion(),
+                diagnosticsHandler,
+                true,
+                true,
+                partitionBuilder -> partitionBuilder.setBuildPreamble(true))
+            .write(consumer);
       } catch (IOException e) {
         throw new RetracePartitionException(e);
       }
     }
-    classNameMapper.sorted().write(new ProguardMapWriter(consumer, diagnosticsHandler));
+    consumer.finished(diagnosticsHandler);
   }
 
-  private static class ProguardMapWriter implements ChainableStringConsumer {
+  private static class ProguardMapWriter implements ChainableStringConsumer, Finishable {
 
     private final StringConsumer consumer;
     private final DiagnosticsHandler diagnosticsHandler;
@@ -76,6 +89,11 @@
       consumer.accept(string, diagnosticsHandler);
       return this;
     }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      consumer.finished(handler);
+    }
   }
 
   public static Builder builder() {
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetracePartitionException.java b/src/main/java/com/android/tools/r8/retrace/RetracePartitionException.java
similarity index 90%
rename from src/main/java/com/android/tools/r8/retrace/internal/RetracePartitionException.java
rename to src/main/java/com/android/tools/r8/retrace/RetracePartitionException.java
index 7e6e924..5712f0d 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetracePartitionException.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetracePartitionException.java
@@ -2,7 +2,7 @@
 // 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.retrace.internal;
+package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.Keep;
 
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionKeyStrategy.java b/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionKeyStrategy.java
index b31d984..6b050c6 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionKeyStrategy.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionKeyStrategy.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.retrace.internal;
 
 public enum MappingPartitionKeyStrategy {
+  UNKNOWN(-1),
   OBFUSCATED_TYPE_NAME_AS_KEY(0),
   OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS(1);
 
@@ -16,6 +17,17 @@
     this.serializedKey = serializedKey;
   }
 
+  public static MappingPartitionKeyStrategy getByKey(int serializedKey) {
+    switch (serializedKey) {
+      case 0:
+        return OBFUSCATED_TYPE_NAME_AS_KEY;
+      case 1:
+        return OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS;
+      default:
+        return UNKNOWN;
+    }
+  }
+
   public int getSerializedKey() {
     return serializedKey;
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java b/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java
index 0cee013..9896856 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java
@@ -6,11 +6,16 @@
 
 import static com.android.tools.r8.retrace.internal.MappingPartitionKeyStrategy.OBFUSCATED_TYPE_NAME_AS_KEY;
 import static com.android.tools.r8.retrace.internal.MappingPartitionKeyStrategy.OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS;
+import static com.android.tools.r8.retrace.internal.MappingPartitionKeyStrategy.getByKey;
+import static com.android.tools.r8.utils.SerializationUtils.getZeroByte;
 
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.dex.CompatByteBuffer;
 import com.android.tools.r8.naming.MapVersion;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.retrace.MappingPartitionMetadata;
+import com.android.tools.r8.retrace.RetracePartitionException;
+import com.android.tools.r8.retrace.internal.MetadataAdditionalInfo.LazyMetadataAdditionalInfo;
 import com.android.tools.r8.retrace.internal.MetadataPartitionCollection.LazyMetadataPartitionCollection;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.google.common.primitives.Ints;
@@ -33,7 +38,14 @@
     return null;
   }
 
-  byte ZERO_BYTE = (byte) 0;
+  default boolean canGetAdditionalInfo() {
+    return false;
+  }
+
+  default MetadataAdditionalInfo getAdditionalInfo() {
+    return MetadataAdditionalInfo.create(null);
+  }
+
   // Magic byte put into the metadata
   byte[] MAGIC = new byte[] {(byte) 0xAA, (byte) 0xA8};
 
@@ -42,20 +54,25 @@
   }
 
   static MappingPartitionMetadataInternal deserialize(
-      byte[] bytes, MapVersion fallBackMapVersion, DiagnosticsHandler diagnosticsHandler) {
-    if (bytes == null) {
+      CompatByteBuffer buffer,
+      MapVersion fallBackMapVersion,
+      DiagnosticsHandler diagnosticsHandler) {
+    if (buffer == null) {
       return ObfuscatedTypeNameAsKeyMetadata.create(fallBackMapVersion);
     }
-    if (bytes.length > 2) {
-      if (startsWithMagic(bytes)) {
-        int serializedKey =
-            Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, bytes[magicOffset()], bytes[magicOffset() + 1]);
-        if (serializedKey == OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS.getSerializedKey()) {
-          return ObfuscatedTypeNameAsKeyMetadataWithPartitionNames.deserialize(bytes);
-        }
-      } else if (OBFUSCATED_TYPE_NAME_AS_KEY.getSerializedKey()
-          == Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, bytes[0], bytes[1])) {
-        return ObfuscatedTypeNameAsKeyMetadata.deserialize(bytes);
+    if (buffer.remaining() > 2) {
+      int magicOrStrategyKey = buffer.getUShort();
+      if (magicOrStrategyKey == Ints.fromBytes(getZeroByte(), getZeroByte(), MAGIC[0], MAGIC[1])) {
+        magicOrStrategyKey = buffer.getShort();
+      }
+      switch (getByKey(magicOrStrategyKey)) {
+        case OBFUSCATED_TYPE_NAME_AS_KEY:
+          return ObfuscatedTypeNameAsKeyMetadata.deserialize(buffer);
+        case OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS:
+          return ObfuscatedTypeNameAsKeyMetadataWithPartitionNames.deserialize(buffer);
+        default:
+          throw new RetracePartitionException(
+              "Could not find partition key strategy from serialized key: " + magicOrStrategyKey);
       }
     }
     // If we arrived here then we could not deserialize the metadata.
@@ -65,18 +82,6 @@
     throw exception;
   }
 
-  private static boolean startsWithMagic(byte[] bytes) {
-    if (bytes.length < MAGIC.length) {
-      return false;
-    }
-    for (int i = 0; i < MAGIC.length; i++) {
-      if (bytes[i] != MAGIC[i]) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   class ObfuscatedTypeNameAsKeyMetadata implements MappingPartitionMetadataInternal {
 
     private final MapVersion mapVersion;
@@ -111,8 +116,9 @@
       }
     }
 
-    public static ObfuscatedTypeNameAsKeyMetadata deserialize(byte[] bytes) {
-      MapVersion mapVersion = MapVersion.fromName(new String(bytes, 2, bytes.length - 2));
+    public static ObfuscatedTypeNameAsKeyMetadata deserialize(CompatByteBuffer buffer) {
+      byte[] array = buffer.array();
+      MapVersion mapVersion = MapVersion.fromName(new String(array, 2, array.length - 2));
       return create(mapVersion);
     }
 
@@ -126,17 +132,23 @@
 
     private final MapVersion mapVersion;
     private final MetadataPartitionCollection metadataPartitionCollection;
+    private final MetadataAdditionalInfo metadataAdditionalInfo;
 
     private ObfuscatedTypeNameAsKeyMetadataWithPartitionNames(
-        MapVersion mapVersion, MetadataPartitionCollection metadataPartitionCollection) {
+        MapVersion mapVersion,
+        MetadataPartitionCollection metadataPartitionCollection,
+        MetadataAdditionalInfo metadataAdditionalInfo) {
       this.mapVersion = mapVersion;
       this.metadataPartitionCollection = metadataPartitionCollection;
+      this.metadataAdditionalInfo = metadataAdditionalInfo;
     }
 
     public static ObfuscatedTypeNameAsKeyMetadataWithPartitionNames create(
-        MapVersion mapVersion, MetadataPartitionCollection metadataPartitionCollection) {
+        MapVersion mapVersion,
+        MetadataPartitionCollection metadataPartitionCollection,
+        MetadataAdditionalInfo metadataAdditionalInfo) {
       return new ObfuscatedTypeNameAsKeyMetadataWithPartitionNames(
-          mapVersion, metadataPartitionCollection);
+          mapVersion, metadataPartitionCollection, metadataAdditionalInfo);
     }
 
     @Override
@@ -159,8 +171,18 @@
       return metadataPartitionCollection.getPartitionKeys();
     }
 
+    @Override
+    public boolean canGetAdditionalInfo() {
+      return true;
+    }
+
+    @Override
+    public MetadataAdditionalInfo getAdditionalInfo() {
+      return metadataAdditionalInfo;
+    }
+
     // The format is:
-    // <type:short><map-version-length:short><map-version>[<partition_key>]
+    // <MAGIC><type:short><map-version-length:short><map-version>{partitions}{additionalinfo}
     @Override
     public byte[] getBytes() {
       try {
@@ -168,10 +190,9 @@
         DataOutputStream dataOutputStream = new DataOutputStream(temp);
         dataOutputStream.write(MAGIC);
         dataOutputStream.writeShort(OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS.getSerializedKey());
-        String name = mapVersion.getName();
-        dataOutputStream.writeShort(name.length());
-        dataOutputStream.writeBytes(name);
-        dataOutputStream.write(metadataPartitionCollection.serialize());
+        dataOutputStream.writeUTF(mapVersion.getName());
+        metadataPartitionCollection.serialize(dataOutputStream);
+        metadataAdditionalInfo.serialize(dataOutputStream);
         dataOutputStream.close();
         return temp.toByteArray();
       } catch (IOException e) {
@@ -179,14 +200,16 @@
       }
     }
 
-    public static ObfuscatedTypeNameAsKeyMetadataWithPartitionNames deserialize(byte[] bytes) {
-      int start = magicOffset();
-      int length = Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, bytes[start + 2], bytes[start + 3]);
-      MapVersion mapVersion = MapVersion.fromName(new String(bytes, start + 4, length));
-      int partitionCollectionStartIndex = start + 4 + length;
+    public static ObfuscatedTypeNameAsKeyMetadataWithPartitionNames deserialize(
+        CompatByteBuffer buffer) {
+      String utf = buffer.getUTFOfUByteSize();
+      MapVersion mapVersion = MapVersion.fromName(utf);
+      LazyMetadataPartitionCollection metadataPartitionCollection =
+          LazyMetadataPartitionCollection.create(buffer);
+      LazyMetadataAdditionalInfo lazyMetadataAdditionalInfo =
+          LazyMetadataAdditionalInfo.create(buffer);
       return ObfuscatedTypeNameAsKeyMetadataWithPartitionNames.create(
-          mapVersion,
-          new LazyMetadataPartitionCollection(bytes, partitionCollectionStartIndex, bytes.length));
+          mapVersion, metadataPartitionCollection, lazyMetadataAdditionalInfo);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/MetadataAdditionalInfo.java b/src/main/java/com/android/tools/r8/retrace/internal/MetadataAdditionalInfo.java
new file mode 100644
index 0000000..706584b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/internal/MetadataAdditionalInfo.java
@@ -0,0 +1,121 @@
+// Copyright (c) 2023, 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.retrace.internal;
+
+import com.android.tools.r8.dex.CompatByteBuffer;
+import com.android.tools.r8.retrace.RetracePartitionException;
+import com.android.tools.r8.utils.SerializationUtils;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+public class MetadataAdditionalInfo {
+
+  public enum AdditionalInfoTypes {
+    UNKNOWN(-1),
+    PREAMBLE(0);
+
+    private final int serializedKey;
+
+    AdditionalInfoTypes(int serializedKey) {
+      this.serializedKey = serializedKey;
+    }
+
+    static AdditionalInfoTypes getByKey(int serializedKey) {
+      if (serializedKey == 0) {
+        return PREAMBLE;
+      }
+      return UNKNOWN;
+    }
+  }
+
+  protected final List<String> preamble;
+
+  private MetadataAdditionalInfo(List<String> preamble) {
+    this.preamble = preamble;
+  }
+
+  public boolean hasPreamble() {
+    return preamble != null;
+  }
+
+  public Collection<String> getPreamble() {
+    return preamble;
+  }
+
+  // The serialized format is an extensible list where we first record the offsets for each data
+  // section and then emit the data.
+  // <total-size:int><number-of-elements:short>[<type-i:short><length-i:int><data-i>]
+  public void serialize(DataOutputStream dataOutputStream) throws IOException {
+    ByteArrayOutputStream temp = new ByteArrayOutputStream();
+    DataOutputStream additionalInfoStream = new DataOutputStream(temp);
+    additionalInfoStream.writeShort(1);
+    additionalInfoStream.writeShort(AdditionalInfoTypes.PREAMBLE.serializedKey);
+    SerializationUtils.writeUTFOfIntSize(additionalInfoStream, StringUtils.unixLines(preamble));
+    byte[] payload = temp.toByteArray();
+    dataOutputStream.writeInt(payload.length);
+    dataOutputStream.write(payload);
+  }
+
+  private static MetadataAdditionalInfo deserialize(byte[] bytes) {
+    CompatByteBuffer compatByteBuffer = CompatByteBuffer.wrap(bytes);
+    int numberOfElements = compatByteBuffer.getShort();
+    List<String> preamble = null;
+    for (int i = 0; i < numberOfElements; i++) {
+      // We are parsing <type:short><length:int><bytes>
+      int additionInfoTypeKey = compatByteBuffer.getShort();
+      AdditionalInfoTypes additionalInfoType = AdditionalInfoTypes.getByKey(additionInfoTypeKey);
+      if (additionalInfoType == AdditionalInfoTypes.PREAMBLE) {
+        preamble = StringUtils.splitLines(compatByteBuffer.getUTFOfIntSize());
+      } else {
+        throw new RetracePartitionException(
+            "Could not additional info from key: " + additionInfoTypeKey);
+      }
+    }
+    return new MetadataAdditionalInfo(preamble);
+  }
+
+  public static MetadataAdditionalInfo create(List<String> preamble) {
+    return new MetadataAdditionalInfo(preamble);
+  }
+
+  public static class LazyMetadataAdditionalInfo extends MetadataAdditionalInfo {
+
+    private byte[] bytes;
+    private MetadataAdditionalInfo metadataAdditionalInfo = null;
+
+    public LazyMetadataAdditionalInfo(byte[] bytes) {
+      super(null);
+      this.bytes = bytes;
+    }
+
+    @Override
+    public boolean hasPreamble() {
+      MetadataAdditionalInfo metadataAdditionalInfo = getMetadataAdditionalInfo();
+      return metadataAdditionalInfo != null && metadataAdditionalInfo.hasPreamble();
+    }
+
+    @Override
+    public Collection<String> getPreamble() {
+      MetadataAdditionalInfo metadataAdditionalInfo = getMetadataAdditionalInfo();
+      return metadataAdditionalInfo == null ? null : metadataAdditionalInfo.getPreamble();
+    }
+
+    private MetadataAdditionalInfo getMetadataAdditionalInfo() {
+      if (metadataAdditionalInfo == null) {
+        metadataAdditionalInfo = MetadataAdditionalInfo.deserialize(bytes);
+        bytes = null;
+      }
+      return metadataAdditionalInfo;
+    }
+
+    public static LazyMetadataAdditionalInfo create(CompatByteBuffer buffer) {
+      return new LazyMetadataAdditionalInfo(buffer.getBytesOfIntSize());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/MetadataPartitionCollection.java b/src/main/java/com/android/tools/r8/retrace/internal/MetadataPartitionCollection.java
index aef50f0..3a3d271 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/MetadataPartitionCollection.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/MetadataPartitionCollection.java
@@ -4,7 +4,11 @@
 
 package com.android.tools.r8.retrace.internal;
 
+import com.android.tools.r8.dex.CompatByteBuffer;
+import com.android.tools.r8.utils.SerializationUtils;
 import com.android.tools.r8.utils.StringUtils;
+import java.io.DataOutputStream;
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Collections;
@@ -23,50 +27,43 @@
     return partitionKeys;
   }
 
-  public byte[] serialize() {
-    return StringUtils.join(SEPARATOR + "", partitionKeys).getBytes(StandardCharsets.UTF_8);
+  // The format is:
+  // <length-in-bytes:int><data>
+  public void serialize(DataOutputStream dataOutputStream) throws IOException {
+    SerializationUtils.writeUTFOfIntSize(
+        dataOutputStream, StringUtils.join(SEPARATOR + "", partitionKeys));
+  }
+
+  private static MetadataPartitionCollection deserialize(byte[] bytes) {
+    String allKeys = new String(bytes, StandardCharsets.UTF_8);
+    return create(StringUtils.split(allKeys, SEPARATOR));
   }
 
   public static MetadataPartitionCollection create(Collection<String> partitionKeys) {
     return new MetadataPartitionCollection(partitionKeys);
   }
 
-  public static MetadataPartitionCollection createLazy(
-      byte[] bytes, int partitionCollectionStartIndex, int partitionCollectionEndIndex) {
-    return new LazyMetadataPartitionCollection(
-        bytes, partitionCollectionStartIndex, partitionCollectionEndIndex);
-  }
-
   public static class LazyMetadataPartitionCollection extends MetadataPartitionCollection {
 
-    private final byte[] bytes;
-    private final int partitionCollectionStartIndex;
-    private final int partitionCollectionEndIndex;
+    private byte[] bytes;
     private MetadataPartitionCollection metadataPartitionCollection = null;
 
-    public LazyMetadataPartitionCollection(
-        byte[] bytes, int partitionCollectionStartIndex, int partitionCollectionEndIndex) {
+    private LazyMetadataPartitionCollection(byte[] bytes) {
       super(Collections.emptyList());
       this.bytes = bytes;
-      this.partitionCollectionStartIndex = partitionCollectionStartIndex;
-      this.partitionCollectionEndIndex = partitionCollectionEndIndex;
     }
 
     @Override
     public Collection<String> getPartitionKeys() {
       if (metadataPartitionCollection == null) {
-        metadataPartitionCollection = deserialize();
+        metadataPartitionCollection = deserialize(bytes);
+        bytes = null;
       }
       return metadataPartitionCollection.getPartitionKeys();
     }
 
-    private MetadataPartitionCollection deserialize() {
-      String allKeys =
-          new String(
-              bytes,
-              partitionCollectionStartIndex,
-              partitionCollectionEndIndex - partitionCollectionStartIndex);
-      return create(StringUtils.split(allKeys, SEPARATOR));
+    public static LazyMetadataPartitionCollection create(CompatByteBuffer buffer) {
+      return new LazyMetadataPartitionCollection(buffer.getBytesOfIntSize());
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/PartitionMappingSupplierImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/PartitionMappingSupplierImpl.java
index 7d663fb..de94d1e 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/PartitionMappingSupplierImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/PartitionMappingSupplierImpl.java
@@ -7,6 +7,7 @@
 import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.dex.CompatByteBuffer;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.LineReader;
 import com.android.tools.r8.naming.MapVersion;
@@ -66,7 +67,7 @@
     }
     return mappingPartitionMetadataCache =
         MappingPartitionMetadataInternal.deserialize(
-            metadata, fallbackMapVersion, diagnosticsHandler);
+            CompatByteBuffer.wrapOrNull(metadata), fallbackMapVersion, diagnosticsHandler);
   }
 
   @Override
@@ -114,7 +115,12 @@
                 new ByteArrayInputStream(suppliedPartition), alwaysTrue(), true);
         classNameMapper =
             ClassNameMapper.mapperFromLineReaderWithFiltering(
-                    reader, metadata.getMapVersion(), diagnosticsHandler, true, allowExperimental)
+                    reader,
+                    metadata.getMapVersion(),
+                    diagnosticsHandler,
+                    true,
+                    allowExperimental,
+                    builder -> builder.setBuildPreamble(true))
                 .combine(this.classNameMapper);
       } catch (IOException e) {
         throw new InvalidMappingFileException(e);
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
index 06ede47..3edde9a 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
@@ -11,13 +11,14 @@
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
 import com.android.tools.r8.naming.LineReader;
 import com.android.tools.r8.naming.MapVersion;
-import com.android.tools.r8.naming.mappinginformation.FileNameInformation;
 import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation;
+import com.android.tools.r8.naming.mappinginformation.PartitionFileNameInformation;
 import com.android.tools.r8.retrace.MappingPartition;
 import com.android.tools.r8.retrace.MappingPartitionMetadata;
 import com.android.tools.r8.retrace.ProguardMapPartitioner;
 import com.android.tools.r8.retrace.ProguardMapPartitionerBuilder;
 import com.android.tools.r8.retrace.ProguardMapProducer;
+import com.android.tools.r8.retrace.RetracePartitionException;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal.ObfuscatedTypeNameAsKeyMetadata;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal.ObfuscatedTypeNameAsKeyMetadataWithPartitionNames;
 import com.android.tools.r8.retrace.internal.ProguardMapReaderWithFiltering.ProguardMapReaderWithFilteringInputBuffer;
@@ -100,7 +101,8 @@
             MapVersion.MAP_VERSION_UNKNOWN,
             diagnosticsHandler,
             allowEmptyMappedRanges,
-            allowExperimentalMapping);
+            allowExperimentalMapping,
+            builder -> builder.setBuildPreamble(true).setAddVersionAsPreamble(true));
     reader.forEachClassMapping(
         (classMapping, entries) -> {
           try {
@@ -131,24 +133,27 @@
         getPartitionsFromProguardMapProducer(
             (classNameMapper, classNamingForNameMapper, payload) -> {
               Set<String> seenMappings = new HashSet<>();
-              StringBuilder payloadWithClassReferences = new StringBuilder();
-              Map<String, String> lookupMap =
-                  classNameMapper.getObfuscatedToOriginalMapping().inverse;
+              PartitionFileNameInformation.Builder partitionFileNameBuilder =
+                  PartitionFileNameInformation.builder();
               classNamingForNameMapper.visitAllFullyQualifiedReferences(
                   holder -> {
-                    if (seenMappings.add(holder)) {
-                      payloadWithClassReferences.append(
-                          getSourceFileMapping(
-                              holder,
-                              lookupMap.get(holder),
-                              classNameMapper.getSourceFile(holder)));
+                    if (classNameMapper.getSourceFile(holder) != null && seenMappings.add(holder)) {
+                      partitionFileNameBuilder.addClassToFileNameMapping(
+                          holder, classNameMapper.getSourceFile(holder));
                     }
                   });
-              payloadWithClassReferences.append(payload);
+              StringBuilder payloadBuilder = new StringBuilder();
+              if (!partitionFileNameBuilder.isEmpty()) {
+                payloadBuilder
+                    .append("# ")
+                    .append(partitionFileNameBuilder.build().serialize())
+                    .append("\n");
+              }
+              payloadBuilder.append(payload);
               mappingPartitionConsumer.accept(
                   new MappingPartitionImpl(
                       classNamingForNameMapper.renamedName,
-                      payloadWithClassReferences.toString().getBytes(StandardCharsets.UTF_8)));
+                      payloadBuilder.toString().getBytes(StandardCharsets.UTF_8)));
               keys.add(classNamingForNameMapper.renamedName);
             });
     MapVersion mapVersion = MapVersion.MAP_VERSION_UNKNOWN;
@@ -161,7 +166,9 @@
     } else if (mappingPartitionKeyStrategy
         == MappingPartitionKeyStrategy.OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS) {
       return ObfuscatedTypeNameAsKeyMetadataWithPartitionNames.create(
-          mapVersion, MetadataPartitionCollection.create(keys));
+          mapVersion,
+          MetadataPartitionCollection.create(keys),
+          MetadataAdditionalInfo.create(classMapper.getPreamble()));
     } else {
       RetracePartitionException retraceError =
           new RetracePartitionException("Unknown mapping partitioning strategy");
@@ -170,20 +177,6 @@
     }
   }
 
-  private String getSourceFileMapping(
-      String className, String obfuscatedClassName, String sourceFile) {
-    if (sourceFile == null) {
-      return "";
-    }
-    return className
-        + " -> "
-        + (obfuscatedClassName == null ? className : obfuscatedClassName)
-        + ":"
-        + "\n  # "
-        + FileNameInformation.build(sourceFile).serialize()
-        + "\n";
-  }
-
   public static class PartitionLineReader implements LineReader {
 
     private final ProguardMapReaderWithFiltering lineReader;
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java
index d791a5c..f2d647a 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java
@@ -119,7 +119,12 @@
                     proguardMapProducer.get(), buildForClass, readPreambleAndSourceFile);
         classNameMapper =
             ClassNameMapper.mapperFromLineReaderWithFiltering(
-                    reader, getMapVersion(), diagnosticsHandler, true, allowExperimental)
+                    reader,
+                    getMapVersion(),
+                    diagnosticsHandler,
+                    true,
+                    allowExperimental,
+                    builder -> builder.setBuildPreamble(true))
                 .combine(classNameMapper);
         builtClassMappings.addAll(pendingClassMappings);
         pendingClassMappings.clear();
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
index dc9c163..eb92c40 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
@@ -96,7 +96,7 @@
     if (value.isDexItemBasedValueString()) {
       DexItemBasedValueString valueString = value.asDexItemBasedValueString();
       DexReference original = valueString.value;
-      DexReference rewritten = lens.lookupReference(original);
+      DexReference rewritten = lens.getRenamedReference(original, annotationLens);
       if (original != rewritten) {
         return new DexItemBasedValueString(rewritten, valueString.getNameComputationInfo());
       }
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 39d8035..3bab5ca 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -1134,7 +1134,7 @@
     assert checkIfObsolete();
 
     // Switchmap classes should never be affected by renaming.
-    assert lens.assertDefinitionsNotModified(
+    assert lens.assertFieldsNotModified(
         switchMaps.keySet().stream()
             .map(this::resolveField)
             .filter(FieldResolutionResult::isSingleFieldResolutionResult)
diff --git a/src/main/java/com/android/tools/r8/shaking/MaximumRemovedAndroidLogLevelRule.java b/src/main/java/com/android/tools/r8/shaking/MaximumRemovedAndroidLogLevelRule.java
new file mode 100644
index 0000000..0090b89
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/MaximumRemovedAndroidLogLevelRule.java
@@ -0,0 +1,136 @@
+// Copyright (c) 2023, 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.shaking;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import java.util.List;
+
+public class MaximumRemovedAndroidLogLevelRule extends ProguardConfigurationRule {
+
+  public static final String RULE_NAME = "maximumremovedandroidloglevel";
+
+  public static final int NOT_SET = 0;
+  public static final int NONE = 1;
+  public static final int VERBOSE = 2;
+  public static final int DEBUG = 3;
+  public static final int INFO = 4;
+  public static final int WARN = 5;
+  public static final int ERROR = 6;
+  public static final int ASSERT = 7;
+
+  public static class Builder
+      extends ProguardConfigurationRule.Builder<MaximumRemovedAndroidLogLevelRule, Builder> {
+
+    private int maxRemovedAndroidLogLevel;
+
+    private Builder() {
+      super();
+    }
+
+    public Builder setMaxRemovedAndroidLogLevel(int maxRemovedAndroidLogLevel) {
+      this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
+      return this;
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    @Override
+    public MaximumRemovedAndroidLogLevelRule build() {
+      assert maxRemovedAndroidLogLevel >= NONE;
+      return new MaximumRemovedAndroidLogLevelRule(
+          origin,
+          getPosition(),
+          source,
+          buildClassAnnotations(),
+          classAccessFlags,
+          negatedClassAccessFlags,
+          classTypeNegated,
+          classType,
+          classNames,
+          buildInheritanceAnnotations(),
+          inheritanceClassName,
+          inheritanceIsExtends,
+          memberRules,
+          maxRemovedAndroidLogLevel);
+    }
+  }
+
+  private int maxRemovedAndroidLogLevel;
+
+  protected MaximumRemovedAndroidLogLevelRule(
+      Origin origin,
+      Position position,
+      String source,
+      List<ProguardTypeMatcher> classAnnotations,
+      ProguardAccessFlags classAccessFlags,
+      ProguardAccessFlags negatedClassAccessFlags,
+      boolean classTypeNegated,
+      ProguardClassType classType,
+      ProguardClassNameList classNames,
+      List<ProguardTypeMatcher> inheritanceAnnotations,
+      ProguardTypeMatcher inheritanceClassName,
+      boolean inheritanceIsExtends,
+      List<ProguardMemberRule> memberRules,
+      int maxRemovedAndroidLogLevel) {
+    super(
+        origin,
+        position,
+        source,
+        classAnnotations,
+        classAccessFlags,
+        negatedClassAccessFlags,
+        classTypeNegated,
+        classType,
+        classNames,
+        inheritanceAnnotations,
+        inheritanceClassName,
+        inheritanceIsExtends,
+        memberRules);
+    this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static int joinMaxRemovedAndroidLogLevel(int a, int b) {
+    if (a == NOT_SET) {
+      return b;
+    }
+    if (b == NOT_SET) {
+      return a;
+    }
+    // If multiple maximum log levels are provided for the same method, the minimum between them
+    // is selected as the maximum removed log level
+    return Math.min(a, b);
+  }
+
+  public int getMaxRemovedAndroidLogLevel() {
+    return maxRemovedAndroidLogLevel;
+  }
+
+  @Override
+  public boolean isMaximumRemovedAndroidLogLevelRule() {
+    return true;
+  }
+
+  @Override
+  public MaximumRemovedAndroidLogLevelRule asMaximumRemovedAndroidLogLevelRule() {
+    return this;
+  }
+
+  @Override
+  String typeString() {
+    return RULE_NAME;
+  }
+
+  @Override
+  String typeSuffix() {
+    return Integer.toString(maxRemovedAndroidLogLevel);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java b/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
index 1ce8f04..f4542d8 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
@@ -28,7 +28,7 @@
     protected ProguardAccessFlags classAccessFlags = new ProguardAccessFlags();
     protected ProguardAccessFlags negatedClassAccessFlags = new ProguardAccessFlags();
     protected boolean classTypeNegated = false;
-    protected ProguardClassType classType = ProguardClassType.UNSPECIFIED;
+    protected ProguardClassType classType;
     protected ProguardClassNameList classNames;
     private final ImmutableList.Builder<ProguardTypeMatcher> inheritanceAnnotations =
         ImmutableList.builder();
@@ -131,6 +131,10 @@
       return self();
     }
 
+    public boolean hasClassType() {
+      return classType != null;
+    }
+
     public ProguardClassType getClassType() {
       return classType;
     }
@@ -216,6 +220,7 @@
       ProguardTypeMatcher inheritanceClassName,
       boolean inheritanceIsExtends,
       List<ProguardMemberRule> memberRules) {
+    assert classType != null;
     assert origin != null;
     assert position != null;
     assert source != null || origin != Origin.unknown();
@@ -227,7 +232,6 @@
     this.negatedClassAccessFlags = negatedClassAccessFlags;
     this.classTypeNegated = classTypeNegated;
     this.classType = classType;
-    assert classType != null;
     this.classNames = classNames;
     this.inheritanceAnnotations = inheritanceAnnotations;
     this.inheritanceClassName = inheritanceClassName;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardClassType.java b/src/main/java/com/android/tools/r8/shaking/ProguardClassType.java
index de70c89..41fe36e 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardClassType.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardClassType.java
@@ -30,12 +30,6 @@
     public boolean matches(DexClass clazz) {
       return clazz.accessFlags.isInterface();
     }
-  },
-  UNSPECIFIED {
-    @Override
-    public boolean matches(DexClass clazz) {
-      return true;
-    }
   };
 
   @Override
@@ -45,8 +39,6 @@
       case CLASS: return "class";
       case ENUM: return "enum";
       case INTERFACE: return "interface";
-      case UNSPECIFIED:
-        return "";
       default:
         throw new Unreachable("Invalid proguard class type '" + this + "'");
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index 3eb32b9..e488088 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -70,7 +71,7 @@
     private boolean configurationDebugging = false;
     private boolean dontUseMixedCaseClassnames = false;
     private boolean protoShrinking = false;
-    private int maxRemovedAndroidLogLevel = -1;
+    private int maxRemovedAndroidLogLevel = MaximumRemovedAndroidLogLevelRule.NOT_SET;
 
     private Builder(DexItemFactory dexItemFactory, Reporter reporter) {
       this.dexItemFactory = dexItemFactory;
@@ -290,14 +291,13 @@
       protoShrinking = true;
     }
 
-    public int getMaxRemovedAndroidLogLevelOrDefault(int defaultValue) {
-      assert maxRemovedAndroidLogLevel == -1 || maxRemovedAndroidLogLevel >= 1;
-      return maxRemovedAndroidLogLevel >= 1 ? maxRemovedAndroidLogLevel : defaultValue;
+    public int getMaxRemovedAndroidLogLevel() {
+      return maxRemovedAndroidLogLevel;
     }
 
     public void joinMaxRemovedAndroidLogLevel(int maxRemovedAndroidLogLevel) {
-      assert maxRemovedAndroidLogLevel >= 1;
-      if (this.maxRemovedAndroidLogLevel == -1) {
+      assert maxRemovedAndroidLogLevel >= MaximumRemovedAndroidLogLevelRule.NONE;
+      if (this.maxRemovedAndroidLogLevel == MaximumRemovedAndroidLogLevelRule.NOT_SET) {
         this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
       } else {
         // If there are multiple -maximumremovedandroidloglevel rules we only allow removing logging
@@ -348,7 +348,7 @@
               configurationDebugging,
               dontUseMixedCaseClassnames,
               protoShrinking,
-              getMaxRemovedAndroidLogLevelOrDefault(1));
+              getMaxRemovedAndroidLogLevel());
 
       reporter.failIfPendingErrors();
 
@@ -667,6 +667,10 @@
     return maxRemovedAndroidLogLevel;
   }
 
+  public boolean hasMaximumRemovedAndroidLogLevelRules() {
+    return Iterables.any(rules, ProguardConfigurationRule::isMaximumRemovedAndroidLogLevelRule);
+  }
+
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 37cad6b..c1de766 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ThrowingAction;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.io.File;
@@ -473,14 +474,8 @@
         configurationBuilder.setConfigurationDebugging(true);
       } else if (acceptString("dontusemixedcaseclassnames")) {
         configurationBuilder.setDontUseMixedCaseClassnames(true);
-      } else if (acceptString("maximumremovedandroidloglevel")) {
-        skipWhitespace();
-        Integer maxRemovedAndroidLogLevel = acceptInteger();
-        if (maxRemovedAndroidLogLevel != null && maxRemovedAndroidLogLevel >= 1) {
-          configurationBuilder.joinMaxRemovedAndroidLogLevel(maxRemovedAndroidLogLevel);
-        } else {
-          throw parseError("Expected integer greater than or equal to 1", getPosition());
-        }
+      } else if (parseMaximumRemovedAndroidLogLevelRule(optionStart)) {
+        return true;
       } else {
         String unknownOption = acceptString();
         String devMessage = "";
@@ -976,6 +971,47 @@
           "Expecting '-keep' option after '-if' option.", origin, getPosition(optionStart)));
     }
 
+    private boolean parseMaximumRemovedAndroidLogLevelRule(Position start)
+        throws ProguardRuleParserException {
+      if (acceptString("maximumremovedandroidloglevel")) {
+        skipWhitespace();
+        // First parse the mandatory log level int.
+        Integer maxRemovedAndroidLogLevel = acceptInteger();
+        if (maxRemovedAndroidLogLevel == null
+            || maxRemovedAndroidLogLevel < MaximumRemovedAndroidLogLevelRule.NONE) {
+          throw parseError("Expected integer greater than or equal to 1", getPosition());
+        }
+        MaximumRemovedAndroidLogLevelRule.Builder builder =
+            MaximumRemovedAndroidLogLevelRule.builder()
+                .setMaxRemovedAndroidLogLevel(maxRemovedAndroidLogLevel)
+                .setOrigin(origin)
+                .setStart(start);
+        // Check if we can parse any class annotations or flag.
+        if (parseClassAnnotationsAndFlags(builder)) {
+          // Parse the remainder of the class specification.
+          parseClassSpecFromClassTypeInclusive(builder, false);
+        } else {
+          // Otherwise check if we can parse a class name.
+          parseClassType(
+              builder,
+              // Parse the remainder of the class specification.
+              () -> parseClassSpecFromClassNameInclusive(builder, false),
+              // In case of an error, move position back to the place we expected an (optional)
+              // class type.
+              expectedClassTypeStart -> position = expectedClassTypeStart.getOffsetAsInt());
+        }
+        if (builder.hasClassType()) {
+          Position end = getPosition();
+          configurationBuilder.addRule(
+              builder.setEnd(end).setSource(getSourceSnippet(contents, start, end)).build());
+        } else {
+          configurationBuilder.joinMaxRemovedAndroidLogLevel(maxRemovedAndroidLogLevel);
+        }
+        return true;
+      }
+      return false;
+    }
+
     private ReprocessClassInitializerRule parseReprocessClassInitializerRule(
         ReprocessClassInitializerRule.Type type, Position start)
         throws ProguardRuleParserException {
@@ -1048,7 +1084,27 @@
         boolean allowValueSpecification)
         throws ProguardRuleParserException {
       parseClassAnnotationsAndFlags(builder);
-      parseClassType(builder);
+      parseClassSpecFromClassTypeInclusive(builder, allowValueSpecification);
+    }
+
+    private <
+            C extends ProguardClassSpecification,
+            B extends ProguardClassSpecification.Builder<C, B>>
+        void parseClassSpecFromClassTypeInclusive(
+            ProguardClassSpecification.Builder<C, B> builder, boolean allowValueSpecification)
+            throws ProguardRuleParserException {
+      parseClassType(
+          builder,
+          () -> parseClassSpecFromClassNameInclusive(builder, allowValueSpecification),
+          this::parseClassTypeErrorHandler);
+    }
+
+    private <
+            C extends ProguardClassSpecification,
+            B extends ProguardClassSpecification.Builder<C, B>>
+        void parseClassSpecFromClassNameInclusive(
+            ProguardClassSpecification.Builder<C, B> builder, boolean allowValueSpecification)
+            throws ProguardRuleParserException {
       builder.setClassNames(parseClassNames());
       parseInheritance(builder);
       parseMemberRules(builder, allowValueSpecification);
@@ -1156,14 +1212,17 @@
       return acceptChar('!');
     }
 
-    private void parseClassAnnotationsAndFlags(ProguardClassSpecification.Builder<?, ?> builder)
+    /** Returns true if any class annotations or flags were parsed. */
+    private boolean parseClassAnnotationsAndFlags(ProguardClassSpecification.Builder<?, ?> builder)
         throws ProguardRuleParserException {
       // We allow interleaving the class annotations and class flags for compatibility with
       // Proguard, although this should not be possible according to the grammar.
+      boolean changed = false;
       while (true) {
         ProguardTypeMatcher annotation = parseAnnotation();
         if (annotation != null) {
           builder.addClassAnnotation(annotation);
+          changed = true;
         } else {
           int start = position;
           ProguardAccessFlags flags =
@@ -1173,10 +1232,13 @@
           skipWhitespace();
           if (acceptString("public")) {
             flags.setPublic();
+            changed = true;
           } else if (acceptString("final")) {
             flags.setFinal();
+            changed = true;
           } else if (acceptString("abstract")) {
             flags.setAbstract();
+            changed = true;
           } else {
             // Undo reading the ! in case there is no modifier following.
             position = start;
@@ -1184,6 +1246,7 @@
           }
         }
       }
+      return changed;
     }
 
     private StringDiagnostic parseClassTypeUnexpected(Origin origin, TextPosition start) {
@@ -1191,7 +1254,11 @@
           "Expected [!]interface|@interface|class|enum", origin, getPosition(start));
     }
 
-    private void parseClassType(ProguardClassSpecification.Builder<?, ?> builder) {
+    private <E extends Throwable> void parseClassType(
+        ProguardClassSpecification.Builder<?, ?> builder,
+        ThrowingAction<E> continuation,
+        Consumer<TextPosition> errorHandler)
+        throws E {
       skipWhitespace();
       TextPosition start = getPosition();
       if (acceptChar('!')) {
@@ -1202,7 +1269,8 @@
         if (acceptString("interface")) {
           builder.setClassType(ProguardClassType.ANNOTATION_INTERFACE);
         } else {
-          throw reporter.fatalError(parseClassTypeUnexpected(origin, start));
+          errorHandler.accept(start);
+          return;
         }
       } else if (acceptString("interface")) {
         builder.setClassType(ProguardClassType.INTERFACE);
@@ -1211,8 +1279,14 @@
       } else if (acceptString("enum")) {
         builder.setClassType(ProguardClassType.ENUM);
       } else {
-        throw reporter.fatalError(parseClassTypeUnexpected(origin, start));
+        errorHandler.accept(start);
+        return;
       }
+      continuation.execute();
+    }
+
+    private void parseClassTypeErrorHandler(TextPosition start) {
+      throw reporter.fatalError(parseClassTypeUnexpected(origin, start));
     }
 
     private void parseInheritance(
@@ -2291,7 +2365,7 @@
         || length <= 0) {
       return null;
     } else {
-      return source.substring((int) start.getOffset(), (int) end.getOffset());
+      return source.substring(start.getOffsetAsInt(), end.getOffsetAsInt());
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
index b2c90e6..b80edf0 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -68,6 +68,14 @@
     used = true;
   }
 
+  public boolean isMaximumRemovedAndroidLogLevelRule() {
+    return false;
+  }
+
+  public MaximumRemovedAndroidLogLevelRule asMaximumRemovedAndroidLogLevelRule() {
+    return null;
+  }
+
   public boolean isProguardCheckDiscardRule() {
     return false;
   }
@@ -168,6 +176,10 @@
 
   abstract String typeString();
 
+  String typeSuffix() {
+    return null;
+  }
+
   String modifierString() {
     return null;
   }
@@ -223,6 +235,7 @@
     builder.append("-");
     builder.append(typeString());
     StringUtils.appendNonEmpty(builder, ",", modifierString(), null);
+    StringUtils.appendNonEmpty(builder, " ", typeSuffix(), null);
     builder.append(' ');
     super.append(builder);
     return builder;
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index d60e464..2b6c654 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -299,7 +299,8 @@
           || rule instanceof NoReturnTypeStrengtheningRule
           || rule instanceof KeepUnusedArgumentRule
           || rule instanceof ReprocessMethodRule
-          || rule instanceof WhyAreYouNotInliningRule) {
+          || rule instanceof WhyAreYouNotInliningRule
+          || rule.isMaximumRemovedAndroidLogLevelRule()) {
         markMatchingMethods(clazz, memberKeepRules, rule, null, ifRule);
       } else if (rule instanceof ClassInlineRule
           || rule instanceof NoUnusedInterfaceRemovalRule
@@ -1359,6 +1360,9 @@
               .disallowOptimization();
         }
         context.markAsUsed();
+      } else if (context.isMaximumRemovedAndroidLogLevelRule()) {
+        evaluateMaximumRemovedAndroidLogLevelRule(
+            item, context.asMaximumRemovedAndroidLogLevelRule());
       } else {
         throw new Unreachable();
       }
@@ -1690,6 +1694,14 @@
       context.markAsUsed();
     }
 
+    private void evaluateMaximumRemovedAndroidLogLevelRule(
+        Definition item, MaximumRemovedAndroidLogLevelRule context) {
+      assert item.isProgramMethod();
+      feedback.joinMaxRemovedAndroidLogLevel(
+          item.asProgramMethod(), context.getMaxRemovedAndroidLogLevel());
+      context.markAsUsed();
+    }
+
     private boolean isInterfaceMethodNeedingDesugaring(ProgramDefinition item) {
       return !isMainDexRootSetBuilder()
           && options.isInterfaceMethodDesugaringEnabled()
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 2397d02..9b437c1 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -1957,7 +1957,7 @@
 
     @Override
     protected MethodLookupResult internalDescribeLookupMethod(
-        MethodLookupResult previous, DexMethod context) {
+        MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
       // This is unreachable since we override the implementation of lookupMethod() above.
       throw new Unreachable();
     }
@@ -1980,7 +1980,7 @@
     }
 
     @Override
-    public boolean isContextFreeForMethods() {
+    public boolean isContextFreeForMethods(GraphLens codeLens) {
       return true;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
index 0b4a990..3aeaad0 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
@@ -108,8 +109,8 @@
 
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
-      MethodLookupResult previous, DexMethod context) {
-    assert context != null || verifyIsContextFreeForMethod(previous.getReference());
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
+    assert context != null || verifyIsContextFreeForMethod(previous.getReference(), codeLens);
     assert context == null || previous.getType() != null;
     if (previous.getType() == InvokeType.SUPER && !mergedMethods.contains(context)) {
       Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
@@ -166,14 +167,21 @@
   }
 
   @Override
-  public boolean isContextFreeForMethods() {
-    return contextualVirtualToDirectMethodMaps.isEmpty() && getPrevious().isContextFreeForMethods();
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    return contextualVirtualToDirectMethodMaps.isEmpty()
+        && getPrevious().isContextFreeForMethods(codeLens);
   }
 
   @Override
-  public boolean verifyIsContextFreeForMethod(DexMethod method) {
-    assert getPrevious().verifyIsContextFreeForMethod(method);
-    DexMethod previous = getPrevious().lookupMethod(method);
+  public boolean verifyIsContextFreeForMethod(DexMethod method, GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    assert getPrevious().verifyIsContextFreeForMethod(method, codeLens);
+    DexMethod previous = getPrevious().lookupMethod(method, null, null, codeLens).getReference();
     assert contextualVirtualToDirectMethodMaps.values().stream()
         .noneMatch(virtualToDirectMethodMap -> virtualToDirectMethodMap.containsKey(previous));
     return true;
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 3e5104a..d90c8e5 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -10,6 +10,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.function.IntFunction;
 import java.util.function.IntPredicate;
 import java.util.function.Predicate;
 
@@ -49,6 +50,13 @@
     return results;
   }
 
+  public static <T> T[] initialize(T[] array, IntFunction<T> fn) {
+    for (int i = 0; i < array.length; i++) {
+      array[i] = fn.apply(i);
+    }
+    return array;
+  }
+
   public static <T> boolean isEmpty(T[] array) {
     return array.length == 0;
   }
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 f48a387..67f4fda 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -64,7 +64,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.desugar.TypeRewriter;
-import com.android.tools.r8.ir.desugar.TypeRewriter.MachineDesugarPrefixRewritingMapper;
+import com.android.tools.r8.ir.desugar.TypeRewriter.MachineTypeRewriter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineDesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.nest.Nest;
@@ -104,7 +104,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -1075,14 +1074,39 @@
     machineDesugaredLibrarySpecification = MachineDesugaredLibrarySpecification.empty();
   }
 
+  public void configureDesugaredLibrary(
+      DesugaredLibrarySpecification desugaredLibrarySpecification, String synthesizedClassPrefix) {
+    assert synthesizedClassPrefix != null;
+    assert desugaredLibrarySpecification != null;
+    String prefix =
+        synthesizedClassPrefix.isEmpty()
+            ? System.getProperty("com.android.tools.r8.synthesizedClassPrefix", "")
+            : synthesizedClassPrefix;
+    String postPrefix = System.getProperty("com.android.tools.r8.desugaredLibraryPostPrefix", null);
+    setDesugaredLibrarySpecification(desugaredLibrarySpecification, postPrefix);
+    String post =
+        postPrefix == null ? "" : DescriptorUtils.getPackageBinaryNameFromJavaType(postPrefix);
+    this.synthesizedClassPrefix = prefix.isEmpty() ? "" : prefix + post;
+  }
+
   public void setDesugaredLibrarySpecification(DesugaredLibrarySpecification specification) {
+    setDesugaredLibrarySpecification(specification, null);
+  }
+
+  private void setDesugaredLibrarySpecification(
+      DesugaredLibrarySpecification specification, String postPrefix) {
     if (specification.isEmpty()) {
       return;
     }
     loadMachineDesugaredLibrarySpecification =
-        (timing, app) ->
-            machineDesugaredLibrarySpecification =
-                specification.toMachineSpecification(app, timing);
+        (timing, app) -> {
+          MachineDesugaredLibrarySpecification machineSpec =
+              specification.toMachineSpecification(app, timing);
+          machineDesugaredLibrarySpecification =
+              postPrefix != null
+                  ? machineSpec.withPostPrefix(dexItemFactory(), postPrefix)
+                  : machineSpec;
+        };
   }
 
   private ThrowingBiConsumer<Timing, DexApplication, IOException>
@@ -1104,7 +1128,7 @@
 
   public TypeRewriter getTypeRewriter() {
     return machineDesugaredLibrarySpecification.requiresTypeRewriting()
-        ? new MachineDesugarPrefixRewritingMapper(machineDesugaredLibrarySpecification)
+        ? new MachineTypeRewriter(machineDesugaredLibrarySpecification)
         : TypeRewriter.empty();
   }
 
@@ -2172,7 +2196,6 @@
     public int basicBlockMuncherIterationLimit = NO_LIMIT;
     public boolean dontReportFailingCheckDiscarded = false;
     public boolean disableRecordApplicationReaderMap = false;
-    public PrintStream whyAreYouNotInliningConsumer = System.out;
     public boolean trackDesugaredAPIConversions =
         System.getProperty("com.android.tools.r8.trackDesugaredAPIConversions") != null;
     public boolean enumUnboxingRewriteJavaCGeneratedMethod = false;
diff --git a/src/main/java/com/android/tools/r8/utils/SerializationUtils.java b/src/main/java/com/android/tools/r8/utils/SerializationUtils.java
new file mode 100644
index 0000000..e400b17
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/SerializationUtils.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2023, 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 java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class SerializationUtils {
+
+  private static final byte ZERO_BYTE = (byte) 0;
+
+  public static byte getZeroByte() {
+    return ZERO_BYTE;
+  }
+
+  public static void writeUTFOfIntSize(DataOutputStream dataOutputStream, String string)
+      throws IOException {
+    // Similar to dataOutputStream.writeUTF except it uses an int for length in bytes.
+    byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
+    dataOutputStream.writeInt(bytes.length);
+    dataOutputStream.write(bytes);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java b/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java
index 5bc7e56..81578f8 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
+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.ProgramMethod;
@@ -166,7 +167,9 @@
       DexMethod rewrittenMethod =
           appView.graphLens().getRenamedMethodSignature(method, appliedGraphLens);
       DexProgramClass holder = appView.definitionForHolder(rewrittenMethod).asProgramClass();
-      result.createAndAdd(holder, holder.lookupMethod(rewrittenMethod));
+      DexEncodedMethod definition = holder.lookupMethod(rewrittenMethod);
+      assert definition != null;
+      result.createAndAdd(holder, definition);
     }
     return result;
   }
diff --git a/src/test/examplesJava17/enumStatic/EnumStaticMain.java b/src/test/examplesJava17/enumStatic/EnumStaticMain.java
new file mode 100644
index 0000000..6e79499
--- /dev/null
+++ b/src/test/examplesJava17/enumStatic/EnumStaticMain.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2023, 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 enumStatic;
+
+public class EnumStaticMain {
+
+  enum EnumStatic {
+    A,
+    B {
+      static int i = 17;
+
+      static void print() {
+        System.out.println("-" + i);
+      }
+
+      public void virtualPrint() {
+        print();
+      }
+    };
+
+    public void virtualPrint() {}
+  }
+
+  public static void main(String[] args) throws Throwable {
+    EnumStatic.B.virtualPrint();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
index 5bb4d63..95dd9bd 100644
--- a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -46,16 +46,6 @@
   }
 
   @Test
-  public void testArrayAccess() throws Exception {
-    runTest("arrayaccess.ArrayAccess");
-  }
-
-  @Test
-  public void testBArray() throws Exception {
-    runTest("barray.BArray");
-  }
-
-  @Test
   public void testBridgeMethod() throws Exception {
     runTest("bridge.BridgeMethod");
   }
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index 49ce60e..f615502 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.debug.CfDebugTestConfig;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -65,6 +66,11 @@
     return new JvmTestRunResult(builder.build(), runtime, result, getState());
   }
 
+  public CfDebugTestConfig debugConfig(TestRuntime runtime) {
+    assert runtime.isCf();
+    return new CfDebugTestConfig(runtime.asCf(), classpath);
+  }
+
   @Override
   public JvmTestBuilder addLibraryFiles(Collection<Path> files) {
     return addRunClasspathFiles(files);
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index 152992d..a98ad79 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -61,6 +61,22 @@
     parameters.assertNoneRuntime();
   }
 
+  static class A {}
+
+  static class B {}
+
+  private Path getJarWithA() throws Exception {
+    Path jar = temp.newFolder().toPath().resolve("out.jar");
+    writeClassesToJar(jar, A.class);
+    return jar;
+  }
+
+  private Path getJarWithB() throws Exception {
+    Path jar = temp.newFolder().toPath().resolve("out.jar");
+    writeClassesToJar(jar, B.class);
+    return jar;
+  }
+
   @Test(expected = CompilationFailedException.class)
   public void emptyBuilder() throws Throwable {
     // The builder must have a program consumer.
@@ -120,14 +136,14 @@
   @Test
   public void defaultOutIsCwd() throws Throwable {
     Path working = temp.getRoot().toPath();
-    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
+    Path input = getJarWithA();
     Path library = ToolHelper.getDefaultAndroidJar();
     Path output = working.resolve("classes.dex");
     assertFalse(Files.exists(output));
     ProcessResult result =
         ToolHelper.forkR8(
             working,
-            input.toString(),
+            input.toAbsolutePath().toString(),
             "--lib",
             library.toAbsolutePath().toString(),
             "--no-tree-shaking");
@@ -138,8 +154,8 @@
   @Test
   public void passFeatureSplit() throws Throwable {
     Path working = temp.getRoot().toPath();
-    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
-    Path inputFeature = Paths.get(EXAMPLES_BUILD_DIR, "arrayaccess.jar").toAbsolutePath();
+    Path input = getJarWithA();
+    Path inputFeature = getJarWithB();
     Path library = ToolHelper.getDefaultAndroidJar();
     Path output = working.resolve("classes.dex");
     Path featureOutput = working.resolve("feature.zip");
@@ -148,7 +164,7 @@
     ProcessResult result =
         ToolHelper.forkR8(
             working,
-            input.toString(),
+            input.toAbsolutePath().toString(),
             "--lib",
             library.toAbsolutePath().toString(),
             "--feature",
@@ -163,8 +179,8 @@
   @Test
   public void featureOnlyOneArgument() throws Throwable {
     Path working = temp.getRoot().toPath();
-    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
-    Path inputFeature = Paths.get(EXAMPLES_BUILD_DIR, "arrayaccess.jar").toAbsolutePath();
+    Path input = getJarWithA();
+    Path inputFeature = getJarWithB();
     Path library = ToolHelper.getDefaultAndroidJar();
     Path output = working.resolve("classes.dex");
     assertFalse(Files.exists(output));
@@ -185,7 +201,7 @@
   public void flagsFile() throws Throwable {
     Path working = temp.getRoot().toPath();
     Path library = ToolHelper.getDefaultAndroidJar();
-    Path input = Paths.get(EXAMPLES_BUILD_DIR + "/arithmetic.jar").toAbsolutePath();
+    Path input = getJarWithA();
     Path output = working.resolve("output.zip");
     Path flagsFile = working.resolve("flags.txt");
     FileUtils.writeTextFile(
@@ -197,7 +213,7 @@
         "--lib",
         library.toAbsolutePath().toString(),
         "--no-tree-shaking",
-        input.toString());
+        input.toAbsolutePath().toString());
     assertEquals(0, ToolHelper.forkR8(working, "@flags.txt").exitCode);
     assertTrue(Files.exists(output));
     Collection<Marker> markers = ExtractMarkerUtils.extractMarkersFromFile(output);
@@ -226,7 +242,7 @@
     Path working = temp.getRoot().toPath();
     Path flagsFile = working.resolve("flags.txt");
     Path recursiveFlagsFile = working.resolve("recursive_flags.txt");
-    Path input = Paths.get(EXAMPLES_BUILD_DIR + "/arithmetic.jar").toAbsolutePath();
+    Path input = getJarWithA();
     FileUtils.writeTextFile(recursiveFlagsFile, "--output", "output.zip");
     FileUtils.writeTextFile(
         flagsFile, "--min-api", "24", input.toString(), "@" + recursiveFlagsFile);
@@ -386,7 +402,7 @@
       Files.createFile(file);
       assertTrue(Files.exists(file));
     }
-    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
+    Path input = getJarWithA();
     ProcessResult result =
         ToolHelper.forkR8(
             Paths.get("."),
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 45122d4..1cfd1a1 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -26,8 +26,6 @@
   public static Collection<String[]> data() {
     String[] tests = {
         "arithmetic.Arithmetic",
-        "arrayaccess.ArrayAccess",
-        "barray.BArray",
         "bridge.BridgeMethod",
         "catchhandleroverlap.CatchHandlerOverlap",
         "cse.CommonSubexpressionElimination",
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTestBase.java b/src/test/java/com/android/tools/r8/R8RunExamplesTestBase.java
index 99fedb6..a8edf73 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTestBase.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTestBase.java
@@ -102,11 +102,6 @@
   public Path getOriginalJarFile(String postFix) {
     return Paths.get(getExampleDir(), pkg + postFix + JAR_EXTENSION);
   }
-
-  private Path getOriginalDexFile() {
-    return Paths.get(getExampleDir(), pkg, ToolHelper.DEFAULT_DEX_FILENAME);
-  }
-
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 4adcef9..f3fc72a 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -58,6 +58,7 @@
 import com.android.tools.r8.shaking.NoHorizontalClassMergingRule;
 import com.android.tools.r8.shaking.NoVerticalClassMergingRule;
 import com.android.tools.r8.shaking.ProguardClassNameList;
+import com.android.tools.r8.shaking.ProguardClassType;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
@@ -910,6 +911,7 @@
     Builder keepRuleBuilder = ProguardKeepRule.builder();
     keepRuleBuilder.setSource("buildKeepRuleForClass " + clazz.getTypeName());
     keepRuleBuilder.setType(ProguardKeepRuleType.KEEP);
+    keepRuleBuilder.setClassType(ProguardClassType.CLASS);
     keepRuleBuilder.setClassNames(
         ProguardClassNameList.singletonList(
             ProguardTypeMatcher.create(
@@ -922,6 +924,7 @@
     Builder keepRuleBuilder = ProguardKeepRule.builder();
     keepRuleBuilder.setSource("buildKeepRuleForClassAndMethods " + clazz.getTypeName());
     keepRuleBuilder.setType(ProguardKeepRuleType.KEEP);
+    keepRuleBuilder.setClassType(ProguardClassType.CLASS);
     keepRuleBuilder.setClassNames(
         ProguardClassNameList.singletonList(
             ProguardTypeMatcher.create(
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index b8bdbae..dfdfa9d 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -660,9 +660,7 @@
             .add(out.toString())
             .build();
     Consumer<ArtCommandBuilder> commandConsumer =
-        withArt6Plus64BitsLib
-                && vm.getVersion().isNewerThanOrEqual(DexVm.Version.V6_0_1)
-                && !ToolHelper.force32BitArt()
+        withArt6Plus64BitsLib && vm.getVersion().isNewerThanOrEqual(DexVm.Version.V6_0_1)
             ? builder -> builder.appendArtOption("--64")
             : builder -> {};
     commandConsumer =
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 206a8ab..1c44201 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -245,12 +245,16 @@
   }
 
   public TestParameters assumeR8TestParameters() {
+    assumeTrue(isR8TestParameters());
+    return this;
+  }
+
+  public boolean isR8TestParameters() {
     assertFalse(
-        "No need to use assumeR8TestParameters() when not using api levels for CF",
+        "No need to use is/assumeR8TestParameters() when not using api levels for CF",
         isCfRuntime() && apiLevel == null);
     assertTrue(apiLevel != null || representativeApiLevelForRuntime);
-    assumeTrue(isDexRuntime() || representativeApiLevelForRuntime);
-    return this;
+    return isDexRuntime() || representativeApiLevelForRuntime;
   }
 
   public TestParameters assumeRuntimeTestParameters() {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 404c51d..8479714 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -488,8 +488,9 @@
         result.add("/bin/bash");
       }
       result.add(getExecutable());
-      result.addAll(getExecutableArguments());
-      result.addAll(options);
+      for (String option : options) {
+        result.add(option);
+      }
       for (Map.Entry<String, String> entry : systemProperties.entrySet()) {
         StringBuilder builder = new StringBuilder("-D");
         builder.append(entry.getKey());
@@ -507,7 +508,9 @@
       if (mainClass != null) {
         result.add(mainClass);
       }
-      result.addAll(programArguments);
+      for (String argument : programArguments) {
+        result.add(argument);
+      }
       return result;
     }
 
@@ -526,8 +529,6 @@
     protected abstract boolean shouldUseDocker();
 
     protected abstract String getExecutable();
-
-    protected abstract List<String> getExecutableArguments();
   }
 
   public static class ArtCommandBuilder extends CommandBuilder {
@@ -562,11 +563,6 @@
       return version != null ? getArtBinary(version) : getArtBinary();
     }
 
-    @Override
-    protected List<String> getExecutableArguments() {
-      return force32BitArt() ? ImmutableList.of("--32") : ImmutableList.of();
-    }
-
     public boolean isForDevice() {
       return version.getKind() == Kind.TARGET;
     }
@@ -1852,10 +1848,6 @@
     return new ProcessResult(passed ? 0 : -1, "", stdErr);
   }
 
-  public static boolean force32BitArt() {
-    return System.getProperty("force_32_bit_art") != null;
-  }
-
   public static boolean dealsWithGoldenFiles() {
     return compareAgaintsGoldenFiles() || generateGoldenFiles();
   }
diff --git a/src/test/java/com/android/tools/r8/d8/DexVersionTests.java b/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
index f413f98..9804829 100644
--- a/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
+++ b/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
@@ -12,23 +12,32 @@
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Set;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
-public class DexVersionTests {
+@RunWith(Parameterized.class)
+public class DexVersionTests extends TestBase {
 
-  private static final Path ARITHMETIC_JAR =
-      Paths.get(ToolHelper.EXAMPLES_BUILD_DIR + "/arithmetic.jar");
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestParameters.builder().withNoneRuntime().build();
+  }
 
-  private static final Path ARRAYACCESS_JAR =
-      Paths.get(ToolHelper.EXAMPLES_BUILD_DIR + "/arrayaccess.jar");
+  public DexVersionTests(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
 
   @Rule public TemporaryFolder defaultApiFolder1 = ToolHelper.getTemporaryFolderForTest();
   @Rule public TemporaryFolder defaultApiFolder2 = ToolHelper.getTemporaryFolderForTest();
@@ -37,9 +46,15 @@
   @Rule public TemporaryFolder androidNApiFolder1 = ToolHelper.getTemporaryFolderForTest();
   @Rule public TemporaryFolder androidNApiFolder2 = ToolHelper.getTemporaryFolderForTest();
 
+  static class Input1 {}
+
+  static class Input2 {}
+
   @Before
   public void compileVersions() throws Exception {
-    D8Command.Builder arrayAccessBuilder = D8Command.builder().addProgramFiles(ARRAYACCESS_JAR);
+    Path inputJar1 = getStaticTemp().newFolder().toPath().resolve("input1.jar");
+    writeClassesToJar(inputJar1, Input1.class);
+    D8Command.Builder arrayAccessBuilder = D8Command.builder().addProgramFiles(inputJar1);
     D8.run(
         arrayAccessBuilder
             .setOutput(defaultApiFolder1.getRoot().toPath(), OutputMode.DexIndexed)
@@ -55,7 +70,9 @@
             .setMinApiLevel(AndroidApiLevel.N.getLevel())
             .build());
 
-    D8Command.Builder arithmeticBuilder = D8Command.builder().addProgramFiles(ARITHMETIC_JAR);
+    Path inputJar2 = getStaticTemp().newFolder().toPath().resolve("input2.jar");
+    writeClassesToJar(inputJar2, Input2.class);
+    D8Command.Builder arithmeticBuilder = D8Command.builder().addProgramFiles(inputJar2);
     D8.run(
         arithmeticBuilder
             .setOutput(defaultApiFolder2.getRoot().toPath(), OutputMode.DexIndexed)
diff --git a/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java b/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
index 426da42..cfd87f6 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
@@ -236,7 +236,9 @@
 
   public static String prettyPrintState(DebuggeeState state, PrintOptions options) {
     StringBuilder builder = new StringBuilder();
-    if (!options.printStack) {
+    if (state == null) {
+      builder.append("null state\n");
+    } else if (!options.printStack) {
       builder.append(prettyPrintFrame(state, options));
     } else {
       for (int i = 0; i < state.getFrameDepth(); i++) {
diff --git a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
index e35ce1f..76f9758 100644
--- a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
@@ -60,16 +60,6 @@
   }
 
   @Test
-  public void testArrayAccess() throws Exception {
-    testDebugging("arrayaccess", "ArrayAccess");
-  }
-
-  @Test
-  public void testBArray() throws Exception {
-    testDebugging("barray", "BArray");
-  }
-
-  @Test
   public void testBridgeMethod() throws Exception {
     testDebugging("bridge", "BridgeMethod");
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/SimpleStreamPostPrefixTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/SimpleStreamPostPrefixTest.java
new file mode 100644
index 0000000..3515f94
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/SimpleStreamPostPrefixTest.java
@@ -0,0 +1,120 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8Jdk11;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SimpleStreamPostPrefixTest extends DesugaredLibraryTestBase {
+
+  private static final String EXPECTED_RESULT = StringUtils.lines("3");
+
+  private final TestParameters parameters;
+  private final CompilationSpecification compilationSpecification;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        getJdk8Jdk11(),
+        DEFAULT_SPECIFICATIONS);
+  }
+
+  public SimpleStreamPostPrefixTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.compilationSpecification = compilationSpecification;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+  }
+
+  @Test
+  public void testSimpleStreamPostPrefix() throws Throwable {
+    Assume.assumeTrue(libraryDesugaringSpecification.hasEmulatedInterfaceDesugaring(parameters));
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Executor.class)
+        .setL8PostPrefix("j$$.")
+        .compile()
+        .inspectL8(this::allRewritten)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  private void allRewritten(CodeInspector codeInspector) {
+    assertTrue(
+        codeInspector.allClasses().stream()
+            .allMatch(c -> c.toString().startsWith("j$.j$$.") || c.toString().startsWith("java.")));
+    Map<InstructionSubject, FoundMethodSubject> errors = new IdentityHashMap<>();
+    codeInspector.forAllClasses(
+        clazz ->
+            clazz.forAllMethods(
+                m -> {
+                  if (m.hasCode()) {
+                    for (InstructionSubject instruction : m.instructions()) {
+                      if (instruction.isInvoke()) {
+                        if (!isValidJ$$Type(instruction.getMethod().getHolderType())) {
+                          errors.put(instruction, m);
+                        }
+                        for (DexType referencedType :
+                            instruction.getMethod().getReferencedTypes()) {
+                          if (!isValidJ$$Type(referencedType)) {
+                            errors.put(instruction, m);
+                          }
+                        }
+                      } else if (instruction.isFieldAccess()) {
+                        if (!isValidJ$$Type(instruction.getField().getHolderType())) {
+                          errors.put(instruction, m);
+                        }
+                        if (!isValidJ$$Type(instruction.getField().getType())) {
+                          errors.put(instruction, m);
+                        }
+                      }
+                    }
+                  }
+                }));
+    assertTrue("Invalid invokes: " + errors, errors.isEmpty());
+  }
+
+  private boolean isValidJ$$Type(DexType type) {
+    return !type.toString().startsWith("j$.") || type.toString().startsWith("j$.j$$.");
+  }
+
+  @SuppressWarnings("unchecked")
+  static class Executor {
+
+    public static void main(String[] args) {
+      ArrayList<Integer> integers = new ArrayList<>();
+      integers.add(1);
+      integers.add(2);
+      integers.add(3);
+      List<Integer> collectedList = integers.stream().map(i -> i + 3).collect(Collectors.toList());
+      System.out.println(collectedList.size());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
index ee7cde7..2cf1e98 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
@@ -57,10 +57,10 @@
   private Consumer<InternalOptions> l8OptionModifier = ConsumerUtils.emptyConsumer();
   private boolean l8FinalPrefixVerification = true;
   private boolean overrideDefaultLibraryFiles = false;
-
   private CustomLibrarySpecification customLibrarySpecification = null;
   private TestingKeepRuleConsumer keepRuleConsumer = null;
   private List<ExternalArtProfile> l8ResidualArtProfiles = new ArrayList<>();
+  private boolean managedPostPrefix = false;
 
   public DesugaredLibraryTestBuilder(
       T test,
@@ -171,6 +171,12 @@
     return this;
   }
 
+  public DesugaredLibraryTestBuilder<T> setL8PostPrefix(String postPrefix) {
+    System.setProperty("com.android.tools.r8.desugaredLibraryPostPrefix", postPrefix);
+    this.managedPostPrefix = true;
+    return this;
+  }
+
   public DesugaredLibraryTestBuilder<T> ignoreL8FinalPrefixVerification() {
     l8FinalPrefixVerification = false;
     return this;
@@ -371,6 +377,9 @@
       throws CompilationFailedException, IOException, ExecutionException {
     L8TestCompileResult l8Compile = compileDesugaredLibrary(compile, keepRuleConsumer);
     D8TestCompileResult customLibCompile = compileCustomLib();
+    if (managedPostPrefix) {
+      System.clearProperty("com.android.tools.r8.desugaredLibraryPostPrefix");
+    }
     return new DesugaredLibraryTestCompileResult<>(
         test,
         compile,
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingArrayTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingArrayTest.java
index c3919c7..ceaf088 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingArrayTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingArrayTest.java
@@ -20,9 +20,12 @@
   private static final Class<?>[] TESTS = {
     Enum2DimArrayReadWrite.class,
     EnumArrayNullRead.class,
-    EnumArrayReadWrite.class,
+    EnumArrayPutNull.class,
     EnumArrayReadWriteNoEscape.class,
     EnumVarArgs.class,
+    EnumArrayPutNull.class,
+    Enum2DimArrayPutNull.class,
+    Enum2DimArrayPutNullArray.class
   };
 
   private final TestParameters parameters;
@@ -51,14 +54,19 @@
             .enableNeverClassInliningAnnotations()
             .addKeepRules(enumKeepRules.getKeepRules())
             .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+            .allowDiagnosticInfoMessages()
             .addEnumUnboxingInspector(
                 inspector ->
                     inspector.assertUnboxed(
                         Enum2DimArrayReadWrite.MyEnum.class,
                         EnumArrayNullRead.MyEnum.class,
-                        EnumArrayReadWrite.MyEnum.class,
+                        EnumArrayPutNull.MyEnum.class,
                         EnumArrayReadWriteNoEscape.MyEnum.class,
-                        EnumVarArgs.MyEnum.class))
+                        EnumVarArgs.MyEnum.class,
+                        EnumArrayPutNull.MyEnum.class,
+                        Enum2DimArrayPutNull.MyEnum.class,
+                        Enum2DimArrayPutNullArray.MyEnum.class))
             .setMinApi(parameters)
             .compile();
     for (Class<?> main : TESTS) {
@@ -113,6 +121,37 @@
     }
   }
 
+  static class EnumArrayPutNull {
+
+    public static void main(String[] args) {
+      MyEnum[] myEnums = getArray();
+      System.out.println(myEnums[1].ordinal());
+      System.out.println(1);
+      setNull(myEnums);
+      System.out.println(myEnums[0] == null);
+      System.out.println("true");
+    }
+
+    @NeverInline
+    public static void setNull(MyEnum[] myEnums) {
+      myEnums[0] = null;
+    }
+
+    @NeverInline
+    public static MyEnum[] getArray() {
+      MyEnum[] myEnums = new MyEnum[2];
+      myEnums[1] = MyEnum.B;
+      myEnums[0] = MyEnum.A;
+      return myEnums;
+    }
+
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B;
+    }
+  }
+
   static class EnumArrayReadWrite {
 
     public static void main(String[] args) {
@@ -193,4 +232,77 @@
       C;
     }
   }
+
+  static class Enum2DimArrayPutNull {
+
+    public static void main(String[] args) {
+      MyEnum[][] myEnums = getArray();
+      System.out.println(myEnums[1][1].ordinal());
+      System.out.println(1);
+      setNull(myEnums);
+      System.out.println(myEnums[0] == null);
+      System.out.println("true");
+    }
+
+    @NeverInline
+    public static void setNull(MyEnum[][] myEnums) {
+      myEnums[0] = null;
+    }
+
+    @NeverInline
+    public static MyEnum[][] getArray() {
+      MyEnum[][] myEnums = new MyEnum[2][2];
+      myEnums[0][1] = MyEnum.B;
+      myEnums[0][0] = MyEnum.A;
+      myEnums[1][1] = MyEnum.B;
+      myEnums[1][0] = MyEnum.A;
+      return myEnums;
+    }
+
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B;
+    }
+  }
+
+  static class Enum2DimArrayPutNullArray {
+
+    public static void main(String[] args) {
+      MyEnum[][] myEnums = getArray();
+      System.out.println(myEnums[1][1].ordinal());
+      System.out.println(1);
+      setNull(myEnums);
+      System.out.println(myEnums[0][0] == null);
+      System.out.println("true");
+    }
+
+    @NeverInline
+    public static void setNull(MyEnum[][] myEnums) {
+      MyEnum[] myEnums1 = new MyEnum[1];
+      setNull(myEnums1);
+      myEnums[0] = myEnums1;
+    }
+
+    @NeverInline
+    public static void setNull(MyEnum[] myEnums1) {
+      myEnums1[0] = null;
+    }
+
+    @NeverInline
+    public static MyEnum[][] getArray() {
+      MyEnum[][] myEnums = new MyEnum[2][2];
+      myEnums[0][1] = MyEnum.B;
+      myEnums[0][0] = MyEnum.A;
+      myEnums[1][1] = MyEnum.B;
+      myEnums[1][0] = MyEnum.A;
+      return myEnums;
+    }
+
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B;
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
index 4690144..420b2e9 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -61,7 +61,7 @@
     options.enableEnumSwitchMapRemoval = enumValueOptimization;
   }
 
-  static List<Object[]> enumUnboxingTestParameters() {
+  public static List<Object[]> enumUnboxingTestParameters() {
     return enumUnboxingTestParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build());
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingTest.java
new file mode 100644
index 0000000..56f3d9e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingTest.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2023, 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.enumunboxing.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class BasicEnumMergingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public BasicEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(BasicEnumMergingTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+        .addEnumUnboxingInspector(
+            inspector ->
+                inspector
+                    .assertUnboxed(
+                        EnumWithVirtualOverride.class,
+                        EnumWithVirtualOverrideAndPrivateMethod.class,
+                        EnumWithVirtualOverrideWide.class)
+                    .assertNotUnboxed(EnumWithVirtualOverrideAndPrivateField.class))
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a", "B", "a", "B", "a", "B", "A 1 1.0 A", "B");
+  }
+
+  enum EnumWithVirtualOverride {
+    A {
+      public void method() {
+        System.out.println("a");
+      }
+    },
+    B;
+
+    public void method() {
+      System.out.println(name());
+    }
+  }
+
+  enum EnumWithVirtualOverrideAndPrivateMethod {
+    A {
+      @NeverInline
+      private void privateMethod() {
+        System.out.println("a");
+      }
+
+      public void methodpm() {
+        privateMethod();
+      }
+    },
+    B;
+
+    public void methodpm() {
+      System.out.println(name());
+    }
+  }
+
+  enum EnumWithVirtualOverrideWide {
+    A {
+      public void methodpmw(long l1, double d2, EnumWithVirtualOverrideWide itself) {
+        System.out.println("A " + l1 + " " + d2 + " " + itself);
+      }
+    },
+    B;
+
+    public void methodpmw(long l1, double d2, EnumWithVirtualOverrideWide itself) {
+      System.out.println(name());
+    }
+  }
+
+  enum EnumWithVirtualOverrideAndPrivateField {
+    A {
+      private String a = "a";
+
+      public void methodpf() {
+        System.out.println(a);
+      }
+    },
+    B;
+
+    public void methodpf() {
+      System.out.println(name());
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      EnumWithVirtualOverrideAndPrivateMethod.A.methodpm();
+      EnumWithVirtualOverrideAndPrivateMethod.B.methodpm();
+      EnumWithVirtualOverrideAndPrivateField.A.methodpf();
+      EnumWithVirtualOverrideAndPrivateField.B.methodpf();
+      EnumWithVirtualOverride.A.method();
+      EnumWithVirtualOverride.B.method();
+      EnumWithVirtualOverrideWide.A.methodpmw(1L, 1.0, EnumWithVirtualOverrideWide.A);
+      EnumWithVirtualOverrideWide.B.methodpmw(1L, 1.0, EnumWithVirtualOverrideWide.A);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/StaticEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/StaticEnumMergingTest.java
new file mode 100644
index 0000000..1c1b779
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/StaticEnumMergingTest.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2023, 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.enumunboxing.enummerging;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StaticEnumMergingTest extends EnumUnboxingTestBase {
+
+  private static final Path JDK17_JAR =
+      Paths.get(ToolHelper.TESTS_BUILD_DIR, "examplesJava17").resolve("enumStatic" + JAR_EXTENSION);
+  private static final String EXPECTED_RESULT = StringUtils.lines("-17");
+  private static final String MAIN = "enumStatic.EnumStaticMain";
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public StaticEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(JDK17_JAR)
+        .addKeepMainRule(MAIN)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperEnumMergingTest.java
new file mode 100644
index 0000000..7a2b509
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperEnumMergingTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2023, 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.enumunboxing.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SuperEnumMergingTest extends EnumUnboxingTestBase {
+
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines(
+          "a", "top", "A", "A", "A", "> A", "subA", "=", "B", "B", "=", "c", "top", "C", "C", "C",
+          "> C", "subB");
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public SuperEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(SuperEnumMergingTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(EnumWithSuper.class))
+        .enableInliningAnnotations()
+        .enableMemberValuePropagationAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .allowUnusedProguardConfigurationRules()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  enum EnumWithSuper {
+    A {
+      @NeverInline
+      @Override
+      public void method() {
+        System.out.println("a");
+        super.top();
+        super.method();
+        System.out.println(super.name());
+        System.out.println(super.toString());
+        sub();
+      }
+
+      @NeverInline
+      public void sub() {
+        System.out.println("subA");
+      }
+    },
+    B,
+    C {
+      @NeverInline
+      @Override
+      public void method() {
+        System.out.println("c");
+        super.top();
+        super.method();
+        System.out.println(super.name());
+        System.out.println(super.toString());
+        sub();
+      }
+
+      @NeverInline
+      public void sub() {
+        System.out.println("subB");
+      }
+    };
+
+    @NeverInline
+    public void method() {
+      System.out.println(super.name());
+      System.out.println(super.toString());
+    }
+
+    @NeverInline
+    @Override
+    public String toString() {
+      return "> " + name();
+    }
+
+    @NeverInline
+    public void top() {
+      System.out.println("top");
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      EnumWithSuper.A.method();
+      System.out.println("=");
+      EnumWithSuper.B.method();
+      System.out.println("=");
+      EnumWithSuper.C.method();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperToStringEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperToStringEnumMergingTest.java
new file mode 100644
index 0000000..a88db9a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/SuperToStringEnumMergingTest.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2023, 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.enumunboxing.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SuperToStringEnumMergingTest extends EnumUnboxingTestBase {
+
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines("> A", "B", "! C", "> - A", "- B", "! - C");
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public SuperToStringEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(SuperToStringEnumMergingTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+        .addEnumUnboxingInspector(
+            inspector -> inspector.assertUnboxed(EnumToString.class, EnumToStringOverride.class))
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  enum EnumToString {
+    A {
+      @NeverInline
+      @Override
+      public String toString() {
+        return "> " + super.toString();
+      }
+    },
+    B,
+    C {
+      @NeverInline
+      @Override
+      public String toString() {
+        return "! " + super.toString();
+      }
+    };
+  }
+
+  enum EnumToStringOverride {
+    A {
+      @NeverInline
+      @Override
+      public String toString() {
+        return "> " + super.toString();
+      }
+    },
+    B,
+    C {
+      @NeverInline
+      @Override
+      public String toString() {
+        return "! " + super.toString();
+      }
+    };
+
+    @NeverInline
+    @Override
+    public String toString() {
+      return "- " + super.toString();
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(EnumToString.A.toString());
+      System.out.println(EnumToString.B.toString());
+      System.out.println(EnumToString.C.toString());
+      System.out.println(EnumToStringOverride.A.toString());
+      System.out.println(EnumToStringOverride.B.toString());
+      System.out.println(EnumToStringOverride.C.toString());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/examples/ExamplesTestBase.java b/src/test/java/com/android/tools/r8/examples/ExamplesTestBase.java
new file mode 100644
index 0000000..093702c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/ExamplesTestBase.java
@@ -0,0 +1,106 @@
+// Copyright (c) 2023, 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.examples;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.debug.CfDebugTestConfig;
+import com.android.tools.r8.debug.DebugStreamComparator;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestConfig;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+
+public abstract class ExamplesTestBase extends DebugTestBase {
+
+  public final TestParameters parameters;
+
+  public ExamplesTestBase(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  public abstract String getExpected();
+
+  public abstract Class<?> getMainClass();
+
+  public List<Class<?>> getTestClasses() {
+    return Collections.singletonList(getMainClass());
+  }
+
+  public void runTestDesugaring() throws Exception {
+    testForDesugaring(parameters)
+        .addProgramClasses(getTestClasses())
+        .run(parameters.getRuntime(), getMainClass())
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  public void runTestR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(getTestClasses())
+        .addKeepMainRule(getMainClass())
+        .run(parameters.getRuntime(), getMainClass())
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  @Test
+  public void runTestDebugComparator() throws Exception {
+    Assume.assumeFalse(ToolHelper.isWindows());
+    Assume.assumeFalse(
+        "Debugging not enabled in 12.0.0", parameters.isDexRuntimeVersion(Version.V12_0_0));
+    Assume.assumeTrue(
+        "Streaming on Dalvik DEX runtimes has some unknown interference issue",
+        parameters.isCfRuntime() || parameters.isDexRuntimeVersionNewerThanOrEqual(Version.V6_0_1));
+
+    String mainTypeName = getMainClass().getTypeName();
+    DebugStreamComparator comparator =
+        new DebugStreamComparator()
+            .add("JVM", streamDebugTest(getJvmConfig(), mainTypeName, ANDROID_FILTER))
+            .add("D8", streamDebugTest(getD8Config(), mainTypeName, ANDROID_FILTER))
+            .setFilter(
+                state -> state.getClassName().startsWith(getMainClass().getPackage().getName()));
+
+    // Only add R8 on the representative config.
+    if (parameters.isR8TestParameters()) {
+      comparator.add("R8", streamDebugTest(getR8Config(), mainTypeName, ANDROID_FILTER));
+    }
+
+    comparator.compare();
+  }
+
+  private CfDebugTestConfig getJvmConfig() throws IOException {
+    // We can't use `testForJvm` as we want to build the reference even for non-representative API.
+    CfRuntime cfRuntime =
+        parameters.isCfRuntime() ? parameters.asCfRuntime() : CfRuntime.getDefaultCfRuntime();
+    Path jar = temp.newFolder().toPath().resolve("out.jar");
+    writeClassesToJar(jar, getTestClasses());
+    return new CfDebugTestConfig(cfRuntime, Collections.singletonList(jar));
+  }
+
+  private DebugTestConfig getD8Config() throws CompilationFailedException {
+    return testForD8(parameters.getBackend())
+        .addProgramClasses(getTestClasses())
+        .setMinApi(parameters)
+        .compile()
+        .debugConfig(parameters.getRuntime());
+  }
+
+  private DebugTestConfig getR8Config() throws CompilationFailedException {
+    return testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(getTestClasses())
+        .addKeepMainRule(getMainClass())
+        .debug()
+        .compile()
+        .debugConfig(parameters.getRuntime());
+  }
+}
diff --git a/src/test/examples/arrayaccess/ArrayAccess.java b/src/test/java/com/android/tools/r8/examples/arrayaccess/ArrayAccess.java
similarity index 94%
rename from src/test/examples/arrayaccess/ArrayAccess.java
rename to src/test/java/com/android/tools/r8/examples/arrayaccess/ArrayAccess.java
index f73fd91..6ab0568 100644
--- a/src/test/examples/arrayaccess/ArrayAccess.java
+++ b/src/test/java/com/android/tools/r8/examples/arrayaccess/ArrayAccess.java
@@ -1,7 +1,7 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2023, 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 arrayaccess;
+package com.android.tools.r8.examples.arrayaccess;
 
 public class ArrayAccess {
 
@@ -73,4 +73,4 @@
     i += loadStoreArray(1, new Object[10]);
     System.out.println("37=" + i);
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/android/tools/r8/examples/arrayaccess/ArrayAccessTestRunner.java b/src/test/java/com/android/tools/r8/examples/arrayaccess/ArrayAccessTestRunner.java
new file mode 100644
index 0000000..ec0a4bf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/arrayaccess/ArrayAccessTestRunner.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, 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.examples.arrayaccess;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.examples.ExamplesTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ArrayAccessTestRunner extends ExamplesTestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().enableApiLevelsForCf().build();
+  }
+
+  public ArrayAccessTestRunner(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return ArrayAccess.class;
+  }
+
+  @Override
+  public String getExpected() {
+    return StringUtils.lines("37=37");
+  }
+
+  @Test
+  public void testDesugaring() throws Exception {
+    runTestDesugaring();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    runTestR8();
+  }
+
+  @Test
+  public void testDebug() throws Exception {
+    runTestDebugComparator();
+  }
+}
diff --git a/src/test/examples/barray/BArray.java b/src/test/java/com/android/tools/r8/examples/barray/BArray.java
similarity index 93%
rename from src/test/examples/barray/BArray.java
rename to src/test/java/com/android/tools/r8/examples/barray/BArray.java
index 26ac5f8..3a46079 100644
--- a/src/test/examples/barray/BArray.java
+++ b/src/test/java/com/android/tools/r8/examples/barray/BArray.java
@@ -1,7 +1,7 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2023, 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 barray;
+package com.android.tools.r8.examples.barray;
 
 public class BArray {
 
diff --git a/src/test/java/com/android/tools/r8/examples/barray/BArrayTestRunner.java b/src/test/java/com/android/tools/r8/examples/barray/BArrayTestRunner.java
new file mode 100644
index 0000000..2f0cc39
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/barray/BArrayTestRunner.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, 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.examples.barray;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.examples.ExamplesTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class BArrayTestRunner extends ExamplesTestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().enableApiLevelsForCf().build();
+  }
+
+  public BArrayTestRunner(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return BArray.class;
+  }
+
+  @Override
+  public String getExpected() {
+    return StringUtils.lines("null boolean: true", "null byte: 42", "boolean: true", "byte: 42");
+  }
+
+  @Test
+  public void testDesugaring() throws Exception {
+    runTestDesugaring();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    runTestR8();
+  }
+
+  @Test
+  public void testDebug() throws Exception {
+    runTestDebugComparator();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningInvokeWithUnknownTargetTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningInvokeWithUnknownTargetTest.java
index 83cb843..5baa779 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningInvokeWithUnknownTargetTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningInvokeWithUnknownTargetTest.java
@@ -4,16 +4,16 @@
 
 package com.android.tools.r8.ir.optimize.inliner.whyareyounotinlining;
 
-import static org.junit.Assert.assertEquals;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
 
+import com.android.tools.r8.DiagnosticsMatcher;
 import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.StringUtils;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -33,28 +33,28 @@
 
   @Test
   public void test() throws Exception {
-    ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    PrintStream out = new PrintStream(baos);
     testForR8(parameters.getBackend())
-        .addInnerClasses(WhyAreYouNotInliningInvokeWithUnknownTargetTest.class)
+        .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-whyareyounotinlining class " + A.class.getTypeName() + " { void m(); }")
-        .addOptionsModification(options -> options.testing.whyAreYouNotInliningConsumer = out)
         .enableExperimentalWhyAreYouNotInlining()
         .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters)
-        .compile();
-    out.close();
-
-    assertEquals(
-        StringUtils.lines(
-            "Method `void "
-                + A.class.getTypeName()
-                + ".m()` was not inlined into `void "
-                + TestClass.class.getTypeName()
-                + ".main(java.lang.String[])`: "
-                + "could not find a single target."),
-        baos.toString());
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .inspectDiagnosticMessages(
+            testDiagnosticMessages ->
+                testDiagnosticMessages.assertInfosMatch(
+                    allOf(
+                        DiagnosticsMatcher.diagnosticType(WhyAreYouNotInliningDiagnostic.class),
+                        DiagnosticsMatcher.diagnosticMessage(
+                            is(
+                                "Method `void "
+                                    + A.class.getTypeName()
+                                    + ".m()` was not inlined into `void "
+                                    + TestClass.class.getTypeName()
+                                    + ".main(java.lang.String[])`: "
+                                    + "could not find a single target.")))));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningLibraryTargetTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningLibraryTargetTest.java
new file mode 100644
index 0000000..1c85f7f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/whyareyounotinlining/WhyAreYouNotInliningLibraryTargetTest.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, 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.optimize.inliner.whyareyounotinlining;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class WhyAreYouNotInliningLibraryTargetTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules(
+            "-whyareyounotinlining class "
+                + System.class.getTypeName()
+                + " { long currentTimeMillis(); }")
+        .enableExperimentalWhyAreYouNotInlining()
+        .setMinApi(parameters)
+        .compile()
+        // The Inliner is currently not reporting -whyareyounotinlining for library calls.
+        .assertNoInfoMessages();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(System.currentTimeMillis() > 0 ? "A" : "B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java
index 58e398d..79a7e19 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java
@@ -4,38 +4,75 @@
 
 package com.android.tools.r8.ir.optimize.logging;
 
+import static org.junit.Assume.assumeTrue;
+
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.nio.file.Path;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class AndroidLogRemovalTest extends TestBase {
 
-  private final int maxRemovedAndroidLogLevel;
-  private final TestParameters parameters;
+  enum ClassSpecification {
+    ANNOTATED_CLASSES,
+    ANNOTATED_METHODS,
+    MAIN_METHOD,
+    NONE;
 
-  @Parameters(name = "{1}, log level: {0}")
+    public String getClassSpecification() {
+      switch (this) {
+        case ANNOTATED_CLASSES:
+          return "@" + StripLogs.class.getTypeName() + " class * { <methods>; }";
+        case ANNOTATED_METHODS:
+          return "class * { @" + StripLogs.class.getTypeName() + " <methods>; }";
+        case MAIN_METHOD:
+          return "class " + Main.class.getTypeName() + " { public static void main(...); }";
+        case NONE:
+          return "";
+        default:
+          throw new Unreachable();
+      }
+    }
+  }
+
+  @Parameter(0)
+  public ClassSpecification classSpecifiction;
+
+  @Parameter(1)
+  public boolean keepLogs;
+
+  @Parameter(2)
+  public int maxRemovedAndroidLogLevel;
+
+  @Parameter(3)
+  public TestParameters parameters;
+
+  @Parameters(name = "{3}, class spec: {0}, keep logs: {1}, log level: {2}")
   public static List<Object[]> data() {
     return buildParameters(
+        ClassSpecification.values(),
+        BooleanUtils.values(),
         ImmutableList.of(1, 2, 3, 4, 5, 6, 7),
         getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
-  public AndroidLogRemovalTest(int maxRemovedAndroidLogLevel, TestParameters parameters) {
-    this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
+    // Only test @KeepLogs with one log level variant.
+    assumeTrue(!keepLogs || maxRemovedAndroidLogLevel == 7);
+
     Path libraryFile =
         testForR8(parameters.getBackend())
             .addProgramClassFileData(
@@ -47,7 +84,7 @@
 
     testForR8(parameters.getBackend())
         .addProgramClassFileData(
-            transformer(TestClass.class)
+            transformer(Main.class)
                 .transformMethodInsnInMethod(
                     "main",
                     (opcode, owner, name, descriptor, isInterface, continuation) ->
@@ -58,18 +95,30 @@
                             descriptor,
                             isInterface))
                 .transform())
+        .addProgramClasses(KeepLogs.class, StripLogs.class)
         .addLibraryFiles(libraryFile, parameters.getDefaultRuntimeLibrary())
-        .addKeepMainRule(TestClass.class)
-        .addKeepRules("-maximumremovedandroidloglevel " + maxRemovedAndroidLogLevel)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-maximumremovedandroidloglevel "
+                + maxRemovedAndroidLogLevel
+                + " "
+                + classSpecifiction.getClassSpecification())
+        .applyIf(
+            keepLogs,
+            testBuilder ->
+                testBuilder.addKeepRules(
+                    "-maximumremovedandroidloglevel 1 class " + Main.class.getTypeName() + " {",
+                    "  @" + KeepLogs.class.getTypeName() + " <methods>;",
+                    "}"))
         .setMinApi(parameters)
         .compile()
         .addRunClasspathFiles(libraryFile)
-        .run(parameters.getRuntime(), TestClass.class)
+        .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutput(getExpectedOutputForMaxLogLevel());
   }
 
   private String getExpectedOutputForMaxLogLevel() {
-    switch (maxRemovedAndroidLogLevel) {
+    switch (keepLogs ? 1 : maxRemovedAndroidLogLevel) {
       case 1:
         return StringUtils.join(
             "",
@@ -114,8 +163,11 @@
     }
   }
 
-  static class TestClass {
+  @StripLogs
+  static class Main {
 
+    @KeepLogs
+    @StripLogs
     public static void main(String[] args) {
       Log.v("R8", "VERBOSE.");
       if (Log.isLoggable("R8", Log.VERBOSE)) {
@@ -192,4 +244,10 @@
       return 42;
     }
   }
+
+  @Retention(RetentionPolicy.CLASS)
+  @interface KeepLogs {}
+
+  @Retention(RetentionPolicy.CLASS)
+  @interface StripLogs {}
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/reflection/ReflectiveConstructionWithInlineClassTest.java b/src/test/java/com/android/tools/r8/kotlin/reflection/ReflectiveConstructionWithInlineClassTest.java
index 19cc939..4047867 100644
--- a/src/test/java/com/android/tools/r8/kotlin/reflection/ReflectiveConstructionWithInlineClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/reflection/ReflectiveConstructionWithInlineClassTest.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.kotlin.metadata.KotlinMetadataTestBase;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
@@ -97,10 +96,6 @@
         .addProgramFiles(kotlinc.getKotlinReflectJar())
         .addProgramFiles(kotlinc.getKotlinAnnotationJar())
         .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
-        .applyIf(
-            parameters.isCfRuntime()
-                && kotlinParameters.isNewerThan(KotlinCompilerVersion.KOTLINC_1_8_0),
-            TestShrinkerBuilder::addDontWarnJavaLangInvokeLambdaMetadataFactory)
         .setMinApi(parameters)
         .addKeepMainRule(MAIN_CLASS)
         .addKeepClassAndMembersRules(PKG + ".Data")
diff --git a/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java
index 1d08f27..e8981e6 100644
--- a/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/ArtProfileCollisionAfterClassMergingRewritingTest.java
@@ -71,11 +71,20 @@
     return ExternalArtProfile.builder()
         .addRules(
             ExternalArtProfileClassRule.builder().setClassReference(mainClassReference).build(),
-            ExternalArtProfileMethodRule.builder().setMethodReference(mainMethodReference).build(),
+            ExternalArtProfileMethodRule.builder()
+                .setMethodReference(mainMethodReference)
+                .setMethodRuleInfo(ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build())
+                .build(),
             ExternalArtProfileClassRule.builder().setClassReference(fooClassReference).build(),
-            ExternalArtProfileMethodRule.builder().setMethodReference(helloMethodReference).build(),
+            ExternalArtProfileMethodRule.builder()
+                .setMethodReference(helloMethodReference)
+                .setMethodRuleInfo(ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build())
+                .build(),
             ExternalArtProfileClassRule.builder().setClassReference(barClassReference).build(),
-            ExternalArtProfileMethodRule.builder().setMethodReference(worldMethodReference).build())
+            ExternalArtProfileMethodRule.builder()
+                .setMethodReference(worldMethodReference)
+                .setMethodRuleInfo(ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build())
+                .build())
         .build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/profile/art/ArtProfilePassthroughTest.java b/src/test/java/com/android/tools/r8/profile/art/ArtProfilePassthroughTest.java
index 1a33d54..e08fd7e 100644
--- a/src/test/java/com/android/tools/r8/profile/art/ArtProfilePassthroughTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/ArtProfilePassthroughTest.java
@@ -78,7 +78,11 @@
       profileBuilder
           .addClassRule(classRuleBuilder -> classRuleBuilder.setClassReference(mainClassReference))
           .addMethodRule(
-              methodRuleBuilder -> methodRuleBuilder.setMethodReference(mainMethodReference));
+              methodRuleBuilder ->
+                  methodRuleBuilder
+                      .setMethodReference(mainMethodReference)
+                      .setMethodRuleInfo(
+                          methodRuleInfoBuilder -> methodRuleInfoBuilder.setIsStartup(true)));
       providerStatus = ProviderStatus.DONE;
     }
 
diff --git a/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java
index 3cd8e10..b812f2d 100644
--- a/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/DesugaredLibraryArtProfileRewritingTest.java
@@ -82,6 +82,7 @@
         .addRule(
             ExternalArtProfileMethodRule.builder()
                 .setMethodReference(forEachMethodReference)
+                .setMethodRuleInfo(ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build())
                 .build())
         .build();
   }
diff --git a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java
index 9951db3..76cb37b 100644
--- a/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java
+++ b/src/test/java/com/android/tools/r8/profile/art/model/ExternalArtProfile.java
@@ -134,7 +134,13 @@
     }
 
     public Builder addMethodRule(MethodReference methodReference) {
-      return addMethodRule(methodReference, ArtProfileMethodRuleInfoImpl.empty());
+      return addMethodRule(
+          methodReference,
+          ArtProfileMethodRuleInfoImpl.builder()
+              .setIsHot()
+              .setIsStartup()
+              .setIsPostStartup()
+              .build());
     }
 
     public Builder addMethodRule(
diff --git a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionAndJoinIdentityTest.java b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionAndJoinIdentityTest.java
index 83287ff..3836c08 100644
--- a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionAndJoinIdentityTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionAndJoinIdentityTest.java
@@ -68,24 +68,6 @@
         .build()
         .run();
     List<String> joinedMapLines = StringUtils.splitLines(builder.toString());
-    // TODO(b/274735214): Partitioning does not capture the preamble of the mapping file yet, so we
-    //  discard it before checking equality.
-    List<String> lines = Files.readAllLines(mappingFile);
-    List<String> filteredLines = lines.subList(computeFirstLine(lines), lines.size());
-    assertListsAreEqual(filteredLines, joinedMapLines);
-  }
-
-  private int computeFirstLine(List<String> lines) {
-    int firstLine = 0;
-    for (int i = 0; i < lines.size(); i++) {
-      String currentLine = lines.get(i).trim();
-      if (!currentLine.startsWith("#")
-          && currentLine.contains(" -> ")
-          && currentLine.endsWith(":")) {
-        firstLine = i;
-        break;
-      }
-    }
-    return firstLine;
+    assertListsAreEqual(Files.readAllLines(mappingFile), joinedMapLines);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataPartitionNamesTest.java b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataPartitionNamesTest.java
index 376d072..2f5bc20 100644
--- a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataPartitionNamesTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataPartitionNamesTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.dex.CompatByteBuffer;
 import com.android.tools.r8.naming.MapVersion;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
@@ -82,7 +83,7 @@
     byte[] bytes = metadataData.getBytes();
     MappingPartitionMetadataInternal mappingPartitionMetadata =
         MappingPartitionMetadataInternal.deserialize(
-            bytes, MapVersion.MAP_VERSION_NONE, diagnosticsHandler);
+            CompatByteBuffer.wrap(bytes), MapVersion.MAP_VERSION_NONE, diagnosticsHandler);
     assertTrue(mappingPartitionMetadata.canGetPartitionKeys());
     assertEquals(expectedPartitionKeys, mappingPartitionMetadata.getPartitionKeys());
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java
index 01159fa..a006cab 100644
--- a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java
@@ -11,9 +11,10 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.dex.CompatByteBuffer;
 import com.android.tools.r8.naming.MapVersion;
+import com.android.tools.r8.retrace.RetracePartitionException;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal;
-import com.android.tools.r8.retrace.internal.RetracePartitionException;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import org.junit.Test;
@@ -46,7 +47,7 @@
             RetracePartitionException.class,
             () ->
                 MappingPartitionMetadataInternal.deserialize(
-                    bytes, MapVersion.MAP_VERSION_NONE, diagnosticsHandler));
+                    CompatByteBuffer.wrap(bytes), MapVersion.MAP_VERSION_NONE, diagnosticsHandler));
     assertEquals(
         "Unknown map partition strategy for metadata", retracePartitionException.getMessage());
   }
diff --git a/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderRewritingTest.java b/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderRewritingTest.java
index a14aafd..ae1e517 100644
--- a/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderRewritingTest.java
@@ -12,11 +12,13 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DiagnosticsMatcher;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ir.optimize.ServiceLoaderRewriterDiagnostic;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -251,12 +253,21 @@
             .addInnerClasses(ServiceLoaderRewritingTest.class)
             .addKeepMainRule(OtherRunner.class)
             .setMinApi(parameters)
+            .addKeepRules(
+                "-whyareyounotinlining class "
+                    + ServiceLoader.class.getTypeName()
+                    + " { *** load(...); }")
+            .enableExperimentalWhyAreYouNotInlining()
             .addDataEntryResources(
                 DataEntryResource.fromBytes(
                     StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
                     "META-INF/services/" + Service.class.getTypeName(),
                     Origin.unknown()))
+            .allowDiagnosticInfoMessages()
             .compile()
+            .assertAllInfosMatch(
+                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
+            .assertAtLeastOneInfoMessage()
             .writeToZip(path)
             .run(parameters.getRuntime(), OtherRunner.class)
             .assertSuccessWithOutput(EXPECTED_OUTPUT)
@@ -282,13 +293,22 @@
             .addKeepMainRule(EscapingRunner.class)
             .enableInliningAnnotations()
             .setMinApi(parameters)
+            .addKeepRules(
+                "-whyareyounotinlining class "
+                    + ServiceLoader.class.getTypeName()
+                    + " { *** load(...); }")
+            .enableExperimentalWhyAreYouNotInlining()
             .addDontObfuscate()
             .addDataEntryResources(
                 DataEntryResource.fromBytes(
                     StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
                     "META-INF/services/" + Service.class.getTypeName(),
                     Origin.unknown()))
+            .allowDiagnosticInfoMessages()
             .compile()
+            .assertAllInfosMatch(
+                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
+            .assertAtLeastOneInfoMessage()
             .writeToZip(path)
             .run(parameters.getRuntime(), EscapingRunner.class)
             .assertSuccessWithOutput(EXPECTED_OUTPUT)
@@ -314,12 +334,21 @@
             .addKeepMainRule(LoadWhereClassLoaderIsPhi.class)
             .enableInliningAnnotations()
             .setMinApi(parameters)
+            .addKeepRules(
+                "-whyareyounotinlining class "
+                    + ServiceLoader.class.getTypeName()
+                    + " { *** load(...); }")
+            .enableExperimentalWhyAreYouNotInlining()
             .addDataEntryResources(
                 DataEntryResource.fromBytes(
                     StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
                     "META-INF/services/" + Service.class.getTypeName(),
                     Origin.unknown()))
+            .allowDiagnosticInfoMessages()
             .compile()
+            .assertAllInfosMatch(
+                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
+            .assertAtLeastOneInfoMessage()
             .writeToZip(path)
             .run(parameters.getRuntime(), LoadWhereClassLoaderIsPhi.class)
             .assertSuccessWithOutputLines("Hello World!")
@@ -350,12 +379,21 @@
             .addKeepMainRule(MainRunner.class)
             .addKeepClassRules(Service.class)
             .setMinApi(parameters)
+            .addKeepRules(
+                "-whyareyounotinlining class "
+                    + ServiceLoader.class.getTypeName()
+                    + " { *** load(...); }")
+            .enableExperimentalWhyAreYouNotInlining()
             .addDataEntryResources(
                 DataEntryResource.fromBytes(
                     StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
                     "META-INF/services/" + Service.class.getTypeName(),
                     Origin.unknown()))
+            .allowDiagnosticInfoMessages()
             .compile()
+            .assertAllInfosMatch(
+                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
+            .assertAtLeastOneInfoMessage()
             .writeToZip(path)
             .run(parameters.getRuntime(), MainRunner.class)
             .assertSuccessWithOutput(EXPECTED_OUTPUT)
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 8149c5b..c049bd6 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -3298,4 +3298,43 @@
       }
     }
   }
+
+  @Test
+  public void parseMaximumRemovedAndroidLogLevelWithoutClassSpecification() {
+    DexItemFactory dexItemFactory = new DexItemFactory();
+    ProguardConfigurationParser parser = new ProguardConfigurationParser(dexItemFactory, reporter);
+    String configuration = StringUtils.lines("-maximumremovedandroidloglevel 2");
+    parser.parse(createConfigurationForTesting(ImmutableList.of(configuration)));
+    verifyParserEndsCleanly();
+
+    ProguardConfiguration config = parser.getConfig();
+    assertEquals(MaximumRemovedAndroidLogLevelRule.VERBOSE, config.getMaxRemovedAndroidLogLevel());
+    assertEquals(0, config.getRules().size());
+  }
+
+  @Test
+  public void parseMaximumRemovedAndroidLogLevelWithClassSpecification() {
+    for (String input :
+        new String[] {
+          "-maximumremovedandroidloglevel 2 class * { <methods>; }",
+          "-maximumremovedandroidloglevel 2 @Foo class * { <methods>; }"
+        }) {
+      DexItemFactory dexItemFactory = new DexItemFactory();
+      ProguardConfigurationParser parser =
+          new ProguardConfigurationParser(dexItemFactory, reporter);
+      String configuration = StringUtils.lines(input);
+      parser.parse(createConfigurationForTesting(ImmutableList.of(configuration)));
+      verifyParserEndsCleanly();
+
+      ProguardConfiguration config = parser.getConfig();
+      assertEquals(
+          MaximumRemovedAndroidLogLevelRule.NOT_SET, config.getMaxRemovedAndroidLogLevel());
+      assertEquals(1, config.getRules().size());
+      assertTrue(config.getRules().get(0).isMaximumRemovedAndroidLogLevelRule());
+
+      MaximumRemovedAndroidLogLevelRule rule =
+          config.getRules().get(0).asMaximumRemovedAndroidLogLevelRule();
+      assertEquals(MaximumRemovedAndroidLogLevelRule.VERBOSE, rule.getMaxRemovedAndroidLogLevel());
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 0ee68d2..ce200a0 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1446,6 +1446,19 @@
         });
   }
 
+  public ClassFileTransformer stripDebugLocals(MethodPredicate predicate) {
+    return addMethodTransformer(
+        new MethodTransformer() {
+          @Override
+          public void visitLocalVariable(
+              String name, String descriptor, String signature, Label start, Label end, int index) {
+            if (!MethodPredicate.testContext(predicate, getContext())) {
+              super.visitLocalVariable(name, descriptor, signature, start, end, index);
+            }
+          }
+        });
+  }
+
   public ClassFileTransformer stripFrames(String methodName) {
     return addMethodTransformer(
         new MethodTransformer() {
diff --git a/tools/r8_release.py b/tools/r8_release.py
index c799d90..b216389 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -15,7 +15,7 @@
 
 import utils
 
-R8_DEV_BRANCH = '8.1'
+R8_DEV_BRANCH = '8.2'
 R8_VERSION_FILE = os.path.join(
     'src', 'main', 'java', 'com', 'android', 'tools', 'r8', 'Version.java')
 THIS_FILE_RELATIVE = os.path.join('tools', 'r8_release.py')
diff --git a/tools/test.py b/tools/test.py
index 3c52be4..2f0254c 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -82,9 +82,6 @@
   result.add_option('--slow-tests', '--slow_tests',
       help='Also run slow tests.',
       default=False, action='store_true')
-  result.add_option('--force-32-bit-art', '--force_32_bit_art',
-      help='Force art to run 32 bits.',
-      default=False, action='store_true')
   result.add_option('-v', '--verbose',
       help='Print test stdout to, well, stdout.',
       default=False, action='store_true')
@@ -293,8 +290,6 @@
     gradle_args.append('-Pall_tests')
   if options.slow_tests:
     gradle_args.append('-Pslow_tests=1')
-  if options.force_32_bit_art:
-    gradle_args.append('-Pforce_32_bit_art=1')
   if options.tool:
     gradle_args.append('-Ptool=%s' % options.tool)
   if options.one_line_per_test: