Merge commit '7801f105b2a5da2f62bbc2d7ac5b2c27f7c84dbc' into dev-release

Change-Id: Ia034cddb95e49de7774a50ba221bd261e4a12c9f
diff --git a/.gitignore b/.gitignore
index d3eb304..1559ec0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -357,6 +357,8 @@
 tools/*/host/art-14.0.0-beta3.tar.gz
 tools/*/host/art-15.0.0-beta2
 tools/*/host/art-15.0.0-beta2.tar.gz
+tools/*/host/art-16.0.0
+tools/*/host/art-16.0.0.tar.gz
 tools/*/host/art-master
 tools/*/host/art-master.tar.gz
 tools/*/art.tar.gz
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index 5dfc0a5..3c25a8d 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -1064,6 +1064,7 @@
 fun getThirdPartyAndroidVms(): List<ThirdPartyDependency> {
   return listOf(
       listOf("host", "art-master"),
+      listOf("host", "art-16.0.0"),
       listOf("host", "art-15.0.0-beta2"),
       listOf("host", "art-14.0.0-beta3"),
       listOf("host", "art-13.0.0"),
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 76170b1..9882315 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -622,6 +622,82 @@
       }
     }
     builders {
+      name: "linux-android-16"
+      swarming_host: "chrome-swarming.appspot.com"
+      swarming_tags: "vpython:native-python-wrapper"
+      dimensions: "cpu:x86-64"
+      dimensions: "normal:true"
+      dimensions: "os:Ubuntu-22.04"
+      dimensions: "pool:luci.r8.ci"
+      exe {
+        cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
+        cipd_version: "refs/heads/main"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "builder_group": "internal.client.r8",'
+        '  "recipe": "rex",'
+        '  "test_options": ['
+        '    "--dex_vm=16.0.0",'
+        '    "--all_tests",'
+        '    "--command_cache_dir=/tmp/ccache",'
+        '    "--tool=r8",'
+        '    "--print-times",'
+        '    "--no_internal",'
+        '    "--one_line_per_test",'
+        '    "--archive_failures"'
+        '  ]'
+        '}'
+      priority: 26
+      execution_timeout_secs: 21600
+      expiration_secs: 126000
+      build_numbers: YES
+      service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+    }
+    builders {
+      name: "linux-android-16_release"
+      swarming_host: "chrome-swarming.appspot.com"
+      swarming_tags: "vpython:native-python-wrapper"
+      dimensions: "cpu:x86-64"
+      dimensions: "normal:true"
+      dimensions: "os:Ubuntu-22.04"
+      dimensions: "pool:luci.r8.ci"
+      exe {
+        cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
+        cipd_version: "refs/heads/main"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "builder_group": "internal.client.r8",'
+        '  "recipe": "rex",'
+        '  "test_options": ['
+        '    "--dex_vm=16.0.0",'
+        '    "--all_tests",'
+        '    "--command_cache_dir=/tmp/ccache",'
+        '    "--tool=r8",'
+        '    "--print-times",'
+        '    "--no_internal",'
+        '    "--one_line_per_test",'
+        '    "--archive_failures"'
+        '  ]'
+        '}'
+      priority: 26
+      execution_timeout_secs: 21600
+      expiration_secs: 126000
+      build_numbers: YES
+      service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+    }
+    builders {
       name: "linux-android-4.0"
       swarming_host: "chrome-swarming.appspot.com"
       swarming_tags: "vpython:native-python-wrapper"
diff --git a/infra/config/global/generated/luci-milo.cfg b/infra/config/global/generated/luci-milo.cfg
index e6c8157..5a6d3a2 100644
--- a/infra/config/global/generated/luci-milo.cfg
+++ b/infra/config/global/generated/luci-milo.cfg
@@ -116,6 +116,11 @@
     short_name: "15"
   }
   builders {
+    name: "buildbucket/luci.r8.ci/linux-android-16"
+    category: "R8"
+    short_name: "16"
+  }
+  builders {
     name: "buildbucket/luci.r8.ci/win"
     category: "R8"
     short_name: "win"
@@ -271,6 +276,11 @@
     short_name: "15"
   }
   builders {
+    name: "buildbucket/luci.r8.ci/linux-android-16_release"
+    category: "Release|R8"
+    short_name: "16"
+  }
+  builders {
     name: "buildbucket/luci.r8.ci/win_release"
     category: "Release|R8"
     short_name: "win"
diff --git a/infra/config/global/generated/luci-notify.cfg b/infra/config/global/generated/luci-notify.cfg
index a13297c..01cd1bd 100644
--- a/infra/config/global/generated/luci-notify.cfg
+++ b/infra/config/global/generated/luci-notify.cfg
@@ -180,6 +180,30 @@
   }
   builders {
     bucket: "ci"
+    name: "linux-android-16"
+    repository: "https://r8.googlesource.com/r8"
+  }
+}
+notifiers {
+  notifications {
+    on_failure: true
+    on_new_failure: true
+    notify_blamelist {}
+  }
+  builders {
+    bucket: "ci"
+    name: "linux-android-16_release"
+    repository: "https://r8.googlesource.com/r8"
+  }
+}
+notifiers {
+  notifications {
+    on_failure: true
+    on_new_failure: true
+    notify_blamelist {}
+  }
+  builders {
+    bucket: "ci"
     name: "linux-android-4.0"
     repository: "https://r8.googlesource.com/r8"
   }
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index ef9103f..2323ed2 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -254,6 +254,35 @@
   }
 }
 job {
+  id: "linux-android-16"
+  realm: "ci"
+  acl_sets: "ci"
+  triggering_policy {
+    kind: GREEDY_BATCHING
+    max_concurrent_invocations: 1
+  }
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "linux-android-16"
+  }
+}
+job {
+  id: "linux-android-16_release"
+  realm: "ci"
+  acl_sets: "ci"
+  triggering_policy {
+    kind: GREEDY_BATCHING
+    max_concurrent_invocations: 1
+    max_batch_size: 1
+  }
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "ci"
+    builder: "linux-android-16_release"
+  }
+}
+job {
   id: "linux-android-4.0"
   realm: "ci"
   acl_sets: "ci"
@@ -846,6 +875,7 @@
   id: "branch-gitiles-9.0-forward"
   realm: "ci"
   acl_sets: "ci"
+  triggers: "linux-android-16_release"
   triggers: "linux-jdk25_release"
   gitiles {
     repo: "https://r8.googlesource.com/r8"
@@ -896,6 +926,7 @@
   triggers: "linux-android-13"
   triggers: "linux-android-14"
   triggers: "linux-android-15"
+  triggers: "linux-android-16"
   triggers: "linux-android-4.0"
   triggers: "linux-android-4.4"
   triggers: "linux-android-5"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 5760b29..30d367e 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -445,6 +445,11 @@
     ["--dex_vm=15.0.0", "--all_tests", "--command_cache_dir=/tmp/ccache"],
     release_trigger = ["branch-gitiles-8.5-forward"],
 )
+r8_tester_with_default(
+    "linux-android-16",
+    ["--dex_vm=16.0.0", "--all_tests", "--command_cache_dir=/tmp/ccache"],
+    release_trigger = ["branch-gitiles-9.0-forward"],
+)
 
 r8_tester_with_default(
     "win",
diff --git a/scripts/update-host-art.sh b/scripts/update-host-art.sh
index a84c946..59ea813 100755
--- a/scripts/update-host-art.sh
+++ b/scripts/update-host-art.sh
@@ -180,8 +180,7 @@
 if [ -f $DEST/product/$ANDROID_PRODUCT/system/framework/boot.vdex ]; then
   for VDEXFILE in $DEST/product/$ANDROID_PRODUCT/system/framework/*.vdex; do
     VDEXNAME=$(basename ${VDEXFILE});
-    # Maybe remove arm here.
-    for ARCH in arm arm64; do
+    for ARCH in arm64; do
       rm $DEST/product/$ANDROID_PRODUCT/system/framework/$ARCH/${VDEXNAME};
       # This relative link command will create a symbolic link of the form
       # ../${VDEXNAME} for each architecture.
@@ -204,5 +203,5 @@
 strip $DEST/framework/x86_64/* 2> /dev/null
 
 echo "Now run"
-echo "(cd $DEST_ROOT; upload_to_google_storage.py -a --bucket r8-deps $ART_DIR)"
+echo "(cd $DEST_ROOT/host; upload_to_google_storage.py -a --bucket r8-deps $ART_DIR)"
 echo "NOTE; If $ART_DIR has several directory elements adjust accordingly."
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
index d6f1ead..137faa3 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
@@ -26,6 +26,9 @@
   }
 
   private String printParameters(Class<?>... parameters) {
+    if (parameters == null) {
+      return "(null)";
+    }
     if (parameters.length == 0) {
       return "()";
     }
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index 41bc7e2..0b93eb9 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.EnqueuerFactory;
 import com.android.tools.r8.shaking.MainDexInfo;
@@ -79,11 +78,8 @@
     MainDexListBuilder.checkForAssumedLibraryTypes(appView.appInfo());
 
     ImmediateAppSubtypingInfo subtypingInfo = ImmediateAppSubtypingInfo.create(appView);
-
-    ProfileCollectionAdditions profileCollectionAdditions = ProfileCollectionAdditions.nop();
     MainDexRootSet mainDexRootSet =
-        MainDexRootSet.builder(
-                appView, profileCollectionAdditions, subtypingInfo, options.mainDexKeepRules)
+        MainDexRootSet.builder(appView, subtypingInfo, options.mainDexKeepRules)
             .evaluateRulesAndBuild(executor);
     appView.setMainDexRootSet(mainDexRootSet);
 
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 8f40057..90e1e84 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -379,8 +379,7 @@
           assert appView.graphLens().isIdentityLens();
           // Find classes which may have code executed before secondary dex files installation.
           MainDexRootSet mainDexRootSet =
-              MainDexRootSet.builder(
-                      appView, profileCollectionAdditions, subtypingInfo, options.mainDexKeepRules)
+              MainDexRootSet.builder(appView, subtypingInfo, options.mainDexKeepRules)
                   .evaluateRulesAndBuild(executorService);
           appView.setMainDexRootSet(mainDexRootSet);
           appView.appInfo().unsetObsolete();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeUtils.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeUtils.java
index 65a5c1a..2f7bce3 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeUtils.java
@@ -93,7 +93,21 @@
         TypeElement instructionUseType =
             computeUseTypeForInstruction(appView, returnType, instruction, item.value, meet, users);
         useType = meet.apply(useType, instructionUseType);
-        if (useType.isTop() || useType.equalUpToNullability(staticType)) {
+        if (useType.isTop()) {
+          // Bail-out.
+          if (staticType.isInt()
+              && appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug()) {
+            return useType;
+          }
+          return staticType;
+        }
+        if (useType.equalUpToNullability(staticType)) {
+          if (staticType.isInt()
+              && appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug()) {
+            // We need to consider all usages. We need to return TOP if we find a use typed as
+            // boolean/byte/char/short and compile to Dalvik.
+            continue;
+          }
           // Bail-out.
           return staticType;
         }
@@ -152,7 +166,7 @@
 
   private static TypeElement computeUseTypeForInstanceGet(
       AppView<?> appView, InstanceGet instanceGet) {
-    return instanceGet.getField().getHolderType().toTypeElement(appView);
+    return toReferenceTypeElement(instanceGet.getField().getHolderType(), appView);
   }
 
   private static TypeElement computeUseTypeForInstancePut(
@@ -163,10 +177,10 @@
     DexField field = instancePut.getField();
     TypeElement useType = TypeElement.getBottom();
     if (instancePut.object() == value) {
-      useType = meet.apply(useType, field.getHolderType().toTypeElement(appView));
+      useType = meet.apply(useType, toReferenceTypeElement(field.getHolderType(), appView));
     }
     if (instancePut.value() == value) {
-      useType = meet.apply(useType, field.getType().toTypeElement(appView));
+      useType = meet.apply(useType, toTypeElementOrTop(field.getType(), appView));
     }
     return useType;
   }
@@ -183,10 +197,9 @@
         continue;
       }
       TypeElement useTypeForArgument =
-          invoke
-              .getInvokedMethod()
-              .getArgumentType(argumentIndex, invoke.isInvokeStatic())
-              .toTypeElement(appView);
+          toTypeElementOrTop(
+              invoke.getInvokedMethod().getArgumentType(argumentIndex, invoke.isInvokeStatic()),
+              appView);
       useType = meet.apply(useType, useTypeForArgument);
     }
     assert !useType.isBottom();
@@ -194,11 +207,11 @@
   }
 
   private static TypeElement computeUseTypeForReturn(AppView<?> appView, DexType returnType) {
-    return returnType.toTypeElement(appView);
+    return toTypeElementOrTop(returnType, appView);
   }
 
   private static TypeElement computeUseTypeForStaticPut(AppView<?> appView, StaticPut staticPut) {
-    return staticPut.getField().getType().toTypeElement(appView);
+    return toTypeElementOrTop(staticPut.getField().getType(), appView);
   }
 
   private static TypeElement meet(
@@ -249,4 +262,24 @@
     }
     return TypeElement.getTop();
   }
+
+  private static TypeElement toReferenceTypeElement(DexType type, AppView<?> appView) {
+    assert type.isReferenceType();
+    return type.toTypeElement(appView);
+  }
+
+  private static TypeElement toTypeElementOrTop(DexType type, AppView<?> appView) {
+    if (appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug()) {
+      switch (type.getDescriptor().getFirstByteAsChar()) {
+        case 'B':
+        case 'C':
+        case 'S':
+        case 'Z':
+          return TypeElement.getTop();
+        default:
+          break;
+      }
+    }
+    return type.toTypeElement(appView);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 003e17f..2287bbc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -96,10 +96,19 @@
     return prev;
   }
 
+  public boolean hasNext() {
+    return next != null;
+  }
+
   public Instruction getNext() {
     return next;
   }
 
+  @SuppressWarnings("TypeParameterUnusedInFormals")
+  public <T extends Instruction> T nextUntilExclusive(Predicate<Instruction> predicate) {
+    return hasNext() ? getNext().nextUntilInclusive(predicate) : null;
+  }
+
   @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
   public <T extends Instruction> T nextUntilInclusive(Predicate<Instruction> predicate) {
     Instruction current = this;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfToLirConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/CfToLirConverter.java
index 77bbc33..64a5d69 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfToLirConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfToLirConverter.java
@@ -7,27 +7,29 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.analysis.EnqueuerAnalysisCollection;
 import com.android.tools.r8.graph.analysis.FinishedEnqueuerAnalysis;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.FieldGet;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.passes.BranchSimplifier;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPassCollection;
 import com.android.tools.r8.ir.conversion.passes.ConstResourceNumberRewriter;
 import com.android.tools.r8.ir.conversion.passes.StringSwitchConverter;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
-import com.android.tools.r8.ir.optimize.membervaluepropagation.D8MemberValuePropagation;
+import com.android.tools.r8.ir.optimize.membervaluepropagation.AssumePropagator;
 import com.android.tools.r8.lightir.IR2LirConverter;
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.lightir.LirStrategy;
 import com.android.tools.r8.naming.IdentifierNameStringMarker;
+import com.android.tools.r8.shaking.AssumeInfoCollection;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -122,8 +124,8 @@
     }
     IRCode code = method.buildIR(appView, MethodConversionOptions.forLirPhase(appView));
     codeRewriterPassCollection.run(code, null, null, Timing.empty(), null, appView.options());
-    if (appView.options().isGeneratingDex() && isCodeReadingSdkInt(code)) {
-      new D8MemberValuePropagation(appView).run(code);
+    if (appView.options().isGeneratingDex() && hasApplicableAssumeValuesRule(code)) {
+      new AssumePropagator(appView).run(code);
       new BranchSimplifier(appView).simplifyIf(code);
       new DeadCodeRemover(appView).run(code, Timing.empty());
     }
@@ -136,10 +138,21 @@
     method.setCode(lirCode, appView);
   }
 
-  private boolean isCodeReadingSdkInt(IRCode code) {
-    DexField SDK_INT = appView.dexItemFactory().androidOsBuildVersionMembers.SDK_INT;
-    for (StaticGet staticGet : code.<StaticGet>instructions(Instruction::isStaticGet)) {
-      if (staticGet.getField().isIdenticalTo(SDK_INT)) {
+  private boolean hasApplicableAssumeValuesRule(IRCode code) {
+    AssumeInfoCollection assumeInfoCollection = appView.getAssumeInfoCollection();
+    for (Instruction instruction : code.instructions()) {
+      DexEncodedMember<?, ?> resolvedMember;
+      if (instruction.isFieldGet()) {
+        FieldGet fieldGet = instruction.asFieldGet();
+        resolvedMember = fieldGet.resolveField(appView, code.context()).getResolvedField();
+      } else if (instruction.isInvokeMethod()) {
+        InvokeMethod invoke = instruction.asInvokeMethod();
+        resolvedMember = invoke.resolveMethod(appView, code.context()).getResolvedMethod();
+      } else {
+        continue;
+      }
+      if (resolvedMember != null
+          && !assumeInfoCollection.get(resolvedMember).getAssumeValue().isUnknown()) {
         return true;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index b84e744..0718bd7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -38,6 +38,7 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.IfType;
@@ -691,6 +692,20 @@
             }
           }
         }
+
+        // Null checks are removed above. If a call to Object#getClass() remains then it must be due
+        // to having an out value.
+        if (invoke.getInvokedMethod().match(dexItemFactory.objectMembers.getClass)) {
+          ConstClass replacement =
+              ConstClass.builder()
+                  .setType(eligibleClass.getType())
+                  .setFreshOutValue(
+                      code, dexItemFactory.classType.toNonNullClassTypeElement(appView))
+                  .setPosition(invoke)
+                  .build();
+          invoke.replace(replacement, affectedValues);
+          continue;
+        }
       }
 
       if (user.isIf()) {
@@ -1302,7 +1317,14 @@
                 .appInfo()
                 .resolveMethodOnLegacy(eligibleClass, invokedMethod)
                 .asSingleResolution();
-        if (resolutionResult == null || !resolutionResult.getResolvedHolder().isProgramClass()) {
+        if (resolutionResult == null) {
+          return false;
+        }
+        if (!resolutionResult.getResolvedHolder().isProgramClass()) {
+          DexMethod method = resolutionResult.getResolvedMethod().getReference();
+          if (method.isIdenticalTo(dexItemFactory.objectMembers.getClass)) {
+            continue;
+          }
           return false;
         }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/AssumePropagator.java b/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/AssumePropagator.java
new file mode 100644
index 0000000..3d19511
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/AssumePropagator.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2025, 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.membervaluepropagation;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.ArrayGet;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfo;
+import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfoLookup;
+import java.util.Set;
+
+public class AssumePropagator extends MemberValuePropagation<AppInfoWithClassHierarchy> {
+
+  public AssumePropagator(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    super(appView);
+  }
+
+  @Override
+  InstructionListIterator rewriteInvokeMethod(
+      IRCode code,
+      ProgramMethod context,
+      Set<Value> affectedValues,
+      BasicBlockIterator blocks,
+      InstructionListIterator iterator,
+      InvokeMethod invoke) {
+    if (invoke.hasUsedOutValue() && invoke.getInvokedMethod().getHolderType().isClassType()) {
+      SingleResolutionResult<?> resolutionResult =
+          invoke.resolveMethod(appView, context).asSingleResolution();
+      if (resolutionResult != null) {
+        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+        AssumeInfo lookup =
+            AssumeInfoLookup.lookupAssumeInfo(appView, resolutionResult, singleTarget);
+        applyAssumeInfo(code, affectedValues, blocks, iterator, invoke, lookup);
+      }
+    }
+    return iterator;
+  }
+
+  @Override
+  InstructionListIterator rewriteInstanceGet(
+      IRCode code,
+      Set<Value> affectedValues,
+      BasicBlockIterator blocks,
+      InstructionListIterator iterator,
+      InstanceGet current) {
+    return rewriteFieldGet(code, affectedValues, blocks, iterator, current);
+  }
+
+  @Override
+  InstructionListIterator rewriteStaticGet(
+      IRCode code,
+      Set<Value> affectedValues,
+      BasicBlockIterator blocks,
+      InstructionListIterator iterator,
+      StaticGet current) {
+    return rewriteFieldGet(code, affectedValues, blocks, iterator, current);
+  }
+
+  private InstructionListIterator rewriteFieldGet(
+      IRCode code,
+      Set<Value> affectedValues,
+      BasicBlockIterator blocks,
+      InstructionListIterator iterator,
+      FieldInstruction current) {
+    DexClassAndField resolvedField =
+        current.resolveField(appView, code.context()).getResolutionPair();
+    if (resolvedField != null) {
+      AssumeInfo lookup = appView.getAssumeInfoCollection().get(resolvedField);
+      applyAssumeInfo(code, affectedValues, blocks, iterator, current, lookup);
+    }
+    return iterator;
+  }
+
+  @Override
+  void rewriteArrayGet(
+      IRCode code,
+      Set<Value> affectedValues,
+      BasicBlockIterator blocks,
+      InstructionListIterator iterator,
+      ArrayGet arrayGet) {
+    // Intentionally empty.
+  }
+
+  @Override
+  void rewriteInstancePut(IRCode code, InstructionListIterator iterator, InstancePut current) {
+    // Intentionally empty.
+  }
+
+  @Override
+  void rewriteStaticPut(IRCode code, InstructionListIterator iterator, StaticPut current) {
+    // Intentionally empty.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/OutlineCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/OutlineCollection.java
new file mode 100644
index 0000000..64bd487
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/OutlineCollection.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2025, 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.outliner.exceptions;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.features.ClassToFeatureSplitMap;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.lightir.LirCode;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+public class OutlineCollection {
+
+  private final AppView<?> appView;
+  private final ClassToFeatureSplitMap classToFeatureSplitMap;
+
+  private final Map<FeatureSplit, Map<Wrapper<LirCode<?>>, ThrowBlockOutline>> outlines =
+      new ConcurrentHashMap<>();
+
+  OutlineCollection(AppView<?> appView) {
+    this.appView = appView;
+    this.classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
+  }
+
+  public ThrowBlockOutline add(LirCode<?> lirCode, DexProto proto, ProgramMethod context) {
+    // Get the outlines in the current feature.
+    FeatureSplit feature = classToFeatureSplitMap.getFeatureSplit(context.getHolder(), appView);
+    Map<Wrapper<LirCode<?>>, ThrowBlockOutline> outlinesInFeature =
+        outlines.computeIfAbsent(feature, ignoreKey(ConcurrentHashMap::new));
+    // Add the outline.
+    Wrapper<LirCode<?>> lirCodeWrapper = ThrowBlockOutlinerLirCodeEquivalence.get().wrap(lirCode);
+    return outlinesInFeature.computeIfAbsent(
+        lirCodeWrapper, w -> new ThrowBlockOutline(w.get(), proto));
+  }
+
+  public Collection<ThrowBlockOutline> getOutlines() {
+    mergeOutlinesFromFeaturesIntoBase();
+    return outlines.values().stream()
+        .flatMap(x -> x.values().stream())
+        .collect(Collectors.toList());
+  }
+
+  private void mergeOutlinesFromFeaturesIntoBase() {
+    Map<Wrapper<LirCode<?>>, ThrowBlockOutline> outlinesInBase = outlines.get(FeatureSplit.BASE);
+    if (outlinesInBase == null) {
+      return;
+    }
+    for (var entry : outlines.entrySet()) {
+      FeatureSplit feature = entry.getKey();
+      if (feature.isBase()) {
+        continue;
+      }
+      Map<Wrapper<LirCode<?>>, ThrowBlockOutline> outlinesInFeature = entry.getValue();
+      var innerIterator = outlinesInFeature.entrySet().iterator();
+      while (innerIterator.hasNext()) {
+        var innerEntry = innerIterator.next();
+        Wrapper<LirCode<?>> lirCodeWrapper = innerEntry.getKey();
+        ThrowBlockOutline outlineInBase = outlinesInBase.get(lirCodeWrapper);
+        if (outlineInBase == null) {
+          continue;
+        }
+        ThrowBlockOutline outlineInFeature = innerEntry.getValue();
+        outlineInBase.merge(outlineInFeature);
+        innerIterator.remove();
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
index f868bc8..353aba6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
@@ -32,6 +32,7 @@
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.google.common.collect.ConcurrentHashMultiset;
@@ -47,11 +48,20 @@
 
 public class ThrowBlockOutline implements LirConstant {
 
-  private final List<AbstractValue> arguments;
-  private final LirCode<?> lirCode;
-  private final DexProto proto;
+  private List<AbstractValue> arguments;
+  private LirCode<?> lirCode;
+  private DexProto proto;
+
   private final Multiset<DexMethod> users = ConcurrentHashMultiset.create();
 
+  // If this is an outline in base, and there are equivalent outlines in features, then the outlines
+  // from features will be removed and merged into the outline in base. This field stores the merged
+  // outlines.
+  //
+  // This is always null in D8.
+  private List<ThrowBlockOutline> children;
+  private ThrowBlockOutline parent;
+
   private ProgramMethod materializedOutlineMethod;
 
   ThrowBlockOutline(LirCode<?> lirCode, DexProto proto) {
@@ -86,6 +96,14 @@
     }
   }
 
+  public void clear() {
+    // Clear the state of this outline since it has been merged with its parent.
+    assert parent != null;
+    arguments = null;
+    lirCode = null;
+    proto = null;
+  }
+
   private AbstractValue encodeArgumentValue(Value value, AbstractValueFactory valueFactory) {
     if (value.isConstNumber()) {
       ConstNumber constNumber = value.getDefinition().asConstNumber();
@@ -102,11 +120,24 @@
     return AbstractValue.unknown();
   }
 
+  // Returns this outline and all outlines that have been merged into this outline.
+  public Iterable<ThrowBlockOutline> getAllOutlines() {
+    if (children == null) {
+      return Collections.singletonList(this);
+    }
+    return IterableUtils.append(children, this);
+  }
+
   public List<AbstractValue> getArguments() {
     return arguments;
   }
 
+  public List<ThrowBlockOutline> getChildren() {
+    return children != null ? children : Collections.emptyList();
+  }
+
   public LirCode<?> getLirCode() {
+    assert verifyNotMerged();
     return lirCode;
   }
 
@@ -120,14 +151,26 @@
   }
 
   public int getNumberOfUsers() {
-    return users.size();
+    int result = users.size();
+    if (children != null) {
+      for (ThrowBlockOutline child : children) {
+        result += child.users.size();
+      }
+    }
+    return result;
+  }
+
+  public ThrowBlockOutline getParentOrSelf() {
+    return parent != null ? parent : this;
   }
 
   public DexProto getProto() {
+    assert verifyNotMerged();
     return proto;
   }
 
   public RewrittenPrototypeDescription getProtoChanges() {
+    assert verifyNotMerged();
     assert hasConstantArgument();
     ArgumentInfoCollection.Builder argumentsInfoBuilder =
         ArgumentInfoCollection.builder().setArgumentInfosSize(proto.getArity());
@@ -145,10 +188,12 @@
   }
 
   public DexProto getOptimizedProto(DexItemFactory factory) {
+    assert verifyNotMerged();
     return proto.withoutParameters((i, p) -> isArgumentConstant(i), factory);
   }
 
   public ProgramMethod getSynthesizingContext(AppView<?> appView) {
+    assert verifyNotMerged();
     DexMethod shortestUser = null;
     for (DexMethod user : users) {
       if (shortestUser == null) {
@@ -227,6 +272,7 @@
   }
 
   public boolean hasConstantArgument() {
+    assert verifyNotMerged();
     return Iterables.any(arguments, argument -> !argument.isUnknown());
   }
 
@@ -241,6 +287,7 @@
   }
 
   public boolean isArgumentConstant(int index) {
+    assert verifyNotMerged();
     return !arguments.get(index).isUnknown();
   }
 
@@ -248,7 +295,16 @@
     return materializedOutlineMethod != null;
   }
 
+  public boolean isStringBuilderToStringOutline() {
+    return !isThrowOutline();
+  }
+
+  public boolean isThrowOutline() {
+    return proto.getReturnType().isVoidType();
+  }
+
   public void materialize(AppView<?> appView, MethodProcessingContext methodProcessingContext) {
+    assert verifyNotMerged();
     SyntheticItems syntheticItems = appView.getSyntheticItems();
     materializedOutlineMethod =
         syntheticItems.createMethod(
@@ -265,6 +321,9 @@
                         appView.apiLevelCompute().computeInitialMinApiLevel(appView.options()))
                     .setCode(methodSig -> lirCode)
                     .setProto(getOptimizedProto(appView.dexItemFactory())));
+    for (ThrowBlockOutline child : getChildren()) {
+      child.materializedOutlineMethod = materializedOutlineMethod;
+    }
   }
 
   private DexAnnotationSet createAnnotations(AppView<?> appView) {
@@ -278,4 +337,23 @@
     }
     return DexAnnotationSet.empty();
   }
+
+  public void merge(ThrowBlockOutline outline) {
+    for (int i = 0; i < arguments.size(); i++) {
+      if (!arguments.get(i).equals(outline.arguments.get(i))) {
+        arguments.set(i, AbstractValue.unknown());
+      }
+    }
+    if (children == null) {
+      children = new ArrayList<>();
+    }
+    children.add(outline);
+    outline.parent = this;
+    outline.clear();
+  }
+
+  public boolean verifyNotMerged() {
+    assert parent == null;
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
index e9de503..04e8567 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
@@ -18,7 +19,6 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Return;
-import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
 import com.android.tools.r8.ir.code.UnusedArgument;
 import com.android.tools.r8.ir.code.Value;
@@ -41,10 +41,12 @@
 
   private final AppView<?> appView;
   private final DeadCodeRemover deadCodeRemover;
+  private final DexItemFactory factory;
 
   ThrowBlockOutlineMarkerRewriter(AppView<?> appView) {
     this.appView = appView;
     this.deadCodeRemover = new DeadCodeRemover(appView);
+    this.factory = appView.dexItemFactory();
   }
 
   public void processOutlineMethod(
@@ -109,80 +111,92 @@
 
   private void processOutlineMarkers(IRCode code) {
     for (BasicBlock block : code.getBlocks()) {
-      Throw throwInstruction = block.exit().asThrow();
-      if (throwInstruction != null) {
-        ThrowBlockOutlineMarker outlineMarker =
-            block.entry().nextUntilInclusive(Instruction::isThrowBlockOutlineMarker);
-        if (outlineMarker != null) {
-          ThrowBlockOutline outline = outlineMarker.getOutline();
-          outlineMarker.detachConstantOutlineArguments(outline);
-          if (outline.isMaterialized()) {
-            // Insert a call to the materialized outline method and load the return value.
-            BasicBlockInstructionListIterator instructionIterator =
-                block.listIterator(block.exit());
-            InvokeStatic invoke =
-                InvokeStatic.builder()
-                    .setArguments(outlineMarker.inValues())
-                    .setIsInterface(false)
-                    .setMethod(outline.getMaterializedOutlineMethod())
-                    .setPosition(throwInstruction)
-                    .build();
+      ThrowBlockOutlineMarker outlineMarker =
+          block.entry().nextUntilInclusive(Instruction::isThrowBlockOutlineMarker);
+      while (outlineMarker != null) {
+        ThrowBlockOutline outline = outlineMarker.getOutline().getParentOrSelf();
+        Instruction outlineEnd = getOutlineEnd(block, outline, outlineMarker);
+        outlineMarker.detachConstantOutlineArguments(outline);
+        if (outline.isMaterialized()) {
+          // Insert a call to the materialized outline method and load the return value.
+          BasicBlockInstructionListIterator instructionIterator = block.listIterator(outlineEnd);
+          InvokeStatic.Builder invokeBuilder =
+              InvokeStatic.builder()
+                  .setArguments(outlineMarker.inValues())
+                  .setIsInterface(false)
+                  .setMethod(outline.getMaterializedOutlineMethod())
+                  .setPosition(outlineEnd);
+          if (outline.isStringBuilderToStringOutline()) {
+            invokeBuilder.setFreshOutValue(
+                code, factory.stringType.toTypeElement(appView), outlineEnd.getLocalInfo());
+          }
+          InvokeStatic invoke = invokeBuilder.build();
+          if (outline.isStringBuilderToStringOutline()) {
+            outlineEnd.replace(invoke);
+            outlineEnd = invoke;
+          } else {
+            assert outline.isThrowOutline();
             instructionIterator.add(invoke);
-            Value returnValue = addReturnOrThrowValue(code, instructionIterator);
+            Value returnOrThrowValue = addReturnOrThrowValue(code, instructionIterator);
 
             // Replace the throw instruction by a normal return, but throw null in initializers.
             if (code.context().getDefinition().isInstanceInitializer()) {
-              throwInstruction.replaceValue(0, returnValue);
+              outlineEnd.replaceValue(0, returnOrThrowValue);
             } else {
               Return returnInstruction =
                   Return.builder()
                       .setPositionForNonThrowingInstruction(
-                          throwInstruction.getPosition(), appView.options())
-                      .setReturnValue(returnValue)
+                          outlineEnd.getPosition(), appView.options())
+                      .setReturnValue(returnOrThrowValue)
                       .build();
               block.replaceLastInstruction(returnInstruction);
+              outlineEnd = returnInstruction;
             }
+          }
 
-            // Remove all outlined instructions bottom up.
-            instructionIterator = block.listIterator(invoke);
-            for (Instruction instruction = instructionIterator.previous();
-                instruction != outlineMarker;
-                instruction = instructionIterator.previous()) {
-              Value outValue = instruction.outValue();
-              if (outValue == null || !outValue.hasNonDebugUsers()) {
-                // Remove all debug users of the out-value.
-                if (outValue != null && outValue.hasDebugUsers()) {
-                  for (Instruction debugUser : outValue.debugUsers()) {
-                    debugUser.getDebugValues().remove(outValue);
-                    if (debugUser.isDebugLocalRead() && debugUser.getDebugValues().isEmpty()) {
-                      debugUser.remove();
-                    }
+          // Remove all outlined instructions bottom up.
+          instructionIterator = block.listIterator(invoke);
+          for (Instruction outlinedInstruction = instructionIterator.previous();
+              outlinedInstruction != outlineMarker;
+              outlinedInstruction = instructionIterator.previous()) {
+            assert !outlinedInstruction.isThrowBlockOutlineMarker();
+            Value outValue = outlinedInstruction.outValue();
+            if (outValue == null || !outValue.hasNonDebugUsers()) {
+              // Remove all debug users of the out-value.
+              if (outValue != null && outValue.hasDebugUsers()) {
+                for (Instruction debugUser : outValue.debugUsers()) {
+                  debugUser.getDebugValues().remove(outValue);
+                  if (debugUser.isDebugLocalRead() && debugUser.getDebugValues().isEmpty()) {
+                    debugUser.remove();
                   }
-                  outValue.clearDebugUsers();
                 }
-                // We are not using `removeOrReplaceByDebugLocalRead` here due to the backwards
-                // iteration.
-                if (instruction.getDebugValues().isEmpty()) {
-                  instruction.remove();
-                } else {
-                  DebugLocalRead replacement = new DebugLocalRead();
-                  instruction.replace(replacement);
-                  Instruction previous = instructionIterator.previous();
-                  assert previous == replacement;
-                }
+                outValue.clearDebugUsers();
+              }
+              // We are not using `removeOrReplaceByDebugLocalRead` here due to the backwards
+              // iteration.
+              if (outlinedInstruction.getDebugValues().isEmpty()) {
+                outlinedInstruction.remove();
+              } else {
+                DebugLocalRead replacement = new DebugLocalRead();
+                outlinedInstruction.replace(replacement);
+                Instruction previous = instructionIterator.previous();
+                assert previous == replacement;
               }
             }
           }
-
-          // Finally delete the outline marker.
-          outlineMarker.removeOrReplaceByDebugLocalRead();
-
-          // Blocks cannot start with DebugLocalRead.
-          while (block.entry().isDebugLocalRead()) {
-            block.entry().moveDebugValues(block.entry().getNext());
-            block.entry().remove();
-          }
         }
+
+        // Finally delete the outline marker.
+        outlineMarker.removeOrReplaceByDebugLocalRead();
+
+        // Blocks cannot start with DebugLocalRead.
+        while (block.entry().isDebugLocalRead()) {
+          block.entry().moveDebugValues(block.entry().getNext());
+          block.entry().remove();
+        }
+
+        // Continue searching for outline markers from the end of the current outline.
+        outlineMarker = outlineEnd.nextUntilExclusive(Instruction::isThrowBlockOutlineMarker);
       }
       assert block.streamInstructions().noneMatch(Instruction::isThrowBlockOutlineMarker);
     }
@@ -216,4 +230,15 @@
       return null;
     }
   }
+
+  private Instruction getOutlineEnd(
+      BasicBlock block, ThrowBlockOutline outline, ThrowBlockOutlineMarker outlineMarker) {
+    if (outline.isThrowOutline()) {
+      return block.exit();
+    } else {
+      // The end of a StringBuilder#toString outline is the call to StringBuilder#toString.
+      return outlineMarker.nextUntilExclusive(
+          i -> ThrowBlockOutlinerScanner.isStringBuilderToString(i, factory));
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
index 004281a..91aefd2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
@@ -227,17 +227,19 @@
 
     // Estimate the savings from this outline.
     int estimatedSavingsInBytes = 0;
-    for (Multiset.Entry<DexMethod> entry : outline.getUsers().entrySet()) {
-      // For each call we save the outlined instructions at the cost of an invoke + return.
-      int estimatedSavingsForUser = codeSizeInBytes - (DexInvokeStatic.SIZE + DexReturn.SIZE);
-      if (entry.getElement().getReturnType().isWideType()) {
-        estimatedSavingsForUser -= DexConstWide16.SIZE;
-      } else if (!entry.getElement().getReturnType().isVoidType()) {
-        estimatedSavingsForUser -= DexConst4.SIZE;
-      }
-      estimatedSavingsInBytes += estimatedSavingsForUser * entry.getCount();
-      if (estimatedSavingsInBytes > estimatedCostInBytes) {
-        return true;
+    for (ThrowBlockOutline outlineOrMergedOutline : outline.getAllOutlines()) {
+      for (Multiset.Entry<DexMethod> entry : outlineOrMergedOutline.getUsers().entrySet()) {
+        // For each call we save the outlined instructions at the cost of an invoke + return.
+        int estimatedSavingsForUser = codeSizeInBytes - (DexInvokeStatic.SIZE + DexReturn.SIZE);
+        if (entry.getElement().getReturnType().isWideType()) {
+          estimatedSavingsForUser -= DexConstWide16.SIZE;
+        } else if (!entry.getElement().getReturnType().isVoidType()) {
+          estimatedSavingsForUser -= DexConst4.SIZE;
+        }
+        estimatedSavingsInBytes += estimatedSavingsForUser * entry.getCount();
+        if (estimatedSavingsInBytes > estimatedCostInBytes) {
+          return true;
+        }
       }
     }
     return false;
@@ -269,10 +271,12 @@
     ProgramMethodMap<ThrowBlockOutline> methodsToReprocess = ProgramMethodMap.create();
     Set<DexMethod> seenUsers = Sets.newIdentityHashSet();
     for (ThrowBlockOutline outline : outlines) {
-      for (DexMethod user : outline.getUsers()) {
-        if (seenUsers.add(user)) {
-          ProgramMethod methodToReprocess = appView.definitionFor(user).asProgramMethod();
-          methodsToReprocess.put(methodToReprocess, null);
+      for (ThrowBlockOutline outlineOrMergedOutline : outline.getAllOutlines()) {
+        for (DexMethod user : outlineOrMergedOutline.getUsers().elementSet()) {
+          if (seenUsers.add(user)) {
+            ProgramMethod methodToReprocess = appView.definitionFor(user).asProgramMethod();
+            methodsToReprocess.put(methodToReprocess, null);
+          }
         }
       }
       if (outline.getMaterializedOutlineMethod() != null) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
index 7f6ea7d..ce48c06 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
@@ -15,6 +15,10 @@
       SystemPropertyUtils.parseSystemPropertyOrDefault(
           "com.android.tools.r8.throwblockoutliner.enable", false);
 
+  public boolean enableStringBuilderOutlining =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.throwblockoutliner.enablestringbuilder", false);
+
   public final int costInBytesForTesting =
       SystemPropertyUtils.parseSystemPropertyOrDefault(
           "com.android.tools.r8.throwblockoutliner.cost", -1);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
index 840483a..c3427e0e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
@@ -13,7 +13,6 @@
 import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
 import static com.android.tools.r8.ir.code.Opcodes.MOVE;
 import static com.android.tools.r8.ir.code.Opcodes.NEW_INSTANCE;
-import static java.util.Collections.emptyList;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -43,6 +42,7 @@
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
+import com.android.tools.r8.ir.code.Return;
 import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
 import com.android.tools.r8.ir.code.Value;
@@ -50,9 +50,9 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.timing.Timing;
-import com.google.common.base.Equivalence.Wrapper;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -61,7 +61,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 
 public class ThrowBlockOutlinerScanner {
@@ -70,13 +69,13 @@
 
   private final AppView<?> appView;
   private final DexItemFactory factory;
+  private final OutlineCollection outlines;
   private final AbstractValueFactory valueFactory;
 
-  private final Map<Wrapper<LirCode<?>>, ThrowBlockOutline> outlines = new ConcurrentHashMap<>();
-
   ThrowBlockOutlinerScanner(AppView<?> appView) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
+    this.outlines = new OutlineCollection(appView);
     this.valueFactory =
         appView.enableWholeProgramOptimizations()
             ? appView.abstractValueFactory()
@@ -85,8 +84,37 @@
 
   public void run(IRCode code) {
     assert !code.metadata().mayHaveThrowBlockOutlineMarker();
-    for (BasicBlock block : getThrowBlocks(code)) {
-      new ThrowBlockOutlinerScannerForBlock(code, block).processThrowBlock();
+    if (IterableUtils.none(code.getBlocks(), BasicBlock::isReturnBlock)) {
+      // Don't outline from methods that unconditionally throw.
+      // They may be manually created throw block outlines.
+      return;
+    }
+    for (BasicBlock block : code.getBlocks()) {
+      if (block.exit().isThrow()) {
+        new ThrowBlockOutlinerScannerForThrow(code, block).tryBuildOutline();
+      }
+      if (!appView.options().getThrowBlockOutlinerOptions().enableStringBuilderOutlining) {
+        continue;
+      }
+      Instruction previousOutlineEnd = null;
+      for (Instruction instruction : block.getInstructions()) {
+        // If we encounter an outline marker it is because we were able to outline the tail of the
+        // block above. Since the remainder of the block has been outlined, abort further outlining.
+        if (instruction.isThrowBlockOutlineMarker()) {
+          assert block.exit().isThrow();
+          break;
+        }
+        if (isStringBuilderToString(instruction, factory)) {
+          InvokeVirtual invoke = instruction.asInvokeVirtual();
+          ThrowBlockOutline outline =
+              new ThrowBlockOutlinerScannerForStringBuilderToString(
+                      code, block, invoke, previousOutlineEnd)
+                  .tryBuildOutline();
+          if (outline != null) {
+            previousOutlineEnd = instruction;
+          }
+        }
+      }
     }
     if (code.metadata().mayHaveThrowBlockOutlineMarker()) {
       if (appView.enableWholeProgramOptimizations()) {
@@ -105,136 +133,71 @@
   }
 
   public Collection<ThrowBlockOutline> getOutlines() {
-    return outlines.values();
+    return outlines.getOutlines();
   }
 
-  private List<BasicBlock> getThrowBlocks(IRCode code) {
-    boolean seenReturn = false;
-    List<BasicBlock> throwBlocks = new ArrayList<>();
-    for (BasicBlock block : code.getBlocks()) {
-      if (block.exit().isReturn()) {
-        seenReturn = true;
-      } else if (block.exit().isThrow()) {
-        throwBlocks.add(block);
-      }
-    }
-    // Never outline from methods that always throw.
-    return seenReturn ? throwBlocks : emptyList();
+  static boolean isStringBuilderToString(Instruction instruction, DexItemFactory factory) {
+    InvokeVirtual invoke = instruction.asInvokeVirtual();
+    return invoke != null
+        && invoke.getInvokedMethod().match(factory.objectMembers.toString)
+        && invoke.getReceiver().getType().isClassType(factory.stringBuilderType)
+        && invoke.hasOutValue();
   }
 
-  private class ThrowBlockOutlinerScannerForBlock {
+  private abstract class ThrowBlockOutlinerScannerForInstruction {
 
-    private final IRCode code;
-    private final Value exceptionValue;
-    private final BasicBlock throwBlock;
-    private final Throw throwInstruction;
+    final IRCode code;
+    final BasicBlock block;
+    final Instruction previousOutlineEnd;
 
     private boolean hasRunPrefixer;
 
-    ThrowBlockOutlinerScannerForBlock(IRCode code, BasicBlock throwBlock) {
+    ThrowBlockOutlinerScannerForInstruction(
+        IRCode code, BasicBlock block, Instruction previousOutlineEnd) {
       this.code = code;
-      this.throwBlock = throwBlock;
-      this.throwInstruction = throwBlock.exit().asThrow();
-      this.exceptionValue = throwInstruction.exception();
+      this.block = block;
+      this.previousOutlineEnd = previousOutlineEnd;
     }
 
-    private void processThrowBlock() {
-      // Recursively build up the outline method. On successful outline creation, the resulting
-      // LirCode is passed to the continuation function.
-      processThrowInstruction(
-          outlineBuilder -> {
-            // On successful outline creation, store the outline for later processing.
-            DexProto proto = outlineBuilder.getProto(appView);
-            if (proto == null) {
+    void processInstruction(Instruction instruction, Consumer<OutlineBuilder> continuation) {
+      if (instruction != previousOutlineEnd) {
+        switch (instruction.opcode()) {
+          case ASSUME:
+            processAssume(instruction.asAssume(), continuation);
+            return;
+          case CONST_NUMBER:
+          case CONST_STRING:
+            processConstInstruction(instruction.asConstInstruction(), continuation);
+            return;
+          case DEBUG_LOCAL_READ:
+          case DEBUG_POSITION:
+            processNonMaterializingDebugInstruction(instruction, continuation);
+            return;
+          case INVOKE_DIRECT:
+            if (instruction.isInvokeConstructor(factory)) {
+              processStringBuilderConstructorCall(instruction.asInvokeDirect(), continuation);
               return;
             }
-            LirCode<?> lirCode = outlineBuilder.buildLirCode(appView, code.context());
-            Wrapper<LirCode<?>> lirCodeWrapper =
-                ThrowBlockOutlinerLirCodeEquivalence.get().wrap(lirCode);
-            ThrowBlockOutline outline =
-                outlines.computeIfAbsent(
-                    lirCodeWrapper, w -> new ThrowBlockOutline(w.get(), proto));
-            assert proto.isIdenticalTo(outline.getProto());
-            List<Value> arguments = outlineBuilder.buildArguments();
-            outline.addUser(code.reference(), arguments, getAbstractValueFactory());
-
-            // Insert a synthetic marker instruction that references the outline so that we know
-            // where to materialize the outline call.
-            Instruction insertionPoint = outlineBuilder.getFirstOutlinedInstruction();
-            assert insertionPoint.getBlock() == throwBlock;
-            ThrowBlockOutlineMarker marker =
-                ThrowBlockOutlineMarker.builder()
-                    .setArguments(arguments)
-                    .setOutline(outline)
-                    .setPosition(Position.none())
-                    .build();
-            throwBlock.listIterator(insertionPoint).add(marker);
-          });
-    }
-
-    private void processThrowInstruction(Consumer<OutlineBuilder> continuation) {
-      if (!exceptionValue.isDefinedByInstructionSatisfying(
-          i -> i.isNewInstance() && i.getBlock() == throwBlock)) {
-        // Exception is not created in the throw block.
-        return;
-      }
-      assert throwInstruction.hasPrev();
-      // We always expect the constructor call corresponding to the thrown exception to be last.
-      processExceptionConstructorCall(
-          throwInstruction.getPrev(),
-          outlineBuilder -> {
-            Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(exceptionValue);
-            if (outlinedExceptionValue == null) {
-              // Fail as we were unable to outline the corresponding new-instance instruction.
+            break;
+          case INVOKE_STATIC:
+            processStringFormatOrValueOf(instruction.asInvokeStatic(), continuation);
+            return;
+          case INVOKE_VIRTUAL:
+            processStringBuilderAppendOrToString(instruction.asInvokeVirtual(), continuation);
+            return;
+          case MOVE:
+            if (instruction.isDebugLocalWrite()) {
+              processDebugLocalWrite(instruction.asDebugLocalWrite(), continuation);
               return;
             }
-            outlineBuilder.add(
-                Throw.builder()
-                    .setExceptionValue(outlinedExceptionValue)
-                    .setPosition(Position.syntheticNone())
-                    .build());
-            continuation.accept(outlineBuilder);
-          });
-    }
-
-    private void processInstruction(
-        Instruction instruction, Consumer<OutlineBuilder> continuation) {
-      switch (instruction.opcode()) {
-        case ASSUME:
-          processAssume(instruction.asAssume(), continuation);
-          return;
-        case CONST_NUMBER:
-        case CONST_STRING:
-          processConstInstruction(instruction.asConstInstruction(), continuation);
-          return;
-        case DEBUG_LOCAL_READ:
-        case DEBUG_POSITION:
-          processNonMaterializingDebugInstruction(instruction, continuation);
-          return;
-        case INVOKE_DIRECT:
-          if (instruction.isInvokeConstructor(factory)) {
-            processStringBuilderConstructorCall(instruction.asInvokeDirect(), continuation);
+            assert false;
+            break;
+          case NEW_INSTANCE:
+            processNewInstanceInstruction(instruction.asNewInstance(), continuation);
             return;
-          }
-          break;
-        case INVOKE_STATIC:
-          processStringFormatOrValueOf(instruction.asInvokeStatic(), continuation);
-          return;
-        case INVOKE_VIRTUAL:
-          processStringBuilderAppendOrToString(instruction.asInvokeVirtual(), continuation);
-          return;
-        case MOVE:
-          if (instruction.isDebugLocalWrite()) {
-            processDebugLocalWrite(instruction.asDebugLocalWrite(), continuation);
-            return;
-          }
-          assert false;
-          break;
-        case NEW_INSTANCE:
-          processNewInstanceInstruction(instruction.asNewInstance(), continuation);
-          return;
-        default:
-          break;
+          default:
+            break;
+        }
       }
       // Unhandled instruction. Start the outline at the successor instruction.
       startOutline(instruction.getNext(), continuation);
@@ -264,7 +227,7 @@
       processPredecessorInstructionOrStartOutline(instruction, continuation);
     }
 
-    private void processPredecessorInstructionOrFail(
+    void processPredecessorInstructionOrFail(
         Instruction instruction, Consumer<OutlineBuilder> continuation) {
       if (instruction.hasPrev()) {
         processInstruction(instruction.getPrev(), continuation);
@@ -284,12 +247,16 @@
 
     private void processNewInstanceInstruction(
         NewInstance newInstance, Consumer<OutlineBuilder> continuation) {
-      if (newInstance.outValue() != exceptionValue
-          && newInstance.getType().isNotIdenticalTo(appView.dexItemFactory().stringBuilderType)) {
+      if (newInstance.getType().isNotIdenticalTo(appView.dexItemFactory().stringBuilderType)) {
         // Unhandled instruction.
         startOutline(newInstance.getNext(), continuation);
         return;
       }
+      internalProcessNewInstanceInstruction(newInstance, continuation);
+    }
+
+    void internalProcessNewInstanceInstruction(
+        NewInstance newInstance, Consumer<OutlineBuilder> continuation) {
       processPredecessorInstructionOrStartOutline(
           newInstance,
           outlineBuilder -> {
@@ -298,8 +265,7 @@
           });
     }
 
-    private void outlineNewInstanceInstruction(
-        NewInstance newInstance, OutlineBuilder outlineBuilder) {
+    void outlineNewInstanceInstruction(NewInstance newInstance, OutlineBuilder outlineBuilder) {
       NewInstance outlinedNewInstance =
           NewInstance.builder()
               .setFreshOutValue(
@@ -312,51 +278,6 @@
       outlineBuilder.map(newInstance.outValue(), outlinedNewInstance.outValue());
     }
 
-    private void processExceptionConstructorCall(
-        Instruction instruction, Consumer<OutlineBuilder> continuation) {
-      InvokeDirect invoke = instruction.asInvokeConstructor(factory);
-      if (invoke == null) {
-        // Not a constructor call.
-        return;
-      }
-      assert !exceptionValue.hasDebugUsers();
-      assert !exceptionValue.hasPhiUsers();
-      if (invoke.getReceiver() != exceptionValue) {
-        // Not the constructor call corresponding to the thrown exception.
-        return;
-      }
-      // This instruction is guaranteed to have a predecessor since the handling of the throw
-      // instruction checks if the new-instance instruction is in the throw block.
-      assert instruction.hasPrev();
-      processPredecessorInstructionOrFail(
-          invoke,
-          outlineBuilder -> {
-            if (outlineBuilder.getOutlinedValue(exceptionValue) == null) {
-              // We were unable to outline the corresponding new-instance instruction. Check if we
-              // can insert it here, right before the constructor call.
-              NewInstance newInstance = exceptionValue.getDefinition().asNewInstance();
-              if (exceptionValue.uniqueUsers().size() == 2
-                  && !newInstance.instructionMayHaveSideEffects(appView, code.context())) {
-                // The exception value is only used by the constructor call and the throw.
-                // By construction, it cannot have debug users nor phi users.
-                outlineNewInstanceInstruction(newInstance, outlineBuilder);
-              } else {
-                // Fail as we were unable to outline the corresponding new-instance instruction.
-                return;
-              }
-            }
-            outlineBuilder.add(
-                InvokeDirect.builder()
-                    .setArguments(
-                        ListUtils.map(
-                            invoke.arguments(), outlineBuilder::getOutlinedValueOrCreateArgument))
-                    .setMethod(invoke.getInvokedMethod())
-                    .setPosition(Position.syntheticNone())
-                    .build());
-            continuation.accept(outlineBuilder);
-          });
-    }
-
     private void processStringBuilderConstructorCall(
         InvokeDirect invoke, Consumer<OutlineBuilder> continuation) {
       if (!factory.stringBuilderMethods.isConstructorMethod(invoke.getInvokedMethod())) {
@@ -415,7 +336,7 @@
           });
     }
 
-    private void processStringBuilderAppendOrToString(
+    void processStringBuilderAppendOrToString(
         InvokeVirtual invoke, Consumer<OutlineBuilder> continuation) {
       DexMethod invokedMethod = invoke.getInvokedMethod();
       if (!factory.stringBuilderMethods.isAppendMethod(invokedMethod)
@@ -459,7 +380,7 @@
         newFirstOutlinedInstruction = firstOutlinedInstruction;
       } else {
         newFirstOutlinedInstruction =
-            new ThrowBlockOutlinerPrefixer(factory, throwBlock)
+            new ThrowBlockOutlinerPrefixer(factory, block)
                 .tryMoveNonOutlinedStringBuilderInstructionsToOutline(firstOutlinedInstruction);
         hasRunPrefixer = true;
       }
@@ -472,6 +393,194 @@
     }
   }
 
+  private class ThrowBlockOutlinerScannerForStringBuilderToString
+      extends ThrowBlockOutlinerScannerForInstruction {
+
+    private final InvokeVirtual stringBuilderToStringInstruction;
+
+    private ThrowBlockOutline outline;
+
+    ThrowBlockOutlinerScannerForStringBuilderToString(
+        IRCode code,
+        BasicBlock block,
+        InvokeVirtual stringBuilderToStringInstruction,
+        Instruction previousOutlineEnd) {
+      super(code, block, previousOutlineEnd);
+      this.stringBuilderToStringInstruction = stringBuilderToStringInstruction;
+    }
+
+    ThrowBlockOutline tryBuildOutline() {
+      // Recursively build up the outline method. On successful outline creation, the resulting
+      // LirCode is passed to the continuation function.
+      processStringBuilderToString(
+          outlineBuilder -> {
+            // On successful outline creation, store the outline for later processing.
+            DexProto proto = outlineBuilder.getProto(appView, factory.stringType);
+            if (proto == null) {
+              return;
+            }
+            LirCode<?> lirCode = outlineBuilder.buildLirCode(appView, code.context());
+            outline = outlines.add(lirCode, proto, code.context());
+            assert proto.isIdenticalTo(outline.getProto());
+            List<Value> arguments = outlineBuilder.buildArguments();
+            outline.addUser(code.reference(), arguments, getAbstractValueFactory());
+
+            // Insert a synthetic marker instruction that references the outline so that we know
+            // where to materialize the outline call.
+            Instruction insertionPoint = outlineBuilder.getFirstOutlinedInstruction();
+            assert insertionPoint.getBlock() == block;
+            ThrowBlockOutlineMarker marker =
+                ThrowBlockOutlineMarker.builder()
+                    .setArguments(arguments)
+                    .setOutline(outline)
+                    .setPosition(Position.none())
+                    .build();
+            block.listIterator(insertionPoint).add(marker);
+          });
+      return outline;
+    }
+
+    private void processStringBuilderToString(Consumer<OutlineBuilder> continuation) {
+      processStringBuilderAppendOrToString(
+          stringBuilderToStringInstruction,
+          outlineBuilder -> {
+            Value outlinedStringValue =
+                outlineBuilder.getOutlinedValue(stringBuilderToStringInstruction.outValue());
+            outlineBuilder.add(
+                Return.builder()
+                    .setPosition(Position.syntheticNone())
+                    .setReturnValue(outlinedStringValue)
+                    .build());
+            continuation.accept(outlineBuilder);
+          });
+    }
+  }
+
+  private class ThrowBlockOutlinerScannerForThrow extends ThrowBlockOutlinerScannerForInstruction {
+
+    private final Throw throwInstruction;
+
+    ThrowBlockOutlinerScannerForThrow(IRCode code, BasicBlock block) {
+      super(code, block, null);
+      this.throwInstruction = block.exit().asThrow();
+    }
+
+    void tryBuildOutline() {
+      // Recursively build up the outline method. On successful outline creation, the resulting
+      // LirCode is passed to the continuation function.
+      processThrowInstruction(
+          outlineBuilder -> {
+            // On successful outline creation, store the outline for later processing.
+            DexProto proto = outlineBuilder.getProto(appView, factory.voidType);
+            if (proto == null) {
+              return;
+            }
+            LirCode<?> lirCode = outlineBuilder.buildLirCode(appView, code.context());
+            ThrowBlockOutline outline = outlines.add(lirCode, proto, code.context());
+            assert proto.isIdenticalTo(outline.getProto());
+            List<Value> arguments = outlineBuilder.buildArguments();
+            outline.addUser(code.reference(), arguments, getAbstractValueFactory());
+
+            // Insert a synthetic marker instruction that references the outline so that we know
+            // where to materialize the outline call.
+            Instruction insertionPoint = outlineBuilder.getFirstOutlinedInstruction();
+            assert insertionPoint.getBlock() == block;
+            ThrowBlockOutlineMarker marker =
+                ThrowBlockOutlineMarker.builder()
+                    .setArguments(arguments)
+                    .setOutline(outline)
+                    .setPosition(Position.none())
+                    .build();
+            block.listIterator(insertionPoint).add(marker);
+          });
+    }
+
+    private void processThrowInstruction(Consumer<OutlineBuilder> continuation) {
+      Throw throwInstruction = block.exit().asThrow();
+      Value exceptionValue = throwInstruction.exception();
+      if (!exceptionValue.isDefinedByInstructionSatisfying(
+          i -> i.isNewInstance() && i.getBlock() == block)) {
+        // Exception is not created in the throw block.
+        return;
+      }
+      assert throwInstruction.hasPrev();
+      // We always expect the constructor call corresponding to the thrown exception to be last.
+      processExceptionConstructorCall(
+          throwInstruction.getPrev(),
+          outlineBuilder -> {
+            Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(exceptionValue);
+            if (outlinedExceptionValue == null) {
+              // Fail as we were unable to outline the corresponding new-instance instruction.
+              return;
+            }
+            outlineBuilder.add(
+                Throw.builder()
+                    .setExceptionValue(outlinedExceptionValue)
+                    .setPosition(Position.syntheticNone())
+                    .build());
+            continuation.accept(outlineBuilder);
+          });
+    }
+
+    private void processExceptionConstructorCall(
+        Instruction instruction, Consumer<OutlineBuilder> continuation) {
+      InvokeDirect invoke = instruction.asInvokeConstructor(factory);
+      if (invoke == null) {
+        // Not a constructor call.
+        return;
+      }
+      Value exceptionValue = block.exit().asThrow().exception();
+      assert !exceptionValue.hasDebugUsers();
+      assert !exceptionValue.hasPhiUsers();
+      if (invoke.getReceiver() != exceptionValue) {
+        // Not the constructor call corresponding to the thrown exception.
+        return;
+      }
+      // This instruction is guaranteed to have a predecessor since the handling of the throw
+      // instruction checks if the new-instance instruction is in the throw block.
+      assert instruction.hasPrev();
+      processPredecessorInstructionOrFail(
+          invoke,
+          outlineBuilder -> {
+            if (outlineBuilder.getOutlinedValue(exceptionValue) == null) {
+              // We were unable to outline the corresponding new-instance instruction. Check if we
+              // can insert it here, right before the constructor call.
+              NewInstance newInstance = exceptionValue.getDefinition().asNewInstance();
+              if (exceptionValue.uniqueUsers().size() == 2
+                  && !newInstance.instructionMayHaveSideEffects(appView, code.context())) {
+                // The exception value is only used by the constructor call and the throw.
+                // By construction, it cannot have debug users nor phi users.
+                outlineNewInstanceInstruction(newInstance, outlineBuilder);
+              } else {
+                // Fail as we were unable to outline the corresponding new-instance instruction.
+                return;
+              }
+            }
+            outlineBuilder.add(
+                InvokeDirect.builder()
+                    .setArguments(
+                        ListUtils.map(
+                            invoke.arguments(), outlineBuilder::getOutlinedValueOrCreateArgument))
+                    .setMethod(invoke.getInvokedMethod())
+                    .setPosition(Position.syntheticNone())
+                    .build());
+            continuation.accept(outlineBuilder);
+          });
+    }
+
+    @Override
+    void processInstruction(Instruction instruction, Consumer<OutlineBuilder> continuation) {
+      if (instruction.isNewInstance()) {
+        NewInstance newInstance = instruction.asNewInstance();
+        if (newInstance.outValue() == throwInstruction.exception()) {
+          internalProcessNewInstanceInstruction(newInstance, continuation);
+          return;
+        }
+      }
+      super.processInstruction(instruction, continuation);
+    }
+  }
+
   private static class OutlineBuilder {
 
     private static final AliasedValueConfiguration aliasing =
@@ -545,9 +654,8 @@
       return addArgument(root).outValue();
     }
 
-    DexProto getProto(AppView<?> appView) {
+    DexProto getProto(AppView<?> appView, DexType returnType) {
       DexItemFactory factory = appView.dexItemFactory();
-      DexType returnType = factory.voidType;
       List<DexType> parameters = new ArrayList<>(outlinedArguments.size());
       for (Argument outlinedArgument : outlinedArguments) {
         TypeElement useType =
@@ -556,6 +664,10 @@
           // Instead of returning null here we could consider removing the parameter.
           return null;
         }
+        if (useType.isTop()) {
+          assert appView.options().canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug();
+          return null;
+        }
         parameters.add(DexTypeUtils.toDexType(factory, useType));
       }
       return factory.createProto(returnType, parameters);
diff --git a/src/main/java/com/android/tools/r8/shaking/AssumeInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/AssumeInfoCollection.java
index 016d1a0..c327aa2 100644
--- a/src/main/java/com/android/tools/r8/shaking/AssumeInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/AssumeInfoCollection.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMember;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.GraphLens;
@@ -46,6 +47,10 @@
     return get(member.getReference());
   }
 
+  public AssumeInfo get(DexEncodedMember<?, ?> member) {
+    return get(member.getReference());
+  }
+
   public boolean isEmpty() {
     return backing.isEmpty();
   }
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 3906ea2..2212b9b 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -44,7 +44,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
-import com.android.tools.r8.graph.InvalidCode;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
@@ -55,8 +54,6 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper;
-import com.android.tools.r8.ir.desugar.itf.InterfaceMethodDesugaringMode;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfo;
 import com.android.tools.r8.partial.R8PartialResourceUseCollector;
@@ -129,7 +126,6 @@
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
     private AssumeInfoCollection.Builder assumeInfoCollectionBuilder;
-    private final RootSetBuilderEventConsumer eventConsumer;
     private final ImmediateAppSubtypingInfo subtypingInfo;
     private final DirectMappedDexApplication application;
     private final Set<DexType> rootNonProgramTypes = Sets.newIdentityHashSet();
@@ -160,7 +156,6 @@
 
     private final OptimizationFeedbackSimple feedback = OptimizationFeedbackSimple.getInstance();
 
-    private final InterfaceDesugaringSyntheticHelper interfaceDesugaringSyntheticHelper;
     private final ProgramMethodMap<ProgramMethod> pendingMethodMoveInverse =
         ProgramMethodMap.create();
 
@@ -172,22 +167,13 @@
 
     private RootSetBuilder(
         AppView<? extends AppInfoWithClassHierarchy> appView,
-        RootSetBuilderEventConsumer eventConsumer,
         ImmediateAppSubtypingInfo subtypingInfo,
         Iterable<? extends ProguardConfigurationRule> rules) {
       this.appView = appView;
-      this.eventConsumer = eventConsumer;
       this.subtypingInfo = subtypingInfo;
       this.application = appView.appInfo().app().asDirect();
       this.rules = rules;
       this.options = appView.options();
-      interfaceDesugaringSyntheticHelper =
-          options.isInterfaceMethodDesugaringEnabled()
-              ? new InterfaceDesugaringSyntheticHelper(
-                  appView,
-                  InterfaceMethodDesugaringMode.createForInterfaceMethodDesugaringInRootSetBuilder(
-                      options))
-              : null;
       attributesConfig =
           options.getProguardConfiguration() != null
               ? options.getProguardConfiguration().getKeepAttributes()
@@ -208,11 +194,9 @@
 
     private RootSetBuilder(
         AppView<? extends AppInfoWithClassHierarchy> appView,
-        Enqueuer enqueuer,
         ImmediateAppSubtypingInfo subtypingInfo) {
       this(
           appView,
-          RootSetBuilderEventConsumer.create(enqueuer.getProfileCollectionAdditions()),
           subtypingInfo,
           null);
     }
@@ -1910,35 +1894,9 @@
         preconditionEvent = UnconditionalKeepInfoEvent.get();
       }
 
-      if (isInterfaceMethodNeedingDesugaring(item)) {
-        ProgramMethod method = item.asMethod();
-        ProgramMethod companion =
-            interfaceDesugaringSyntheticHelper.ensureMethodOfProgramCompanionClassStub(
-                method, eventConsumer);
-        // Add the method to the inverse map as tracing will now directly target the CC method.
-        if (InvalidCode.isInvalidCode(companion.getDefinition().getCode())) {
-          pendingMethodMoveInverse.put(companion, method);
-        }
-
-        LazyBox<Joiner<?, ?, ?>> companionJoiner =
-            new LazyBox<>(
-                () ->
-                    dependentMinimumKeepInfo.getOrCreateMinimumKeepInfoFor(
-                        preconditionEvent, companion.getReference()));
-
-        // Only shrinking and optimization are transferred for interface companion methods.
-        if (appView.options().isOptimizationEnabled() && !modifiers.allowsOptimization) {
-          companionJoiner.computeIfAbsent().disallowOptimization();
-          markAsUsed.execute();
-        }
-        if (appView.options().isShrinking() && !modifiers.allowsShrinking) {
-          companionJoiner.computeIfAbsent().addRule(whyAreYouKeepingKeepRule).disallowShrinking();
-          markAsUsed.execute();
-        }
-        if (!item.asMethod().isDefaultMethod()) {
-          // Static and private methods do not apply to the original item.
-          return;
-        }
+      if (isInterfaceMethodNeedingDesugaring(item) && !item.asMethod().isDefaultMethod()) {
+        // Static and private methods do not apply to the original item.
+        return;
       }
 
       // Memoize the joiner to avoid repeated lookups and to validate it as non-bottom if set.
@@ -2546,7 +2504,7 @@
         AppView<? extends AppInfoWithClassHierarchy> appView,
         Enqueuer enqueuer,
         ImmediateAppSubtypingInfo subtypingInfo) {
-      return new RootSetBuilder(appView, enqueuer, subtypingInfo);
+      return new RootSetBuilder(appView, subtypingInfo);
     }
 
     public static RootSetBuilder builder(
@@ -2556,7 +2514,6 @@
         Iterable<? extends ProguardConfigurationRule> rules) {
       return new RootSetBuilder(
           appView,
-          RootSetBuilderEventConsumer.create(profileCollectionAdditions),
           subtypingInfo,
           rules);
     }
@@ -2572,7 +2529,6 @@
         ImmediateAppSubtypingInfo subtypingInfo) {
       super(
           appView,
-          RootSetBuilderEventConsumer.create(enqueuer.getProfileCollectionAdditions()),
           subtypingInfo,
           null);
       this.enqueuer = enqueuer;
@@ -2614,12 +2570,10 @@
 
     private MainDexRootSetBuilder(
         AppView<? extends AppInfoWithClassHierarchy> appView,
-        ProfileCollectionAdditions profileCollectionAdditions,
         ImmediateAppSubtypingInfo subtypingInfo,
         Iterable<? extends ProguardConfigurationRule> rules) {
       super(
           appView,
-          RootSetBuilderEventConsumer.create(profileCollectionAdditions),
           subtypingInfo,
           rules);
     }
@@ -2667,10 +2621,9 @@
 
     public static MainDexRootSetBuilder builder(
         AppView<? extends AppInfoWithClassHierarchy> appView,
-        ProfileCollectionAdditions profileCollectionAdditions,
         ImmediateAppSubtypingInfo subtypingInfo,
         Iterable<? extends ProguardConfigurationRule> rules) {
-      return new MainDexRootSetBuilder(appView, profileCollectionAdditions, subtypingInfo, rules);
+      return new MainDexRootSetBuilder(appView, subtypingInfo, rules);
     }
 
     @Override
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 2cf24ca..982ec3f 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -3169,10 +3169,10 @@
     return true;
   }
 
-  // Art 7 and up can fail access check for array clone calls from within interface methods.
+  // Art 7 and up to 15 can fail access check for array clone calls from within interface methods.
   // See b/342802978.
   public boolean canHaveArtArrayCloneFromInterfaceMethodBug() {
-    return true;
+    return canHaveBugPresentUntilExclusive(AndroidApiLevel.BAKLAVA);
   }
 
   // The dalvik verifier will crash the program if there is a try catch block with an exception
diff --git a/src/main/java/com/android/tools/r8/utils/IterableUtils.java b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
index acd31a4..bf62ee6 100644
--- a/src/main/java/com/android/tools/r8/utils/IterableUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
@@ -143,6 +143,15 @@
     return min;
   }
 
+  public static <T> boolean none(Iterable<T> iterable, Predicate<T> predicate) {
+    for (T element : iterable) {
+      if (predicate.test(element)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   @SuppressWarnings("UnusedVariable")
   public static <T> int size(Iterable<T> iterable) {
     int result = 0;
diff --git a/src/test/examplesJava9/twr/twrcloseresourceduplication/TwrCloseResourceDuplicationTest.java b/src/test/examplesJava9/twr/twrcloseresourceduplication/TwrCloseResourceDuplicationTest.java
index 471dffb..3383085 100644
--- a/src/test/examplesJava9/twr/twrcloseresourceduplication/TwrCloseResourceDuplicationTest.java
+++ b/src/test/examplesJava9/twr/twrcloseresourceduplication/TwrCloseResourceDuplicationTest.java
@@ -175,10 +175,12 @@
                               Reference.classFromTypeName(BAR), 1)
                           .getTypeName());
                 }
-                classOutputWithSynthetics.add(
-                    SyntheticItemsTestUtils.syntheticAutoCloseableDispatcherClass(
-                            Reference.classFromTypeName(BAR), 0)
-                        .getTypeName());
+                if (!parameters.corelibWithExecutorServiceImplementingAutoClosable()) {
+                  classOutputWithSynthetics.add(
+                      SyntheticItemsTestUtils.syntheticAutoCloseableDispatcherClass(
+                              Reference.classFromTypeName(BAR), 0)
+                          .getTypeName());
+                }
                 assertEquals(classOutputWithSynthetics, foundClasses);
               }
             });
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 535c25f..cc5e538 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -101,6 +101,10 @@
               Version.V15_0_0,
               // TODO(120402963) Triage.
               ImmutableList.of("invokecustom-with-shrinking", "invokecustom2-with-shrinking"))
+          .put(
+              Version.V16_0_0,
+              // TODO(120402963) Triage.
+              ImmutableList.of("invokecustom-with-shrinking", "invokecustom2-with-shrinking"))
           .put(Version.DEFAULT, ImmutableList.of())
           .build();
 
diff --git a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
index 17e87f0..cd24893 100644
--- a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
@@ -131,56 +131,38 @@
                   "sparse-switch", "regression/33846227"));
 
   // Tests where the output has a different output than the original on certain VMs.
+  private static final Map<String, String> dalvikExpectations =
+      ImmutableMap.of(
+          "bad-codegen", "java.lang.NullPointerException\n",
+          "type-confusion-regression2", "java.lang.NullPointerException\n",
+          "type-confusion-regression3", "java.lang.NullPointerException\n",
+          "merge-blocks-regression", "java.lang.NullPointerException\n");
+  private static final Map<String, String> art13PlusExpectations =
+      ImmutableMap.of(
+          "bad-codegen",
+          StringUtils.lines(
+              "java.lang.NullPointerException: Attempt to read from field 'Test Test.a'"
+                  + " on a null object reference in method 'Test TestObject.a(Test,"
+                  + " Test, Test, Test, boolean)'"),
+          "type-confusion-regression3",
+          StringUtils.lines(
+              "java.lang.NullPointerException: Attempt to read from field 'byte[]"
+                  + " Test.a' on a null object reference in method 'int"
+                  + " TestObject.a(Test, Test)'"));
   private static final Map<DexVm.Version, Map<String, String>> customProcessedOutputExpectation =
       ImmutableMap.of(
           Version.V4_4_4,
-          ImmutableMap.of(
-              "bad-codegen", "java.lang.NullPointerException\n",
-              "type-confusion-regression2", "java.lang.NullPointerException\n",
-              "type-confusion-regression3", "java.lang.NullPointerException\n",
-              "merge-blocks-regression", "java.lang.NullPointerException\n"),
+          dalvikExpectations,
           Version.V4_0_4,
-          ImmutableMap.of(
-              "bad-codegen", "java.lang.NullPointerException\n",
-              "type-confusion-regression2", "java.lang.NullPointerException\n",
-              "type-confusion-regression3", "java.lang.NullPointerException\n",
-              "merge-blocks-regression", "java.lang.NullPointerException\n"),
+          dalvikExpectations,
           Version.V13_0_0,
-          ImmutableMap.of(
-              "bad-codegen",
-              StringUtils.lines(
-                  "java.lang.NullPointerException: Attempt to read from field 'Test Test.a'"
-                      + " on a null object reference in method 'Test TestObject.a(Test,"
-                      + " Test, Test, Test, boolean)'"),
-              "type-confusion-regression3",
-              StringUtils.lines(
-                  "java.lang.NullPointerException: Attempt to read from field 'byte[]"
-                      + " Test.a' on a null object reference in method 'int"
-                      + " TestObject.a(Test, Test)'")),
+          art13PlusExpectations,
           Version.V14_0_0,
-          ImmutableMap.of(
-              "bad-codegen",
-                  StringUtils.lines(
-                      "java.lang.NullPointerException: Attempt to read from field 'Test Test.a'"
-                          + " on a null object reference in method 'Test TestObject.a(Test,"
-                          + " Test, Test, Test, boolean)'"),
-              "type-confusion-regression3",
-                  StringUtils.lines(
-                      "java.lang.NullPointerException: Attempt to read from field 'byte[]"
-                          + " Test.a' on a null object reference in method 'int"
-                          + " TestObject.a(Test, Test)'")),
+          art13PlusExpectations,
           Version.V15_0_0,
-          ImmutableMap.of(
-              "bad-codegen",
-              StringUtils.lines(
-                  "java.lang.NullPointerException: Attempt to read from field 'Test Test.a'"
-                      + " on a null object reference in method 'Test TestObject.a(Test,"
-                      + " Test, Test, Test, boolean)'"),
-              "type-confusion-regression3",
-              StringUtils.lines(
-                  "java.lang.NullPointerException: Attempt to read from field 'byte[]"
-                      + " Test.a' on a null object reference in method 'int"
-                      + " TestObject.a(Test, Test)'")));
+          art13PlusExpectations,
+          Version.V16_0_0,
+          art13PlusExpectations);
 
   // Tests where the input fails with a verification error on Dalvik instead of the
   // expected runtime exception.
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 0b3baa3..b3ba2d3 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -322,6 +322,11 @@
             ImmutableList.of(
                 // TODO(b/120402963): Triage.
                 "invokecustom", "invokecustom2"))
+        .put(
+            Version.V16_0_0,
+            ImmutableList.of(
+                // TODO(b/120402963): Triage.
+                "invokecustom", "invokecustom2"))
         .put(DexVm.Version.DEFAULT, ImmutableList.of());
     failsOn = builder.build();
   }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
index f72e4ac..c9315a2 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -47,6 +48,7 @@
         .apply(setMockApiLevelForClass(ApiLevel23.class, AndroidApiLevel.M))
         .apply(ApiModelingTestHelper::enableApiCallerIdentification)
         .apply(ApiModelingTestHelper::disableOutliningAndStubbing)
+        .enableInliningAnnotations()
         .setMinApi(parameters)
         .compile()
         .inspect(
@@ -85,6 +87,12 @@
       if (Version.getSdkInt(sdk) >= 22) {
         FIELD = new ApiLevel22();
       }
+      print();
+    }
+
+    // So that we don't eliminate the field as a result of redundant field load elimination.
+    @NeverInline
+    static void print() {
       System.out.println(FIELD);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
index 1f218a2..13ea62a 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -50,6 +51,7 @@
         .apply(setMockApiLevelForClass(ApiLevel23.class, AndroidApiLevel.M))
         .apply(ApiModelingTestHelper::enableApiCallerIdentification)
         .apply(ApiModelingTestHelper::disableOutliningAndStubbing)
+        .enableInliningAnnotations()
         .setMinApi(parameters)
         .compile()
         .inspect(
@@ -89,6 +91,12 @@
       if (Version.getSdkInt(sdk) >= 23) {
         FIELD = new ApiLevel23();
       }
+      print();
+    }
+
+    // So that we don't eliminate the field as a result of redundant field load elimination.
+    @NeverInline
+    static void print() {
       System.out.println(FIELD);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java b/src/test/java/com/android/tools/r8/assistant/R8AssistantReflectiveInstrumentationTest.java
similarity index 98%
rename from src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
rename to src/test/java/com/android/tools/r8/assistant/R8AssistantReflectiveInstrumentationTest.java
index bcf3dc6..31fe370 100644
--- a/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/R8AssistantReflectiveInstrumentationTest.java
@@ -29,7 +29,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class R8AssistentReflectiveInstrumentationTest extends TestBase {
+public class R8AssistantReflectiveInstrumentationTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
@@ -134,7 +134,7 @@
       }
       String topOfStack = stack.getStackTraceElements()[0].toString();
       String secondToTopOfStack = stack.getStackTraceElements()[1].toString();
-      String sourceFile = "R8AssistentReflectiveInstrumentationTest";
+      String sourceFile = "R8AssistantReflectiveInstrumentationTest";
       if (!topOfStack.contains("reflectOn(" + sourceFile)) {
         throw new RuntimeException("reflectOn must be top of stack, got " + topOfStack);
       }
diff --git a/src/test/java/com/android/tools/r8/checkdiscarded/CheckSubclassDiscardedEntirelyTest.java b/src/test/java/com/android/tools/r8/checkdiscarded/CheckSubclassDiscardedEntirelyTest.java
index 516998b..f14b254 100644
--- a/src/test/java/com/android/tools/r8/checkdiscarded/CheckSubclassDiscardedEntirelyTest.java
+++ b/src/test/java/com/android/tools/r8/checkdiscarded/CheckSubclassDiscardedEntirelyTest.java
@@ -121,11 +121,17 @@
     public static void main(String[] args) {
       Public.printPublic();
       Public.printPublicAllowInlining();
-      if (!shrink()) {
+      if (!callShrink()) {
         Secret.printSecret();
       }
     }
 
+    // Outline call to shrink() to avoid that the call to printSecret() is removed in the first
+    // round of tree shaking.
+    static boolean callShrink() {
+      return shrink();
+    }
+
     static boolean shrink() {
       throw new RuntimeException();
     }
diff --git a/src/test/java/com/android/tools/r8/debug/ExceptionTest.java b/src/test/java/com/android/tools/r8/debug/ExceptionTest.java
index 6778a46..5f45f46 100644
--- a/src/test/java/com/android/tools/r8/debug/ExceptionTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ExceptionTest.java
@@ -7,6 +7,7 @@
 
 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.debug.classes.Exceptions;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import org.junit.Test;
@@ -53,8 +54,10 @@
   @Test
   public void testStepOnCatchD8() throws Throwable {
     parameters.assumeDexRuntime();
-    // ART/Dalvik jumps to 'move-exception' which initializes the local variable with the pending
-    // exception. Thus it is "attached" to the line declaring the exception in the catch handler.
+    // ART/Dalvik until ART 15 jumps to 'move-exception' which initializes the local variable with
+    // the pending exception. Thus it is "attached" to the line declaring the exception in the
+    // catch handler. From ART 16 the stepping is the same as for the JVM.
+    boolean art15OrOlder = parameters.getDexRuntimeVersion().isOlderThanOrEqual(Version.V15_0_0);
     runDebugTest(
         testForD8(parameters.getBackend())
             .setMinApi(parameters)
@@ -66,9 +69,9 @@
         run(),
         checkLine(SOURCE_FILE, 11), // line of the method call throwing the exception
         stepOver(),
-        checkLine(SOURCE_FILE, 12), // line of the catch declaration
-        checkNoLocal("e"),
-        stepOver(),
+        applyIf(art15OrOlder, () -> checkLine(SOURCE_FILE, 12)), // line of the catch declaration
+        applyIf(art15OrOlder, () -> checkNoLocal("e")),
+        applyIf(art15OrOlder, this::stepOver),
         checkLine(SOURCE_FILE, 13), // first line in the catch handler
         checkLocal("e"),
         run());
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfacesTest.java b/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfacesTest.java
index 498db5c..df4566a 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfacesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfacesTest.java
@@ -23,14 +23,20 @@
 
   private final List<String> EXPECTED_RESULT_WITHOUT_DESUGARING =
       ImmutableList.of(
-          WithAnonymousInner.class.getName(), "true", WithLocalInner.class.getName(), "true");
+          WithAnonymousInner.class.getName(),
+          WithAnonymousInner.class.getName(),
+          WithLocalInner.class.getName(),
+          WithLocalInner.class.getName());
 
   private final List<String> EXPECTED_RESULT_WITH_DESUGARING =
       ImmutableList.of(
           WithAnonymousInner.class.getName() + getCompanionClassNameSuffix(),
-          "true",
+          WithAnonymousInner.class.getName() + getCompanionClassNameSuffix(),
           WithLocalInner.class.getName() + getCompanionClassNameSuffix(),
-          "true");
+          WithLocalInner.class.getName() + getCompanionClassNameSuffix());
+
+  private final List<String> EXPECTED_RESULT_WITH_DESUGARING_R8 =
+      ImmutableList.of("null", "null", "null", "null");
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
@@ -78,7 +84,7 @@
         .applyIf(
             parameters.isCfRuntime() || parameters.canUseDefaultAndStaticInterfaceMethods(),
             result -> result.assertSuccessWithOutputLines(EXPECTED_RESULT_WITHOUT_DESUGARING),
-            result -> result.assertSuccessWithOutputLines(EXPECTED_RESULT_WITH_DESUGARING));
+            result -> result.assertSuccessWithOutputLines(EXPECTED_RESULT_WITH_DESUGARING_R8));
   }
 
   @Test
@@ -95,7 +101,7 @@
         .applyIf(
             parameters.isCfRuntime() || parameters.canUseDefaultAndStaticInterfaceMethods(),
             result -> result.assertSuccessWithOutputLines(EXPECTED_RESULT_WITHOUT_DESUGARING),
-            result -> result.assertSuccessWithOutputLines(EXPECTED_RESULT_WITH_DESUGARING));
+            result -> result.assertSuccessWithOutputLines(EXPECTED_RESULT_WITH_DESUGARING_R8));
   }
 
   interface WithAnonymousInner {
@@ -143,16 +149,14 @@
   public static class TestClass {
 
     public static void main(String[] args) throws Exception {
-      System.out.println(new WithAnonymousInner() {}.defaultOuter().call().getName());
-      System.out.println(
-          new WithAnonymousInner() {}.defaultOuter()
-              .call()
-              .equals(WithAnonymousInner.staticOuter().call()));
-      System.out.println(new WithLocalInner() {}.defaultOuter().call().getName());
-      System.out.println(
-          new WithLocalInner() {}.defaultOuter()
-              .call()
-              .equals(WithLocalInner.staticOuter().call()));
+      System.out.println(getName(new WithAnonymousInner() {}.defaultOuter().call()));
+      System.out.println(getName(WithAnonymousInner.staticOuter().call()));
+      System.out.println(getName(new WithLocalInner() {}.defaultOuter().call()));
+      System.out.println(getName(WithLocalInner.staticOuter().call()));
+    }
+
+    static String getName(Class<?> clazz) {
+      return clazz != null ? clazz.getName() : "null";
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildBackportTest.java
index c680498..9d0537d 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildBackportTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildBackportTest.java
@@ -4,14 +4,15 @@
 
 package com.android.tools.r8.desugar.backports;
 
+import static com.android.tools.r8.utils.AndroidApiLevel.BAKLAVA;
+
 import com.android.tools.r8.D8TestBuilder;
 import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.desugar.backports.AndroidOsBuildVersionBackportTest.VERSION;
 import com.android.tools.r8.graph.AccessFlags;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -41,13 +42,24 @@
     super(parameters, ANDROID_OS_BUILD_TYPE_NAME, ImmutableList.of(getTestRunner()));
 
     // android.os.Build getMajorSdkVersion and getMinorSdkVersion added on API 36.
-    registerTarget(AndroidApiLevel.BAKLAVA, 4);
+    registerTarget(BAKLAVA, 4);
+  }
+
+  public void testD8() throws Exception {
+    testD8(
+        r ->
+            r.applyIf(
+                parameters.getApiLevel().isLessThan(BAKLAVA),
+                SingleTestRunResult::assertSuccess,
+                // No backporting from BAKLAVA, so android.os.Build not found (not in host ART
+                // runtime).
+                rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class)));
   }
 
   @Override
   protected void configure(D8TestBuilder builder) throws Exception {
     // Use BAKLAVA library to get API level for getMajorVersion() and getMinorVersion().
-    builder.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.BAKLAVA));
+    builder.addLibraryFiles(ToolHelper.getAndroidJar(BAKLAVA));
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionBackportTest.java
index 1838d12..3fc6119 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionBackportTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionBackportTest.java
@@ -57,6 +57,11 @@
             .writeToZip());
   }
 
+  @Override
+  protected String[] configureD8RunArguments() {
+    return new String[] {Integer.toString(parameters.getApiLevel().getLevel())};
+  }
+
   private static byte[] getTransformedBuildVERSIONClassForRuntimeClasspath()
       throws IOException, NoSuchFieldException {
     return transformer(VERSION.class)
@@ -85,7 +90,8 @@
   public static class TestRunner extends MiniAssert {
 
     public static void main(String[] args) throws Exception {
-      assertEquals(2100_000, VERSION.SDK_INT_FULL);
+      // No desugaring use the SDK_INT_FULL from the injected android.os.Build$VERSION class.
+      assertEquals(Integer.parseInt(args[0]) < 36 ? 2100_000 : -1, VERSION.SDK_INT_FULL);
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionCodesFullBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionCodesFullBackportTest.java
index 9ecf156..4f89067 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionCodesFullBackportTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/AndroidOsBuildVersionCodesFullBackportTest.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.desugar.backports;
 
+import static com.android.tools.r8.utils.AndroidApiLevel.BAKLAVA;
+
 import com.android.tools.r8.D8TestBuilder;
 import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.TestParameters;
@@ -47,7 +49,7 @@
         ImmutableList.of(getTestRunner()));
 
     // android.os.Build$VERSION.SDK_INT_FULL is on API 36.
-    registerFieldTarget(AndroidApiLevel.BAKLAVA, 36);
+    registerFieldTarget(BAKLAVA, 36);
   }
 
   @Override
@@ -59,8 +61,8 @@
   @Override
   // Add android.os.Build$VERSION_CODES_FULL class to runtime classpath.
   protected void configure(D8TestCompileResult result) throws Exception {
-    // Only add this for BAKLAVA or higher where this is not backported away.
-    if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.BAKLAVA)) {
+    // Only add when this is not backported away.
+    if (parameters.frameworkHasBuildVersionCodesFull()) {
       result.addRunClasspathFiles(
           testForD8()
               .addProgramClassFileData(
@@ -71,6 +73,11 @@
     }
   }
 
+  @Override
+  protected String[] configureD8RunArguments() {
+    return new String[] {Integer.toString(parameters.getApiLevel().getLevel())};
+  }
+
   private static byte[] getTransformedBuildVERSION_CODES_FULLClassForRuntimeClasspath()
       throws IOException {
     ClassFileTransformer transformer =
@@ -94,14 +101,9 @@
     ClassSubject versionCodesFullClass =
         new CodeInspector(parameters.getDefaultAndroidJar())
             .clazz("android.os.Build$VERSION_CODES_FULL");
-    Assert.assertFalse(versionCodesFullClass.isPresent());
-    // Update test when fully testing Android Baklava.
-    Assert.assertFalse(parameters.getApiLevel().equals(AndroidApiLevel.BAKLAVA));
-    if (parameters.getApiLevel().equals(AndroidApiLevel.V)) {
-      versionCodesFullClass =
-          new CodeInspector(ToolHelper.getAndroidJar(AndroidApiLevel.BAKLAVA))
-              .clazz("android.os.Build$VERSION_CODES_FULL");
-      Assert.assertTrue(versionCodesFullClass.isPresent());
+    Assert.assertEquals(
+        parameters.frameworkHasBuildVersionCodesFull(), versionCodesFullClass.isPresent());
+    if (parameters.frameworkHasBuildVersionCodesFull()) {
       // Update test when more version codes full are added.
       Assert.assertEquals(36, versionCodesFullClass.allFields().size());
       // Copied from BackportedMethodRewriter.
@@ -163,82 +165,97 @@
   public static class /*android.os.Build$*/ VERSION_CODES_FULL {
 
     public static /* final */ int BASE = -1;
-    public static /* final */ int BASE_1_1 = -1;
-    public static /* final */ int CUPCAKE = -1;
-    public static /* final */ int DONUT = -1;
-    public static /* final */ int ECLAIR = -1;
-    public static /* final */ int ECLAIR_0_1 = -1;
-    public static /* final */ int ECLAIR_MR1 = -1;
-    public static /* final */ int FROYO = -1;
-    public static /* final */ int GINGERBREAD = -1;
-    public static /* final */ int GINGERBREAD_MR1 = -1;
-    public static /* final */ int HONEYCOMB = -1;
-    public static /* final */ int HONEYCOMB_MR1 = -1;
-    public static /* final */ int HONEYCOMB_MR2 = -1;
-    public static /* final */ int ICE_CREAM_SANDWICH = -1;
-    public static /* final */ int ICE_CREAM_SANDWICH_MR1 = -1;
-    public static /* final */ int JELLY_BEAN = -1;
-    public static /* final */ int JELLY_BEAN_MR1 = -1;
-    public static /* final */ int JELLY_BEAN_MR2 = -1;
-    public static /* final */ int KITKAT = -1;
-    public static /* final */ int KITKAT_WATCH = -1;
-    public static /* final */ int LOLLIPOP = -1;
-    public static /* final */ int LOLLIPOP_MR1 = -1;
-    public static /* final */ int M = -1;
-    public static /* final */ int N = -1;
-    public static /* final */ int N_MR1 = -1;
-    public static /* final */ int O = -1;
-    public static /* final */ int O_MR1 = -1;
-    public static /* final */ int P = -1;
-    public static /* final */ int Q = -1;
-    public static /* final */ int R = -1;
-    public static /* final */ int S = -1;
-    public static /* final */ int S_V2 = -1;
-    public static /* final */ int TIRAMISU = -1;
-    public static /* final */ int UPSIDE_DOWN_CAKE = -1;
-    public static /* final */ int VANILLA_ICE_CREAM = -1;
-    public static /* final */ int BAKLAVA = -1;
+    public static /* final */ int BASE_1_1 = -2;
+    public static /* final */ int CUPCAKE = -3;
+    public static /* final */ int DONUT = -4;
+    public static /* final */ int ECLAIR = -5;
+    public static /* final */ int ECLAIR_0_1 = -6;
+    public static /* final */ int ECLAIR_MR1 = -7;
+    public static /* final */ int FROYO = -8;
+    public static /* final */ int GINGERBREAD = -9;
+    public static /* final */ int GINGERBREAD_MR1 = -10;
+    public static /* final */ int HONEYCOMB = -11;
+    public static /* final */ int HONEYCOMB_MR1 = -12;
+    public static /* final */ int HONEYCOMB_MR2 = -13;
+    public static /* final */ int ICE_CREAM_SANDWICH = -14;
+    public static /* final */ int ICE_CREAM_SANDWICH_MR1 = -15;
+    public static /* final */ int JELLY_BEAN = -16;
+    public static /* final */ int JELLY_BEAN_MR1 = -17;
+    public static /* final */ int JELLY_BEAN_MR2 = -18;
+    public static /* final */ int KITKAT = -19;
+    public static /* final */ int KITKAT_WATCH = -20;
+    public static /* final */ int LOLLIPOP = -21;
+    public static /* final */ int LOLLIPOP_MR1 = -22;
+    public static /* final */ int M = -23;
+    public static /* final */ int N = -24;
+    public static /* final */ int N_MR1 = -25;
+    public static /* final */ int O = -26;
+    public static /* final */ int O_MR1 = -27;
+    public static /* final */ int P = -28;
+    public static /* final */ int Q = -29;
+    public static /* final */ int R = -30;
+    public static /* final */ int S = -31;
+    public static /* final */ int S_V2 = -32;
+    public static /* final */ int TIRAMISU = -33;
+    public static /* final */ int UPSIDE_DOWN_CAKE = -34;
+    public static /* final */ int VANILLA_ICE_CREAM = -35;
+    public static /* final */ int BAKLAVA = -36;
   }
 
   public static class TestRunner extends MiniAssert {
 
     public static void main(String[] args) throws Exception {
-      assertEquals(100_000, VERSION_CODES_FULL.BASE);
-      assertEquals(200_000, VERSION_CODES_FULL.BASE_1_1);
-      assertEquals(300_000, VERSION_CODES_FULL.CUPCAKE);
-      assertEquals(400_000, VERSION_CODES_FULL.DONUT);
-      assertEquals(500_000, VERSION_CODES_FULL.ECLAIR);
-      assertEquals(600_000, VERSION_CODES_FULL.ECLAIR_0_1);
-      assertEquals(700_000, VERSION_CODES_FULL.ECLAIR_MR1);
-      assertEquals(800_000, VERSION_CODES_FULL.FROYO);
-      assertEquals(900_000, VERSION_CODES_FULL.GINGERBREAD);
-      assertEquals(1000_000, VERSION_CODES_FULL.GINGERBREAD_MR1);
-      assertEquals(1100_000, VERSION_CODES_FULL.HONEYCOMB);
-      assertEquals(1200_000, VERSION_CODES_FULL.HONEYCOMB_MR1);
-      assertEquals(1300_000, VERSION_CODES_FULL.HONEYCOMB_MR2);
-      assertEquals(1400_000, VERSION_CODES_FULL.ICE_CREAM_SANDWICH);
-      assertEquals(1500_000, VERSION_CODES_FULL.ICE_CREAM_SANDWICH_MR1);
-      assertEquals(1600_000, VERSION_CODES_FULL.JELLY_BEAN);
-      assertEquals(1700_000, VERSION_CODES_FULL.JELLY_BEAN_MR1);
-      assertEquals(1800_000, VERSION_CODES_FULL.JELLY_BEAN_MR2);
-      assertEquals(1900_000, VERSION_CODES_FULL.KITKAT);
-      assertEquals(2000_000, VERSION_CODES_FULL.KITKAT_WATCH);
-      assertEquals(2100_000, VERSION_CODES_FULL.LOLLIPOP);
-      assertEquals(2200_000, VERSION_CODES_FULL.LOLLIPOP_MR1);
-      assertEquals(2300_000, VERSION_CODES_FULL.M);
-      assertEquals(2400_000, VERSION_CODES_FULL.N);
-      assertEquals(2500_000, VERSION_CODES_FULL.N_MR1);
-      assertEquals(2600_000, VERSION_CODES_FULL.O);
-      assertEquals(2700_000, VERSION_CODES_FULL.O_MR1);
-      assertEquals(2800_000, VERSION_CODES_FULL.P);
-      assertEquals(2900_000, VERSION_CODES_FULL.Q);
-      assertEquals(3000_000, VERSION_CODES_FULL.R);
-      assertEquals(3100_000, VERSION_CODES_FULL.S);
-      assertEquals(3200_000, VERSION_CODES_FULL.S_V2);
-      assertEquals(3300_000, VERSION_CODES_FULL.TIRAMISU);
-      assertEquals(3400_000, VERSION_CODES_FULL.UPSIDE_DOWN_CAKE);
-      assertEquals(3500_000, VERSION_CODES_FULL.VANILLA_ICE_CREAM);
-      assertEquals(3600_000, VERSION_CODES_FULL.BAKLAVA);
+      int minSdk = Integer.parseInt(args[0]);
+      int sdkWithVersionCodesFull = 36;
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 100_000 : -1, VERSION_CODES_FULL.BASE);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 200_000 : -2, VERSION_CODES_FULL.BASE_1_1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 300_000 : -3, VERSION_CODES_FULL.CUPCAKE);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 400_000 : -4, VERSION_CODES_FULL.DONUT);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 500_000 : -5, VERSION_CODES_FULL.ECLAIR);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 600_000 : -6, VERSION_CODES_FULL.ECLAIR_0_1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 700_000 : -7, VERSION_CODES_FULL.ECLAIR_MR1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 800_000 : -8, VERSION_CODES_FULL.FROYO);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 900_000 : -9, VERSION_CODES_FULL.GINGERBREAD);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1000_000 : -10, VERSION_CODES_FULL.GINGERBREAD_MR1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 1100_000 : -11, VERSION_CODES_FULL.HONEYCOMB);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1200_000 : -12, VERSION_CODES_FULL.HONEYCOMB_MR1);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1300_000 : -13, VERSION_CODES_FULL.HONEYCOMB_MR2);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1400_000 : -14, VERSION_CODES_FULL.ICE_CREAM_SANDWICH);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1500_000 : -15,
+          VERSION_CODES_FULL.ICE_CREAM_SANDWICH_MR1);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1600_000 : -16, VERSION_CODES_FULL.JELLY_BEAN);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1700_000 : -17, VERSION_CODES_FULL.JELLY_BEAN_MR1);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 1800_000 : -18, VERSION_CODES_FULL.JELLY_BEAN_MR2);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 1900_000 : -19, VERSION_CODES_FULL.KITKAT);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 2000_000 : -20, VERSION_CODES_FULL.KITKAT_WATCH);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2100_000 : -21, VERSION_CODES_FULL.LOLLIPOP);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 2200_000 : -22, VERSION_CODES_FULL.LOLLIPOP_MR1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2300_000 : -23, VERSION_CODES_FULL.M);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2400_000 : -24, VERSION_CODES_FULL.N);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2500_000 : -25, VERSION_CODES_FULL.N_MR1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2600_000 : -26, VERSION_CODES_FULL.O);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2700_000 : -27, VERSION_CODES_FULL.O_MR1);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2800_000 : -28, VERSION_CODES_FULL.P);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 2900_000 : -29, VERSION_CODES_FULL.Q);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 3000_000 : -30, VERSION_CODES_FULL.R);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 3100_000 : -31, VERSION_CODES_FULL.S);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 3200_000 : -32, VERSION_CODES_FULL.S_V2);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 3300_000 : -33, VERSION_CODES_FULL.TIRAMISU);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 3400_000 : -34, VERSION_CODES_FULL.UPSIDE_DOWN_CAKE);
+      assertEquals(
+          minSdk < sdkWithVersionCodesFull ? 3500_000 : -35, VERSION_CODES_FULL.VANILLA_ICE_CREAM);
+      assertEquals(minSdk < sdkWithVersionCodesFull ? 3600_000 : -36, VERSION_CODES_FULL.BAKLAVA);
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/enclosingmethod/EnclosingMethodRewriteTest.java b/src/test/java/com/android/tools/r8/desugar/enclosingmethod/EnclosingMethodRewriteTest.java
index 50920ee..1d93acf 100644
--- a/src/test/java/com/android/tools/r8/desugar/enclosingmethod/EnclosingMethodRewriteTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/enclosingmethod/EnclosingMethodRewriteTest.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar.enclosingmethod;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -17,6 +19,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
 interface A {
   default int def() {
@@ -59,19 +62,14 @@
         "42"
       };
 
-  private final String[] EXPECTED_CC =
-      new String[] {
-        "class " + A.class.getTypeName() + "$-CC",
-        "public static int " + A.class.getTypeName() + "$-CC.a(" + A.class.getTypeName() + ")",
-        "42"
-      };
+  private final String[] EXPECTED_CC = new String[] {"null", "null", "42"};
 
   private final String[] EXPECTED_NOUGAT =
       new String[] {
         "interface " + A.class.getTypeName(), "public int " + A.class.getTypeName() + ".def()", "42"
       };
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
@@ -140,15 +138,21 @@
   private void inspect(CodeInspector inspector) {
     ClassSubject cImplSubject = inspector.clazz(A.class.getTypeName() + "$1");
     assertThat(cImplSubject, isPresent());
-    ClassSubject enclosingClassSubject =
-        parameters.canUseDefaultAndStaticInterfaceMethods()
-            ? inspector.clazz(A.class.getTypeName())
-            : inspector.clazz(A.class.getTypeName()).toCompanionClass();
-    assertThat(enclosingClassSubject, isPresent());
-    EnclosingMethodAttribute enclosingMethodAttribute =
-        cImplSubject.getDexProgramClass().getEnclosingMethodAttribute();
-    assertEquals(
-        enclosingClassSubject.getDexProgramClass().getType(),
-        enclosingMethodAttribute.getEnclosingMethod().getHolderType());
+    if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+      ClassSubject enclosingClassSubject = inspector.clazz(A.class.getTypeName());
+      assertThat(enclosingClassSubject, isPresent());
+      EnclosingMethodAttribute enclosingMethodAttribute =
+          cImplSubject.getDexProgramClass().getEnclosingMethodAttribute();
+      assertEquals(
+          enclosingClassSubject.getDexProgramClass().getType(),
+          enclosingMethodAttribute.getEnclosingMethod().getHolderType());
+    } else {
+      ClassSubject enclosingClassSubject =
+          inspector.clazz(A.class.getTypeName()).toCompanionClass();
+      assertThat(enclosingClassSubject, isAbsent());
+      EnclosingMethodAttribute enclosingMethodAttribute =
+          cImplSubject.getDexProgramClass().getEnclosingMethodAttribute();
+      assertNull(enclosingMethodAttribute);
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java b/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java
index 4c09231..8f54230 100644
--- a/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java
+++ b/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V15_0_0;
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertTrue;
@@ -132,7 +133,7 @@
       result.assertFailureWithErrorThatThrows(VerifyError.class);
     } else if (version == Version.V5_1_1 || version == Version.V6_0_1) {
       result.assertFailure();
-    } else if (version == Version.V15_0_0) {
+    } else if (version.isNewerThanOrEqual(V15_0_0)) {
       result.assertFailureWithErrorThatThrows(VerifyError.class);
     } else {
       result.assertSuccessWithOutputThatMatches(containsString(NoSuchMethodError.class.getName()));
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java b/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java
index 9686456..8d0d237 100644
--- a/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java
@@ -5,6 +5,7 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.core.AnyOf.anyOf;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.D8TestBuilder;
 import com.android.tools.r8.D8TestCompileResult;
@@ -78,6 +79,11 @@
       throws Exception {
     return testForD8()
         .addProgramFiles(base.resolve(DEPLOY_JAR))
+        .addOptionsModification(
+            options -> {
+              assertTrue(options.getThrowBlockOutlinerOptions().enable);
+              options.getThrowBlockOutlinerOptions().enable = false;
+            })
         .setMinApi(parameters)
         .apply(configuration)
         .compile();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B418719343Test.java b/src/test/java/com/android/tools/r8/ir/optimize/B418719343Test.java
index 00c112b..ab654fa 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/B418719343Test.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B418719343Test.java
@@ -3,10 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V14_0_0;
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V15_0_0;
+
 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 org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -54,7 +56,7 @@
         // TODO(b/418568424): Should succeed with expected output.
         .applyIf(
             parameters.isDexRuntime()
-                && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0),
+                && parameters.getDexRuntimeVersion().isInRangeInclusive(V14_0_0, V15_0_0),
             rr -> rr.assertSuccessWithOutputLines("-21090195", "over"),
             rr -> rr.assertSuccessWithOutputLines("-21130949", "over"));
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index a3de438..3ad5193 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -322,7 +322,7 @@
     assertEquals(Collections.emptyList(), synthesizedJavaLambdaClasses);
 
     assertEquals(
-        Collections.singleton("java.lang.StringBuilder"),
+        Collections.emptySet(),
         collectTypes(clazz.uniqueMethodWithOriginalName("testStatelessLambda")));
 
     assertEquals(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/SdkIntMemberValuePropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/SdkIntMemberValuePropagationTest.java
index 900a30b..0fee177 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/SdkIntMemberValuePropagationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/SdkIntMemberValuePropagationTest.java
@@ -77,12 +77,11 @@
   @Parameterized.Parameters(name = "{0}, rule: {1}")
   public static List<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), Rule.values());
+        getTestParameters().withDexRuntimesAndAllApiLevels().build(), Rule.values());
   }
 
   @Test
   public void testD8() throws Exception {
-    parameters.assumeDexRuntime();
     assumeTrue(rule.getRule().equals(""));
     testForD8()
         .addProgramClassFileData(getTransformedMainClass())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
index 2badee6..87e57c2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
@@ -22,6 +22,8 @@
 import it.unimi.dsi.fastutil.ints.IntArraySet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 
 public class ThrowBlockOutlinerConstArgumentTest extends ThrowBlockOutlinerTestBase {
@@ -62,10 +64,12 @@
 
   @Override
   public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
-    // Verify that we have two outlines with one and three users, respectively.
-    assertEquals(2, outlines.size());
+    // Verify that we have two throw block outlines with one and three users, respectively.
+    List<ThrowBlockOutline> throwOutlines =
+        outlines.stream().filter(ThrowBlockOutline::isThrowOutline).collect(Collectors.toList());
+    assertEquals(2, throwOutlines.size());
     IntSet numberOfUsers = new IntArraySet();
-    for (ThrowBlockOutline outline : outlines) {
+    for (ThrowBlockOutline outline : throwOutlines) {
       numberOfUsers.add(outline.getNumberOfUsers());
     }
     assertTrue(numberOfUsers.contains(1));
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerFeatureTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerFeatureTest.java
new file mode 100644
index 0000000..b05fe38
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerFeatureTest.java
@@ -0,0 +1,132 @@
+// Copyright (c) 2025, 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.outliner.exceptions;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8TestCompileResultBase;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.Collection;
+import java.util.Iterator;
+import org.junit.Test;
+
+public class ThrowBlockOutlinerFeatureTest extends ThrowBlockOutlinerTestBase {
+
+  @Test
+  public void testR8() throws Exception {
+    assumeRelease();
+    R8TestCompileResultBase<?> compileResult =
+        testForR8(parameters)
+            .addProgramClasses(Main.class)
+            .addFeatureSplit(Feature1.class)
+            .addFeatureSplit(Feature2.class)
+            .addKeepMainRules(Main.class, Feature1.class, Feature2.class)
+            .apply(this::configure)
+            .noInliningOfSynthetics()
+            .compile()
+            .inspect(this::inspectOutput, this::inspectFeature1Output, this::inspectFeature2Output)
+            .addFeatureSplitsToRunClasspathFiles();
+    compileResult
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalArgumentException.class);
+    compileResult
+        .run(parameters.getRuntime(), Feature1.class)
+        .assertFailureWithErrorThatThrows(IllegalArgumentException.class);
+    compileResult
+        .run(parameters.getRuntime(), Feature2.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class);
+  }
+
+  @Override
+  public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
+    // Verify that we have two outlines after merging.
+    Iterator<ThrowBlockOutline> iterator = outlines.iterator();
+    ThrowBlockOutline outlineFromBase = iterator.next();
+    ThrowBlockOutline outlineFromFeature2;
+    if (outlineFromBase.getChildren().isEmpty()) {
+      outlineFromFeature2 = outlineFromBase;
+      outlineFromBase = iterator.next();
+    } else {
+      outlineFromFeature2 = iterator.next();
+    }
+    assert !iterator.hasNext();
+
+    // Verify that the outline from base has a single child.
+    assertEquals(1, outlineFromBase.getChildren().size());
+    assertEquals(2, outlineFromBase.getNumberOfUsers());
+    assertThat(
+        outlineFromBase.getMaterializedOutlineMethod().getHolder().getTypeName(),
+        containsString(Main.class.getTypeName()));
+
+    // Verify that the outline from feature 2 has no children.
+    assertEquals(0, outlineFromFeature2.getChildren().size());
+    assertEquals(1, outlineFromFeature2.getNumberOfUsers());
+    assertThat(
+        outlineFromFeature2.getMaterializedOutlineMethod().getHolder().getTypeName(),
+        containsString(Feature2.class.getTypeName()));
+  }
+
+  private void inspectOutput(CodeInspector inspector) {
+    assertEquals(2, inspector.allClasses().size());
+    assertThat(inspector.clazz(Main.class), isPresent());
+
+    ClassSubject outlineClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(Main.class, 0));
+    assertThat(outlineClassSubject, isPresent());
+    assertEquals(1, outlineClassSubject.allMethods().size());
+  }
+
+  private void inspectFeature1Output(CodeInspector inspector) {
+    assertEquals(1, inspector.allClasses().size());
+    assertThat(inspector.clazz(Feature1.class), isPresent());
+  }
+
+  private void inspectFeature2Output(CodeInspector inspector) {
+    assertEquals(2, inspector.allClasses().size());
+    assertThat(inspector.clazz(Feature2.class), isPresent());
+
+    ClassSubject outlineClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(Feature2.class, 0));
+    assertThat(outlineClassSubject, isPresent());
+    assertEquals(1, outlineClassSubject.allMethods().size());
+  }
+
+  @Override
+  public boolean shouldOutline(ThrowBlockOutline outline) {
+    return true;
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      if (args.length == 0) {
+        throw new IllegalArgumentException();
+      }
+    }
+  }
+
+  static class Feature1 {
+
+    public static void main(String[] args) {
+      if (args.length == 0) {
+        throw new IllegalArgumentException();
+      }
+    }
+  }
+
+  static class Feature2 {
+
+    public static void main(String[] args) {
+      if (args.length == 0) {
+        throw new RuntimeException();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerBooleanTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerBooleanTypeTest.java
new file mode 100644
index 0000000..be655a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerBooleanTypeTest.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2025, 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.outliner.stringbuilders;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutlinerTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.Collection;
+import org.junit.Test;
+
+public class StringBuilderOutlinerBooleanTypeTest extends ThrowBlockOutlinerTestBase {
+
+  @Test
+  public void testD8() throws Exception {
+    runTest(testForD8(parameters));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeRelease();
+    runTest(testForR8(parameters).addKeepAllClassesRule());
+  }
+
+  private void runTest(
+      TestCompilerBuilder<?, ?, ?, ? extends SingleTestRunResult<?>, ?> testBuilder)
+      throws Exception {
+    testBuilder
+        .addInnerClasses(getClass())
+        .apply(this::configure)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("true");
+  }
+
+  @Override
+  public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
+    if (parameters.getApiLevel().isLessThan(AndroidApiLevel.L)) {
+      assertTrue(outlines.isEmpty());
+    } else {
+      assertEquals(1, outlines.size());
+      ThrowBlockOutline outline = outlines.iterator().next();
+      assertTrue(outline.getProto().getParameter(0).isIntType());
+    }
+  }
+
+  @Override
+  public boolean shouldOutline(ThrowBlockOutline outline) {
+    return true;
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(toString(true));
+    }
+
+    static String toString(boolean b) {
+      return new StringBuilder().append(b).toString();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerCharTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerCharTypeTest.java
new file mode 100644
index 0000000..08c02a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerCharTypeTest.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2025, 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.outliner.stringbuilders;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutlinerTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.Collection;
+import org.junit.Test;
+
+public class StringBuilderOutlinerCharTypeTest extends ThrowBlockOutlinerTestBase {
+
+  @Test
+  public void testD8() throws Exception {
+    runTest(testForD8(parameters));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeRelease();
+    runTest(testForR8(parameters).addKeepAllClassesRule());
+  }
+
+  private void runTest(
+      TestCompilerBuilder<?, ?, ?, ? extends SingleTestRunResult<?>, ?> testBuilder)
+      throws Exception {
+    testBuilder
+        .addInnerClasses(getClass())
+        .apply(this::configure)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a");
+  }
+
+  @Override
+  public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
+    if (parameters.getApiLevel().isLessThan(AndroidApiLevel.L)) {
+      assertTrue(outlines.isEmpty());
+    } else {
+      assertEquals(1, outlines.size());
+      ThrowBlockOutline outline = outlines.iterator().next();
+      assertTrue(outline.getProto().getParameter(0).isIntType());
+    }
+  }
+
+  @Override
+  public boolean shouldOutline(ThrowBlockOutline outline) {
+    return true;
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(toString('a'));
+    }
+
+    static String toString(char c) {
+      return new StringBuilder().append(c).toString();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerTest.java
new file mode 100644
index 0000000..b64c5ba
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerTest.java
@@ -0,0 +1,71 @@
+// Copyright (c) 2025, 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.outliner.stringbuilders;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutlinerTestBase;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Collection;
+import org.junit.Test;
+
+public class StringBuilderOutlinerTest extends ThrowBlockOutlinerTestBase {
+
+  @Test
+  public void testD8() throws Exception {
+    runTest(testForD8(parameters));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeRelease();
+    runTest(testForR8(parameters).addKeepMainRule(Main.class));
+  }
+
+  private void runTest(
+      TestCompilerBuilder<?, ?, ?, ? extends SingleTestRunResult<?>, ?> testBuilder)
+      throws Exception {
+    testBuilder
+        .addInnerClasses(getClass())
+        .apply(this::configure)
+        .compile()
+        .inspect(this::inspectOutput)
+        .run(parameters.getRuntime(), Main.class, "Hel", "lo", ", world", "!")
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Override
+  public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
+    assertEquals(1, outlines.size());
+    ThrowBlockOutline outline = outlines.iterator().next();
+    assertEquals(2, outline.getNumberOfUsers());
+  }
+
+  private void inspectOutput(CodeInspector inspector) {
+    MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+    assertTrue(mainMethod.streamInstructions().noneMatch(InstructionSubject::isNewInstance));
+  }
+
+  @Override
+  public boolean shouldOutline(ThrowBlockOutline outline) {
+    return true;
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      String s1 = new StringBuilder().append(args[0]).append(args[1]).toString();
+      String s2 = new StringBuilder().append(args[2]).append(args[3]).toString();
+      System.out.print(s1);
+      System.out.println(s2);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
index 8132920..b23e434 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
@@ -158,6 +158,7 @@
 
   private void test(
       CodeInspector codeInspector,
+      boolean isR8,
       boolean expectCallPresent,
       int expectedGetClassCount,
       int expectedConstClassCount) {
@@ -177,7 +178,7 @@
     MethodSubject getMainClass = getterClass.uniqueMethodWithOriginalName("getMainClass");
     assertThat(getMainClass, isPresent());
     // Because of nullable argument, getClass() should remain.
-    assertEquals(1, countGetClass(getMainClass));
+    assertEquals(mode == CompilationMode.RELEASE && isR8 ? 0 : 1, countGetClass(getMainClass));
     assertEquals(0, countConstClass(getMainClass));
 
     MethodSubject call = getterClass.method("java.lang.Class", "call", ImmutableList.of());
@@ -200,7 +201,7 @@
         .setMinApi(parameters)
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(JAVA_OUTPUT)
-        .inspect(inspector -> test(inspector, true, 6, 0));
+        .inspect(inspector -> test(inspector, false, true, 6, 0));
   }
 
   @Test
@@ -208,19 +209,23 @@
     boolean isRelease = mode == CompilationMode.RELEASE;
     boolean expectCallPresent = !isRelease;
     int expectedGetClassCount = isRelease ? 0 : 5;
-    int expectedConstClassCount = isRelease ? (parameters.isCfRuntime() ? 8 : 6) : 1;
-    testForR8(parameters.getBackend())
+    int expectedConstClassCount = isRelease ? (parameters.isCfRuntime() ? 9 : 6) : 1;
+    testForR8(parameters)
         .setMode(mode)
         .addInnerClasses(GetClassTest.class)
         .enableInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .addKeepMainRule(MAIN)
         .addDontObfuscate()
-        .setMinApi(parameters)
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(JAVA_OUTPUT)
         .inspect(
             inspector ->
-                test(inspector, expectCallPresent, expectedGetClassCount, expectedConstClassCount));
+                test(
+                    inspector,
+                    true,
+                    expectCallPresent,
+                    expectedGetClassCount,
+                    expectedConstClassCount));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 9646cc8..d5965e5 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -6,12 +6,10 @@
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_3_72;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_5_0;
-import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_6_0;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_0_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_1_10;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -60,7 +58,7 @@
   }
 
   @Test
-  public void testJStyleLambdas() throws Exception {
+  public void testJStyleLambdasNoClassInlining() throws Exception {
     // SAM interfaces lambdas are implemented by invoke dynamic in kotlin 1.5 unlike 1.4 where a
     // class is generated for each. In CF we leave invokeDynamic but for DEX we desugar the classes
     // and merge them.
@@ -87,41 +85,40 @@
                                 "class_inliner_lambda_j_style.MainKt$testStateful3$1");
                           } else if (testParameters.isDexRuntime()) {
                             Set<Set<DexType>> mergeGroups = inspector.getMergeGroups();
-                            assertEquals(1, mergeGroups.size());
-                            inspector.assertIsCompleteMergeGroup(
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda0",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda4",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda3",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda5",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda6");
+                            assertEquals(2, mergeGroups.size());
+                            inspector
+                                .assertIsCompleteMergeGroup(
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda1",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda3",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda4",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda5",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda6",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda7")
+                                .assertIsCompleteMergeGroup(
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticThrowBlockOutline0");
                           }
                           inspector.assertNoOtherClassesMerged();
                         })
                     .noClassInlining())
         .inspect(
             inspector -> {
+              if (kotlinc.getCompilerVersion().isLessThan(KOTLINC_2_0_20)) {
+                // Do not inspect output for older Kotlin versions.
+                return;
+              }
               if (testParameters.isCfRuntime() && !hasKotlinCGeneratedLambdaClasses) {
                 assertEquals(5, inspector.allClasses().size());
-              } else if (!hasKotlinCGeneratedLambdaClasses) {
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda1"),
-                    isPresent());
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2"),
-                    isAbsent());
               } else {
-                assertThat(
-                    inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateless$1"),
-                    isAbsent());
-                assertThat(
-                    inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1"),
-                    isPresent());
+                assertEquals(7, inspector.allClasses().size());
               }
             });
+  }
 
+  @Test
+  public void testJStyleLambdas() throws Exception {
+    boolean hasKotlinCGeneratedLambdaClasses = kotlinParameters.isOlderThan(KOTLINC_1_5_0);
+    String mainClassName = "class_inliner_lambda_j_style.MainKt";
     runTest(
             "class_inliner_lambda_j_style",
             mainClassName,
@@ -134,41 +131,20 @@
                     .addNoVerticalClassMergingRule("class_inliner_lambda_j_style.SamIface"))
         .inspect(
             inspector -> {
-              if (testParameters.isCfRuntime() && !hasKotlinCGeneratedLambdaClasses) {
-                assertEquals(5, inspector.allClasses().size());
+              if (kotlinc.getCompilerVersion().isLessThan(KOTLINC_2_0_20)) {
+                // Do not inspect output for older Kotlin versions.
                 return;
               }
-              if (!hasKotlinCGeneratedLambdaClasses) {
-                // Kotlin 1.6.20 and later do not create intrinsics.stringPlus for two argument
-                // string concatination. That allow R8's stringbuilder optimization to reduce the
-                // size of strings and therefore inline the synthetic lambda.
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda1"),
-                    isAbsentIf(kotlinParameters.isNewerThan(KOTLINC_1_6_0)));
+              if (testParameters.isCfRuntime() && !hasKotlinCGeneratedLambdaClasses) {
+                assertEquals(5, inspector.allClasses().size());
               } else {
-                assertThat(
-                    inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateless$1"),
-                    isAbsent());
-              }
-
-              if (hasKotlinCGeneratedLambdaClasses) {
-                assertThat(
-                    testParameters.isCfRuntime()
-                        ? inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1")
-                        : inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$2"),
-                    isPresent());
-              } else {
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2"),
-                    isAbsent());
+                assertEquals(7, inspector.allClasses().size());
               }
             });
   }
 
   @Test
-  public void testKStyleLambdas() throws Exception {
+  public void testKStyleLambdasNoClassInlining() throws Exception {
     String mainClassName = "class_inliner_lambda_k_style.MainKt";
     runTest(
             "class_inliner_lambda_k_style",
@@ -264,7 +240,11 @@
                     isPresent());
               }
             });
+  }
 
+  @Test
+  public void testKStyleLambdas() throws Exception {
+    String mainClassName = "class_inliner_lambda_k_style.MainKt";
     runTest(
             "class_inliner_lambda_k_style",
             mainClassName,
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java
index c9467fa..cdb615d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java
@@ -13,10 +13,12 @@
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -81,7 +83,21 @@
         .addProgramFiles(getProgramFiles())
         .addKeepMainRule(getMainClassName())
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.isDexRuntime()
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L)
+                  && kotlinParameters.getLambdaGeneration().isInvokeDynamic()) {
+                inspector
+                    .assertIsCompleteMergeGroup(
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            getMainClassReference(), 0),
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            getMainClassReference(), 1))
+                    .assertNoOtherClassesMerged();
+              } else {
+                inspector.assertNoClassesMerged();
+              }
+            })
         .allowAccessModification(allowAccessModification)
         .setMinApi(parameters)
         .compile()
@@ -138,6 +154,10 @@
     return getTestName() + ".MainKt";
   }
 
+  private ClassReference getMainClassReference() {
+    return Reference.classFromTypeName(getMainClassName());
+  }
+
   private List<Path> getProgramFiles() {
     Path kotlinJarFile =
         getCompileMemoizer(getKotlinFilesInResource(getTestName()), getTestName())
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java
index da524c4..56178bd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.kotlin.lambda;
 
-import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.ENCLOSING_METHOD;
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.INNER_CLASSES;
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.SIGNATURE;
@@ -189,21 +188,6 @@
         ClassReference mainKt = Reference.classFromTypeName(getMainClassName());
         List<ClassReference> mergeGroup =
             ImmutableList.of(
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 9),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 10),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 11),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 12),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 13),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 14),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 19),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 18),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 20),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 21),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 22),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 23));
-        List<ClassReference> otherMergeGroup =
-            ImmutableList.of(
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 0),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 1),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 2),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 3),
@@ -212,24 +196,24 @@
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 6),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 7),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 8),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 9),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 10),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 11),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 12),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 13),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 14),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 15),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 16),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 17));
-        inspector
-            .applyIf(
-                kotlinc.getCompilerVersion().isGreaterThanOrEqualTo(KOTLINC_1_9_21)
-                    && parameters.isDexRuntime()
-                    && lambdaGeneration.isInvokeDynamic(),
-                i ->
-                    i.assertIsCompleteMergeGroup(
-                        ImmutableList.<ClassReference>builder()
-                            .addAll(mergeGroup)
-                            .addAll(otherMergeGroup)
-                            .build()),
-                i ->
-                    i.assertIsCompleteMergeGroup(mergeGroup)
-                        .assertIsCompleteMergeGroup(otherMergeGroup))
-            .assertNoOtherClassesMerged();
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 17),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 18),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 19),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 20),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 21),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 22),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 23),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 24),
+                SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(mainKt, 0));
+        inspector.assertIsCompleteMergeGroup(mergeGroup).assertNoOtherClassesMerged();
       }
     }
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java
index ae02a7f..84575f1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.kotlin.lambda;
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_5_0;
-import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_0_20;
 import static com.android.tools.r8.utils.PredicateUtils.not;
 import static junit.framework.TestCase.assertEquals;
 import static org.junit.Assume.assumeFalse;
@@ -18,6 +18,7 @@
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -98,139 +99,84 @@
 
   private void inspect(
       HorizontallyMergedClassesInspector inspector, KotlinLambdasInInput lambdasInInput) {
-    if (hasKotlinCGeneratedLambdaClasses()
-        && kotlinParameters.getCompilerVersion().isLessThan(KOTLINC_1_5_0)) {
-      // Don't check exactly how J-style Kotlin lambdas are merged for kotlinc before 1.5.0.
-      assertEquals(
-          parameters.isDexRuntime() && parameters.canUseDefaultAndStaticInterfaceMethods() ? 3 : 10,
-          inspector.getMergeGroups().size());
-      return;
-    }
-
-    if (!allowAccessModification && hasKotlinCGeneratedLambdaClasses()) {
-      // Only a subset of all J-style Kotlin lambdas are merged without -allowaccessmodification.
-      Set<ClassReference> unmergedLambdas =
-          ImmutableSet.of(
-              lambdasInInput.getJStyleLambdaReferenceFromTypeName(
-                  getTestName(), "inner.InnerKt$testInner1$1"),
-              lambdasInInput.getJStyleLambdaReferenceFromTypeName(
-                  getTestName(), "inner.InnerKt$testInner1$2"),
-              lambdasInInput.getJStyleLambdaReferenceFromTypeName(
-                  getTestName(), "inner.InnerKt$testInner1$3"),
-              lambdasInInput.getJStyleLambdaReferenceFromTypeName(
-                  getTestName(), "inner.InnerKt$testInner1$4"),
-              lambdasInInput.getJStyleLambdaReferenceFromTypeName(
-                  getTestName(), "inner.InnerKt$testInner1$5"));
-      inspector
-          .assertIsCompleteMergeGroup(
-              lambdasInInput.getJStyleLambdas().stream()
-                  .filter(not(unmergedLambdas::contains))
-                  .collect(Collectors.toList()))
-          .assertClassReferencesNotMerged(unmergedLambdas);
+    if (kotlinParameters.getCompilerVersion().isLessThan(KOTLINC_2_0_20)) {
+      // Don't inspect the output for Kotlin 1.9 and older.
       return;
     }
 
     if (hasKotlinCGeneratedLambdaClasses()) {
-      // All J-style Kotlin lambdas are merged with -allowaccessmodification or because they are
-      // generated by R8.
-      inspector.assertIsCompleteMergeGroup(lambdasInInput.getJStyleLambdas());
-      return;
-    }
-
-    ClassReference mainClassReference = Reference.classFromTypeName(getTestName() + ".MainKt");
-    ClassReference innerClassReference =
-        Reference.classFromTypeName(getTestName() + ".inner.InnerKt");
-    if (parameters.isCfRuntime()) {
-      inspector.assertClassReferencesNotMerged(
-          SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0),
-          SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1),
-          SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
-          SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3));
-      for (int id = 0; id < 30; id++) {
-        inspector.assertClassReferencesNotMerged(
-            SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, id));
+      if (allowAccessModification) {
+        // All J-style Kotlin lambdas are merged with -allowaccessmodification or because they are
+        // generated by R8.
+        inspector.assertIsCompleteMergeGroup(lambdasInInput.getJStyleLambdas());
+      } else {
+        // Only a subset of all J-style Kotlin lambdas are merged without -allowaccessmodification.
+        Set<ClassReference> unmergedLambdas =
+            ImmutableSet.of(
+                lambdasInInput.getJStyleLambdaReferenceFromTypeName(
+                    getTestName(), "inner.InnerKt$testInner1$1"),
+                lambdasInInput.getJStyleLambdaReferenceFromTypeName(
+                    getTestName(), "inner.InnerKt$testInner1$2"),
+                lambdasInInput.getJStyleLambdaReferenceFromTypeName(
+                    getTestName(), "inner.InnerKt$testInner1$3"),
+                lambdasInInput.getJStyleLambdaReferenceFromTypeName(
+                    getTestName(), "inner.InnerKt$testInner1$4"),
+                lambdasInInput.getJStyleLambdaReferenceFromTypeName(
+                    getTestName(), "inner.InnerKt$testInner1$5"));
+        inspector
+            .assertIsCompleteMergeGroup(
+                lambdasInInput.getJStyleLambdas().stream()
+                    .filter(not(unmergedLambdas::contains))
+                    .collect(Collectors.toList()))
+            .assertClassReferencesNotMerged(unmergedLambdas);
       }
-    } else if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-      inspector
-          .applyIf(
-              kotlinc.getCompilerVersion().isLessThanOrEqualTo(KOTLINC_1_9_21),
-              i -> {
-                i.assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 0),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 1))
-                    .assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 2),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3))
-                    .assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 4),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 5));
-                for (int id = 6; id < 30; id++) {
-                  inspector.assertClassReferencesNotMerged(
-                      SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, id));
-                }
-              },
-              i -> {
-                i.assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 0),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 1),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0))
-                    .assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 2),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1));
-                for (int id = 4; id < 30; id++) {
-                  inspector.assertClassReferencesNotMerged(
-                      SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, id));
-                }
-              })
-          .assertClassReferencesNotMerged(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3));
     } else {
-      inspector
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 1),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 2),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 4))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 0),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 9),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 10),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 11),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 12),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 21),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 22),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 23),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 24),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 25),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 26),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 27),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 28))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 13),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 14))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 15),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 16))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 17),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 18))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 19),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 20))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 5),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 6))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 7),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 8));
+      ClassReference mainClassReference = Reference.classFromTypeName(getTestName() + ".MainKt");
+      ClassReference innerClassReference =
+          Reference.classFromTypeName(getTestName() + ".inner.InnerKt");
+      if (parameters.isCfRuntime()) {
+        inspector.assertClassReferencesNotMerged(
+            SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0),
+            SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1),
+            SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
+            SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3));
+        for (int id = 0; id < 30; id++) {
+          inspector.assertClassReferencesNotMerged(
+              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, id));
+        }
+      } else if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+        inspector
+            .assertIsCompleteMergeGroup(
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 4),
+                SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0))
+            .assertIsCompleteMergeGroup(
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 7),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 8),
+                SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2))
+            .assertIsCompleteMergeGroup(
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 5),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 6),
+                SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1))
+            .assertIsCompleteMergeGroup(
+                SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(mainClassReference, 0),
+                SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(mainClassReference, 1),
+                SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(mainClassReference, 2));
+        for (int id = 4; id < 30; id++) {
+          inspector.assertClassReferencesNotMerged(
+              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, id));
+        }
+        inspector.assertClassReferencesNotMerged(
+            SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
+            SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3));
+      } else {
+        assertEquals(10, inspector.getMergeGroups().size());
+        assertEquals(
+            parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.L)
+                ? 28
+                : 34,
+            inspector.getSources().size());
+      }
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java
index 3521a29..8a694dc 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java
@@ -3,10 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming.applymapping.desugar;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -14,7 +17,6 @@
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -76,17 +78,81 @@
             .addProgramClasses(LibraryInterface.class)
             .addKeepClassAndMembersRules(LibraryInterface.class)
             .setMinApi(parameters)
-            .compile();
-    CodeInspector inspector = libraryResult.inspector();
-    assertThat(inspector.clazz(LibraryInterface.class), isPresent());
-    assertThat(inspector.method(LibraryInterface.class.getMethod("foo")), isPresent());
-    if (!parameters.canUseDefaultAndStaticInterfaceMethods()) {
-      ClassSubject companion =
-          inspector.clazz(SyntheticItemsTestUtils.syntheticCompanionClass(LibraryInterface.class));
-      // Check that we included the companion class and method.
-      assertThat(companion, isPresent());
-      assertEquals(1, companion.allMethods().size());
-    }
+            .compile()
+            .inspect(
+                inspector -> {
+                  ClassSubject libraryClass = inspector.clazz(LibraryInterface.class);
+                  assertThat(libraryClass, isPresent());
+                  assertThat(libraryClass.uniqueMethodWithOriginalName("foo"), isPresent());
+                  if (!parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                    // We don't expect the companion class to be present since -keep does not apply
+                    // to synthetics.
+                    ClassSubject companion =
+                        inspector.clazz(
+                            SyntheticItemsTestUtils.syntheticCompanionClass(
+                                LibraryInterface.class));
+                    assertThat(companion, isAbsent());
+                  }
+                });
+
+    testForR8(parameters.getBackend())
+        .addDontShrink()
+        .addProgramClasses(ProgramClass.class)
+        .addClasspathClasses(LibraryInterface.class)
+        .addApplyMapping(libraryResult.getProguardMap())
+        .addKeepMainRule(ProgramClass.class)
+        .setMinApi(parameters)
+        .compile()
+        .addRunClasspathFiles(libraryResult.writeToZip())
+        .run(parameters.getRuntime(), ProgramClass.class)
+        .applyIf(
+            parameters.canUseDefaultAndStaticInterfaceMethods(),
+            rr -> rr.assertSuccessWithOutput(EXPECTED),
+            parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik(),
+            rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+            rr -> rr.assertFailureWithErrorThatThrows(ClassNotFoundException.class));
+  }
+
+  @Test
+  public void testDesugaredLibraryLinkedWithProgram() throws Throwable {
+    parameters.assumeDexRuntime("Desugaring not required when compiling to CF");
+
+    D8TestCompileResult libraryDesugarResult =
+        testForD8(Backend.CF)
+            .addProgramClasses(LibraryInterface.class)
+            .setMinApi(parameters)
+            .compile()
+            .inspect(
+                inspector -> {
+                  ClassSubject companion =
+                      inspector.clazz(
+                          SyntheticItemsTestUtils.syntheticCompanionClass(LibraryInterface.class));
+                  assertThat(
+                      companion, isAbsentIf(parameters.canUseDefaultAndStaticInterfaceMethods()));
+                });
+
+    R8TestCompileResult libraryResult =
+        testForR8(parameters.getBackend())
+            .addProgramFiles(libraryDesugarResult.writeToZip())
+            .addKeepClassAndMembersRulesWithAllowObfuscation("*")
+            .setMinApi(parameters)
+            .compile()
+            .inspect(
+                inspector -> {
+                  ClassSubject libraryClass = inspector.clazz(LibraryInterface.class);
+                  assertThat(libraryClass, isPresent());
+                  assertThat(libraryClass.uniqueMethodWithOriginalName("foo"), isPresent());
+                  if (!parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                    ClassSubject companion =
+                        inspector.clazz(
+                            SyntheticItemsTestUtils.syntheticCompanionClass(
+                                LibraryInterface.class));
+                    assertThat(companion, isPresentAndRenamed());
+                    assertThat(
+                        companion.uniqueMethodWithOriginalName("$default$foo"),
+                        isPresentAndRenamed());
+                  }
+                });
 
     testForR8(parameters.getBackend())
         .addDontShrink()
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/desugar/StaticInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/desugar/StaticInterfaceMethodTest.java
index 45d0a3b..5f6aad0 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/desugar/StaticInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/desugar/StaticInterfaceMethodTest.java
@@ -4,18 +4,20 @@
 package com.android.tools.r8.naming.applymapping.desugar;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.applymapping.desugar.DefaultInterfaceMethodTest.LibraryInterface;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -78,21 +80,84 @@
             .addProgramClasses(LibraryInterface.class)
             .addKeepClassAndMembersRules(LibraryInterface.class)
             .setMinApi(parameters)
-            .compile();
-    CodeInspector inspector = libraryResult.inspector();
-    ClassSubject libraryInterface = inspector.clazz(LibraryInterface.class);
-    assertThat(libraryInterface, isPresent());
-    if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-      assertThat(libraryInterface.method(LibraryInterface.class.getMethod("foo")), isPresent());
-    } else {
-      // Desugaring must remove the static on the interface.
-      assertThat(libraryInterface.method(LibraryInterface.class.getMethod("foo")), isAbsent());
-      // Check that we included the companion class and method.
-      ClassSubject companion =
-          inspector.clazz(SyntheticItemsTestUtils.syntheticCompanionClass(LibraryInterface.class));
-      assertThat(companion, isPresent());
-      assertEquals(1, companion.allMethods().size());
-    }
+            .compile()
+            .inspect(
+                inspector -> {
+                  ClassSubject libraryClass = inspector.clazz(LibraryInterface.class);
+                  assertThat(libraryClass, isPresent());
+                  if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                    assertThat(libraryClass.uniqueMethodWithOriginalName("foo"), isPresent());
+                  } else {
+                    // We don't expect the companion class to be present since -keep does not apply
+                    // to synthetics.
+                    ClassSubject companion =
+                        inspector.clazz(
+                            SyntheticItemsTestUtils.syntheticCompanionClass(
+                                LibraryInterface.class));
+                    assertThat(companion, isAbsent());
+                    assertThat(libraryClass.uniqueMethodWithOriginalName("foo"), isAbsent());
+                  }
+                });
+
+    testForR8(parameters.getBackend())
+        .addDontShrink()
+        .addProgramClasses(ProgramClass.class)
+        .addClasspathClasses(LibraryInterface.class)
+        .addApplyMapping(libraryResult.getProguardMap())
+        .addKeepMainRule(ProgramClass.class)
+        .setMinApi(parameters)
+        .compile()
+        .addRunClasspathFiles(libraryResult.writeToZip())
+        .run(parameters.getRuntime(), ProgramClass.class)
+        .applyIf(
+            parameters.canUseDefaultAndStaticInterfaceMethods(),
+            rr -> rr.assertSuccessWithOutput(EXPECTED),
+            parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik(),
+            rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+            rr -> rr.assertFailureWithErrorThatThrows(ClassNotFoundException.class));
+  }
+
+  @Test
+  public void testDesugaredLibraryLinkedWithProgram() throws Throwable {
+    parameters.assumeDexRuntime("Desugaring not required when compiling to CF");
+
+    D8TestCompileResult libraryDesugarResult =
+        testForD8(Backend.CF)
+            .addProgramClasses(LibraryInterface.class)
+            .setMinApi(parameters)
+            .compile()
+            .inspect(
+                inspector -> {
+                  ClassSubject companion =
+                      inspector.clazz(
+                          SyntheticItemsTestUtils.syntheticCompanionClass(LibraryInterface.class));
+                  assertThat(
+                      companion, isAbsentIf(parameters.canUseDefaultAndStaticInterfaceMethods()));
+                });
+
+    R8TestCompileResult libraryResult =
+        testForR8(parameters.getBackend())
+            .addProgramFiles(libraryDesugarResult.writeToZip())
+            .addKeepClassAndMembersRulesWithAllowObfuscation("*")
+            .setMinApi(parameters)
+            .compile()
+            .inspect(
+                inspector -> {
+                  ClassSubject libraryClass = inspector.clazz(LibraryInterface.class);
+                  assertThat(libraryClass, isPresentAndRenamed());
+                  if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                    assertThat(
+                        libraryClass.uniqueMethodWithOriginalName("foo"), isPresentAndRenamed());
+                  } else {
+                    ClassSubject companion =
+                        inspector.clazz(
+                            SyntheticItemsTestUtils.syntheticCompanionClass(
+                                LibraryInterface.class));
+                    assertThat(companion, isPresentAndRenamed());
+                    assertThat(
+                        companion.uniqueMethodWithOriginalName("foo"), isPresentAndRenamed());
+                  }
+                });
 
     testForR8(parameters.getBackend())
         .addDontShrink()
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationLambdaClassInliningTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationLambdaClassInliningTest.java
new file mode 100644
index 0000000..e270d8c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationLambdaClassInliningTest.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2025, 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.partial;
+
+import static org.junit.Assert.assertEquals;
+
+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.utils.codeinspector.ClassSubject;
+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 PartialCompilationLambdaClassInliningTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    parameters.assumeCanUseR8Partial();
+    testForR8Partial(parameters)
+        .addR8IncludedClasses(Main.class, I.class)
+        .addR8IncludedClasses(false, NeverInline.class)
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .compile()
+        .inspect(
+            inspector -> {
+              // The call to run() has been inlined so that only the main() method remains.
+              ClassSubject mainClass = inspector.clazz(Main.class);
+              assertEquals(1, mainClass.allMethods().size());
+              // I should be removed and there should be no lambda class.
+              assertEquals(1, inspector.allClasses().size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      run(() -> System.out.println("Hello, world!"));
+    }
+
+    @NeverInline
+    static void run(I i) {
+      i.m();
+    }
+  }
+
+  interface I {
+
+    void m();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationLambdaClassMergingTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationLambdaClassMergingTest.java
new file mode 100644
index 0000000..b31e356
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationLambdaClassMergingTest.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2025, 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.partial;
+
+import static org.junit.Assert.assertEquals;
+
+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.synthesis.SyntheticItemsTestUtils;
+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 PartialCompilationLambdaClassMergingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    parameters.assumeCanUseR8Partial();
+    testForR8Partial(parameters)
+        .addR8IncludedClasses(Main.class, I.class)
+        .addR8IncludedClasses(false, NeverInline.class)
+        .addKeepClassAndMembersRules(Main.class)
+        .compile()
+        .inspect(
+            inspector -> {
+              // The output has three classes: Main, I and a lambda.
+              assertEquals(3, inspector.allClasses().size());
+              // The output has a single synthetic lambda due to class merging.
+              assertEquals(
+                  1,
+                  inspector.allClasses().stream()
+                      .filter(
+                          clazz ->
+                              SyntheticItemsTestUtils.isExternalLambda(
+                                  clazz.getOriginalReference()))
+                      .count());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      call(() -> System.out.print("Hello"));
+      call(() -> System.out.println(", world!"));
+    }
+
+    static void call(I i) {
+      i.f();
+    }
+  }
+
+  interface I {
+
+    void f();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationSyntheticKeepTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationSyntheticKeepTest.java
new file mode 100644
index 0000000..960cdbb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationSyntheticKeepTest.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2025, 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.partial;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.errors.UnusedProguardKeepRuleDiagnostic;
+import com.android.tools.r8.partial.PartialCompilationSyntheticKeepTest.Main.NestMember;
+import com.google.common.collect.ImmutableList;
+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 PartialCompilationSyntheticKeepTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters)
+        .addProgramClassFileData(getProgramClassFileData())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keepclassmembers class " + NestMember.class.getTypeName() + " { synthetic *; }")
+        .allowUnusedProguardConfigurationRules()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics.assertInfosMatch(
+                    diagnosticType(UnusedProguardKeepRuleDiagnostic.class)))
+        .inspect(inspector -> assertEquals(1, inspector.allClasses().size()))
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isDexRuntime() || parameters.getCfRuntime().isNewerThan(CfVm.JDK9),
+            rr -> rr.assertSuccessWithOutputLines("Hello, world!"));
+  }
+
+  @Test
+  public void testR8Partial() throws Exception {
+    testForR8Partial(parameters)
+        .addProgramClassFileData(getProgramClassFileData())
+        .addR8IncludedClasses(false, Main.class, NestMember.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keepclassmembers class " + NestMember.class.getTypeName() + " { synthetic *; }")
+        .compile()
+        // TODO(b/394488245): Should be 1.
+        .inspect(inspector -> assertEquals(2, inspector.allClasses().size()))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private static List<byte[]> getProgramClassFileData() throws Exception {
+    return ImmutableList.of(
+        transformer(Main.class).setNest(Main.class, NestMember.class).transform(),
+        transformer(NestMember.class)
+            .setNest(Main.class, NestMember.class)
+            .setAccessFlags(
+                NestMember.class.getDeclaredMethod("greet"),
+                flags -> {
+                  flags.unsetPublic();
+                  flags.setPrivate();
+                })
+            .transform());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      NestMember.greet();
+    }
+
+    static class NestMember {
+
+      public static void greet() {
+        System.out.println("Hello, world!");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationSyntheticTreeShakingTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationSyntheticTreeShakingTest.java
new file mode 100644
index 0000000..ffe845e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationSyntheticTreeShakingTest.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2025, 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.partial;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 PartialCompilationSyntheticTreeShakingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    parameters.assumeCanUseR8Partial();
+    testForR8Partial(parameters)
+        .addR8IncludedClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .compile()
+        .inspect(
+            inspector -> {
+              // The main() method has been optimized into being empty.
+              MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+              assertThat(mainMethod, isPresent());
+              assertTrue(mainMethod.getMethod().getCode().isEmptyVoidMethod());
+              // There should be no lambda class.
+              assertEquals(1, inspector.allClasses().size());
+            });
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      if (alwaysFalse()) {
+        Runnable r = () -> System.out.println("Hello, world!");
+        System.out.println(r);
+      }
+    }
+
+    static boolean alwaysFalse() {
+      return false;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/B341476044Test.java b/src/test/java/com/android/tools/r8/regress/B341476044Test.java
index 89d48ef..f495b56 100644
--- a/src/test/java/com/android/tools/r8/regress/B341476044Test.java
+++ b/src/test/java/com/android/tools/r8/regress/B341476044Test.java
@@ -3,12 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.regress;
 
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V13_0_0;
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V15_0_0;
 import static org.junit.Assume.assumeTrue;
 
 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.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
@@ -75,7 +76,7 @@
         .run(parameters.getRuntime(), TestClass.class)
         .applyIf(
             parameters.isDexRuntime()
-                && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V13_0_0),
+                && parameters.getDexRuntimeVersion().isInRangeInclusive(V13_0_0, V15_0_0),
             // TODO(b/341476044): Should be EXPECTED_OUTPUT.
             r -> r.assertSuccessWithOutputLines(NOT_EXPECTED_OUTPUT),
             r -> r.assertSuccessWithOutputLines(EXPECTED_OUTPUT));
diff --git a/src/test/java/com/android/tools/r8/resolution/ObjectCloneInStaticInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/resolution/ObjectCloneInStaticInterfaceMethodTest.java
index 3f081a4..6b8d4a5 100644
--- a/src/test/java/com/android/tools/r8/resolution/ObjectCloneInStaticInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/ObjectCloneInStaticInterfaceMethodTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.resolution;
 
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V16_0_0;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.SingleTestRunResult;
@@ -85,7 +86,12 @@
   }
 
   private void checkOutput(SingleTestRunResult<?> r) {
-    r.assertFailureWithErrorThatThrows(IllegalAccessError.class);
+    if (parameters.isDexRuntimeVersion(V16_0_0)) {
+      // TODO(b/454529390): ART 16 does not enforce protected access to Object.clone.
+      r.assertSuccessWithOutputLines("0");
+    } else {
+      r.assertFailureWithErrorThatThrows(IllegalAccessError.class);
+    }
   }
 
   interface I {
diff --git a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
index 9289f8e..484425e 100644
--- a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V16_0_0;
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.containsString;
@@ -89,6 +90,7 @@
               case V13_0_0:
               case V14_0_0:
               case V15_0_0:
+              case V16_0_0:
                 return StringUtils.joinLines(
                     "Hello!",
                     "Unexpected outcome of checkcast",
@@ -377,7 +379,10 @@
     return allOf(
         containsString("java.lang.VerifyError"),
         anyOf(
-            containsString("register v0 has type Precise Reference: B but expected Reference: A"),
+            containsString(
+                "register v0 has type "
+                    + (parameters.getDexRuntimeVersion().isOlderThan(V16_0_0) ? "Precise " : "")
+                    + "Reference: B but expected Reference: A"),
             containsString("VFY: storing type 'LB;' into field type 'LA;'")));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
index ab72799..db98e26 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -321,7 +321,13 @@
                 result.assertFailureWithErrorThatMatches(
                     containsString(
                         parameters.getDexRuntimeVersion().isNewerThan(Version.V4_4_4)
-                            ? "type Precise Reference: Foo[] but expected Reference: SubInterface[]"
+                            ? "type "
+                                + (parameters
+                                        .getDexRuntimeVersion()
+                                        .isOlderThanOrEqual(Version.V15_0_0)
+                                    ? "Precise "
+                                    : "")
+                                + "Reference: Foo[] but expected Reference: SubInterface[]"
                             : "[LFoo; is not instance of [LSubInterface;")))
         .assertStderrMatches(not(containsString("ClassNotFoundException")));
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
index edc8bce..5771588 100644
--- a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
@@ -22,22 +22,20 @@
 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 DefaultMethodsTest extends TestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public DefaultMethodsTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   private void runTest(
       List<String> additionalKeepRules,
       ThrowingConsumer<CodeInspector, RuntimeException> inspection)
@@ -66,23 +64,6 @@
     assertThat(clazz.method("int", "method", ImmutableList.of()), not(isPresent()));
   }
 
-  private void defaultMethodKept(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz(InterfaceWithDefaultMethods.class);
-    assertThat(clazz, isPresent());
-    MethodSubject method = clazz.method("int", "method", ImmutableList.of());
-    assertThat(method, isPresent());
-    ClassSubject companionClass = clazz.toCompanionClass();
-    if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-      assertThat(method, not(isAbstract()));
-      assertThat(companionClass, not(isPresent()));
-    } else {
-      assertThat(method, isAbstract());
-      assertThat(companionClass, isPresent());
-      MethodSubject defaultMethod = method.toMethodOnCompanionClass();
-      assertThat(defaultMethod, isPresent());
-    }
-  }
-
   private void defaultMethodKeptWithoutCompanionClass(CodeInspector inspector) {
     ClassSubject clazz = inspector.clazz(InterfaceWithDefaultMethods.class);
     assertThat(clazz, isPresent());
@@ -117,7 +98,7 @@
             "-keep interface " + InterfaceWithDefaultMethods.class.getTypeName() + "{",
             "  <methods>;",
             "}"),
-        this::defaultMethodKept);
+        this::defaultMethodKeptWithoutCompanionClass);
   }
 
   @Test
@@ -127,7 +108,7 @@
             "-keep interface " + InterfaceWithDefaultMethods.class.getTypeName() + "{",
             "  public int method();",
             "}"),
-        this::defaultMethodKept);
+        this::defaultMethodKeptWithoutCompanionClass);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/staticinterfacemethods/defaultmethods/StaticInterfaceMethodsTest.java b/src/test/java/com/android/tools/r8/shaking/staticinterfacemethods/defaultmethods/StaticInterfaceMethodsTest.java
index 0d9f97c..6d28f87 100644
--- a/src/test/java/com/android/tools/r8/shaking/staticinterfacemethods/defaultmethods/StaticInterfaceMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/staticinterfacemethods/defaultmethods/StaticInterfaceMethodsTest.java
@@ -4,11 +4,11 @@
 
 package com.android.tools.r8.shaking.staticinterfacemethods.defaultmethods;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
-import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -31,24 +31,24 @@
 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 StaticInterfaceMethodsTest extends TestBase {
 
-  private final TestParameters parameters;
-  private final boolean allowObfuscation;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}, allowObfuscation: {1}")
+  @Parameter(1)
+  public boolean allowObfuscation;
+
+  @Parameters(name = "{0}, allowObfuscation: {1}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
-  public StaticInterfaceMethodsTest(TestParameters parameters, boolean allowObfuscation) {
-    this.parameters = parameters;
-    this.allowObfuscation = allowObfuscation;
-  }
-
   private R8TestCompileResult compileTest(
       List<String> additionalKeepRules,
       ThrowingConsumer<CodeInspector, RuntimeException> inspection)
@@ -113,12 +113,12 @@
     MethodSubject method = clazz.method("int", "method", ImmutableList.of());
     ClassSubject companionClass = clazz.toCompanionClass();
     if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+      assertThat(method, isPresent());
       assertThat(method, isStatic());
-      assertThat(companionClass, not(isPresent()));
+      assertThat(companionClass, isAbsent());
     } else {
-      assertThat(method, not(isPresent()));
-      assertThat(companionClass, isPresent());
-      assertThat(companionClass.uniqueMethodWithOriginalName("method"), isPresent());
+      assertThat(method, isAbsent());
+      assertThat(companionClass, isAbsent());
     }
   }
 
@@ -129,14 +129,13 @@
     if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
       assertThat(clazz, allowObfuscation ? isPresentAndRenamed() : isPresentAndNotRenamed());
       assertThat(method, isStatic());
-      assertThat(companionClass, not(isPresent()));
+      assertThat(companionClass, isAbsent());
     } else {
       // When there is only a static method in the interface nothing is left on the interface itself
       // after desugaring, only the companion class is left.
-      assertThat(clazz, not(isPresent()));
-      assertThat(method, not(isPresent()));
+      assertThat(clazz, isAbsent());
       // TODO(160142903): The companion class should be present.
-      assertThat(companionClass, not(isPresent()));
+      assertThat(companionClass, isAbsent());
       // Also check that method exists on companion class.
     }
   }
@@ -161,7 +160,6 @@
   @Test
   public void testDefaultMethodKeptWithMethods() throws Exception {
     assumeTrue(!allowObfuscation); // No use of allowObfuscation.
-
     compileTest(
         ImmutableList.of(
             "-keep interface " + InterfaceWithStaticMethods.class.getTypeName() + "{",
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDefaultMethodInSubInterfaceTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDefaultMethodInSubInterfaceTest.java
index 54510e4..3f90d60 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDefaultMethodInSubInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDefaultMethodInSubInterfaceTest.java
@@ -208,7 +208,12 @@
         .addKeepMainRule(Main.class)
         .addRunClasspathFiles(r8CompiledTarget)
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+        .applyIf(
+            parameters.isCfRuntime() || parameters.canUseDefaultAndStaticInterfaceMethods(),
+            rr -> rr.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik(),
+            rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+            rr -> rr.assertFailureWithErrorThatThrows(ClassNotFoundException.class));
   }
 
   @Test
@@ -290,7 +295,12 @@
         .addProgramFiles(sourceJar)
         .addRunClasspathFiles(r8CompiledTarget)
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+        .applyIf(
+            parameters.canUseDefaultAndStaticInterfaceMethods(),
+            rr -> rr.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik(),
+            rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+            rr -> rr.assertFailureWithErrorThatThrows(ClassNotFoundException.class));
   }
 
   // Interfaces I and J are in the target set for trace references.
diff --git a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeInterfaceTest.java b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeInterfaceTest.java
index 7a0ac0d..34df157 100644
--- a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeInterfaceTest.java
+++ b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeInterfaceTest.java
@@ -77,7 +77,7 @@
   public void testD8() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     testForD8(parameters)
         .addProgramClassFileData(
             dumpHost(),
@@ -111,7 +111,7 @@
   public void testD8WithClasspathAndMerge() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
 
     Path host =
         testForD8(parameters)
@@ -196,7 +196,7 @@
   public void testD8WithoutMembersOnClasspath() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     assertFailsCompilation(
         () ->
             testForD8(parameters)
@@ -214,7 +214,7 @@
   public void testD8WithoutHostOnClasspath() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     assertFailsCompilation(
         () ->
             testForD8(parameters)
diff --git a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSpecialTest.java b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSpecialTest.java
index fb7f97c..83f2676 100644
--- a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSpecialTest.java
+++ b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSpecialTest.java
@@ -163,7 +163,7 @@
     parameters.assumeDexRuntime();
     assumeTrue(parameters.getApiLevel().getLevel() >= 35);
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     testForD8(parameters)
         .addProgramClassesAndInnerClasses(NestHierachy.class)
         .setMinApi(AndroidApiLevel.U)
diff --git a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSuperTest.java b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSuperTest.java
index 0adf57e..2e20775 100644
--- a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSuperTest.java
+++ b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeSuperTest.java
@@ -73,7 +73,7 @@
   public void testD8() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     testForD8(parameters)
         .addProgramClassFileData(dumpHost(), dumpMember(), dumpSubMember())
         .apply(this::configureEmitNestAnnotationsInDex)
@@ -105,7 +105,7 @@
   public void testD8WithClasspathAndMerge() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
 
     Path host =
         testForD8(parameters)
@@ -190,7 +190,7 @@
   public void testD8WithoutMembersOnClasspath() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     assertFailsCompilation(
         () ->
             testForD8(parameters)
@@ -208,7 +208,7 @@
   public void testD8WithoutHostOnClasspath() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     assertFailsCompilation(
         () ->
             testForD8(parameters)
diff --git a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeVirtualTest.java b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeVirtualTest.java
index 9b5bf8b..2c6fae1 100644
--- a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeVirtualTest.java
+++ b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexRewriteInvokeVirtualTest.java
@@ -73,7 +73,7 @@
   public void testD8() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     testForD8(parameters)
         .addProgramClassFileData(dumpHost(), dumpMember1(), dumpMember2())
         .apply(this::configureEmitNestAnnotationsInDex)
@@ -105,7 +105,7 @@
   public void testD8WithClasspathAndMerge() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
 
     Path host =
         testForD8(parameters)
@@ -190,7 +190,7 @@
   public void testD8WithoutMembersOnClasspath() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     assertFailsCompilation(
         () ->
             testForD8(parameters)
@@ -208,7 +208,7 @@
   public void testD8WithoutHostOnClasspath() throws Exception {
     parameters.assumeDexRuntime();
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.canUseNestBasedAccesses());
     assertFailsCompilation(
         () ->
             testForD8(parameters)
diff --git a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingFieldsTest.java b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingFieldsTest.java
index d2ed4a2..2742121 100644
--- a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingFieldsTest.java
+++ b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingFieldsTest.java
@@ -108,7 +108,7 @@
     parameters.assumeR8TestParameters();
     assumeTrue(parameters.isDexRuntime() || isRuntimeWithNestSupport(parameters.asCfRuntime()));
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.isDexRuntime() && isRuntimeWithNestSupport(parameters.asDexRuntime()));
     testForR8(parameters)
         .addProgramClassFileData(
             dumpHost(ACC_PRIVATE), dumpMember1(ACC_PRIVATE), dumpMember2(ACC_PRIVATE))
@@ -134,7 +134,7 @@
     parameters.assumeR8TestParameters();
     assumeTrue(parameters.isDexRuntime() || isRuntimeWithNestSupport(parameters.asCfRuntime()));
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.isDexRuntime() && isRuntimeWithNestSupport(parameters.asDexRuntime()));
     testForR8(parameters)
         .addProgramClassFileData(
             dumpHost(ACC_PRIVATE), dumpMember1(ACC_PRIVATE), dumpMember2(ACC_PRIVATE))
@@ -155,7 +155,7 @@
     parameters.assumeR8TestParameters();
     assumeTrue(parameters.isDexRuntime() || isRuntimeWithNestSupport(parameters.asCfRuntime()));
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.isDexRuntime() && isRuntimeWithNestSupport(parameters.asDexRuntime()));
     testForR8(parameters)
         .addProgramClassFileData(
             dumpHost(ACC_PUBLIC), dumpMember1(ACC_PUBLIC), dumpMember2(ACC_PUBLIC))
diff --git a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingMethodsTest.java b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingMethodsTest.java
index f368942..7e0d654 100644
--- a/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingMethodsTest.java
+++ b/src/test/java11/com/android/tools/r8/jdk11/nest/dex/NestAttributesInDexShrinkingMethodsTest.java
@@ -107,7 +107,7 @@
     parameters.assumeR8TestParameters();
     assumeTrue(parameters.isDexRuntime() || isRuntimeWithNestSupport(parameters.asCfRuntime()));
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.isDexRuntime() && isRuntimeWithNestSupport(parameters.asDexRuntime()));
     testForR8(parameters)
         .addProgramClassFileData(
             dumpHost(ACC_PRIVATE), dumpMember1(ACC_PRIVATE), dumpMember2(ACC_PRIVATE))
@@ -133,7 +133,7 @@
     parameters.assumeR8TestParameters();
     assumeTrue(parameters.isDexRuntime() || isRuntimeWithNestSupport(parameters.asCfRuntime()));
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.isDexRuntime() && isRuntimeWithNestSupport(parameters.asDexRuntime()));
     testForR8(parameters)
         .addProgramClassFileData(
             dumpHost(ACC_PRIVATE), dumpMember1(ACC_PRIVATE), dumpMember2(ACC_PRIVATE))
@@ -154,7 +154,7 @@
     parameters.assumeR8TestParameters();
     assumeTrue(parameters.isDexRuntime() || isRuntimeWithNestSupport(parameters.asCfRuntime()));
     // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
-    assertFalse(parameters.getApiLevel().getLevel() > 35);
+    assertFalse(parameters.isDexRuntime() && isRuntimeWithNestSupport(parameters.asDexRuntime()));
     testForR8(parameters)
         .addProgramClassFileData(
             dumpHost(ACC_PUBLIC), dumpMember1(ACC_PUBLIC), dumpMember2(ACC_PUBLIC))
diff --git a/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTest.java b/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTest.java
index 517f276..608686c 100644
--- a/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTest.java
+++ b/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.jdk21.autocloseable;
 
 import static com.android.tools.r8.desugar.AutoCloseableAndroidLibraryFileData.getAutoCloseableAndroidClassData;
+import static com.android.tools.r8.utils.AndroidApiLevel.BAKLAVA;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.NeverInline;
@@ -72,7 +73,7 @@
     assumeTrue(parameters.isCfRuntime());
     testForJvm(parameters)
         .addInnerClassesAndStrippedOuter(getClass())
-        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.BAKLAVA))
+        .addLibraryFiles(ToolHelper.getAndroidJar(BAKLAVA))
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
   }
@@ -83,7 +84,7 @@
     testForD8(parameters.getBackend())
         .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
-        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.BAKLAVA))
+        .addLibraryFiles(ToolHelper.getAndroidJar(BAKLAVA))
         .addLibraryClassFileData(getAutoCloseableAndroidClassData(parameters))
         .compile()
         .addRunClasspathClassFileData((getAutoCloseableAndroidClassData(parameters)))
@@ -104,8 +105,12 @@
             Executor2.class)) {
       ClassSubject subj = inspector.clazz(clazz);
       Assert.assertTrue(subj.isPresent());
-      Assert.assertTrue(subj.allMethods().stream().anyMatch(m -> m.getFinalName().equals("close")));
-      Assert.assertTrue(
+      Assert.assertEquals(
+          ImmutableList.of(PrintForkJoinPool.class, Executor2.class).contains(clazz)
+              || !parameters.corelibWithExecutorServiceImplementingAutoClosable(),
+          subj.allMethods().stream().anyMatch(m -> m.getFinalName().equals("close")));
+      Assert.assertEquals(
+          !parameters.corelibWithExecutorServiceImplementingAutoClosable(),
           subj.getDexProgramClass()
               .getInterfaces()
               .contains(inspector.getFactory().autoCloseableType));
@@ -120,7 +125,7 @@
         .addInnerClassesAndStrippedOuter(getClass())
         .addInliningAnnotations()
         .setMinApi(parameters)
-        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.BAKLAVA))
+        .addLibraryFiles(ToolHelper.getAndroidJar(BAKLAVA))
         .addLibraryClassFileData(getAutoCloseableAndroidClassData(parameters))
         .compile()
         .addRunClasspathClassFileData((getAutoCloseableAndroidClassData(parameters)))
diff --git a/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTwrTest.java b/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTwrTest.java
index 9ecfc4c..ef5b8e7 100644
--- a/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTwrTest.java
+++ b/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableRetargeterExecutorServiceSubtypeTwrTest.java
@@ -13,6 +13,8 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.jdk21.autocloseable.AutoCloseableRetargeterExecutorServiceSubtypeTest.Executor2;
+import com.android.tools.r8.jdk21.autocloseable.AutoCloseableRetargeterExecutorServiceSubtypeTest.PrintForkJoinPool;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DeterminismChecker;
 import com.android.tools.r8.utils.StringUtils;
@@ -91,8 +93,13 @@
             Executor2.class)) {
       ClassSubject subj = inspector.clazz(clazz);
       Assert.assertTrue(subj.isPresent());
-      Assert.assertTrue(subj.allMethods().stream().anyMatch(m -> m.getFinalName().equals("close")));
-      Assert.assertTrue(
+      Assert.assertEquals(
+          ImmutableList.of(PrintForkJoinPool.class, Executor2.class).contains(clazz)
+              || !parameters.corelibWithExecutorServiceImplementingAutoClosable(),
+          subj.allMethods().stream().anyMatch(m -> m.getFinalName().equals("close")));
+      Assert.assertEquals(
+          clazz.toString(),
+          !parameters.corelibWithExecutorServiceImplementingAutoClosable(),
           subj.getDexProgramClass()
               .getInterfaces()
               .contains(inspector.getFactory().autoCloseableType));
diff --git a/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java b/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java
index b5c6800..c91ec95 100644
--- a/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java
+++ b/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.jdk21.desugaredlibrary;
 
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V16_0_0;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
@@ -61,7 +62,8 @@
         .setMinApi(parameters)
         .run(parameters.getRuntime(), Executor.class)
         .applyIf(
-            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O),
+            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O)
+                || parameters.getDexRuntimeVersion().isNewerThanOrEqual(V16_0_0),
             b -> b.assertSuccessWithOutput(EXPECTED_RESULT),
             parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V8_1_0),
             b -> b.assertFailureWithErrorThatThrows(NoSuchMethodError.class),
diff --git a/src/test/testbase/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/testbase/java/com/android/tools/r8/R8RunArtTestsTest.java
index 3cfbd85..34b4b48 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -105,7 +105,8 @@
           DexVm.Version.V12_0_0,
           DexVm.Version.V13_0_0,
           DexVm.Version.V14_0_0,
-          DexVm.Version.V15_0_0);
+          DexVm.Version.V15_0_0,
+          DexVm.Version.V16_0_0);
 
   private static final TestCondition beforeAndroidN =
       TestCondition.match(
@@ -122,8 +123,8 @@
       TestCondition.match(TestCondition.runtimesUpTo(DexVm.Version.V7_0_0));
   private static final TestCondition fromAndroidS =
       TestCondition.match(TestCondition.runtimesFrom(DexVm.Version.V12_0_0));
-  private static final TestCondition fromAndroidV =
-      TestCondition.match(TestCondition.runtimesFrom(DexVm.Version.V15_0_0));
+  private static final TestCondition androidV =
+      TestCondition.match(TestCondition.runtimes(DexVm.Version.V15_0_0));
 
   // Test that required to set min-api to a specific value.
   private static Map<String, AndroidApiLevel> needMinSdkVersion =
@@ -501,6 +502,7 @@
   static {
     ImmutableMap.Builder<DexVm.Version, List<String>> builder = ImmutableMap.builder();
     builder
+        .put(DexVm.Version.V16_0_0, ImmutableList.of("543-env-long-ref", "518-null-array-get"))
         .put(DexVm.Version.V15_0_0, ImmutableList.of("543-env-long-ref", "518-null-array-get"))
         .put(DexVm.Version.V14_0_0, ImmutableList.of("543-env-long-ref", "518-null-array-get"))
         .put(DexVm.Version.V13_0_0, ImmutableList.of("543-env-long-ref", "518-null-array-get"))
@@ -846,7 +848,8 @@
                           DexVm.Version.V7_0_0,
                           DexVm.Version.V13_0_0,
                           DexVm.Version.V14_0_0,
-                          DexVm.Version.V15_0_0)),
+                          DexVm.Version.V15_0_0,
+                          DexVm.Version.V16_0_0)),
                   TestCondition.match(
                       compilers(
                           CompilerUnderTest.R8,
@@ -876,7 +879,8 @@
                       DexVm.Version.V12_0_0,
                       DexVm.Version.V13_0_0,
                       DexVm.Version.V14_0_0,
-                      DexVm.Version.V15_0_0)))
+                      DexVm.Version.V15_0_0,
+                      DexVm.Version.V16_0_0)))
           .put("454-get-vreg", TestCondition.match(TestCondition.R8DEX_COMPILER))
           // Fails: regs_jni.cc:42] Check failed: GetVReg(m, 0, kIntVReg, &value)
           // The R8/D8 code does not put values in the same registers as the tests expects.
@@ -895,7 +899,8 @@
                       DexVm.Version.V12_0_0,
                       DexVm.Version.V13_0_0,
                       DexVm.Version.V14_0_0,
-                      DexVm.Version.V15_0_0)))
+                      DexVm.Version.V15_0_0,
+                      DexVm.Version.V16_0_0)))
           .put("457-regs", TestCondition.match(TestCondition.R8DEX_COMPILER))
           // Class not found.
           .put("529-checker-unresolved", TestCondition.any())
@@ -956,7 +961,7 @@
           .put("979-const-method-handle", beforeAndroidP)
           .put(
               "021-string2",
-              fromAndroidV) // Test use com.android.org.bouncycastle.util.Strings.fromUTF8ByteArray
+              androidV) // Test use com.android.org.bouncycastle.util.Strings.fromUTF8ByteArray
           // - no longer present.
           // Missing class junit.framework.Assert (see JunitAvailabilityInHostArtTest).
           // TODO(120884788): Add this again.
diff --git a/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
index 25efdbf..86abea7 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -65,6 +65,7 @@
       options -> {
         options.testing.enableTestAssertions = true;
         options.getThrowBlockOutlinerOptions().enable = true;
+        options.getThrowBlockOutlinerOptions().enableStringBuilderOutlining = true;
       };
 
   public static final Consumer<InternalOptions> DEFAULT_D8_OPTIONS = DEFAULT_OPTIONS;
diff --git a/src/test/testbase/java/com/android/tools/r8/TestCondition.java b/src/test/testbase/java/com/android/tools/r8/TestCondition.java
index a4eca96..b7e4851 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestCondition.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestCondition.java
@@ -28,6 +28,7 @@
     ART_V13_0_0,
     ART_V14_0_0,
     ART_V15_0_0,
+    ART_V16_0_0,
     ART_DEFAULT,
     ART_MASTER,
     JAVA;
@@ -58,6 +59,8 @@
           return ART_V14_0_0;
         case V15_0_0:
           return ART_V15_0_0;
+        case V16_0_0:
+          return ART_V16_0_0;
         case DEFAULT:
           return ART_DEFAULT;
         case MASTER:
diff --git a/src/test/testbase/java/com/android/tools/r8/TestParameters.java b/src/test/testbase/java/com/android/tools/r8/TestParameters.java
index baee34e..7318a40 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestParameters.java
@@ -107,6 +107,8 @@
   }
 
   public boolean canUseNestBasedAccesses() {
+    // TODO(b/247047415): Update test when a DEX VM natively supporting nests is added.
+    assertFalse(getApiLevel() != null && getApiLevel().getLevel() > 36);
     assert isCfRuntime() || isDexRuntime();
     return isCfRuntime() && getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11);
   }
@@ -125,6 +127,14 @@
     return isDexRuntime() && getApiLevel().isGreaterThan(AndroidApiLevel.U);
   }
 
+  public boolean corelibWithExecutorServiceImplementingAutoClosable() {
+    return isDexRuntime() && getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.BAKLAVA);
+  }
+
+  public boolean frameworkHasBuildVersionCodesFull() {
+    return isDexRuntime() && getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.BAKLAVA);
+  }
+
   public boolean isAccessModificationEnabled(boolean allowAccessModification) {
     return allowAccessModification || isAccessModificationEnabledByDefault();
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index 268a9e0..f65356c 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -4,6 +4,8 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.ToolHelper.TestDataSourceSet.computeLegacyOrGradleSpecifiedLocation;
+import static com.android.tools.r8.utils.DexVersion.V39;
+import static com.android.tools.r8.utils.DexVersion.V41;
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.JAVA_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
@@ -357,6 +359,8 @@
     ART_14_0_0_HOST(Version.V14_0_0, Kind.HOST),
     ART_15_0_0_TARGET(Version.V15_0_0, Kind.TARGET),
     ART_15_0_0_HOST(Version.V15_0_0, Kind.HOST),
+    ART_16_0_0_TARGET(Version.V16_0_0, Kind.TARGET),
+    ART_16_0_0_HOST(Version.V16_0_0, Kind.HOST),
     ART_MASTER_TARGET(Version.MASTER, Kind.TARGET),
     ART_MASTER_HOST(Version.MASTER, Kind.HOST);
 
@@ -379,6 +383,7 @@
       V13_0_0("13.0.0"),
       V14_0_0("14.0.0"),
       V15_0_0("15.0.0"),
+      V16_0_0("16.0.0"),
       MASTER("master");
 
       /** This should generally be the latest DEX VM fully supported. */
@@ -453,7 +458,7 @@
       }
 
       public static Version last() {
-        return V15_0_0;
+        return V16_0_0;
       }
 
       public static Version master() {
@@ -972,6 +977,7 @@
       ImmutableMap.<DexVm, String>builder()
           .put(DexVm.ART_DEFAULT, "art")
           .put(DexVm.ART_MASTER_HOST, "host/art-master")
+          .put(DexVm.ART_16_0_0_HOST, "host/art-16.0.0")
           .put(DexVm.ART_15_0_0_HOST, "host/art-15.0.0-beta2")
           .put(DexVm.ART_14_0_0_HOST, "host/art-14.0.0-beta3")
           .put(DexVm.ART_13_0_0_HOST, "host/art-13.0.0")
@@ -989,6 +995,7 @@
       ImmutableMap.<DexVm, String>builder()
           .put(DexVm.ART_DEFAULT, "bin/art")
           .put(DexVm.ART_MASTER_HOST, "bin/art")
+          .put(DexVm.ART_16_0_0_HOST, "bin/art")
           .put(DexVm.ART_15_0_0_HOST, "bin/art")
           .put(DexVm.ART_14_0_0_HOST, "bin/art")
           .put(DexVm.ART_13_0_0_HOST, "bin/art")
@@ -1006,6 +1013,7 @@
   private static final Map<DexVm, String> ART_BINARY_VERSIONS_X64 =
       ImmutableMap.<DexVm, String>builder()
           .put(DexVm.ART_DEFAULT, "bin/art")
+          .put(DexVm.ART_16_0_0_HOST, "bin/art")
           .put(DexVm.ART_15_0_0_HOST, "bin/art")
           .put(DexVm.ART_14_0_0_HOST, "bin/art")
           .put(DexVm.ART_13_0_0_HOST, "bin/art")
@@ -1042,6 +1050,7 @@
     ImmutableMap.Builder<DexVm, List<String>> builder = ImmutableMap.builder();
     builder
         .put(DexVm.ART_DEFAULT, ART_7_TO_10_BOOT_LIBS)
+        .put(DexVm.ART_16_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
         .put(DexVm.ART_15_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
         .put(DexVm.ART_14_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
         .put(DexVm.ART_13_0_0_HOST, ART_12_PLUS_BOOT_LIBS)
@@ -1063,6 +1072,7 @@
     ImmutableMap.Builder<DexVm, String> builder = ImmutableMap.builder();
     builder
         .put(DexVm.ART_DEFAULT, "angler")
+        .put(DexVm.ART_16_0_0_HOST, "akita")
         .put(DexVm.ART_15_0_0_HOST, "akita")
         .put(DexVm.ART_14_0_0_HOST, "redfin")
         .put(DexVm.ART_13_0_0_HOST, "redfin")
@@ -1098,6 +1108,7 @@
       case V13_0_0:
       case V14_0_0:
       case V15_0_0:
+      case V16_0_0:
       case MASTER:
         return base.resolve("host").resolve("art-" + version);
       default:
@@ -1134,6 +1145,7 @@
       case V13_0_0:
       case V14_0_0:
       case V15_0_0:
+      case V16_0_0:
       case MASTER:
         return "x86_64";
       default:
@@ -1406,6 +1418,8 @@
     switch (dexVm.version) {
       case MASTER:
         return AndroidApiLevel.MAIN;
+      case V16_0_0:
+        return AndroidApiLevel.BAKLAVA;
       case V15_0_0:
         return AndroidApiLevel.V;
       case V14_0_0:
@@ -1441,6 +1455,8 @@
     switch (apiLevel) {
       case MAIN:
         return DexVm.Version.MASTER;
+      case BAKLAVA:
+        return DexVm.Version.V16_0_0;
       case V:
         return DexVm.Version.V15_0_0;
       case U:
@@ -2470,6 +2486,10 @@
         .collect(Collectors.toList())
         .equals(SUPPORTED_DEX2OAT_VMS);
     DexVersion requiredDexFileVersion = getDexFileVersionForVm(targetVm);
+    // TODO(b/453564724): Get dex2oat supporting V41.
+    if (requiredDexFileVersion.isEqualTo(V41)) {
+      requiredDexFileVersion = V39;
+    }
     DexVm vm = null;
     for (DexVm supported : SUPPORTED_DEX2OAT_VMS) {
       DexVersion supportedDexFileVersion = getDexFileVersionForVm(supported);
diff --git a/src/test/testbase/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java b/src/test/testbase/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
index f2c3ee2..1e26b4b 100644
--- a/src/test/testbase/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
+++ b/src/test/testbase/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
@@ -242,10 +242,15 @@
         .compileWithExpectedDiagnostics(this::checkDiagnostics)
         .apply(this::configure)
         .inspect(this::assertDesugaring)
-        .run(parameters.getRuntime(), testClassName)
+        .run(parameters.getRuntime(), testClassName, configureD8RunArguments())
         .apply(runResultConsumer);
   }
 
+  protected String[] configureD8RunArguments() {
+    // For subclasses to configure arguments passed to running the test code.
+    return new String[] {};
+  }
+
   protected void configure(D8TestBuilder builder) throws Exception {
     // For subclasses to further configure the test builder.
   }
@@ -264,7 +269,7 @@
         .setIncludeClassesChecksum(true)
         .compileWithExpectedDiagnostics(this::checkDiagnostics)
         .apply(this::configure)
-        .run(parameters.getRuntime(), testClassName)
+        .run(parameters.getRuntime(), testClassName, configureD8RunArguments())
         .assertSuccess()
         .inspect(this::assertDesugaring);
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
index 9b5bb95..e70142c 100644
--- a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -105,6 +105,10 @@
   }
 
   public static ClassReference syntheticThrowBlockOutlineClass(Class<?> clazz, int id) {
+    return syntheticThrowBlockOutlineClass(Reference.classFromClass(clazz), id);
+  }
+
+  public static ClassReference syntheticThrowBlockOutlineClass(ClassReference clazz, int id) {
     return syntheticClass(clazz, naming.THROW_BLOCK_OUTLINE, id);
   }
 
diff --git a/tools/linux/README.art-versions b/tools/linux/README.art-versions
index ac3139a..12bde86 100644
--- a/tools/linux/README.art-versions
+++ b/tools/linux/README.art-versions
@@ -67,6 +67,34 @@
 
 repo init -b udc-preview1-release
 
+art-16 (Android Baklava)
+------------------------
+Build branch 25Q2-release. Art at 4a203afc54cfc93b525849fe6dd9aeb9a95f7a06
+
+export BRANCH=25Q2-release
+mkdir ${BRANCH}
+cd ${BRANCH}
+export ANDROID_CHECKOUT=$(pwd)
+repo init -u https://android.googlesource.com/platform/manifest -b ${BRANCH}
+repo sync -cq -j48
+source build/envsetup.sh
+lunch aosp_akita-trunk_staging-userdebug
+m -j
+m -j build-art
+m -j test-art-host
+repo manifest -r -o build_spec.xml
+
+Collected into tools/linux/host/art-16.0.0. The "host" path element is checked
+by the script for running Art.
+
+  cd <r8 checkout>
+  scripts/update-host-art.sh \
+     --android-checkout $ANDROID_CHECKOUT \
+     --art-dir host/art-16.0.0 \
+     --android-product akita
+
+  (cd tools/linux/host; upload_to_google_storage.py -a --bucket r8-deps art-16.0.0)
+
 
 art-15 (Android V)
 ------------------
diff --git a/tools/linux/host/art-16.0.0.tar.gz.sha1 b/tools/linux/host/art-16.0.0.tar.gz.sha1
new file mode 100644
index 0000000..753d730
--- /dev/null
+++ b/tools/linux/host/art-16.0.0.tar.gz.sha1
@@ -0,0 +1 @@
+28e853aaa247b3dd4435df1a339e2b11e4488105
\ No newline at end of file
diff --git a/tools/test.py b/tools/test.py
index 3ed2c71..5887e9d 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -28,8 +28,8 @@
     import thread
 
 ALL_ART_VMS = [
-    "default", "15.0.0", "14.0.0", "13.0.0", "12.0.0", "10.0.0", "9.0.0",
-    "8.1.0", "7.0.0", "6.0.1", "5.1.1", "4.4.4", "4.0.4"
+    "default", "16.0.0", "15.0.0", "14.0.0", "13.0.0", "12.0.0", "10.0.0",
+    "9.0.0", "8.1.0", "7.0.0", "6.0.1", "5.1.1", "4.4.4", "4.0.4"
 ]
 
 # How often do we check for progress on the bots: