Merge commit '51676e787bd368b5c2c9fa8ddbd04c95bf770e5e' into dev-release

Change-Id: I28698766989c1dd49d980d822edb81f787612706
diff --git a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
index d7dbde7..9139e16 100644
--- a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
@@ -286,7 +286,7 @@
         return new DexItemBasedConstString(
             stackValue, computedConstant.getItem(), computedConstant.getNameComputationInfo());
       } else if (constant.isConstClass()) {
-        return new ConstClass(stackValue, constant.asConstClass().getValue());
+        return new ConstClass(stackValue, constant.asConstClass().getType());
       } else {
         throw new Unreachable("Unexpected constant value: " + value);
       }
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index d52e9ae..4151107 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -158,6 +158,10 @@
     return isSet(Constants.ACC_PUBLIC);
   }
 
+  public boolean wasPublic() {
+    return wasSet(Constants.ACC_PUBLIC);
+  }
+
   public void setPublic() {
     assert !isPrivate() && !isProtected();
     set(Constants.ACC_PUBLIC);
diff --git a/src/main/java/com/android/tools/r8/graph/AppServices.java b/src/main/java/com/android/tools/r8/graph/AppServices.java
index 5e03a38..da91743 100644
--- a/src/main/java/com/android/tools/r8/graph/AppServices.java
+++ b/src/main/java/com/android/tools/r8/graph/AppServices.java
@@ -12,10 +12,8 @@
 import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -78,10 +76,6 @@
     assert verifyRewrittenWithLens();
     Map<FeatureSplit, List<DexType>> featureSplitListMap = services.get(serviceType);
     if (featureSplitListMap == null) {
-      assert false
-          : "Unexpected attempt to get service implementations for non-service type `"
-              + serviceType.toSourceString()
-              + "`";
       return ImmutableList.of();
     }
     ImmutableList.Builder<DexType> builder = ImmutableList.builder();
@@ -91,43 +85,8 @@
     return builder.build();
   }
 
-  public boolean hasServiceImplementationsInFeature(
-      AppView<? extends AppInfoWithLiveness> appView, DexType serviceType) {
-    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
-    if (classToFeatureSplitMap.isEmpty()) {
-      return false;
-    }
-    Map<FeatureSplit, List<DexType>> featureImplementations = services.get(serviceType);
-    if (featureImplementations == null || featureImplementations.isEmpty()) {
-      assert false
-          : "Unexpected attempt to get service implementations for non-service type `"
-              + serviceType.toSourceString()
-              + "`";
-      return true;
-    }
-    if (featureImplementations.keySet().stream().anyMatch(feature -> !feature.isBase())) {
-      return true;
-    }
-    // All service implementations are in one of the base splits.
-    assert featureImplementations.size() <= 2;
-    // Check if service is defined feature
-    DexProgramClass serviceClass = appView.definitionForProgramType(serviceType);
-    if (serviceClass != null && classToFeatureSplitMap.isInFeature(serviceClass, appView)) {
-      return true;
-    }
-    for (Entry<FeatureSplit, List<DexType>> entry : featureImplementations.entrySet()) {
-      FeatureSplit feature = entry.getKey();
-      assert feature.isBase();
-      List<DexType> implementationTypes = entry.getValue();
-      for (DexType implementationType : implementationTypes) {
-        DexProgramClass implementationClass = appView.definitionForProgramType(implementationType);
-        if (implementationClass != null
-            && classToFeatureSplitMap.isInFeature(implementationClass, appView)) {
-          return true;
-        }
-      }
-    }
-    return false;
+  public Map<FeatureSplit, List<DexType>> serviceImplementationsByFeatureFor(DexType serviceType) {
+    return services.get(serviceType);
   }
 
   public AppServices rewrittenWithLens(GraphLens graphLens, Timing timing) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index a7e37bc..3c056f8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -249,6 +249,8 @@
 
   public final DexString runtimeExceptionDescriptor = createString("Ljava/lang/RuntimeException;");
   public final DexString assertionErrorDescriptor = createString("Ljava/lang/AssertionError;");
+  public final DexString noSuchElementExceptionDescriptor =
+      createString("Ljava/util/NoSuchElementException;");
   public final DexString charSequenceDescriptor = createString("Ljava/lang/CharSequence;");
   public final DexString charSequenceArrayDescriptor = createString("[Ljava/lang/CharSequence;");
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
@@ -575,6 +577,11 @@
   public final DexType runtimeExceptionType = createStaticallyKnownType(runtimeExceptionDescriptor);
   public final DexType assertionErrorType = createStaticallyKnownType(assertionErrorDescriptor);
   public final DexType throwableType = createStaticallyKnownType(throwableDescriptor);
+  public final DexType noSuchElementExceptionType =
+      createStaticallyKnownType(noSuchElementExceptionDescriptor);
+  public final DexMethod noSuchElementExceptionInit =
+      createInstanceInitializer(noSuchElementExceptionType);
+
   public final DexType illegalAccessErrorType =
       createStaticallyKnownType(illegalAccessErrorDescriptor);
   public final DexType illegalArgumentExceptionType =
@@ -870,6 +877,7 @@
 
   public final ObjectMethodsMembers objectMethodsMembers = new ObjectMethodsMembers();
   public final ServiceLoaderMethods serviceLoaderMethods = new ServiceLoaderMethods();
+  public final IteratorMethods iteratorMethods = new IteratorMethods();
   public final StringConcatFactoryMembers stringConcatFactoryMembers =
       new StringConcatFactoryMembers();
 
@@ -2659,6 +2667,12 @@
     }
   }
 
+  public class IteratorMethods {
+    public final DexMethod hasNext =
+        createMethod(iteratorType, createProto(booleanType), "hasNext");
+    public final DexMethod next = createMethod(iteratorType, createProto(objectType), "next");
+  }
+
   private static <T extends DexItem> T canonicalize(Map<T, T> map, T item) {
     assert item != null;
     assert !DexItemFactory.isInternalSentinel(item);
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
index 1a3e78f..0cc9356 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
@@ -249,6 +249,9 @@
       return typeParameterContext;
     }
     TypeParameterSubstitutions methodFormals = this.formalsInfo.get(reference);
+    if (methodFormals == null) {
+      return typeParameterContext;
+    }
     if (clazz != null && !prunedHere) {
       DexEncodedMethod method = clazz.lookupMethod(reference.asDexMethod());
       prunedHere =
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
index 35619e1..1da7136 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
@@ -282,7 +282,7 @@
       }
       ConstClass constClassInstruction = instruction.asConstClass();
       ConstClass otherConstClassInstruction = other.asConstClass();
-      return constClassInstruction.getValue() == otherConstClassInstruction.getValue();
+      return constClassInstruction.getType() == otherConstClassInstruction.getType();
     }
 
     if (instruction.isConstNumber()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index d5f90ba..da5c7fc 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -35,6 +35,7 @@
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepFieldInfo;
 import com.android.tools.r8.threading.ThreadingModule;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.SetUtils;
@@ -126,7 +127,8 @@
   private void markFieldAsDead(DexEncodedField field) {
     // Don't mark pinned fields as dead, since they need to remain in the app even if all reads and
     // writes are removed.
-    if (appView.appInfo().isPinned(field)) {
+    KeepFieldInfo keepInfo = appView.getKeepInfo().getFieldInfo(field, appView);
+    if (!keepInfo.isOptimizationAllowed(appView.options())) {
       assert field.getType().isAlwaysNull(appView);
     } else {
       getSimpleFeedback().markFieldAsDead(field);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/DataflowAnalysisResult.java b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/DataflowAnalysisResult.java
index 73eeb55..550019d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/DataflowAnalysisResult.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/DataflowAnalysisResult.java
@@ -44,6 +44,10 @@
       this.blockExitStates = blockExitStates;
     }
 
+    public StateType getBlockExitState(Block block) {
+      return blockExitStates.get(block);
+    }
+
     public StateType join(AppView<?> appView) {
       StateType result = null;
       for (StateType blockExitState : blockExitStates.values()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysis.java
new file mode 100644
index 0000000..dd5b2c9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysis.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2024, 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.analysis.path;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraproceduralDataflowAnalysis;
+import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+/**
+ * An analysis that computes the set of active path constraints for each program point. Each path
+ * constraint is an expression where only the arguments of the current method are allowed as open
+ * variables.
+ *
+ * <p>Consider the following example.
+ *
+ * <pre>
+ *   static Object Foo(Object o, int flags) {
+ *     if ((flags & 1) != 0) {
+ *       o = DEFAULT_VALUE;
+ *     }
+ *     return o;
+ *   }
+ * </pre>
+ *
+ * <ul>
+ *   <li>The path constraint for the entry block is the empty set, since control flow
+ *       unconditionally hits this block.
+ *   <li>The path constraint for the block containing {@code o = DEFAULT_VALUE} is [(flags & 1) !=
+ *       0].
+ *   <li>The path constraint for the (empty) ELSE block is [(flags & 1) == 0].
+ *   <li>The path constraint for the return block is the empty set.
+ * </ul>
+ */
+public class PathConstraintAnalysis
+    extends IntraproceduralDataflowAnalysis<PathConstraintAnalysisState> {
+
+  public PathConstraintAnalysis(AppView<AppInfoWithLiveness> appView, IRCode code) {
+    super(
+        appView,
+        PathConstraintAnalysisState.bottom(),
+        code,
+        new PathConstraintAnalysisTransferFunction(appView.abstractValueFactory()));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java
new file mode 100644
index 0000000..6b86310
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2024, 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.analysis.path;
+
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractTransferFunction;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
+import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeBuilder;
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
+
+public class PathConstraintAnalysisTransferFunction
+    implements AbstractTransferFunction<BasicBlock, Instruction, PathConstraintAnalysisState> {
+
+  private final ComputationTreeBuilder computationTreeBuilder;
+
+  PathConstraintAnalysisTransferFunction(AbstractValueFactory abstractValueFactory) {
+    computationTreeBuilder = new ComputationTreeBuilder(abstractValueFactory);
+  }
+
+  @Override
+  public TransferFunctionResult<PathConstraintAnalysisState> apply(
+      Instruction instruction, PathConstraintAnalysisState state) {
+    // Instructions normally to not change the current path constraint.
+    //
+    // One exception is when information can be deduced from throwing instructions that succeed.
+    // For example, if the instruction `arg.method()` succeeds then it can be inferred that the
+    // subsequent instruction is only executed if `arg != null`.
+    return state;
+  }
+
+  @Override
+  public PathConstraintAnalysisState computeBlockEntryState(
+      BasicBlock block, BasicBlock predecessor, PathConstraintAnalysisState predecessorExitState) {
+    if (predecessorExitState.isUnknown()) {
+      return predecessorExitState;
+    }
+    // We currently only amend the path constraint in presence of if-instructions.
+    If theIf = predecessor.exit().asIf();
+    if (theIf != null) {
+      // TODO(b/302281503): Ensure the computed computation tree is cached in the builder so that
+      //  we do not rebuild the tree over-and-over again during the execution of this (worklist)
+      //  analysis.
+      ComputationTreeNode newPathConstraint = computationTreeBuilder.buildComputationTree(theIf);
+      if (!newPathConstraint.isUnknown()) {
+        boolean negate = block != theIf.getTrueTarget();
+        return predecessorExitState.add(newPathConstraint, negate);
+      }
+    }
+    return predecessorExitState;
+  }
+
+  @Override
+  public PathConstraintAnalysisState computeExceptionalBlockEntryState(
+      BasicBlock block,
+      DexType guard,
+      BasicBlock throwBlock,
+      Instruction throwInstruction,
+      PathConstraintAnalysisState throwState) {
+    // For the purpose of this analysis we don't (?) care much about the path constraints for blocks
+    // that are reached from catch handlers. Therefore, we currently set the state to UNKNOWN for
+    // all blocks that can be reached from a catch handler.
+    return PathConstraintAnalysisState.unknown();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/state/BottomPathConstraintAnalysisState.java b/src/main/java/com/android/tools/r8/ir/analysis/path/state/BottomPathConstraintAnalysisState.java
new file mode 100644
index 0000000..6ed306f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/state/BottomPathConstraintAnalysisState.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2024, 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.analysis.path.state;
+
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
+
+public class BottomPathConstraintAnalysisState extends PathConstraintAnalysisState {
+
+  private static final BottomPathConstraintAnalysisState INSTANCE =
+      new BottomPathConstraintAnalysisState();
+
+  private BottomPathConstraintAnalysisState() {}
+
+  static BottomPathConstraintAnalysisState getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public PathConstraintAnalysisState add(ComputationTreeNode pathConstraint, boolean negate) {
+    return ConcretePathConstraintAnalysisState.create(pathConstraint, negate);
+  }
+
+  @Override
+  public boolean isBottom() {
+    return true;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return this == other;
+  }
+
+  @Override
+  public int hashCode() {
+    return System.identityHashCode(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java b/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java
new file mode 100644
index 0000000..361ed99
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java
@@ -0,0 +1,160 @@
+// Copyright (c) 2024, 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.analysis.path.state;
+
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a non-trivial (neither bottom nor top) path constraint that must be satisfied to reach
+ * a given program point.
+ *
+ * <p>The state should be interpreted as follows:
+ *
+ * <ol>
+ *   <li>If a path constraint is BOTH in {@link #pathConstraints} AND {@link
+ *       #negatedPathConstraints} then the path constraint should be IGNORED.
+ *   <li>If a path constraint is ONLY in {@link #pathConstraints} then the current program point can
+ *       only be reached if the path constraint is satisfied.
+ *   <li>If a path constraint is ONLY in {@link #negatedPathConstraints} then the current program
+ *       point can only be reached if the path constraint is NOT satisfied.
+ * </ol>
+ *
+ * <p>Example: In the below example, when entering the IF-THEN branch, we add the path constraint
+ * [(flags & 1) != 0] to {@link #pathConstraints}. When entering the (empty) IF-ELSE branch, we add
+ * the same path constraint to {@link #negatedPathConstraints}. When reaching the return block, the
+ * two path constraint states are joined, resulting in the state where {@link #pathConstraints} and
+ * {@link #negatedPathConstraints} both contains the constraint [(flags & 1) != 0].
+ *
+ * <pre>
+ *   static Object Foo(Object o, int flags) {
+ *     if ((flags & 1) != 0) {
+ *       o = DEFAULT_VALUE;
+ *     }
+ *     return o;
+ *   }
+ * </pre>
+ */
+public class ConcretePathConstraintAnalysisState extends PathConstraintAnalysisState {
+
+  private final Set<ComputationTreeNode> pathConstraints;
+  private final Set<ComputationTreeNode> negatedPathConstraints;
+
+  ConcretePathConstraintAnalysisState(
+      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> negatedPathConstraints) {
+    this.pathConstraints = pathConstraints;
+    this.negatedPathConstraints = negatedPathConstraints;
+  }
+
+  static ConcretePathConstraintAnalysisState create(
+      ComputationTreeNode pathConstraint, boolean negate) {
+    Set<ComputationTreeNode> pathConstraints = Collections.singleton(pathConstraint);
+    if (negate) {
+      return new ConcretePathConstraintAnalysisState(Collections.emptySet(), pathConstraints);
+    } else {
+      return new ConcretePathConstraintAnalysisState(pathConstraints, Collections.emptySet());
+    }
+  }
+
+  @Override
+  public PathConstraintAnalysisState add(ComputationTreeNode pathConstraint, boolean negate) {
+    if (negate) {
+      if (negatedPathConstraints.contains(pathConstraint)) {
+        return this;
+      }
+      return new ConcretePathConstraintAnalysisState(
+          pathConstraints, add(pathConstraint, negatedPathConstraints));
+    } else {
+      if (pathConstraints.contains(pathConstraint)) {
+        return this;
+      }
+      return new ConcretePathConstraintAnalysisState(
+          add(pathConstraint, pathConstraints), negatedPathConstraints);
+    }
+  }
+
+  private static Set<ComputationTreeNode> add(
+      ComputationTreeNode pathConstraint, Set<ComputationTreeNode> pathConstraints) {
+    if (pathConstraints.isEmpty()) {
+      return Collections.singleton(pathConstraint);
+    }
+    assert !pathConstraints.contains(pathConstraint);
+    Set<ComputationTreeNode> newPathConstraints = new HashSet<>(pathConstraints);
+    newPathConstraints.add(pathConstraint);
+    return newPathConstraints;
+  }
+
+  public Set<ComputationTreeNode> getPathConstraints() {
+    return pathConstraints;
+  }
+
+  public Set<ComputationTreeNode> getNegatedPathConstraints() {
+    return negatedPathConstraints;
+  }
+
+  @Override
+  public boolean isConcrete() {
+    return true;
+  }
+
+  @Override
+  public ConcretePathConstraintAnalysisState asConcreteState() {
+    return this;
+  }
+
+  public ConcretePathConstraintAnalysisState join(ConcretePathConstraintAnalysisState other) {
+    Set<ComputationTreeNode> newPathConstraints = join(pathConstraints, other.pathConstraints);
+    Set<ComputationTreeNode> newNegatedPathConstraints =
+        join(negatedPathConstraints, other.negatedPathConstraints);
+    if (identical(newPathConstraints, newNegatedPathConstraints)) {
+      return this;
+    }
+    if (other.identical(newPathConstraints, newNegatedPathConstraints)) {
+      return other;
+    }
+    return new ConcretePathConstraintAnalysisState(newPathConstraints, newNegatedPathConstraints);
+  }
+
+  private static Set<ComputationTreeNode> join(
+      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> otherPathConstraints) {
+    if (pathConstraints.isEmpty()) {
+      return otherPathConstraints;
+    }
+    if (otherPathConstraints.isEmpty()) {
+      return pathConstraints;
+    }
+    Set<ComputationTreeNode> newPathConstraints =
+        new HashSet<>(pathConstraints.size() + otherPathConstraints.size());
+    newPathConstraints.addAll(pathConstraints);
+    newPathConstraints.addAll(otherPathConstraints);
+    return newPathConstraints;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ConcretePathConstraintAnalysisState)) {
+      return false;
+    }
+    ConcretePathConstraintAnalysisState state = (ConcretePathConstraintAnalysisState) obj;
+    return pathConstraints.equals(state.pathConstraints)
+        && negatedPathConstraints.equals(state.negatedPathConstraints);
+  }
+
+  public boolean identical(
+      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> negatedPathConstraints) {
+    return this.pathConstraints == pathConstraints
+        && this.negatedPathConstraints == negatedPathConstraints;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(pathConstraints, negatedPathConstraints);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/state/PathConstraintAnalysisState.java b/src/main/java/com/android/tools/r8/ir/analysis/path/state/PathConstraintAnalysisState.java
new file mode 100644
index 0000000..fea03cc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/state/PathConstraintAnalysisState.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2024, 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.analysis.path.state;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractState;
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
+
+public abstract class PathConstraintAnalysisState
+    extends AbstractState<PathConstraintAnalysisState> {
+
+  public static BottomPathConstraintAnalysisState bottom() {
+    return BottomPathConstraintAnalysisState.getInstance();
+  }
+
+  public static UnknownPathConstraintAnalysisState unknown() {
+    return UnknownPathConstraintAnalysisState.getInstance();
+  }
+
+  public abstract PathConstraintAnalysisState add(
+      ComputationTreeNode pathConstraint, boolean negate);
+
+  public boolean isBottom() {
+    return false;
+  }
+
+  public boolean isConcrete() {
+    return false;
+  }
+
+  public boolean isUnknown() {
+    return false;
+  }
+
+  @Override
+  public PathConstraintAnalysisState asAbstractState() {
+    return this;
+  }
+
+  public ConcretePathConstraintAnalysisState asConcreteState() {
+    return null;
+  }
+
+  @Override
+  public PathConstraintAnalysisState join(AppView<?> appView, PathConstraintAnalysisState other) {
+    if (isBottom() || other.isUnknown()) {
+      return other;
+    }
+    if (other.isBottom() || isUnknown()) {
+      return this;
+    }
+    return asConcreteState().join(other.asConcreteState());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/state/UnknownPathConstraintAnalysisState.java b/src/main/java/com/android/tools/r8/ir/analysis/path/state/UnknownPathConstraintAnalysisState.java
new file mode 100644
index 0000000..17684e6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/state/UnknownPathConstraintAnalysisState.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2024, 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.analysis.path.state;
+
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
+
+public class UnknownPathConstraintAnalysisState extends PathConstraintAnalysisState {
+
+  private static final UnknownPathConstraintAnalysisState INSTANCE =
+      new UnknownPathConstraintAnalysisState();
+
+  private UnknownPathConstraintAnalysisState() {}
+
+  static UnknownPathConstraintAnalysisState getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public PathConstraintAnalysisState add(ComputationTreeNode pathConstraint, boolean negate) {
+    return this;
+  }
+
+  @Override
+  public boolean isUnknown() {
+    return true;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return this == other;
+  }
+
+  @Override
+  public int hashCode() {
+    return System.identityHashCode(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
index ef36cea..226e172 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
@@ -202,7 +202,7 @@
       Instruction definition = root.definition;
       if (definition.isConstClass()) {
         ConstClass constClass = definition.asConstClass();
-        return new ProtoTypeObject(constClass.getValue());
+        return new ProtoTypeObject(constClass.getType());
       } else if (definition.isConstString()) {
         ConstString constString = definition.asConstString();
         DexEncodedField field =
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index cc8cfb9..b83c92e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -9,9 +9,11 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.function.IntFunction;
 
-public abstract class AbstractValue {
+public abstract class AbstractValue implements ComputationTreeNode {
 
   public static BottomValue bottom() {
     return BottomValue.getInstance();
@@ -21,6 +23,12 @@
     return UnknownValue.getInstance();
   }
 
+  @Override
+  public AbstractValue evaluate(
+      IntFunction<AbstractValue> argumentAssignment, AbstractValueFactory abstractValueFactory) {
+    return this;
+  }
+
   public abstract boolean isNonTrivial();
 
   public boolean isSingleBoolean() {
@@ -241,6 +249,7 @@
     return false;
   }
 
+  @Override
   public boolean isUnknown() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
index 27534c2..cf6875d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.analysis.value.objectstate.KnownLengthArrayState;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import com.android.tools.r8.utils.BooleanUtils;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class AbstractValueFactory {
@@ -102,6 +103,10 @@
         : new SingleStatefulFieldValue(field, state);
   }
 
+  public SingleNumberValue createSingleBooleanValue(boolean value) {
+    return createUncheckedSingleNumberValue(BooleanUtils.intValue(value));
+  }
+
   public SingleNumberValue createSingleNumberValue(long value, TypeElement type) {
     assert type.isPrimitiveType();
     return createUncheckedSingleNumberValue(value);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java b/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java
index 9b37213..92e4816 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java
@@ -7,12 +7,18 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.utils.BitUtils;
 
 public class AbstractCalculator {
 
   public static AbstractValue andIntegers(
       AppView<?> appView, AbstractValue left, AbstractValue right) {
+    return andIntegers(appView.abstractValueFactory(), left, right);
+  }
+
+  public static AbstractValue andIntegers(
+      AbstractValueFactory abstractValueFactory, AbstractValue left, AbstractValue right) {
     if (left.isZero()) {
       return left;
     }
@@ -22,25 +28,21 @@
     if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
       int result =
           left.asSingleNumberValue().getIntValue() & right.asSingleNumberValue().getIntValue();
-      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+      return abstractValueFactory.createUncheckedSingleNumberValue(result);
     }
     if (left.hasDefinitelySetAndUnsetBitsInformation()
         && right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              left.getDefinitelySetIntBits() & right.getDefinitelySetIntBits(),
-              left.getDefinitelyUnsetIntBits() | right.getDefinitelyUnsetIntBits());
+      return abstractValueFactory.createDefiniteBitsNumberValue(
+          left.getDefinitelySetIntBits() & right.getDefinitelySetIntBits(),
+          left.getDefinitelyUnsetIntBits() | right.getDefinitelyUnsetIntBits());
     }
     if (left.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(0, left.getDefinitelyUnsetIntBits());
+      return abstractValueFactory.createDefiniteBitsNumberValue(
+          0, left.getDefinitelyUnsetIntBits());
     }
     if (right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(0, right.getDefinitelyUnsetIntBits());
+      return abstractValueFactory.createDefiniteBitsNumberValue(
+          0, right.getDefinitelyUnsetIntBits());
     }
     return AbstractValue.unknown();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 2efeeae..d1fc0ed 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -229,30 +229,6 @@
     return dstIterator;
   }
 
-  @Override
-  public BasicBlock addThrowingInstructionToPossiblyThrowingBlock(
-      IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      Instruction instruction,
-      InternalOptions options) {
-    if (block.hasCatchHandlers()) {
-      BasicBlock splitBlock = split(code, blockIterator, false);
-      splitBlock.listIterator(code).add(instruction);
-      assert !block.hasCatchHandlers();
-      assert splitBlock.hasCatchHandlers();
-      block.copyCatchHandlers(code, blockIterator, splitBlock, options);
-      if (blockIterator != null) {
-        while (IteratorUtils.peekPrevious(blockIterator) != splitBlock) {
-          blockIterator.previous();
-        }
-      }
-      return splitBlock;
-    } else {
-      add(instruction);
-      return null;
-    }
-  }
-
   /**
    * Replaces the last instruction returned by {@link #next} or {@link #previous} with the specified
    * instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
index 72721d2..fe1c8b3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
@@ -68,17 +68,13 @@
 
   public static ConstClass copyOf(Value newValue, ConstClass original) {
     assert newValue != original.outValue();
-    return new ConstClass(newValue, original.getValue());
+    return new ConstClass(newValue, original.getType());
   }
 
   public Value dest() {
     return outValue;
   }
 
-  public DexType getValue() {
-    return clazz;
-  }
-
   @Override
   public void buildDex(DexBuilder builder) {
     int dest = builder.allocatedRegister(dest(), getNumber());
@@ -123,7 +119,7 @@
       ProgramMethod context,
       AbstractValueSupplier abstractValueSupplier,
       SideEffectAssumption assumption) {
-    DexType baseType = getValue().toBaseType(appView.dexItemFactory());
+    DexType baseType = getType().toBaseType(appView.dexItemFactory());
     if (baseType.isPrimitiveType()) {
       return false;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 634225b..95df669 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -339,13 +339,16 @@
   @Override
   public AbstractValue getAbstractValue(
       AppView<?> appView, ProgramMethod context, AbstractValueSupplier abstractValueSupplier) {
+    return getAbstractValue(appView.abstractValueFactory());
+  }
+
+  public AbstractValue getAbstractValue(AbstractValueFactory abstractValueFactory) {
     if (outValue.hasLocalInfo()) {
       return AbstractValue.unknown();
     }
-    AbstractValueFactory factory = appView.abstractValueFactory();
     return getOutType().isReferenceType()
-        ? factory.createNullValue(getOutType())
-        : factory.createSingleNumberValue(value, getOutType());
+        ? abstractValueFactory.createNullValue(getOutType())
+        : abstractValueFactory.createSingleNumberValue(value, getOutType());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstantValueUtils.java b/src/main/java/com/android/tools/r8/ir/code/ConstantValueUtils.java
index 9855ee6..381f4b4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstantValueUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstantValueUtils.java
@@ -24,7 +24,7 @@
     }
 
     if (alias.definition.isConstClass()) {
-      return alias.definition.asConstClass().getValue();
+      return alias.definition.asConstClass().getType();
     }
 
     if (alias.definition.isInvokeStatic()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 5077746..307acdf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -1109,7 +1109,7 @@
     return new IRCodeInstructionIterator(this);
   }
 
-  public InstructionListIterator instructionListIterator() {
+  public IRCodeInstructionListIterator instructionListIterator() {
     return new IRCodeInstructionListIterator(this);
   }
 
@@ -1241,6 +1241,14 @@
     return createNumberConstant(Float.floatToIntBits(value), TypeElement.getFloat(), local);
   }
 
+  public ConstNumber createBooleanConstant(boolean value) {
+    return createBooleanConstant(value, null);
+  }
+
+  public ConstNumber createBooleanConstant(boolean value, DebugLocalInfo local) {
+    return createNumberConstant(value ? 1 : 0, TypeElement.getInt(), local);
+  }
+
   public ConstNumber createIntConstant(int value) {
     return createIntConstant(value, null);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 6b33cca..13e393a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -35,6 +35,10 @@
     this.instructionIterator = blockIterator.next().listIterator(code);
   }
 
+  public IRCode getCode() {
+    return code;
+  }
+
   @Override
   public Value insertConstNumberInstruction(
       IRCode code, InternalOptions options, long value, TypeElement type) {
@@ -244,14 +248,11 @@
         code, blockIterator, instructionsToAdd, options);
   }
 
-  @Override
-  public BasicBlock addThrowingInstructionToPossiblyThrowingBlock(
-      IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      Instruction instruction,
-      InternalOptions options) {
-    return instructionIterator.addThrowingInstructionToPossiblyThrowingBlock(
-        code, blockIterator, instruction, options);
+  public void addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+      Collection<? extends Instruction> instructionsToAdd, InternalOptions options) {
+    instructionIterator =
+        instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+            code, blockIterator, instructionsToAdd, options);
   }
 
   @Override
@@ -259,6 +260,13 @@
     instructionIterator.remove();
   }
 
+  public void removeRemainingInBlockIgnoreOutValue() {
+    while (instructionIterator.hasNext()) {
+      instructionIterator.next();
+      instructionIterator.removeInstructionIgnoreOutValue();
+    }
+  }
+
   @Override
   public void set(Instruction instruction) {
     instructionIterator.set(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/code/IfType.java b/src/main/java/com/android/tools/r8/ir/code/IfType.java
index eef3f3b..5babdae 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IfType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IfType.java
@@ -4,14 +4,63 @@
 package com.android.tools.r8.ir.code;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
 
 public enum IfType {
-  EQ,
-  GE,
-  GT,
-  LE,
-  LT,
-  NE;
+  EQ {
+    @Override
+    public AbstractValue evaluate(
+        SingleNumberValue operand, AbstractValueFactory abstractValueFactory) {
+      return abstractValueFactory.createSingleBooleanValue(operand.getValue() == 0);
+    }
+  },
+  GE {
+    @Override
+    public AbstractValue evaluate(
+        SingleNumberValue operand, AbstractValueFactory abstractValueFactory) {
+      return abstractValueFactory.createSingleBooleanValue(operand.getValue() >= 0);
+    }
+  },
+  GT {
+    @Override
+    public AbstractValue evaluate(
+        SingleNumberValue operand, AbstractValueFactory abstractValueFactory) {
+      return abstractValueFactory.createSingleBooleanValue(operand.getValue() > 0);
+    }
+  },
+  LE {
+    @Override
+    public AbstractValue evaluate(
+        SingleNumberValue operand, AbstractValueFactory abstractValueFactory) {
+      return abstractValueFactory.createSingleBooleanValue(operand.getValue() <= 0);
+    }
+  },
+  LT {
+    @Override
+    public AbstractValue evaluate(
+        SingleNumberValue operand, AbstractValueFactory abstractValueFactory) {
+      return abstractValueFactory.createSingleBooleanValue(operand.getValue() < 0);
+    }
+  },
+  NE {
+    @Override
+    public AbstractValue evaluate(
+        SingleNumberValue operand, AbstractValueFactory abstractValueFactory) {
+      return abstractValueFactory.createSingleBooleanValue(operand.getValue() != 0);
+    }
+  };
+
+  public AbstractValue evaluate(AbstractValue operand, AbstractValueFactory abstractValueFactory) {
+    if (operand.isSingleNumberValue()) {
+      return evaluate(operand.asSingleNumberValue(), abstractValueFactory);
+    }
+    return AbstractValue.unknown();
+  }
+
+  public abstract AbstractValue evaluate(
+      SingleNumberValue operand, AbstractValueFactory abstractValueFactory);
 
   public boolean isEqualsOrNotEquals() {
     return this == EQ || this == NE;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 19c675b..c1e8aa6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Sets;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.ListIterator;
 import java.util.Set;
@@ -69,11 +70,14 @@
         code, blockIterator, Arrays.asList(instructionsToAdd), options);
   }
 
-  BasicBlock addThrowingInstructionToPossiblyThrowingBlock(
+  default InstructionListIterator addPossiblyThrowingInstructionToPossiblyThrowingBlock(
       IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      Instruction instruction,
-      InternalOptions options);
+      BasicBlockIterator blockIterator,
+      Instruction instructionToAdd,
+      InternalOptions options) {
+    return addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+        code, blockIterator, Collections.singleton(instructionToAdd), options);
+  }
 
   default void addAndPositionBeforeNewInstruction(Instruction instruction) {
     add(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 63c5c94..0885e72 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -210,16 +210,6 @@
   }
 
   @Override
-  public BasicBlock addThrowingInstructionToPossiblyThrowingBlock(
-      IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      Instruction instruction,
-      InternalOptions options) {
-    return currentBlockIterator.addThrowingInstructionToPossiblyThrowingBlock(
-        code, blockIterator, instruction, options);
-  }
-
-  @Override
   public void removeOrReplaceByDebugLocalRead() {
     currentBlockIterator.removeOrReplaceByDebugLocalRead();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
index d87fed8..8877b0e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
@@ -24,4 +24,19 @@
     }
     return null;
   }
+
+  /**
+   * Continue to call {@link #next} until it returns the given item.
+   *
+   * @returns item if it was found, null otherwise.
+   */
+  @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
+  default <S extends T> S nextUntil(T item) {
+    while (hasNext()) {
+      if (next() == item) {
+        return (S) item;
+      }
+    }
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NumericType.java b/src/main/java/com/android/tools/r8/ir/code/NumericType.java
index 85b80b5..4e8d0d0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NumericType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NumericType.java
@@ -16,6 +16,10 @@
   FLOAT,
   DOUBLE;
 
+  public boolean isInt() {
+    return this == INT;
+  }
+
   public DexType toDexType(DexItemFactory factory) {
     switch (this) {
       case BYTE:
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 7383048..dcc3abf 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -898,7 +898,7 @@
       timing.end();
     }
 
-    if (options.debug || appView.getKeepInfo(code.context()).isPinned(options)) {
+    if (options.debug || appView.getKeepInfo(code.context()).isCodeReplacementAllowed(options)) {
       return;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index ca0f4269..679a884 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -116,6 +116,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LazyBox;
 import com.android.tools.r8.verticalclassmerging.InterfaceTypeToClassTypeLensCodeRewriterHelper;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -721,7 +722,7 @@
               Instruction replacement =
                   new InstructionReplacer(code, current, iterator, affectedPhis)
                       .replaceInstructionIfTypeChanged(
-                          constClass.getValue(),
+                          constClass.getType(),
                           (t, v) ->
                               t.isPrimitiveType() || t.isVoidType()
                                   ? StaticGet.builder()
@@ -1062,7 +1063,8 @@
             .setOutValue(castOutValue)
             .setPosition(fieldGet.asFieldInstruction())
             .build();
-    iterator.addThrowingInstructionToPossiblyThrowingBlock(code, blocks, checkCast, options);
+    iterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+        code, blocks, ImmutableList.of(checkCast), options);
     affectedPhis.addAll(checkCast.outValue().uniquePhiUsers());
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
index a8fe188..d31a884 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -52,13 +52,17 @@
       CallGraph callGraph,
       MethodProcessorEventConsumer eventConsumer,
       ProgramMethodSet methodsToProcess) {
-    this.callSiteInformation = callGraph.createCallSiteInformation(appView, this);
     this.eventConsumer = eventConsumer;
     this.methodsToProcess = methodsToProcess;
     this.processorContext = appView.createProcessorContext();
+    this.callSiteInformation = callGraph.createCallSiteInformation(appView, this);
     this.waves = createWaves(callGraph);
   }
 
+  public void addMethodToProcess(ProgramMethod method) {
+    methodsToProcess.add(method);
+  }
+
   public void markCallersForProcessing(ProgramMethod method) {
     assert wave.contains(method);
     synchronized (methodsToProcess) {
@@ -173,10 +177,6 @@
             });
         put(set);
       }
-      if (methodsToReprocessBuilder.isEmpty()) {
-        // Nothing to revisit.
-        return null;
-      }
       ProgramMethodSet methodsToReprocess = methodsToReprocessBuilder.build(appView);
       // TODO(b/333677610): Check this assert when bridges synthesized by member rebinding is
       //  always removed
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
index 84685ae..f89b900 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
@@ -140,15 +140,19 @@
             }
           }
           Set<Node> callersWithDeterministicOrder = node.getCallersWithDeterministicOrder();
-          DexMethod caller = reference;
+          ProgramMethod caller = method;
           // We can have recursive methods where the recursive call is the only call site. We do
           // not track callers for these.
           if (!callersWithDeterministicOrder.isEmpty()) {
             assert callersWithDeterministicOrder.size() == 1;
-            caller = callersWithDeterministicOrder.iterator().next().getMethod().getReference();
+            caller = callersWithDeterministicOrder.iterator().next().getProgramMethod();
           }
           assert !singleCallerMethods.containsKey(reference);
-          singleCallerMethods.put(reference, caller);
+          singleCallerMethods.put(reference, caller.getReference());
+          if (methodProcessor.isPostMethodProcessor()
+              && appView.getKeepInfo(caller).isReprocessingAllowed(options, caller)) {
+            methodProcessor.asPostMethodProcessor().addMethodToProcess(caller);
+          }
         } else if (numberOfCallSites > 1 && methodProcessor.isPrimaryMethodProcessor()) {
           multiCallerInlineCandidates.add(reference);
         }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
index 809f647..5ab36a6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
@@ -57,13 +57,13 @@
       // We don't care about calls to native methods.
       return;
     }
-    if (!appViewWithLiveness
+    if (appViewWithLiveness
         .getKeepInfo(callee)
-        .isOptimizationAllowed(appViewWithLiveness.options())) {
-      // Since the callee is kept and optimizations are disallowed, we cannot inline it into the
-      // caller, and we also cannot collect any optimization info for the method. Therefore, we
-      // drop the call edge to reduce the total number of call graph edges, which should lead to
-      // fewer call graph cycles.
+        .isCodeReplacementAllowed(appViewWithLiveness.options())) {
+      // Since the code of the callee may be replaced, we cannot inline it into the caller, and we
+      // also cannot collect any optimization info for the method. Therefore, we drop the call edge
+      // to reduce the total number of call graph edges, which should lead to fewer call graph
+      // cycles.
       return;
     }
     nodeFactory.apply(callee).addCallerConcurrently(currentMethod, likelySpuriousCallEdge);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java
index 73f5f8a..9941ea0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java
@@ -100,7 +100,7 @@
       }
 
       ConstClass constClass = in.definition.asConstClass();
-      DexType type = constClass.getValue();
+      DexType type = constClass.getType();
       int arrayDepth = type.getNumberOfLeadingSquareBrackets();
       DexType baseType = type.toBaseType(dexItemFactory);
       // Make sure base type is a class type.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index c5e7aaa..e408515 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -582,7 +582,7 @@
         if (elementValue.isConstString()) {
           addOccurrence(elementValue.getDefinition().asConstString().getValue());
         } else if (elementValue.isConstClass()) {
-          addOccurrence(elementValue.getDefinition().asConstClass().getValue());
+          addOccurrence(elementValue.getDefinition().asConstClass().getType());
         } else if (elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet)) {
           addOccurrence(elementValue.getDefinition().asStaticGet().getField());
         }
@@ -600,7 +600,7 @@
           return value;
         }
       } else if (elementValue.isConstClass()) {
-        DexType type = elementValue.getDefinition().asConstClass().getValue();
+        DexType type = elementValue.getDefinition().asConstClass().getType();
         Value value = constantValue.get(type);
         if (value != null) {
           seenOcourence(type);
@@ -657,7 +657,7 @@
         return instruction.asStaticGet().getField();
       } else {
         assert instruction.isConstClass();
-        return instruction.asConstClass().getValue();
+        return instruction.asConstClass().getType();
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
index 984e829..4a2d298 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
@@ -274,8 +274,7 @@
     if (checkCast.isSafeCheckCast()
         || checkCast
             .getFirstOperand()
-            .getDynamicType(appViewWithLiveness)
-            .getDynamicUpperBoundType()
+            .getDynamicUpperBoundType(appViewWithLiveness)
             .lessThanOrEqualUpToNullability(castTypeLattice, appView)) {
       TypeElement useType =
           TypeUtils.computeUseType(appViewWithLiveness, context, checkCast.outValue());
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
index b6804e7..f8b502a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -563,7 +563,7 @@
       Value inValue = invoke.inValues().get(0);
       return !inValue.isPhi()
           && inValue.definition.isConstClass()
-          && inValue.definition.asConstClass().getValue() == clazz.type;
+          && inValue.definition.asConstClass().getType() == clazz.type;
     }
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 13a06ea..7a18075 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -163,7 +163,7 @@
                 assert candidate.instructionTypeCanBeCanonicalized();
                 switch (candidate.opcode()) {
                   case CONST_CLASS:
-                    return candidate.asConstClass().getValue().hashCode();
+                    return candidate.asConstClass().getType().hashCode();
                   case CONST_NUMBER:
                     return Long.hashCode(candidate.asConstNumber().getRawValue())
                         + 13 * candidate.outType().hashCode();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 4b9c547..99463ac 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -1299,15 +1299,28 @@
           // Convert and remove virtual single caller inlined methods to abstract or throw null.
           singleCallerInlinedMethodsForClass.removeIf(
               (callee, caller) -> {
-                // TODO(b/203188583): Enable pruning of methods with generic signatures. For this to
-                //  work we need to pass in a seed to GenericSignatureContextBuilder.create in R8.
-                if (callee.getDefinition().belongsToVirtualPool()
-                    || callee.getDefinition().getGenericSignature().hasSignature()) {
+                boolean convertToAbstractOrThrowNullMethod =
+                    callee.getDefinition().belongsToVirtualPool();
+                if (callee.getDefinition().getGenericSignature().hasSignature()) {
+                  // TODO(b/203188583): Enable pruning of methods with generic signatures. For this
+                  //  to work we need to pass in a seed to GenericSignatureContextBuilder.create in
+                  //  R8.
+                  convertToAbstractOrThrowNullMethod = true;
+                } else if (appView.options().configurationDebugging
+                    && appView.getSyntheticItems().isSynthetic(callee.getHolder())) {
+                  // If static synthetic methods are removed after being single caller inlined, we
+                  // need to unregister them as synthetic methods in the synthetic items collection.
+                  // This means that they will not be renamed to ExternalSynthetic leading to
+                  // assertion errors. This should only be a problem when configuration debugging is
+                  // enabled, since configuration debugging disables shrinking of the synthetic
+                  // method's holder.
+                  convertToAbstractOrThrowNullMethod = true;
+                }
+                if (convertToAbstractOrThrowNullMethod) {
                   callee.convertToAbstractOrThrowNullMethod(appView);
                   converter.onMethodCodePruned(callee);
-                  return true;
                 }
-                return false;
+                return convertToAbstractOrThrowNullMethod;
               });
 
           // Remove direct single caller inlined methods from the application.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 86706f2..d7bf459 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -4,34 +4,45 @@
 
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory.IteratorMethods;
 import com.android.tools.r8.graph.DexItemFactory.ServiceLoaderMethods;
-import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRCodeInstructionListIterator;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.desugar.ServiceLoaderSourceCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.ListUtils;
-import com.google.common.collect.ImmutableList;
+import com.android.tools.r8.utils.WorkList;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,11 +78,16 @@
 
   private final AndroidApiLevelCompute apiLevelCompute;
   private final ServiceLoaderMethods serviceLoaderMethods;
+  private final IteratorMethods iteratorMethods;
+  private final boolean hasAssumeNoSideEffects;
 
   public ServiceLoaderRewriter(AppView<?> appView) {
     super(appView);
     this.apiLevelCompute = appView.apiLevelCompute();
     this.serviceLoaderMethods = appView.dexItemFactory().serviceLoaderMethods;
+    this.iteratorMethods = appView.dexItemFactory().iteratorMethods;
+    hasAssumeNoSideEffects =
+        appView.getAssumeInfoCollection().isSideEffectFree(serviceLoaderMethods.load);
   }
 
   @Override
@@ -100,156 +116,396 @@
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    InstructionListIterator instructionIterator = code.instructionListIterator();
     // Create a map from service type to loader methods local to this context since two
     // service loader calls to the same type in different methods and in the same wave can race.
     Map<DexType, DexEncodedMethod> synthesizedServiceLoaders = new IdentityHashMap<>();
-    while (instructionIterator.hasNext()) {
-      Instruction instruction = instructionIterator.next();
+    IRCodeInstructionListIterator iterator = code.instructionListIterator();
+    Map<Instruction, Instruction> replacements = new HashMap<>();
+    Map<Instruction, List<Instruction>> replacementExtras = new HashMap<>();
 
-      // Check if instruction is an invoke static on the desired form of ServiceLoader.load.
-      if (!instruction.isInvokeStatic()) {
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+
+      // Find ServiceLoader.load() calls.
+      InvokeStatic invokeInstr = instruction.asInvokeStatic();
+      if (invokeInstr == null
+          || !serviceLoaderMethods.isLoadMethod(invokeInstr.getInvokedMethod())) {
         continue;
       }
 
-      InvokeStatic serviceLoaderLoad = instruction.asInvokeStatic();
-      DexMethod invokedMethod = serviceLoaderLoad.getInvokedMethod();
-      if (!serviceLoaderMethods.isLoadMethod(invokedMethod)) {
+      // See if it can be optimized.
+      ServiceLoaderLoadResult loadResult = analyzeServiceLoaderLoad(code, invokeInstr);
+      if (loadResult == null) {
         continue;
       }
+      // See if there is a subsequent iterator() call that can be optimized.
+      DirectRewriteResult directRewriteResult = analyzeForDirectRewrite(loadResult);
 
-      // Check that the first argument is a const class.
-      Value argument = serviceLoaderLoad.getFirstArgument().getAliasedValue();
-      if (!argument.isDefinedByInstructionSatisfying(Instruction::isConstClass)) {
-        report(code.context(), null, "The service loader type could not be determined");
-        continue;
+      // Remove ServiceLoader.load()
+      replacements.put(loadResult.loadInvoke, code.createConstNull());
+      // Remove getClassLoader() if we are about to remove all users.
+      if (loadResult.classLoaderInvoke != null
+          && loadResult.classLoaderInvoke.outValue().aliasedUsers().stream()
+              .allMatch(ins -> ins.isAssume() || replacements.containsKey(ins))) {
+        replacements.put(loadResult.classLoaderInvoke, code.createConstNull());
       }
-
-      ConstClass constClass = argument.getDefinition().asConstClass();
-      if (invokedMethod.isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
-        report(
-            code.context(),
-            constClass.getType(),
-            "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
-                + " java.lang.ClassLoader)`");
-        continue;
+      if (directRewriteResult != null) {
+        populateDirectRewriteChanges(
+            code, replacements, replacementExtras, loadResult, directRewriteResult);
+      } else {
+        populateSyntheticChanges(
+            code,
+            methodProcessor,
+            methodProcessingContext,
+            synthesizedServiceLoaders,
+            replacements,
+            loadResult);
       }
-
-      String invalidUserMessage =
-          "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
-              + " java.lang.ServiceLoader.iterator()`";
-      Value serviceLoaderLoadOut = serviceLoaderLoad.outValue();
-      if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
-        report(code.context(), constClass.getType(), invalidUserMessage);
-        continue;
-      }
-
-      // Check that the only user is a call to iterator().
-      InvokeVirtual singleUniqueUser = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
-      if (singleUniqueUser == null
-          || singleUniqueUser.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
-        report(
-            code.context(), constClass.getType(), invalidUserMessage + ", but found other usages");
-        continue;
-      }
-
-      // Check that the service is not kept.
-      if (appView().appInfo().isPinnedWithDefinitionLookup(constClass.getValue())) {
-        report(code.context(), constClass.getType(), "The service loader type is kept");
-        continue;
-      }
-
-      // Check that the service is configured in the META-INF/services.
-      AppServices appServices = appView.appServices();
-      if (!appServices.allServiceTypes().contains(constClass.getValue())) {
-        // Error already reported in the Enqueuer.
-        continue;
-      }
-
-      // Check that we are not service loading anything from a feature into base.
-      if (appServices.hasServiceImplementationsInFeature(appView(), constClass.getValue())) {
-        report(
-            code.context(),
-            constClass.getType(),
-            "The service loader type has implementations in a feature split");
-        continue;
-      }
-
-      // Check that ClassLoader used is the ClassLoader defined for the service configuration
-      // that we are instantiating or NULL.
-      Value classLoaderValue = serviceLoaderLoad.getLastArgument().getAliasedValue();
-      if (classLoaderValue.isPhi()) {
-        report(
-            code.context(),
-            constClass.getType(),
-            "The java.lang.ClassLoader argument must be defined locally as null or "
-                + constClass.getType()
-                + ".class.getClassLoader()");
-        continue;
-      }
-      InvokeVirtual classLoaderInvoke = classLoaderValue.getDefinition().asInvokeVirtual();
-      boolean isGetClassLoaderOnConstClassOrNull =
-          classLoaderValue.getType().isNullType()
-              || (classLoaderInvoke != null
-                  && classLoaderInvoke.arguments().size() == 1
-                  && classLoaderInvoke.getReceiver().getAliasedValue().isConstClass()
-                  && classLoaderInvoke
-                      .getReceiver()
-                      .getAliasedValue()
-                      .getDefinition()
-                      .asConstClass()
-                      .getValue()
-                      .isIdenticalTo(constClass.getValue()));
-      if (!isGetClassLoaderOnConstClassOrNull) {
-        report(
-            code.context(),
-            constClass.getType(),
-            "The java.lang.ClassLoader argument must be defined locally as null or "
-                + constClass.getType()
-                + ".class.getClassLoader()");
-        continue;
-      }
-
-      List<DexType> dexTypes = appServices.serviceImplementationsFor(constClass.getValue());
-      List<DexClass> classes = new ArrayList<>(dexTypes.size());
-      boolean seenNull = false;
-      for (DexType serviceImpl : dexTypes) {
-        DexClass serviceImplementation = appView.definitionFor(serviceImpl);
-        if (serviceImplementation == null) {
-          report(
-              code.context(),
-              constClass.getType(),
-              "Unable to find definition for service implementation " + serviceImpl.getTypeName());
-          seenNull = true;
-        }
-        classes.add(serviceImplementation);
-      }
-      if (seenNull) {
-        continue;
-      }
-
-      // We can perform the rewrite of the ServiceLoader.load call.
-      DexEncodedMethod synthesizedMethod =
-          synthesizedServiceLoaders.computeIfAbsent(
-              constClass.getValue(),
-              service -> {
-                DexEncodedMethod addedMethod =
-                    createSynthesizedMethod(
-                        service, classes, methodProcessor, methodProcessingContext);
-                if (appView.options().isGeneratingClassFiles()) {
-                  addedMethod.upgradeClassFileVersion(
-                      code.context().getDefinition().getClassFileVersion());
-                }
-                return addedMethod;
-              });
-
-      new Rewriter(code, instructionIterator, serviceLoaderLoad)
-          .perform(classLoaderInvoke, synthesizedMethod.getReference());
     }
+
+    if (replacements.isEmpty()) {
+      return CodeRewriterResult.NO_CHANGE;
+    }
+
+    AffectedValues affectedValues = new AffectedValues();
+    iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      Instruction replacement = replacements.get(instruction);
+      if (replacement == null) {
+        continue;
+      }
+      iterator.replaceCurrentInstruction(replacement, affectedValues);
+      List<Instruction> extras = replacementExtras.get(instruction);
+      if (extras != null) {
+        iterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(extras, options);
+        if (ListUtils.last(extras).isThrow()) {
+          iterator.removeRemainingInBlockIgnoreOutValue();
+        }
+      }
+    }
+
+    code.removeUnreachableBlocks(affectedValues, ConsumerUtils.emptyConsumer());
+    code.removeRedundantBlocks();
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     assert code.isConsistentSSA(appView);
-    return synthesizedServiceLoaders.isEmpty()
-        ? CodeRewriterResult.NO_CHANGE
-        : CodeRewriterResult.HAS_CHANGED;
+
+    return CodeRewriterResult.HAS_CHANGED;
+  }
+
+  private void populateSyntheticChanges(
+      IRCode code,
+      MethodProcessor methodProcessor,
+      MethodProcessingContext methodProcessingContext,
+      Map<DexType, DexEncodedMethod> synthesizedServiceLoaders,
+      Map<Instruction, Instruction> replacements,
+      ServiceLoaderLoadResult loadResult) {
+    DexEncodedMethod synthesizedMethod =
+        synthesizedServiceLoaders.computeIfAbsent(
+            loadResult.serviceType,
+            service -> {
+              DexEncodedMethod addedMethod =
+                  createSynthesizedMethod(
+                      service, loadResult.implClasses, methodProcessor, methodProcessingContext);
+              if (appView.options().isGeneratingClassFiles()) {
+                addedMethod.upgradeClassFileVersion(
+                    code.context().getDefinition().getClassFileVersion());
+              }
+              return addedMethod;
+            });
+    InvokeStatic synthesizedInvoke =
+        new InvokeStatic(
+            synthesizedMethod.getReference(),
+            loadResult.iteratorInvoke.outValue(),
+            Collections.emptyList());
+    replacements.put(loadResult.iteratorInvoke, synthesizedInvoke);
+  }
+
+  private void populateDirectRewriteChanges(
+      IRCode code,
+      Map<Instruction, Instruction> replacements,
+      Map<Instruction, List<Instruction>> replacementExtras,
+      ServiceLoaderLoadResult loadResult,
+      DirectRewriteResult directRewriteResult) {
+    // Remove ServiceLoader.iterator()
+    replacements.put(loadResult.iteratorInvoke, code.createConstNull());
+
+    // Iterator.hasNext() --> true / false
+    if (directRewriteResult.hasNextInstr != null) {
+      replacements.put(
+          directRewriteResult.hasNextInstr,
+          code.createBooleanConstant(!loadResult.implClasses.isEmpty()));
+    }
+    if (directRewriteResult.nextInstr != null) {
+      Position position = directRewriteResult.nextInstr.getPosition();
+      if (loadResult.implClasses.isEmpty()) {
+        // Iterator.next() -> null
+        replacements.put(directRewriteResult.nextInstr, code.createConstNull());
+
+        // throw new NoSuchElementException()
+        NewInstance newInstanceInstr =
+            new NewInstance(
+                dexItemFactory.noSuchElementExceptionType,
+                code.createValue(
+                    dexItemFactory.noSuchElementExceptionType.toNonNullTypeElement(appView)));
+        InvokeDirect initInstr =
+            new InvokeDirect(
+                dexItemFactory.noSuchElementExceptionInit,
+                null,
+                List.of(newInstanceInstr.outValue()));
+        Throw throwInstr = new Throw(newInstanceInstr.outValue());
+
+        newInstanceInstr.setPosition(position);
+        initInstr.setPosition(position);
+        throwInstr.setPosition(position);
+        replacementExtras.put(
+            directRewriteResult.nextInstr, List.of(newInstanceInstr, initInstr, throwInstr));
+      } else {
+        // Iterator.next() -> new ServiceImpl()
+        DexType clazz = loadResult.implClasses.get(0).getType();
+        NewInstance newInstance =
+            new NewInstance(
+                clazz,
+                code.createValue(
+                    clazz.toNonNullTypeElement(appView),
+                    directRewriteResult.nextInstr.getLocalInfo()));
+        replacements.put(directRewriteResult.nextInstr, newInstance);
+        InvokeDirect initInstr =
+            new InvokeDirect(
+                dexItemFactory.createInstanceInitializer(clazz),
+                null,
+                List.of(newInstance.outValue()));
+        initInstr.setPosition(position);
+        replacementExtras.put(directRewriteResult.nextInstr, List.of(initInstr));
+      }
+    }
+  }
+
+  private ServiceLoaderLoadResult analyzeServiceLoaderLoad(IRCode code, InvokeStatic invokeInstr) {
+    // Check that the first argument is a const class.
+    Value argument = invokeInstr.getFirstArgument().getAliasedValue();
+    if (!argument.isDefinedByInstructionSatisfying(Instruction::isConstClass)) {
+      report(code.context(), null, "The service loader type could not be determined");
+      return null;
+    }
+
+    ConstClass constClass = argument.getDefinition().asConstClass();
+    DexType serviceType = constClass.getType();
+    if (invokeInstr.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
+      report(
+          code.context(),
+          serviceType,
+          "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
+              + " java.lang.ClassLoader)`");
+      return null;
+    }
+
+    String invalidUserMessage =
+        "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
+            + " java.lang.ServiceLoader.iterator()`";
+    Value serviceLoaderLoadOut = invokeInstr.outValue();
+    if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
+      report(code.context(), serviceType, invalidUserMessage);
+      return null;
+    }
+
+    // Check that the only user is a call to iterator().
+    InvokeVirtual iteratorInvoke = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
+    if (iteratorInvoke == null
+        || iteratorInvoke.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
+      report(code.context(), serviceType, invalidUserMessage + ", but found other usages");
+      return null;
+    }
+
+    // Check that the service is not kept.
+    if (appView().appInfo().isPinnedWithDefinitionLookup(serviceType)) {
+      report(code.context(), serviceType, "The service loader type is kept");
+      return null;
+    }
+
+    // Check that ClassLoader used is the ClassLoader defined for the service configuration
+    // that we are instantiating or NULL.
+    Value classLoaderValue = invokeInstr.getLastArgument().getAliasedValue();
+    if (classLoaderValue.isPhi()) {
+      report(
+          code.context(),
+          serviceType,
+          "The java.lang.ClassLoader argument must be defined locally as null or "
+              + serviceType
+              + ".class.getClassLoader()");
+      return null;
+    }
+    boolean isNullClassLoader = classLoaderValue.getType().isNullType();
+    InvokeVirtual classLoaderInvoke = classLoaderValue.getDefinition().asInvokeVirtual();
+    boolean isGetClassLoaderOnConstClass =
+        classLoaderInvoke != null
+            && classLoaderInvoke.arguments().size() == 1
+            && classLoaderInvoke.getReceiver().getAliasedValue().isConstClass()
+            && classLoaderInvoke
+                .getReceiver()
+                .getAliasedValue()
+                .getDefinition()
+                .asConstClass()
+                .getType()
+                .isIdenticalTo(serviceType);
+    if (!isNullClassLoader && !isGetClassLoaderOnConstClass) {
+      report(
+          code.context(),
+          serviceType,
+          "The java.lang.ClassLoader argument must be defined locally as null or "
+              + serviceType
+              + ".class.getClassLoader()");
+      return null;
+    }
+
+    // Check that we are not service loading anything from a feature into base.
+    AppServices appServices = appView.appServices();
+    // Check that we are not service loading anything from a feature into base.
+    if (hasServiceImplementationInDifferentFeature(code, serviceType, isNullClassLoader)) {
+      return null;
+    }
+
+    List<DexType> dexTypes = appServices.serviceImplementationsFor(serviceType);
+    List<DexClass> implClasses = new ArrayList<>(dexTypes.size());
+    for (DexType serviceImpl : dexTypes) {
+      DexClass serviceImplementation = appView.definitionFor(serviceImpl);
+      if (serviceImplementation == null) {
+        report(
+            code.context(),
+            serviceType,
+            "Unable to find definition for service implementation " + serviceImpl.getTypeName());
+        return null;
+      }
+      if (appView.isSubtype(serviceImpl, serviceType).isFalse()) {
+        report(
+            code.context(),
+            serviceType,
+            "Implementation is not a subtype of the service: " + serviceImpl.getTypeName());
+        return null;
+      }
+      DexEncodedMethod method = serviceImplementation.getDefaultInitializer();
+      if (method == null) {
+        report(
+            code.context(),
+            serviceType,
+            "Implementation has no default constructor: " + serviceImpl.getTypeName());
+        return null;
+      }
+      if (!method.getAccessFlags().wasPublic()) {
+        // A non-public constructor causes a ServiceConfigurationError on APIs 24 & 25 (Nougat).
+        report(
+            code.context(),
+            serviceType,
+            "Implementation's default constructor is not public: " + serviceImpl.getTypeName());
+        return null;
+      }
+      implClasses.add(serviceImplementation);
+    }
+
+    return new ServiceLoaderLoadResult(
+        invokeInstr, classLoaderInvoke, serviceType, implClasses, iteratorInvoke);
+  }
+
+  /**
+   * Checks that:
+   *
+   * <pre>
+   *   * -assumenosideeffects for ServiceLoader.load() is set.
+   *   * ServiceLoader.iterator() is used only by a single .hasNext() and/or .next()
+   *   * .iterator(), .hasNext(), and .next() are all in the same try/catch.
+   *   * .hasNext() never comes after a call to .next()
+   *   * .hasNext() and .next() are not in a loop.
+   * </pre>
+   */
+  private DirectRewriteResult analyzeForDirectRewrite(ServiceLoaderLoadResult loadResult) {
+    // Require -assumenosideeffects class java.util.ServiceLoader { java.lang.Object load(...); }
+    // because this direct rewriting does not wrap exceptions in ServiceConfigurationError.
+    if (!hasAssumeNoSideEffects) {
+      return null;
+    }
+    InvokeVirtual iteratorInvoke = loadResult.iteratorInvoke;
+
+    if (iteratorInvoke.outValue().hasPhiUsers()) {
+      return null;
+    }
+    InvokeMethod hasNextInstr = null;
+    InvokeMethod nextInstr = null;
+    // We only bother to support a single call to hasNext() and next(), and they must appear within
+    // the same try/catch.
+    for (Instruction user : iteratorInvoke.outValue().aliasedUsers()) {
+      if (user.isAssume()) {
+        if (user.outValue().hasPhiUsers()) {
+          return null;
+        }
+        continue;
+      }
+      if (!user.getBlock().hasEquivalentCatchHandlers(iteratorInvoke.getBlock())) {
+        return null;
+      }
+      InvokeMethod curCall = user.asInvokeMethod();
+      if (curCall == null) {
+        return null;
+      }
+      if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.hasNext)) {
+        if (hasNextInstr != null) {
+          return null;
+        }
+        hasNextInstr = curCall;
+      } else if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.next)) {
+        if (nextInstr != null) {
+          return null;
+        }
+        nextInstr = curCall;
+      } else {
+        return null;
+      }
+    }
+
+    BasicBlock iteratorBlock = iteratorInvoke.getBlock();
+    BasicBlock hasNextBlock = hasNextInstr != null ? hasNextInstr.getBlock() : null;
+    BasicBlock nextBlock = nextInstr != null ? nextInstr.getBlock() : null;
+    if (hasNextBlock != null && nextBlock != null) {
+      // See if hasNext() is reachable after next().
+      if (hasNextBlock == nextBlock) {
+        if (nextBlock.iterator(nextInstr).nextUntil(hasNextInstr) != null) {
+          return null;
+        }
+      } else if (hasPredecessorPathTo(iteratorBlock, hasNextBlock, nextBlock)) {
+        return null;
+      }
+    }
+
+    // Make sure each instruction can be run at most once (no loops).
+    if (hasNextBlock != null && loopExists(iteratorBlock, hasNextBlock)) {
+      return null;
+    }
+    if (nextBlock != null && loopExists(iteratorBlock, nextBlock)) {
+      return null;
+    }
+
+    return new DirectRewriteResult(hasNextInstr, nextInstr);
+  }
+
+  private static boolean loopExists(BasicBlock subgraphEntryBlock, BasicBlock targetBlock) {
+    return hasPredecessorPathTo(subgraphEntryBlock, targetBlock, targetBlock);
+  }
+
+  private static boolean hasPredecessorPathTo(
+      BasicBlock subgraphEntryBlock, BasicBlock subgraphExitBlock, BasicBlock targetBlock) {
+    if (subgraphEntryBlock == subgraphExitBlock) {
+      return false;
+    }
+    WorkList<BasicBlock> workList = WorkList.newIdentityWorkList();
+    workList.markAsSeen(subgraphEntryBlock);
+    workList.addIfNotSeen(subgraphExitBlock.getPredecessors());
+    while (workList.hasNext()) {
+      BasicBlock curBlock = workList.next();
+      if (curBlock == targetBlock) {
+        return true;
+      }
+      workList.addIfNotSeen(curBlock.getPredecessors());
+    }
+    return false;
   }
 
   private void report(ProgramMethod method, DexType serviceLoaderType, String message) {
@@ -268,6 +524,64 @@
     }
   }
 
+  private boolean hasServiceImplementationInDifferentFeature(
+      IRCode code, DexType serviceType, boolean baseFeatureOnly) {
+    AppView<AppInfoWithLiveness> appViewWithClasses = appView();
+    ClassToFeatureSplitMap classToFeatureSplitMap =
+        appViewWithClasses.appInfo().getClassToFeatureSplitMap();
+    if (classToFeatureSplitMap.isEmpty()) {
+      return false;
+    }
+    Map<FeatureSplit, List<DexType>> featureImplementations =
+        appView.appServices().serviceImplementationsByFeatureFor(serviceType);
+    if (featureImplementations == null || featureImplementations.isEmpty()) {
+      return false;
+    }
+    DexProgramClass serviceClass = appView.definitionForProgramType(serviceType);
+    if (serviceClass == null) {
+      return false;
+    }
+    FeatureSplit serviceFeature =
+        classToFeatureSplitMap.getFeatureSplit(serviceClass, appViewWithClasses);
+    if (baseFeatureOnly && !serviceFeature.isBase()) {
+      report(
+          code.context(),
+          serviceType,
+          "ClassLoader arg was null and service interface is in non-base feature");
+      return true;
+    }
+    for (var entry : featureImplementations.entrySet()) {
+      FeatureSplit metaInfFeature = entry.getKey();
+      if (!metaInfFeature.isBase()) {
+        if (baseFeatureOnly) {
+          report(
+              code.context(),
+              serviceType,
+              "ClassLoader arg was null and META-INF/ service entry found in non-base feature");
+          return true;
+        }
+        if (metaInfFeature != serviceFeature) {
+          report(
+              code.context(),
+              serviceType,
+              "META-INF/ service found in different feature from service interface");
+          return true;
+        }
+      }
+      for (DexType impl : entry.getValue()) {
+        FeatureSplit implFeature = classToFeatureSplitMap.getFeatureSplit(impl, appViewWithClasses);
+        if (implFeature != serviceFeature) {
+          report(
+              code.context(),
+              serviceType,
+              "Implementation found in different feature from service interface: " + impl);
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   private DexEncodedMethod createSynthesizedMethod(
       DexType serviceType,
       List<DexClass> classes,
@@ -300,77 +614,34 @@
     return method.getDefinition();
   }
 
-  /**
-   * Rewriter assumes that the code is of the form:
-   *
-   * <pre>
-   * ConstClass         v1 <- X
-   * ConstClass         v2 <- X or NULL
-   * Invoke-Virtual     v3 <- v2; method: java.lang.ClassLoader java.lang.Class.getClassLoader()
-   * Invoke-Static      v4 <- v1, v3; method: java.util.ServiceLoader java.util.ServiceLoader
-   *     .load(java.lang.Class, java.lang.ClassLoader)
-   * Invoke-Virtual     v5 <- v4; method: java.util.Iterator java.util.ServiceLoader.iterator()
-   * </pre>
-   *
-   * and rewrites it to:
-   *
-   * <pre>
-   * Invoke-Static      v5 <- ; method: java.util.Iterator syn(X)()
-   * </pre>
-   *
-   * where syn(X) is the synthesized method generated for the service class.
-   *
-   * <p>We rely on the DeadCodeRemover to remove the ConstClasses and any aliased values no longer
-   * used.
-   */
-  private static class Rewriter {
+  private static class ServiceLoaderLoadResult {
+    public final InvokeStatic loadInvoke;
+    public final InvokeVirtual classLoaderInvoke;
+    public final DexType serviceType;
+    public final List<DexClass> implClasses;
+    public final InvokeVirtual iteratorInvoke;
 
-    private final IRCode code;
-    private final InvokeStatic serviceLoaderLoad;
-
-    private final InstructionListIterator iterator;
-
-    Rewriter(IRCode code, InstructionListIterator iterator, InvokeStatic serviceLoaderLoad) {
-      this.iterator = iterator;
-      this.code = code;
-      this.serviceLoaderLoad = serviceLoaderLoad;
-    }
-
-    public void perform(InvokeVirtual classLoaderInvoke, DexMethod method) {
-      // Remove the ClassLoader call since this can throw and will not be removed otherwise.
-      if (classLoaderInvoke != null) {
-        BooleanBox allClassLoaderUsersAreServiceLoaders =
-            new BooleanBox(!classLoaderInvoke.outValue().hasPhiUsers());
-        classLoaderInvoke
-            .outValue()
-            .aliasedUsers()
-            .forEach(user -> allClassLoaderUsersAreServiceLoaders.and(user == serviceLoaderLoad));
-        if (allClassLoaderUsersAreServiceLoaders.get()) {
-          clearGetClassLoader(classLoaderInvoke);
-          iterator.nextUntil(i -> i == serviceLoaderLoad);
-        }
+    public ServiceLoaderLoadResult(
+        InvokeStatic loadInvoke,
+        InvokeVirtual classLoaderInvoke,
+        DexType serviceType,
+        List<DexClass> implClasses,
+        InvokeVirtual iteratorInvoke) {
+      this.loadInvoke = loadInvoke;
+      this.classLoaderInvoke = classLoaderInvoke;
+      this.serviceType = serviceType;
+      this.implClasses = implClasses;
+      this.iteratorInvoke = iteratorInvoke;
       }
+  }
 
-      // Remove the ServiceLoader.load call.
-      InvokeVirtual serviceLoaderIterator =
-          serviceLoaderLoad.outValue().singleUniqueUser().asInvokeVirtual();
-      iterator.replaceCurrentInstruction(code.createConstNull());
+  private static class DirectRewriteResult {
+    public final InvokeMethod nextInstr;
+    public final InvokeMethod hasNextInstr;
 
-      // Find the iterator instruction and replace it.
-      iterator.nextUntil(x -> x == serviceLoaderIterator);
-      InvokeStatic synthesizedInvoke =
-          new InvokeStatic(method, serviceLoaderIterator.outValue(), ImmutableList.of());
-      iterator.replaceCurrentInstruction(synthesizedInvoke);
-    }
-
-    private void clearGetClassLoader(InvokeVirtual classLoaderInvoke) {
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
-        if (instruction == classLoaderInvoke) {
-          iterator.replaceCurrentInstruction(code.createConstNull());
-          break;
-        }
-      }
+    public DirectRewriteResult(InvokeMethod hasNextInstr, InvokeMethod nextInstr) {
+      this.hasNextInstr = hasNextInstr;
+      this.nextInstr = nextInstr;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index b26ed45..7a20f638 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -485,7 +485,7 @@
     //    MyEnum a = Enum.valueOf(MyEnum.class, "A");
     // - as a receiver for a name method, to allow unboxing of:
     //    MyEnum.class.getName();
-    DexType enumType = constClass.getValue();
+    DexType enumType = constClass.getType();
     if (!enumUnboxingCandidatesInfo.isCandidate(enumType)) {
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index aabff86..bbfd363 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -614,8 +614,7 @@
         if (!invoke.getFirstArgument().isConstClass()) {
           return;
         }
-        DexType enumType =
-            invoke.getFirstArgument().getConstInstruction().asConstClass().getValue();
+        DexType enumType = invoke.getFirstArgument().getConstInstruction().asConstClass().getType();
         if (!unboxedEnumsData.isUnboxedEnum(enumType)) {
           return;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index c2b8988..fbf3767 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -247,8 +247,6 @@
       IRCode code,
       OptimizationFeedback feedback,
       InstanceFieldInitializationInfoCollection instanceFieldInitializationInfos) {
-    assert !appView.appInfo().isPinned(method);
-
     if (!method.isInstanceInitializer()) {
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java
index 7cd5d83..291d46c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodResolutionOptimizationInfoAnalysis.java
@@ -169,9 +169,9 @@
       } else {
         for (DexEncodedMethod method : clazz.virtualMethods()) {
           KeepMethodInfo keepInfo = appViewWithLiveness.getKeepInfo().getMethodInfo(method, clazz);
-          if (!keepInfo.isShrinkingAllowed(appViewWithLiveness.options())) {
-            // Method is kept and could be overridden outside app (e.g., in tests). Verify we don't
-            // have any optimization info recorded for non-abstract methods.
+          if (keepInfo.isCodeReplacementAllowed(appViewWithLiveness.options())) {
+            // Method can be replaced. Verify we don't have any optimization info recorded for
+            // non-abstract methods.
             assert method.isAbstract()
                 || method.getOptimizationInfo().isDefault()
                 || method.getOptimizationInfo().returnValueHasBeenPropagated();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InlinerUtils.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InlinerUtils.java
index a29b465..970ecac 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InlinerUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InlinerUtils.java
@@ -33,7 +33,7 @@
     if (monitorEnterValue.isPhi() || !monitorEnterValue.definition.isConstClass()) {
       nonConstantMonitorEnterValues.add(monitorEnterValue);
     } else {
-      constantMonitorEnterValues.add(monitorEnterValue.definition.asConstClass().getValue());
+      constantMonitorEnterValues.add(monitorEnterValue.definition.asConstClass().getType());
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
index f5cc0a5..6af000b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
@@ -66,7 +66,7 @@
     if (invoke.getBlock().hasCatchHandlers()) {
       return;
     }
-    DexType enumType = invoke.inValues().get(0).getConstInstruction().asConstClass().getValue();
+    DexType enumType = invoke.inValues().get(0).getConstInstruction().asConstClass().getType();
     DexProgramClass enumClass = appView.definitionForProgramType(enumType);
     if (enumClass == null
         || !enumClass.isEnum()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
index 2e2c0a1..7bda442 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
@@ -119,10 +119,10 @@
       return;
     }
 
-    // Verify that the methods are not pinned. They shouldn't be, since we've computed an abstract
-    // return value for both.
-    assert !appView.appInfo().isPinned(method);
-    assert !appView.appInfo().isPinned(parentMethod);
+    // Verify that the methods are not replaceable. They shouldn't be, since we've computed an
+    // abstract return value for both.
+    assert !appView.getKeepInfo(method).isCodeReplacementAllowed(appView.options());
+    assert !appView.getKeepInfo(parentMethod).isCodeReplacementAllowed(appView.options());
 
     if (appView
         .appInfo()
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
index 9074bdb..f312d7d 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
@@ -245,7 +245,7 @@
       if (!classValue.isConstClass()) {
         return null;
       }
-      DexType holderType = classValue.getConstInstruction().asConstClass().getValue();
+      DexType holderType = classValue.getConstInstruction().asConstClass().getType();
       if (holderType.isArrayType()) {
         // None of the fields or methods of an array type will be renamed, since they are all
         // declared in the library. Hence there is no need to handle this case.
@@ -262,7 +262,7 @@
         if (!fieldTypeValue.isConstClass()) {
           return null;
         }
-        DexType fieldType = fieldTypeValue.getConstInstruction().asConstClass().getValue();
+        DexType fieldType = fieldTypeValue.getConstInstruction().asConstClass().getType();
         return IdentifierNameStringLookupResult.fromUncategorized(
             inferFieldInHolder(holder, dexString.toString(), fieldType));
       }
@@ -419,7 +419,7 @@
       return null;
     }
     if (value.isConstant() && value.getConstInstruction().isConstClass()) {
-      return value.getConstInstruction().asConstClass().getValue();
+      return value.getConstInstruction().asConstClass().getType();
     }
     if (value.definition.isStaticGet()) {
       return factory.primitiveTypesBoxedTypeFields.boxedFieldTypeToPrimitiveType(
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 4f374e6..8b19ac3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -211,644 +211,610 @@
       IRCode code,
       AbstractValueSupplier abstractValueSupplier,
       Timing timing) {
-    timing.begin("Argument propagation scanner");
-    for (Instruction instruction : code.instructions()) {
-      if (instruction.isFieldPut()) {
-        scan(instruction.asFieldPut(), abstractValueSupplier, method, timing);
-      } else if (instruction.isInvokeMethod()) {
-        scan(instruction.asInvokeMethod(), abstractValueSupplier, method, timing);
-      } else if (instruction.isInvokeCustom()) {
-        scan(instruction.asInvokeCustom());
-      }
-    }
-    timing.end();
+    new CodeScanner(abstractValueSupplier, code, method).scan(timing);
   }
 
-  private void scan(
-      FieldPut fieldPut,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      Timing timing) {
-    ProgramField field = fieldPut.resolveField(appView, context).getProgramField();
-    if (field == null) {
-      // Nothing to propagate.
-      return;
-    }
-    addTemporaryFieldState(fieldPut, field, abstractValueSupplier, context, timing);
-  }
+  protected class CodeScanner {
 
-  private void addTemporaryFieldState(
-      FieldPut fieldPut,
-      ProgramField field,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      Timing timing) {
-    timing.begin("Add field state");
-    fieldStates.addTemporaryFieldState(
-        field,
-        () -> computeFieldState(fieldPut, field, abstractValueSupplier, context, timing),
-        timing,
-        (existingFieldState, fieldStateToAdd) -> {
-          DexType inStaticType = null;
-          NonEmptyValueState newFieldState =
-              existingFieldState.mutableJoin(
-                  appView,
-                  fieldStateToAdd,
-                  inStaticType,
-                  field.getType(),
-                  StateCloner.getCloner(),
-                  Action.empty());
-          return narrowFieldState(field, newFieldState);
-        });
-    timing.end();
-  }
+    protected final AbstractValueSupplier abstractValueSupplier;
+    protected final IRCode code;
+    protected final ProgramMethod context;
 
-  private NonEmptyValueState computeFieldState(
-      FieldPut fieldPut,
-      ProgramField resolvedField,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      Timing timing) {
-    timing.begin("Compute field state for field-put");
-    NonEmptyValueState result =
-        computeFieldState(fieldPut, resolvedField, abstractValueSupplier, context);
-    timing.end();
-    return result;
-  }
-
-  private NonEmptyValueState computeFieldState(
-      FieldPut fieldPut,
-      ProgramField field,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context) {
-    TypeElement fieldType = field.getType().toTypeElement(appView);
-    if (!fieldPut.value().getType().lessThanOrEqual(fieldType, appView)) {
-      return ValueState.unknown();
+    protected CodeScanner(
+        AbstractValueSupplier abstractValueSupplier, IRCode code, ProgramMethod method) {
+      this.abstractValueSupplier = abstractValueSupplier;
+      this.code = code;
+      this.context = method;
     }
 
-    NonEmptyValueState inFlowState = computeInFlowState(field.getType(), fieldPut.value(), context);
-    if (inFlowState != null) {
-      return inFlowState;
-    }
-
-    if (field.getType().isArrayType()) {
-      Nullability nullability = fieldPut.value().getType().nullability();
-      return ConcreteArrayTypeValueState.create(nullability);
-    }
-
-    AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(fieldPut.value());
-    if (abstractValue.isUnknown()) {
-      abstractValue =
-          getFallbackAbstractValueForField(
-              field,
-              () -> ObjectStateAnalysis.computeObjectState(fieldPut.value(), appView, context));
-    }
-    if (field.getType().isClassType()) {
-      DynamicType dynamicType =
-          WideningUtils.widenDynamicNonReceiverType(
-              appView, fieldPut.value().getDynamicType(appView), field.getType());
-      return ConcreteClassTypeValueState.create(abstractValue, dynamicType);
-    } else {
-      assert field.getType().isPrimitiveType();
-      return ConcretePrimitiveTypeValueState.create(abstractValue);
-    }
-  }
-
-  // If the value is an argument of the enclosing method or defined by a field-get, then clearly we
-  // have no information about its abstract value (yet). Instead of treating this as having an
-  // unknown runtime value, we instead record a flow constraint.
-  private InFlow computeInFlow(DexType staticType, Value value, ProgramMethod context) {
-    Value valueRoot = value.getAliasedValue(aliasedValueConfiguration);
-    if (valueRoot.isArgument()) {
-      MethodParameter inParameter =
-          methodParameterFactory.create(context, valueRoot.getDefinition().asArgument().getIndex());
-      return castBaseInFlow(widenBaseInFlow(staticType, inParameter, context), value);
-    } else if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
-      FieldGet fieldGet = valueRoot.getDefinition().asFieldGet();
-      ProgramField field = fieldGet.resolveField(appView, context).getProgramField();
-      if (field == null) {
-        return null;
-      }
-      if (fieldGet.isInstanceGet()) {
-        Value receiverValue = fieldGet.asInstanceGet().object();
-        BaseInFlow receiverInFlow =
-            asBaseInFlowOrNull(computeInFlow(staticType, receiverValue, context));
-        if (receiverInFlow != null
-            && receiverInFlow.equals(widenBaseInFlow(staticType, receiverInFlow, context))) {
-          return new InstanceFieldReadAbstractFunction(receiverInFlow, field.getReference());
+    public void scan(Timing timing) {
+      timing.begin("Argument propagation scanner");
+      for (Instruction instruction : code.instructions()) {
+        if (instruction.isFieldPut()) {
+          scanFieldPut(instruction.asFieldPut(), timing);
+        } else if (instruction.isInvokeMethod()) {
+          scanInvoke(instruction.asInvokeMethod(), timing);
+        } else if (instruction.isInvokeCustom()) {
+          scanInvokeCustom(instruction.asInvokeCustom());
         }
       }
-      return castBaseInFlow(
-          widenBaseInFlow(staticType, fieldValueFactory.create(field), context), value);
+      timing.end();
     }
-    return null;
-  }
 
-  private InFlow castBaseInFlow(InFlow inFlow, Value value) {
-    if (inFlow.isUnknownAbstractFunction()) {
-      return inFlow;
-    }
-    assert inFlow.isBaseInFlow();
-    Value valueRoot = value.getAliasedValue();
-    if (!valueRoot.isDefinedByInstructionSatisfying(Instruction::isCheckCast)) {
-      return inFlow;
-    }
-    CheckCast checkCast = valueRoot.getDefinition().asCheckCast();
-    return new CastAbstractFunction(inFlow.asBaseInFlow(), checkCast.getType());
-  }
-
-  private InFlow widenBaseInFlow(DexType staticType, BaseInFlow inFlow, ProgramMethod context) {
-    if (inFlow.isFieldValue()) {
-      if (isFieldValueAlreadyUnknown(staticType, inFlow.asFieldValue().getField())) {
-        return AbstractFunction.unknown();
+    private void scanFieldPut(FieldPut fieldPut, Timing timing) {
+      ProgramField field = fieldPut.resolveField(appView, context).getProgramField();
+      if (field == null) {
+        // Nothing to propagate.
+        return;
       }
-    } else {
-      assert inFlow.isMethodParameter();
-      if (isMethodParameterAlreadyUnknown(staticType, inFlow.asMethodParameter(), context)) {
-        return AbstractFunction.unknown();
-      }
+      addTemporaryFieldState(fieldPut, field, timing);
     }
-    return inFlow;
-  }
 
-  private NonEmptyValueState computeInFlowState(
-      DexType staticType, Value value, ProgramMethod context) {
-    InFlow inFlow = computeInFlow(staticType, value, context);
-    if (inFlow == null) {
-      return null;
+    private void addTemporaryFieldState(FieldPut fieldPut, ProgramField field, Timing timing) {
+      timing.begin("Add field state");
+      fieldStates.addTemporaryFieldState(
+          field,
+          () -> computeFieldState(fieldPut, field, timing),
+          timing,
+          (existingFieldState, fieldStateToAdd) -> {
+            DexType inStaticType = null;
+            NonEmptyValueState newFieldState =
+                existingFieldState.mutableJoin(
+                    appView,
+                    fieldStateToAdd,
+                    inStaticType,
+                    field.getType(),
+                    StateCloner.getCloner(),
+                    Action.empty());
+            return narrowFieldState(field, newFieldState);
+          });
+      timing.end();
     }
-    if (inFlow.isUnknownAbstractFunction()) {
-      return ValueState.unknown();
-    }
-    assert inFlow.isBaseInFlow()
-        || inFlow.isCastAbstractFunction()
-        || inFlow.isInstanceFieldReadAbstractFunction();
-    return ConcreteValueState.create(staticType, inFlow);
-  }
 
-  // Strengthens the abstract value of static final fields to a (self-)SingleFieldValue when the
-  // abstract value is unknown. The soundness of this is based on the fact that static final fields
-  // will never have their value changed after the <clinit> finishes, so value in a static final
-  // field can always be rematerialized by reading the field.
-  private NonEmptyValueState narrowFieldState(ProgramField field, NonEmptyValueState fieldState) {
-    AbstractValue fallbackAbstractValue =
-        getFallbackAbstractValueForField(field, ObjectState::empty);
-    if (!fallbackAbstractValue.isUnknown()) {
-      AbstractValue abstractValue = fieldState.getAbstractValue(appView);
-      if (!abstractValue.isUnknown()) {
-        return fieldState;
+    private NonEmptyValueState computeFieldState(
+        FieldPut fieldPut, ProgramField resolvedField, Timing timing) {
+      timing.begin("Compute field state for field-put");
+      NonEmptyValueState result = computeFieldState(fieldPut, resolvedField);
+      timing.end();
+      return result;
+    }
+
+    private NonEmptyValueState computeFieldState(FieldPut fieldPut, ProgramField field) {
+      TypeElement fieldType = field.getType().toTypeElement(appView);
+      if (!fieldPut.value().getType().lessThanOrEqual(fieldType, appView)) {
+        return ValueState.unknown();
       }
+
+      NonEmptyValueState inFlowState =
+          computeInFlowState(field.getType(), fieldPut.value(), context);
+      if (inFlowState != null) {
+        return inFlowState;
+      }
+
       if (field.getType().isArrayType()) {
-        // We do not track an abstract value for array types.
-        return fieldState;
+        Nullability nullability = fieldPut.value().getType().nullability();
+        return ConcreteArrayTypeValueState.create(nullability);
+      }
+
+      AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(fieldPut.value());
+      if (abstractValue.isUnknown()) {
+        abstractValue =
+            getFallbackAbstractValueForField(
+                field,
+                () -> ObjectStateAnalysis.computeObjectState(fieldPut.value(), appView, context));
       }
       if (field.getType().isClassType()) {
         DynamicType dynamicType =
-            fieldState.isReferenceState()
-                ? fieldState.asReferenceState().getDynamicType()
-                : DynamicType.unknown();
-        return new ConcreteClassTypeValueState(fallbackAbstractValue, dynamicType);
+            WideningUtils.widenDynamicNonReceiverType(
+                appView, fieldPut.value().getDynamicType(appView), field.getType());
+        return ConcreteClassTypeValueState.create(abstractValue, dynamicType);
       } else {
         assert field.getType().isPrimitiveType();
-        return new ConcretePrimitiveTypeValueState(fallbackAbstractValue);
+        return ConcretePrimitiveTypeValueState.create(abstractValue);
       }
     }
-    return fieldState;
-  }
 
-  private AbstractValue getFallbackAbstractValueForField(
-      ProgramField field, Supplier<ObjectState> objectStateSupplier) {
-    if (field.isFinalOrEffectivelyFinal(appView) && field.getAccessFlags().isStatic()) {
-      return appView
-          .abstractValueFactory()
-          .createSingleFieldValue(field.getReference(), objectStateSupplier.get());
-    }
-    return AbstractValue.unknown();
-  }
-
-  private void scan(
-      InvokeMethod invoke,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      Timing timing) {
-    DexMethod invokedMethod = invoke.getInvokedMethod();
-    if (invokedMethod.getHolderType().isArrayType()) {
-      // Nothing to propagate; the targeted method is not a program method.
-      return;
+    // If the value is an argument of the enclosing method or defined by a field-get, then clearly
+    // we have no information about its abstract value (yet). Instead of treating this as having an
+    // unknown runtime value, we instead record a flow constraint.
+    private InFlow computeInFlow(DexType staticType, Value value, ProgramMethod context) {
+      Value valueRoot = value.getAliasedValue(aliasedValueConfiguration);
+      if (valueRoot.isArgument()) {
+        MethodParameter inParameter =
+            methodParameterFactory.create(
+                context, valueRoot.getDefinition().asArgument().getIndex());
+        return castBaseInFlow(widenBaseInFlow(staticType, inParameter, context), value);
+      } else if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
+        FieldGet fieldGet = valueRoot.getDefinition().asFieldGet();
+        ProgramField field = fieldGet.resolveField(appView, context).getProgramField();
+        if (field == null) {
+          return null;
+        }
+        if (fieldGet.isInstanceGet()) {
+          Value receiverValue = fieldGet.asInstanceGet().object();
+          BaseInFlow receiverInFlow =
+              asBaseInFlowOrNull(computeInFlow(staticType, receiverValue, context));
+          if (receiverInFlow != null
+              && receiverInFlow.equals(widenBaseInFlow(staticType, receiverInFlow, context))) {
+            return new InstanceFieldReadAbstractFunction(receiverInFlow, field.getReference());
+          }
+        }
+        return castBaseInFlow(
+            widenBaseInFlow(staticType, fieldValueFactory.create(field), context), value);
+      }
+      return null;
     }
 
-    if (appView.options().testing.checkReceiverAlwaysNullInCallSiteOptimization
-        && invoke.isInvokeMethodWithReceiver()
-        && invoke.asInvokeMethodWithReceiver().getReceiver().isAlwaysNull(appView)) {
-      // Nothing to propagate; the invoke instruction always fails.
-      return;
+    private InFlow castBaseInFlow(InFlow inFlow, Value value) {
+      if (inFlow.isUnknownAbstractFunction()) {
+        return inFlow;
+      }
+      assert inFlow.isBaseInFlow();
+      Value valueRoot = value.getAliasedValue();
+      if (!valueRoot.isDefinedByInstructionSatisfying(Instruction::isCheckCast)) {
+        return inFlow;
+      }
+      CheckCast checkCast = valueRoot.getDefinition().asCheckCast();
+      return new CastAbstractFunction(inFlow.asBaseInFlow(), checkCast.getType());
     }
 
-    SingleResolutionResult<?> resolutionResult =
-        invoke.resolveMethod(appView, context).asSingleResolution();
-    if (resolutionResult == null) {
-      // Nothing to propagate; the invoke instruction fails.
-      return;
+    private InFlow widenBaseInFlow(DexType staticType, BaseInFlow inFlow, ProgramMethod context) {
+      if (inFlow.isFieldValue()) {
+        if (isFieldValueAlreadyUnknown(staticType, inFlow.asFieldValue().getField())) {
+          return AbstractFunction.unknown();
+        }
+      } else {
+        assert inFlow.isMethodParameter();
+        if (isMethodParameterAlreadyUnknown(staticType, inFlow.asMethodParameter(), context)) {
+          return AbstractFunction.unknown();
+        }
+      }
+      return inFlow;
     }
 
-    if (!resolutionResult.getResolvedHolder().isProgramClass()) {
-      // Nothing to propagate; this could dispatch to a program method, but we cannot optimize
-      // methods that override non-program methods.
-      return;
+    private NonEmptyValueState computeInFlowState(
+        DexType staticType, Value value, ProgramMethod context) {
+      InFlow inFlow = computeInFlow(staticType, value, context);
+      if (inFlow == null) {
+        return null;
+      }
+      if (inFlow.isUnknownAbstractFunction()) {
+        return ValueState.unknown();
+      }
+      assert inFlow.isBaseInFlow()
+          || inFlow.isCastAbstractFunction()
+          || inFlow.isInstanceFieldReadAbstractFunction();
+      return ConcreteValueState.create(staticType, inFlow);
     }
 
-    ProgramMethod resolvedMethod = resolutionResult.getResolvedProgramMethod();
-    if (resolvedMethod.getDefinition().isLibraryMethodOverride().isPossiblyTrue()) {
-      assert resolvedMethod.getDefinition().isLibraryMethodOverride().isTrue();
-      // Nothing to propagate; we don't know anything about methods that can be called from outside
-      // the program.
-      return;
+    // Strengthens the abstract value of static final fields to a (self-)SingleFieldValue when the
+    // abstract value is unknown. The soundness of this is based on the fact that static final
+    // fields will never have their value changed after the <clinit> finishes, so value in a static
+    // final field can always be rematerialized by reading the field.
+    private NonEmptyValueState narrowFieldState(ProgramField field, NonEmptyValueState fieldState) {
+      AbstractValue fallbackAbstractValue =
+          getFallbackAbstractValueForField(field, ObjectState::empty);
+      if (!fallbackAbstractValue.isUnknown()) {
+        AbstractValue abstractValue = fieldState.getAbstractValue(appView);
+        if (!abstractValue.isUnknown()) {
+          return fieldState;
+        }
+        if (field.getType().isArrayType()) {
+          // We do not track an abstract value for array types.
+          return fieldState;
+        }
+        if (field.getType().isClassType()) {
+          DynamicType dynamicType =
+              fieldState.isReferenceState()
+                  ? fieldState.asReferenceState().getDynamicType()
+                  : DynamicType.unknown();
+          return new ConcreteClassTypeValueState(fallbackAbstractValue, dynamicType);
+        } else {
+          assert field.getType().isPrimitiveType();
+          return new ConcretePrimitiveTypeValueState(fallbackAbstractValue);
+        }
+      }
+      return fieldState;
     }
 
-    if (invoke.arguments().size() != resolvedMethod.getDefinition().getNumberOfArguments()
-        || invoke.isInvokeStatic() != resolvedMethod.getAccessFlags().isStatic()) {
-      // Nothing to propagate; the invoke instruction fails.
-      return;
+    private AbstractValue getFallbackAbstractValueForField(
+        ProgramField field, Supplier<ObjectState> objectStateSupplier) {
+      if (field.isFinalOrEffectivelyFinal(appView) && field.getAccessFlags().isStatic()) {
+        return appView
+            .abstractValueFactory()
+            .createSingleFieldValue(field.getReference(), objectStateSupplier.get());
+      }
+      return AbstractValue.unknown();
     }
 
-    if (invoke.isInvokeInterface()) {
-      if (!resolutionResult.getInitialResolutionHolder().isInterface()) {
+    private void scanInvoke(InvokeMethod invoke, Timing timing) {
+      DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (invokedMethod.getHolderType().isArrayType()) {
+        // Nothing to propagate; the targeted method is not a program method.
+        return;
+      }
+
+      if (appView.options().testing.checkReceiverAlwaysNullInCallSiteOptimization
+          && invoke.isInvokeMethodWithReceiver()
+          && invoke.asInvokeMethodWithReceiver().getReceiver().isAlwaysNull(appView)) {
+        // Nothing to propagate; the invoke instruction always fails.
+        return;
+      }
+
+      SingleResolutionResult<?> resolutionResult =
+          invoke.resolveMethod(appView, context).asSingleResolution();
+      if (resolutionResult == null) {
         // Nothing to propagate; the invoke instruction fails.
         return;
       }
-    }
 
-    if (invoke.isInvokeSuper()) {
-      // Use the super target instead of the resolved method to ensure that we propagate the
-      // argument information to the targeted method.
-      DexClassAndMethod target =
-          resolutionResult.lookupInvokeSuperTarget(context.getHolder(), appView);
-      if (target == null) {
+      if (!resolutionResult.getResolvedHolder().isProgramClass()) {
+        // Nothing to propagate; this could dispatch to a program method, but we cannot optimize
+        // methods that override non-program methods.
+        return;
+      }
+
+      ProgramMethod resolvedMethod = resolutionResult.getResolvedProgramMethod();
+      if (resolvedMethod.getDefinition().isLibraryMethodOverride().isPossiblyTrue()) {
+        assert resolvedMethod.getDefinition().isLibraryMethodOverride().isTrue();
+        // Nothing to propagate; we don't know anything about methods that can be called from
+        // outside the program.
+        return;
+      }
+
+      if (invoke.arguments().size() != resolvedMethod.getDefinition().getNumberOfArguments()
+          || invoke.isInvokeStatic() != resolvedMethod.getAccessFlags().isStatic()) {
         // Nothing to propagate; the invoke instruction fails.
         return;
       }
-      if (!target.isProgramMethod()) {
-        throw new Unreachable(
-            "Expected super target of a non-library override to be a program method ("
-                + "resolved program method: "
-                + resolvedMethod
-                + ", "
-                + "super non-program method: "
-                + target
-                + ")");
+
+      if (invoke.isInvokeInterface()) {
+        if (!resolutionResult.getInitialResolutionHolder().isInterface()) {
+          // Nothing to propagate; the invoke instruction fails.
+          return;
+        }
       }
-      resolvedMethod = target.asProgramMethod();
+
+      if (invoke.isInvokeSuper()) {
+        // Use the super target instead of the resolved method to ensure that we propagate the
+        // argument information to the targeted method.
+        DexClassAndMethod target =
+            resolutionResult.lookupInvokeSuperTarget(context.getHolder(), appView);
+        if (target == null) {
+          // Nothing to propagate; the invoke instruction fails.
+          return;
+        }
+        if (!target.isProgramMethod()) {
+          throw new Unreachable(
+              "Expected super target of a non-library override to be a program method ("
+                  + "resolved program method: "
+                  + resolvedMethod
+                  + ", "
+                  + "super non-program method: "
+                  + target
+                  + ")");
+        }
+        resolvedMethod = target.asProgramMethod();
+      }
+
+      // Find the method where to store the information about the arguments from this invoke.
+      // If the invoke may dispatch to more than one method, we intentionally do not compute all
+      // possible dispatch targets and propagate the information to these methods (this is
+      // expensive). Instead we record the information in one place and then later propagate the
+      // information to all dispatch targets.
+      addTemporaryMethodState(invoke, resolvedMethod, timing);
     }
 
-    // Find the method where to store the information about the arguments from this invoke.
-    // If the invoke may dispatch to more than one method, we intentionally do not compute all
-    // possible dispatch targets and propagate the information to these methods (this is expensive).
-    // Instead we record the information in one place and then later propagate the information to
-    // all dispatch targets.
-    addTemporaryMethodState(invoke, resolvedMethod, abstractValueSupplier, context, timing);
-  }
+    protected void addTemporaryMethodState(
+        InvokeMethod invoke, ProgramMethod resolvedMethod, Timing timing) {
+      timing.begin("Add method state");
+      methodStates.addTemporaryMethodState(
+          appView,
+          getRepresentative(invoke, resolvedMethod),
+          existingMethodState ->
+              computeMethodState(invoke, resolvedMethod, existingMethodState, timing),
+          timing);
+      timing.end();
+    }
 
-  protected void addTemporaryMethodState(
-      InvokeMethod invoke,
-      ProgramMethod resolvedMethod,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      Timing timing) {
-    timing.begin("Add method state");
-    methodStates.addTemporaryMethodState(
-        appView,
-        getRepresentative(invoke, resolvedMethod),
-        existingMethodState ->
-            computeMethodState(
+    private MethodState computeMethodState(
+        InvokeMethod invoke,
+        ProgramMethod resolvedMethod,
+        MethodState existingMethodState,
+        Timing timing) {
+      assert !existingMethodState.isUnknown();
+
+      // If this invoke may target at most one method, then we compute a state that maps each
+      // parameter to the abstract value and dynamic type provided by this call site. Otherwise, we
+      // compute a polymorphic method state, which includes information about the receiver's dynamic
+      // type bounds.
+      timing.begin("Compute method state for invoke");
+      MethodState result;
+      if (shouldUsePolymorphicMethodState(invoke, resolvedMethod)) {
+        assert existingMethodState.isBottom() || existingMethodState.isPolymorphic();
+        result =
+            computePolymorphicMethodState(
+                invoke.asInvokeMethodWithReceiver(),
+                resolvedMethod,
+                existingMethodState.asPolymorphicOrBottom());
+      } else {
+        assert existingMethodState.isBottom() || existingMethodState.isMonomorphic();
+        result =
+            computeMonomorphicMethodState(
                 invoke,
                 resolvedMethod,
-                abstractValueSupplier,
-                context,
-                existingMethodState,
-                timing),
-        timing);
-    timing.end();
-  }
+                invoke.lookupSingleProgramTarget(appView, context),
+                existingMethodState.asMonomorphicOrBottom());
+      }
+      timing.end();
+      return result;
+    }
 
-  private MethodState computeMethodState(
-      InvokeMethod invoke,
-      ProgramMethod resolvedMethod,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      MethodState existingMethodState,
-      Timing timing) {
-    assert !existingMethodState.isUnknown();
+    // TODO(b/190154391): Add a strategy that widens the dynamic receiver type to allow easily
+    //  experimenting with the performance/size trade-off between precise/imprecise handling of
+    //  dynamic dispatch.
+    private MethodState computePolymorphicMethodState(
+        InvokeMethodWithReceiver invoke,
+        ProgramMethod resolvedMethod,
+        ConcretePolymorphicMethodStateOrBottom existingMethodState) {
+      DynamicTypeWithUpperBound dynamicReceiverType = invoke.getReceiver().getDynamicType(appView);
+      // TODO(b/331587404): Investigate if we can replace the receiver by null before entering this
+      //  pass, so that this special case is not needed.
+      if (dynamicReceiverType.isNullType()) {
+        assert appView.testing().allowNullDynamicTypeInCodeScanner : "b/250634405";
+        // This can happen if we were unable to determine that the receiver is a phi value where
+        // null information has not been propagated down. Ideally this case would never happen as it
+        // should be possible to replace the receiver by the null constant in this case. Since the
+        // receiver is known to be null, no argument information should be propagated to the
+        // callees, so we return bottom here.
+        return MethodState.bottom();
+      }
 
-    // If this invoke may target at most one method, then we compute a state that maps each
-    // parameter to the abstract value and dynamic type provided by this call site. Otherwise, we
-    // compute a polymorphic method state, which includes information about the receiver's dynamic
-    // type bounds.
-    timing.begin("Compute method state for invoke");
-    MethodState result;
-    if (shouldUsePolymorphicMethodState(invoke, resolvedMethod)) {
-      assert existingMethodState.isBottom() || existingMethodState.isPolymorphic();
-      result =
-          computePolymorphicMethodState(
-              invoke.asInvokeMethodWithReceiver(),
-              resolvedMethod,
-              abstractValueSupplier,
-              context,
-              existingMethodState.asPolymorphicOrBottom());
-    } else {
-      assert existingMethodState.isBottom() || existingMethodState.isMonomorphic();
-      result =
+      ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
+      DynamicTypeWithUpperBound bounds =
+          computeBoundsForPolymorphicMethodState(resolvedMethod, singleTarget, dynamicReceiverType);
+      MethodState existingMethodStateForBounds =
+          existingMethodState.isPolymorphic()
+              ? existingMethodState.asPolymorphic().getMethodStateForBounds(bounds)
+              : MethodState.bottom();
+
+      if (existingMethodStateForBounds.isPolymorphic()) {
+        assert false;
+        return MethodState.unknown();
+      }
+
+      // If we already don't know anything about the parameters for the given type bounds, then
+      // don't compute a method state.
+      if (existingMethodStateForBounds.isUnknown()) {
+        return MethodState.bottom();
+      }
+
+      ConcreteMonomorphicMethodStateOrUnknown methodStateForBounds =
           computeMonomorphicMethodState(
               invoke,
               resolvedMethod,
-              invoke.lookupSingleProgramTarget(appView, context),
-              abstractValueSupplier,
-              context,
-              existingMethodState.asMonomorphicOrBottom());
-    }
-    timing.end();
-    return result;
-  }
-
-  // TODO(b/190154391): Add a strategy that widens the dynamic receiver type to allow easily
-  //  experimenting with the performance/size trade-off between precise/imprecise handling of
-  //  dynamic dispatch.
-  private MethodState computePolymorphicMethodState(
-      InvokeMethodWithReceiver invoke,
-      ProgramMethod resolvedMethod,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      ConcretePolymorphicMethodStateOrBottom existingMethodState) {
-    DynamicTypeWithUpperBound dynamicReceiverType = invoke.getReceiver().getDynamicType(appView);
-    // TODO(b/331587404): Investigate if we can replace the receiver by null before entering this
-    //  pass, so that this special case is not needed.
-    if (dynamicReceiverType.isNullType()) {
-      assert appView.testing().allowNullDynamicTypeInCodeScanner : "b/250634405";
-      // This can happen if we were unable to determine that the receiver is a phi value where null
-      // information has not been propagated down. Ideally this case would never happen as it should
-      // be possible to replace the receiver by the null constant in this case.
-      //
-      // Since the receiver is known to be null, no argument information should be propagated to the
-      // callees, so we return bottom here.
-      return MethodState.bottom();
+              singleTarget,
+              existingMethodStateForBounds.asMonomorphicOrBottom(),
+              dynamicReceiverType);
+      return ConcretePolymorphicMethodState.create(bounds, methodStateForBounds);
     }
 
-    ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
-    DynamicTypeWithUpperBound bounds =
-        computeBoundsForPolymorphicMethodState(resolvedMethod, singleTarget, dynamicReceiverType);
-    MethodState existingMethodStateForBounds =
-        existingMethodState.isPolymorphic()
-            ? existingMethodState.asPolymorphic().getMethodStateForBounds(bounds)
-            : MethodState.bottom();
+    private DynamicTypeWithUpperBound computeBoundsForPolymorphicMethodState(
+        ProgramMethod resolvedMethod,
+        ProgramMethod singleTarget,
+        DynamicTypeWithUpperBound dynamicReceiverType) {
+      DynamicTypeWithUpperBound bounds =
+          singleTarget != null
+              ? DynamicType.createExact(
+                  singleTarget.getHolderType().toTypeElement(appView).asClassType())
+              : dynamicReceiverType.withNullability(Nullability.maybeNull());
 
-    if (existingMethodStateForBounds.isPolymorphic()) {
-      assert false;
-      return MethodState.unknown();
-    }
+      // We intentionally drop the nullability for the type bounds. This increases the number of
+      // collisions in the polymorphic method states, which does not change the precision (since the
+      // nullability does not have any impact on the possible dispatch targets) and is good for
+      // state pruning.
+      assert bounds.getDynamicUpperBoundType().nullability().isMaybeNull();
 
-    // If we already don't know anything about the parameters for the given type bounds, then don't
-    // compute a method state.
-    if (existingMethodStateForBounds.isUnknown()) {
-      return MethodState.bottom();
-    }
+      // If the bounds are trivial (i.e., the upper bound is equal to the holder of the virtual root
+      // method), then widen the type bounds to 'unknown'.
+      DexMethod virtualRootMethod = getVirtualRootMethod(resolvedMethod);
+      if (virtualRootMethod == null) {
+        assert false : "Unexpected virtual method without root: " + resolvedMethod;
+        return bounds;
+      }
 
-    ConcreteMonomorphicMethodStateOrUnknown methodStateForBounds =
-        computeMonomorphicMethodState(
-            invoke,
-            resolvedMethod,
-            singleTarget,
-            abstractValueSupplier,
-            context,
-            existingMethodStateForBounds.asMonomorphicOrBottom(),
-            dynamicReceiverType);
-    return ConcretePolymorphicMethodState.create(bounds, methodStateForBounds);
-  }
-
-  private DynamicTypeWithUpperBound computeBoundsForPolymorphicMethodState(
-      ProgramMethod resolvedMethod,
-      ProgramMethod singleTarget,
-      DynamicTypeWithUpperBound dynamicReceiverType) {
-    DynamicTypeWithUpperBound bounds =
-        singleTarget != null
-            ? DynamicType.createExact(
-                singleTarget.getHolderType().toTypeElement(appView).asClassType())
-            : dynamicReceiverType.withNullability(Nullability.maybeNull());
-
-    // We intentionally drop the nullability for the type bounds. This increases the number of
-    // collisions in the polymorphic method states, which does not change the precision (since the
-    // nullability does not have any impact on the possible dispatch targets) and is good for state
-    // pruning.
-    assert bounds.getDynamicUpperBoundType().nullability().isMaybeNull();
-
-    // If the bounds are trivial (i.e., the upper bound is equal to the holder of the virtual root
-    // method), then widen the type bounds to 'unknown'.
-    DexMethod virtualRootMethod = getVirtualRootMethod(resolvedMethod);
-    if (virtualRootMethod == null) {
-      assert false : "Unexpected virtual method without root: " + resolvedMethod;
+      DynamicType trivialBounds =
+          DynamicType.create(
+              appView, virtualRootMethod.getHolderType().toTypeElement(appView).asClassType());
+      if (bounds.equals(trivialBounds)) {
+        return DynamicType.unknown();
+      }
       return bounds;
     }
 
-    DynamicType trivialBounds =
-        DynamicType.create(
-            appView, virtualRootMethod.getHolderType().toTypeElement(appView).asClassType());
-    if (bounds.equals(trivialBounds)) {
-      return DynamicType.unknown();
-    }
-    return bounds;
-  }
-
-  private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState(
-      InvokeMethod invoke,
-      ProgramMethod resolvedMethod,
-      ProgramMethod singleTarget,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
-    return computeMonomorphicMethodState(
-        invoke,
-        resolvedMethod,
-        singleTarget,
-        abstractValueSupplier,
-        context,
-        existingMethodState,
-        invoke.isInvokeMethodWithReceiver()
-            ? invoke.getFirstArgument().getDynamicType(appView)
-            : null);
-  }
-
-  @SuppressWarnings("UnusedVariable")
-  private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState(
-      InvokeMethod invoke,
-      ProgramMethod resolvedMethod,
-      ProgramMethod singleTarget,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      ConcreteMonomorphicMethodStateOrBottom existingMethodState,
-      DynamicType dynamicReceiverType) {
-    List<ValueState> parameterStates = new ArrayList<>(invoke.arguments().size());
-
-    MethodReprocessingCriteria methodReprocessingCriteria =
-        singleTarget != null
-            ? reprocessingCriteriaCollection.getReprocessingCriteria(singleTarget)
-            : MethodReprocessingCriteria.alwaysReprocess();
-
-    int argumentIndex = 0;
-    if (invoke.isInvokeMethodWithReceiver()) {
-      assert dynamicReceiverType != null;
-      parameterStates.add(
-          computeParameterStateForReceiver(
-              resolvedMethod,
-              dynamicReceiverType,
-              existingMethodState,
-              methodReprocessingCriteria.getParameterReprocessingCriteria(0)));
-      argumentIndex++;
+    private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState(
+        InvokeMethod invoke,
+        ProgramMethod resolvedMethod,
+        ProgramMethod singleTarget,
+        ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
+      return computeMonomorphicMethodState(
+          invoke,
+          resolvedMethod,
+          singleTarget,
+          existingMethodState,
+          invoke.isInvokeMethodWithReceiver()
+              ? invoke.getFirstArgument().getDynamicType(appView)
+              : null);
     }
 
-    for (; argumentIndex < invoke.arguments().size(); argumentIndex++) {
-      parameterStates.add(
-          computeParameterStateForNonReceiver(
-              invoke,
-              singleTarget,
-              argumentIndex,
-              invoke.getArgument(argumentIndex),
-              abstractValueSupplier,
-              context,
-              existingMethodState));
+    @SuppressWarnings("UnusedVariable")
+    private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState(
+        InvokeMethod invoke,
+        ProgramMethod resolvedMethod,
+        ProgramMethod singleTarget,
+        ConcreteMonomorphicMethodStateOrBottom existingMethodState,
+        DynamicType dynamicReceiverType) {
+      List<ValueState> parameterStates = new ArrayList<>(invoke.arguments().size());
+
+      MethodReprocessingCriteria methodReprocessingCriteria =
+          singleTarget != null
+              ? reprocessingCriteriaCollection.getReprocessingCriteria(singleTarget)
+              : MethodReprocessingCriteria.alwaysReprocess();
+
+      int argumentIndex = 0;
+      if (invoke.isInvokeMethodWithReceiver()) {
+        assert dynamicReceiverType != null;
+        parameterStates.add(
+            computeParameterStateForReceiver(
+                resolvedMethod,
+                dynamicReceiverType,
+                existingMethodState,
+                methodReprocessingCriteria.getParameterReprocessingCriteria(0)));
+        argumentIndex++;
+      }
+
+      for (; argumentIndex < invoke.arguments().size(); argumentIndex++) {
+        parameterStates.add(
+            computeParameterStateForNonReceiver(
+                invoke,
+                singleTarget,
+                argumentIndex,
+                invoke.getArgument(argumentIndex),
+                existingMethodState));
+      }
+
+      // We simulate that the return value is used for methods with void return type. This ensures
+      // that we will widen the method state to unknown if/when all parameter states become unknown.
+      boolean isReturnValueUsed = invoke.getReturnType().isVoidType() || invoke.hasUsedOutValue();
+      return ConcreteMonomorphicMethodState.create(isReturnValueUsed, parameterStates);
     }
 
-    // We simulate that the return value is used for methods with void return type. This ensures
-    // that we will widen the method state to unknown if/when all parameter states become unknown.
-    boolean isReturnValueUsed = invoke.getReturnType().isVoidType() || invoke.hasUsedOutValue();
-    return ConcreteMonomorphicMethodState.create(isReturnValueUsed, parameterStates);
-  }
+    // For receivers there is not much point in trying to track an abstract value. Therefore we only
+    // track the dynamic type for receivers.
+    // TODO(b/190154391): Consider validating the above hypothesis by using
+    //  computeParameterStateForNonReceiver() for receivers.
+    private ValueState computeParameterStateForReceiver(
+        ProgramMethod resolvedMethod,
+        DynamicType dynamicReceiverType,
+        ConcreteMonomorphicMethodStateOrBottom existingMethodState,
+        ParameterReprocessingCriteria parameterReprocessingCriteria) {
+      // Don't compute a state for this parameter if the stored state is already unknown.
+      if (existingMethodState.isMonomorphic()
+          && existingMethodState.asMonomorphic().getParameterState(0).isUnknown()) {
+        return ValueState.unknown();
+      }
 
-  // For receivers there is not much point in trying to track an abstract value. Therefore we only
-  // track the dynamic type for receivers.
-  // TODO(b/190154391): Consider validating the above hypothesis by using
-  //  computeParameterStateForNonReceiver() for receivers.
-  private ValueState computeParameterStateForReceiver(
-      ProgramMethod resolvedMethod,
-      DynamicType dynamicReceiverType,
-      ConcreteMonomorphicMethodStateOrBottom existingMethodState,
-      ParameterReprocessingCriteria parameterReprocessingCriteria) {
-    // Don't compute a state for this parameter if the stored state is already unknown.
-    if (existingMethodState.isMonomorphic()
-        && existingMethodState.asMonomorphic().getParameterState(0).isUnknown()) {
-      return ValueState.unknown();
+      // For receivers we only track the dynamic type. Therefore, if there is no need to track the
+      // dynamic type of the receiver of the targeted method, then just return unknown.
+      if (!parameterReprocessingCriteria.shouldReprocessDueToDynamicType()) {
+        return ValueState.unknown();
+      }
+
+      DynamicType widenedDynamicReceiverType =
+          WideningUtils.widenDynamicReceiverType(appView, resolvedMethod, dynamicReceiverType);
+      return widenedDynamicReceiverType.isUnknown()
+          ? ValueState.unknown()
+          : new ConcreteReceiverValueState(dynamicReceiverType);
     }
 
-    // For receivers we only track the dynamic type. Therefore, if there is no need to track the
-    // dynamic type of the receiver of the targeted method, then just return unknown.
-    if (!parameterReprocessingCriteria.shouldReprocessDueToDynamicType()) {
-      return ValueState.unknown();
+    @SuppressWarnings("UnusedVariable")
+    private ValueState computeParameterStateForNonReceiver(
+        InvokeMethod invoke,
+        ProgramMethod singleTarget,
+        int argumentIndex,
+        Value argument,
+        ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
+      ValueState modeledState =
+          modeling.modelParameterStateForArgumentToFunction(
+              invoke, singleTarget, argumentIndex, argument, context);
+      if (modeledState != null) {
+        return modeledState;
+      }
+
+      // Don't compute a state for this parameter if the stored state is already unknown.
+      if (existingMethodState.isMonomorphic()
+          && existingMethodState.asMonomorphic().getParameterState(argumentIndex).isUnknown()) {
+        return ValueState.unknown();
+      }
+
+      DexType parameterType =
+          invoke.getInvokedMethod().getArgumentType(argumentIndex, invoke.isInvokeStatic());
+
+      // If the value is an argument of the enclosing method, then clearly we have no information
+      // about its abstract value. Instead of treating this as having an unknown runtime value, we
+      // instead record a flow constraint that specifies that all values that flow into the
+      // parameter of this enclosing method also flows into the corresponding parameter of the
+      // methods potentially called from this invoke instruction.
+      NonEmptyValueState inFlowState = computeInFlowState(parameterType, argument, context);
+      if (inFlowState != null) {
+        return inFlowState;
+      }
+
+      // Only track the nullability for array types.
+      if (parameterType.isArrayType()) {
+        Nullability nullability = argument.getType().nullability();
+        return ConcreteArrayTypeValueState.create(nullability);
+      }
+
+      AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(argument);
+
+      // For class types, we track both the abstract value and the dynamic type. If both are
+      // unknown, then use UnknownParameterState.
+      if (parameterType.isClassType()) {
+        DynamicType dynamicType = argument.getDynamicType(appView);
+        DynamicType widenedDynamicType =
+            WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
+        return ConcreteClassTypeValueState.create(abstractValue, widenedDynamicType);
+      } else {
+        // For primitive types, we only track the abstract value, thus if the abstract value is
+        // unknown, we use UnknownParameterState.
+        assert parameterType.isPrimitiveType();
+        return ConcretePrimitiveTypeValueState.create(abstractValue);
+      }
     }
 
-    DynamicType widenedDynamicReceiverType =
-        WideningUtils.widenDynamicReceiverType(appView, resolvedMethod, dynamicReceiverType);
-    return widenedDynamicReceiverType.isUnknown()
-        ? ValueState.unknown()
-        : new ConcreteReceiverValueState(dynamicReceiverType);
-  }
+    @SuppressWarnings("ReferenceEquality")
+    private DexMethod getRepresentative(InvokeMethod invoke, ProgramMethod resolvedMethod) {
+      if (resolvedMethod.getDefinition().belongsToDirectPool()) {
+        return resolvedMethod.getReference();
+      }
 
-  @SuppressWarnings("UnusedVariable")
-  private ValueState computeParameterStateForNonReceiver(
-      InvokeMethod invoke,
-      ProgramMethod singleTarget,
-      int argumentIndex,
-      Value argument,
-      AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
-      ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
-    ValueState modeledState =
-        modeling.modelParameterStateForArgumentToFunction(
-            invoke, singleTarget, argumentIndex, argument, context);
-    if (modeledState != null) {
-      return modeledState;
+      if (isMonomorphicVirtualMethod(resolvedMethod)) {
+        return resolvedMethod.getReference();
+      }
+
+      if (invoke.isInvokeInterface()) {
+        assert !isMonomorphicVirtualMethod(resolvedMethod);
+        return getVirtualRootMethod(resolvedMethod);
+      }
+
+      assert invoke.isInvokeSuper() || invoke.isInvokeVirtual();
+
+      DexMethod rootMethod = getVirtualRootMethod(resolvedMethod);
+      assert rootMethod != null;
+      assert !isMonomorphicVirtualMethod(resolvedMethod)
+          || rootMethod == resolvedMethod.getReference();
+      return rootMethod;
     }
 
-    // Don't compute a state for this parameter if the stored state is already unknown.
-    if (existingMethodState.isMonomorphic()
-        && existingMethodState.asMonomorphic().getParameterState(argumentIndex).isUnknown()) {
-      return ValueState.unknown();
+    private boolean shouldUsePolymorphicMethodState(
+        InvokeMethod invoke, ProgramMethod resolvedMethod) {
+      return !resolvedMethod.getDefinition().belongsToDirectPool()
+          && !isMonomorphicVirtualMethod(getRepresentative(invoke, resolvedMethod));
     }
 
-    DexType parameterType =
-        invoke.getInvokedMethod().getArgumentType(argumentIndex, invoke.isInvokeStatic());
-
-    // If the value is an argument of the enclosing method, then clearly we have no information
-    // about its abstract value. Instead of treating this as having an unknown runtime value, we
-    // instead record a flow constraint that specifies that all values that flow into the parameter
-    // of this enclosing method also flows into the corresponding parameter of the methods
-    // potentially called from this invoke instruction.
-    NonEmptyValueState inFlowState = computeInFlowState(parameterType, argument, context);
-    if (inFlowState != null) {
-      return inFlowState;
-    }
-
-    // Only track the nullability for array types.
-    if (parameterType.isArrayType()) {
-      Nullability nullability = argument.getType().nullability();
-      return ConcreteArrayTypeValueState.create(nullability);
-    }
-
-    AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(argument);
-
-    // For class types, we track both the abstract value and the dynamic type. If both are unknown,
-    // then use UnknownParameterState.
-    if (parameterType.isClassType()) {
-      DynamicType dynamicType = argument.getDynamicType(appView);
-      DynamicType widenedDynamicType =
-          WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
-      return ConcreteClassTypeValueState.create(abstractValue, widenedDynamicType);
-    } else {
-      // For primitive types, we only track the abstract value, thus if the abstract value is
-      // unknown,
-      // we use UnknownParameterState.
-      assert parameterType.isPrimitiveType();
-      return ConcretePrimitiveTypeValueState.create(abstractValue);
-    }
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  private DexMethod getRepresentative(InvokeMethod invoke, ProgramMethod resolvedMethod) {
-    if (resolvedMethod.getDefinition().belongsToDirectPool()) {
-      return resolvedMethod.getReference();
-    }
-
-    if (isMonomorphicVirtualMethod(resolvedMethod)) {
-      return resolvedMethod.getReference();
-    }
-
-    if (invoke.isInvokeInterface()) {
-      assert !isMonomorphicVirtualMethod(resolvedMethod);
-      return getVirtualRootMethod(resolvedMethod);
-    }
-
-    assert invoke.isInvokeSuper() || invoke.isInvokeVirtual();
-
-    DexMethod rootMethod = getVirtualRootMethod(resolvedMethod);
-    assert rootMethod != null;
-    assert !isMonomorphicVirtualMethod(resolvedMethod)
-        || rootMethod == resolvedMethod.getReference();
-    return rootMethod;
-  }
-
-  private boolean shouldUsePolymorphicMethodState(
-      InvokeMethod invoke, ProgramMethod resolvedMethod) {
-    return !resolvedMethod.getDefinition().belongsToDirectPool()
-        && !isMonomorphicVirtualMethod(getRepresentative(invoke, resolvedMethod));
-  }
-
-  private void scan(InvokeCustom invoke) {
-    // If the bootstrap method is program declared it will be called. The call is with runtime
-    // provided arguments so ensure that the argument information is unknown.
-    DexMethodHandle bootstrapMethod = invoke.getCallSite().bootstrapMethod;
-    SingleResolutionResult<?> resolution =
-        appView
-            .appInfo()
-            .resolveMethodLegacy(bootstrapMethod.asMethod(), bootstrapMethod.isInterface)
-            .asSingleResolution();
-    if (resolution != null && resolution.getResolvedHolder().isProgramClass()) {
-      methodStates.set(resolution.getResolvedProgramMethod(), UnknownMethodState.get());
+    private void scanInvokeCustom(InvokeCustom invoke) {
+      // If the bootstrap method is program declared it will be called. The call is with runtime
+      // provided arguments so ensure that the argument information is unknown.
+      DexMethodHandle bootstrapMethod = invoke.getCallSite().bootstrapMethod;
+      SingleResolutionResult<?> resolution =
+          appView
+              .appInfo()
+              .resolveMethodLegacy(bootstrapMethod.asMethod(), bootstrapMethod.isInterface)
+              .asSingleResolution();
+      if (resolution != null && resolution.getResolvedHolder().isProgramClass()) {
+        methodStates.set(resolution.getResolvedProgramMethod(), UnknownMethodState.get());
+      }
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeArgumentNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeArgumentNode.java
new file mode 100644
index 0000000..2ea279f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeArgumentNode.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.utils.ArrayUtils;
+import java.util.Objects;
+import java.util.function.IntFunction;
+
+/** Represents the read of an argument. */
+public class ComputationTreeArgumentNode extends ComputationTreeBaseNode {
+
+  private static final int NUM_CANONICALIZED_INSTANCES = 32;
+  private static final ComputationTreeArgumentNode[] CANONICALIZED_INSTANCES =
+      ArrayUtils.initialize(
+          new ComputationTreeArgumentNode[NUM_CANONICALIZED_INSTANCES],
+          ComputationTreeArgumentNode::new);
+
+  private final int argumentIndex;
+
+  private ComputationTreeArgumentNode(int argumentIndex) {
+    this.argumentIndex = argumentIndex;
+  }
+
+  public static ComputationTreeArgumentNode create(int argumentIndex) {
+    return argumentIndex < NUM_CANONICALIZED_INSTANCES
+        ? CANONICALIZED_INSTANCES[argumentIndex]
+        : new ComputationTreeArgumentNode(argumentIndex);
+  }
+
+  @Override
+  public AbstractValue evaluate(
+      IntFunction<AbstractValue> argumentAssignment, AbstractValueFactory abstractValueFactory) {
+    return argumentAssignment.apply(argumentIndex);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ComputationTreeArgumentNode)) {
+      return false;
+    }
+    ComputationTreeArgumentNode node = (ComputationTreeArgumentNode) obj;
+    assert argumentIndex >= NUM_CANONICALIZED_INSTANCES
+        || node.argumentIndex >= NUM_CANONICALIZED_INSTANCES;
+    return argumentIndex == node.argumentIndex;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), argumentIndex);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeBaseNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeBaseNode.java
new file mode 100644
index 0000000..2228c3e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeBaseNode.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+/**
+ * Represents a computation tree with no open variables other than the arguments of a given method.
+ */
+abstract class ComputationTreeBaseNode implements ComputationTreeNode {
+
+  @Override
+  public abstract boolean equals(Object obj);
+
+  @Override
+  public abstract int hashCode();
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeBuilder.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeBuilder.java
new file mode 100644
index 0000000..aca8a03
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeBuilder.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+import static com.android.tools.r8.ir.code.Opcodes.AND;
+import static com.android.tools.r8.ir.code.Opcodes.ARGUMENT;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_NUMBER;
+import static com.android.tools.r8.ir.code.Opcodes.IF;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.analysis.value.UnknownValue;
+import com.android.tools.r8.ir.code.And;
+import com.android.tools.r8.ir.code.Argument;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Value;
+
+public class ComputationTreeBuilder {
+
+  private final AbstractValueFactory abstractValueFactory;
+
+  public ComputationTreeBuilder(AbstractValueFactory abstractValueFactory) {
+    this.abstractValueFactory = abstractValueFactory;
+  }
+
+  // TODO(b/302281503): "Long lived" computation trees (i.e., the ones that survive past the IR
+  //  conversion of the current method) should be canonicalized.
+  // TODO(b/302281503): If we start building larger computation trees then make sure to the
+  //  computation trees for intermediate instructions to ensure that we do not build the computation
+  //  tree for a given instruction more than once.
+  public ComputationTreeNode buildComputationTree(Instruction instruction) {
+    switch (instruction.opcode()) {
+      case AND:
+        {
+          And and = instruction.asAnd();
+          ComputationTreeNode left = buildComputationTreeFromValue(and.leftValue());
+          ComputationTreeNode right = buildComputationTreeFromValue(and.rightValue());
+          return ComputationTreeLogicalBinopAndNode.create(left, right);
+        }
+      case ARGUMENT:
+        {
+          Argument argument = instruction.asArgument();
+          if (argument.getOutType().isInt()) {
+            return ComputationTreeArgumentNode.create(argument.getIndex());
+          }
+          break;
+        }
+      case CONST_NUMBER:
+        {
+          ConstNumber constNumber = instruction.asConstNumber();
+          if (constNumber.getOutType().isInt()) {
+            return constNumber.getAbstractValue(abstractValueFactory);
+          }
+          break;
+        }
+      case IF:
+        {
+          If theIf = instruction.asIf();
+          if (theIf.isZeroTest()) {
+            ComputationTreeNode operand = buildComputationTreeFromValue(theIf.lhs());
+            return ComputationTreeUnopCompareNode.create(operand, theIf.getType());
+          }
+          break;
+        }
+      default:
+        break;
+    }
+    return AbstractValue.unknown();
+  }
+
+  private ComputationTreeNode buildComputationTreeFromValue(Value value) {
+    if (value.isPhi()) {
+      return unknown();
+    }
+    return buildComputationTree(value.getDefinition());
+  }
+
+  private static UnknownValue unknown() {
+    return AbstractValue.unknown();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeLogicalBinopAndNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeLogicalBinopAndNode.java
new file mode 100644
index 0000000..f18f081
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeLogicalBinopAndNode.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
+import java.util.Objects;
+import java.util.function.IntFunction;
+
+public class ComputationTreeLogicalBinopAndNode extends ComputationTreeLogicalBinopNode {
+
+  private ComputationTreeLogicalBinopAndNode(ComputationTreeNode left, ComputationTreeNode right) {
+    super(left, right);
+  }
+
+  public static ComputationTreeNode create(ComputationTreeNode left, ComputationTreeNode right) {
+    if (left.isUnknown() && right.isUnknown()) {
+      return AbstractValue.unknown();
+    }
+    return new ComputationTreeLogicalBinopAndNode(left, right);
+  }
+
+  @Override
+  public AbstractValue evaluate(
+      IntFunction<AbstractValue> argumentAssignment, AbstractValueFactory abstractValueFactory) {
+    assert getNumericType().isInt();
+    AbstractValue leftValue = left.evaluate(argumentAssignment, abstractValueFactory);
+    AbstractValue rightValue = right.evaluate(argumentAssignment, abstractValueFactory);
+    return AbstractCalculator.andIntegers(abstractValueFactory, leftValue, rightValue);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ComputationTreeLogicalBinopAndNode)) {
+      return false;
+    }
+    ComputationTreeLogicalBinopAndNode node = (ComputationTreeLogicalBinopAndNode) obj;
+    return internalIsEqualTo(node);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), left, right);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeLogicalBinopNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeLogicalBinopNode.java
new file mode 100644
index 0000000..8f0c95f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeLogicalBinopNode.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+import com.android.tools.r8.ir.code.NumericType;
+
+public abstract class ComputationTreeLogicalBinopNode extends ComputationTreeBaseNode {
+
+  final ComputationTreeNode left;
+  final ComputationTreeNode right;
+
+  ComputationTreeLogicalBinopNode(ComputationTreeNode left, ComputationTreeNode right) {
+    assert !left.isUnknown() || !right.isUnknown();
+    this.left = left;
+    this.right = right;
+  }
+
+  public NumericType getNumericType() {
+    return NumericType.INT;
+  }
+
+  boolean internalIsEqualTo(ComputationTreeLogicalBinopNode node) {
+    return left.equals(node.left) && right.equals(node.right);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeNode.java
new file mode 100644
index 0000000..f055cff
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeNode.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import java.util.function.IntFunction;
+
+/**
+ * Represents a computation tree with no open variables other than the arguments of a given method.
+ */
+public interface ComputationTreeNode {
+
+  /** Evaluates the current computation tree on the given argument assignment. */
+  AbstractValue evaluate(
+      IntFunction<AbstractValue> argumentAssignment, AbstractValueFactory abstractValueFactory);
+
+  default boolean isUnknown() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeUnopCompareNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeUnopCompareNode.java
new file mode 100644
index 0000000..fb4c465
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeUnopCompareNode.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.code.IfType;
+import java.util.Objects;
+import java.util.function.IntFunction;
+
+public class ComputationTreeUnopCompareNode extends ComputationTreeUnopNode {
+
+  private final IfType type;
+
+  private ComputationTreeUnopCompareNode(ComputationTreeNode operand, IfType type) {
+    super(operand);
+    this.type = type;
+  }
+
+  public static ComputationTreeNode create(ComputationTreeNode operand, IfType type) {
+    if (operand.isUnknown()) {
+      return AbstractValue.unknown();
+    }
+    return new ComputationTreeUnopCompareNode(operand, type);
+  }
+
+  @Override
+  public AbstractValue evaluate(
+      IntFunction<AbstractValue> argumentAssignment, AbstractValueFactory abstractValueFactory) {
+    AbstractValue operandValue = operand.evaluate(argumentAssignment, abstractValueFactory);
+    return type.evaluate(operandValue, abstractValueFactory);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ComputationTreeUnopCompareNode)) {
+      return false;
+    }
+    ComputationTreeUnopCompareNode node = (ComputationTreeUnopCompareNode) obj;
+    return type == node.type && internalIsEqualTo(node);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), operand, type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeUnopNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeUnopNode.java
new file mode 100644
index 0000000..3c3fc1e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/computation/ComputationTreeUnopNode.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.computation;
+
+public abstract class ComputationTreeUnopNode extends ComputationTreeBaseNode {
+
+  final ComputationTreeNode operand;
+
+  ComputationTreeUnopNode(ComputationTreeNode operand) {
+    assert !operand.isUnknown();
+    this.operand = operand;
+  }
+
+  boolean internalIsEqualTo(ComputationTreeUnopNode node) {
+    return operand.equals(node.operand);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
index 45848ad..28d0ead 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteReferenceTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
@@ -120,9 +121,12 @@
             if (state.isUnknown()) {
               return;
             }
-            if (state.isReferenceState()
-                && state.asReferenceState().getNullability().isNullable()) {
-              return;
+            if (state.isReferenceState()) {
+              ConcreteReferenceTypeValueState referenceState = state.asReferenceState();
+              if (referenceState.getNullability().isNullable()
+                  && referenceState.getAbstractValue(appView).isUnknown()) {
+                return;
+              }
             }
             fieldsOfInterest
                 .computeIfAbsent(field.getHolder(), ignoreKey(ArrayList::new))
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
index a1de155..b91bff4 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -26,13 +26,13 @@
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -102,38 +102,42 @@
         eligibleSubclasses.add(subclass);
       }
     }
-    for (Wrapper<DexMethod> candidate : getCandidatesForHoisting(eligibleSubclasses)) {
-      hoistBridgeIfPossible(candidate.get(), clazz, eligibleSubclasses);
+    for (ProgramMethod candidate : getCandidatesForHoisting(eligibleSubclasses)) {
+      hoistBridgeIfPossible(candidate, clazz, eligibleSubclasses);
     }
   }
 
-  private Set<Wrapper<DexMethod>> getCandidatesForHoisting(List<DexProgramClass> subclasses) {
+  private Collection<ProgramMethod> getCandidatesForHoisting(List<DexProgramClass> subclasses) {
     Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
-    Set<Wrapper<DexMethod>> candidates = new HashSet<>();
+    Map<Wrapper<DexMethod>, ProgramMethod> candidates = new LinkedHashMap<>();
     for (DexProgramClass subclass : subclasses) {
-      for (DexEncodedMethod method : subclass.virtualMethods()) {
+      for (ProgramMethod method : subclass.virtualProgramMethods()) {
+        if (appView.getKeepInfo(method).isPinned(appView.options())) {
+          continue;
+        }
         BridgeInfo bridgeInfo = method.getOptimizationInfo().getBridgeInfo();
         if (bridgeInfo != null && bridgeInfo.isVirtualBridgeInfo()) {
-          candidates.add(equivalence.wrap(method.getReference()));
+          candidates.put(equivalence.wrap(method.getReference()), method);
         }
       }
     }
-    return candidates;
+    return candidates.values();
   }
 
   private void hoistBridgeIfPossible(
-      DexMethod method, DexProgramClass clazz, List<DexProgramClass> subclasses) {
+      ProgramMethod method, DexProgramClass clazz, List<DexProgramClass> subclasses) {
     // If the method is defined on the parent class, we cannot hoist the bridge.
     // TODO(b/153147967): If the declared method is abstract, we could replace it by the bridge.
     //  Add a test.
-    if (clazz.lookupProgramMethod(method) != null) {
+    DexMethod methodReference = method.getReference();
+    if (clazz.lookupProgramMethod(methodReference) != null) {
       return;
     }
 
     // Bail out if the bridge is also declared in the parent class. In that case, hoisting would
     // change the behavior of calling the bridge on an instance of the parent class.
     MethodResolutionResult res =
-        appView.appInfo().resolveMethodOnClass(clazz.getSuperType(), method);
+        appView.appInfo().resolveMethodOnClass(clazz.getSuperType(), methodReference);
     if (res.isSingleResolution()) {
       if (!res.getResolvedMethod().isAbstract()) {
         return;
@@ -147,10 +151,13 @@
     // implicitly defined by the signature of the invoke-virtual instruction).
     Map<Wrapper<DexMethod>, List<DexProgramClass>> eligibleVirtualInvokeBridges = new HashMap<>();
     for (DexProgramClass subclass : subclasses) {
-      DexEncodedMethod definition = subclass.lookupVirtualMethod(method);
+      DexEncodedMethod definition = subclass.lookupVirtualMethod(methodReference);
       if (definition == null) {
         DexEncodedMethod resolutionTarget =
-            appView.appInfo().resolveMethodOnClassLegacy(subclass, method).getSingleTarget();
+            appView
+                .appInfo()
+                .resolveMethodOnClassLegacy(subclass, methodReference)
+                .getSingleTarget();
         if (resolutionTarget == null || resolutionTarget.isAbstract()) {
           // The fact that this class does not declare the bridge (or the bridge is abstract) should
           // not prevent us from hoisting the bridge.
@@ -208,7 +215,7 @@
 
     // Choose one of the bridge definitions as the one that we will be moving to the superclass.
     List<ProgramMethod> eligibleBridgeMethods =
-        getBridgesEligibleForHoisting(eligibleSubclasses, method);
+        getBridgesEligibleForHoisting(eligibleSubclasses, methodReference);
     ProgramMethod representative = eligibleBridgeMethods.iterator().next();
 
     // Guard against accessibility issues.
@@ -234,8 +241,7 @@
     feedback.setBridgeInfo(representative, new VirtualBridgeInfo(methodToInvoke));
 
     // Move the bridge method to the super class, and record this in the graph lens.
-    DexMethod newMethodReference =
-        appView.dexItemFactory().createMethod(clazz.type, method.proto, method.name);
+    DexMethod newMethodReference = methodReference.withHolder(clazz, appView.dexItemFactory());
     DexEncodedMethod newMethod =
         representative
             .getDefinition()
@@ -250,9 +256,8 @@
         representative.getReference());
 
     // Remove all of the bridges in the eligible subclasses.
-    assert !appView.appInfo().isPinnedWithDefinitionLookup(method);
     for (DexProgramClass subclass : eligibleSubclasses) {
-      DexEncodedMethod removed = subclass.removeMethod(method);
+      DexEncodedMethod removed = subclass.removeMethod(methodReference);
       assert removed != null;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
index 174e8ba..2116f3a 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.AbstractValueSupplier;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
@@ -25,16 +26,12 @@
   }
 
   @Override
-  protected void addTemporaryMethodState(
-      InvokeMethod invoke,
-      ProgramMethod resolvedMethod,
+  public void scan(
+      ProgramMethod method,
+      IRCode code,
       AbstractValueSupplier abstractValueSupplier,
-      ProgramMethod context,
       Timing timing) {
-    ComposableCallGraphNode node = callGraph.getNodes().get(resolvedMethod);
-    if (node != null && node.isComposable()) {
-      super.addTemporaryMethodState(invoke, resolvedMethod, abstractValueSupplier, context, timing);
-    }
+    new CodeScanner(abstractValueSupplier, code, method).scan(timing);
   }
 
   @Override
@@ -43,4 +40,21 @@
     // We haven't defined the virtual root mapping, so we can't tell.
     return false;
   }
+
+  private class CodeScanner extends ArgumentPropagatorCodeScanner.CodeScanner {
+
+    protected CodeScanner(
+        AbstractValueSupplier abstractValueSupplier, IRCode code, ProgramMethod method) {
+      super(abstractValueSupplier, code, method);
+    }
+
+    @Override
+    protected void addTemporaryMethodState(
+        InvokeMethod invoke, ProgramMethod resolvedMethod, Timing timing) {
+      ComposableCallGraphNode node = callGraph.getNodes().get(resolvedMethod);
+      if (node != null && node.isComposable()) {
+        super.addTemporaryMethodState(invoke, resolvedMethod, timing);
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 1766f40..147ce09 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3921,7 +3921,7 @@
         timing.begin("Retain keep info");
         applicableRules = keepInfoCollection.getApplicableRules();
         EnqueuerEvent preconditionEvent = UnconditionalKeepInfoEvent.get();
-        keepInfo.registerCompilerSynthesizedMethods(keepInfoCollection);
+        keepInfo.registerCompilerSynthesizedItems(keepInfoCollection);
         keepInfoCollection.forEachRuleInstance(
             appView,
             (clazz, minimumKeepInfo) ->
@@ -5594,7 +5594,7 @@
     // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
     // call this method.
     if (invoke.inValues().get(0).isConstClass()) {
-      DexType type = invoke.inValues().get(0).definition.asConstClass().getValue();
+      DexType type = invoke.inValues().get(0).definition.asConstClass().getType();
       DexProgramClass clazz = getProgramClassOrNull(type, method);
       if (clazz != null && clazz.isEnum()) {
         markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(method));
@@ -5610,7 +5610,7 @@
 
     Value argument = invoke.inValues().get(0).getAliasedValue();
     if (!argument.isPhi() && argument.definition.isConstClass()) {
-      DexType serviceType = argument.definition.asConstClass().getValue();
+      DexType serviceType = argument.definition.asConstClass().getType();
       if (!appView.appServices().allServiceTypes().contains(serviceType)) {
         // Should never happen.
         return;
diff --git a/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java b/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java
index 7dfbfeb..994e94c 100644
--- a/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java
@@ -6,6 +6,8 @@
 /** Globally controlled settings that affect the default values for kept items. */
 public interface GlobalKeepInfoConfiguration {
 
+  boolean isCodeReplacementForceEnabled();
+
   boolean isTreeShakingEnabled();
 
   boolean isMinificationEnabled();
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
index c1c5ec0..3bb0f20 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
@@ -11,7 +11,7 @@
 import java.util.function.Function;
 
 /** Immutable keep requirements for a class. */
-public final class KeepClassInfo extends KeepInfo<KeepClassInfo.Builder, KeepClassInfo> {
+public class KeepClassInfo extends KeepInfo<KeepClassInfo.Builder, KeepClassInfo> {
 
   // Requires all aspects of a class to be kept.
   private static final KeepClassInfo TOP =
@@ -53,16 +53,18 @@
   private final boolean allowHorizontalClassMerging;
   private final boolean allowPermittedSubclassesRemoval;
   private final boolean allowRepackaging;
+  private final boolean allowSyntheticSharing;
   private final boolean allowUnusedInterfaceRemoval;
   private final boolean allowVerticalClassMerging;
   private final boolean checkEnumUnboxed;
 
-  private KeepClassInfo(Builder builder) {
+  KeepClassInfo(Builder builder) {
     super(builder);
     this.allowClassInlining = builder.isClassInliningAllowed();
     this.allowHorizontalClassMerging = builder.isHorizontalClassMergingAllowed();
     this.allowPermittedSubclassesRemoval = builder.isPermittedSubclassesRemovalAllowed();
     this.allowRepackaging = builder.isRepackagingAllowed();
+    this.allowSyntheticSharing = builder.isSyntheticSharingAllowed();
     this.allowUnusedInterfaceRemoval = builder.isUnusedInterfaceRemovalAllowed();
     this.allowVerticalClassMerging = builder.isVerticalClassMergingAllowed();
     this.checkEnumUnboxed = builder.isCheckEnumUnboxedEnabled();
@@ -147,6 +149,14 @@
     return allowRepackaging;
   }
 
+  public boolean isSyntheticSharingAllowed() {
+    return internalIsSyntheticSharingAllowed();
+  }
+
+  private boolean internalIsSyntheticSharingAllowed() {
+    return allowSyntheticSharing;
+  }
+
   boolean internalIsUnusedInterfaceRemovalAllowed() {
     return allowUnusedInterfaceRemoval;
   }
@@ -182,20 +192,22 @@
     private boolean allowHorizontalClassMerging;
     private boolean allowPermittedSubclassesRemoval;
     private boolean allowRepackaging;
+    private boolean allowSyntheticSharing;
     private boolean allowUnusedInterfaceRemoval;
     private boolean allowVerticalClassMerging;
     private boolean checkEnumUnboxed;
 
-    private Builder() {
+    Builder() {
       super();
     }
 
-    private Builder(KeepClassInfo original) {
+    Builder(KeepClassInfo original) {
       super(original);
       allowClassInlining = original.internalIsClassInliningAllowed();
       allowHorizontalClassMerging = original.internalIsHorizontalClassMergingAllowed();
       allowPermittedSubclassesRemoval = original.internalIsPermittedSubclassesRemovalAllowed();
       allowRepackaging = original.internalIsRepackagingAllowed();
+      allowSyntheticSharing = original.internalIsSyntheticSharingAllowed();
       allowUnusedInterfaceRemoval = original.internalIsUnusedInterfaceRemovalAllowed();
       allowVerticalClassMerging = original.internalIsVerticalClassMergingAllowed();
       checkEnumUnboxed = original.internalIsCheckEnumUnboxedEnabled();
@@ -296,6 +308,17 @@
       return setAllowRepackaging(false);
     }
 
+    // Synthetic sharing.
+
+    public boolean isSyntheticSharingAllowed() {
+      return allowSyntheticSharing;
+    }
+
+    private Builder setAllowSyntheticSharing(boolean allowSyntheticSharing) {
+      this.allowSyntheticSharing = allowSyntheticSharing;
+      return self();
+    }
+
     // Unused interface removal.
 
     public Builder allowUnusedInterfaceRemoval() {
@@ -363,6 +386,7 @@
           && isPermittedSubclassesRemovalAllowed()
               == other.internalIsPermittedSubclassesRemovalAllowed()
           && isRepackagingAllowed() == other.internalIsRepackagingAllowed()
+          && isSyntheticSharingAllowed() == other.internalIsSyntheticSharingAllowed()
           && isUnusedInterfaceRemovalAllowed() == other.internalIsUnusedInterfaceRemovalAllowed()
           && isVerticalClassMergingAllowed() == other.internalIsVerticalClassMergingAllowed();
     }
@@ -379,6 +403,8 @@
           .disallowHorizontalClassMerging()
           .disallowPermittedSubclassesRemoval()
           .disallowRepackaging()
+          // Synthetic sharing is always allowed, unless explicitly set to false.
+          .setAllowSyntheticSharing(true)
           .disallowUnusedInterfaceRemoval()
           .disallowVerticalClassMerging()
           .unsetCheckEnumUnboxed();
@@ -391,6 +417,7 @@
           .allowHorizontalClassMerging()
           .allowPermittedSubclassesRemoval()
           .allowRepackaging()
+          .setAllowSyntheticSharing(true)
           .allowUnusedInterfaceRemoval()
           .allowVerticalClassMerging()
           .unsetCheckEnumUnboxed();
@@ -403,6 +430,10 @@
       super(info.builder());
     }
 
+    protected Joiner(KeepClassInfo.Builder builder) {
+      super(builder);
+    }
+
     public Joiner disallowClassInlining() {
       builder.disallowClassInlining();
       return self();
@@ -423,6 +454,11 @@
       return self();
     }
 
+    public Joiner disallowSyntheticSharing() {
+      builder.setAllowSyntheticSharing(false);
+      return self();
+    }
+
     public Joiner disallowUnusedInterfaceRemoval() {
       builder.disallowUnusedInterfaceRemoval();
       return self();
@@ -460,6 +496,7 @@
               !joiner.builder.isPermittedSubclassesRemovalAllowed(),
               Joiner::disallowPermittedSubclassesRemoval)
           .applyIf(!joiner.builder.isRepackagingAllowed(), Joiner::disallowRepackaging)
+          .applyIf(!joiner.builder.isSyntheticSharingAllowed(), Joiner::disallowSyntheticSharing)
           .applyIf(
               !joiner.builder.isUnusedInterfaceRemovalAllowed(),
               Joiner::disallowUnusedInterfaceRemoval)
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 990c260..69f6f02 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -531,15 +531,26 @@
       }
     }
 
+    public void ensureCompilerSynthesizedClass(DexProgramClass clazz) {
+      keepClassInfo.computeIfAbsent(clazz.getType(), ignoreKey(SyntheticKeepClassInfo::bottom));
+    }
+
     @Override
     public void registerCompilerSynthesizedMethod(ProgramMethod method) {
       assert !keepMethodInfo.containsKey(method.getReference());
       keepMethodInfo.put(method.getReference(), SyntheticKeepMethodInfo.bottom());
     }
 
-    public void registerCompilerSynthesizedMethods(KeepInfoCollection keepInfoCollection) {
+    public void registerCompilerSynthesizedItems(KeepInfoCollection keepInfoCollection) {
       keepInfoCollection.mutate(
           mutableKeepInfoCollection -> {
+            mutableKeepInfoCollection.keepClassInfo.forEach(
+                (c, info) -> {
+                  if (info instanceof SyntheticKeepClassInfo) {
+                    assert !keepClassInfo.containsKey(c);
+                    keepClassInfo.put(c, info);
+                  }
+                });
             mutableKeepInfoCollection.keepMethodInfo.forEach(
                 (m, info) -> {
                   if (info instanceof SyntheticKeepMethodInfo) {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
index 7394058..ab20efa 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMember;
 import com.android.tools.r8.shaking.KeepMemberInfo.Builder;
+import com.android.tools.r8.utils.InternalOptions;
 
 /** Immutable keep requirements for a member. */
 @SuppressWarnings("BadImport")
@@ -21,7 +22,6 @@
     this.allowValuePropagation = builder.isValuePropagationAllowed();
   }
 
-  @SuppressWarnings("BadImport")
   public boolean isKotlinMetadataRemovalAllowed(
       DexProgramClass holder, GlobalKeepInfoConfiguration configuration) {
     // Checking the holder for missing kotlin information relies on the holder being processed
@@ -31,18 +31,17 @@
 
   public boolean isValuePropagationAllowed(
       AppView<AppInfoWithLiveness> appView, ProgramMember<?, ?> member) {
+    InternalOptions options = appView.options();
+    if (!internalIsValuePropagationAllowed()) {
+      return false;
+    }
+    if (member.isMethod() && !asMethodInfo().isCodeReplacementAllowed(options)) {
+      return true;
+    }
     DexType type =
         member.isField() ? member.asField().getType() : member.asMethod().getReturnType();
     boolean isTypeInstantiated = !type.isAlwaysNull(appView);
-    return isValuePropagationAllowed(appView.options(), isTypeInstantiated);
-  }
-
-  public boolean isValuePropagationAllowed(
-      GlobalKeepInfoConfiguration configuration, boolean isTypeInstantiated) {
-    if (!isOptimizationAllowed(configuration) && isTypeInstantiated) {
-      return false;
-    }
-    return internalIsValuePropagationAllowed();
+    return isOptimizationAllowed(options) || !isTypeInstantiated;
   }
 
   boolean internalIsValuePropagationAllowed() {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
index 571d3e8..a40c7e6 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
@@ -31,6 +31,7 @@
   private final boolean allowThrowsRemoval;
   private final boolean allowClassInlining;
   private final boolean allowClosedWorldReasoning;
+  private final boolean allowCodeReplacement;
   private final boolean allowConstantArgumentOptimization;
   private final boolean allowInlining;
   private final boolean allowMethodStaticizing;
@@ -50,6 +51,7 @@
     this.allowThrowsRemoval = builder.isThrowsRemovalAllowed();
     this.allowClassInlining = builder.isClassInliningAllowed();
     this.allowClosedWorldReasoning = builder.isClosedWorldReasoningAllowed();
+    this.allowCodeReplacement = builder.isCodeReplacementAllowed();
     this.allowConstantArgumentOptimization = builder.isConstantArgumentOptimizationAllowed();
     this.allowInlining = builder.isInliningAllowed();
     this.allowMethodStaticizing = builder.isMethodStaticizingAllowed();
@@ -129,6 +131,16 @@
     return allowClosedWorldReasoning;
   }
 
+  public boolean isCodeReplacementAllowed(GlobalKeepInfoConfiguration configuration) {
+    return configuration.isCodeReplacementForceEnabled()
+        ? !isOptimizationAllowed(configuration)
+        : internalIsCodeReplacementAllowed();
+  }
+
+  boolean internalIsCodeReplacementAllowed() {
+    return allowCodeReplacement;
+  }
+
   public boolean isConstantArgumentOptimizationAllowed(GlobalKeepInfoConfiguration configuration) {
     return isOptimizationAllowed(configuration) && internalIsConstantArgumentOptimizationAllowed();
   }
@@ -273,6 +285,7 @@
     private boolean allowThrowsRemoval;
     private boolean allowClassInlining;
     private boolean allowClosedWorldReasoning;
+    private boolean allowCodeReplacement;
     private boolean allowConstantArgumentOptimization;
     private boolean allowInlining;
     private boolean allowMethodStaticizing;
@@ -296,6 +309,7 @@
       allowThrowsRemoval = original.internalIsThrowsRemovalAllowed();
       allowClassInlining = original.internalIsClassInliningAllowed();
       allowClosedWorldReasoning = original.internalIsClosedWorldReasoningAllowed();
+      allowCodeReplacement = original.internalIsCodeReplacementAllowed();
       allowConstantArgumentOptimization = original.internalIsConstantArgumentOptimizationAllowed();
       allowInlining = original.internalIsInliningAllowed();
       allowMethodStaticizing = original.internalIsMethodStaticizingAllowed();
@@ -339,6 +353,15 @@
       return self();
     }
 
+    public boolean isCodeReplacementAllowed() {
+      return allowCodeReplacement;
+    }
+
+    public Builder setAllowCodeReplacement(boolean allowCodeReplacement) {
+      this.allowCodeReplacement = allowCodeReplacement;
+      return self();
+    }
+
     public boolean isConstantArgumentOptimizationAllowed() {
       return allowConstantArgumentOptimization;
     }
@@ -483,6 +506,7 @@
           && isThrowsRemovalAllowed() == other.internalIsThrowsRemovalAllowed()
           && isClassInliningAllowed() == other.internalIsClassInliningAllowed()
           && isClosedWorldReasoningAllowed() == other.internalIsClosedWorldReasoningAllowed()
+          && isCodeReplacementAllowed() == other.internalIsCodeReplacementAllowed()
           && isConstantArgumentOptimizationAllowed()
               == other.internalIsConstantArgumentOptimizationAllowed()
           && isInliningAllowed() == other.internalIsInliningAllowed()
@@ -513,6 +537,7 @@
           .setAllowThrowsRemoval(false)
           .setAllowClassInlining(false)
           .setAllowClosedWorldReasoning(false)
+          .setAllowCodeReplacement(true)
           .setAllowConstantArgumentOptimization(false)
           .setAllowInlining(false)
           .setAllowMethodStaticizing(false)
@@ -534,6 +559,7 @@
           .setAllowThrowsRemoval(true)
           .setAllowClassInlining(true)
           .setAllowClosedWorldReasoning(true)
+          .setAllowCodeReplacement(false)
           .setAllowConstantArgumentOptimization(true)
           .setAllowInlining(true)
           .setAllowMethodStaticizing(true)
@@ -575,6 +601,11 @@
       return self();
     }
 
+    public Joiner allowCodeReplacement() {
+      builder.setAllowCodeReplacement(true);
+      return self();
+    }
+
     public Joiner disallowConstantArgumentOptimization() {
       builder.setAllowConstantArgumentOptimization(false);
       return self();
@@ -662,6 +693,7 @@
           .applyIf(!joiner.builder.isClassInliningAllowed(), Joiner::disallowClassInlining)
           .applyIf(
               !joiner.builder.isClosedWorldReasoningAllowed(), Joiner::disallowClosedWorldReasoning)
+          .applyIf(joiner.builder.isCodeReplacementAllowed(), Joiner::allowCodeReplacement)
           .applyIf(
               !joiner.builder.isConstantArgumentOptimizationAllowed(),
               Joiner::disallowConstantArgumentOptimization)
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index f511bee..5b11df3 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1074,6 +1074,8 @@
           } else if (options.isTestingOptionsEnabled()) {
             if (acceptString("annotationremoval")) {
               builder.getModifiersBuilder().setAllowsAnnotationRemoval(true);
+            } else if (acceptString("codereplacement")) {
+              builder.getModifiersBuilder().setAllowsCodeReplacement(true);
             }
           }
         } else if (acceptString("includedescriptorclasses")) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleBase.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleBase.java
index 308ab6e..fb02899 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleBase.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleBase.java
@@ -22,10 +22,6 @@
       super();
     }
 
-    public ProguardKeepRuleType getKeepRuleType() {
-      return type;
-    }
-
     public B setType(ProguardKeepRuleType type) {
       this.type = type;
       return self();
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
index 6073953..c904cc0 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
@@ -10,6 +10,8 @@
 
     private boolean allowsAccessModification = false;
     private boolean allowsAnnotationRemoval = false;
+    // The default value is determined by InternalOptions.
+    private boolean allowsCodeReplacement = false;
     private boolean allowsRepackaging = false;
     private boolean allowsShrinking = false;
     private boolean allowsOptimization = false;
@@ -22,6 +24,7 @@
     public Builder setAllowsAll() {
       setAllowsAccessModification(true);
       setAllowsAnnotationRemoval(true);
+      setAllowsCodeReplacement(false);
       setAllowsObfuscation(true);
       setAllowsOptimization(true);
       setAllowsRepackaging(true);
@@ -39,6 +42,11 @@
       return this;
     }
 
+    public Builder setAllowsCodeReplacement(boolean allowsCodeReplacement) {
+      this.allowsCodeReplacement = allowsCodeReplacement;
+      return this;
+    }
+
     public Builder setAllowsShrinking(boolean allowsShrinking) {
       this.allowsShrinking = allowsShrinking;
       return this;
@@ -75,6 +83,7 @@
       return new ProguardKeepRuleModifiers(
           allowsAccessModification,
           allowsAnnotationRemoval,
+          allowsCodeReplacement,
           allowsRepackaging,
           allowsShrinking,
           allowsOptimization,
@@ -86,6 +95,7 @@
 
   public final boolean allowsAccessModification;
   public final boolean allowsAnnotationRemoval;
+  public final boolean allowsCodeReplacement;
   public final boolean allowsRepackaging;
   public final boolean allowsShrinking;
   public final boolean allowsOptimization;
@@ -96,6 +106,7 @@
   private ProguardKeepRuleModifiers(
       boolean allowsAccessModification,
       boolean allowsAnnotationRemoval,
+      boolean allowsCodeReplacement,
       boolean allowsRepackaging,
       boolean allowsShrinking,
       boolean allowsOptimization,
@@ -104,6 +115,7 @@
       boolean allowsPermittedSubclassesRemoval) {
     this.allowsAccessModification = allowsAccessModification;
     this.allowsAnnotationRemoval = allowsAnnotationRemoval;
+    this.allowsCodeReplacement = allowsCodeReplacement;
     this.allowsRepackaging = allowsRepackaging;
     this.allowsShrinking = allowsShrinking;
     this.allowsOptimization = allowsOptimization;
@@ -122,6 +134,7 @@
   public boolean isBottom() {
     return allowsAccessModification
         && allowsAnnotationRemoval
+        && !allowsCodeReplacement
         && allowsRepackaging
         && allowsObfuscation
         && allowsOptimization
@@ -138,6 +151,7 @@
     ProguardKeepRuleModifiers that = (ProguardKeepRuleModifiers) o;
     return allowsAccessModification == that.allowsAccessModification
         && allowsAnnotationRemoval == that.allowsAnnotationRemoval
+        && allowsCodeReplacement == that.allowsCodeReplacement
         && allowsRepackaging == that.allowsRepackaging
         && allowsShrinking == that.allowsShrinking
         && allowsOptimization == that.allowsOptimization
@@ -151,6 +165,7 @@
     return Objects.hash(
         allowsAccessModification,
         allowsAnnotationRemoval,
+        allowsCodeReplacement,
         allowsRepackaging,
         allowsShrinking,
         allowsOptimization,
@@ -164,6 +179,7 @@
     StringBuilder builder = new StringBuilder();
     appendWithComma(builder, allowsAccessModification, "allowaccessmodification");
     appendWithComma(builder, allowsAnnotationRemoval, "allowannotationremoval");
+    appendWithComma(builder, allowsCodeReplacement, "allowcodereplacement");
     appendWithComma(builder, allowsRepackaging, "allowrepackaging");
     appendWithComma(builder, allowsObfuscation, "allowobfuscation");
     appendWithComma(builder, allowsShrinking, "allowshrinking");
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 e0170ef..4836910 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -1713,6 +1713,13 @@
         context.markAsUsed();
       }
 
+      if (item.isProgramMethod()
+          && !appView.options().isCodeReplacementForceEnabled()
+          && modifiers.allowsCodeReplacement) {
+        itemJoiner.computeIfAbsent().asMethodJoiner().allowCodeReplacement();
+        context.markAsUsed();
+      }
+
       if (item.isProgramClass()
           && appView.options().isKeepPermittedSubclassesEnabled()
           && !modifiers.allowsPermittedSubclassesRemoval) {
diff --git a/src/main/java/com/android/tools/r8/shaking/SyntheticKeepClassInfo.java b/src/main/java/com/android/tools/r8/shaking/SyntheticKeepClassInfo.java
new file mode 100644
index 0000000..7a64979
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/SyntheticKeepClassInfo.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+public class SyntheticKeepClassInfo extends KeepClassInfo {
+
+  // Requires no aspects of a method to be kept.
+  private static final SyntheticKeepClassInfo BOTTOM = new Builder().makeBottom().build();
+
+  public static SyntheticKeepClassInfo bottom() {
+    return BOTTOM;
+  }
+
+  public static Joiner newEmptyJoiner() {
+    return bottom().joiner();
+  }
+
+  @Override
+  Builder builder() {
+    return new Builder(this);
+  }
+
+  public static class Builder extends KeepClassInfo.Builder {
+
+    public Builder() {
+      super();
+    }
+
+    private Builder(SyntheticKeepClassInfo original) {
+      super(original);
+    }
+
+    @Override
+    public boolean isMinificationAllowed() {
+      // Synthetic items can always be minified.
+      return true;
+    }
+
+    @Override
+    public boolean isOptimizationAllowed() {
+      // Synthetic items can always be optimized.
+      return true;
+    }
+
+    @Override
+    public boolean isShrinkingAllowed() {
+      // Synthetic items can always be removed.
+      return true;
+    }
+
+    @Override
+    public SyntheticKeepClassInfo doBuild() {
+      return new SyntheticKeepClassInfo(this);
+    }
+
+    @Override
+    public SyntheticKeepClassInfo build() {
+      return (SyntheticKeepClassInfo) super.build();
+    }
+
+    @Override
+    public Builder makeBottom() {
+      super.makeBottom();
+      return self();
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
+
+  public static class Joiner extends KeepClassInfo.Joiner {
+
+    public Joiner(SyntheticKeepClassInfo info) {
+      super(info.builder());
+    }
+
+    @Override
+    public Joiner disallowMinification() {
+      // Ignore as synthetic items can always be minified.
+      return self();
+    }
+
+    @Override
+    public Joiner disallowOptimization() {
+      // Ignore as synthetic items can always be optimized.
+      return self();
+    }
+
+    @Override
+    public Joiner disallowShrinking() {
+      // Ignore as synthetic items can always be removed.
+      return self();
+    }
+
+    @Override
+    Joiner self() {
+      return this;
+    }
+  }
+
+  public SyntheticKeepClassInfo(Builder builder) {
+    super(builder);
+  }
+
+  @Override
+  public boolean isMinificationAllowed(GlobalKeepInfoConfiguration configuration) {
+    // Synthetic items can always be minified.
+    return true;
+  }
+
+  @Override
+  public boolean isOptimizationAllowed(GlobalKeepInfoConfiguration configuration) {
+    // Synthetic items can always be minified.
+    return true;
+  }
+
+  @Override
+  public boolean isShrinkingAllowed(GlobalKeepInfoConfiguration configuration) {
+    // Synthetic items can always be minified.
+    return true;
+  }
+
+  @Override
+  public Joiner joiner() {
+    return new Joiner(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 4644989..ece8caa 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -26,8 +26,10 @@
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepClassInfo;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.shaking.SyntheticKeepClassInfo;
 import com.android.tools.r8.synthesis.SyntheticItems.State;
 import com.android.tools.r8.synthesis.SyntheticNaming.Phase;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
@@ -298,7 +300,9 @@
         mainDexInfoBuilder.build());
   }
 
-  private <R extends SyntheticReference<R, D, ?>, D extends SyntheticDefinition<R, D, ?>>
+  private <
+          R extends SyntheticReference<R, D, DexProgramClass>,
+          D extends SyntheticDefinition<R, D, DexProgramClass>>
       Map<DexType, EquivalenceGroup<D>> computeEquivalences(
           AppView<?> appView,
           ImmutableMap<DexType, List<R>> references,
@@ -314,6 +318,7 @@
     timing.begin("Potential equivalences");
     Collection<List<D>> potentialEquivalences =
         computePotentialEquivalences(
+            appView,
             definitions,
             intermediate,
             appView.options(),
@@ -836,8 +841,9 @@
   }
 
   @SuppressWarnings("MixedMutabilityReturnType")
-  private static <T extends SyntheticDefinition<?, T, ?>>
+  private static <T extends SyntheticDefinition<?, T, DexProgramClass>>
       Collection<List<T>> computePotentialEquivalences(
+          AppView<?> appView,
           Map<DexType, T> definitions,
           boolean intermediate,
           InternalOptions options,
@@ -870,12 +876,24 @@
     }
     RepresentativeMap map = t -> syntheticTypes.contains(t) ? options.dexItemFactory().voidType : t;
     Map<HashCode, List<T>> equivalences = new HashMap<>(definitions.size());
+    List<List<T>> result = new ArrayList<>();
     for (T definition : definitions.values()) {
-      HashCode hash =
-          definition.computeHash(map, intermediate, classToFeatureSplitMap, syntheticItems);
-      equivalences.computeIfAbsent(hash, k -> new ArrayList<>()).add(definition);
+      DexProgramClass holder = definition.getHolder();
+      KeepClassInfo keepInfo =
+          appView.getKeepInfoOrDefault(holder, SyntheticKeepClassInfo.bottom());
+      if (keepInfo.isSyntheticSharingAllowed()) {
+        HashCode hash =
+            definition.computeHash(map, intermediate, classToFeatureSplitMap, syntheticItems);
+        equivalences.computeIfAbsent(hash, k -> new ArrayList<>()).add(definition);
+      } else {
+        result.add(ImmutableList.of(definition));
+      }
     }
-    return equivalences.values();
+    if (result.isEmpty()) {
+      return equivalences.values();
+    }
+    result.addAll(equivalences.values());
+    return result;
   }
 
   private <R extends SyntheticReference<R, D, ?>, D extends SyntheticDefinition<R, D, ?>>
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 615f440..8ac9315 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -797,6 +797,11 @@
   }
 
   @Override
+  public boolean isCodeReplacementForceEnabled() {
+    return getTestingOptions().allowCodeReplacement;
+  }
+
+  @Override
   public boolean isTreeShakingEnabled() {
     return isShrinking();
   }
@@ -2401,6 +2406,7 @@
     public boolean addCallEdgesForLibraryInvokes = false;
 
     public boolean allowClassInliningOfSynthetics = true;
+    public boolean allowCodeReplacement = true;
     public boolean allowInjectedAnnotationMethods = false;
     public boolean allowInliningOfOutlines = true;
     public boolean allowInliningOfSynthetics = true;
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
index 6772ced..acb8b0d 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -24,6 +25,7 @@
 import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepClassInfo.Joiner;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -286,6 +288,13 @@
               PrunedItems.builder()
                   .setRemovedClasses(verticallyMergedClasses.getSources())
                   .build());
+          for (DexType target : verticallyMergedClasses.getTargets()) {
+            DexProgramClass targetClass = appView.definitionFor(target).asProgramClass();
+            if (appView.getSyntheticItems().isSynthetic(targetClass)) {
+              mutator.ensureCompilerSynthesizedClass(targetClass);
+              mutator.joinClass(targetClass, Joiner::disallowSyntheticSharing);
+            }
+          }
         });
     timing.end();
   }
diff --git a/src/test/examples/identifiernamestring/keep-rules-1.txt b/src/test/examples/identifiernamestring/keep-rules-1.txt
index 682999f..2e0c2ca 100644
--- a/src/test/examples/identifiernamestring/keep-rules-1.txt
+++ b/src/test/examples/identifiernamestring/keep-rules-1.txt
@@ -9,4 +9,5 @@
 
 -keepnames class identifiernamestring.A
 
+-dontoptimize
 -dontshrink
diff --git a/src/test/examples/memberrebinding3/keep-rules.txt b/src/test/examples/memberrebinding3/keep-rules.txt
new file mode 100644
index 0000000..51b9f3c
--- /dev/null
+++ b/src/test/examples/memberrebinding3/keep-rules.txt
@@ -0,0 +1 @@
+-dontoptimize
diff --git a/src/test/examplesJava11/nesthostexample/FullNestOnProgramPathTest.java b/src/test/examplesJava11/nesthostexample/FullNestOnProgramPathTest.java
index c36a73b..5bc8808 100644
--- a/src/test/examplesJava11/nesthostexample/FullNestOnProgramPathTest.java
+++ b/src/test/examplesJava11/nesthostexample/FullNestOnProgramPathTest.java
@@ -106,7 +106,7 @@
     parameters.assumeR8TestParameters();
     R8TestCompileResult compileResult =
         testForR8(parameters.getBackend())
-            .noTreeShaking()
+            .addDontShrink()
             .addDontObfuscate()
             .addKeepAllAttributes()
             .addOptionsModification(options -> options.enableNestReduction = false)
@@ -134,7 +134,7 @@
     parameters.assumeR8TestParameters();
     for (Class<?> clazz : NEST_MAIN_RESULT.keySet()) {
       testForR8(parameters.getBackend())
-          .noTreeShaking()
+          .addDontShrink()
           .addDontObfuscate()
           .addKeepAllAttributes()
           .setMinApi(parameters)
diff --git a/src/test/examplesJava11/nesthostexample/NestAttributesUpdateTest.java b/src/test/examplesJava11/nesthostexample/NestAttributesUpdateTest.java
index 52960eb..ecac6fb 100644
--- a/src/test/examplesJava11/nesthostexample/NestAttributesUpdateTest.java
+++ b/src/test/examplesJava11/nesthostexample/NestAttributesUpdateTest.java
@@ -125,7 +125,7 @@
       throws Exception {
     testForR8(parameters.getBackend())
         .addKeepMainRule(mainClass)
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .addOptionsModification(
             options -> {
               // Disable optimizations else additional classes are removed since they become unused.
diff --git a/src/test/examplesJava11/nesthostexample/NestConstructorRemovedArgTest.java b/src/test/examplesJava11/nesthostexample/NestConstructorRemovedArgTest.java
index 70c0120..216673a 100644
--- a/src/test/examplesJava11/nesthostexample/NestConstructorRemovedArgTest.java
+++ b/src/test/examplesJava11/nesthostexample/NestConstructorRemovedArgTest.java
@@ -56,7 +56,7 @@
   public void testRemoveArgConstructorNestsR8NoTreeShaking() throws Exception {
     parameters.assumeR8TestParameters();
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepMainRule(MAIN_CLASS)
         .addDontObfuscate()
         .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/RemoveVisibilityBridgeMethodsTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/RemoveVisibilityBridgeMethodsTest.java
index 2c372de..8d9654e 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/RemoveVisibilityBridgeMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/RemoveVisibilityBridgeMethodsTest.java
@@ -43,7 +43,7 @@
         .addInnerClasses(RemoveVisibilityBridgeMethodsTest.class)
         .addKeepMainRule(Main.class)
         .allowAccessModification()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(parameters)
         .compile()
         .inspect(this::inspect)
@@ -106,7 +106,7 @@
         .addKeepMainRule(mainClass.name)
         .addOptionsModification(options -> options.inlinerOptions().enableInlining = false)
         .allowAccessModification()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(parameters)
         .compile()
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
index 52fd6e9..33dea90 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
@@ -60,7 +60,7 @@
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .enableProguardTestOptions()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(parameters)
         .compile()
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
index a8f2ac2..9a2e6e4 100644
--- a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
@@ -39,7 +39,7 @@
         .addProgramClassesAndInnerClasses(CLASS)
         .debug()
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .setMinApi(parameters)
         .compile()
         .run(parameters.getRuntime(), CLASS)
diff --git a/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java b/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java
index dea6642..8fa6bb6 100644
--- a/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java
@@ -45,7 +45,7 @@
         .addKeepMainRule(CloserTest.class)
         .setMode(CompilationMode.RELEASE)
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .enableInliningAnnotations()
         .compile()
         .run(CloserTest.class)
diff --git a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
index 6ab2aef..d0023d0 100644
--- a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
@@ -60,7 +60,7 @@
   private R8FullTestBuilder builder() {
     return testForR8(parameters.getBackend())
         .debug()
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addOptionsModification(o -> o.invalidDebugInfoFatal = true)
         .setMinApi(parameters);
diff --git a/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java b/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java
index e002b80..6716f80 100644
--- a/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java
@@ -91,7 +91,7 @@
           // TODO(b/148836254): Support deserialized lambdas without the need of additional rules.
           "-keep class * { private static synthetic void lambda$*(); }");
     } else {
-      builder.addDontObfuscate().noTreeShaking();
+      builder.addDontObfuscate().addDontShrink();
     }
     builder
         .run(parameters.getRuntime(), getMainClass())
diff --git a/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java b/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java
index 5d9dd66..89f13b9 100644
--- a/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java
+++ b/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java
@@ -59,7 +59,7 @@
         .addPrintSeeds()
         .allowStdoutMessages()
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .run(parameters.getRuntime(), getMainClass())
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkPresenceOfDeserializedLambdas);
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
index 80a7beb..21ee0fc 100644
--- a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
@@ -55,7 +55,7 @@
         .addKeepMainRule(TryRangeTest.class)
         .setMode(CompilationMode.RELEASE)
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .enableInliningAnnotations()
         .addOptionsModification(o -> o.enableLoadStoreOptimization = false)
         .run(parameters.getRuntime(), TryRangeTest.class)
@@ -70,7 +70,7 @@
             .addKeepMainRule(TryRangeTestLimitRange.class)
             .setMode(CompilationMode.RELEASE)
             .addDontObfuscate()
-            .noTreeShaking()
+            .addDontShrink()
             .enableInliningAnnotations()
             .addOptionsModification(
                 o -> {
diff --git a/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedOverriddenMethodTest.java b/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedOverriddenMethodTest.java
index 6227c12..c321302 100644
--- a/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedOverriddenMethodTest.java
+++ b/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedOverriddenMethodTest.java
@@ -50,7 +50,7 @@
           .enableNeverClassInliningAnnotations()
           .enableInliningAnnotations()
           .enableNoVerticalClassMergingAnnotations()
-          .minification(minification)
+          .addDontObfuscateUnless(minification)
           .setMinApi(parameters)
           // Asserting that -checkdiscard is not giving any information out on an un-removed
           // sub-type member.
diff --git a/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java b/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java
index ef048be..7260ad8 100644
--- a/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java
+++ b/src/test/java/com/android/tools/r8/checkdiscarded/CheckDiscardedTest.java
@@ -58,7 +58,7 @@
                   UnusedClass.class, UsedClass.class, Main.class, WillBeGone.class, WillStay.class)
               .addKeepMainRule(Main.class)
               .addKeepRules(checkDiscardRule(checkMembers, annotation))
-              .minification(minify)
+              .addDontObfuscateUnless(minify)
               .addOptionsModification(this::noInlining)
               .compile();
       assertNull(onCompilationFailure);
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java
new file mode 100644
index 0000000..4f44022
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2024, 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.classmerging.horizontal;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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 ConstClassAfterVerticalClassMergingTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableSyntheticSharing;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, synthetic sharing: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addVerticallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .applyIf(
+                        parameters.isDexRuntime(), i -> i.assertMergedIntoSubtype(I.class, J.class))
+                    .assertNoOtherClassesMerged())
+        .setMinApi(parameters)
+        .addOptionsModification(
+            options -> options.getTestingOptions().enableSyntheticSharing = enableSyntheticSharing)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("2");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Map<Class<?>, Object> map = new HashMap<>();
+      map.put(I.class, (I) () -> System.out.println("I"));
+      map.put(J.class, (J) () -> System.out.println("J"));
+      System.out.println(map.size());
+    }
+  }
+
+  interface I {
+
+    void f();
+  }
+
+  interface J {
+
+    void g();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 723e674..627c16f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -855,7 +855,6 @@
         ImmutableSet.of(
             "classmerging.SuperCallToMergedClassIsRewrittenTest",
             "classmerging.A",
-            "classmerging.D",
             "classmerging.F");
 
     JasminBuilder jasminBuilder = new JasminBuilder();
diff --git a/src/test/java/com/android/tools/r8/classpath/SuperclassOfLocalClassesOnClasspathTest.java b/src/test/java/com/android/tools/r8/classpath/SuperclassOfLocalClassesOnClasspathTest.java
new file mode 100644
index 0000000..8f5f324
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classpath/SuperclassOfLocalClassesOnClasspathTest.java
@@ -0,0 +1,138 @@
+// Copyright (c) 2024, 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.classpath;
+
+package com.android.tools.r8.classpath;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.nio.file.Path;
+import org.junit.BeforeClass;
+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 SuperclassOfLocalClassesOnClasspathTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  static Path classpathDex;
+  static Path classpathCf;
+
+  @BeforeClass
+  public static void setUpClasspath() throws Exception {
+    // Build classpath DEX with API level 1 to work with all API levels.
+    classpathDex =
+        testForD8(getStaticTemp())
+            .addProgramClasses(A.class)
+            .setMinApi(AndroidApiLevel.B)
+            .compile()
+            .writeToZip();
+    classpathCf = getStaticTemp().newFile("classpath2.zip").toPath();
+    ZipBuilder.builder(classpathCf)
+        .addFile(
+            DescriptorUtils.getPathFromJavaType(A.class),
+            ToolHelper.getClassFileForTestClass(A.class))
+        .build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addClasspathClasses(A.class)
+        .addInnerClasses(A.class)
+        .addProgramClasses(I.class)
+        .addProgramClasses(TestClass.class)
+        .setMinApi(parameters)
+        .addRunClasspathFiles(classpathDex)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccess();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        // Split A and its inner classes on classpath/program.
+        .addClasspathClasses(A.class)
+        .addInnerClasses(A.class)
+        .addProgramClasses(I.class)
+        .addProgramClasses(TestClass.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules("-keep class " + A.class.getTypeName() + "$* { *; }")
+        .setMinApi(parameters)
+        .addRunClasspathFiles(parameters.isDexRuntime() ? classpathDex : classpathCf)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccess();
+  }
+
+  interface I {
+    void m();
+  }
+
+  static class A {
+    static {
+      // Anonymous local class in <clinit>.
+      new I() {
+        public void m() {}
+      };
+    }
+
+    static {
+      // Named local class in <clinit>.
+      class Local {}
+      new Local();
+    }
+
+    public A() {
+      anonymousLocalClass();
+      namedLocalClass();
+
+      // Anonymous local class in <init>.
+      new I() {
+        public void m() {}
+      };
+
+      // Named local class in <init>.
+      class Local {
+        public void m() {}
+      }
+
+      new Local();
+    }
+
+    private void anonymousLocalClass() {
+      new I() {
+        public void m() {}
+      };
+    }
+
+    private void namedLocalClass() {
+      class Local {
+        public void m() {}
+      }
+      new Local();
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new A();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java b/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
index 97cb7c9..9c7cf17 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
@@ -43,7 +43,7 @@
                     // Add main dex rule to disable Class.forName() optimization.
                     .addMainDexRules("-keep class " + CLASS_NAME)
                     .addDontOptimize()
-                    .noTreeShaking()
+                    .addDontShrink()
                     .setMinApi(AndroidApiLevel.B));
 
     ClassSubject clazz = inspector.clazz(CLASS_NAME);
@@ -79,7 +79,7 @@
                     .addMainDexRules("-keep class " + CLASS_NAME)
                     .addDontOptimize()
                     .addDontObfuscate()
-                    .noTreeShaking()
+                    .addDontShrink()
                     .setMinApi(AndroidApiLevel.B));
 
     ClassSubject clazz = inspector.clazz(CLASS_NAME);
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java b/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java
index 7a10656..11b4f24 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java
@@ -80,7 +80,7 @@
         .addKeepAllAttributes()
         .allowDiagnosticWarningMessages()
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .setMode(CompilationMode.DEBUG)
         .setMinApi(parameters)
         .applyIf(
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
index d6b8dc4..72f67f8 100644
--- a/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
@@ -60,7 +60,7 @@
         .addProgramClassesAndInnerClasses(CLASS)
         .debug()
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .setMinApi(parameters)
         .run(parameters.getRuntime(), CLASS)
         .assertSuccessWithOutput(EXPECTED)
diff --git a/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
index ec4a891..a811ca2 100644
--- a/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
@@ -52,7 +52,7 @@
             .addProgramClasses(LineNumberOptimization1.class, LineNumberOptimization2.class)
             .setMinApi(parameters)
             .setMode(dontOptimizeByEnablingDebug ? CompilationMode.DEBUG : CompilationMode.RELEASE)
-            .noTreeShaking()
+            .addDontShrink()
             .addDontObfuscate()
             .addKeepAttributeSourceFile()
             .addKeepAttributeLineNumberTable()
diff --git a/src/test/java/com/android/tools/r8/debug/LoadInvokeLoadOptimizationTestRunner.java b/src/test/java/com/android/tools/r8/debug/LoadInvokeLoadOptimizationTestRunner.java
index e33be2e..d2e3120 100644
--- a/src/test/java/com/android/tools/r8/debug/LoadInvokeLoadOptimizationTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/LoadInvokeLoadOptimizationTestRunner.java
@@ -58,7 +58,7 @@
   @Test
   public void testR8() throws Throwable {
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addKeepRules("-keepattributes SourceFile,LineNumberTable")
         .addProgramClasses(CLASS)
diff --git a/src/test/java/com/android/tools/r8/debug/LocalsTest.java b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
index add47dc..8b914ad 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalsTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
@@ -48,8 +48,8 @@
             .addProgramClasses(Locals.class)
             .setMinApi(parameters)
             .debug()
-            .treeShaking(false)
-            .minification(false)
+            .addDontShrink()
+            .addDontObfuscate()
             .addKeepAllClassesRule()
             .debugConfig(parameters.getRuntime())
         : testForRuntime(parameters)
diff --git a/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java b/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java
index 6aafde3..c7ee7f1 100644
--- a/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java
+++ b/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java
@@ -45,8 +45,8 @@
             .addProgramClasses(SynchronizedBlock.class)
             .setMinApi(parameters)
             .debug()
-            .treeShaking(false)
-            .minification(false)
+            .addDontShrink()
+            .addDontObfuscate()
             .addKeepAllClassesRule()
             .debugConfig(parameters.getRuntime())
         : testForRuntime(parameters)
diff --git a/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
index f2fa291..6b2e7f0 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
@@ -86,7 +86,7 @@
         .apply(configuration)
         .debug()
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .run(parameters.getRuntime(), className)
         .assertSuccessWithOutput(runInput.stdout);
diff --git a/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountMultilineCodeTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountMultilineCodeTestRunner.java
index 8cd026d..5c35005 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountMultilineCodeTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountMultilineCodeTestRunner.java
@@ -43,7 +43,7 @@
         .addProgramClasses(CLASS)
         .addKeepMainRule(CLASS)
         // Keep all the methods but allow renaming.
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepAttributeLineNumberTable()
         .addKeepAttributeSourceFile()
         .addKeepRules("-renamesourcefileattribute " + (customSourceFile ? "X" : "SourceFile"))
diff --git a/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountSingleLineCodeTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountSingleLineCodeTestRunner.java
index 6c31cf2..7a1bd68 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountSingleLineCodeTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/pc2pc/DifferentParameterCountSingleLineCodeTestRunner.java
@@ -43,7 +43,7 @@
         .addProgramClasses(CLASS)
         .addKeepMainRule(CLASS)
         // Keep all the methods but allow renaming.
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepAttributeLineNumberTable()
         .addKeepAttributeSourceFile()
         .addKeepRules("-renamesourcefileattribute " + (customSourceFile ? "X" : "SourceFile"))
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
index 42e6975..e9a894d 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
@@ -55,7 +55,7 @@
         testForR8(Backend.CF)
             .addProgramClassesAndInnerClasses(CLASS)
             .addDontObfuscate()
-            .noTreeShaking()
+            .addDontShrink()
             .debug()
             .compile();
     compileResult
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
index ce5f0bd..04f4efe 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
@@ -100,7 +100,7 @@
         .addProgramClassesAndInnerClasses(CLASS)
         .setMinApi(parameters)
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepAllAttributes()
         .debug()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index 9284fa8..4b9b180 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -90,7 +90,7 @@
         .addKeepMainRule(TestClass.class)
         .addKeepClassAndMembersRules(MiniAssert.class)
         .setMinApi(parameters)
-        .minification(minify)
+        .addDontObfuscateUnless(minify)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkNoOriginalsAndNoInternalSynthetics);
diff --git a/src/test/java/com/android/tools/r8/desugar/jdk8272564/Jdk8272564Test.java b/src/test/java/com/android/tools/r8/desugar/jdk8272564/Jdk8272564Test.java
index 43853de..52809ee 100644
--- a/src/test/java/com/android/tools/r8/desugar/jdk8272564/Jdk8272564Test.java
+++ b/src/test/java/com/android/tools/r8/desugar/jdk8272564/Jdk8272564Test.java
@@ -155,7 +155,7 @@
     testForR8(parameters.getBackend())
         .addProgramFiles(Jdk8272564.jar())
         .setMinApi(parameters)
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepClassAndMembersRules(Jdk8272564.Main.typeName())
         .run(parameters.getRuntime(), Jdk8272564.Main.typeName())
         .inspect(this::assertJdk8272564NotFixedCodeR8)
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java
index 7c2fab0..1aeff4d 100644
--- a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.StringUtils;
 import java.util.ArrayList;
 import java.util.List;
@@ -85,7 +84,7 @@
             .addKeepMainRule(TestRunner.class)
             .addKeepAttributeSourceFile()
             .addKeepRules("-renamesourcefileattribute SourceFile")
-            .noTreeShaking()
+            .addDontShrink()
             .addDontOptimize()
             .run(parameters.getRuntime(), TestRunner.class, Boolean.toString(isDalvik))
             .assertSuccess()
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java
index a33995b..4d56e6a 100644
--- a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java
@@ -72,7 +72,7 @@
         // Prevent R8 from eliminating the lambdas by keeping the application of them.
         .addKeepClassAndMembersRules(Accept.class)
         .setMinApi(parameters)
-        .minification(minify)
+        .addDontObfuscateUnless(minify)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkNoOriginalsAndNoInternalSynthetics);
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java
index 0581c23..05870c7 100644
--- a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java
@@ -70,7 +70,7 @@
         .addProgramClasses(CLASSES)
         .addKeepMainRule(TestClass.class)
         .setMinApi(parameters)
-        .minification(minify)
+        .addDontObfuscateUnless(minify)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkNoOriginalsAndNoInternalSynthetics);
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java
index 9ac144b..ec66458 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestCompilationExceptionTest.java
@@ -93,7 +93,7 @@
           .addProgramFiles(matchingClasses);
     } else {
       return testForR8(parameters.getBackend())
-          .noTreeShaking()
+          .addDontShrink()
           .addDontObfuscate()
           .addKeepAllAttributes()
           .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
index 7b1065a..9c88c93 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
@@ -81,7 +81,7 @@
         .applyIf(
             parameters.isCfRuntime(),
             testBuilder -> testBuilder.addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp)))
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .applyIf(enableRepackaging, b -> b.addKeepRules("-repackageclasses p"))
         .setMinApi(parameters)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java
index 0c007f2..e8587ca 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java
@@ -67,7 +67,7 @@
         .addProgramClassFileData(PROGRAM_DATA)
         .setMinApi(parameters)
         .addKeepMainRule(MAIN_TYPE)
-        .minification(minifying)
+        .addDontObfuscateUnless(minifying)
         .compile()
         .inspect(inspector -> inspect(inspector, false))
         .run(parameters.getRuntime(), MAIN_TYPE)
@@ -82,7 +82,7 @@
         testForR8(Backend.CF)
             .addProgramClassFileData(PROGRAM_DATA)
             .addKeepMainRule(MAIN_TYPE)
-            .minification(minifying)
+            .addDontObfuscateUnless(minifying)
             .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
             .compile()
             .writeToZip();
@@ -90,7 +90,7 @@
         .addProgramFiles(desugared)
         .setMinApi(parameters)
         .addKeepMainRule(MAIN_TYPE)
-        .minification(minifying)
+        .addDontObfuscateUnless(minifying)
         .compile()
         .inspect(inspector -> inspect(inspector, true))
         .run(parameters.getRuntime(), MAIN_TYPE)
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/Regress148461139.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/Regress148461139.java
index e7219f1..d73eeb6 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/Regress148461139.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/Regress148461139.java
@@ -33,7 +33,7 @@
     testForR8(parameters.getBackend())
         .setMinApi(parameters)
         .addProgramClasses(Condition.class)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .inspect(i -> assertTrue(i.clazz(Condition.class).isAbstract()));
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java
index bd6b65d..df5e669 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java
@@ -61,7 +61,7 @@
             .addKeepRules(enumKeepRules.getKeepRules())
             .enableNeverClassInliningAnnotations()
             .enableInliningAnnotations()
-            .minification(minification)
+            .addDontObfuscateUnless(minification)
             .compile()
             .writeToZip();
     // Compile the app with the lib.
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java
index aa341a7..a5d0de3 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java
@@ -81,7 +81,7 @@
         .addKeepRules(enumKeepRules.getKeepRules())
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .addKeepPackageNamesRule(libClass.getPackage())
         .compile()
         .writeToZip();
diff --git a/src/test/java/com/android/tools/r8/graph/MissingClassThrowingTest.java b/src/test/java/com/android/tools/r8/graph/MissingClassThrowingTest.java
index 333ab92..2a502d7 100644
--- a/src/test/java/com/android/tools/r8/graph/MissingClassThrowingTest.java
+++ b/src/test/java/com/android/tools/r8/graph/MissingClassThrowingTest.java
@@ -69,7 +69,7 @@
                 .addKeepAllClassesRule()
                 .addKeepAllAttributes()
                 .addDontObfuscate()
-                .noTreeShaking()
+                .addDontShrink()
                 .enableInliningAnnotations()
                 .enableNoHorizontalClassMergingAnnotations()
                 .debug()
diff --git a/src/test/java/com/android/tools/r8/internal/Regression127524985.java b/src/test/java/com/android/tools/r8/internal/Regression127524985.java
index 8bfef9e..c9e61ed 100644
--- a/src/test/java/com/android/tools/r8/internal/Regression127524985.java
+++ b/src/test/java/com/android/tools/r8/internal/Regression127524985.java
@@ -68,7 +68,7 @@
     assumeTrue(parameters.isCfRuntime());
     testForR8(parameters.getBackend())
         .debug()
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addKeepAllAttributes()
         .addKeepRules("-dontwarn")
diff --git a/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java b/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
index 05d0d8b..b11215a 100644
--- a/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
+++ b/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
@@ -50,6 +50,7 @@
   public void testR8() throws Exception {
     testForR8(Backend.DEX)
         .addProgramFiles(outDirectory.resolve("program.jar"))
+        .addOptionsModification(options -> options.getTestingOptions().allowCodeReplacement = false)
         .apply(this::configure)
         .compile();
   }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
index 21314c0..375c5bc 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
@@ -68,7 +68,7 @@
         .allowUnusedDontWarnPatterns()
         .allowUnusedProguardConfigurationRules()
         .enableProtoShrinking()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .compile()
         .assertAllInfoMessagesMatch(
@@ -120,7 +120,7 @@
         .allowUnusedDontWarnPatterns()
         .allowUnusedProguardConfigurationRules()
         .enableProtoShrinking()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .compile()
         .assertAllInfoMessagesMatch(
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTest.java
index e9181de..71c43bf 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTest.java
@@ -110,7 +110,7 @@
             .allowUnusedProguardConfigurationRules()
             .enableProguardTestOptions()
             .enableProtoShrinking()
-            .minification(enableMinification)
+            .addDontObfuscateUnless(enableMinification)
             .setMinApi(parameters)
             .compile()
             .assertAllInfoMessagesMatch(
@@ -425,7 +425,7 @@
         .allowUnusedDontWarnPatterns()
         .allowUnusedProguardConfigurationRules()
         .enableProtoShrinking()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .compile()
         .assertAllInfoMessagesMatch(
@@ -454,7 +454,7 @@
         .allowUnusedDontWarnPatterns()
         .allowUnusedProguardConfigurationRules()
         .enableProtoShrinking()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .compile()
         .assertAllInfoMessagesMatch(
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/constant/B132897042.java b/src/test/java/com/android/tools/r8/ir/analysis/constant/B132897042.java
index 253612a..3415ebc 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/constant/B132897042.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/constant/B132897042.java
@@ -35,7 +35,7 @@
                 "-assumevalues class" + LibClass.class.getName() + " {",
                 "  static int SDK_INT return 1..28;",
                 "}"))
-        .noTreeShaking()
+        .addDontShrink()
         .setMinApi(parameters)
         .compile()
         .assertNoMessages();
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java b/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java
new file mode 100644
index 0000000..56573a5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2024, 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.analysis.path;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult.SuccessfulDataflowAnalysisResult;
+import com.android.tools.r8.ir.analysis.path.state.ConcretePathConstraintAnalysisState;
+import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
+import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeUnopCompareNode;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 PathConstraintAnalysisUnitTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    AndroidApp app =
+        AndroidApp.builder()
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(Main.class))
+            .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+            .build();
+    AppView<AppInfoWithLiveness> appView = computeAppViewWithLiveness(app);
+    CodeInspector inspector = new CodeInspector(app);
+    IRCode code =
+        inspector.clazz(Main.class).uniqueMethodWithOriginalName("greet").buildIR(appView);
+    PathConstraintAnalysis analysis = new PathConstraintAnalysis(appView, code);
+    DataflowAnalysisResult result = analysis.run(code.entryBlock());
+    assertTrue(result.isSuccessfulAnalysisResult());
+    SuccessfulDataflowAnalysisResult<BasicBlock, PathConstraintAnalysisState> successfulResult =
+        result.asSuccessfulAnalysisResult();
+
+    // Inspect ENTRY state.
+    PathConstraintAnalysisState entryConstraint =
+        successfulResult.getBlockExitState(code.entryBlock());
+    assertTrue(entryConstraint.isBottom());
+
+    // Inspect THEN state.
+    PathConstraintAnalysisState thenConstraint =
+        successfulResult.getBlockExitState(code.entryBlock().exit().asIf().getTrueTarget());
+    assertTrue(thenConstraint.isConcrete());
+
+    ConcretePathConstraintAnalysisState concreteThenConstraint = thenConstraint.asConcreteState();
+    assertEquals(1, concreteThenConstraint.getPathConstraints().size());
+    assertEquals(0, concreteThenConstraint.getNegatedPathConstraints().size());
+
+    ComputationTreeNode thenPathConstraint =
+        concreteThenConstraint.getPathConstraints().iterator().next();
+    assertTrue(thenPathConstraint instanceof ComputationTreeUnopCompareNode);
+
+    // Inspect ELSE state.
+    PathConstraintAnalysisState elseConstraint =
+        successfulResult.getBlockExitState(code.entryBlock().exit().asIf().fallthroughBlock());
+    assertTrue(elseConstraint.isConcrete());
+
+    ConcretePathConstraintAnalysisState concreteElseConstraint = elseConstraint.asConcreteState();
+    assertEquals(0, concreteElseConstraint.getPathConstraints().size());
+    assertEquals(1, concreteElseConstraint.getNegatedPathConstraints().size());
+
+    ComputationTreeNode elsePathConstraint =
+        concreteElseConstraint.getNegatedPathConstraints().iterator().next();
+    assertEquals(thenPathConstraint, elsePathConstraint);
+
+    // Inspect RETURN state.
+    PathConstraintAnalysisState returnConstraint =
+        successfulResult.getBlockExitState(code.computeNormalExitBlocks().get(0));
+    assertTrue(returnConstraint.isConcrete());
+
+    ConcretePathConstraintAnalysisState concreteReturnConstraint =
+        returnConstraint.asConcreteState();
+    assertEquals(1, concreteReturnConstraint.getPathConstraints().size());
+    assertEquals(
+        concreteReturnConstraint.getPathConstraints(),
+        concreteReturnConstraint.getNegatedPathConstraints());
+  }
+
+  static class Main {
+
+    public static void greet(String greeting, int flags) {
+      if ((flags & 1) != 0) {
+        greeting = "Hello, world!";
+      }
+      System.out.println(greeting);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/AllowCodeReplacementTest.java b/src/test/java/com/android/tools/r8/ir/optimize/AllowCodeReplacementTest.java
new file mode 100644
index 0000000..0180424
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/AllowCodeReplacementTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
+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.assertTrue;
+
+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 com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 AllowCodeReplacementTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withMinimumApiLevel().build();
+  }
+
+  @Test
+  public void testFeatureDisabledByDefault() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keepclassmembers,allowshrinking class " + Main.class.getTypeName() + " {",
+            "  boolean alwaysFalse();",
+            "}")
+        .addOptionsModification(options -> assertTrue(options.testing.allowCodeReplacement))
+        .setMinApi(parameters)
+        .compile()
+        .inspect(inspector -> inspect(inspector, false))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Test
+  public void testAnalyzeKeptMethod() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keepclassmembers,allowshrinking class " + Main.class.getTypeName() + " {",
+            "  boolean alwaysFalse();",
+            "}")
+        .addOptionsModification(options -> options.testing.allowCodeReplacement = false)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(inspector -> inspect(inspector, true))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Test
+  public void testKeepForCodeReplacement() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keepclassmembers,allowcodereplacement,allowshrinking class "
+                + Main.class.getTypeName()
+                + " {",
+            "  boolean alwaysFalse();",
+            "}")
+        .addOptionsModification(options -> options.testing.allowCodeReplacement = false)
+        .enableProguardTestOptions()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(inspector -> inspect(inspector, false))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private void inspect(CodeInspector inspector, boolean isOptimized) {
+    ClassSubject mainClassSubject = inspector.clazz(Main.class);
+    assertThat(mainClassSubject, isPresent());
+
+    MethodSubject mainMethodSubject = mainClassSubject.mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertEquals(
+        isOptimized,
+        inspector
+            .clazz(Main.class)
+            .mainMethod()
+            .streamInstructions()
+            .noneMatch(i -> i.isConstString("Unreachable")));
+
+    MethodSubject alwaysFalseMethodSubject =
+        mainClassSubject.uniqueMethodWithOriginalName("alwaysFalse");
+    assertThat(alwaysFalseMethodSubject, isAbsentIf(isOptimized));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      if (alwaysFalse()) {
+        System.out.println("Unreachable");
+      } else {
+        System.out.println("Hello, world!");
+      }
+    }
+
+    static boolean alwaysFalse() {
+      return false;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationWithDefaultArgumentTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationWithDefaultArgumentTest.java
new file mode 100644
index 0000000..9d571ee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationWithDefaultArgumentTest.java
@@ -0,0 +1,154 @@
+// Copyright (c) 2024, 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.callsites;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.isInvokeWithTarget;
+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.assertTrue;
+
+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 com.android.tools.r8.utils.codeinspector.ClassSubject;
+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 RestartLambdaPropagationWithDefaultArgumentTest extends TestBase {
+
+  // Deliberately setting the highest bit in this mask to be able to distinguish it from the int 2.
+  private static final int FLAGS = 0b10 | (1 << 31);
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimesAndAllApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              ClassSubject lambdaClassSubject =
+                  inspector.clazz(SyntheticItemsTestUtils.syntheticLambdaClass(Main.class, 0));
+              assertThat(lambdaClassSubject, isPresent());
+              // TODO(b/302281503): Lambda should not capture the two constant string arguments.
+              assertEquals(2, lambdaClassSubject.allInstanceFields().size());
+
+              MethodSubject lambdaInitSubject = lambdaClassSubject.uniqueInstanceInitializer();
+              assertThat(lambdaInitSubject, isPresent());
+              // TODO(b/302281503): Lambda should not capture the two constant string arguments.
+              assertEquals(2, lambdaInitSubject.getParameters().size());
+              assertTrue(lambdaInitSubject.getParameter(0).is(String.class));
+              assertTrue(lambdaInitSubject.getParameter(1).is(String.class));
+
+              MethodSubject mainMethodSubject = mainClassSubject.mainMethod();
+              assertThat(mainMethodSubject, isPresent());
+              // TODO(b/302281503): This argument should be removed as a result of constant
+              //  propagation into the restartableMethod.
+              assertTrue(
+                  mainMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("DefaultValueNeverUsed")));
+              // TODO(b/302281503): This argument is never used and should be removed.
+              assertTrue(
+                  mainMethodSubject
+                      .streamInstructions()
+                      .anyMatch(
+                          instruction ->
+                              instruction.isConstString("Unused[DefaultValueAlwaysUsed]")));
+
+              MethodSubject restartableMethodSubject =
+                  mainClassSubject.uniqueMethodWithOriginalName("restartableMethod");
+              assertThat(restartableMethodSubject, isPresent());
+              assertTrue(
+                  restartableMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstNumber(FLAGS)));
+              assertTrue(
+                  restartableMethodSubject
+                      .streamInstructions()
+                      .noneMatch(
+                          instruction ->
+                              instruction.isConstString("Unused[DefaultValueNeverUsed]")));
+              assertTrue(
+                  restartableMethodSubject
+                      .streamInstructions()
+                      .anyMatch(
+                          instruction -> instruction.isConstString("DefaultValueAlwaysUsed")));
+              assertTrue(
+                  restartableMethodSubject
+                      .streamInstructions()
+                      .anyMatch(
+                          instruction -> isInvokeWithTarget(lambdaInitSubject).test(instruction)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "Postponing!",
+            "Restarting!",
+            "DefaultValueNeverUsed",
+            "DefaultValueAlwaysUsed",
+            Integer.toString(FLAGS),
+            "Stopping!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Runnable restarter =
+          restartableMethod("DefaultValueNeverUsed", "Unused[DefaultValueAlwaysUsed]", FLAGS, true);
+      restarter.run();
+    }
+
+    @NeverInline
+    static Runnable restartableMethod(
+        String defaultValueNeverUsed, String defaultValueAlwaysUsed, int flags, boolean doRestart) {
+      if ((flags & 1) != 0) {
+        defaultValueNeverUsed = "Unused[DefaultValueNeverUsed]";
+      }
+      if ((flags & 2) != 0) {
+        defaultValueAlwaysUsed = "DefaultValueAlwaysUsed";
+      }
+      if (doRestart) {
+        System.out.println("Postponing!");
+        String finalDefaultValueNeverUsed = defaultValueNeverUsed;
+        String finalDefaultValueAlwaysUsed = defaultValueAlwaysUsed;
+        return () -> {
+          System.out.println("Restarting!");
+          Runnable restarter =
+              restartableMethod(
+                  finalDefaultValueNeverUsed, finalDefaultValueAlwaysUsed, flags, false);
+          if (restarter == null) {
+            System.out.println("Stopping!");
+          } else {
+            throw new RuntimeException();
+          }
+        };
+      }
+      System.out.println(defaultValueNeverUsed);
+      System.out.println(defaultValueAlwaysUsed);
+      System.out.println(flags);
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java
index a8bd35a..372423d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java
@@ -113,7 +113,7 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(MAIN, CanonicalizationTestClass.class)
             .addKeepMainRule(MAIN)
-            .minification(enableMinification)
+            .addDontObfuscateUnless(enableMinification)
             .setMinApi(parameters)
             .addOptionsModification(this::configure)
             .run(parameters.getRuntime(), MAIN)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CastInDeadCodeafterInstanceOfOptimizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CastInDeadCodeafterInstanceOfOptimizationTest.java
new file mode 100644
index 0000000..c690efd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CastInDeadCodeafterInstanceOfOptimizationTest.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2024, 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.checkcast;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+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;
+
+// This is a regression test for b/358913905.
+@RunWith(Parameterized.class)
+public class CastInDeadCodeafterInstanceOfOptimizationTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      Object object = new Object();
+      if (object instanceof int[]) {
+        int[] object2 = (int[]) object;
+      }
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java
index 9887c3f..6d45501 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java
@@ -44,7 +44,7 @@
         .addKeepMainRule(TestClass.class)
         .setMinApi(parameters)
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
index 59e609d..3bd367b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -25,13 +25,17 @@
 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 SingleTargetAfterInliningTest extends TestBase {
 
-  private final int maxInliningDepth;
-  private final TestParameters parameters;
+  @Parameter(0)
+  public int maxInliningDepth;
+
+  @Parameter(1)
+  public TestParameters parameters;
 
   @Parameters(name = "{1}, max inlining depth: {0}")
   public static List<Object[]> data() {
@@ -39,11 +43,6 @@
         ImmutableList.of(0, 1), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
-  public SingleTargetAfterInliningTest(int maxInliningDepth, TestParameters parameters) {
-    this.maxInliningDepth = maxInliningDepth;
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithUnknownDynamicTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithUnknownDynamicTypeTest.java
new file mode 100644
index 0000000..0a5e1ab
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithUnknownDynamicTypeTest.java
@@ -0,0 +1,103 @@
+// Copyright (c) 2024, 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.NeverClassInline;
+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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+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 DefaultFieldValueJoinerWithUnknownDynamicTypeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keepclassmembers class " + Main.class.getTypeName() + "{",
+            "  *** emptyList();",
+            "  *** emptySet(boolean);",
+            "}")
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @NeverClassInline
+  static class Main {
+
+    static final Collection<Object> DEFAULT = emptyList();
+
+    Collection<Object> cache;
+
+    public static void main(String[] args) {
+      Main main = new Main();
+      Collection<Object> emptySet = main.test(false);
+      Collection<Object> emptyList = main.test(true);
+      checkNotIdentical(emptySet, emptyList);
+      Collection<Object> cachedEmptyList = main.test(true);
+      checkIdentical(emptyList, cachedEmptyList);
+    }
+
+    @NeverInline
+    Collection<Object> test(boolean doThrow) {
+      if (cache != null) {
+        return cache;
+      }
+      try {
+        return emptySet(doThrow);
+      } catch (Throwable t) {
+        cache = DEFAULT;
+        return cache;
+      }
+    }
+
+    // @Keep
+    static Collection<Object> emptyList() {
+      return new ArrayList<>();
+    }
+
+    // @Keep
+    static Collection<Object> emptySet(boolean doThrow) {
+      if (doThrow) {
+        throw new RuntimeException();
+      }
+      return Collections.emptySet();
+    }
+
+    static void checkIdentical(Object o1, Object o2) {
+      if (o1 != o2) {
+        throw new RuntimeException();
+      }
+    }
+
+    static void checkNotIdentical(Object o1, Object o2) {
+      if (o1 == o2) {
+        throw new RuntimeException();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldWriteBeforeFieldReadTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldWriteBeforeFieldReadTest.java
index a603d83..4a681ad 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldWriteBeforeFieldReadTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldWriteBeforeFieldReadTest.java
@@ -24,22 +24,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 FieldWriteBeforeFieldReadTest extends TestBase {
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public FieldWriteBeforeFieldReadTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameInterfaceTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameInterfaceTest.java
index 817463a..1c3626f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameInterfaceTest.java
@@ -50,7 +50,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(ForNameInterfaceTest.class)
         .addKeepMainRule(Main.class)
-        .minification(false)
+        .addDontObfuscate()
         .setMinApi(parameters)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java
index 45b83f4..d4c23d5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameInClassInitializerTest.java
@@ -67,7 +67,7 @@
         .enableInliningAnnotations()
         .addKeepMainRule(MAIN)
         .addKeepRules("-keep class **.GetNameClinit*")
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .addOptionsModification(this::configure)
         .run(parameters.getRuntime(), MAIN)
@@ -83,7 +83,7 @@
             .enableInliningAnnotations()
             .addKeepMainRule(MAIN)
             .addKeepRules("-keep,allowobfuscation class **.GetNameClinit*")
-            .minification(enableMinification)
+            .addDontObfuscateUnless(enableMinification)
             .setMinApi(parameters)
             .addOptionsModification(this::configure)
             .compile();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
index e322f8d..dbee197 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
@@ -271,7 +271,7 @@
             .addKeepRules("-keep class **.GetName0*")
             .addKeepRules("-keepattributes InnerClasses,EnclosingMethod")
             .addKeepRules("-printmapping " + createNewMappingPath().toAbsolutePath().toString())
-            .minification(enableMinification)
+            .addDontObfuscateUnless(enableMinification)
             .setMinApi(parameters)
             .addOptionsModification(this::configure)
             .compile()
@@ -298,7 +298,7 @@
             .addKeepRules("-keep,allowobfuscation class **.GetName0*")
             .addKeepRules("-keepattributes InnerClasses,EnclosingMethod")
             .addKeepRules("-printmapping " + createNewMappingPath().toAbsolutePath().toString())
-            .minification(enableMinification)
+            .addDontObfuscateUnless(enableMinification)
             .setMinApi(parameters)
             .addOptionsModification(this::configure)
             .compile()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
index 533c75f..aa4ae93 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
@@ -206,7 +206,7 @@
         .enableInliningAnnotations()
         .addKeepAllClassesRule()
         .addKeepAttributeInnerClassesAndEnclosingMethod()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .addOptionsModification(this::configure)
         .run(parameters.getRuntime(), MAIN)
@@ -224,7 +224,7 @@
         .addKeepRules("-keep class **.ClassGetSimpleName*")
         .addKeepRules("-keep class **.Outer*")
         .addKeepAttributeInnerClassesAndEnclosingMethod()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .addOptionsModification(this::configure)
         .run(parameters.getRuntime(), MAIN)
@@ -245,7 +245,7 @@
         // then use OUTPUT_WITH_SHRUNK_ATTRIBUTES
         .addKeepRules("-keep,allowobfuscation class **.Outer*")
         .addKeepAttributeInnerClassesAndEnclosingMethod()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .addOptionsModification(this::configure)
         .run(parameters.getRuntime(), MAIN)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java
index e88fb06..61ea74d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java
@@ -229,7 +229,7 @@
             .addKeepAttributeInnerClassesAndEnclosingMethod()
             .addProgramClassFileData(InnerClassNameTestDump.dump(config, parameters))
             .allowDiagnosticInfoMessages(hasMalformedInnerClassAttribute())
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .addOptionsModification(
                 options -> {
                   options.disableInnerClassSeparatorValidationWhenRepackaging = true;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java
index eff87f7..d45d543 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java
@@ -67,7 +67,7 @@
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .enableNoMethodStaticizingAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .allowAccessModification(allowAccessModification)
         .applyIf(
             allowAccessModification,
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
index e5c56c7..798002a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
@@ -72,7 +72,7 @@
         .enableNoParameterReorderingAnnotations()
         .enableUnusedArgumentAnnotations()
         // TODO(b/123060011): Mapping not working in presence of argument removal.
-        .minification(keepUninstantiatedArguments)
+        .addDontObfuscateUnless(keepUninstantiatedArguments)
         .setMinApi(parameters)
         .compile()
         .inspect(this::verifyOutput)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/CollisionWithLibraryMethodsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/CollisionWithLibraryMethodsTest.java
index 44fd6bc..3f2cd87 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/CollisionWithLibraryMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/CollisionWithLibraryMethodsTest.java
@@ -53,7 +53,7 @@
         .enableNeverClassInliningAnnotations()
         .enableNoMethodStaticizingAnnotations()
         .enableInliningAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(parameters)
         .compile()
         .inspect(this::verify)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java
index ccfb422..c55bb32 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/PrivateInstanceMethodCollisionTest.java
@@ -67,7 +67,7 @@
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoMethodStaticizingAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .allowAccessModification(allowAccessModification)
         .applyIf(
             allowAccessModification,
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
index 764d4f8..c4cef26 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
@@ -70,7 +70,7 @@
         .enableConstantArgumentAnnotations()
         .enableUnusedArgumentAnnotations(keepUnusedArguments)
         // TODO(b/123060011): Mapping not working in presence of unused argument removal.
-        .minification(keepUnusedArguments)
+        .addDontObfuscateUnless(keepUnusedArguments)
         .setMinApi(parameters)
         .compile()
         .inspect(this::verifyOutput)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java
index 9a844ac..b07f468 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java
@@ -50,7 +50,7 @@
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(parameters)
         .compile()
         .inspect(this::verify)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java
index d772b8f..ba233be 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsCollisionTest.java
@@ -62,7 +62,7 @@
         .enableNeverClassInliningAnnotations()
         .enableNoMethodStaticizingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(parameters)
         .compile()
         .inspect(this::verifyUnusedArgumentsRemovedAndNoCollisions)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsTestBase.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsTestBase.java
index 49efe19..2cd91b2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsTestBase.java
@@ -75,7 +75,7 @@
         .addProgramClasses(getTestClass())
         .addProgramClasses(getAdditionalClasses())
         .addKeepMainRule(getTestClass())
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .addOptionsModification(options -> options.enableSideEffectAnalysis = false)
         .apply(this::configure)
         .run(parameters.getRuntime(), getTestClass())
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index 9dc6503..ed272e7 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -211,15 +211,6 @@
     }
 
     @Override
-    public BasicBlock addThrowingInstructionToPossiblyThrowingBlock(
-        IRCode code,
-        ListIterator<BasicBlock> blockIterator,
-        Instruction instruction,
-        InternalOptions options) {
-      throw new Unimplemented();
-    }
-
-    @Override
     public BasicBlock split(
         IRCode code, ListIterator<BasicBlock> blockIterator, boolean keepCatchHandlers) {
       throw new Unimplemented();
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
index ede0d93..52ee123 100644
--- a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -67,31 +67,29 @@
 
   @Test
   public void testAsIs() throws Exception {
-    test(builder -> builder.addDontObfuscate().addDontOptimize().noTreeShaking());
+    test(builder -> builder.addDontObfuscate().addDontOptimize().addDontShrink());
   }
 
   @Test
   public void testDontShrinkAndDontOptimize() throws Exception {
-    test(builder -> builder.addDontOptimize().noTreeShaking());
+    test(builder -> builder.addDontOptimize().addDontShrink());
   }
 
   @Test
   public void testDontShrinkAndDontObfuscate() throws Exception {
-    test(builder -> builder.addDontObfuscate().noTreeShaking());
+    test(builder -> builder.addDontObfuscate().addDontShrink());
   }
 
   @Test
   public void testDontShrink() throws Exception {
-    test(TestShrinkerBuilder::noTreeShaking);
+    test(TestShrinkerBuilder::addDontShrink);
   }
 
   @Test
   public void testDontShrinkDifferently() throws Exception {
     test(
         builder ->
-            builder
-                .addKeepRules("-keep,allowobfuscation class **.*KClasses*")
-                .noTreeShaking());
+            builder.addKeepRules("-keep,allowobfuscation class **.*KClasses*").addDontShrink());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
index 5bc57ba..4de0f57 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -99,7 +99,7 @@
     Path mainDexListOutput = temp.getRoot().toPath().resolve("main-dex-output.txt");
     testForR8(Backend.DEX)
         .addProgramClasses(HelloWorldMain.class)
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .setMinApi(AndroidApiLevel.K)
         .addMainDexRuleFiles(mainDexRules)
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index 19db97f..9eea1c5 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -396,7 +396,7 @@
         .assumeAllMethodsMayHaveSideEffects()
         .setMinApi(minSdk)
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .addDontOptimize()
         .setMainDexListConsumer(ToolHelper.consumeString(r8MainDexListOutput::set))
         .allowDiagnosticMessages()
diff --git a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
index 3f1fb69..db968c1 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
@@ -54,7 +54,7 @@
         .setMinApi(AndroidApiLevel.K)
         .addMainDexRules(keepMainProguardConfiguration(HelloWorldMain.class))
         .addMainDexRules(checkDiscardRule)
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .compile();
   }
diff --git a/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java b/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
index 0aafb8d..c8a267a 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
@@ -85,7 +85,7 @@
   public void runTestWithR8(GraphConsumer consumer, String rule) throws Exception {
     R8FullTestBuilder builder =
         testForR8(Backend.DEX)
-            .noTreeShaking()
+            .addDontShrink()
             .addDontObfuscate()
             .setMinApi(AndroidApiLevel.K)
             .addProgramClasses(CLASSES)
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
index 6ed1857..15d91c2 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
@@ -6,32 +6,23 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.TestDescriptionWatcher;
+import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldAccessInstructionSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
-import java.io.File;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import org.junit.Assume;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -49,7 +40,7 @@
   private final Backend backend;
   private final String keepRuleFile;
   private final Path programFile;
-  private final Consumer<CodeInspector> inspection;
+  private final ThrowingConsumer<CodeInspector, Exception> inspection;
   private final int minApiLevel;
 
   @Rule
@@ -64,34 +55,6 @@
     this.minApiLevel = configuration.getMinApiLevel();
   }
 
-  @Before
-  public void runR8() throws Exception {
-    // Generate R8 processed version without library option.
-    String out = temp.getRoot().getCanonicalPath();
-    // NOTE: It is important to turn off inlining to ensure
-    // dex inspection of invokes is predictable.
-    R8Command.Builder builder =
-        R8Command.builder()
-            .setOutput(Paths.get(out), TestBase.outputMode(backend))
-            .addLibraryFiles(JAR_LIBRARY, TestBase.runtimeJar(backend))
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true);
-    if (backend == Backend.DEX) {
-      builder.setMinApiLevel(minApiLevel);
-    }
-    if (keepRuleFile != null) {
-      builder.addProguardConfigurationFiles(Paths.get(ToolHelper.EXAMPLES_DIR, name, keepRuleFile));
-      ToolHelper.allowTestProguardOptions(builder);
-    }
-    ToolHelper.getAppBuilder(builder).addProgramFiles(programFile);
-    ToolHelper.runR8(
-        builder.build(),
-        options -> {
-          options.inlinerOptions().enableInlining = false;
-          options.enableRedundantFieldLoadElimination = false;
-        });
-  }
-
   private static boolean coolInvokes(InstructionSubject instruction) {
     if (!instruction.isInvokeVirtual() && !instruction.isInvokeInterface() &&
         !instruction.isInvokeStatic()) {
@@ -185,14 +148,14 @@
     final Backend backend;
     final String keepRuleFile;
     final AndroidVersion version;
-    final Consumer<CodeInspector> processedInspection;
+    final ThrowingConsumer<CodeInspector, Exception> processedInspection;
 
     private TestConfiguration(
         String name,
         Backend backend,
         String keepRuleFile,
         AndroidVersion version,
-        Consumer<CodeInspector> processedInspection) {
+        ThrowingConsumer<CodeInspector, Exception> processedInspection) {
       this.name = name;
       this.backend = backend;
       this.keepRuleFile = keepRuleFile;
@@ -206,14 +169,10 @@
         Backend backend,
         String keepRuleFile,
         AndroidVersion version,
-        Consumer<CodeInspector> processedInspection) {
+        ThrowingConsumer<CodeInspector, Exception> processedInspection) {
       builder.add(new TestConfiguration(name, backend, keepRuleFile, version, processedInspection));
     }
 
-    public Path getDexPath() {
-      return getBuildPath().resolve(name).resolve("classes.dex");
-    }
-
     public Path getJarPath() {
       return getBuildPath().resolve(name + ".jar");
     }
@@ -267,7 +226,7 @@
           builder,
           "memberrebinding3",
           backend,
-          null,
+          "keep-rules.txt",
           TestConfiguration.AndroidVersion.PRE_N,
           MemberRebindingTest::inspect3);
       TestConfiguration.add(
@@ -282,23 +241,24 @@
   }
 
   @Test
-  public void memberRebindingTest() throws IOException {
-    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
-
-    Path out = Paths.get(temp.getRoot().getCanonicalPath());
-    List<Path> processed;
-    if (backend == Backend.DEX) {
-      processed = Collections.singletonList(out.resolve("classes.dex"));
-    } else {
-      assert backend == Backend.CF;
-      processed =
-          Arrays.stream(out.resolve(name).toFile().listFiles(f -> f.toString().endsWith(".class")))
-              .map(File::toPath)
-              .collect(Collectors.toList());
-    }
-
-    CodeInspector inspector = new CodeInspector(processed);
-    inspection.accept(inspector);
-
+  public void memberRebindingTest() throws Exception {
+    testForR8(backend)
+        .addProgramFiles(programFile)
+        .addClasspathFiles(JAR_LIBRARY)
+        .applyIf(
+            keepRuleFile != null,
+            b -> b.addKeepRuleFiles(Paths.get(ToolHelper.EXAMPLES_DIR, name, keepRuleFile)))
+        .applyIf(backend.isDex(), b -> b.setMinApi(minApiLevel))
+        .addDontObfuscate()
+        .addDontShrink()
+        .addKeepRules("-neverpropagatevalue class * { *; }")
+        .addOptionsModification(
+            options -> {
+              options.enableRedundantFieldLoadElimination = false;
+              options.inlinerOptions().enableInlining = false;
+            })
+        .enableProguardTestOptions()
+        .compile()
+        .inspect(inspection);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromThrowsClauseWithNoShrinkingTest.java b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromThrowsClauseWithNoShrinkingTest.java
index 7542952..8cece91 100644
--- a/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromThrowsClauseWithNoShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/missingclasses/MissingClassReferencedFromThrowsClauseWithNoShrinkingTest.java
@@ -51,7 +51,7 @@
                   .transform());
           b.enableInliningAnnotations();
           b.addKeepClassAndMembersRules(DescriptorUtils.descriptorToJavaType(NEW_A_DESCRIPTOR));
-          b.noTreeShaking();
+          b.addDontShrink();
         });
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/AvoidRTest.java b/src/test/java/com/android/tools/r8/naming/AvoidRTest.java
index 4a35018..38e2e99 100644
--- a/src/test/java/com/android/tools/r8/naming/AvoidRTest.java
+++ b/src/test/java/com/android/tools/r8/naming/AvoidRTest.java
@@ -61,7 +61,7 @@
     builder.addProgramClassFileData(jasminBuilder.buildClasses());
     Set<String> usedDescriptors = new HashSet<>();
     builder
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepRules("-classobfuscationdictionary " + dictionary)
         .compile()
         .inspect(
@@ -89,7 +89,7 @@
     builder.addProgramClassFileData(jasminBuilder.buildClasses());
     Set<String> usedNames = new HashSet<>();
     builder
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .inspect(
             codeInspector -> {
@@ -122,7 +122,7 @@
     builder.addProgramClassFileData(jasminBuilder.buildClasses());
     Set<String> usedDescriptors = new HashSet<>();
     builder
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepRules(keepRule)
         .compile()
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java b/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java
index 5d11b24..9c28fa1 100644
--- a/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeTest.java
@@ -63,7 +63,7 @@
             .addKeepMainRule("package.TestClass")
             .addKeepRules("-keepconstantarguments class * { *; }")
             .enableProguardTestOptions()
-            .noTreeShaking()
+            .addDontShrink()
             .compile()
             .inspector();
 
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java b/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
index 8bca773..af2865a 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
@@ -59,7 +59,7 @@
             .addKeepMainRule(MAIN_CLASS_NAME)
             .addKeepClassRulesWithAllowObfuscation(ENUM_CLASS_NAME)
             .allowDiagnosticWarningMessages()
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .assertAllWarningMessagesMatch(
diff --git a/src/test/java/com/android/tools/r8/naming/FieldMinificationObfuscationDictionaryDuplicateTest.java b/src/test/java/com/android/tools/r8/naming/FieldMinificationObfuscationDictionaryDuplicateTest.java
index 60208a1c..28a0ba9 100644
--- a/src/test/java/com/android/tools/r8/naming/FieldMinificationObfuscationDictionaryDuplicateTest.java
+++ b/src/test/java/com/android/tools/r8/naming/FieldMinificationObfuscationDictionaryDuplicateTest.java
@@ -45,7 +45,7 @@
     FileUtils.writeTextFile(dictionary, "a");
     testForR8(parameters.getBackend())
         .addProgramClasses(A.class)
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepRules("-obfuscationdictionary " + dictionary.toString())
         .addKeepMainRule(A.class)
         .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
index b89cc49..0c52a0e 100644
--- a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
@@ -102,7 +102,7 @@
                 "}")
             .allowDiagnosticWarningMessages()
             .enableProguardTestOptions()
-            .minification(minification)
+            .addDontObfuscateUnless(minification)
             .compile()
             .assertAllWarningMessagesMatch(
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."));
@@ -172,7 +172,7 @@
                     "}"))
             .addNeverSingleCallerInlineAnnotations()
             .allowDiagnosticWarningMessages()
-            .minification(minification)
+            .addDontObfuscateUnless(minification)
             .compile()
             .assertAllWarningMessagesMatch(
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
diff --git a/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java b/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java
index fd8b808..b6c69c2 100644
--- a/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java
+++ b/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java
@@ -54,7 +54,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(MinificationMixedCaseAndNumbersTest.class)
         .addKeepMainRule(Main.class)
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepRules(
             "-dontusemixedcaseclassnames", "-keeppackagenames com.android.tools.r8.naming")
         .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java b/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java
index a3936e0..a769120 100644
--- a/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java
+++ b/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java
@@ -46,7 +46,7 @@
     FileUtils.writeTextFile(dictionary, "a");
     testForR8(parameters.getBackend())
         .addProgramClassesAndInnerClasses(Top.class, A.class, C.class)
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepRules("-packageobfuscationdictionary " + dictionary.toString())
         .addKeepMainRule(C.class)
         .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/naming/PackagePrivateOverrideSameMethodNameTest.java b/src/test/java/com/android/tools/r8/naming/PackagePrivateOverrideSameMethodNameTest.java
index a5687f1..aa311d4 100644
--- a/src/test/java/com/android/tools/r8/naming/PackagePrivateOverrideSameMethodNameTest.java
+++ b/src/test/java/com/android/tools/r8/naming/PackagePrivateOverrideSameMethodNameTest.java
@@ -63,7 +63,7 @@
             .setMinApi(parameters)
             .enableInliningAnnotations()
             .enableNeverClassInliningAnnotations()
-            .minification(minification)
+            .addDontObfuscateUnless(minification)
             .run(parameters.getRuntime(), Main.class)
             .apply(this::assertSuccessOutput);
     if (parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik()) {
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterDevirtualizationTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterDevirtualizationTest.java
index 1219836..319c4e8 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterDevirtualizationTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterDevirtualizationTest.java
@@ -125,7 +125,7 @@
     assertThat(inspector.clazz(LibInterfaceB.class), not(isPresent()));
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(libraryResult.getProguardMap())
@@ -158,7 +158,7 @@
     assertThat(inspector.clazz(LibInterfaceB.class), not(isPresent()));
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(libraryResult.getProguardMap())
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingFieldTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingFieldTest.java
index 22a0643..1e11234 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingFieldTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingFieldTest.java
@@ -102,7 +102,7 @@
         != inspector.clazz(LibraryB.class).isPresent());
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(libraryResult.getProguardMap())
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingMethodTest.java
index 1c19a2e..38b04ec 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterHorizontalMergingMethodTest.java
@@ -110,7 +110,7 @@
         != inspector.clazz(LibraryB.class).isPresent());
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(libraryResult.getProguardMap())
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingFieldTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingFieldTest.java
index 3b08988..139eae6 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingFieldTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingFieldTest.java
@@ -90,7 +90,7 @@
     assertThat(inspector.clazz(LibrarySubclass.class), isPresent());
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(libraryResult.getProguardMap())
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
index c5817de..80bc496 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
@@ -127,7 +127,7 @@
   private static R8TestCompileResult compileProgram(Backend backend, String proguardMap)
       throws CompilationFailedException {
     return testForR8(getStaticTemp(), backend)
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(proguardMap)
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInnerClassesPreserveTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInnerClassesPreserveTest.java
index bf3d004..4db3cd8 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInnerClassesPreserveTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInnerClassesPreserveTest.java
@@ -50,7 +50,7 @@
         .addApplyMapping(libraryCompileResult.getProguardMap())
         .addKeepAttributes("EnclosingMethod", "InnerClasses")
         .setMinApi(parameters)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .addRunClasspathFiles(libraryCompileResult.writeToZip())
         .run(parameters.getRuntime(), ProgramClassWithSimpleLibraryReference.class)
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceInvokeTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceInvokeTest.java
index da531ea..c8bb6e6 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceInvokeTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceInvokeTest.java
@@ -74,7 +74,7 @@
         .addClasspathClasses(classPathClasses)
         .addProgramClasses(TestApp.class)
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .addApplyMapping(libraryResult.getProguardMap())
         .setMinApi(parameters)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceTest.java
index ff6de98..db2a10b 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceTest.java
@@ -86,7 +86,7 @@
         .addKeepAllClassesRule()
         .addApplyMapping(libraryCompileResult.getProguardMap())
         .setMinApi(parameters)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .addRunClasspathFiles(libraryCompileResult.writeToZip())
         .run(parameters.getRuntime(), testClass)
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingReservationTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingReservationTest.java
index 278caf6..8b9c128 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingReservationTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingReservationTest.java
@@ -48,7 +48,7 @@
         .addKeepMainRule(Runner.class)
         .addApplyMapping(R.class.getTypeName() + " -> " + R.class.getTypeName() + ":")
         .setMinApi(parameters)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .addRunClasspathFiles(libraryCompileResult.writeToZip())
         .run(parameters.getRuntime(), Runner.class)
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java
index a0ce1ee..81ba247 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java
@@ -45,7 +45,7 @@
         .addLibraryFiles(parameters.getDefaultRuntimeLibrary())
         .addProgramClasses(C.class)
         .addKeepMainRule(C.class)
-        .noTreeShaking()
+        .addDontShrink()
         .addApplyMapping(
             StringUtils.lines(
                 A.class.getTypeName() + " -> " + B.class.getTypeName() + ":",
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingSameStaticNameTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingSameStaticNameTest.java
index 168cf97..71e101a 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingSameStaticNameTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingSameStaticNameTest.java
@@ -59,7 +59,7 @@
   @Test
   public void test_b131532229() throws Exception {
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addLibraryClasses(A.class, B.class)
         .addLibraryFiles(parameters.getDefaultRuntimeLibrary())
         .addProgramClasses(C.class)
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java
index a72ff12..3e4723a 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingVirtualInvokeTest.java
@@ -152,7 +152,7 @@
       throws ExecutionException, IOException, CompilationFailedException {
     R8TestCompileResult libraryCompileResult = compilationResults.apply(parameters);
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addProgramClasses(PROGRAM_CLASSES)
         .addApplyMapping(libraryCompileResult.getProguardMap())
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 37f2b71..3521a29 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
@@ -89,7 +89,7 @@
     }
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addProgramClasses(ProgramClass.class)
         .addClasspathClasses(LibraryInterface.class)
         .addApplyMapping(libraryResult.getProguardMap())
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 d42b085..45d0a3b 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
@@ -95,7 +95,7 @@
     }
 
     testForR8(parameters.getBackend())
-        .noTreeShaking()
+        .addDontShrink()
         .addProgramClasses(ProgramClass.class)
         .addClasspathClasses(LibraryInterface.class)
         .addApplyMapping(libraryResult.getProguardMap())
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/shared/NameClashTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/shared/NameClashTest.java
index 4a2c79e..97f2633 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/shared/NameClashTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/shared/NameClashTest.java
@@ -120,7 +120,7 @@
         .addProgramFiles(prgJarThatUsesOriginalLib)
         .addKeepMainRule(MAIN)
         .addKeepRules("-applymapping " + mappingFile)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .run(MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
@@ -133,7 +133,7 @@
         .addProgramFiles(prgJarThatUsesOriginalLib)
         .addKeepMainRule(MAIN)
         .addKeepRules("-applymapping " + mappingFile)
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .compile()
         .run(MAIN)
@@ -146,7 +146,7 @@
         .addProgramFiles(prgJarThatUsesOriginalLib)
         .addKeepMainRule(MAIN)
         .addKeepRules("-applymapping " + mappingFile)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .run(MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
@@ -158,7 +158,7 @@
         .addProgramFiles(prgJarThatUsesOriginalLib)
         .addKeepMainRule(MAIN)
         .addKeepRules("-applymapping " + mappingFile)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .run(MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
@@ -170,7 +170,7 @@
         .addProgramFiles(prgJarThatUsesMinifiedLib)
         .addKeepMainRule(MAIN)
         .addKeepRules("-applymapping " + mappingFile)
-        .noTreeShaking()
+        .addDontShrink()
         .compile()
         .run(MAIN)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/ApplyMappingTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/ApplyMappingTest.java
index e0d32a3..223b45c 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/ApplyMappingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/sourcelibrary/ApplyMappingTest.java
@@ -144,7 +144,8 @@
         testForR8(parameters.getBackend())
             .addProgramFiles(NAMING001_JAR)
             .addKeepRuleFiles(Paths.get(ToolHelper.EXAMPLES_DIR, "naming001", "keep-rules-106.txt"))
-            .noTreeShaking()
+            .addDontOptimize()
+            .addDontShrink()
             .setMinApi(parameters)
             .compile()
             .inspector();
diff --git a/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java b/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java
index 74d750d..fb4f3ef6 100644
--- a/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java
+++ b/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java
@@ -68,7 +68,7 @@
 
   private void runR8Test(boolean enableMinification) throws Exception {
     testForR8(parameters.getBackend())
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .addProgramClasses(Main.class, A.class)
         .addProgramClassFileData(generateTestClass())
         .addKeepMainRule(Main.class)
@@ -103,7 +103,7 @@
         .addKeepMainRule(Main.class)
         .addKeepRules("-applymapping " + mappingFile.toAbsolutePath())
         .addDontObfuscate()
-        .noTreeShaking()
+        .addDontShrink()
         .setMinApi(parameters)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutput(expectedOutput);
diff --git a/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java b/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java
index 4b3c35f..50b9bc5 100644
--- a/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java
+++ b/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java
@@ -70,7 +70,7 @@
                             + " { *** test(); }"))
             .enableInliningAnnotations()
             .enableKeepUnusedReturnValueAnnotations()
-            .minification(minification)
+            .addDontObfuscateUnless(minification)
             .setMinApi(parameters)
             .compile()
             .inspect(
diff --git a/src/test/java/com/android/tools/r8/naming/b126592786/B126592786.java b/src/test/java/com/android/tools/r8/naming/b126592786/B126592786.java
index ce1faf5..adc08a2 100644
--- a/src/test/java/com/android/tools/r8/naming/b126592786/B126592786.java
+++ b/src/test/java/com/android/tools/r8/naming/b126592786/B126592786.java
@@ -42,7 +42,7 @@
   public void runTest(boolean genericTypeLive) throws Exception {
     Class<?> mainClass = genericTypeLive ? MainGenericTypeLive.class : MainGenericTypeNotLive.class;
     testForR8(parameters.getBackend())
-        .minification(minify)
+        .addDontObfuscateUnless(minify)
         .addProgramClasses(GetClassUtil.class, A.class, GenericType.class, mainClass, Marker.class)
         .addKeepMainRule(mainClass)
         .addKeepRules(
diff --git a/src/test/java/com/android/tools/r8/naming/b131810441/AnonymousClassRenamingTest.java b/src/test/java/com/android/tools/r8/naming/b131810441/AnonymousClassRenamingTest.java
index bc4f872..2adbaec 100644
--- a/src/test/java/com/android/tools/r8/naming/b131810441/AnonymousClassRenamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/b131810441/AnonymousClassRenamingTest.java
@@ -50,7 +50,7 @@
         .addKeepMainRule(TestMain.class)
         .addKeepAttributes("InnerClasses", "EnclosingMethod")
         .enableInliningAnnotations()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .run(parameters.getRuntime(), TestMain.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
diff --git a/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java b/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java
index 8be4f0b..1c18921 100644
--- a/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java
@@ -83,8 +83,8 @@
         .addProgramClasses(TestMain.class)
         .addKeepMainRule(TestMain.class)
         .addKeepAttributes("Signature", "InnerClasses")
-        .noTreeShaking()
-        .minification(enableMinification)
+        .addDontShrink()
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .compile()
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderClassLoaderRewritingTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderClassLoaderRewritingTest.java
index 1df6b3a..9695e90 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderClassLoaderRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderClassLoaderRewritingTest.java
@@ -4,25 +4,17 @@
 
 package com.android.tools.r8.optimize.serviceloader;
 
-import static junit.framework.TestCase.assertNull;
-
-import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
-import java.nio.file.Path;
 import java.util.ServiceLoader;
-import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
 public class ServiceLoaderClassLoaderRewritingTest extends ServiceLoaderTestBase {
-
-  private final TestParameters parameters;
   private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!");
 
   public interface Service {
@@ -67,30 +59,17 @@
   }
 
   public ServiceLoaderClassLoaderRewritingTest(TestParameters parameters) {
-    this.parameters = parameters;
+    super(parameters);
   }
 
   @Test
   public void testRewritings() throws Exception {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(ServiceLoaderClassLoaderRewritingTest.class)
+    serviceLoaderTest(Service.class, ServiceImpl.class)
         .addKeepMainRule(MainRunner.class)
-        .setMinApi(parameters)
         .enableInliningAnnotations()
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
         .compile()
-        .writeToZip(path)
         .inspect(inspector -> verifyNoServiceLoaderLoads(inspector.clazz(MainRunner.class)))
         .run(parameters.getRuntime(), MainRunner.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
-
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderConstClassFromCalleeTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderConstClassFromCalleeTest.java
index da90e4a..a6556a2 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderConstClassFromCalleeTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderConstClassFromCalleeTest.java
@@ -5,41 +5,30 @@
 
 import static junit.framework.TestCase.assertEquals;
 
-import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.StringUtils;
 import java.util.ServiceLoader;
 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 ServiceLoaderConstClassFromCalleeTest extends ServiceLoaderTestBase {
 
-  @Parameter(0)
-  public TestParameters parameters;
-
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
+  public ServiceLoaderConstClassFromCalleeTest(TestParameters parameters) {
+    super(parameters);
+  }
+
   @Test
   public void test() throws Exception {
-    testForR8(parameters.getBackend())
-        .addInnerClasses(getClass())
+    serviceLoaderTest(Service.class, ServiceImpl.class, ServiceImpl2.class)
         .addKeepMainRule(Main.class)
-        .setMinApi(parameters)
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName(), ServiceImpl2.class.getTypeName())
-                    .getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("Hello, world!")
         // Check that the call to ServiceLoader.load is removed.
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
index 149fd33..5cd038d 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
@@ -5,31 +5,24 @@
 package com.android.tools.r8.optimize.serviceloader;
 
 import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.ServiceLoader;
 import java.util.concurrent.ExecutionException;
-import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
 public class ServiceLoaderMultipleCallsSameMethodTest extends ServiceLoaderTestBase {
-
-  private final TestParameters parameters;
   private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!", "Hello World!");
 
   public interface Service {
@@ -45,13 +38,6 @@
     }
   }
 
-  public static class ServiceImpl2 implements Service {
-
-    @Override
-    public void print() {
-      System.out.println("Hello World 2!");
-    }
-  }
 
   public static class MainRunner {
 
@@ -77,32 +63,23 @@
   }
 
   public ServiceLoaderMultipleCallsSameMethodTest(TestParameters parameters) {
-    this.parameters = parameters;
+    super(parameters);
   }
 
   @Test
   public void testRewritings() throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(ServiceLoaderMultipleCallsSameMethodTest.class)
+    serviceLoaderTest(Service.class, ServiceImpl.class)
         .addKeepMainRule(MainRunner.class)
-        .setMinApi(parameters)
         .enableInliningAnnotations()
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
         .compile()
-        .writeToZip(path)
         .run(parameters.getRuntime(), MainRunner.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
-        // Check that we have actually rewritten the calls to ServiceLoader.load.
-        .inspect(this::verifyNoServiceLoaderLoads)
-        .inspect(this::verifyNoClassLoaders)
         .inspect(
             inspector -> {
-              // Check the synthesize service loader method is a single shared method.
+              verifyNoServiceLoaderLoads(inspector.clazz(MainRunner.class));
+              verifyNoClassLoaders(inspector);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+              // Check the synthesized service loader method is a single shared method.
               // Due to minification we just check there is only a single synthetic class with a
               // single static method.
               boolean found = false;
@@ -115,9 +92,5 @@
                 }
               }
             });
-
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsTest.java
index c688bd4..5b2d452 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsTest.java
@@ -5,31 +5,24 @@
 package com.android.tools.r8.optimize.serviceloader;
 
 import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.ServiceLoader;
 import java.util.concurrent.ExecutionException;
-import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
 public class ServiceLoaderMultipleCallsTest extends ServiceLoaderTestBase {
-
-  private final TestParameters parameters;
   private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!", "Hello World!");
 
   public interface Service {
@@ -45,14 +38,6 @@
     }
   }
 
-  public static class ServiceImpl2 implements Service {
-
-    @Override
-    public void print() {
-      System.out.println("Hello World 2!");
-    }
-  }
-
   public static class MainRunner {
 
     public static void main(String[] args) {
@@ -81,31 +66,22 @@
   }
 
   public ServiceLoaderMultipleCallsTest(TestParameters parameters) {
-    this.parameters = parameters;
+    super(parameters);
   }
 
   @Test
   public void testRewritings() throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(ServiceLoaderMultipleCallsTest.class)
+    serviceLoaderTest(Service.class, ServiceImpl.class)
         .addKeepMainRule(MainRunner.class)
-        .setMinApi(parameters)
         .enableInliningAnnotations()
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
         .compile()
-        .writeToZip(path)
         .run(parameters.getRuntime(), MainRunner.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
         .inspect(
             inspector -> {
               // Check that we have actually rewritten the calls to ServiceLoader.load.
               assertEquals(0, getServiceLoaderLoads(inspector));
-              // Check the synthesize service loader method is a single shared method.
+              // Check the synthesized service loader method is a single shared method.
               // Due to minification we just check there is only a single synthetic class with a
               // single static method.
               boolean found = false;
@@ -118,9 +94,5 @@
                 }
               }
             });
-
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingLineSeparatorTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingLineSeparatorTest.java
index 7610575..c142442 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingLineSeparatorTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingLineSeparatorTest.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.optimize.serviceloader;
 
 import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNull;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DataEntryResource;
@@ -13,10 +12,8 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -24,8 +21,6 @@
 
 @RunWith(Parameterized.class)
 public class ServiceLoaderRewritingLineSeparatorTest extends ServiceLoaderTestBase {
-
-  private final TestParameters parameters;
   private final Separator lineSeparator;
 
   private final String EXPECTED_OUTPUT =
@@ -55,18 +50,16 @@
   }
 
   public ServiceLoaderRewritingLineSeparatorTest(TestParameters parameters, Separator separator) {
-    this.parameters = parameters;
+    super(parameters);
     this.lineSeparator = separator;
   }
 
   @Test
   public void testRewritingWithMultipleWithLineSeparator()
       throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
+    serviceLoaderTest(null)
         .addInnerClasses(ServiceLoaderRewritingTest.class)
         .addKeepMainRule(ServiceLoaderRewritingTest.MainRunner.class)
-        .setMinApi(parameters)
         .addDataEntryResources(
             DataEntryResource.fromBytes(
                 StringUtils.join(
@@ -77,14 +70,17 @@
                 "META-INF/services/" + ServiceLoaderRewritingTest.Service.class.getTypeName(),
                 Origin.unknown()))
         .compile()
-        .writeToZip(path)
         .run(parameters.getRuntime(), ServiceLoaderRewritingTest.MainRunner.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT + StringUtils.lines("Hello World 2!"))
         // Check that we have actually rewritten the calls to ServiceLoader.load.
-        .inspect(inspector -> assertEquals(0, getServiceLoaderLoads(inspector)));
-
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services"));
+        .inspect(
+            inspector -> {
+              assertEquals(0, getServiceLoaderLoads(inspector));
+              verifyServiceMetaInf(
+                  inspector,
+                  ServiceLoaderRewritingTest.Service.class,
+                  ServiceLoaderRewritingTest.ServiceImpl.class,
+                  ServiceLoaderRewritingTest.ServiceImpl2.class);
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
index 1483561..18f4a79 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
@@ -4,30 +4,23 @@
 
 package com.android.tools.r8.optimize.serviceloader;
 
-import static com.android.tools.r8.TestBase.getTestParameters;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNull;
-import static junit.framework.TestCase.assertTrue;
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V7_0_0;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.DataEntryResource;
-import com.android.tools.r8.DiagnosticsMatcher;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.ir.optimize.ServiceLoaderRewriterDiagnostic;
-import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
-import java.nio.file.Path;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.ServiceConfigurationError;
 import java.util.ServiceLoader;
 import java.util.concurrent.ExecutionException;
-import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -35,7 +28,6 @@
 @RunWith(Parameterized.class)
 public class ServiceLoaderRewritingTest extends ServiceLoaderTestBase {
 
-  private final TestParameters parameters;
   private final String EXPECTED_OUTPUT =
       StringUtils.lines("Hello World!", "Hello World!", "Hello World!");
 
@@ -60,6 +52,14 @@
     }
   }
 
+  public static class ServiceImplNoDefaultConstructor extends ServiceImpl {
+    public ServiceImplNoDefaultConstructor(int unused) {}
+  }
+
+  public static class ServiceImplNonPublicConstructor extends ServiceImpl {
+    ServiceImplNonPublicConstructor() {}
+  }
+
   public static class MainRunner {
 
     public static void main(String[] args) {
@@ -150,208 +150,183 @@
     }
   }
 
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameterized.Parameters(name = "{0}, enableRewriting: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
-  public ServiceLoaderRewritingTest(TestParameters parameters) {
-    this.parameters = parameters;
+  public ServiceLoaderRewritingTest(TestParameters parameters, boolean enableRewriting) {
+    super(parameters, enableRewriting);
+  }
+
+  private void expectRewritten(CodeInspector inspector) {
+    long found = getServiceLoaderLoads(inspector);
+    if (enableRewriting) {
+      assertEquals(0, found);
+    } else {
+      assertNotEquals(0, found);
+    }
+  }
+
+  private boolean isDexV7() {
+    // Runtime uses boot classloader rather than system classloader on this version.
+    return parameters.isDexRuntime() && parameters.getDexRuntimeVersion() == V7_0_0;
   }
 
   @Test
-  public void testRewritings() throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(ServiceLoaderRewritingTest.class)
-        .addKeepMainRule(MainRunner.class)
-        .setMinApi(parameters)
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
-        .compile()
-        .writeToZip(path)
-        .run(parameters.getRuntime(), MainRunner.class)
-        .assertSuccessWithOutput(EXPECTED_OUTPUT)
-        // Check that we have actually rewritten the calls to ServiceLoader.load.
-        .inspect(inspector -> assertEquals(0, getServiceLoaderLoads(inspector)));
-
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
-  }
-
-  @Test
-  public void testRewritingWithMultiple()
+  public void testRewritingWithNoImpls()
       throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(ServiceLoaderRewritingTest.class)
+    serviceLoaderTest(null)
         .addKeepMainRule(MainRunner.class)
-        .setMinApi(parameters)
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName(), ServiceImpl2.class.getTypeName())
-                    .getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
         .compile()
-        .writeToZip(path)
         .run(parameters.getRuntime(), MainRunner.class)
-        .assertSuccessWithOutput(EXPECTED_OUTPUT + StringUtils.lines("Hello World 2!"))
-        // Check that we have actually rewritten the calls to ServiceLoader.load.
-        .inspect(inspector -> assertEquals(0, getServiceLoaderLoads(inspector)));
+        .assertFailureWithErrorThatThrows(NoSuchElementException.class)
+        .inspectFailure(this::expectRewritten);
+  }
 
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
+  @Test
+  public void testRewritings() throws Exception {
+    serviceLoaderTest(Service.class, ServiceImpl.class)
+        .addKeepMainRule(MainRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), MainRunner.class)
+        .applyIf(
+            isDexV7() && !enableRewriting,
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class),
+            runResult ->
+                runResult
+                    .assertSuccessWithOutput(EXPECTED_OUTPUT)
+                    .inspect(
+                        inspector -> {
+                          expectRewritten(inspector);
+                          verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+                        }));
+  }
+
+  @Test
+  public void testRewritingWithMultiple() throws Exception {
+    serviceLoaderTest(Service.class, ServiceImpl.class, ServiceImpl2.class)
+        .addKeepMainRule(MainRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), MainRunner.class)
+        .applyIf(
+            isDexV7() && !enableRewriting,
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class),
+            runResult ->
+                runResult
+                    .assertSuccessWithOutput(EXPECTED_OUTPUT + StringUtils.lines("Hello World 2!"))
+                    .inspect(
+                        inspector -> {
+                          expectRewritten(inspector);
+                          verifyServiceMetaInf(
+                              inspector, Service.class, ServiceImpl.class, ServiceImpl2.class);
+                        }));
   }
 
   @Test
   public void testRewritingsWithCatchHandlers()
       throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(ServiceLoaderRewritingTest.class)
+    serviceLoaderTest(Service.class, ServiceImpl.class, ServiceImpl2.class)
         .addKeepMainRule(MainWithTryCatchRunner.class)
-        .setMinApi(parameters)
-        .addDataEntryResources(
-            DataEntryResource.fromBytes(
-                StringUtils.lines(ServiceImpl.class.getTypeName(), ServiceImpl2.class.getTypeName())
-                    .getBytes(),
-                "META-INF/services/" + Service.class.getTypeName(),
-                Origin.unknown()))
         .compile()
-        .writeToZip(path)
         .run(parameters.getRuntime(), MainWithTryCatchRunner.class)
         .assertSuccessWithOutput(StringUtils.lines("Hello World!"))
-        // Check that we have actually rewritten the calls to ServiceLoader.load.
-        .inspect(inspector -> assertEquals(0, getServiceLoaderLoads(inspector)));
-
-    // Check that we have removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
+        .inspect(
+            inspector -> {
+              expectRewritten(inspector);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class, ServiceImpl2.class);
+            });
   }
 
   @Test
   public void testDoNoRewrite() throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    CodeInspector inspector =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(ServiceLoaderRewritingTest.class)
-            .addKeepMainRule(OtherRunner.class)
-            .setMinApi(parameters)
-            .addKeepRules(
-                "-whyareyounotinlining class "
-                    + ServiceLoader.class.getTypeName()
-                    + " { *** load(...); }")
-            .enableExperimentalWhyAreYouNotInlining()
-            .addDataEntryResources(
-                DataEntryResource.fromBytes(
-                    StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                    "META-INF/services/" + Service.class.getTypeName(),
-                    Origin.unknown()))
-            .allowDiagnosticInfoMessages()
-            .compile()
-            .assertAllInfosMatch(
-                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
-            .assertAtLeastOneInfoMessage()
-            .writeToZip(path)
-            .run(parameters.getRuntime(), OtherRunner.class)
-            .assertSuccessWithOutput(EXPECTED_OUTPUT)
-            .inspector();
+    serviceLoaderTest(Service.class, ServiceImpl.class)
+        .addKeepMainRule(OtherRunner.class)
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), OtherRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertEquals(3, getServiceLoaderLoads(inspector));
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
 
-    // Check that we have not rewritten the calls to ServiceLoader.load.
-    assertEquals(3, getServiceLoaderLoads(inspector));
+  @Test
+  public void testDoNoRewriteNoDefaultConstructor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    serviceLoaderTest(Service.class, ServiceImplNoDefaultConstructor.class)
+        .addKeepMainRule(MainRunner.class)
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertFailureWithErrorThatThrows(ServiceConfigurationError.class);
+  }
 
-    // Check that we have not removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    ClassSubject serviceImpl = inspector.clazz(ServiceImpl.class);
-    assertTrue(serviceImpl.isPresent());
-    assertNotNull(zip.getEntry("META-INF/services/" + serviceImpl.getFinalName()));
+  @Test
+  public void testDoNoRewriteNonSubclass()
+      throws IOException, CompilationFailedException, ExecutionException {
+    serviceLoaderTest(Service.class, MainRunner.class)
+        .addKeepMainRule(MainRunner.class)
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertFailureWithErrorThatThrows(ServiceConfigurationError.class);
+  }
+
+  @Test
+  public void testDoNoRewriteNonPublicConstructor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    // This throws a ServiceConfigurationError only on Android 7.
+    serviceLoaderTest(Service.class, ServiceImplNonPublicConstructor.class)
+        .addKeepMainRule(MainRunner.class)
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .applyIf(
+            !isDexV7(),
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class));
   }
 
   @Test
   public void testDoNoRewriteWhenEscaping()
       throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    CodeInspector inspector =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(ServiceLoaderRewritingTest.class)
-            .addKeepMainRule(EscapingRunner.class)
-            .enableInliningAnnotations()
-            .setMinApi(parameters)
-            .addKeepRules(
-                "-whyareyounotinlining class "
-                    + ServiceLoader.class.getTypeName()
-                    + " { *** load(...); }")
-            .enableExperimentalWhyAreYouNotInlining()
-            .addDontObfuscate()
-            .addDataEntryResources(
-                DataEntryResource.fromBytes(
-                    StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                    "META-INF/services/" + Service.class.getTypeName(),
-                    Origin.unknown()))
-            .allowDiagnosticInfoMessages()
-            .compile()
-            .assertAllInfosMatch(
-                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
-            .assertAtLeastOneInfoMessage()
-            .writeToZip(path)
-            .run(parameters.getRuntime(), EscapingRunner.class)
-            .assertSuccessWithOutput(EXPECTED_OUTPUT)
-            .inspector();
-
-    // Check that we have not rewritten the calls to ServiceLoader.load.
-    assertEquals(3, getServiceLoaderLoads(inspector));
-
-    // Check that we have not removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    ClassSubject serviceImpl = inspector.clazz(ServiceImpl.class);
-    assertTrue(serviceImpl.isPresent());
-    assertNotNull(zip.getEntry("META-INF/services/" + serviceImpl.getFinalName()));
+    serviceLoaderTest(Service.class, ServiceImpl.class)
+        .addKeepMainRule(EscapingRunner.class)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), EscapingRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertEquals(3, getServiceLoaderLoads(inspector));
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
   }
 
   @Test
   public void testDoNoRewriteWhenClassLoaderIsPhi()
       throws IOException, CompilationFailedException, ExecutionException {
-    Path path = temp.newFile("out.zip").toPath();
-    CodeInspector inspector =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(ServiceLoaderRewritingTest.class)
-            .addKeepMainRule(LoadWhereClassLoaderIsPhi.class)
-            .enableInliningAnnotations()
-            .setMinApi(parameters)
-            .addKeepRules(
-                "-whyareyounotinlining class "
-                    + ServiceLoader.class.getTypeName()
-                    + " { *** load(...); }")
-            .enableExperimentalWhyAreYouNotInlining()
-            .addDataEntryResources(
-                DataEntryResource.fromBytes(
-                    StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                    "META-INF/services/" + Service.class.getTypeName(),
-                    Origin.unknown()))
-            .allowDiagnosticInfoMessages()
-            .compile()
-            .assertAllInfosMatch(
-                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
-            .assertAtLeastOneInfoMessage()
-            .writeToZip(path)
-            .run(parameters.getRuntime(), LoadWhereClassLoaderIsPhi.class)
-            .assertSuccessWithOutputLines("Hello World!")
-            .inspector();
-
-    // Check that we have not rewritten the calls to ServiceLoader.load.
-    assertEquals(1, getServiceLoaderLoads(inspector));
-
-    // Check that we have not removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    ClassSubject serviceImpl = inspector.clazz(ServiceImpl.class);
-    assertTrue(serviceImpl.isPresent());
-    assertNotNull(zip.getEntry("META-INF/services/" + serviceImpl.getFinalName()));
+    serviceLoaderTest(Service.class, ServiceImpl.class)
+        .addKeepMainRule(LoadWhereClassLoaderIsPhi.class)
+        .enableInliningAnnotations()
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), LoadWhereClassLoaderIsPhi.class)
+        .assertSuccessWithOutputLines("Hello World!")
+        .inspect(
+            inspector -> {
+              assertEquals(1, getServiceLoaderLoads(inspector));
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
   }
 
   @Test
@@ -361,41 +336,18 @@
     // https://android-review.googlesource.com/c/platform/libcore/+/273135
     assumeTrue(
         parameters.getRuntime().isCf()
-            || !parameters.getRuntime().asDex().getVm().getVersion().equals(Version.V7_0_0));
-    Path path = temp.newFile("out.zip").toPath();
-    CodeInspector inspector =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(ServiceLoaderRewritingTest.class)
-            .addKeepMainRule(MainRunner.class)
-            .addKeepClassRules(Service.class)
-            .setMinApi(parameters)
-            .addKeepRules(
-                "-whyareyounotinlining class "
-                    + ServiceLoader.class.getTypeName()
-                    + " { *** load(...); }")
-            .enableExperimentalWhyAreYouNotInlining()
-            .addDataEntryResources(
-                DataEntryResource.fromBytes(
-                    StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
-                    "META-INF/services/" + Service.class.getTypeName(),
-                    Origin.unknown()))
-            .allowDiagnosticInfoMessages()
-            .compile()
-            .assertAllInfosMatch(
-                DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class))
-            .assertAtLeastOneInfoMessage()
-            .writeToZip(path)
-            .run(parameters.getRuntime(), MainRunner.class)
-            .assertSuccessWithOutput(EXPECTED_OUTPUT)
-            .inspector();
-
-    // Check that we have not rewritten the calls to ServiceLoader.load.
-    assertEquals(3, getServiceLoaderLoads(inspector));
-
-    // Check that we have not removed the service configuration from META-INF/services.
-    ZipFile zip = new ZipFile(path.toFile());
-    ClassSubject service = inspector.clazz(Service.class);
-    assertTrue(service.isPresent());
-    assertNotNull(zip.getEntry("META-INF/services/" + service.getFinalName()));
+            || !parameters.getRuntime().asDex().getVm().getVersion().equals(V7_0_0));
+    serviceLoaderTest(Service.class, ServiceImpl.class)
+        .addKeepMainRule(MainRunner.class)
+        .addKeepClassRules(Service.class)
+        .allowDiagnosticInfoMessages(enableRewriting)
+        .compileWithExpectedDiagnostics(expectedDiagnostics)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertEquals(3, getServiceLoaderLoads(inspector));
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
new file mode 100644
index 0000000..c57f579
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
@@ -0,0 +1,278 @@
+// Copyright (c) 2024, 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.optimize.serviceloader;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderRewritingWithAssumeNoSideEffectsTest extends ServiceLoaderTestBase {
+  private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!");
+
+  public interface Service {
+
+    void print();
+  }
+
+  public static class ServiceImpl implements Service {
+
+    @Override
+    public void print() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class ServiceImpl2 implements Service {
+
+    @Override
+    public void print() {
+      System.out.println("Hello World 2!");
+    }
+  }
+
+  public static class MainRunner {
+    public static void main(String[] args) {
+      ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator().next().print();
+    }
+  }
+
+  public static class HasNextRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      if (iterator.hasNext()) {
+        iterator.next().print();
+      }
+    }
+  }
+
+  public static class MultipleCallsRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      if (iterator.hasNext() && iterator.hasNext()) {
+        iterator.next().print();
+      }
+    }
+  }
+
+  public static class HasNextAfterNextRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+
+      iterator.next().print();
+      if (iterator.hasNext()) {
+        System.out.println("not reached");
+      }
+    }
+  }
+
+  public static class HasNextAfterNextWithTryCatch {
+    public static void main(String[] args) {
+      try {
+        Iterator<Service> iterator =
+            ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+
+        iterator.next().print();
+        if (iterator.hasNext()) {
+          System.out.println("not reached");
+        }
+      } catch (Throwable t) {
+        System.out.println("unreachable");
+      }
+    }
+  }
+
+  public static class LoopingRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      // Loop without a call to hasNext().
+      for (int i = System.currentTimeMillis() > 0 ? 0 : 1; i < 1; ++i) {
+        iterator.next().print();
+      }
+    }
+  }
+
+  public static class PhiRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          System.currentTimeMillis() > 0
+              ? ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator()
+              : null;
+      iterator.next().print();
+    }
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ServiceLoaderRewritingWithAssumeNoSideEffectsTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  private static void assertIteratorPresent(CodeInspector inspector, boolean expected) {
+    assertEquals(0, getServiceLoaderLoads(inspector));
+
+    boolean hasIteratorCall =
+        inspector
+            .streamInstructions()
+            .anyMatch(ins -> ins.isInvoke() && ins.getMethod().name.toString().equals("iterator"));
+    assertEquals(expected, hasIteratorCall);
+  }
+
+  private R8FullTestBuilder doTest(Class<?>... implClasses) throws IOException {
+    return serviceLoaderTest(Service.class, implClasses)
+        .addKeepRules("-assumenosideeffects class java.util.ServiceLoader { *** load(...); }");
+  }
+
+  @Test
+  public void testRewritingWithNoImplsHasNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest()
+        .addKeepMainRule(HasNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextRunner.class)
+        .assertSuccessWithOutput("")
+        .inspect(inspector -> assertIteratorPresent(inspector, false));
+  }
+
+  @Test
+  public void testRewritingWithMultiple()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class, ServiceImpl2.class)
+        .addKeepMainRule(MainRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, false);
+              // ServiceImpl2 gets removed since next() is called only once.
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testRewritingWithHasNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, false);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteMultipleCalls()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(MultipleCallsRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), MultipleCallsRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteHasNextAfterNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextAfterNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextAfterNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteHasNextAfterNextWithTryCatch()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextAfterNextWithTryCatch.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextAfterNextWithTryCatch.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteHasNextAfterNextBlocks()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextAfterNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextAfterNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteLoop()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(LoopingRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), LoopingRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewritePhiUser()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(PhiRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), PhiRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
index 21276a2..809f782 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
@@ -4,38 +4,65 @@
 package com.android.tools.r8.optimize.serviceloader;
 
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
-import static junit.framework.TestCase.assertTrue;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DiagnosticsMatcher;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder.DiagnosticsConsumer;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.graph.AppServices;
+import com.android.tools.r8.ir.optimize.ServiceLoaderRewriterDiagnostic;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DataResourceConsumerForTesting;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 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 com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 public class ServiceLoaderTestBase extends TestBase {
+  private static final DiagnosticsConsumer<CompilationFailedException> REWRITER_DIAGNOSTICS =
+      diagnostics ->
+          diagnostics
+              .assertOnlyInfos()
+              .assertAllInfosMatch(
+                  DiagnosticsMatcher.diagnosticType(ServiceLoaderRewriterDiagnostic.class));
+
+  protected final TestParameters parameters;
+  protected final boolean enableRewriting;
+  protected DataResourceConsumerForTesting dataResourceConsumer;
+  protected final DiagnosticsConsumer<CompilationFailedException> expectedDiagnostics;
+
+  public ServiceLoaderTestBase(TestParameters parameters) {
+    this(parameters, true);
+  }
+
+  public ServiceLoaderTestBase(TestParameters parameters, boolean enableRewriting) {
+    this.parameters = parameters;
+    this.enableRewriting = enableRewriting;
+    if (enableRewriting) {
+      expectedDiagnostics = REWRITER_DIAGNOSTICS;
+    } else {
+      expectedDiagnostics = diagnostics -> diagnostics.assertNoInfos();
+    }
+  }
 
   public static long getServiceLoaderLoads(CodeInspector inspector) {
-    return inspector.allClasses().stream()
-        .mapToLong(ServiceLoaderTestBase::getServiceLoaderLoads)
-        .reduce(0, Long::sum);
-  }
-
-  public static long getServiceLoaderLoads(CodeInspector inspector, Class<?> clazz) {
-    return getServiceLoaderLoads(inspector.clazz(clazz));
-  }
-
-  public static long getServiceLoaderLoads(ClassSubject classSubject) {
-    assertTrue(classSubject.isPresent());
-    return classSubject.allMethods(MethodSubject::hasCode).stream()
-        .mapToLong(
-            method ->
-                method
-                    .streamInstructions()
-                    .filter(ServiceLoaderTestBase::isServiceLoaderLoad)
-                    .count())
-        .sum();
+    return inspector
+        .streamInstructions()
+        .filter(ServiceLoaderTestBase::isServiceLoaderLoad)
+        .count();
   }
 
   private static boolean isServiceLoaderLoad(InstructionSubject instruction) {
@@ -43,22 +70,89 @@
         && instruction.getMethod().qualifiedName().contains("ServiceLoader.load");
   }
 
-  public void verifyNoClassLoaders(CodeInspector inspector) {
-    inspector.allClasses().forEach(this::verifyNoClassLoaders);
+  public static void verifyNoClassLoaders(CodeInspector inspector) {
+    inspector.allClasses().forEach(ServiceLoaderTestBase::verifyNoClassLoaders);
   }
 
-  public void verifyNoClassLoaders(ClassSubject classSubject) {
+  public static void verifyNoClassLoaders(ClassSubject classSubject) {
     assertTrue(classSubject.isPresent());
     classSubject.forAllMethods(
         method -> assertThat(method, not(invokesMethodWithName("getClassLoader"))));
   }
 
-  public void verifyNoServiceLoaderLoads(CodeInspector inspector) {
-    inspector.allClasses().forEach(this::verifyNoServiceLoaderLoads);
-  }
-
-  public void verifyNoServiceLoaderLoads(ClassSubject classSubject) {
+  public static void verifyNoServiceLoaderLoads(ClassSubject classSubject) {
     assertTrue(classSubject.isPresent());
     classSubject.forAllMethods(method -> assertThat(method, not(invokesMethodWithName("load"))));
   }
+
+  public Map<String, List<String>> getServiceMappings() {
+    return dataResourceConsumer.getAll().entrySet().stream()
+        .filter(e -> e.getKey().startsWith(AppServices.SERVICE_DIRECTORY_NAME))
+        .collect(
+            Collectors.toMap(
+                e -> e.getKey().substring(AppServices.SERVICE_DIRECTORY_NAME.length()),
+                e -> e.getValue()));
+  }
+
+  public void verifyServiceMetaInf(
+      CodeInspector inspector, Class<?> serviceClass, Class<?> serviceImplClass) {
+    // Account for renaming, and for the impl to be merged with the interface.
+    String finalServiceName = inspector.clazz(serviceClass).getFinalName();
+    String finalImplName = inspector.clazz(serviceImplClass).getFinalName();
+    if (finalServiceName == null) {
+      finalServiceName = finalImplName;
+    }
+    Map<String, List<String>> actual = getServiceMappings();
+    Map<String, List<String>> expected =
+        ImmutableMap.of(finalServiceName, Collections.singletonList(finalImplName));
+    assertEquals(expected, actual);
+  }
+
+  public void verifyServiceMetaInf(
+      CodeInspector inspector,
+      Class<?> serviceClass,
+      Class<?> serviceImplClass1,
+      Class<?> serviceImplClass2) {
+    // Account for renaming. No class merging should happen.
+    String finalServiceName = inspector.clazz(serviceClass).getFinalName();
+    String finalImplName1 = inspector.clazz(serviceImplClass1).getFinalName();
+    String finalImplName2 = inspector.clazz(serviceImplClass2).getFinalName();
+    Map<String, List<String>> actual = getServiceMappings();
+    Map<String, List<String>> expected =
+        ImmutableMap.of(finalServiceName, Arrays.asList(finalImplName1, finalImplName2));
+    assertEquals(expected, actual);
+  }
+
+  protected R8FullTestBuilder serviceLoaderTest(Class<?> serviceClass, Class<?>... implClasses)
+      throws IOException {
+    return serviceLoaderTestNoClasses(serviceClass, implClasses).addInnerClasses(getClass());
+  }
+
+  protected R8FullTestBuilder serviceLoaderTestNoClasses(
+      Class<?> serviceClass, Class<?>... implClasses) throws IOException {
+    R8FullTestBuilder ret =
+        testForR8(parameters.getBackend())
+            .setMinApi(parameters)
+            .addOptionsModification(
+                o -> {
+                  dataResourceConsumer = new DataResourceConsumerForTesting(o.dataResourceConsumer);
+                  o.dataResourceConsumer = dataResourceConsumer;
+                  o.enableServiceLoaderRewriting = enableRewriting;
+                })
+            // Enables ServiceLoader optimization failure diagnostics.
+            .enableExperimentalWhyAreYouNotInlining()
+            .addKeepRules("-whyareyounotinlining class java.util.ServiceLoader { *** load(...); }");
+    if (implClasses.length > 0) {
+      String implLines =
+          Arrays.stream(implClasses)
+              .map(c -> c.getTypeName() + "\n")
+              .collect(Collectors.joining(""));
+      ret.addDataEntryResources(
+          DataEntryResource.fromBytes(
+              implLines.getBytes(),
+              AppServices.SERVICE_DIRECTORY_NAME + serviceClass.getTypeName(),
+              Origin.unknown()));
+    }
+    return ret;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceWithFeatureNullClassLoaderTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceWithFeatureNullClassLoaderTest.java
new file mode 100644
index 0000000..a815899
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceWithFeatureNullClassLoaderTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2024, 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.optimize.serviceloader;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.ServiceLoader;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ServiceWithFeatureNullClassLoaderTest extends ServiceLoaderTestBase {
+
+  public interface Service {
+
+    void print();
+  }
+
+  public static class ServiceImpl implements Service {
+
+    @Override
+    public void print() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class MainRunner {
+
+    public static void main(String[] args) {
+      run1();
+    }
+
+    @NeverInline
+    public static void run1() {
+      for (Service x : ServiceLoader.load(Service.class, null)) {
+        x.print();
+      }
+    }
+
+    @NeverInline
+    public static void checkNotNull(ClassLoader classLoader) {
+      if (classLoader == null) {
+        throw new NullPointerException("ClassLoader should not be null");
+      }
+    }
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public ServiceWithFeatureNullClassLoaderTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Test
+  public void testNoRewritings() throws Exception {
+    R8TestCompileResult result =
+        serviceLoaderTestNoClasses(Service.class, ServiceImpl.class)
+            .addFeatureSplit(MainRunner.class, Service.class, ServiceImpl.class)
+            .enableInliningAnnotations()
+            .addKeepMainRule(MainRunner.class)
+            .allowDiagnosticInfoMessages()
+            .compileWithExpectedDiagnostics(expectedDiagnostics);
+
+    CodeInspector inspector = result.featureInspector();
+    assertEquals(getServiceLoaderLoads(inspector), 1);
+    // Check that we have not removed the service configuration from META-INF/services.
+    verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceWithFeatureTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceWithFeatureTest.java
new file mode 100644
index 0000000..34237e1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceWithFeatureTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2024, 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.optimize.serviceloader;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.ServiceLoader;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ServiceWithFeatureTest extends ServiceLoaderTestBase {
+
+  public interface Service {
+
+    void print();
+  }
+
+  public static class ServiceImpl implements Service {
+
+    @Override
+    public void print() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class MainRunner {
+
+    public static void main(String[] args) {
+      run1();
+    }
+
+    @NeverInline
+    public static void run1() {
+      ClassLoader classLoader = Service.class.getClassLoader();
+      checkNotNull(classLoader);
+      for (Service x : ServiceLoader.load(Service.class, classLoader)) {
+        x.print();
+      }
+    }
+
+    @NeverInline
+    public static void checkNotNull(ClassLoader classLoader) {
+      if (classLoader == null) {
+        throw new NullPointerException("ClassLoader should not be null");
+      }
+    }
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public ServiceWithFeatureTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Test
+  public void testRewritings() throws Exception {
+    serviceLoaderTestNoClasses(Service.class, ServiceImpl.class)
+        .addFeatureSplit(MainRunner.class, Service.class, ServiceImpl.class)
+        .enableInliningAnnotations()
+        .addKeepMainRule(MainRunner.class)
+        .compile()
+        .inspect(
+            inspector -> {},
+            inspector -> {
+              verifyNoServiceLoaderLoads(inspector.clazz(MainRunner.class));
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java b/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java
index 8f93035..4034d59 100644
--- a/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b165825758/Regress165825758Test.java
@@ -26,23 +26,22 @@
 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 Regress165825758Test extends TestBase {
 
-  static final String EXPECTED = StringUtils.lines("Hello, world");
+  private static final String EXPECTED = StringUtils.lines("Hello, world");
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public Regress165825758Test(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void testReference() throws Exception {
     testForRuntime(parameters)
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
index d052107..07b52af 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -88,7 +89,7 @@
             .addProgramClasses(CLASSES)
             .addProgramClassFileData(CLASS_BYTES)
             .allowDiagnosticWarningMessages()
-            .treeShaking(treeShake)
+            .applyIf(!treeShake, R8FullTestBuilder::addDontShrink)
             .addDontObfuscate()
             .setMinApi(parameters)
             .addOptionsModification(options -> options.testing.readInputStackMaps = false)
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceLambdaTest.java b/src/test/java/com/android/tools/r8/retrace/RetraceLambdaTest.java
index eb871a6..e78aece 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceLambdaTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceLambdaTest.java
@@ -98,7 +98,7 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addKeepPackageNamesRule(getClass().getPackage())
-        .noTreeShaking()
+        .addDontShrink()
         .addDontOptimize()
         .addKeepAttributeSourceFile()
         .addKeepAttributeLineNumberTable()
diff --git a/src/test/java/com/android/tools/r8/retrace/StackTraceWithPcAndJumboStringTestRunner.java b/src/test/java/com/android/tools/r8/retrace/StackTraceWithPcAndJumboStringTestRunner.java
index 25e7cc8..655a095 100644
--- a/src/test/java/com/android/tools/r8/retrace/StackTraceWithPcAndJumboStringTestRunner.java
+++ b/src/test/java/com/android/tools/r8/retrace/StackTraceWithPcAndJumboStringTestRunner.java
@@ -46,7 +46,7 @@
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
         .addProgramClasses(getTestClass())
-        .noTreeShaking()
+        .addDontShrink()
         .addKeepAttributeLineNumberTable()
         .addKeepMainRule(getTestClass())
         .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/rewrite/ScriptEngineTest.java b/src/test/java/com/android/tools/r8/rewrite/ScriptEngineTest.java
index 045cc76..e954b79 100644
--- a/src/test/java/com/android/tools/r8/rewrite/ScriptEngineTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/ScriptEngineTest.java
@@ -89,7 +89,7 @@
             // TODO(b/136633154): This should work both with and without -dontobfuscate.
             .addDontObfuscate()
             // TODO(b/136633154): This should work both with and without -dontshrink.
-            .noTreeShaking()
+            .addDontShrink()
             .compile()
             .applyIf(
                 parameters.isDexRuntime(),
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java b/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
index e8bfc88..04a1132 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
@@ -197,7 +197,7 @@
     return testForR8(getStaticTemp(), Backend.CF)
         .addProgramClasses(ClassWithAssertions.class)
         .debug()
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .addAssertionsConfiguration(transformation)
         .compile();
@@ -221,7 +221,7 @@
         .addProgramClasses(ChromuimAssertionHookMock.class)
         .setMinApi(AndroidApiLevel.B)
         .debug()
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .setMinApi(minApi)
         .compile();
@@ -350,7 +350,7 @@
             .addProgramClasses(ClassWithAssertions.class)
             .debug()
             .setMinApi(minApi)
-            .noTreeShaking()
+            .addDontShrink()
             .addDontObfuscate()
             .compile()
             .writeToZip();
diff --git a/src/test/java/com/android/tools/r8/rewrite/staticvalues/inlibraries/StaticLibraryValuesChangeTest.java b/src/test/java/com/android/tools/r8/rewrite/staticvalues/inlibraries/StaticLibraryValuesChangeTest.java
index 7d2c4bc..906d8e4 100644
--- a/src/test/java/com/android/tools/r8/rewrite/staticvalues/inlibraries/StaticLibraryValuesChangeTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/staticvalues/inlibraries/StaticLibraryValuesChangeTest.java
@@ -85,7 +85,7 @@
             PreloadedClassFileProvider.fromClassData(
                 DescriptorUtils.javaTypeToDescriptor(LibraryClass.class.getName()),
                 compileTimeLibrary.buildClasses().get(0)))
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .compile()
         // Merge the compiled TestMain with the runtime version of LibraryClass.
diff --git a/src/test/java/com/android/tools/r8/shaking/FunctionTest.java b/src/test/java/com/android/tools/r8/shaking/FunctionTest.java
index eda629d..a5c498a 100644
--- a/src/test/java/com/android/tools/r8/shaking/FunctionTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FunctionTest.java
@@ -42,7 +42,7 @@
   public void testR8Working() throws Exception {
     testForR8(parameters.getBackend())
         .addKeepMainRule(TestClass.class)
-        .noTreeShaking()
+        .addDontShrink()
         .addDontObfuscate()
         .enableInliningAnnotations()
         .addInnerClasses(FunctionTest.class)
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index 6ae395d..023b120 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -166,7 +166,7 @@
                     ToolHelper.getAppBuilder(b.getBuilder())
                         .addProgramFiles(Paths.get(programFile)))
             .enableProguardTestOptions()
-            .minification(minify.isMinify())
+            .addDontObfuscateUnless(minify.isMinify())
             .setMinApi(parameters)
             .addKeepRuleFiles(
                 ListUtils.map(
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
index 6af8bd4..82e9f74 100644
--- a/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
@@ -102,7 +102,7 @@
             .addKeepRules(KEEP_ANNOTATIONS)
             .addKeepRules("-keep @interface " + ANNOTATION_NAME + " {", "  *;", "}")
             .allowDiagnosticWarningMessages()
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .assertAllWarningMessagesMatch(
@@ -143,7 +143,7 @@
                 "  java.lang.String *f2();",
                 "}")
             .allowDiagnosticWarningMessages()
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .assertAllWarningMessagesMatch(
@@ -181,7 +181,7 @@
             .addKeepMainRule(MAIN_CLASS_NAME)
             .addKeepRules(KEEP_ANNOTATIONS)
             .allowDiagnosticWarningMessages()
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .assertAllWarningMessagesMatch(
@@ -218,7 +218,7 @@
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(MAIN_CLASS_NAME)
             .allowDiagnosticWarningMessages()
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .assertAllWarningMessagesMatch(
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/EnclosingMethodTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/EnclosingMethodTest.java
index d93c615..971b408 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/EnclosingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/EnclosingMethodTest.java
@@ -95,7 +95,7 @@
         .addKeepMainRule(MAIN)
         .addKeepRules("-keep class **.GetName*")
         .addKeepRules("-keepattributes InnerClasses,EnclosingMethod")
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(OUTPUT_WITH_SHRUNK_ATTRIBUTES);
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/InnerClassesSimpleTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/InnerClassesSimpleTest.java
index 41de889..0af92c9 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/InnerClassesSimpleTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/InnerClassesSimpleTest.java
@@ -40,7 +40,7 @@
             .addInnerClasses(getClass())
             .addKeepMainRule(Main.class)
             .addKeepPackageNamesRule(getClass().getPackage())
-            .noTreeShaking()
+            .addDontShrink()
             .applyIf(!minify, TestShrinkerBuilder::addDontObfuscate)
             .compile()
             .writeToZip();
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/MissingEnclosingMethodTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/MissingEnclosingMethodTest.java
index 06e43a0..615f9cf 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/MissingEnclosingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/MissingEnclosingMethodTest.java
@@ -116,7 +116,7 @@
         .apply(config::addInnerClasses)
         .addKeepMainRule(DataClassUser.class)
         .addKeepAttributes("Signature", "InnerClasses", "EnclosingMethod")
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .setMinApi(parameters)
         .run(parameters.getRuntime(), DataClassUser.class)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldAnnotation.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldAnnotation.java
index 18d41dd..aff5d9d 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldAnnotation.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfRuleWithFieldAnnotation.java
@@ -94,7 +94,7 @@
         .addProgramClasses(Foo.class, Bar.class, SerializedName.class, FooNotCallingBar.class)
         .addDontWarn(getClass())
         .addKeepMainRule(FooNotCallingBar.class)
-        .noMinification()
+        .addDontObfuscate()
         .addKeepRules(CONDITIONAL_KEEP_RULE)
         .compile()
         .inspect(
@@ -113,7 +113,7 @@
         .addProgramClasses(Foo.class, Bar.class, SerializedName.class, FooNotCallingBar.class)
         .addDontWarn(getClass())
         .addKeepMainRule(FooNotCallingBar.class)
-        .noMinification()
+        .addDontObfuscate()
         .compile()
         .inspect(
             codeInspector -> {
diff --git a/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java b/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java
index 41f3dcc..2dd2b3f 100644
--- a/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesTest.java
@@ -178,7 +178,7 @@
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableUnusedArgumentAnnotations()
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .apply(this::configureKeepParameterNames)
         .compile()
         .inspect(this::checkLocalVariableTable)
@@ -217,7 +217,7 @@
         .enableUnusedArgumentAnnotations()
         .addInnerClasses(KeepParameterNamesTest.class)
         .addKeepMainRule(TestClass.class)
-        .minification(enableMinification)
+        .addDontObfuscateUnless(enableMinification)
         .apply(this::configureKeepParameterNames)
         .compile()
         .inspect(this::checkLocalVariableTableNotKept)
diff --git a/src/test/java/com/android/tools/r8/shaking/proxy/MockitoTest.java b/src/test/java/com/android/tools/r8/shaking/proxy/MockitoTest.java
index 1b75532..0865b95 100644
--- a/src/test/java/com/android/tools/r8/shaking/proxy/MockitoTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/proxy/MockitoTest.java
@@ -52,7 +52,7 @@
             .addProgramFiles(MOCKITO_INTERFACE_JAR)
             .addKeepRuleFiles(flagToKeepTestRunner)
             .addDontWarn("org.mockito.**")
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .inspector();
@@ -71,7 +71,7 @@
             .addProgramFiles(MOCKITO_INTERFACE_JAR)
             .addKeepRuleFiles(flagToKeepInterfaceConditionally)
             .addDontWarn("org.mockito.**")
-            .minification(minify)
+            .addDontObfuscateUnless(minify)
             .setMinApi(parameters)
             .compile()
             .inspector();
diff --git a/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingOverriddenMethodTest.java b/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingOverriddenMethodTest.java
index 79c5edc..ba197fb 100644
--- a/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingOverriddenMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingOverriddenMethodTest.java
@@ -54,7 +54,7 @@
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(AndroidApiLevel.B)
         // Redirect the compilers stdout to intercept the '-whyareyoukeeping' output
         .collectStdout()
@@ -72,7 +72,7 @@
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
-        .minification(minification)
+        .addDontObfuscateUnless(minification)
         .setMinApi(AndroidApiLevel.B)
         .setKeptGraphConsumer(graphConsumer)
         .compile();
diff --git a/src/test/java/com/android/tools/r8/utils/DataResourceConsumerForTesting.java b/src/test/java/com/android/tools/r8/utils/DataResourceConsumerForTesting.java
index 77274e6..dc2dfe8 100644
--- a/src/test/java/com/android/tools/r8/utils/DataResourceConsumerForTesting.java
+++ b/src/test/java/com/android/tools/r8/utils/DataResourceConsumerForTesting.java
@@ -54,6 +54,10 @@
   @Override
   public void finished(DiagnosticsHandler handler) {}
 
+  public Map<String, ImmutableList<String>> getAll() {
+    return resources;
+  }
+
   public ImmutableList<String> get(String name) {
     return resources.get(name);
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/ProguardTestBuilder.java
index 758f1cf..275592f 100644
--- a/src/test/testbase/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -110,12 +110,6 @@
       command.add(outJar.toString());
       command.add("-printmapping");
       command.add(mapFile.toString());
-      if (enableTreeShaking.isFalse()) {
-        command.add("-dontshrink");
-      }
-      if (enableMinification.isFalse()) {
-        command.add("-dontobfuscate");
-      }
       ProcessBuilder pbuilder = new ProcessBuilder(command);
       ProcessResult result = ToolHelper.runProcess(pbuilder, getStdoutForTesting());
       if (result.exitCode != 0) {
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index ecd1ea7..bf3998d 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -114,12 +114,6 @@
       builder.addProguardConfiguration(keepRules, Origin.unknown());
     }
     builder.addMainDexRulesFiles(mainDexRulesFiles);
-    if (enableTreeShaking.isFalse()) {
-      builder.setDisableTreeShaking(true);
-    }
-    if (enableMinification.isFalse()) {
-      builder.setDisableMinification(true);
-    }
     StringBuilder proguardMapBuilder = wrapProguardMapConsumer(builder);
     if (!applyMappingMaps.isEmpty()) {
       try {
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
index f9db531..942cc92 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
@@ -140,6 +140,11 @@
         AndroidApp.builder().addProgramFile(feature).setProguardMapOutputData(proguardMap).build());
   }
 
+  public CodeInspector featureInspector() throws IOException {
+    assert features.size() == 1;
+    return featureInspector(features.get(0));
+  }
+
   @SafeVarargs
   public final <E extends Throwable> R8TestCompileResult inspect(
       ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
diff --git a/src/test/testbase/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestShrinkerBuilder.java
index 57d2137..2dbe0e2 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -35,9 +34,6 @@
         T extends TestShrinkerBuilder<C, B, CR, RR, T>>
     extends TestCompilerBuilder<C, B, CR, RR, T> {
 
-  protected OptionalBool enableTreeShaking = OptionalBool.UNKNOWN;
-  protected OptionalBool enableMinification = OptionalBool.UNKNOWN;
-
   private final Set<Class<? extends Annotation>> addedTestingAnnotations =
       Sets.newIdentityHashSet();
 
@@ -49,10 +45,6 @@
     return false;
   }
 
-  public boolean isR8TestBuilder() {
-    return false;
-  }
-
   public boolean isR8CompatTestBuilder() {
     return false;
   }
@@ -90,25 +82,6 @@
     return backend == Backend.DEX ? super.getMinApiLevel() : -1;
   }
 
-  public T treeShaking(boolean enable) {
-    enableTreeShaking = OptionalBool.of(enable);
-    return self();
-  }
-
-  public T noTreeShaking() {
-    return treeShaking(false);
-  }
-
-  public T minification(boolean enable) {
-    enableMinification = OptionalBool.of(enable);
-    return self();
-  }
-
-  @Deprecated
-  public T noMinification() {
-    return minification(false);
-  }
-
   public T addClassObfuscationDictionary(String... names) throws IOException {
     Path path = getState().getNewTempFolder().resolve("classobfuscationdictionary.txt");
     FileUtils.writeTextFile(path, StringUtils.join(" ", names));
@@ -140,6 +113,13 @@
             + clazz.getTypeName());
   }
 
+  public T addDontObfuscateUnless(boolean enableMinification) {
+    if (!enableMinification) {
+      return addDontObfuscate();
+    }
+    return self();
+  }
+
   public T addDontOptimize() {
     return addKeepRules("-dontoptimize");
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index 74bdc47..7c06d42 100644
--- a/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -67,6 +67,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class CodeInspector {
 
@@ -401,6 +402,12 @@
     return builder.build();
   }
 
+  public Stream<InstructionSubject> streamInstructions() {
+    return allClasses().stream()
+        .flatMap(cls -> cls.allMethods(MethodSubject::hasCode).stream())
+        .flatMap(MethodSubject::streamInstructions);
+  }
+
   public FieldSubject field(Field field) {
     return field(Reference.fieldFromField(field));
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/TypeSubject.java b/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/TypeSubject.java
index efa2c30..1140771 100644
--- a/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/TypeSubject.java
+++ b/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/TypeSubject.java
@@ -41,6 +41,10 @@
     throw new Unreachable("Cannot determine if a type is synthetic");
   }
 
+  public boolean is(Class<?> clazz) {
+    return is(clazz.getTypeName());
+  }
+
   public boolean is(String type) {
     return dexType.equals(codeInspector.toDexType(type));
   }
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 6090377..5e0f695 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -358,8 +358,9 @@
         message = f'DO NOT SUBMIT: {message}'
     if commit_info:
         message += f'\n\n{commit_info}'
+    message = message.replace("'", r"\'")
     return subprocess.check_output(
-        f"g4 change --desc '{message}\n'",
+        f"g4 change --desc $'{message}\n'",
         shell=True).decode('utf-8')