Merge commit 'c6539655d527593303332f12a9aeb76c8a2ed1c5' into dev-release
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 9b4bb2a..2312362 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -5,9 +5,14 @@
 from os import path
 import datetime
 from subprocess import check_output, Popen, PIPE, STDOUT
-import sys
 import inspect
+import os
+import sys
+# Add both current path to allow us to package import utils and the tools
+# dir to allow transitive (for utils) dependendies to be loaded.
 sys.path.append(path.dirname(inspect.getfile(lambda: None)))
+sys.path.append(os.path.join(
+    path.dirname(inspect.getfile(lambda: None)), 'tools'))
 from tools.utils import EnsureDepFromGoogleCloudStorage
 
 FMT_CMD = path.join(
diff --git a/build.gradle b/build.gradle
index 834e3d6..04704bc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -139,11 +139,6 @@
             srcDirs = ['src/test/examplesJava20']
         }
     }
-    examplesTestNGRunner {
-        java {
-            srcDirs = ['src/test/testngrunner']
-        }
-    }
     examplesAndroidN {
         java {
             srcDirs = ['src/test/examplesAndroidN']
@@ -245,8 +240,6 @@
     main17Implementation group: 'org.ow2.asm', name: 'asm-analysis', version: asmVersion
     main17Implementation group: 'org.ow2.asm', name: 'asm-util', version: asmVersion
 
-    examplesTestNGRunnerCompile group: 'org.testng', name: 'testng', version: testngVersion
-
     testCompile sourceSets.examples.output
     testCompile "junit:junit:$junitVersion"
     testCompile "com.google.guava:guava:$guavaVersion"
@@ -624,11 +617,6 @@
         JavaVersion.VERSION_11,
         false)
 setJdkCompilationWithCompatibility(
-        sourceSets.examplesTestNGRunner.compileJavaTaskName,
-        'jdk-11',
-        JavaVersion.VERSION_11,
-        false)
-setJdkCompilationWithCompatibility(
         sourceSets.examplesJava17.compileJavaTaskName,
         'jdk-17',
         JavaVersion.VERSION_17,
@@ -640,19 +628,6 @@
         JavaVersion.VERSION_17,
         false)
 
-task provideJdk11TestsDependencies(type: org.gradle.api.tasks.Copy) {
-    from sourceSets.examplesTestNGRunner.compileClasspath
-    include "**/**.jar"
-    into file("$buildDir/test/jdk11Tests")
-}
-
-task compileTestNGRunner (type: JavaCompile) {
-    dependsOn provideJdk11TestsDependencies
-    destinationDir = file("$buildDir/classes/java/examplesTestNGRunner")
-    source = sourceSets.examplesTestNGRunner.allSource
-    classpath = sourceSets.examplesTestNGRunner.compileClasspath
-}
-
 if (!project.hasProperty('without_error_prone') &&
         // Don't enable error prone on Java 8 as the plugin setup does not support it.
         !org.gradle.internal.jvm.Jvm.current().javaVersion.java8) {
@@ -2232,7 +2207,6 @@
         dependsOn buildExamples
         dependsOn buildKotlinR8TestResources
         dependsOn buildPreNJdwpTestsJar
-        dependsOn compileTestNGRunner
         dependsOn provideArtFrameworksDependencies
     } else {
         logger.lifecycle("WARNING: Testing in not supported on your platform. Testing is only fully supported on " +
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 080447c..10d9755 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -64,6 +64,7 @@
 import com.android.tools.r8.naming.ProguardMapMinifier;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
+import com.android.tools.r8.optimize.LegacyAccessModifier;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
@@ -452,6 +453,9 @@
       // to clear the cache, so that we will recompute the type lattice elements.
       appView.dexItemFactory().clearTypeElementsCache();
 
+      // TODO(b/132677331): Remove legacy access modifier.
+      LegacyAccessModifier.run(appViewWithLiveness, executorService, timing);
+
       // This pass attempts to reduce the number of nests and nest size to allow further passes, and
       // should therefore be run after the publicizer.
       new NestReducer(appViewWithLiveness).run(executorService, timing);
@@ -506,7 +510,7 @@
           .notifyHorizontalClassMergerFinished(HorizontalClassMerger.Mode.INITIAL);
 
       // TODO(b/225838009): Horizontal merging currently assumes pre-phase CF conversion.
-      appView.testing().enterLirSupportedPhase();
+      appView.testing().enterLirSupportedPhase(appView, executorService);
 
       new ProtoNormalizer(appViewWithLiveness).run(executorService, timing);
 
@@ -544,10 +548,10 @@
       appView.setGraphLens(new AppliedGraphLens(appView));
       timing.end();
 
-      // TODO(b/225838009): Support tracing and building LIR in Enqueuer.
-      PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-
-      if (options.shouldRerunEnqueuer()) {
+      if (!options.shouldRerunEnqueuer()) {
+        // TODO(b/225838009): Support tracing and building LIR in Enqueuer.
+        PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+      } else {
         timing.begin("Post optimization code stripping");
         try {
           GraphConsumer keptGraphConsumer = null;
@@ -574,9 +578,11 @@
           EnqueuerResult enqueuerResult =
               enqueuer.traceApplication(appView.rootSet(), executorService, timing);
           appView.setAppInfo(enqueuerResult.getAppInfo());
+          appView.dissallowFurtherInitClassUses();
+
           // Rerunning the enqueuer should not give rise to any method rewritings.
           MutableMethodConversionOptions conversionOptions =
-              MethodConversionOptions.forPostLirPhase(appView);
+              MethodConversionOptions.forLirPhase(appView);
           appView.withGeneratedMessageLiteBuilderShrinker(
               shrinker ->
                   shrinker.rewriteDeadBuilderReferencesFromDynamicMethods(
@@ -650,6 +656,9 @@
           timing.end();
         }
 
+        // TODO(b/225838009): Support LIR in proto shrinking.
+        PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+
         if (appView.options().protoShrinking().isProtoShrinkingEnabled()) {
           if (appView.options().protoShrinking().isEnumLiteProtoShrinkingEnabled()) {
             appView.protoShrinker().enumLiteProtoShrinker.verifyDeadEnumLiteMapsAreDead();
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 fe6e49c..825655a 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -10,6 +10,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
 
 /** Access flags common to classes, methods and fields. */
 public abstract class AccessFlags<T extends AccessFlags<T>> implements StructuralItem<T> {
@@ -65,6 +66,13 @@
     return AccessFlags::specify;
   }
 
+  public T applyIf(boolean condition, Consumer<T> fn) {
+    if (condition) {
+      fn.accept(self());
+    }
+    return self();
+  }
+
   public abstract T copy();
 
   @Override
@@ -231,8 +239,9 @@
     demote(Constants.ACC_SYNTHETIC);
   }
 
-  public void promoteToFinal() {
+  public T promoteToFinal() {
     promote(Constants.ACC_FINAL);
+    return self();
   }
 
   public T demoteFromFinal() {
@@ -248,9 +257,10 @@
     return isPromoted(Constants.ACC_PUBLIC);
   }
 
-  public void promoteToPublic() {
+  public T promoteToPublic() {
     demote(Constants.ACC_PRIVATE | Constants.ACC_PROTECTED);
     promote(Constants.ACC_PUBLIC);
+    return self();
   }
 
   public T withPublic() {
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 06deed3..d8597d2 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -619,8 +619,14 @@
     return false;
   }
 
+  private boolean disallowFurtherInitClassUses = false;
+
+  public void dissallowFurtherInitClassUses() {
+    disallowFurtherInitClassUses = true;
+  }
+
   public boolean canUseInitClass() {
-    return options().isShrinking() && !initClassLens.isFinal();
+    return !disallowFurtherInitClassUses && options().isShrinking();
   }
 
   public InitClassLens initClassLens() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index a87febd..538f0e3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -1047,6 +1047,16 @@
     permittedSubclasses.removeIf(predicate);
   }
 
+  public void replacePermittedSubclass(
+      DexType currentPermittedSubclass, DexType newPermittedSubclass) {
+    for (int i = 0; i < permittedSubclasses.size(); i++) {
+      if (permittedSubclasses.get(i).getPermittedSubclass() == currentPermittedSubclass) {
+        permittedSubclasses.set(i, new PermittedSubclassAttribute(newPermittedSubclass));
+        return;
+      }
+    }
+  }
+
   public boolean isLocalClass() {
     InnerClassAttribute innerClass = getInnerClassAttributeForThisClass();
     // The corresponding enclosing-method attribute might be not available, e.g., CF version 50.
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java b/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
index b2b6111..c2627bc 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
@@ -8,6 +8,7 @@
 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.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.FieldInstruction;
@@ -26,7 +27,7 @@
  * that it is dead.
  *
  * <p>Pruning code that does not verify is necessary in order to be able to assert that the types
- * are sound using {@link Instruction#verifyTypes(AppView, VerifyTypesHelper)}.
+ * are sound using {@link Instruction#verifyTypes(AppView, ProgramMethod, VerifyTypesHelper)}.
  */
 public class TypeChecker {
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index 9676db7..1c46b14 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
@@ -19,8 +18,8 @@
 import com.android.tools.r8.ir.code.Value;
 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.optimize.AffectedValues;
 import com.android.tools.r8.utils.BooleanBox;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.Deque;
@@ -28,7 +27,6 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 /**
  * Implementation of Sparse Conditional Constant Propagation from the paper of Wegman and Zadeck
@@ -107,12 +105,11 @@
         }
       }
       boolean hasChanged = rewriteConstants();
-      assert code.isConsistentSSA(appView);
       return CodeRewriterResult.hasChanged(hasChanged);
     }
 
     private boolean rewriteConstants() {
-      Set<Value> affectedValues = Sets.newIdentityHashSet();
+      AffectedValues affectedValues = new AffectedValues();
       List<BasicBlock> blockToAnalyze = new ArrayList<>();
       BooleanBox hasChanged = new BooleanBox(false);
       mapping.entrySet().stream()
@@ -122,7 +119,6 @@
                 Value value = entry.getKey();
                 ConstNumber evaluatedConst = entry.getValue().asConst().getConstNumber();
                 if (value.definition != evaluatedConst) {
-                  value.addAffectedValuesTo(affectedValues);
                   if (value.isPhi()) {
                     // D8 relies on dead code removal to get rid of the dead phi itself.
                     if (value.hasAnyUsers()) {
@@ -139,14 +135,14 @@
                         iterator.previous();
                       }
                       iterator.add(newConst);
-                      value.replaceUsers(newConst.outValue());
+                      value.replaceUsers(newConst.outValue(), affectedValues);
                       hasChanged.set();
                     }
                   } else {
                     BasicBlock block = value.definition.getBlock();
                     InstructionListIterator iterator = block.listIterator(code);
                     iterator.nextUntil(i -> i == value.definition);
-                    iterator.replaceCurrentInstruction(evaluatedConst);
+                    iterator.replaceCurrentInstruction(evaluatedConst, affectedValues);
                     hasChanged.set();
                   }
                 }
@@ -154,9 +150,7 @@
       for (BasicBlock block : blockToAnalyze) {
         block.deduplicatePhis();
       }
-      if (!affectedValues.isEmpty()) {
-        new TypeAnalysis(appView).narrowing(affectedValues);
-      }
+      affectedValues.narrowingWithAssumeRemoval(appView, code);
       boolean changed = hasChanged.get();
       if (changed) {
         code.removeAllDeadAndTrivialPhis();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 613d763..5907290 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -156,6 +156,7 @@
       }
       IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(code, instruction);
     }
+    assert code.isConsistentSSA(appView);
   }
 
   public boolean wasRemoved(DexField field) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
index 9ecd2a9..322907a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
@@ -21,13 +21,13 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.analysis.EnqueuerAnalysis;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.IRCode;
 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.InvokeVirtual;
+import com.android.tools.r8.ir.code.LinearFlowInstructionListIterator;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.SafeCheckCast;
 import com.android.tools.r8.ir.code.StaticGet;
@@ -36,6 +36,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.callgraph.Node;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.enums.EnumValueOptimizer;
@@ -250,6 +251,10 @@
             instruction ->
                 instruction.isNewInstance() && instruction.asNewInstance().clazz == builder.type);
     assert newInstance != null;
+    // Once the new instance is found, create a new linear iterator to allow subsequent instructions
+    // to be in trivially split blocks.
+    instructionIterator = new LinearFlowInstructionListIterator(code, newInstance.getBlock());
+    instructionIterator.nextUntil(i -> i == newInstance);
     instructionIterator.replaceCurrentInstruction(new NewInstance(builder.superType, builderValue));
 
     // Replace `builder.<init>()` by `builder.<init>(Message.DEFAULT_INSTANCE)`.
@@ -391,7 +396,7 @@
    * MethodToInvoke.NEW_MUTABLE_INSTANCE will create an instance of the enclosing class.
    */
   private void strengthenCheckCastInstructions(IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     InstructionListIterator instructionIterator = code.instructionListIterator();
     CheckCast checkCast;
     while ((checkCast = instructionIterator.nextUntil(Instruction::isCheckCast)) != null) {
@@ -425,9 +430,7 @@
         }
       }
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
   }
 
   private static class RootSetExtension {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index 608c06f..49b2e91 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -22,7 +22,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoMessageInfo;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoObject;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -44,16 +43,15 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodProcessorEventConsumer;
 import com.android.tools.r8.ir.conversion.OneTimeMethodProcessor;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.DependentMinimumKeepInfoCollection;
 import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
@@ -159,7 +157,7 @@
   }
 
   private void optimizeNewMutableInstance(AppView<AppInfoWithLiveness> appView, IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     BasicBlockIterator blockIterator = code.listIterator();
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
@@ -214,9 +212,8 @@
         }
       }
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    assert code.isConsistentSSA(appView);
   }
 
   private DexType getNewMutableInstanceType(
@@ -314,6 +311,7 @@
         assert false;
       }
     }
+    assert code.isConsistentSSA(appView);
   }
 
   private void rewriteArgumentsToNewMessageInfo(
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 3cf7971..a22f001 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
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoObjectFromStaticGet;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoTypeObject;
 import com.android.tools.r8.ir.code.ArrayPut;
+import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.DexItemBasedConstString;
@@ -333,50 +334,67 @@
     // Create an iterator for the block of interest.
     InstructionIterator instructionIterator = newArrayEmpty.getBlock().iterator();
     instructionIterator.nextUntil(instruction -> instruction == newArrayEmpty);
+    return new ArrayPutIterator(instructionIterator, objectsValue);
+  }
 
-    return new ThrowingIterator<Value, InvalidRawMessageInfoException>() {
+  private static class ArrayPutIterator
+      extends ThrowingIterator<Value, InvalidRawMessageInfoException> {
 
-      private int expectedNextIndex = 0;
+    private InstructionIterator instructionIterator;
+    private final Value objectsValue;
+    private int expectedNextIndex;
 
-      @Override
-      public boolean hasNext() {
-        while (instructionIterator.hasNext()) {
-          Instruction next = instructionIterator.peekNext();
-          if (isArrayPutOfInterest(next)) {
-            return true;
-          }
-          if (next.isJumpInstruction()) {
-            return false;
-          }
-          instructionIterator.next();
+    public ArrayPutIterator(InstructionIterator instructionIterator, Value objectsValue) {
+      this.instructionIterator = instructionIterator;
+      this.objectsValue = objectsValue;
+      expectedNextIndex = 0;
+    }
+
+    @Override
+    public boolean hasNext() {
+      while (instructionIterator.hasNext()) {
+        Instruction next = instructionIterator.peekNext();
+        if (isArrayPutOfInterest(next)) {
+          return true;
         }
-        return false;
+        if (next.isGoto()) {
+          // We may have split the block so allow continuing in linear jumps.
+          BasicBlock target = next.asGoto().getTarget();
+          assert target.hasUniquePredecessor();
+          instructionIterator = target.iterator();
+          continue;
+        }
+        if (next.isJumpInstruction()) {
+          return false;
+        }
+        instructionIterator.next();
+      }
+      return false;
+    }
+
+    @Override
+    public Value next() throws InvalidRawMessageInfoException {
+      if (!hasNext()) {
+        throw new NoSuchElementException();
+      }
+      ArrayPut arrayPut = instructionIterator.next().asArrayPut();
+
+      // Verify that the index correct.
+      Value indexValue = arrayPut.index().getAliasedValue();
+      if (indexValue.isPhi()
+          || !indexValue.definition.isConstNumber()
+          || indexValue.definition.asConstNumber().getIntValue() != expectedNextIndex) {
+        throw new InvalidRawMessageInfoException();
       }
 
-      @Override
-      public Value next() throws InvalidRawMessageInfoException {
-        if (!hasNext()) {
-          throw new NoSuchElementException();
-        }
-        ArrayPut arrayPut = instructionIterator.next().asArrayPut();
+      expectedNextIndex++;
+      return arrayPut.value().getAliasedValue();
+    }
 
-        // Verify that the index correct.
-        Value indexValue = arrayPut.index().getAliasedValue();
-        if (indexValue.isPhi()
-            || !indexValue.definition.isConstNumber()
-            || indexValue.definition.asConstNumber().getIntValue() != expectedNextIndex) {
-          throw new InvalidRawMessageInfoException();
-        }
-
-        expectedNextIndex++;
-        return arrayPut.value().getAliasedValue();
-      }
-
-      private boolean isArrayPutOfInterest(Instruction instruction) {
-        return instruction.isArrayPut()
-            && instruction.asArrayPut().array().getAliasedValue() == objectsValue;
-      }
-    };
+    private boolean isArrayPutOfInterest(Instruction instruction) {
+      return instruction.isArrayPut()
+          && instruction.asArrayPut().array().getAliasedValue() == objectsValue;
+    }
   }
 
   private static class InvalidRawMessageInfoException extends Exception {}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DestructivePhiTypeUpdater.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DestructivePhiTypeUpdater.java
index b0fc54d..1e624e9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/DestructivePhiTypeUpdater.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DestructivePhiTypeUpdater.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
-import com.google.common.collect.Sets;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.ListIterator;
@@ -56,7 +56,7 @@
     // replaced with correct types and all other phi operands are BOTTOM.
     assert verifyAllPhiOperandsAreBottom(affectedPhis);
 
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     worklist.addAll(affectedPhis);
     while (!worklist.isEmpty()) {
       Phi phi = worklist.poll();
@@ -69,13 +69,11 @@
       }
     }
 
-    assert new TypeAnalysis(appView).verifyValuesUpToDate(affectedPhis);
+    assert TypeAnalysis.verifyValuesUpToDate(appView, code, affectedPhis);
 
     // Now that the types of all transitively type affected phis have been reset, we can
     // perform a narrowing, starting from the values that are affected by those phis.
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
   }
 
   private boolean verifyAllPhiOperandsAreBottom(Set<Phi> affectedPhis) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeElement.java
index 1f06630..7fcdde4 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeElement.java
@@ -56,13 +56,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (!(o instanceof NullElement)) {
-        return false;
-      }
-      return true;
+      return this == o;
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
index a0fd66f..9752734 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
@@ -7,15 +7,20 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AssumeRemover;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.ArrayDeque;
-import java.util.Deque;
+import com.android.tools.r8.utils.ConsumerUtils;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.function.Consumer;
 
 public class TypeAnalysis {
 
@@ -29,30 +34,42 @@
 
   private final boolean mayHaveImpreciseTypes;
 
+  private boolean keepRedundantBlocksAfterAssumeRemoval = false;
   private Mode mode = Mode.UNSET;
 
   private final AppView<?> appView;
+  private final AssumeRemover assumeRemover;
+  private final IRCode code;
 
-  private final Deque<Value> worklist = new ArrayDeque<>();
+  private final WorkList<Value> worklist = WorkList.newIdentityWorkList();
 
-  public TypeAnalysis(AppView<?> appView) {
-    this(appView, false);
+  public TypeAnalysis(AppView<?> appView, IRCode code) {
+    this(appView, code, false);
   }
 
-  public TypeAnalysis(AppView<?> appView, boolean mayHaveImpreciseTypes) {
+  public TypeAnalysis(AppView<?> appView, IRCode code, boolean mayHaveImpreciseTypes) {
     this.appView = appView;
+    this.assumeRemover = new AssumeRemover(appView, code);
+    this.code = code;
     this.mayHaveImpreciseTypes = mayHaveImpreciseTypes;
   }
 
+  // Allows disabling the removal of redundant blocks after assume removal.
+  public TypeAnalysis setKeepRedundantBlocksAfterAssumeRemoval(
+      boolean keepRedundantBlocksAfterAssumeRemoval) {
+    this.keepRedundantBlocksAfterAssumeRemoval = keepRedundantBlocksAfterAssumeRemoval;
+    return this;
+  }
+
   private void analyze() {
-    while (!worklist.isEmpty()) {
-      analyzeValue(worklist.poll());
+    while (worklist.hasNext()) {
+      analyzeValue(worklist.removeSeen());
     }
   }
 
-  public void widening(IRCode code) {
+  public void widening() {
     mode = Mode.WIDENING;
-    assert worklist.isEmpty();
+    assert verifyIsEmpty();
     code.topologicallySortedBlocks().forEach(this::analyzeBasicBlock);
     analyze();
   }
@@ -61,9 +78,9 @@
     analyzeValues(values, Mode.WIDENING);
   }
 
-  public void narrowing(IRCode code) {
+  public void narrowing() {
     mode = Mode.NARROWING;
-    assert worklist.isEmpty();
+    assert verifyIsEmpty();
     code.topologicallySortedBlocks().forEach(this::analyzeBasicBlock);
     analyze();
   }
@@ -72,8 +89,47 @@
     analyzeValues(values, Mode.NARROWING);
   }
 
-  public boolean verifyValuesUpToDate(Iterable<? extends Value> values) {
-    analyzeValues(values, Mode.NO_CHANGE);
+  public void narrowingWithAssumeRemoval(Iterable<? extends Value> values) {
+    narrowingWithAssumeRemoval(values, ConsumerUtils.emptyConsumer());
+  }
+
+  public void narrowingWithAssumeRemoval(
+      Iterable<? extends Value> values, Consumer<Assume> redundantAssumeConsumer) {
+    narrowing(values);
+    removeRedundantAssumeInstructions(redundantAssumeConsumer);
+  }
+
+  private void removeRedundantAssumeInstructions(Consumer<Assume> redundantAssumeConsumer) {
+    Set<Value> affectedValuesFromAssumeRemoval = Sets.newIdentityHashSet();
+    while (assumeRemover.removeRedundantAssumeInstructions(
+        affectedValuesFromAssumeRemoval, redundantAssumeConsumer)) {
+      widening(affectedValuesFromAssumeRemoval);
+      Set<Value> affectedValuesFromPhiRemoval = Sets.newIdentityHashSet();
+      code.removeAllDeadAndTrivialPhis(affectedValuesFromPhiRemoval);
+      narrowing(affectedValuesFromPhiRemoval);
+      affectedValuesFromAssumeRemoval.clear();
+    }
+    if (!keepRedundantBlocksAfterAssumeRemoval) {
+      code.removeRedundantBlocks();
+    }
+  }
+
+  public boolean verifyIsEmpty() {
+    assert !assumeRemover.hasAffectedAssumeInstructions();
+    assert worklist.isEmpty();
+    return true;
+  }
+
+  public static void verifyValuesUpToDate(AppView<?> appView, IRCode code) {
+    TypeAnalysis typeAnalysis = new TypeAnalysis(appView, code);
+    typeAnalysis.mode = Mode.NO_CHANGE;
+    code.topologicallySortedBlocks().forEach(typeAnalysis::analyzeBasicBlock);
+    typeAnalysis.analyze();
+  }
+
+  public static boolean verifyValuesUpToDate(
+      AppView<?> appView, IRCode code, Iterable<? extends Value> values) {
+    new TypeAnalysis(appView, code).analyzeValues(values, Mode.NO_CHANGE);
     return true;
   }
 
@@ -85,10 +141,7 @@
   }
 
   private void enqueue(Value v) {
-    assert v != null;
-    if (!worklist.contains(v)) {
-      worklist.add(v);
-    }
+    worklist.addFirstIfNotSeen(v);
   }
 
   private void analyzeBasicBlock(BasicBlock block) {
@@ -124,12 +177,27 @@
   private void updateTypeOfValue(Value value, TypeElement type) {
     assert mode != Mode.UNSET;
 
+    if (value.isDefinedByInstructionSatisfying(Instruction::isAssume)) {
+      assumeRemover.addAffectedAssumeInstruction(value.getDefinition().asAssume());
+    }
+
     TypeElement current = value.getType();
     if (current.equals(type)) {
       return;
     }
 
-    assert mode != Mode.NO_CHANGE;
+    assert mode != Mode.NO_CHANGE
+        : "Unexpected type change for value "
+            + value
+            + " defined by "
+            + (value.isPhi() ? "phi" : value.getDefinition())
+            + ": was "
+            + type
+            + ", but expected "
+            + current
+            + " (context: "
+            + code.context()
+            + ")";
 
     if (type.isBottom()) {
       return;
@@ -139,7 +207,7 @@
       value.widening(appView, type);
     } else {
       assert mode == Mode.NARROWING;
-      value.narrowing(appView, type);
+      value.narrowing(appView, code.context(), type);
     }
 
     // propagate the type change to (instruction) users if any.
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java
index a4ed229..020db76 100644
--- a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.dex.code.DexConst4;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
@@ -55,6 +56,12 @@
   }
 
   @Override
+  public TypeElement evaluate(AppView<?> appView) {
+    assert outValue.getType().isInt();
+    return TypeElement.getInt();
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Assume.java b/src/main/java/com/android/tools/r8/ir/code/Assume.java
index 174b913..279d1b9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Assume.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Assume.java
@@ -10,14 +10,15 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.lightir.LirBuilder;
-import java.util.Objects;
 import java.util.Set;
 
 public class Assume extends Instruction {
@@ -25,41 +26,32 @@
   private static final String ERROR_MESSAGE =
       "Expected Assume instructions to be removed after IR processing.";
 
-  private DynamicTypeAssumption dynamicTypeAssumption;
-  private final NonNullAssumption nonNullAssumption;
+  private DynamicType dynamicType;
   private final Instruction origin;
 
-  public Assume(
-      DynamicTypeAssumption dynamicTypeAssumption,
-      NonNullAssumption nonNullAssumption,
-      Value dest,
-      Value src,
-      Instruction origin,
-      AppView<?> appView) {
+  public Assume(DynamicType dynamicType, Value dest, Value src, Instruction origin) {
     super(dest, src);
-    assert dynamicTypeAssumption != null || nonNullAssumption != null;
-    assert dynamicTypeAssumption == null
-        || dynamicTypeAssumption.verifyCorrectnessOfValues(dest, src, appView);
-    assert nonNullAssumption == null
-        || nonNullAssumption.verifyCorrectnessOfValues(dest, src, appView);
-    assert dest != null;
-    this.dynamicTypeAssumption = dynamicTypeAssumption;
-    this.nonNullAssumption = nonNullAssumption;
+    assert dynamicType != null;
+    assert !dynamicType.isUnknown();
+    this.dynamicType = dynamicType;
     this.origin = origin;
   }
 
-  public static Assume createAssumeNonNullInstruction(
-      Value dest, Value src, Instruction origin, AppView<?> appView) {
-    return new Assume(null, NonNullAssumption.get(), dest, src, origin, appView);
-  }
-
-  public static Assume createAssumeDynamicTypeInstruction(
-      DynamicTypeWithUpperBound dynamicType,
+  public static Assume create(
+      DynamicType dynamicType,
       Value dest,
       Value src,
       Instruction origin,
-      AppView<?> appView) {
-    return new Assume(new DynamicTypeAssumption(dynamicType), null, dest, src, origin, appView);
+      AppView<?> appView,
+      ProgramMethod context) {
+    Assume assume = new Assume(dynamicType, dest, src, origin);
+    assert assume.verifyInstruction(appView, context);
+    return assume;
+  }
+
+  public void clearDynamicTypeAssumption() {
+    assert dynamicType.getNullability().isDefinitelyNotNull();
+    dynamicType = DynamicType.definitelyNotNull();
   }
 
   @Override
@@ -67,24 +59,17 @@
     return Opcodes.ASSUME;
   }
 
-  public boolean verifyInstructionIsNeeded(AppView<?> appView) {
-    if (hasDynamicTypeAssumption()) {
-      assert dynamicTypeAssumption.verifyCorrectnessOfValues(outValue(), src(), appView);
-    }
-    return true;
-  }
-
   @Override
   public <T> T accept(InstructionVisitor<T> visitor) {
     return visitor.visit(this);
   }
 
-  public DynamicTypeAssumption getDynamicTypeAssumption() {
-    return dynamicTypeAssumption;
+  public DynamicType getDynamicType() {
+    return dynamicType;
   }
 
-  public NonNullAssumption getNonNullAssumption() {
-    return nonNullAssumption;
+  public DynamicTypeWithUpperBound getDynamicTypeAssumption() {
+    return dynamicType.asDynamicTypeWithUpperBound();
   }
 
   public Value src() {
@@ -115,16 +100,17 @@
     return this;
   }
 
-  public boolean hasDynamicTypeAssumption() {
-    return dynamicTypeAssumption != null;
-  }
-
-  public void unsetDynamicTypeAssumption() {
-    dynamicTypeAssumption = null;
+  public boolean hasDynamicTypeIgnoringNullability() {
+    if (dynamicType.isNotNullType()) {
+      return false;
+    }
+    assert !dynamicType.isUnknown();
+    assert dynamicType.isDynamicTypeWithUpperBound();
+    return true;
   }
 
   public boolean hasNonNullAssumption() {
-    return nonNullAssumption != null;
+    return dynamicType.getNullability().isDefinitelyNotNull();
   }
 
   @Override
@@ -135,8 +121,8 @@
     if (outType.isPrimitiveType()) {
       return false;
     }
-    if (hasDynamicTypeAssumption()) {
-      outType = dynamicTypeAssumption.getDynamicType().getDynamicUpperBoundType();
+    if (hasDynamicTypeIgnoringNullability()) {
+      outType = dynamicType.asDynamicTypeWithUpperBound().getDynamicUpperBoundType();
     }
     if (appView.appInfo().hasLiveness()) {
       if (outType.isClassType()
@@ -199,8 +185,7 @@
       return false;
     }
     Assume assumeInstruction = other.asAssume();
-    return Objects.equals(dynamicTypeAssumption, assumeInstruction.dynamicTypeAssumption)
-        && Objects.equals(nonNullAssumption, assumeInstruction.nonNullAssumption);
+    return dynamicType.equals(assumeInstruction.dynamicType);
   }
 
   @Override
@@ -249,22 +234,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    assert super.verifyTypes(appView, verifyTypesHelper);
-
-    TypeElement inType = src().getType();
-    assert inType.isReferenceType() : inType;
-
-    TypeElement outType = getOutType();
-    if (hasNonNullAssumption()) {
-      assert inType.isNullType() || outType.equals(inType.asReferenceType().asMeetWithNotNull())
-          : "At " + this + System.lineSeparator() + outType + " != " + inType;
-    } else {
-      assert hasDynamicTypeAssumption();
-      assert !src().isConstNumber();
-      assert outType.equals(inType)
-          : "At " + this + System.lineSeparator() + outType + " != " + inType;
-    }
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    verifyInstruction(appView, context);
     return true;
   }
 
@@ -279,8 +251,8 @@
     if (hasNonNullAssumption()) {
       builder.append("; not null");
     }
-    if (hasDynamicTypeAssumption()) {
-      DynamicTypeWithUpperBound dynamicType = dynamicTypeAssumption.getDynamicType();
+    if (hasDynamicTypeIgnoringNullability()) {
+      DynamicTypeWithUpperBound dynamicType = getDynamicType().asDynamicTypeWithUpperBound();
       if (hasOutValue()) {
         if (!dynamicType.getDynamicUpperBoundType().equalUpToNullability(outValue.getType())) {
           builder.append("; upper bound: ").append(dynamicType.getDynamicUpperBoundType());
@@ -293,59 +265,58 @@
     return builder.toString();
   }
 
-  public static class DynamicTypeAssumption {
-
-    private final DynamicTypeWithUpperBound dynamicType;
-
-    public DynamicTypeAssumption(DynamicTypeWithUpperBound dynamicType) {
-      assert dynamicType != null;
-      this.dynamicType = dynamicType;
-    }
-
-    public DynamicTypeWithUpperBound getDynamicType() {
-      return dynamicType;
-    }
-
-    public boolean verifyCorrectnessOfValues(Value dest, Value src, AppView<?> appView) {
+  public boolean verifyInstruction(AppView<?> appView, ProgramMethod context) {
+    assert !src().isConstant()
+        : "Unexpected Assume value "
+            + outValue()
+            + " for constant value "
+            + src()
+            + " defined by "
+            + src().getDefinition()
+            + " (context: "
+            + context.toSourceString()
+            + ", type: "
+            + src().getType()
+            + ")";
+    assert !src().getType().isDefinitelyNull();
+    assert !src().getType().isNullType();
+    assert hasOutValue();
+    if (hasDynamicTypeIgnoringNullability()) {
       assert !dynamicType.isBottom();
+      assert !dynamicType.isNotNullType();
+      assert !dynamicType.isNullType();
       assert !dynamicType.isUnknown();
-      assert dynamicType
+      DynamicTypeWithUpperBound dynamicTypeWithUpperBound =
+          dynamicType.asDynamicTypeWithUpperBound();
+      assert dynamicTypeWithUpperBound
           .getDynamicUpperBoundType()
-          .lessThanOrEqualUpToNullability(src.getType(), appView);
-      return true;
+          .lessThanOrEqualUpToNullability(src().getType(), appView);
+    } else {
+      assert dynamicType.isNotNullType();
+      assert hasNonNullAssumption();
+      assert !src().getType().isDefinitelyNotNull()
+          : "Unexpected AssumeNotNull instruction for non-null value "
+              + src()
+              + " defined by "
+              + (src().isPhi() ? "phi" : src().getDefinition())
+              + " (context: "
+              + context.toSourceString()
+              + ", type: "
+              + src().getType()
+              + ")";
     }
-
-    @Override
-    public boolean equals(Object other) {
-      if (other == null) {
-        return false;
-      }
-      if (getClass() != other.getClass()) {
-        return false;
-      }
-      DynamicTypeAssumption assumption = (DynamicTypeAssumption) other;
-      return dynamicType.equals(assumption.dynamicType);
-    }
-
-    @Override
-    public int hashCode() {
-      return dynamicType.hashCode();
-    }
-  }
-
-  public static class NonNullAssumption {
-
-    private static final NonNullAssumption instance = new NonNullAssumption();
-
-    private NonNullAssumption() {}
-
-    public static NonNullAssumption get() {
-      return instance;
-    }
-
-    public boolean verifyCorrectnessOfValues(Value dest, Value src, AppView<?> appView) {
-      assert !src.isNeverNull();
-      return true;
-    }
+    assert !hasNonNullAssumption() || outValue().getType().isDefinitelyNotNull()
+        : "Unexpected nullability for value "
+            + outValue()
+            + " defined by "
+            + this
+            + ": "
+            + outValue().getType().nullability()
+            + ", but expected: "
+            + Nullability.definitelyNotNull()
+            + " (context: "
+            + context.toSourceString()
+            + ")";
+    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index bef0a26..588a39a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -103,9 +103,10 @@
     return true;
   }
 
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
     assert instructions.stream()
-        .allMatch(instruction -> instruction.verifyTypes(appView, verifyTypesHelper));
+        .allMatch(instruction -> instruction.verifyTypes(appView, context, verifyTypesHelper));
     return true;
   }
 
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 bc3297f..510ed2d 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
@@ -242,7 +242,7 @@
     }
     if (current.hasUsedOutValue()) {
       assert newInstruction.outValue() != null;
-      if (affectedValues != null && newInstruction.getOutType() != current.getOutType()) {
+      if (affectedValues != null && !newInstruction.getOutType().equals(current.getOutType())) {
         current.outValue().addAffectedValuesTo(affectedValues);
       }
       current.outValue().replaceUsers(newInstruction.outValue());
@@ -881,7 +881,7 @@
     assert entryBlock.getInstructions().stream().noneMatch(Instruction::isArgument);
 
     // Actual arguments are flown to the inlinee.
-    new TypeAnalysis(appView).narrowing(argumentUsers);
+    new TypeAnalysis(appView, code).narrowing(argumentUsers);
 
     // The inline entry is the first block now the argument instructions are gone.
     BasicBlock inlineEntry = inlinee.entryBlock();
@@ -900,8 +900,9 @@
         Return returnInstruction = inlineeIterator.peekNext().asReturn();
         invoke.outValue().replaceUsers(returnInstruction.returnValue());
         // The return type is flown to the original context.
-        new TypeAnalysis(appView)
-            .narrowing(
+        new TypeAnalysis(appView, code)
+            .setKeepRedundantBlocksAfterAssumeRemoval(true)
+            .narrowingWithAssumeRemoval(
                 Iterables.concat(
                     ImmutableList.of(returnInstruction.returnValue()), affectedValues));
       }
@@ -962,7 +963,9 @@
       DominatorTree dominatorTree = new DominatorTree(code, MAY_HAVE_UNREACHABLE_BLOCKS);
       Set<Value> affectedValues = Sets.newIdentityHashSet();
       blocksToRemove.addAll(invokePredecessor.unlink(invokeBlock, dominatorTree, affectedValues));
-      new TypeAnalysis(appView).narrowing(affectedValues);
+      new TypeAnalysis(appView, code)
+          .setKeepRedundantBlocksAfterAssumeRemoval(true)
+          .narrowingWithAssumeRemoval(affectedValues);
     }
 
     // Position the iterator after the invoke block.
@@ -1013,7 +1016,7 @@
                 null,
                 RegisterReadType.NORMAL);
         phi.addOperands(operands);
-        new TypeAnalysis(appView).widening(ImmutableSet.of(phi));
+        new TypeAnalysis(appView, code).widening(ImmutableSet.of(phi));
         value = phi;
       }
       newReturn = new Return(value);
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index ed02fe2..a202a35 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -216,8 +216,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    assert super.verifyTypes(appView, verifyTypesHelper);
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, context, verifyTypesHelper);
 
     TypeElement inType = object().getType();
 
@@ -226,31 +227,24 @@
     TypeElement outType = getOutType();
     TypeElement castType = TypeElement.fromDexType(getType(), inType.nullability(), appView);
 
-    if (inType.lessThanOrEqual(castType, appView)) {
-      // Cast can be removed. Check that it is sound to replace all users of the out-value by the
-      // in-value.
-      assert inType.lessThanOrEqual(outType, appView);
+    // We don't have enough information to remove the cast. Check that the out-value does not
+    // have a more precise type than the cast-type.
+    assert outType.equalUpToNullability(castType);
 
-      // TODO(b/72693244): Consider checking equivalence. This requires that the types are always
-      // as precise as possible, though, meaning that almost all changes to the IR must be followed
-      // by a fix-point analysis.
-      // assert outType.equals(inType);
-    } else {
-      // We don't have enough information to remove the cast. Check that the out-value does not
-      // have a more precise type than the cast-type.
-      assert outType.equalUpToNullability(castType);
+    // Check soundness of null information.
+    assert inType.nullability() == outType.nullability() || inType.isNullType()
+        : "Expected nullability of value "
+            + outValue()
+            + " defined by "
+            + this
+            + " to be "
+            + inType.nullability()
+            + ", but was "
+            + outType.nullability()
+            + "(context: "
+            + context.toSourceString()
+            + ")";
 
-      // Check soundness of null information.
-      assert inType.nullability() == outType.nullability();
-
-      // Since we cannot remove the cast the in-value must be different from null.
-      assert !inType.isNullType();
-
-      // TODO(b/72693244): Consider checking equivalence. This requires that the types are always
-      // as precise as possible, though, meaning that almost all changes to the IR must be followed
-      // by a fix-point analysis.
-      // assert outType.equals(castType);
-    }
     return true;
   }
 
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 00b983b..ddb8bf1 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
@@ -332,8 +332,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    assert super.verifyTypes(appView, verifyTypesHelper);
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, context, verifyTypesHelper);
     assert !isZero() || getOutType().isPrimitiveType() || getOutType().isNullType();
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index 803223f..9c1a421 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -178,8 +178,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    assert super.verifyTypes(appView, verifyTypesHelper);
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, context, verifyTypesHelper);
     TypeElement expectedType = TypeElement.stringClassType(appView, definitelyNotNull());
     assert getOutType().equals(expectedType);
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
index 9bb5f9a..7304858 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.cf.code.CfStore;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.lightir.LirBuilder;
@@ -83,8 +84,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    super.verifyTypes(appView, verifyTypesHelper);
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    super.verifyTypes(appView, context, verifyTypesHelper);
     assert verifyTypesHelper.isAssignable(src().getType(), getOutType());
     return true;
   }
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 a0dc9f6..a4db6a0 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
@@ -20,11 +20,13 @@
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.IRControlFlowGraph;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DequeUtils;
@@ -38,7 +40,6 @@
 import com.android.tools.r8.utils.TriFunction;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
@@ -589,12 +590,14 @@
   public boolean isConsistentSSA(AppView<?> appView) {
     isConsistentSSABeforeTypesAreCorrect(appView);
     assert verifyNoImpreciseOrBottomTypes();
+    assert verifyTypes(appView);
     return true;
   }
 
   public boolean isConsistentSSAAllowingRedundantBlocks(AppView<?> appView) {
     isConsistentSSABeforeTypesAreCorrectAllowingRedundantBlocks(appView);
     assert verifyNoImpreciseOrBottomTypes();
+    assert verifyTypes(appView);
     return true;
   }
 
@@ -658,22 +661,11 @@
     // We can only type check the program if we have subtyping information. Therefore, we do not
     // require that the program type checks in D8.
     VerifyTypesHelper verifyTypesHelper = VerifyTypesHelper.create(appView);
-    if (appView.enableWholeProgramOptimizations()) {
-      assert validAssumeInstructions(appView);
-      assert new TypeChecker(appView.withLiveness(), verifyTypesHelper).check(this);
-    }
-    assert blocks.stream().allMatch(block -> block.verifyTypes(appView, verifyTypesHelper));
-    return true;
-  }
-
-  private boolean validAssumeInstructions(AppView<?> appView) {
-    for (BasicBlock block : blocks) {
-      for (Instruction instruction : block.getInstructions()) {
-        if (instruction.isAssume()) {
-          assert instruction.asAssume().verifyInstructionIsNeeded(appView);
-        }
-      }
-    }
+    assert !appView.enableWholeProgramOptimizations()
+        || new TypeChecker(appView.withLiveness(), verifyTypesHelper).check(this);
+    TypeAnalysis.verifyValuesUpToDate(appView, this);
+    assert blocks.stream()
+        .allMatch(block -> block.verifyTypes(appView, context(), verifyTypesHelper));
     return true;
   }
 
@@ -723,7 +715,7 @@
       int predecessorCount = block.getPredecessors().size();
       // Check that all phi uses are consistent.
       for (Phi phi : block.getPhis()) {
-        assert !phi.isTrivialPhi();
+        assert !phi.isTrivialPhi() : "Unexpected trivial phi in " + context().toSourceString();
         assert phi.getOperands().size() == predecessorCount;
         addValueAndCheckUniqueNumber(values, phi);
         for (Value value : phi.getOperands()) {
@@ -1247,7 +1239,7 @@
   }
 
   public ConstClass createConstClass(AppView<?> appView, DexType type) {
-    Value out = createValue(TypeElement.fromDexType(type, definitelyNotNull(), appView));
+    Value out = createValue(TypeElement.classClassType(appView, definitelyNotNull()));
     return new ConstClass(out, type);
   }
 
@@ -1507,20 +1499,20 @@
     return unreachableBlocks;
   }
 
-  public Set<Value> removeUnreachableBlocks() {
-    ImmutableSet.Builder<Value> affectedValueBuilder = ImmutableSet.builder();
+  public AffectedValues removeUnreachableBlocks() {
+    AffectedValues affectedValues = new AffectedValues();
     int color = reserveMarkingColor();
     markTransitiveSuccessors(entryBlock(), color);
     ListIterator<BasicBlock> blockIterator = listIterator();
     while (blockIterator.hasNext()) {
       BasicBlock current = blockIterator.next();
       if (!current.isMarked(color)) {
-        affectedValueBuilder.addAll(current.cleanForRemoval());
+        affectedValues.addAll(current.cleanForRemoval());
         blockIterator.remove();
       }
     }
     returnMarkingColor(color);
-    return affectedValueBuilder.build();
+    return affectedValues;
   }
 
   // Note: It is the responsibility of the caller to return the marking color.
diff --git a/src/main/java/com/android/tools/r8/ir/code/If.java b/src/main/java/com/android/tools/r8/ir/code/If.java
index 0149b70..b0ecd2d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/If.java
+++ b/src/main/java/com/android/tools/r8/ir/code/If.java
@@ -125,15 +125,22 @@
 
   @Override
   public String toString() {
-    return super.toString()
-        + " "
-        + type
-        + (isZeroTest() ? "Z" : " ")
-        + " block "
-        + getTrueTarget().getNumberAsString()
-        + " (fallthrough "
-        + fallthroughBlock().getNumberAsString()
-        + ")";
+    StringBuilder builder =
+        new StringBuilder(super.toString())
+            .append(' ')
+            .append(type)
+            .append(isZeroTest() ? 'Z' : ' ');
+    // If this instruction is in a block that has been marked for removal, but not yet removed from
+    // the IR, make sure we can still print the code.
+    if (getBlock().exit() == this && getBlock().getSuccessors().size() >= 2) {
+      builder
+          .append(" block ")
+          .append(getTrueTarget().getNumberAsString())
+          .append(" (fallthrough ")
+          .append(fallthroughBlock().getNumberAsString())
+          .append(')');
+    }
+    return builder.toString();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 9321203..2da97aa 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -95,6 +95,14 @@
     this.position = position;
   }
 
+  public void setPosition(Position position, InternalOptions options) {
+    if (instructionTypeCanThrow() || options.debug) {
+      setPosition(position);
+    } else {
+      setPosition(Position.none());
+    }
+  }
+
   public void forceOverwritePosition(Position position) {
     assert position != null;
     assert this.position != null;
@@ -228,6 +236,10 @@
   public abstract void buildLir(LirBuilder<Value, ?> builder);
 
   public void replaceValue(Value oldValue, Value newValue) {
+    replaceValue(oldValue, newValue, null);
+  }
+
+  public void replaceValue(Value oldValue, Value newValue, Set<Value> affectedValues) {
     for (int i = 0; i < inValues.size(); i++) {
       if (oldValue == inValues.get(i)) {
         inValues.set(i, newValue);
@@ -235,6 +247,9 @@
       }
     }
     oldValue.removeUser(this);
+    if (affectedValues != null && hasOutValue()) {
+      affectedValues.add(outValue());
+    }
   }
 
   public void replaceValue(int index, Value newValue) {
@@ -757,7 +772,7 @@
   }
 
   public final boolean isAssumeWithDynamicTypeAssumption() {
-    return isAssume() && asAssume().hasDynamicTypeAssumption();
+    return isAssume() && asAssume().hasDynamicTypeIgnoringNullability();
   }
 
   public final boolean isAssumeWithNonNullAssumption() {
@@ -1459,21 +1474,8 @@
         "Implement type lattice evaluation for: " + getInstructionName());
   }
 
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    if (outValue != null) {
-      TypeElement outTypeElement = outValue.getType();
-      if (outTypeElement.isArrayType()) {
-        DexType outBaseType =
-            outTypeElement
-                .asArrayType()
-                .toDexType(appView.dexItemFactory())
-                .toBaseType(appView.dexItemFactory());
-        assert appView.graphLens().lookupType(outBaseType) == outBaseType;
-      } else if (outTypeElement.isClassType()) {
-        DexType outType = outTypeElement.asClassType().getClassType();
-        assert appView.graphLens().lookupType(outType) == outType;
-      }
-    }
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index a0c67d5..9d49a0b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -146,8 +146,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    assert super.verifyTypes(appView, verifyTypesHelper);
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, context, verifyTypesHelper);
 
     Value receiver = getReceiver();
     TypeElement receiverType = receiver.getType();
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 d4951ac..b397c80 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
@@ -54,6 +54,11 @@
   }
 
   @Override
+  public void setInsertionPosition(Position position) {
+    currentBlockIterator.setInsertionPosition(position);
+  }
+
+  @Override
   public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
     currentBlockIterator.replaceCurrentInstruction(newInstruction, affectedValues);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 5dc04da..a1c5475 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -131,8 +131,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
-    super.verifyTypes(appView, verifyTypesHelper);
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
+    super.verifyTypes(appView, context, verifyTypesHelper);
     // DebugLocalWrite defines it's own verification of types but should be allowed to call super.
     if (!this.isDebugLocalWrite()) {
       assert src().getType().equals(getOutType());
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index 0fa2583..56de63b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -227,7 +227,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
     TypeElement type = getOutType();
     assert type.isClassType();
     assert type.asClassType().getClassType() == clazz || appView.options().testing.allowTypeErrors;
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java
index d47c812..d42756d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewUnboxedEnumInstance.java
@@ -155,7 +155,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+  public boolean verifyTypes(
+      AppView<?> appView, ProgramMethod context, VerifyTypesHelper verifyTypesHelper) {
     TypeElement type = getOutType();
     assert type.isClassType();
     assert type.asClassType().getClassType() == clazz || appView.options().testing.allowTypeErrors;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 049c514..944d68e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -208,19 +208,33 @@
   }
 
   public void replaceOperandAt(int predIndex, Value newValue) {
+    replaceOperandAt(predIndex, newValue, null);
+  }
+
+  public void replaceOperandAt(int predIndex, Value newValue, Set<Value> affectedValues) {
     Value current = operands.get(predIndex);
     operands.set(predIndex, newValue);
     newValue.addPhiUser(this);
     current.removePhiUser(this);
+    if (affectedValues != null) {
+      affectedValues.add(this);
+    }
   }
 
   public void replaceOperand(Value current, Value newValue) {
+    replaceOperand(current, newValue, null);
+  }
+
+  public void replaceOperand(Value current, Value newValue, Set<Value> affectedValues) {
     for (int i = 0; i < operands.size(); i++) {
       if (operands.get(i) == current) {
         operands.set(i, newValue);
         newValue.addPhiUser(this);
       }
     }
+    if (affectedValues != null) {
+      affectedValues.add(this);
+    }
   }
 
   public boolean isTrivialPhi() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java b/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
index 5d079559..1b4372e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
@@ -97,7 +97,11 @@
 
   @Override
   public void buildLir(LirBuilder<Value, ?> builder) {
-    throw new Unreachable();
+    BasicBlock[] targetBlocks = new BasicBlock[keys.length];
+    for (int i = 0; i < keys.length; i++) {
+      targetBlocks[i] = targetBlock(i);
+    }
+    builder.addStringSwitch(value(), keys, targetBlocks, fallthroughBlock());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index fcec4f3..3257f90 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.NumberFromIntervalValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.regalloc.LiveIntervals;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.MethodPosition;
@@ -582,10 +583,10 @@
   }
 
   // Returns the set of Value that are affected if the current value's type lattice is updated.
-  public Set<Value> affectedValues() {
-    ImmutableSet.Builder<Value> affectedValues = ImmutableSet.builder();
+  public AffectedValues affectedValues() {
+    AffectedValues affectedValues = new AffectedValues();
     forEachAffectedValue(affectedValues::add);
-    return affectedValues.build();
+    return affectedValues;
   }
 
   public void addAffectedValuesTo(Set<Value> affectedValues) {
@@ -610,16 +611,10 @@
       return;
     }
     for (Instruction user : uniqueUsers()) {
-      user.replaceValue(this, newValue);
-      if (affectedValues != null && user.hasOutValue()) {
-        affectedValues.add(user.outValue);
-      }
+      user.replaceValue(this, newValue, affectedValues);
     }
     for (Phi user : uniquePhiUsers()) {
-      user.replaceOperand(this, newValue);
-      if (affectedValues != null) {
-        affectedValues.add(user);
-      }
+      user.replaceOperand(this, newValue, affectedValues);
     }
     if (debugData != null) {
       for (Instruction user : debugData.users) {
@@ -656,6 +651,14 @@
       Value newValue,
       Set<Instruction> selectedInstructions,
       Map<Phi, IntList> selectedPhisWithPredecessorIndexes) {
+    replaceSelectiveUsers(newValue, selectedInstructions, selectedPhisWithPredecessorIndexes, null);
+  }
+
+  public void replaceSelectiveUsers(
+      Value newValue,
+      Set<Instruction> selectedInstructions,
+      Map<Phi, IntList> selectedPhisWithPredecessorIndexes,
+      Set<Value> affectedValues) {
     if (this == newValue) {
       return;
     }
@@ -665,7 +668,7 @@
     for (Instruction user : uniqueUsers()) {
       if (selectedInstructions.contains(user)) {
         fullyRemoveUser(user);
-        user.replaceValue(this, newValue);
+        user.replaceValue(this, newValue, affectedValues);
       }
     }
     Set<Phi> selectedPhis = selectedPhisWithPredecessorIndexes.keySet();
@@ -679,7 +682,7 @@
         }
         for (int position : positionsToUpdate) {
           assert user.getOperand(position) == this;
-          user.replaceOperandAt(position, newValue);
+          user.replaceOperandAt(position, newValue, affectedValues);
         }
       }
     }
@@ -1017,7 +1020,7 @@
     setType(newType);
   }
 
-  public void narrowing(AppView<?> appView, TypeElement newType) {
+  public void narrowing(AppView<?> appView, ProgramMethod context, TypeElement newType) {
     // During NARROWING (e.g., after inlining), type update is monotonically downwards,
     //   i.e., towards something narrower, with more specific type info.
     assert skipWideningOrNarrowingCheck(appView) || !this.type.strictlyLessThan(newType, appView)
@@ -1026,7 +1029,10 @@
             + " < "
             + newType
             + " at "
-            + (isPhi() ? asPhi().printPhi() : definition.toString());
+            + (isPhi() ? asPhi().printPhi() : definition.toString())
+            + " (context: "
+            + context
+            + ")";
     setType(newType);
   }
 
@@ -1050,54 +1056,39 @@
 
   public TypeElement getDynamicUpperBoundType(
       AppView<? extends AppInfoWithClassHierarchy> appView) {
-    Value root = getAliasedValue();
-    if (root.isPhi()) {
-      assert getSpecificAliasedValue(
-              value ->
-                  value.isDefinedByInstructionSatisfying(
-                      Instruction::isAssumeWithDynamicTypeAssumption))
-          == null;
-      TypeElement result = root.getDynamicUpperBoundType(appView);
-      if (getType().isReferenceType() && getType().isDefinitelyNotNull()) {
-        return result.asReferenceType().asMeetWithNotNull();
-      }
-      return result;
-    }
-
     // Try to find an alias of the receiver, which is defined by an instruction of the type Assume.
     Value aliasedValue =
         getSpecificAliasedValue(
             value ->
                 value.isDefinedByInstructionSatisfying(
                     Instruction::isAssumeWithDynamicTypeAssumption));
-    TypeElement lattice;
+    Value root = getAliasedValue();
+    TypeElement upperBoundType;
     if (aliasedValue != null) {
       // If there is an alias of the receiver, which is defined by an Assume instruction that
       // carries a dynamic type, then use the dynamic type as the refined receiver type.
-      lattice =
+      upperBoundType =
           aliasedValue
-              .definition
+              .getDefinition()
               .asAssume()
               .getDynamicTypeAssumption()
-              .getDynamicType()
               .getDynamicUpperBoundType();
-
-      // For precision, verify that the dynamic type is at least as precise as the static type.
-      assert lattice.lessThanOrEqualUpToNullability(type, appView) : type + " < " + lattice;
+    } else if (root.isPhi()) {
+      upperBoundType = root.getDynamicUpperBoundType(appView);
     } else {
       // Otherwise, simply use the static type.
-      lattice = type;
+      upperBoundType = type;
     }
 
     // Account for nullability, which could be flown from non-null assumption in between dynamic
     // type assumption or simply from array/object creation.
-    if (type.isDefinitelyNotNull() && lattice.isNullable()) {
+    if (type.isDefinitelyNotNull() && upperBoundType.isNullable()) {
       // Having non-null assumption means it is a reference type.
-      assert lattice.isReferenceType();
+      assert upperBoundType.isReferenceType();
       // Then, we can return the non-null variant of dynamic type if both assumptions are aliased.
-      return lattice.asReferenceType().asMeetWithNotNull();
+      return upperBoundType.asReferenceType().asMeetWithNotNull();
     }
-    return lattice;
+    return upperBoundType;
   }
 
   public ClassTypeElement getDynamicLowerBoundType(AppView<AppInfoWithLiveness> appView) {
@@ -1146,7 +1137,6 @@
               .getDefinition()
               .asAssume()
               .getDynamicTypeAssumption()
-              .getDynamicType()
               .getDynamicLowerBoundType();
       if (aliasedValueType != null) {
         aliasedValueType = aliasedValueType.meetNullability(getType().nullability());
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index e1b5d36..b5b9532 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -170,7 +170,7 @@
       timing.end();
     }
     code.removeRedundantBlocks();
-    assert code.isConsistentSSA(appView);
+    assert code.isConsistentGraph(appView, false);
     // Insert reads for uninitialized read blocks to ensure correct stack maps.
     timing.begin("Insert uninitialized local reads");
     Set<UninitializedThisLocalRead> uninitializedThisLocalReads =
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 3afb79b..d45ffdf 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -738,11 +738,11 @@
     } else if (!canUseStackMapTypes() || hasIncorrectStackMapTypes) {
       // TODO(b/169137397): We may have ended up generating StackMapPhi's before concluding
       //  having incorrect stack map types. Figure out a way to clean that up.
-      new TypeAnalysis(appView).widening(ir);
+      new TypeAnalysis(appView, ir).widening();
     } else {
       assert canUseStackMapTypes() && !hasIncorrectStackMapTypes;
       assert allPhisAreStackMapPhis(ir);
-      new TypeAnalysis(appView).narrowing(ir);
+      new TypeAnalysis(appView, ir).narrowing();
     }
 
     if (conversionOptions.isStringSwitchConversionEnabled()) {
@@ -750,7 +750,7 @@
     }
 
     ir.removeRedundantBlocks();
-    assert ir.isConsistentSSA(appView);
+    assert ir.isConsistentSSABeforeTypesAreCorrect(appView);
 
     // Clear the code so we don't build multiple times.
     source.clear();
@@ -2411,10 +2411,10 @@
   }
 
   private boolean verifyOutValueType(Instruction ir) {
-    assert ir.outValue() == null || ir.isArrayGet() || ir.evaluate(appView) == ir.getOutType();
+    assert ir.outValue() == null || ir.isArrayGet() || ir.evaluate(appView).equals(ir.getOutType());
     assert ir.outValue() == null
         || !ir.isArrayGet()
-        || ir.evaluate(appView) == ir.getOutType()
+        || ir.evaluate(appView).equals(ir.getOutType())
         || (ir.getOutType().isBottom() && ir.evaluate(appView).isReferenceType());
     return true;
   }
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 cd7af3b..a7cd093 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
@@ -386,7 +386,6 @@
    */
   public void replaceCodeForTesting(IRCode code) {
     ProgramMethod method = code.context();
-    DexEncodedMethod definition = method.getDefinition();
     assert code.isConsistentSSA(appView);
     Timing timing = Timing.empty();
     deadCodeRemover.run(code, timing);
@@ -548,24 +547,6 @@
       options.testing.irModifier.accept(code, appView);
     }
 
-    if (options.canHaveArtStringNewInitBug()) {
-      timing.begin("Check for new-init issue");
-      TrivialPhiSimplifier.ensureDirectStringNewToInit(code, appView.dexItemFactory());
-      timing.end();
-    }
-
-    if (options.canHaveInvokeInterfaceToObjectMethodBug()) {
-      timing.begin("JDK-8272564 fix rewrite");
-      CodeRewriter.rewriteJdk8272564Fix(code, context, appView);
-      timing.end();
-    }
-
-    boolean isDebugMode = options.debug || context.getOrComputeReachabilitySensitive(appView);
-
-    if (isDebugMode) {
-      codeRewriter.simplifyDebugLocals(code);
-    }
-
     if (lensCodeRewriter != null) {
       timing.begin("Lens rewrite");
       lensCodeRewriter.rewrite(code, context, methodProcessor);
@@ -573,6 +554,7 @@
       previous = printMethod(code, "IR after disable assertions (SSA)", previous);
     }
 
+    boolean isDebugMode = options.debug || context.getOrComputeReachabilitySensitive(appView);
     assert !method.isProcessed() || !isDebugMode
         : "Method already processed: "
             + context.toSourceString()
@@ -600,7 +582,6 @@
     // assert fails, then the types that we have inferred are unsound, or the method does not type
     // check. In the latter case, the type checker should be extended to detect the issue such that
     // we will return with a throw-null method above.
-    assert code.verifyTypes(appView);
     assert code.isConsistentSSA(appView);
 
     if (shouldPassThrough(context)) {
@@ -620,6 +601,22 @@
       return timing;
     }
 
+    if (options.canHaveArtStringNewInitBug()) {
+      timing.begin("Check for new-init issue");
+      TrivialPhiSimplifier.ensureDirectStringNewToInit(appView, code);
+      timing.end();
+    }
+
+    if (options.canHaveInvokeInterfaceToObjectMethodBug()) {
+      timing.begin("JDK-8272564 fix rewrite");
+      CodeRewriter.rewriteJdk8272564Fix(code, context, appView);
+      timing.end();
+    }
+
+    if (isDebugMode) {
+      codeRewriter.simplifyDebugLocals(code);
+    }
+
     assertionsRewriter.run(method, code, deadCodeRemover, timing);
     CheckNotNullConverter.runIfNecessary(appView, code);
     previous = printMethod(code, "IR after disable assertions (SSA)", previous);
@@ -636,7 +633,6 @@
       timing.begin("Decouple identifier-name strings");
       identifierNameStringMarker.decoupleIdentifierNameStringsInMethod(code);
       timing.end();
-      assert code.isConsistentSSA(appView);
       previous = printMethod(code, "IR after identifier-name strings (SSA)", previous);
     }
 
@@ -868,7 +864,6 @@
     // Insert code to log arguments if requested.
     if (options.methodMatchesLogArgumentsFilter(method) && !method.isProcessed()) {
       codeRewriter.logArgumentTypes(method, code);
-      assert code.isConsistentSSA(appView);
     }
 
     previous = printMethod(code, "IR after argument type logging (SSA)", previous);
@@ -1040,29 +1035,17 @@
       BytecodeMetadataProvider bytecodeMetadataProvider,
       Timing timing) {
     if (options.testing.roundtripThroughLir) {
-      code = roundtripThroughLir(code, feedback, bytecodeMetadataProvider, timing);
+      code = roundtripThroughLir(code, bytecodeMetadataProvider, timing);
     }
-    MethodConversionOptions conversionOptions = code.getConversionOptions();
-    if (conversionOptions.isGeneratingLir()) {
-      timing.begin("IR->LIR");
-      finalizeToLir(code, feedback, bytecodeMetadataProvider, timing);
-      timing.end();
-    } else if (conversionOptions.isGeneratingClassFiles()) {
-      timing.begin("IR->CF");
-      finalizeToCf(code, feedback, bytecodeMetadataProvider, timing);
-      timing.end();
-    } else {
-      assert conversionOptions.isGeneratingDex();
-      timing.begin("IR->DEX");
-      finalizeToDex(code, feedback, bytecodeMetadataProvider, timing);
-      timing.end();
-    }
+    ProgramMethod method = code.context();
+    IRFinalizer<?> finalizer = code.getConversionOptions().getFinalizer(deadCodeRemover, appView);
+    method.setCode(finalizer.finalizeCode(code, bytecodeMetadataProvider, timing), appView);
+    markProcessed(code, feedback);
     printMethod(code.context(), "After finalization");
   }
 
   private IRCode roundtripThroughLir(
       IRCode code,
-      OptimizationFeedback feedback,
       BytecodeMetadataProvider bytecodeMetadataProvider,
       Timing timing) {
     IRCode round1 =
@@ -1111,50 +1094,6 @@
     return irCode;
   }
 
-  private void finalizeToLir(
-      IRCode code,
-      OptimizationFeedback feedback,
-      BytecodeMetadataProvider bytecodeMetadataProvider,
-      Timing timing) {
-    assert deadCodeRemover.verifyNoDeadCode(code);
-    LirCode<Integer> lirCode =
-        IR2LirConverter.translate(
-            code,
-            bytecodeMetadataProvider,
-            LirStrategy.getDefaultStrategy().getEncodingStrategy(),
-            appView.options());
-    ProgramMethod method = code.context();
-    method.setCode(lirCode, appView);
-    markProcessed(code, feedback);
-  }
-
-  private void finalizeToCf(
-      IRCode code,
-      OptimizationFeedback feedback,
-      BytecodeMetadataProvider bytecodeMetadataProvider,
-      Timing timing) {
-    ProgramMethod method = code.context();
-    method.setCode(
-        new IRToCfFinalizer(appView, deadCodeRemover)
-            .finalizeCode(code, bytecodeMetadataProvider, timing),
-        appView);
-    markProcessed(code, feedback);
-  }
-
-  private void finalizeToDex(
-      IRCode code,
-      OptimizationFeedback feedback,
-      BytecodeMetadataProvider bytecodeMetadataProvider,
-      Timing timing) {
-    ProgramMethod method = code.context();
-    DexEncodedMethod definition = method.getDefinition();
-    method.setCode(
-        new IRToDexFinalizer(appView, deadCodeRemover)
-            .finalizeCode(code, bytecodeMetadataProvider, timing),
-        appView);
-    markProcessed(code, feedback);
-  }
-
   public void markProcessed(IRCode code, OptimizationFeedback feedback) {
     // After all the optimizations have take place, we compute whether method should be inlined.
     ProgramMethod method = code.context();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRToLirFinalizer.java b/src/main/java/com/android/tools/r8/ir/conversion/IRToLirFinalizer.java
new file mode 100644
index 0000000..d981b26
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRToLirFinalizer.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.conversion;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.lightir.IR2LirConverter;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirStrategy;
+import com.android.tools.r8.utils.Timing;
+
+public class IRToLirFinalizer extends IRFinalizer<LirCode<Integer>> {
+
+  public IRToLirFinalizer(AppView<?> appView, DeadCodeRemover deadCodeRemover) {
+    super(appView, deadCodeRemover);
+  }
+
+  @Override
+  public LirCode<Integer> finalizeCode(
+      IRCode code, BytecodeMetadataProvider bytecodeMetadataProvider, Timing timing) {
+    assert deadCodeRemover.verifyNoDeadCode(code);
+    timing.begin("Finalize LIR code");
+    LirCode<Integer> lirCode =
+        IR2LirConverter.translate(
+            code,
+            bytecodeMetadataProvider,
+            LirStrategy.getDefaultStrategy().getEncodingStrategy(),
+            appView.options());
+    timing.end();
+    return lirCode;
+  }
+}
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 565fa11..82f55e3 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
@@ -61,11 +61,13 @@
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
 import com.android.tools.r8.ir.analysis.type.DestructivePhiTypeUpdater;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
 import com.android.tools.r8.ir.analysis.value.SingleValue;
 import com.android.tools.r8.ir.code.Argument;
+import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -105,6 +107,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.passes.TrivialPhiSimplifier;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.argumentpropagation.lenscoderewriter.NullCheckInserter;
@@ -218,6 +221,7 @@
     // Rewriting types that affects phi can cause us to compute TOP for cyclic phi's. To solve this
     // we track all phi's that needs to be re-computed.
     Set<Phi> affectedPhis = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     Set<UnusedArgument> unusedArguments = Sets.newIdentityHashSet();
     rewriteArguments(
         code, originalMethodReference, prototypeChanges, affectedPhis, unusedArguments);
@@ -236,7 +240,14 @@
       }
     }
     rewritePartialDefault(
-        code, method, graphLens, codeLens, prototypeChanges, affectedPhis, unusedArguments);
+        code,
+        method,
+        graphLens,
+        codeLens,
+        prototypeChanges,
+        affectedPhis,
+        affectedValues,
+        unusedArguments);
   }
 
   private void rewritePartialDefault(
@@ -246,6 +257,7 @@
       GraphLens codeLens,
       RewrittenPrototypeDescription prototypeChangesForMethod,
       Set<Phi> affectedPhis,
+      AffectedValues affectedValues,
       Set<UnusedArgument> unusedArguments) {
     BasicBlockIterator blocks = code.listIterator();
     LazyBox<LensCodeRewriterUtils> helper =
@@ -460,9 +472,11 @@
                     }
                     invoke
                         .outValue()
-                        .replaceUsers(constantReturnMaterializingInstruction.outValue());
-                    if (invoke.getOutType()
-                        != constantReturnMaterializingInstruction.getOutType()) {
+                        .replaceUsers(
+                            constantReturnMaterializingInstruction.outValue(), affectedValues);
+                    if (!invoke
+                        .getOutType()
+                        .equals(constantReturnMaterializingInstruction.getOutType())) {
                       affectedPhis.addAll(
                           constantReturnMaterializingInstruction.outValue().uniquePhiUsers());
                     }
@@ -703,21 +717,43 @@
           case CONST_CLASS:
             {
               ConstClass constClass = current.asConstClass();
-              new InstructionReplacer(code, current, iterator, affectedPhis)
-                  .replaceInstructionIfTypeChanged(
-                      constClass.getValue(),
-                      (t, v) ->
-                          t.isPrimitiveType() || t.isVoidType()
-                              ? StaticGet.builder()
-                                  .setField(
-                                      factory
-                                          .getBoxedMembersForPrimitiveOrVoidType(t)
-                                          .getTypeField())
-                                  .setOutValue(v)
-                                  .build()
-                              : new ConstClass(v, t),
-                      graphLens,
-                      codeLens);
+              Instruction replacement =
+                  new InstructionReplacer(code, current, iterator, affectedPhis)
+                      .replaceInstructionIfTypeChanged(
+                          constClass.getValue(),
+                          (t, v) ->
+                              t.isPrimitiveType() || t.isVoidType()
+                                  ? StaticGet.builder()
+                                      .setField(
+                                          factory
+                                              .getBoxedMembersForPrimitiveOrVoidType(t)
+                                              .getTypeField())
+                                      .setOutValue(v)
+                                      .build()
+                                  : new ConstClass(v, t),
+                          graphLens,
+                          codeLens);
+              if (replacement != null && replacement.isStaticGet()) {
+                Value nonNullableValue = replacement.outValue();
+                Value nullableValue =
+                    code.createValue(
+                        nonNullableValue
+                            .getType()
+                            .asReferenceType()
+                            .getOrCreateVariant(Nullability.maybeNull()),
+                        nonNullableValue.getLocalInfo());
+                replacement.setOutValue(nullableValue);
+                Assume assume =
+                    Assume.create(
+                        DynamicType.definitelyNotNull(),
+                        nonNullableValue,
+                        nullableValue,
+                        replacement,
+                        appView,
+                        method);
+                assume.setPosition(replacement.getPosition(), options);
+                iterator.add(assume);
+              }
             }
             break;
 
@@ -848,6 +884,7 @@
     if (mayHaveUnreachableBlocks) {
       code.removeUnreachableBlocks();
     }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     if (!affectedPhis.isEmpty()) {
       new DestructivePhiTypeUpdater(appView, graphLens, codeLens)
           .recomputeAndPropagateTypes(code, affectedPhis);
@@ -876,6 +913,7 @@
       RewrittenPrototypeDescription prototypeChanges,
       Set<Phi> affectedPhis,
       Set<UnusedArgument> unusedArguments) {
+    AffectedValues affectedValues = new AffectedValues();
     ArgumentInfoCollection argumentInfoCollection = prototypeChanges.getArgumentInfoCollection();
     List<Instruction> argumentPostlude = new LinkedList<>();
     int oldArgumentIndex = 0;
@@ -898,6 +936,7 @@
             argument,
             argumentInfo.asRemovedArgumentInfo(),
             affectedPhis,
+            affectedValues,
             argumentPostlude,
             unusedArguments);
         numberOfRemovedArguments++;
@@ -957,6 +996,8 @@
         instructionIterator.add(instruction);
       }
     }
+
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
   }
 
   private void rewriteRemovedArgument(
@@ -966,6 +1007,7 @@
       Argument argument,
       RemovedArgumentInfo removedArgumentInfo,
       Set<Phi> affectedPhis,
+      AffectedValues affectedValues,
       List<Instruction> argumentPostlude,
       Set<UnusedArgument> unusedArguments) {
     Instruction replacement;
@@ -986,7 +1028,7 @@
       replacement.setPosition(Position.none());
       unusedArguments.add(replacement.asUnusedArgument());
     }
-    argument.outValue().replaceUsers(replacement.outValue());
+    argument.outValue().replaceUsers(replacement.outValue(), affectedValues);
     affectedPhis.addAll(replacement.outValue().uniquePhiUsers());
     argumentPostlude.add(replacement);
     instructionIterator.removeOrReplaceByDebugLocalRead();
@@ -1061,7 +1103,11 @@
       CheckCast checkCast =
           SafeCheckCast.builder()
               .setObject(fieldPut.value())
-              .setFreshOutValue(code, lookup.getWriteCastType().toTypeElement(appView))
+              .setFreshOutValue(
+                  code,
+                  lookup
+                      .getWriteCastType()
+                      .toTypeElement(appView, fieldPut.value().getType().nullability()))
               .setCastType(lookup.getWriteCastType())
               .setPosition(fieldPut.getPosition())
               .build();
@@ -1322,7 +1368,7 @@
       this.affectedPhis = affectedPhis;
     }
 
-    void replaceInstructionIfTypeChanged(
+    Instruction replaceInstructionIfTypeChanged(
         DexType type,
         BiFunction<DexType, Value, Instruction> constructor,
         NonIdentityGraphLens graphLens,
@@ -1333,7 +1379,7 @@
         Instruction newInstruction = constructor.apply(newType, newOutValue);
         iterator.replaceCurrentInstruction(newInstruction);
         if (newOutValue != null) {
-          if (newOutValue.getType() != current.getOutType()) {
+          if (!newOutValue.getType().equals(current.getOutType())) {
             affectedPhis.addAll(newOutValue.uniquePhiUsers());
           } else {
             assert current.hasInvariantOutType();
@@ -1344,7 +1390,9 @@
                     && current.asInvokeVirtual().getInvokedMethod().holder.isArrayType());
           }
         }
+        return newInstruction;
       }
+      return null;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
index 69c916e..ceed103 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.utils.InternalOptions;
 
 public abstract class MethodConversionOptions {
@@ -24,7 +25,8 @@
     }
     assert appView.testing().isPostLirPhase();
     Target target = appView.options().isGeneratingClassFiles() ? Target.CF : Target.DEX;
-    return new MutableMethodConversionOptions(target, appView.options());
+    return new MutableMethodConversionOptions(target, appView.options())
+        .disableStringSwitchConversion();
   }
 
   public static MutableMethodConversionOptions forLirPhase(AppView<?> appView) {
@@ -44,6 +46,17 @@
     return new ThrowingMethodConversionOptions();
   }
 
+  public IRFinalizer<?> getFinalizer(DeadCodeRemover deadCodeRemover, AppView<?> appView) {
+    if (isGeneratingLir()) {
+      return new IRToLirFinalizer(appView, deadCodeRemover);
+    }
+    if (isGeneratingClassFiles()) {
+      return new IRToCfFinalizer(appView, deadCodeRemover);
+    }
+    assert isGeneratingDex();
+    return new IRToDexFinalizer(appView, deadCodeRemover);
+  }
+
   private enum Target {
     CF,
     DEX,
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 93ce2a2..b34e8b3 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -255,24 +255,15 @@
     Timing onThreadTiming = Timing.empty();
     IRCode irCode = method.buildIR(appView, MethodConversionOptions.forPostLirPhase(appView));
     // Processing is done and no further uses of the meta-data should arise.
-    BytecodeMetadataProvider bytecodeMetadataProvider = BytecodeMetadataProvider.empty();
+    BytecodeMetadataProvider noMetadata = BytecodeMetadataProvider.empty();
     // During processing optimization info may cause previously live code to become dead.
     // E.g., we may now have knowledge that an invoke does not have side effects.
     // Thus, we re-run the dead-code remover now as it is assumed complete by CF/DEX finalization.
     deadCodeRemover.run(irCode, onThreadTiming);
     MethodConversionOptions conversionOptions = irCode.getConversionOptions();
-    if (conversionOptions.isGeneratingClassFiles()) {
-      method.setCode(
-          new IRToCfFinalizer(appView, deadCodeRemover)
-              .finalizeCode(irCode, bytecodeMetadataProvider, onThreadTiming),
-          appView);
-    } else {
-      assert conversionOptions.isGeneratingDex();
-      method.setCode(
-          new IRToDexFinalizer(appView, deadCodeRemover)
-              .finalizeCode(irCode, bytecodeMetadataProvider, onThreadTiming),
-          appView);
-    }
+    assert !conversionOptions.isGeneratingLir();
+    IRFinalizer<?> finalizer = conversionOptions.getFinalizer(deadCodeRemover, appView);
+    method.setCode(finalizer.finalizeCode(irCode, noMetadata, onThreadTiming), appView);
   }
 
   private void clearDexMethodCompilationState() {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
index f08253d..489f0a6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
@@ -105,9 +105,10 @@
  *   }
  * </pre>
  */
-class StringSwitchConverter {
+public class StringSwitchConverter {
 
-  static void convertToStringSwitchInstructions(IRCode code, DexItemFactory dexItemFactory) {
+  public static boolean convertToStringSwitchInstructions(
+      IRCode code, DexItemFactory dexItemFactory) {
     List<BasicBlock> rewritingCandidates = getRewritingCandidates(code, dexItemFactory);
     if (rewritingCandidates != null) {
       boolean changed = false;
@@ -120,7 +121,9 @@
         code.removeAllDeadAndTrivialPhis();
         code.removeUnreachableBlocks();
       }
+      return changed;
     }
+    return false;
   }
 
   private static List<BasicBlock> getRewritingCandidates(
@@ -349,9 +352,15 @@
             intermediateIdValue = operand.asPhi();
           }
         }
-        assert intermediateIdValue == null
-            || intermediateIdValue.getOperands().stream().noneMatch(Value::isPhi);
-        return intermediateIdValue != null ? intermediateIdValue : defaultValue;
+        if (intermediateIdValue == null) {
+          return defaultValue;
+        }
+        for (Value operand : intermediateIdValue.getOperands()) {
+          if (operand.isPhi()) {
+            return defaultValue;
+          }
+        }
+        return intermediateIdValue;
       }
 
       // Attempts to build a mapping from strings to their ids starting from the given block. The
@@ -732,7 +741,9 @@
           }
         }
 
-        if (idValue == null || (toBeExtended != null && idValue != toBeExtended.idValue)) {
+        if (idValue == null
+            || !idValue.getType().isInt()
+            || (toBeExtended != null && idValue != toBeExtended.idValue)) {
           // Not an extension of `toBeExtended`.
           return setFallthroughBlock(toBeExtended, fallthroughBlock);
         }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/TypeConstraintResolver.java b/src/main/java/com/android/tools/r8/ir/conversion/TypeConstraintResolver.java
index 2c6e476..8dfef13 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/TypeConstraintResolver.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/TypeConstraintResolver.java
@@ -97,7 +97,7 @@
     List<Value> remainingImpreciseValues = resolveRoundOne(code);
     // Round two will resolve any remaining single and wide types. These can depend on the types
     // of array instructions, thus we need to complete the type fixed point prior to resolving.
-    new TypeAnalysis(appView, true).widening(code);
+    new TypeAnalysis(appView, code, true).widening();
     // Round two resolves any remaining imprecision and finally selects a final precise type for
     // any unconstrained imprecise type.
     resolveRoundTwo(code, impreciseInstructions, remainingImpreciseValues);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
index aed22c2..7852989 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
@@ -274,7 +274,6 @@
       code.removeAllDeadAndTrivialPhis();
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.hasChanged(hasChanged);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
index a239404..bc9c351 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerUtils;
 import com.android.tools.r8.ir.analysis.equivalence.BasicBlockBehavioralSubsumption;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.ConstantOrNonConstantNumberValue;
 import com.android.tools.r8.ir.analysis.value.SingleConstClassValue;
@@ -40,13 +39,13 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.controlflow.SwitchCaseAnalyzer;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOutputMode;
 import com.android.tools.r8.utils.LongInterval;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
@@ -136,10 +135,8 @@
         }
       }
     }
-    Set<Value> affectedValues = code.removeUnreachableBlocks();
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    AffectedValues affectedValues = code.removeUnreachableBlocks();
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
     return create(!affectedValues.isEmpty(), simplified);
@@ -673,15 +670,12 @@
     // critical edges at exit.
     code.splitCriticalEdges();
 
-    Set<Value> affectedValues =
-        needToRemoveUnreachableBlocks ? code.removeUnreachableBlocks() : ImmutableSet.of();
-    boolean anyAffectedValues = !affectedValues.isEmpty();
-    if (anyAffectedValues) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    AffectedValues affectedValues =
+        needToRemoveUnreachableBlocks ? code.removeUnreachableBlocks() : AffectedValues.empty();
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
-    return create(anyAffectedValues, anySimplifications);
+    return create(affectedValues.hasNext(), anySimplifications);
   }
 
   public void rewriteSingleKeySwitchToIf(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
index 5f1705f..389dee9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
@@ -50,7 +50,14 @@
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     if (shouldRewriteCode(code)) {
-      return rewriteCode(code, methodProcessor, methodProcessingContext);
+      assert isAcceptingSSA()
+          ? code.isConsistentSSA(appView)
+          : code.isConsistentGraph(appView, false);
+      CodeRewriterResult result = rewriteCode(code, methodProcessor, methodProcessingContext);
+      assert isProducingSSA()
+          ? code.isConsistentSSA(appView)
+          : code.isConsistentGraph(appView, false);
+      return result;
     }
     return noChange();
   }
@@ -65,6 +72,14 @@
 
   protected abstract String getTimingId();
 
+  protected boolean isAcceptingSSA() {
+    return true;
+  }
+
+  protected boolean isProducingSSA() {
+    return true;
+  }
+
   protected CodeRewriterResult rewriteCode(IRCode code) {
     throw new Unreachable("Should Override or use overload");
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
index 33ec433..03178bb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
@@ -83,7 +83,6 @@
     if (hasChanged) {
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.hasChanged(hasChanged);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index bcfd88c..edecd38 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -150,8 +150,6 @@
         }
       }
     }
-
-    assert code.isConsistentSSA(appView);
   }
 
   // Check if a binop can be represented in the binop/lit8 or binop/lit16 form.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
index 05c3702..76138c9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/KnownArrayLengthRewriter.java
@@ -84,7 +84,6 @@
     if (hasChanged) {
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.hasChanged(hasChanged);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
index 18c3757..0516fdc 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/MoveResultRewriter.java
@@ -18,10 +18,10 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
-import com.android.tools.r8.ir.optimize.AssumeRemover;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.utils.BooleanBox;
 import com.google.common.collect.Sets;
-import java.util.Collections;
 import java.util.ListIterator;
 import java.util.Set;
 
@@ -44,11 +44,12 @@
   // Replace result uses for methods where something is known about what is returned.
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    AssumeRemover assumeRemover = new AssumeRemover(appView, code);
     boolean changed = false;
     boolean mayHaveRemovedTrivialPhi = false;
     Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
+    TypeAnalysis typeAnalysis =
+        new TypeAnalysis(appView, code).setKeepRedundantBlocksAfterAssumeRemoval(true);
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
       if (blocksToBeRemoved.contains(block)) {
@@ -90,38 +91,42 @@
           continue;
         }
 
-        Set<Value> affectedValues =
+        AffectedValues affectedValues =
             argument.getType().equals(outValue.getType())
-                ? Collections.emptySet()
+                ? AffectedValues.empty()
                 : outValue.affectedValues();
 
-        assumeRemover.markAssumeDynamicTypeUsersForRemoval(outValue);
         mayHaveRemovedTrivialPhi |= outValue.numberOfPhiUsers() > 0;
         outValue.replaceUsers(argument);
-        invoke.setOutValue(null);
+        invoke.clearOutValue();
         changed = true;
 
         if (!affectedValues.isEmpty()) {
-          new TypeAnalysis(appView).narrowing(affectedValues);
+          BooleanBox removedAssumeInstructionInCurrentBlock = new BooleanBox();
+          typeAnalysis
+              .setKeepRedundantBlocksAfterAssumeRemoval(true)
+              .narrowingWithAssumeRemoval(
+                  affectedValues,
+                  assume -> removedAssumeInstructionInCurrentBlock.or(assume.getBlock() == block));
+          if (removedAssumeInstructionInCurrentBlock.isTrue()) {
+            // Workaround ConcurrentModificationException.
+            iterator = block.listIterator(code);
+          }
         }
       }
     }
-    assumeRemover.removeMarkedInstructions(blocksToBeRemoved).finish();
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     if (!blocksToBeRemoved.isEmpty()) {
       code.removeBlocks(blocksToBeRemoved);
       code.removeAllDeadAndTrivialPhis(affectedValues);
       assert code.getUnreachableBlocks().isEmpty();
-    } else if (mayHaveRemovedTrivialPhi || assumeRemover.mayHaveIntroducedTrivialPhi()) {
+    } else if (mayHaveRemovedTrivialPhi) {
       code.removeAllDeadAndTrivialPhis(affectedValues);
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    typeAnalysis.narrowingWithAssumeRemoval(affectedValues);
     if (changed) {
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.hasChanged(changed);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index 2666be2..3bb77d8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.code.Sub;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import java.util.Set;
@@ -43,15 +44,16 @@
   @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
     boolean loopRemoved = false;
+    AffectedValues affectedValues = new AffectedValues();
     for (BasicBlock comparisonBlockCandidate : code.blocks) {
       if (isComparisonBlock(comparisonBlockCandidate)) {
-        loopRemoved |= tryRemoveLoop(code, comparisonBlockCandidate.exit().asIf());
+        loopRemoved |= tryRemoveLoop(code, comparisonBlockCandidate.exit().asIf(), affectedValues);
       }
     }
     if (loopRemoved) {
-      code.removeAllDeadAndTrivialPhis();
+      code.removeAllDeadAndTrivialPhis(affectedValues);
+      affectedValues.narrowingWithAssumeRemoval(appView, code);
       code.removeRedundantBlocks();
-      assert code.isConsistentSSA(appView);
     }
     return CodeRewriterResult.hasChanged(loopRemoved);
   }
@@ -78,7 +80,7 @@
     throw new Unreachable();
   }
 
-  private boolean tryRemoveLoop(IRCode code, If comparison) {
+  private boolean tryRemoveLoop(IRCode code, If comparison, AffectedValues affectedValues) {
     Phi loopPhi = computeLoopPhi(comparison);
     if (loopPhi == null) {
       return false;
@@ -111,7 +113,7 @@
     NaturalIntLoopWithKnowIterations loop = builder.build();
 
     if (loop.has1Iteration()) {
-      loop.remove1IterationLoop(code);
+      loop.remove1IterationLoop(code, affectedValues);
       return true;
     }
     return false;
@@ -399,9 +401,9 @@
           && target(initCounter + counterIncrement) == loopExit;
     }
 
-    private void remove1IterationLoop(IRCode code) {
+    private void remove1IterationLoop(IRCode code, AffectedValues affectedValues) {
       BasicBlock comparisonBlock = comparison.getBlock();
-      updatePhis(comparisonBlock);
+      updatePhis(comparisonBlock, affectedValues);
       patchControlFlow(code, comparisonBlock);
     }
 
@@ -416,23 +418,23 @@
       loopExit.replacePredecessor(comparisonBlock, backPredecessor);
     }
 
-    private void updatePhis(BasicBlock comparisonBlock) {
+    private void updatePhis(BasicBlock comparisonBlock, AffectedValues affectedValues) {
       int backIndex = comparisonBlock.getPredecessors().indexOf(backPredecessor);
       for (Phi phi : comparisonBlock.getPhis()) {
         Value loopEntryValue = phi.getOperand(1 - backIndex);
         Value loopExitValue = phi.getOperand(backIndex);
         for (Instruction uniqueUser : phi.uniqueUsers()) {
           if (loopBody.contains(uniqueUser.getBlock())) {
-            uniqueUser.replaceValue(phi, loopEntryValue);
+            uniqueUser.replaceValue(phi, loopEntryValue, affectedValues);
           } else {
-            uniqueUser.replaceValue(phi, loopExitValue);
+            uniqueUser.replaceValue(phi, loopExitValue, affectedValues);
           }
         }
         for (Phi phiUser : phi.uniquePhiUsers()) {
           if (loopBody.contains(phiUser.getBlock())) {
-            phiUser.replaceOperand(phi, loopEntryValue);
+            phiUser.replaceOperand(phi, loopEntryValue, affectedValues);
           } else {
-            phiUser.replaceOperand(phi, loopExitValue);
+            phiUser.replaceOperand(phi, loopExitValue, affectedValues);
           }
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
index 6a7c040..704669d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
@@ -53,6 +53,7 @@
     for (InvokeDirect invoke : getOrComputeSideEffectFreeConstructorCalls(code)) {
       hoistSideEffectFreeConstructorCall(code, invoke);
     }
+    code.removeRedundantBlocks();
     return CodeRewriterResult.NONE;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
index 99caca6..4c542af 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
@@ -162,7 +162,6 @@
     if (changed) {
       code.removeAllDeadAndTrivialPhis();
     }
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.hasChanged(changed);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index 6ceb56d..1e1b91c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.Goto;
@@ -15,9 +14,9 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.WorkList;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -63,17 +62,13 @@
       return CodeRewriterResult.NO_CHANGE;
     }
     retargetGotos(newTargets);
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
-    affectedValues.addAll(code.removeUnreachableBlocks());
+    AffectedValues affectedValues = code.removeUnreachableBlocks();
     code.removeAllDeadAndTrivialPhis(affectedValues);
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     if (ALLOW_PARTIAL_REWRITE) {
       code.splitCriticalEdges();
     }
     code.removeRedundantBlocks();
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.HAS_CHANGED;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
index 920417a..fc64a45 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ThrowCatchOptimizer.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -33,6 +32,7 @@
 import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -200,15 +200,12 @@
       }
     }
     if (shouldRemoveUnreachableBlocks) {
-      Set<Value> affectedValues = code.removeUnreachableBlocks();
-      if (!affectedValues.isEmpty()) {
-        new TypeAnalysis(appView).narrowing(affectedValues);
-      }
+      AffectedValues affectedValues = code.removeUnreachableBlocks();
+      affectedValues.narrowingWithAssumeRemoval(appView, code);
     }
     if (hasChanged) {
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return hasChanged;
   }
 
@@ -216,7 +213,7 @@
   // it with a block throwing a null value (which should result in NPE). Note that this throw is not
   // expected to be ever reached, but is intended to satisfy verifier.
   private boolean optimizeAlwaysThrowingInstructions(IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     ProgramMethod context = code.context();
@@ -336,13 +333,10 @@
       affectedValues.addAll(code.removeUnreachableBlocks());
     }
     assert code.getUnreachableBlocks().isEmpty();
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     if (hasChanged) {
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return hasChanged;
   }
 
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 a2654cf..028dcb1 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
@@ -13,13 +13,12 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
-import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeUtils;
+import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.CheckCast;
-import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstanceOf;
 import com.android.tools.r8.ir.code.Instruction;
@@ -79,7 +78,8 @@
     // CheckCast at line 4 unless we update v7 with the most precise information by narrowing the
     // affected values of v5. We therefore have to run the type analysis after each CheckCast
     // removal.
-    TypeAnalysis typeAnalysis = new TypeAnalysis(appView);
+    TypeAnalysis typeAnalysis =
+        new TypeAnalysis(appView, code).setKeepRedundantBlocksAfterAssumeRemoval(true);
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     InstructionListIterator it = code.instructionListIterator();
     boolean needToRemoveTrivialPhis = false;
@@ -121,14 +121,11 @@
     // v3 <- phi(v1, v1)
     if (needToRemoveTrivialPhis) {
       code.removeAllDeadAndTrivialPhis(affectedValues);
-      if (!affectedValues.isEmpty()) {
-        typeAnalysis.narrowing(affectedValues);
-      }
     }
+    typeAnalysis.narrowingWithAssumeRemoval(affectedValues);
     if (hasChanged) {
       code.removeRedundantBlocks();
     }
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.hasChanged(hasChanged);
   }
 
@@ -140,7 +137,11 @@
   private enum InstanceOfResult {
     UNKNOWN,
     TRUE,
-    FALSE
+    FALSE;
+
+    boolean isTrue() {
+      return this == TRUE;
+    }
   }
 
   // Returns true if the given check-cast instruction was removed.
@@ -346,27 +347,23 @@
                     value.isDefinedByInstructionSatisfying(
                         Instruction::isAssumeWithDynamicTypeAssumption));
         if (aliasedValue != null) {
-          DynamicTypeWithUpperBound dynamicType =
-              aliasedValue.getDefinition().asAssume().getDynamicTypeAssumption().getDynamicType();
-          Nullability nullability = dynamicType.getNullability();
-          if (nullability.isDefinitelyNull()) {
+          Assume assumeInstruction = aliasedValue.getDefinition().asAssume();
+          DynamicType dynamicType = assumeInstruction.getDynamicType();
+          if (dynamicType.getNullability().isDefinitelyNull()) {
             result = InstanceOfResult.FALSE;
-          } else if (dynamicType.getDynamicUpperBoundType().lessThanOrEqual(instanceOfType, appView)
-              && (!inType.isNullable() || !nullability.isNullable())) {
+          } else if (dynamicType.isDynamicTypeWithUpperBound()
+              && dynamicType
+                  .asDynamicTypeWithUpperBound()
+                  .getDynamicUpperBoundType()
+                  .lessThanOrEqual(instanceOfType, appView)
+              && (!inType.isNullable() || dynamicType.getNullability().isDefinitelyNotNull())) {
             result = InstanceOfResult.TRUE;
           }
         }
       }
     }
     if (result != InstanceOfResult.UNKNOWN) {
-      ConstNumber newInstruction =
-          new ConstNumber(
-              new Value(
-                  code.valueNumberGenerator.next(),
-                  TypeElement.getInt(),
-                  instanceOf.outValue().getLocalInfo()),
-              result == InstanceOfResult.TRUE ? 1 : 0);
-      it.replaceCurrentInstruction(newInstruction);
+      it.replaceCurrentInstructionWithConstBoolean(code, result.isTrue());
       return true;
     }
     return false;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
index a08d82a..3055661 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
@@ -34,8 +34,17 @@
   }
 
   @Override
+  protected boolean isAcceptingSSA() {
+    return false;
+  }
+
+  @Override
+  protected boolean isProducingSSA() {
+    return false;
+  }
+
+  @Override
   protected CodeRewriterResult rewriteCode(IRCode code) {
-    assert code.isConsistentGraph(appView);
     List<BasicBlock> blocksToRemove = new ArrayList<>();
     // Rewrite all non-fallthrough targets to the end of trivial goto chains and remove
     // first round of trivial goto blocks.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
index 6c4c66f..9bd4eb5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.algorithms.scc.SCC;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.IRCode;
@@ -29,7 +30,8 @@
     assert !unusedArgument.outValue().hasPhiUsers();
   }
 
-  public static void ensureDirectStringNewToInit(IRCode code, DexItemFactory dexItemFactory) {
+  public static void ensureDirectStringNewToInit(AppView<?> appView, IRCode code) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
     for (Instruction instruction : code.instructions()) {
       if (instruction.isInvokeDirect()) {
         InvokeDirect invoke = instruction.asInvokeDirect();
@@ -47,6 +49,7 @@
         }
       }
     }
+    assert code.isConsistentSSA(appView);
   }
 
   private static NewInstance findNewInstance(Phi phi) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AffectedValues.java b/src/main/java/com/android/tools/r8/ir/optimize/AffectedValues.java
new file mode 100644
index 0000000..dc3f975
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AffectedValues.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Value;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+
+public class AffectedValues implements Set<Value> {
+
+  private static final AffectedValues EMPTY = new AffectedValues(ImmutableSet.of());
+
+  private final Set<Value> affectedValues;
+
+  public AffectedValues() {
+    this(Sets.newIdentityHashSet());
+  }
+
+  private AffectedValues(Set<Value> affectedValues) {
+    this.affectedValues = affectedValues;
+  }
+
+  public static AffectedValues empty() {
+    return EMPTY;
+  }
+
+  public void narrowingWithAssumeRemoval(AppView<?> appView, IRCode code) {
+    if (hasNext()) {
+      new TypeAnalysis(appView, code).narrowingWithAssumeRemoval(this);
+    }
+  }
+
+  public void widening(AppView<?> appView, IRCode code) {
+    if (hasNext()) {
+      new TypeAnalysis(appView, code).widening(this);
+    }
+  }
+
+  @Override
+  public boolean add(Value value) {
+    return affectedValues.add(value);
+  }
+
+  @Override
+  public boolean addAll(Collection<? extends Value> c) {
+    return affectedValues.addAll(c);
+  }
+
+  @Override
+  public void clear() {
+    affectedValues.clear();
+  }
+
+  @Override
+  public boolean contains(Object o) {
+    return affectedValues.contains(o);
+  }
+
+  @Override
+  public boolean containsAll(Collection<?> c) {
+    return affectedValues.containsAll(c);
+  }
+
+  public boolean hasNext() {
+    return !isEmpty();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return affectedValues.isEmpty();
+  }
+
+  @Override
+  public Iterator<Value> iterator() {
+    return affectedValues.iterator();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    return affectedValues.remove(o);
+  }
+
+  @Override
+  public boolean removeAll(Collection<?> c) {
+    return affectedValues.removeAll(c);
+  }
+
+  @Override
+  public boolean retainAll(Collection<?> c) {
+    return affectedValues.retainAll(c);
+  }
+
+  @Override
+  public int size() {
+    return affectedValues.size();
+  }
+
+  @Override
+  public Object[] toArray() {
+    return affectedValues.toArray();
+  }
+
+  @Override
+  public <T> T[] toArray(T[] a) {
+    return affectedValues.toArray(a);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssertionErrorTwoArgsConstructorRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssertionErrorTwoArgsConstructorRewriter.java
index ca2babb..1ba1bb4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssertionErrorTwoArgsConstructorRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssertionErrorTwoArgsConstructorRewriter.java
@@ -13,11 +13,11 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 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.InvokeStatic;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Value;
@@ -26,11 +26,9 @@
 import com.android.tools.r8.ir.desugar.backports.BackportedMethods;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.ListIterator;
-import java.util.Set;
 
 public class AssertionErrorTwoArgsConstructorRewriter {
 
@@ -52,36 +50,34 @@
     if (options.canUseAssertionErrorTwoArgumentConstructor()) {
       return;
     }
-    Set<Value> newOutValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
       InstructionListIterator insnIterator = block.listIterator(code);
       List<NewInstance> newInstancesToRemove = new ArrayList<>();
       while (insnIterator.hasNext()) {
-        Instruction current = insnIterator.next();
-        if (current.isInvokeMethod()) {
-          DexMethod invokedMethod = current.asInvokeMethod().getInvokedMethod();
+        InvokeDirect invoke = insnIterator.next().asInvokeDirect();
+        if (invoke != null) {
+          DexMethod invokedMethod = invoke.getInvokedMethod();
           if (invokedMethod == dexItemFactory.assertionErrorMethods.initMessageAndCause) {
-            List<Value> inValues = current.inValues();
-            assert inValues.size() == 3; // receiver, message, cause
-            Value newInstanceValue = current.getFirstOperand();
+            assert invoke.arguments().size() == 3; // receiver, message, cause
+            Value newInstanceValue = invoke.getReceiver();
             Instruction definition = newInstanceValue.getDefinition();
             if (!definition.isNewInstance()) {
               continue;
             }
-            InvokeStatic invoke =
+            InvokeStatic replacement =
                 InvokeStatic.builder()
                     .setMethod(
                         createSynthetic(methodProcessor, methodProcessingContext).getReference())
                     .setFreshOutValue(
                         code, dexItemFactory.assertionErrorType.toTypeElement(appView))
-                    .setPosition(current)
-                    .setArguments(inValues.subList(1, 3))
+                    .setPosition(invoke)
+                    .setArguments(invoke.arguments().subList(1, 3))
                     .build();
-            insnIterator.replaceCurrentInstruction(invoke);
-            newInstanceValue.replaceUsers(invoke.outValue());
-            newOutValues.add(invoke.outValue());
+            insnIterator.replaceCurrentInstruction(replacement);
+            newInstanceValue.replaceUsers(replacement.outValue(), affectedValues);
             newInstancesToRemove.add(definition.asNewInstance());
           }
         }
@@ -89,9 +85,7 @@
       newInstancesToRemove.forEach(
           newInstance -> newInstance.removeOrReplaceByDebugLocalRead(code));
     }
-    if (!newOutValues.isEmpty()) {
-      new TypeAnalysis(appView).widening(newOutValues);
-    }
+    affectedValues.widening(appView, code);
     assert code.isConsistentSSA(appView);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java
index ef8b039..0680915 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java
@@ -18,8 +18,6 @@
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Assume;
-import com.android.tools.r8.ir.code.Assume.DynamicTypeAssumption;
-import com.android.tools.r8.ir.code.Assume.NonNullAssumption;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.ConstNumber;
@@ -40,7 +38,6 @@
 import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfoLookup;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.TriConsumer;
 import com.android.tools.r8.utils.TriFunction;
 import com.android.tools.r8.utils.TriPredicate;
 import com.google.common.collect.Sets;
@@ -62,13 +59,21 @@
 public class AssumeInserter {
 
   private final AppView<AppInfoWithLiveness> appView;
+  private final boolean keepRedundantBlocks;
 
   public AssumeInserter(AppView<AppInfoWithLiveness> appView) {
+    this(appView, false);
+  }
+
+  public AssumeInserter(AppView<AppInfoWithLiveness> appView, boolean keepRedundantBlocks) {
     this.appView = appView;
+    this.keepRedundantBlocks = keepRedundantBlocks;
   }
 
   public void insertAssumeInstructions(IRCode code, Timing timing) {
     insertAssumeInstructionsInBlocks(code, code.listIterator(), alwaysTrue(), timing);
+    code.removeRedundantBlocks();
+    assert code.isConsistentSSA(appView);
   }
 
   public void insertAssumeInstructionsInBlocks(
@@ -283,7 +288,7 @@
       return false;
     }
 
-    SingleFieldResolutionResult resolutionResult =
+    SingleFieldResolutionResult<?> resolutionResult =
         appView.appInfo().resolveField(fieldGet.getField()).asSingleFieldResolutionResult();
     if (resolutionResult == null) {
       return false;
@@ -340,7 +345,7 @@
     assumedValues.removeIf(
         (instruction, assumedValue, assumedValueInfo) -> {
           // Assumed values with dynamic type information are never redundant.
-          if (assumedValueInfo.hasDynamicTypeInfo()) {
+          if (assumedValueInfo.hasDynamicTypeInfoIgnoringNullability()) {
             return false;
           }
 
@@ -352,7 +357,7 @@
             return false;
           }
 
-          Instruction definition = assumedValue.definition;
+          Instruction definition = assumedValue.getDefinition();
           if (definition == instruction) {
             return false;
           }
@@ -364,10 +369,12 @@
           }
 
           if (!otherAssumedValueInfo.isNonNull()) {
-            // This is not redundant, but we can strenghten it with the dynamic type information
+            // This is not redundant, but we can strengthen it with the dynamic type information
             // from the other assume instruction.
-            assumedValueInfo.setDynamicTypeAssumption(
-                otherAssumedValueInfo.getDynamicTypeAssumption());
+            assumedValueInfo.setDynamicType(
+                otherAssumedValueInfo
+                    .getDynamicType()
+                    .withNullability(Nullability.definitelyNotNull()));
             return false;
           }
 
@@ -537,7 +544,9 @@
           }
         });
     if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
+      new TypeAnalysis(appView, code)
+          .setKeepRedundantBlocksAfterAssumeRemoval(keepRedundantBlocks)
+          .narrowingWithAssumeRemoval(affectedValues);
     }
   }
 
@@ -597,13 +606,13 @@
             assumeInstruction = new ConstNumber(newValue, 0);
           } else {
             assumeInstruction =
-                new Assume(
-                    assumedValueInfo.dynamicTypeAssumption,
-                    assumedValueInfo.nonNullAssumption,
+                Assume.create(
+                    assumedValueInfo.dynamicType,
                     newValue,
                     assumedValue,
                     instruction,
-                    appView);
+                    appView,
+                    code.context());
           }
           assumeInstruction.setPosition(instruction.getPosition());
           if (insertionBlock != block) {
@@ -656,7 +665,9 @@
 
   private static boolean isNullableReferenceType(Value value) {
     TypeElement type = value.getType();
-    return type.isReferenceType() && type.asReferenceType().isNullable();
+    return type.isReferenceType()
+        && type.asReferenceType().isNullable()
+        && !type.nullability().isDefinitelyNull();
   }
 
   private static boolean isNullableReferenceTypeWithOtherNonDebugUsers(
@@ -677,8 +688,7 @@
   static class AssumedValueInfo {
 
     AssumedDominance dominance;
-    DynamicTypeAssumption dynamicTypeAssumption;
-    NonNullAssumption nonNullAssumption;
+    DynamicType dynamicType = DynamicType.unknown();
 
     AssumedValueInfo(AssumedDominance dominance) {
       this.dominance = dominance;
@@ -692,49 +702,53 @@
       this.dominance = dominance;
     }
 
-    boolean hasDynamicTypeInfo() {
-      return dynamicTypeAssumption != null;
+    boolean hasDynamicTypeInfoIgnoringNullability() {
+      return dynamicType.isDynamicTypeWithUpperBound() && !dynamicType.isUnknown();
     }
 
-    DynamicTypeAssumption getDynamicTypeAssumption() {
-      return dynamicTypeAssumption;
+    DynamicType getDynamicType() {
+      return dynamicType;
     }
 
-    void setDynamicTypeAssumption(DynamicTypeAssumption dynamicTypeAssumption) {
-      this.dynamicTypeAssumption = dynamicTypeAssumption;
-    }
-
-    void setDynamicTypeAssumption(DynamicTypeWithUpperBound dynamicType) {
+    void setDynamicType(DynamicType dynamicType) {
       assert dynamicType != null;
-      dynamicTypeAssumption = new DynamicTypeAssumption(dynamicType);
-      if (dynamicType.getDynamicUpperBoundType().isDefinitelyNotNull()) {
-        setNotNull();
-      }
+      assert this.dynamicType.isNotNullType()
+          ? dynamicType.getNullability().isDefinitelyNotNull()
+          : this.dynamicType.isUnknown();
+      this.dynamicType = dynamicType;
+    }
+
+    Nullability getNullability() {
+      return dynamicType.getNullability();
     }
 
     boolean isNull() {
-      return dynamicTypeAssumption != null
-          && dynamicTypeAssumption.getDynamicType().getNullability().isDefinitelyNull();
+      return getNullability().isDefinitelyNull();
     }
 
     boolean isNonNull() {
-      return nonNullAssumption != null;
+      return getNullability().isDefinitelyNotNull();
     }
 
     void setNotNull() {
-      nonNullAssumption = NonNullAssumption.get();
+      if (dynamicType.isUnknown()) {
+        dynamicType = DynamicType.definitelyNotNull();
+      } else {
+        dynamicType = dynamicType.withNullability(Nullability.definitelyNotNull());
+      }
     }
 
     boolean isSubsumedBy(AssumedValueInfo other) {
-      return !hasDynamicTypeInfo() && other.isNonNull();
+      return !hasDynamicTypeInfoIgnoringNullability() && other.isNonNull();
     }
 
     void strengthenWith(AssumedValueInfo info) {
       if (info.isNonNull()) {
         setNotNull();
       }
-      if (!hasDynamicTypeInfo() && info.hasDynamicTypeInfo()) {
-        setDynamicTypeAssumption(info.getDynamicTypeAssumption());
+      if (!hasDynamicTypeInfoIgnoringNullability()
+          && info.hasDynamicTypeInfoIgnoringNullability()) {
+        setDynamicType(info.getDynamicType().withNullability(getNullability()));
       }
     }
   }
@@ -799,14 +813,6 @@
       return assumedValues.isEmpty();
     }
 
-    void forEach(TriConsumer<Instruction, Value, AssumedValueInfo> consumer) {
-      assumedValues.forEach(
-          (instruction, dominancePerValue) ->
-              dominancePerValue.forEach(
-                  (assumedValue, assumedValueInfo) ->
-                      consumer.accept(instruction, assumedValue, assumedValueInfo)));
-    }
-
     void removeAll(Map<Instruction, Map<Value, AssumedValueInfo>> keys) {
       keys.forEach(
           (instruction, redundantAssumedValues) -> {
@@ -872,7 +878,7 @@
             instruction,
             assumedValue,
             AssumedDominance.everything(),
-            assumedValueInfo -> assumedValueInfo.setDynamicTypeAssumption(dynamicType));
+            assumedValueInfo -> assumedValueInfo.setDynamicType(dynamicType));
       }
 
       void addNonNullValueKnownToDominateAllUsers(Instruction instruction, Value nonNullValue) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssumeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/AssumeRemover.java
index 70057fc..20ba610 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssumeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssumeRemover.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.ir.optimize;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
@@ -13,8 +12,8 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Value;
 import com.google.common.collect.Sets;
-import java.util.Collection;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * When we have Assume instructions we generally verify that the Assume instructions contribute with
@@ -33,98 +32,101 @@
   private final AppView<?> appView;
   private final IRCode code;
 
-  private final Set<Value> affectedValues;
-  private final Set<Assume> assumeInstructionsToRemove = Sets.newIdentityHashSet();
-
-  private boolean mayHaveIntroducedTrivialPhi = false;
+  private final Set<Assume> affectedAssumeInstructions = Sets.newIdentityHashSet();
 
   public AssumeRemover(AppView<?> appView, IRCode code) {
-    this(appView, code, Sets.newIdentityHashSet());
-  }
-
-  public AssumeRemover(AppView<?> appView, IRCode code, Set<Value> affectedValues) {
     this.appView = appView;
     this.code = code;
-    this.affectedValues = affectedValues;
   }
 
-  public Set<Value> getAffectedValues() {
-    return affectedValues;
+  public void addAffectedAssumeInstruction(Assume assumeInstruction) {
+    affectedAssumeInstructions.add(assumeInstruction);
   }
 
-  public boolean mayHaveIntroducedTrivialPhi() {
-    return mayHaveIntroducedTrivialPhi;
+  public boolean hasAffectedAssumeInstructions() {
+    return !affectedAssumeInstructions.isEmpty();
   }
 
-  public void markAssumeDynamicTypeUsersForRemoval(Value value) {
-    for (Instruction user : value.aliasedUsers()) {
-      if (user.isAssume()) {
-        Assume assumeInstruction = user.asAssume();
-        assumeInstruction.unsetDynamicTypeAssumption();
-        if (!assumeInstruction.hasNonNullAssumption()) {
-          markForRemoval(assumeInstruction);
+  private boolean removeAssumeInstructionIfRedundant(
+      Assume assumeInstruction,
+      InstructionListIterator instructionIterator,
+      Set<Value> newAffectedValues,
+      Consumer<Assume> redundantAssumeConsumer) {
+    if (!affectedAssumeInstructions.remove(assumeInstruction)) {
+      return false;
+    }
+    if (assumeInstruction.src().isConstant()) {
+      removeRedundantAssumeInstruction(
+          assumeInstruction, instructionIterator, newAffectedValues, redundantAssumeConsumer);
+      return true;
+    }
+    if (assumeInstruction.hasDynamicTypeIgnoringNullability()
+        && assumeInstruction
+            .getDynamicType()
+            .asDynamicTypeWithUpperBound()
+            .getDynamicUpperBoundType()
+            .strictlyLessThan(assumeInstruction.src().getType(), appView)) {
+      assert assumeInstruction
+          .getDynamicType()
+          .getNullability()
+          .lessThanOrEqual(assumeInstruction.src().getType().nullability());
+      return false;
+    }
+    if (assumeInstruction.hasNonNullAssumption()
+        && !assumeInstruction.src().isConstant()
+        && assumeInstruction.src().getType().isNullable()
+        && !assumeInstruction.src().getType().isDefinitelyNull()) {
+      assumeInstruction.clearDynamicTypeAssumption();
+      return false;
+    }
+    removeRedundantAssumeInstruction(
+        assumeInstruction, instructionIterator, newAffectedValues, redundantAssumeConsumer);
+    return true;
+  }
+
+  private void removeRedundantAssumeInstruction(
+      Assume assumeInstruction,
+      InstructionListIterator instructionIterator,
+      Set<Value> newAffectedValues,
+      Consumer<Assume> redundantAssumeConsumer) {
+    Value inValue = assumeInstruction.src();
+    Value outValue = assumeInstruction.outValue();
+    if (outValue == null) {
+      // Already removed.
+      return;
+    }
+
+    // Check if we need to run the type analysis for the affected values of the out-value.
+    if (!outValue.getType().equals(inValue.getType())) {
+      newAffectedValues.addAll(outValue.affectedValues());
+    }
+
+    outValue.replaceUsers(inValue);
+    redundantAssumeConsumer.accept(assumeInstruction);
+    instructionIterator.removeOrReplaceByDebugLocalRead();
+  }
+
+  public boolean removeRedundantAssumeInstructions(
+      Set<Value> newAffectedValues, Consumer<Assume> redundantAssumeConsumer) {
+    if (affectedAssumeInstructions.isEmpty()) {
+      return false;
+    }
+    boolean changed = false;
+    for (BasicBlock block : code.getBlocks()) {
+      InstructionListIterator instructionIterator = block.listIterator(code);
+      while (instructionIterator.hasNext()) {
+        Instruction instruction = instructionIterator.next();
+        if (instruction.isAssume()) {
+          changed |=
+              removeAssumeInstructionIfRedundant(
+                  instruction.asAssume(),
+                  instructionIterator,
+                  newAffectedValues,
+                  redundantAssumeConsumer);
         }
       }
     }
-  }
-
-  public void markUnusedAssumeValuesForRemoval(Collection<Value> values) {
-    for (Value value : values) {
-      if (value.isDefinedByInstructionSatisfying(Instruction::isAssume) && !value.hasAnyUsers()) {
-        markForRemoval(value.getDefinition().asAssume());
-      }
-    }
-  }
-
-  private void markForRemoval(Assume assumeInstruction) {
-    assumeInstructionsToRemove.add(assumeInstruction);
-  }
-
-  public void removeIfMarked(
-      Assume assumeInstruction, InstructionListIterator instructionIterator) {
-    if (assumeInstructionsToRemove.remove(assumeInstruction)) {
-      Value inValue = assumeInstruction.src();
-      Value outValue = assumeInstruction.outValue();
-
-      // Check if we need to run the type analysis for the affected values of the out-value.
-      if (!outValue.getType().equals(inValue.getType())) {
-        affectedValues.addAll(outValue.affectedValues());
-      }
-
-      if (outValue.hasPhiUsers()) {
-        mayHaveIntroducedTrivialPhi = true;
-      }
-
-      outValue.replaceUsers(inValue);
-      instructionIterator.removeOrReplaceByDebugLocalRead();
-    }
-  }
-
-  public AssumeRemover removeMarkedInstructions() {
-    return removeMarkedInstructions(null);
-  }
-
-  public AssumeRemover removeMarkedInstructions(Set<BasicBlock> blocksToBeRemoved) {
-    if (!assumeInstructionsToRemove.isEmpty()) {
-      for (BasicBlock block : code.blocks) {
-        if (blocksToBeRemoved != null && blocksToBeRemoved.contains(block)) {
-          continue;
-        }
-        InstructionListIterator instructionIterator = block.listIterator(code);
-        while (instructionIterator.hasNext()) {
-          Instruction instruction = instructionIterator.next();
-          if (instruction.isAssume()) {
-            removeIfMarked(instruction.asAssume(), instructionIterator);
-          }
-        }
-      }
-    }
-    return this;
-  }
-
-  public void finish() {
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedAssumeInstructions.clear();
+    return changed;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CheckNotNullConverter.java b/src/main/java/com/android/tools/r8/ir/optimize/CheckNotNullConverter.java
index 9cffada..58a667d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CheckNotNullConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CheckNotNullConverter.java
@@ -21,6 +21,7 @@
   public static void runIfNecessary(AppView<?> appView, IRCode code) {
     if (appView.enableWholeProgramOptimizations()) {
       run(appView.withClassHierarchy(), code);
+      assert code.isConsistentSSA(appView);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 19f053f..e96ef16 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize;
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
@@ -113,10 +114,13 @@
     }
 
     if (!valuesThatRequireWidening.isEmpty()) {
-      new TypeAnalysis(appView).widening(valuesThatRequireWidening);
+      new TypeAnalysis(appView, code).widening(valuesThatRequireWidening);
     }
 
+    code.removeRedundantBlocks();
+
     assert Streams.stream(code.instructions()).noneMatch(Instruction::isAssume);
+    assert code.isConsistentSSA(appView);
   }
 
   public static void removeOrReplaceByDebugLocalWrite(
@@ -200,6 +204,7 @@
         }
       }
     }
+    assert code.isConsistentSSA(appView);
   }
 
   /**
@@ -361,8 +366,7 @@
     DexType javaLangSystemType = dexItemFactory.javaLangSystemType;
     DexType javaIoPrintStreamType = dexItemFactory.javaIoPrintStreamType;
     Value out =
-        code.createValue(
-            TypeElement.fromDexType(javaIoPrintStreamType, definitelyNotNull(), appView));
+        code.createValue(TypeElement.fromDexType(javaIoPrintStreamType, maybeNull(), appView));
 
     DexProto proto = dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.objectType);
     DexMethod print = dexItemFactory.createMethod(javaIoPrintStreamType, proto, "print");
@@ -431,7 +435,7 @@
         iterator.add(new InvokeVirtual(print, null, ImmutableList.of(out, nul)));
         iterator = isNotNullBlock.listIterator(code);
         iterator.setInsertionPosition(position);
-        value = code.createValue(TypeElement.classClassType(appView, definitelyNotNull()));
+        value = code.createValue(TypeElement.classClassType(appView, maybeNull()));
         iterator.add(
             new InvokeVirtual(
                 dexItemFactory.objectMembers.getClass, value, ImmutableList.of(arguments.get(i))));
@@ -449,6 +453,7 @@
     }
     // When we fall out of the loop the iterator is in the last eol block.
     iterator.add(new InvokeVirtual(printLn, null, ImmutableList.of(out, empty)));
+    assert code.isConsistentSSA(appView);
   }
 
   // The javac fix for JDK-8272564 has to be rewritten back to invoke-virtual on Object if the
@@ -474,5 +479,6 @@
         }
       }
     }
+    assert code.isConsistentSSA(appView);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index b41131a..b74dff0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -53,13 +53,15 @@
     // more dead instructions.
     Deque<BasicBlock> worklist = new ArrayDeque<>();
     do {
+      AffectedValues affectedValues = new AffectedValues();
       ValueIsDeadAnalysis valueIsDeadAnalysis = new ValueIsDeadAnalysis(appView, code);
       worklist.addAll(code.topologicallySortedBlocks());
       while (!worklist.isEmpty()) {
         BasicBlock block = worklist.removeLast();
-        removeDeadInstructions(worklist, code, block, valueIsDeadAnalysis);
+        removeDeadInstructions(worklist, code, block, affectedValues, valueIsDeadAnalysis);
         removeDeadPhis(worklist, block, valueIsDeadAnalysis);
       }
+      affectedValues.narrowingWithAssumeRemoval(appView, code);
     } while (branchSimplifier
             .simplifyIf(code)
             .asControlFlowSimplificationResult()
@@ -134,6 +136,7 @@
       Queue<BasicBlock> worklist,
       IRCode code,
       BasicBlock block,
+      AffectedValues affectedValues,
       ValueIsDeadAnalysis valueIsDeadAnalysis) {
     InstructionListIterator iterator = block.listIterator(code, block.getInstructions().size());
     while (iterator.hasPrevious()) {
@@ -145,7 +148,7 @@
           if (!checkCast.isRefiningStaticType(appView.options())
               && checkCast.outValue().getLocalInfo() == checkCast.object().getLocalInfo()) {
             updateWorklistWithNonDebugUses(worklist, checkCast);
-            checkCast.outValue().replaceUsers(checkCast.object());
+            checkCast.outValue().replaceUsers(checkCast.object(), affectedValues);
             checkCast.object().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
           }
         }
@@ -235,7 +238,8 @@
       }
     }
     if (mayHaveIntroducedUnreachableBlocks) {
-      code.removeUnreachableBlocks();
+      AffectedValues affectedValues = code.removeUnreachableBlocks();
+      affectedValues.narrowingWithAssumeRemoval(appView, code);
     }
     assert code.isConsistentGraph(appView);
     return mayHaveIntroducedUnreachableBlocks;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index 9a086cd..f53576a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -57,8 +56,7 @@
   }
 
   public void devirtualizeInvokeInterface(IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
-    AssumeRemover assumeRemover = new AssumeRemover(appView, code);
+    AffectedValues affectedValues = new AffectedValues();
     ProgramMethod context = code.context();
     Map<InvokeInterface, InvokeVirtual> devirtualizedCall = new IdentityHashMap<>();
     DominatorTree dominatorTree = new DominatorTree(code);
@@ -115,7 +113,7 @@
                 assert nonNull.src() == oldReceiver;
                 assert !oldReceiver.hasLocalInfo();
                 oldReceiver.replaceSelectiveUsers(
-                    newReceiver, ImmutableSet.of(nonNull), ImmutableMap.of());
+                    newReceiver, ImmutableSet.of(nonNull), ImmutableMap.of(), affectedValues);
               }
             }
           }
@@ -297,24 +295,20 @@
                 it.next();
               }
             }
-            affectedValues.addAll(receiver.affectedValues());
-            assumeRemover.markAssumeDynamicTypeUsersForRemoval(receiver);
             if (!receiver.hasLocalInfo()) {
               receiver.replaceSelectiveUsers(
-                  newReceiver, ImmutableSet.of(devirtualizedInvoke), ImmutableMap.of());
+                  newReceiver,
+                  ImmutableSet.of(devirtualizedInvoke),
+                  ImmutableMap.of(),
+                  affectedValues);
             } else {
-              receiver.removeUser(devirtualizedInvoke);
-              devirtualizedInvoke.replaceValue(receiver, newReceiver);
+              devirtualizedInvoke.replaceValue(receiver, newReceiver, affectedValues);
             }
           }
         }
       }
     }
-    assumeRemover.removeMarkedInstructions();
-    affectedValues.addAll(assumeRemover.getAffectedValues());
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
   }
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 11d016e..95f43b9 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
@@ -25,7 +25,6 @@
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.proto.ProtoInliningReasonStrategy;
 import com.android.tools.r8.ir.analysis.type.Nullability;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
@@ -667,9 +666,7 @@
         // using a const-class instruction.
         Value lockValue;
         if (target.getDefinition().isStatic()) {
-          lockValue =
-              code.createValue(
-                  TypeElement.fromDexType(dexItemFactory.objectType, definitelyNotNull(), appView));
+          lockValue = code.createValue(TypeElement.classClassType(appView, definitelyNotNull()));
           monitorEnterBlockIterator.add(new ConstClass(lockValue, target.getHolderType()));
         } else {
           lockValue = entryBlock.getInstructions().getFirst().asArgument().outValue();
@@ -940,7 +937,7 @@
       InliningIRProvider inliningIRProvider,
       MethodProcessor methodProcessor,
       Timing timing) {
-    AssumeRemover assumeRemover = new AssumeRemover(appView, code);
+    AffectedValues affectedValues = new AffectedValues();
     Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
     BasicBlockIterator blockIterator = code.listIterator();
     ClassInitializationAnalysis classInitializationAnalysis =
@@ -973,7 +970,7 @@
           }
 
           if (tryInlineMethodWithoutSideEffects(
-              code, iterator, invoke, resolutionResult.getResolutionPair(), assumeRemover)) {
+              code, iterator, invoke, resolutionResult.getResolutionPair())) {
             continue;
           }
 
@@ -1032,12 +1029,6 @@
           // Verify this code went through the full pipeline.
           assert singleTarget.getDefinition().isProcessed();
 
-          // Mark AssumeDynamicType instruction for the out-value for removal, if any.
-          Value outValue = invoke.outValue();
-          if (outValue != null) {
-            assumeRemover.markAssumeDynamicTypeUsersForRemoval(outValue);
-          }
-
           boolean inlineeMayHaveInvokeMethod = inlinee.code.metadata().mayHaveInvokeMethod();
 
           // Inline the inlinee code in place of the invoke instruction
@@ -1065,7 +1056,8 @@
           }
 
           classInitializationAnalysis.notifyCodeHasChanged();
-          postProcessInlineeBlocks(code, blockIterator, block, blocksToRemove, timing);
+          postProcessInlineeBlocks(
+              code, blockIterator, block, affectedValues, blocksToRemove, timing);
 
           // The synthetic and bridge flags are maintained only if the inlinee has also these flags.
           if (context.getDefinition().isBridge() && !inlinee.code.method().isBridge()) {
@@ -1089,15 +1081,13 @@
               blockIterator.next();
             }
           }
-        } else if (current.isAssume()) {
-          assumeRemover.removeIfMarked(current.asAssume(), iterator);
         }
       }
     }
     assert inlineeStack.isEmpty();
-    assumeRemover.removeMarkedInstructions(blocksToRemove).finish();
-    classInitializationAnalysis.finish();
     code.removeBlocks(blocksToRemove);
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    classInitializationAnalysis.finish();
     code.removeAllDeadAndTrivialPhis();
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
@@ -1107,8 +1097,7 @@
       IRCode code,
       InstructionListIterator iterator,
       InvokeMethod invoke,
-      DexClassAndMethod resolvedMethod,
-      AssumeRemover assumeRemover) {
+      DexClassAndMethod resolvedMethod) {
     if (invoke.isInvokeMethodWithReceiver()) {
       if (!iterator.replaceCurrentInstructionByNullCheckIfPossible(appView, code.context())) {
         return false;
@@ -1123,7 +1112,6 @@
     }
 
     // Succeeded.
-    assumeRemover.markUnusedAssumeValuesForRemoval(invoke.arguments());
     return true;
   }
 
@@ -1149,6 +1137,7 @@
       IRCode code,
       BasicBlockIterator blockIterator,
       BasicBlock block,
+      AffectedValues affectedValues,
       Set<BasicBlock> blocksToRemove,
       Timing timing) {
     BasicBlock state = IteratorUtils.peekNext(blockIterator);
@@ -1164,7 +1153,7 @@
             inlineeBlocks.add(inlineeBlock);
           }
         });
-    applyMemberValuePropagationToInlinee(code, blockIterator, inlineeBlocks);
+    applyMemberValuePropagationToInlinee(code, blockIterator, affectedValues, inlineeBlocks);
 
     // Add non-null IRs only to the inlinee blocks.
     rewindBlockIterator(blockIterator, block);
@@ -1179,19 +1168,19 @@
       BasicBlockIterator blockIterator,
       Set<BasicBlock> inlineeBlocks,
       Timing timing) {
-    new AssumeInserter(appView)
+    boolean keepRedundantBlocks = true; // since we have a live block iterator
+    new AssumeInserter(appView, keepRedundantBlocks)
         .insertAssumeInstructionsInBlocks(code, blockIterator, inlineeBlocks::contains, timing);
     assert !blockIterator.hasNext();
   }
 
   private void applyMemberValuePropagationToInlinee(
-      IRCode code, BasicBlockIterator blockIterator, Set<BasicBlock> inlineeBlocks) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      AffectedValues affectedValues,
+      Set<BasicBlock> inlineeBlocks) {
     new R8MemberValuePropagation(appView)
         .run(code, blockIterator, affectedValues, inlineeBlocks::contains);
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
     assert !blockIterator.hasNext();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java
new file mode 100644
index 0000000..bd0376b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java
@@ -0,0 +1,276 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+
+// Per-class collection of member signatures.
+public abstract class MemberPoolCollection<R extends DexMember<?, R>> {
+
+  final Equivalence<R> equivalence;
+  final AppView<AppInfoWithLiveness> appView;
+  final SubtypingInfo subtypingInfo;
+  final Map<DexClass, MemberPool<R>> memberPools = new ConcurrentHashMap<>();
+
+  MemberPoolCollection(
+      AppView<AppInfoWithLiveness> appView,
+      Equivalence<R> equivalence,
+      SubtypingInfo subtypingInfo) {
+    this.appView = appView;
+    this.equivalence = equivalence;
+    this.subtypingInfo = subtypingInfo;
+  }
+
+  public void buildAll(ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Building member pool collection");
+    try {
+      List<Future<?>> futures = new ArrayList<>();
+
+      // Generate a future for each class that will build the member pool collection for the
+      // corresponding class. Note that, we visit the classes using a top-down class hierarchy
+      // traversal, since this ensures that we do not visit library classes that are not
+      // reachable from any program class.
+      TopDownClassHierarchyTraversal.forAllClasses(appView)
+          .visit(appView.appInfo().classes(), clazz -> submit(clazz, futures, executorService));
+      ThreadUtils.awaitFutures(futures);
+    } finally {
+      timing.end();
+    }
+  }
+
+  public MemberPool<R> buildForHierarchy(
+      DexClass clazz, ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Building member pool collection");
+    try {
+      List<Future<?>> futures = new ArrayList<>();
+      submitAll(
+          getAllSuperTypesInclusive(clazz, memberPools::containsKey), futures, executorService);
+      submitAll(getAllSubTypesExclusive(clazz, memberPools::containsKey), futures, executorService);
+      ThreadUtils.awaitFutures(futures);
+    } finally {
+      timing.end();
+    }
+    return get(clazz);
+  }
+
+  public boolean hasPool(DexClass clazz) {
+    return memberPools.containsKey(clazz);
+  }
+
+  public MemberPool<R> get(DexClass clazz) {
+    assert hasPool(clazz);
+    return memberPools.get(clazz);
+  }
+
+  public boolean markIfNotSeen(DexClass clazz, R reference) {
+    MemberPool<R> memberPool = get(clazz);
+    Wrapper<R> key = equivalence.wrap(reference);
+    if (memberPool.hasSeen(key)) {
+      return true;
+    }
+    memberPool.seen(key);
+    return false;
+  }
+
+  private void submitAll(
+      Iterable<? extends DexClass> classes,
+      List<Future<?>> futures,
+      ExecutorService executorService) {
+    for (DexClass clazz : classes) {
+      submit(clazz, futures, executorService);
+    }
+  }
+
+  private void submit(DexClass clazz, List<Future<?>> futures, ExecutorService executorService) {
+    futures.add(executorService.submit(computeMemberPoolForClass(clazz)));
+  }
+
+  abstract Runnable computeMemberPoolForClass(DexClass clazz);
+
+  private Set<DexClass> getAllSuperTypesInclusive(
+      DexClass subject, Predicate<DexClass> stoppingCriterion) {
+    Set<DexClass> superTypes = new HashSet<>();
+    Deque<DexClass> worklist = new ArrayDeque<>();
+    worklist.add(subject);
+    while (!worklist.isEmpty()) {
+      DexClass clazz = worklist.pop();
+      if (stoppingCriterion.test(clazz)) {
+        continue;
+      }
+      if (superTypes.add(clazz)) {
+        if (clazz.superType != null) {
+          addNonNull(worklist, appView.definitionFor(clazz.superType));
+        }
+        for (DexType interfaceType : clazz.interfaces.values) {
+          addNonNull(worklist, appView.definitionFor(interfaceType));
+        }
+      }
+    }
+    return superTypes;
+  }
+
+  private Set<DexClass> getAllSubTypesExclusive(
+      DexClass subject, Predicate<DexClass> stoppingCriterion) {
+    Set<DexClass> subTypes = new HashSet<>();
+    Deque<DexClass> worklist = new ArrayDeque<>();
+    subtypingInfo.forAllImmediateExtendsSubtypes(
+        subject.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+    subtypingInfo.forAllImmediateImplementsSubtypes(
+        subject.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+    while (!worklist.isEmpty()) {
+      DexClass clazz = worklist.pop();
+      if (stoppingCriterion.test(clazz)) {
+        continue;
+      }
+      if (subTypes.add(clazz)) {
+        subtypingInfo.forAllImmediateExtendsSubtypes(
+            clazz.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+        subtypingInfo.forAllImmediateImplementsSubtypes(
+            clazz.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+      }
+    }
+    return subTypes;
+  }
+
+  public static class MemberPool<T> {
+
+    private final DexClass clazz;
+    private final Equivalence<T> equivalence;
+    private MemberPool<T> superType;
+    private final Set<MemberPool<T>> interfaces = new HashSet<>();
+    private final Set<MemberPool<T>> subTypes = new HashSet<>();
+    private final Set<Wrapper<T>> memberPool = new HashSet<>();
+
+    MemberPool(Equivalence<T> equivalence, DexClass clazz) {
+      this.equivalence = equivalence;
+      this.clazz = clazz;
+    }
+
+    synchronized void linkSupertype(MemberPool<T> superType) {
+      assert this.superType == null;
+      this.superType = superType;
+    }
+
+    synchronized void linkSubtype(MemberPool<T> subType) {
+      boolean added = subTypes.add(subType);
+      assert added;
+    }
+
+    synchronized void linkInterface(MemberPool<T> itf) {
+      boolean added = interfaces.add(itf);
+      assert added;
+    }
+
+    public void seen(T member) {
+      seen(equivalence.wrap(member));
+    }
+
+    public synchronized void seen(Wrapper<T> member) {
+      boolean added = memberPool.add(member);
+      assert added;
+    }
+
+    public boolean hasSeen(Wrapper<T> member) {
+      return fold(member, false, true, (t, ignored) -> true);
+    }
+
+    public boolean hasSeenDirectly(Wrapper<T> member) {
+      return here(member, false, (t, ignored) -> true);
+    }
+
+    public boolean hasSeenStrictlyAbove(Wrapper<T> member) {
+      return above(member, false, false, true, (t, ignored) -> true);
+    }
+
+    public boolean hasSeenStrictlyBelow(Wrapper<T> member) {
+      return below(member, false, true, (t, ignored) -> true);
+    }
+
+    private <S> S above(
+        Wrapper<T> member,
+        boolean inclusive,
+        S value,
+        S terminator,
+        BiFunction<DexClass, S, S> accumulator) {
+      WorkList<MemberPool<T>> workList = WorkList.newIdentityWorkList(this);
+      while (workList.hasNext()) {
+        MemberPool<T> next = workList.next();
+        if (inclusive) {
+          value = next.here(member, value, accumulator);
+          if (value == terminator) {
+            return value;
+          }
+        }
+        inclusive = true;
+        if (next.superType != null) {
+          workList.addIfNotSeen(next.superType);
+        }
+        workList.addIfNotSeen(next.interfaces);
+      }
+      return value;
+    }
+
+    private <S> S here(Wrapper<T> member, S value, BiFunction<DexClass, S, S> accumulator) {
+      if (memberPool.contains(member)) {
+        return accumulator.apply(clazz, value);
+      }
+      return value;
+    }
+
+    public <S> S below(
+        Wrapper<T> member, S value, S terminator, BiFunction<DexClass, S, S> accumulator) {
+      WorkList<MemberPool<T>> workList = WorkList.newIdentityWorkList(this.subTypes);
+      while (workList.hasNext()) {
+        MemberPool<T> next = workList.next();
+        value = next.here(member, value, accumulator);
+        if (value == terminator) {
+          return value;
+        }
+        workList.addIfNotSeen(next.interfaces);
+        workList.addIfNotSeen(next.subTypes);
+      }
+      return value;
+    }
+
+    public <S> S fold(
+        Wrapper<T> member, S initialValue, S terminator, BiFunction<DexClass, S, S> accumulator) {
+      S value = above(member, true, initialValue, terminator, accumulator);
+      if (value == terminator) {
+        return value;
+      }
+      return below(member, initialValue, terminator, accumulator);
+    }
+  }
+
+  private static <T> void addNonNull(Collection<T> collection, T item) {
+    if (item != null) {
+      collection.add(item);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
new file mode 100644
index 0000000..78eb343
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.google.common.base.Predicates;
+import java.util.function.Predicate;
+
+// Per-class collection of method signatures.
+//
+// Example use cases:
+// *) in publicizer,
+//   to determine if a private method does not collide with methods in that class hierarchy.
+// *) in vertical class merger,
+//   before moving a default interface method to its subtype, check if it does not collide with one
+//   in the given class hierarchy.
+public class MethodPoolCollection extends MemberPoolCollection<DexMethod> {
+
+  private final Predicate<DexEncodedMethod> methodTester;
+
+  public MethodPoolCollection(AppView<AppInfoWithLiveness> appView, SubtypingInfo subtypingInfo) {
+    this(appView, subtypingInfo, Predicates.alwaysTrue());
+  }
+
+  public MethodPoolCollection(
+      AppView<AppInfoWithLiveness> appView,
+      SubtypingInfo subtypingInfo,
+      Predicate<DexEncodedMethod> methodTester) {
+    super(appView, MethodSignatureEquivalence.get(), subtypingInfo);
+    this.methodTester = methodTester;
+  }
+
+  public static boolean excludesPrivateInstanceMethod(DexEncodedMethod method) {
+    return !method.isPrivateMethod() || method.isStatic();
+  }
+
+  @Override
+  Runnable computeMemberPoolForClass(DexClass clazz) {
+    return () -> {
+      MemberPool<DexMethod> methodPool =
+          memberPools.computeIfAbsent(clazz, k -> new MemberPool<>(equivalence, k));
+      clazz.forEachMethod(
+          encodedMethod -> {
+            if (methodTester.test(encodedMethod)) {
+              methodPool.seen(equivalence.wrap(encodedMethod.getReference()));
+            }
+          });
+      if (clazz.superType != null) {
+        DexClass superClazz = appView.definitionFor(clazz.superType);
+        if (superClazz != null) {
+          MemberPool<DexMethod> superPool =
+              memberPools.computeIfAbsent(
+                  superClazz, k -> new MemberPool<>(equivalence, superClazz));
+          superPool.linkSubtype(methodPool);
+          methodPool.linkSupertype(superPool);
+        }
+      }
+      if (clazz.isInterface()) {
+        for (DexType subtype : subtypingInfo.allImmediateSubtypes(clazz.type)) {
+          DexClass subClazz = appView.definitionFor(subtype);
+          if (subClazz != null) {
+            MemberPool<DexMethod> childPool =
+                memberPools.computeIfAbsent(subClazz, k -> new MemberPool<>(equivalence, subClazz));
+            methodPool.linkSubtype(childPool);
+            childPool.linkInterface(methodPool);
+          }
+        }
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index b2b0782..e9c9747 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -26,7 +26,6 @@
 import com.android.tools.r8.ir.code.ArrayGet;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.FieldGet;
 import com.android.tools.r8.ir.code.FieldInstruction;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InitClass;
@@ -232,7 +231,7 @@
     private final boolean release;
 
     // Values that may require type propagation.
-    private final Set<Value> affectedValues = Sets.newIdentityHashSet();
+    private final AffectedValues affectedValues = new AffectedValues();
 
     // Maps keeping track of fields that have an already loaded value at basic block entry.
     private final BlockStates activeStates = new BlockStates();
@@ -268,8 +267,7 @@
 
       @Override
       public void eliminateRedundantRead(InstructionListIterator it, Instruction redundant) {
-        affectedValues.addAll(redundant.outValue().affectedValues());
-        redundant.outValue().replaceUsers(value);
+        redundant.outValue().replaceUsers(value, affectedValues);
         it.removeOrReplaceByDebugLocalRead();
         value.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
         hasChanged = true;
@@ -359,7 +357,6 @@
         }
       }
 
-      AssumeRemover assumeRemover = new AssumeRemover(appView, code, affectedValues);
       for (BasicBlock head : code.topologicallySortedBlocks()) {
         if (head.hasUniquePredecessor() && head.getUniquePredecessor().hasUniqueNormalSuccessor()) {
           // Already visited.
@@ -389,16 +386,14 @@
               }
 
               if (instruction.isInstanceGet()) {
-                handleInstanceGet(it, instruction.asInstanceGet(), field, assumeRemover);
+                handleInstanceGet(it, instruction.asInstanceGet(), field);
               } else if (instruction.isInstancePut()) {
                 handleInstancePut(instruction.asInstancePut(), field);
               } else if (instruction.isStaticGet()) {
-                handleStaticGet(it, instruction.asStaticGet(), field, assumeRemover);
+                handleStaticGet(it, instruction.asStaticGet(), field);
               } else if (instruction.isStaticPut()) {
                 handleStaticPut(instruction.asStaticPut(), field);
               }
-            } else if (instruction.isAssume()) {
-              assumeRemover.removeIfMarked(instruction.asAssume(), it);
             } else if (instruction.isInitClass()) {
               handleInitClass(it, instruction.asInitClass());
             } else if (instruction.isMonitor()) {
@@ -468,11 +463,10 @@
         activeStates.recordActiveStateOnBlockExit(end, activeState);
       }
       processInstructionsToRemove();
-      assumeRemover.removeMarkedInstructions().finish();
+      affectedValues.narrowingWithAssumeRemoval(appView, code);
       if (hasChanged) {
         code.removeRedundantBlocks();
       }
-      assert code.isConsistentSSA(appView);
       return CodeRewriterResult.hasChanged(hasChanged);
     }
 
@@ -669,10 +663,7 @@
     }
 
     private void handleInstanceGet(
-        InstructionListIterator it,
-        InstanceGet instanceGet,
-        DexClassAndField field,
-        AssumeRemover assumeRemover) {
+        InstructionListIterator it, InstanceGet instanceGet, DexClassAndField field) {
       if (instanceGet.outValue().hasLocalInfo()) {
         clearMostRecentInstanceFieldWrite(instanceGet, field);
         return;
@@ -683,7 +674,6 @@
       FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
       if (replacement != null) {
         if (isRedundantFieldLoadEliminationAllowed(field)) {
-          markAssumeDynamicTypeUsersForRemoval(instanceGet, replacement, assumeRemover);
           replacement.eliminateRedundantRead(it, instanceGet);
         }
         return;
@@ -723,21 +713,6 @@
       }
     }
 
-    private void markAssumeDynamicTypeUsersForRemoval(
-        FieldGet fieldGet, FieldValue replacement, AssumeRemover assumeRemover) {
-      ExistingValue existingValue = replacement.asExistingValue();
-      if (existingValue == null
-          || !existingValue
-              .getValue()
-              .isDefinedByInstructionSatisfying(
-                  definition ->
-                      definition.isFieldGet()
-                          && definition.asFieldGet().getField().getType()
-                              == fieldGet.getField().getType())) {
-        assumeRemover.markAssumeDynamicTypeUsersForRemoval(fieldGet.outValue());
-      }
-    }
-
     private void handleInstancePut(InstancePut instancePut, DexClassAndField field) {
       // An instance-put instruction can potentially write the given field on all objects because of
       // aliases.
@@ -778,10 +753,7 @@
     }
 
     private void handleStaticGet(
-        InstructionListIterator instructionIterator,
-        StaticGet staticGet,
-        DexClassAndField field,
-        AssumeRemover assumeRemover) {
+        InstructionListIterator instructionIterator, StaticGet staticGet, DexClassAndField field) {
       markClassAsInitialized(field.getHolderType());
 
       if (staticGet.outValue().hasLocalInfo()) {
@@ -792,7 +764,6 @@
 
       FieldValue replacement = activeState.getStaticFieldValue(field.getReference());
       if (replacement != null) {
-        markAssumeDynamicTypeUsersForRemoval(staticGet, replacement, assumeRemover);
         replacement.eliminateRedundantRead(instructionIterator, staticGet);
         return;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
index d7d41bd..d03fccb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
@@ -28,7 +27,6 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.google.common.collect.Sets;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -41,7 +39,7 @@
     if (!appView.appInfo().canUseConstClassInstructions(appView.options())) {
       return;
     }
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     ProgramMethod context = code.context();
     BasicBlockIterator blockIterator = code.listIterator();
     while (blockIterator.hasNext()) {
@@ -71,9 +69,7 @@
       }
     }
     // Newly introduced const-class is not null, and thus propagate that information.
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
   }
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 9a75fc0..e79cac4 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
@@ -239,6 +239,7 @@
       new Rewriter(code, instructionIterator, serviceLoaderLoad)
           .perform(classLoaderInvoke, synthesizedMethod.getReference());
     }
+    assert code.isConsistentSSA(appView);
   }
 
   public void onLastWaveDone(PostMethodProcessor.Builder postMethodProcessorBuilder) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java b/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
index 1a594fc..2a90c2e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
@@ -166,7 +166,7 @@
     }
     // We are changing a NewInstance to a method call where we loose that the type is not null.
     assert !newOutValues.isEmpty();
-    new TypeAnalysis(appView).widening(newOutValues);
+    new TypeAnalysis(appView, code).widening(newOutValues);
 
     // Outlining of instance initializers will in most cases change the api level of the context
     // since all other soft verification issues has been outlined. To ensure that we do not inline
@@ -175,6 +175,8 @@
     if (appView.enableWholeProgramOptimizations()) {
       recomputeApiLevel(context, code);
     }
+
+    assert code.isConsistentSSA(appView);
   }
 
   public void onLastWaveDone(PostMethodProcessor.Builder postMethodProcessorBuilder) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index 19b2d81..112697a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -17,11 +17,10 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionOrPhi;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.passes.BranchSimplifier;
 import com.android.tools.r8.ir.conversion.passes.TrivialCheckCastAndInstanceOfRemover;
-import com.android.tools.r8.ir.optimize.AssumeRemover;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.InliningOracle;
 import com.android.tools.r8.ir.optimize.classinliner.InlineCandidateProcessor.IllegalClassInlinerStateException;
@@ -32,10 +31,8 @@
 import com.android.tools.r8.utils.LazyBox;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 public final class ClassInliner {
@@ -207,11 +204,9 @@
         }
 
         // Inline the class instance.
-        Set<Value> affectedValues = Sets.newIdentityHashSet();
-        AssumeRemover assumeRemover = new AssumeRemover(appView, code, affectedValues);
+        AffectedValues affectedValues = new AffectedValues();
         try {
-          anyInlinedMethods |=
-              processor.processInlining(code, affectedValues, assumeRemover, inliningIRProvider);
+          anyInlinedMethods |= processor.processInlining(code, affectedValues, inliningIRProvider);
         } catch (IllegalClassInlinerStateException e) {
           // We introduced a user that we cannot handle in the class inliner as a result of force
           // inlining. Abort gracefully from class inlining without removing the instance.
@@ -225,10 +220,9 @@
         }
 
         // Restore normality.
-        assumeRemover.removeMarkedInstructions();
         code.removeAllDeadAndTrivialPhis(affectedValues);
+        affectedValues.narrowingWithAssumeRemoval(appView, code);
         code.removeRedundantBlocks();
-        assumeRemover.finish();
         assert code.isConsistentSSA(appView);
         rootsIterator.remove();
         repeat = true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 52eaa3a..256d368 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.SingleConstValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
@@ -52,7 +51,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.optimize.AssumeRemover;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
@@ -379,10 +378,7 @@
   //
   // Returns `true` if at least one method was inlined.
   boolean processInlining(
-      IRCode code,
-      Set<Value> affectedValues,
-      AssumeRemover assumeRemover,
-      InliningIRProvider inliningIRProvider)
+      IRCode code, AffectedValues affectedValues, InliningIRProvider inliningIRProvider)
       throws IllegalClassInlinerStateException {
     // Verify that `eligibleInstance` is not aliased.
     assert eligibleInstance == eligibleInstance.getAliasedValue();
@@ -392,7 +388,7 @@
 
     rebindIndirectEligibleInstanceUsersFromPhis();
     removeMiscUsages(code, affectedValues);
-    removeFieldReads(code, assumeRemover);
+    removeFieldReads(code, affectedValues);
     removeFieldWrites();
     removeInstruction(root);
     return anyInlinedMethods;
@@ -590,7 +586,7 @@
   }
 
   // Remove miscellaneous users before handling field reads.
-  private void removeMiscUsages(IRCode code, Set<Value> affectedValues) {
+  private void removeMiscUsages(IRCode code, AffectedValues affectedValues) {
     boolean needToRemoveUnreachableBlocks = false;
     for (Instruction user : eligibleInstance.uniqueUsers()) {
       if (user.isInstanceOf()) {
@@ -682,25 +678,19 @@
   }
 
   // Replace field reads with appropriate values, insert phis when needed.
-  private void removeFieldReads(IRCode code, AssumeRemover assumeRemover) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+  private void removeFieldReads(IRCode code, AffectedValues affectedValues) {
     if (root.isNewInstance()) {
-      removeFieldReadsFromNewInstance(code, affectedValues, assumeRemover);
+      removeFieldReadsFromNewInstance(code, affectedValues);
     } else {
       assert root.isStaticGet();
-      removeFieldReadsFromStaticGet(code, affectedValues, assumeRemover);
-    }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
+      removeFieldReadsFromStaticGet(code, affectedValues);
     }
   }
 
-  private void removeFieldReadsFromNewInstance(
-      IRCode code, Set<Value> affectedValues, AssumeRemover assumeRemover) {
+  private void removeFieldReadsFromNewInstance(IRCode code, AffectedValues affectedValues) {
     List<InstanceGet> uniqueInstanceGetUsersWithDeterministicOrder = new ArrayList<>();
     for (Instruction user : eligibleInstance.uniqueUsers()) {
       if (user.isInstanceGet()) {
-        assumeRemover.markAssumeDynamicTypeUsersForRemoval(user.outValue());
         if (user.hasUsedOutValue()) {
           uniqueInstanceGetUsersWithDeterministicOrder.add(user.asInstanceGet());
         } else {
@@ -734,7 +724,7 @@
   private void removeFieldReadFromNewInstance(
       IRCode code,
       InstanceGet fieldRead,
-      Set<Value> affectedValues,
+      AffectedValues affectedValues,
       Map<DexField, FieldValueHelper> fieldHelpers) {
     Value value = fieldRead.outValue();
     if (value != null) {
@@ -756,8 +746,7 @@
     removeInstruction(fieldRead);
   }
 
-  private void removeFieldReadsFromStaticGet(
-      IRCode code, Set<Value> affectedValues, AssumeRemover assumeRemover) {
+  private void removeFieldReadsFromStaticGet(IRCode code, AffectedValues affectedValues) {
     Set<BasicBlock> seen = Sets.newIdentityHashSet();
     Set<Instruction> users = eligibleInstance.uniqueUsers();
     for (Instruction user : users) {
@@ -778,7 +767,6 @@
         }
 
         if (instruction.isInstanceGet()) {
-          assumeRemover.markAssumeDynamicTypeUsersForRemoval(instruction.outValue());
           if (instruction.hasUsedOutValue()) {
             replaceFieldReadFromStaticGet(
                 code, instructionIterator, user.asInstanceGet(), affectedValues);
@@ -806,7 +794,7 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InstanceGet fieldRead,
-      Set<Value> affectedValues) {
+      AffectedValues affectedValues) {
     DexField fieldReference = fieldRead.getField();
     DexClass holder = appView.definitionFor(fieldReference.getHolderType(), method);
     DexEncodedField field = fieldReference.lookupOnClass(holder);
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 7f34299..4ca2bb2 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
@@ -527,7 +527,6 @@
               Assume assume = enumUser.asAssume();
               if (assume
                   .getDynamicTypeAssumption()
-                  .getDynamicType()
                   .getDynamicUpperBoundType()
                   .equalUpToNullability(enumClass.getType().toTypeElement(appView))) {
                 // OK.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index c88b48d..6ae8689 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
@@ -34,10 +33,10 @@
 import com.android.tools.r8.ir.code.Value;
 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.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ArrayUtils;
-import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -63,7 +62,7 @@
   protected CodeRewriterResult rewriteCode(IRCode code) {
     assert appView.enableWholeProgramOptimizations();
     boolean hasChanged = false;
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     InstructionListIterator iterator = code.instructionListIterator();
     while (iterator.hasNext()) {
       Instruction current = iterator.next();
@@ -171,10 +170,7 @@
         hasChanged = true;
       }
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
-    assert code.isConsistentSSA(appView);
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     return CodeRewriterResult.hasChanged(hasChanged);
   }
 
@@ -221,7 +217,7 @@
       return;
     }
     assert appView.enableWholeProgramOptimizations();
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     boolean mayHaveIntroducedUnreachableBlocks = false;
     for (BasicBlock block : code.blocks) {
       IntSwitch switchInsn = block.exit().asIntSwitch();
@@ -323,9 +319,8 @@
     if (mayHaveIntroducedUnreachableBlocks) {
       affectedValues.addAll(code.removeUnreachableBlocks());
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    assert code.isConsistentSSA(appView);
   }
 
   private Int2IntArrayMap computeOrdinalToTargetMap(
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 ee5dc08..fd78da3 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
@@ -47,7 +47,8 @@
     if (appView.hasLiveness()
         && singleTarget.getReference() == appView.dexItemFactory().enumMembers.valueOf
         && invoke.inValues().get(0).isConstClass()) {
-      insertAssumeDynamicType(appView.withLiveness(), code, instructionIterator, invoke);
+      insertAssumeDynamicType(
+          appView.withLiveness(), code, instructionIterator, invoke, affectedValues);
     }
   }
 
@@ -55,7 +56,8 @@
       AppView<AppInfoWithLiveness> appView,
       IRCode code,
       InstructionListIterator instructionIterator,
-      InvokeMethod invoke) {
+      InvokeMethod invoke,
+      Set<Value> affectedValues) {
     // TODO(b/152516470): Support unboxing enums with Enum#valueOf in try-catch.
     if (invoke.getBlock().hasCatchHandlers()) {
       return;
@@ -72,14 +74,15 @@
       return;
     }
     // Replace usages of out-value by the out-value of the AssumeDynamicType instruction.
-    Value specializedOutValue = code.createValue(outValue.getType(), outValue.getLocalInfo());
-    outValue.replaceUsers(specializedOutValue);
+    Value specializedOutValue =
+        code.createValue(
+            outValue.getType().asReferenceType().asMeetWithNotNull(), outValue.getLocalInfo());
+    outValue.replaceUsers(specializedOutValue, affectedValues);
 
     // Insert AssumeDynamicType instruction.
     DynamicTypeWithUpperBound dynamicType = enumType.toDynamicType(appView, definitelyNotNull());
     Assume assumeInstruction =
-        Assume.createAssumeDynamicTypeInstruction(
-            dynamicType, specializedOutValue, outValue, invoke, appView);
+        Assume.create(dynamicType, specializedOutValue, outValue, invoke, appView, code.context());
     assumeInstruction.setPosition(appView.options().debug ? invoke.getPosition() : Position.none());
     instructionIterator.add(assumeInstruction);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index b00db83..a162069 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -12,16 +12,15 @@
 import com.android.tools.r8.graph.DexItemFactory.LibraryMembers;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.CodeOptimization;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
@@ -115,7 +114,7 @@
       OptimizationFeedback feedback,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     BasicBlockIterator blockIterator = code.listIterator();
     Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
     while (blockIterator.hasNext()) {
@@ -170,9 +169,8 @@
     }
 
     code.removeBlocks(blocksToRemove);
-
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    code.removeRedundantBlocks();
+    assert code.isConsistentSSA(appView);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
index 456ecc7..fb14d81 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
@@ -86,6 +86,7 @@
             instructionIterator,
             invokeWithReceiver,
             singleTarget,
+            affectedValues,
             state,
             methodProcessor,
             methodProcessingContext);
@@ -99,12 +100,13 @@
       InstructionListIterator instructionIterator,
       InvokeMethodWithReceiver invoke,
       DexClassAndMethod singleTarget,
+      Set<Value> affectedValues,
       State state,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     boolean isStringBuilderUnused = state.isUnusedBuilder(invoke.getReceiver());
     if (invoke.hasOutValue() && (options.isGeneratingDex() || isStringBuilderUnused)) {
-      invoke.outValue().replaceUsers(invoke.getReceiver());
+      invoke.outValue().replaceUsers(invoke.getReceiver(), affectedValues);
       invoke.getReceiver().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
       invoke.clearOutValue();
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagation.java
index 90fd365..e48be77 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagation.java
@@ -19,7 +19,6 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.SingleValue;
 import com.android.tools.r8.ir.code.ArrayGet;
@@ -36,9 +35,9 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfo;
 import com.android.tools.r8.utils.IteratorUtils;
-import com.google.common.collect.Sets;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -60,14 +59,11 @@
     if (!metadata.mayHaveFieldInstruction() && !metadata.mayHaveInvokeMethod()) {
       return;
     }
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     run(code, code.listIterator(), affectedValues, alwaysTrue());
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
-    assert code.verifyTypes(appView);
   }
 
   public void run(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index 45f03bc..50154ca 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.PrimitiveTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Add;
@@ -64,6 +65,7 @@
 import com.android.tools.r8.ir.conversion.MethodProcessorEventConsumer;
 import com.android.tools.r8.ir.conversion.SourceCode;
 import com.android.tools.r8.ir.conversion.passes.MoveResultRewriter;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
@@ -1233,6 +1235,7 @@
     @Override
     protected void handle(int start, int end, Outline outline) {
       DexMethod outlineMethod = generatedOutlines.get(outline);
+      AffectedValues affectedValues = new AffectedValues();
       if (outlineMethod != null) {
         assert removeMethodFromOutlineList(outline);
         List<Value> in = new ArrayList<>();
@@ -1295,9 +1298,23 @@
             lastInstruction.getBlock().listIterator(code, lastInstruction);
         Instruction instructionBeforeEnd = endIterator.previous();
         assert instructionBeforeEnd == lastInstruction;
-        endIterator.set(outlineInvoke); // Replaces instructionBeforeEnd.
+        endIterator.set(outlineInvoke);
+        if (outlineInvoke.hasOutValue()
+            && returnValue.getType().isReferenceType()
+            && returnValue.getType().nullability().isDefinitelyNotNull()) {
+          Value nullableReturnValue =
+              code.createValue(
+                  returnValue
+                      .getType()
+                      .asReferenceType()
+                      .getOrCreateVariant(Nullability.maybeNull()),
+                  returnValue.getLocalInfo());
+          outlineInvoke.outValue().replaceUsers(nullableReturnValue, affectedValues);
+          outlineInvoke.setOutValue(nullableReturnValue);
+        }
         invokesToOutlineMethods.add(outlineInvoke);
       }
+      affectedValues.widening(appView, code);
     }
 
     /** When assertions are enabled, remove method from the outline's list. */
@@ -1612,7 +1629,10 @@
               }
             });
       }
+      code.removeRedundantBlocks();
     }
+    code.removeRedundantBlocks();
+    assert code.isConsistentSSA(appView);
   }
 
   public boolean checkAllOutlineSitesFoundAgain() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
index fd6ae9b..c19f65e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.google.common.collect.ImmutableList;
 
 /** StringBuilderAction defines an interface for updating the IR code based on optimizations. */
@@ -25,6 +26,7 @@
       IRCode code,
       InstructionListIterator iterator,
       Instruction instruction,
+      AffectedValues affectedValues,
       StringBuilderOracle oracle);
 
   default boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() {
@@ -50,8 +52,9 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
-      removeStringBuilderInstruction(iterator, instruction, oracle);
+      removeStringBuilderInstruction(iterator, instruction, affectedValues, oracle);
     }
 
     static RemoveStringBuilderAction getInstance() {
@@ -81,8 +84,12 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
       assert oracle.isToString(instruction, instruction.getFirstOperand());
+      if (instruction.hasOutValue()) {
+        instruction.outValue().addAffectedValuesTo(affectedValues);
+      }
       iterator.replaceCurrentInstructionWithConstString(appView, code, replacement);
     }
   }
@@ -106,6 +113,7 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
       Instruction previous = iterator.previous();
       InvokeMethodWithReceiver invoke = previous.asInvokeMethodWithReceiver();
@@ -168,8 +176,9 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
-      instruction.outValue().replaceUsers(existingString);
+      instruction.outValue().replaceUsers(existingString, affectedValues);
       iterator.removeOrReplaceByDebugLocalRead();
     }
 
@@ -214,6 +223,7 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
       Value constString = null;
       if (newConstant != null) {
@@ -252,6 +262,7 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
       instruction.replaceValue(1, string);
     }
@@ -305,6 +316,7 @@
         IRCode code,
         InstructionListIterator iterator,
         Instruction instruction,
+        AffectedValues affectedValues,
         StringBuilderOracle oracle) {
       assert instruction.isInvokeMethod();
       assert instruction.inValues().size() == 2;
@@ -330,7 +342,7 @@
       Instruction next = iterator.next();
       assert next == instruction;
       if (removeInstruction) {
-        removeStringBuilderInstruction(iterator, instruction, oracle);
+        removeStringBuilderInstruction(iterator, instruction, affectedValues, oracle);
       } else {
         instruction.replaceValue(1, outValue);
       }
@@ -389,16 +401,19 @@
   }
 
   static void removeStringBuilderInstruction(
-      InstructionListIterator iterator, Instruction instruction, StringBuilderOracle oracle) {
+      InstructionListIterator iterator,
+      Instruction instruction,
+      AffectedValues affectedValues,
+      StringBuilderOracle oracle) {
     assert oracle.isModeledStringBuilderInstruction(
         instruction,
         value ->
             value.getType().isClassType()
                 && oracle.isStringBuilderType(value.getType().asClassType().getClassType()));
-    if (oracle.isAppend(instruction) && instruction.outValue() != null) {
+    if (oracle.isAppend(instruction) && instruction.hasOutValue()) {
       // Append will return the string builder instance. Before removing, ensure that
       // all users of the output values uses the receiver.
-      instruction.outValue().replaceUsers(instruction.getFirstOperand());
+      instruction.outValue().replaceUsers(instruction.getFirstOperand(), affectedValues);
     }
     iterator.removeOrReplaceByDebugLocalRead();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
index 2bc3346..af05e80 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.code.Value;
 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.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
@@ -108,17 +109,18 @@
     if (actions.isEmpty()) {
       return CodeRewriterResult.NO_CHANGE;
     }
+    AffectedValues affectedValues = new AffectedValues();
     InstructionListIterator it = code.instructionListIterator();
     while (it.hasNext()) {
       Instruction instruction = it.next();
       StringBuilderAction stringBuilderAction = actions.get(instruction);
       if (stringBuilderAction != null) {
-        stringBuilderAction.perform(appView, code, it, instruction, oracle);
+        stringBuilderAction.perform(appView, code, it, instruction, affectedValues, oracle);
       }
     }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     code.removeAllDeadAndTrivialPhis();
     code.removeRedundantBlocks();
-    assert code.isConsistentSSA(appView);
     return CodeRewriterResult.HAS_CHANGED;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 5972b0e9..202dc62 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
 import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.ConstNumber;
@@ -32,10 +31,9 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo;
-import com.google.common.collect.Sets;
 import java.io.UTFDataFormatException;
-import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
@@ -71,7 +69,7 @@
     if (!code.metadata().mayHaveConstString()) {
       return;
     }
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     InstructionListIterator it = code.instructionListIterator();
     while (it.hasNext()) {
       Instruction instr = it.next();
@@ -125,8 +123,8 @@
         Value stringValue =
             code.createValue(
                 TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-        affectedValues.addAll(invoke.outValue().affectedValues());
-        it.replaceCurrentInstruction(new ConstString(stringValue, factory.createString(sub)));
+        it.replaceCurrentInstruction(
+            new ConstString(stringValue, factory.createString(sub)), affectedValues);
         continue;
       }
 
@@ -140,8 +138,7 @@
         Value newOutValue =
             code.createValue(
                 TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-        affectedValues.addAll(invoke.outValue().affectedValues());
-        it.replaceCurrentInstruction(new ConstString(newOutValue, resultString));
+        it.replaceCurrentInstruction(new ConstString(newOutValue, resultString), affectedValues);
         continue;
       }
 
@@ -228,14 +225,13 @@
       it.replaceCurrentInstruction(constNumber);
     }
     // Computed substring is not null, and thus propagate that information.
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    assert code.isConsistentSSA(appView);
   }
 
   // Find Class#get*Name() with a constant-class and replace it with a const-string if possible.
   public void rewriteClassGetName(AppView<?> appView, IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     InstructionListIterator it = code.instructionListIterator();
     while (it.hasNext()) {
       Instruction instr = it.next();
@@ -313,9 +309,12 @@
       DexString name = null;
       if (invokedMethod == factory.classMethods.getName) {
         if (mayBeRenamed) {
+          Value stringValue =
+              code.createValue(
+                  TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
           deferred =
               new DexItemBasedConstString(
-                  invoke.outValue(), baseType, ClassNameComputationInfo.create(NAME, arrayDepth));
+                  stringValue, baseType, ClassNameComputationInfo.create(NAME, arrayDepth));
         } else {
           name = NAME.map(descriptor, holder, factory, arrayDepth);
         }
@@ -325,9 +324,8 @@
       } else if (invokedMethod == factory.classMethods.getCanonicalName) {
         // Always returns null if the target type is local or anonymous class.
         if (holder.isLocalClass() || holder.isAnonymousClass()) {
-          affectedValues.addAll(invoke.outValue().affectedValues());
           ConstNumber constNull = code.createConstNull();
-          it.replaceCurrentInstruction(constNull);
+          it.replaceCurrentInstruction(constNull, affectedValues);
         } else {
           // b/119471127: If an outer class is shrunk, we may compute a wrong canonical name.
           // Leave it as-is so that the class's canonical name is consistent across the app.
@@ -335,9 +333,13 @@
             continue;
           }
           if (mayBeRenamed) {
+            Value stringValue =
+                code.createValue(
+                    TypeElement.stringClassType(appView, definitelyNotNull()),
+                    invoke.getLocalInfo());
             deferred =
                 new DexItemBasedConstString(
-                    invoke.outValue(),
+                    stringValue,
                     baseType,
                     ClassNameComputationInfo.create(CANONICAL_NAME, arrayDepth));
           } else {
@@ -355,9 +357,13 @@
             continue;
           }
           if (mayBeRenamed) {
+            Value stringValue =
+                code.createValue(
+                    TypeElement.stringClassType(appView, definitelyNotNull()),
+                    invoke.getLocalInfo());
             deferred =
                 new DexItemBasedConstString(
-                    invoke.outValue(),
+                    stringValue,
                     baseType,
                     ClassNameComputationInfo.create(SIMPLE_NAME, arrayDepth));
           } else {
@@ -366,29 +372,26 @@
         }
       }
       if (name != null) {
-        affectedValues.addAll(invoke.outValue().affectedValues());
         Value stringValue =
             code.createValue(
                 TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
         ConstString constString = new ConstString(stringValue, name);
-        it.replaceCurrentInstruction(constString);
+        it.replaceCurrentInstruction(constString, affectedValues);
       } else if (deferred != null) {
-        affectedValues.addAll(invoke.outValue().affectedValues());
-        it.replaceCurrentInstruction(deferred);
+        it.replaceCurrentInstruction(deferred, affectedValues);
       }
     }
     // Computed name is not null or literally null (for canonical name of local/anonymous class).
     // In either way, that is narrower information, and thus propagate that.
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    assert code.isConsistentSSA(appView);
   }
 
   // String#valueOf(null) -> "null"
   // String#valueOf(String s) -> s
   // str.toString() -> str
   public void removeTrivialConversions(IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     InstructionListIterator it = code.instructionListIterator();
     while (it.hasNext()) {
       Instruction instr = it.next();
@@ -407,11 +410,7 @@
         TypeElement inType = in.getType();
         if (out != null && in.isAlwaysNull(appView)) {
           affectedValues.addAll(out.affectedValues());
-          Value nullStringValue =
-              code.createValue(
-                  TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-          ConstString nullString = new ConstString(nullStringValue, factory.createString("null"));
-          it.replaceCurrentInstruction(nullString);
+          it.replaceCurrentInstructionWithConstString(appView, code, factory.createString("null"));
         } else if (inType.nullability().isDefinitelyNotNull()
             && inType.isClassType()
             && inType.asClassType().getClassType().equals(factory.stringType)) {
@@ -445,9 +444,9 @@
       }
     }
     // Newly added "null" string is not null, and thus propagate that information.
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    code.removeRedundantBlocks();
+    assert code.isConsistentSSA(appView);
   }
 
   static class StringOptimizerEscapeAnalysisConfiguration
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 079b862..16a09d6 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -91,6 +91,7 @@
 import com.android.tools.r8.ir.code.Shr;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.StringSwitch;
 import com.android.tools.r8.ir.code.Sub;
 import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Ushr;
@@ -100,7 +101,9 @@
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.conversion.StringSwitchConverter;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
+import com.android.tools.r8.lightir.LirBuilder.StringSwitchPayload;
 import com.android.tools.r8.lightir.LirCode.PositionEntry;
 import com.android.tools.r8.lightir.LirCode.StructuredPositionEntry;
 import com.android.tools.r8.lightir.LirCode.TryCatchTable;
@@ -116,6 +119,7 @@
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.function.BiFunction;
 
 public class Lir2IRConverter {
 
@@ -144,7 +148,14 @@
     lirCode.forEach(view -> view.accept(parser));
     IRCode irCode = parser.getIRCode(method, conversionOptions);
     // Some instructions have bottom types (e.g., phis). Compute their actual types by widening.
-    new TypeAnalysis(appView).widening(irCode);
+    new TypeAnalysis(appView, irCode).widening();
+
+    if (conversionOptions.isStringSwitchConversionEnabled()) {
+      if (StringSwitchConverter.convertToStringSwitchInstructions(
+          irCode, appView.dexItemFactory())) {
+        irCode.removeRedundantBlocks();
+      }
+    }
     return irCode;
   }
 
@@ -168,6 +179,7 @@
     private BasicBlock currentBlock = null;
     private int nextInstructionIndex = 0;
 
+    private final Position entryPosition;
     private Position currentPosition;
     private PositionEntry nextPositionEntry = null;
     private int nextIndexInPositionsTable = 0;
@@ -226,6 +238,11 @@
                           .build()));
         }
       }
+      if (positionTable.length > 0 && positionTable[0].getFromInstructionIndex() == 0) {
+        entryPosition = positionTable[0].getPosition(originalMethod);
+      } else {
+        entryPosition = currentPosition;
+      }
     }
 
     @Override
@@ -360,7 +377,7 @@
       return new IRCode(
           appView.options(),
           method,
-          Position.syntheticNone(),
+          entryPosition,
           blockList,
           strategy.getValueNumberGenerator(),
           basicBlockNumberGenerator,
@@ -702,27 +719,47 @@
     public void onIntSwitch(EV value, IntSwitchPayload payload) {
       // keys is the 'value' -> 'target index' mapping.
       int[] keys = payload.keys;
+      addSwitchInstruction(
+          payload.targets,
+          (successors, fallthrough) ->
+              new IntSwitch(getValue(value), keys, successors, fallthrough));
+    }
+
+    @Override
+    public void onStringSwitch(EV value, StringSwitchPayload payload) {
+      int size = payload.keys.length;
+      DexString[] keys = new DexString[size];
+      for (int i = 0; i < size; i++) {
+        keys[i] = (DexString) code.getConstantItem(payload.keys[i]);
+      }
+      addSwitchInstruction(
+          payload.targets,
+          (successors, fallthrough) ->
+              new StringSwitch(getValue(value), keys, successors, fallthrough));
+    }
+
+    private void addSwitchInstruction(
+        int[] targets, BiFunction<int[], Integer, Instruction> createSwitchInstruction) {
+      int size = targets.length;
       // successorIndices is the 'target index' to 'IR successor index'.
-      int[] successorIndices = new int[keys.length];
-      List<BasicBlock> successorBlocks = new ArrayList<>();
-      {
-        // The mapping from instruction to successor is a temp mapping to track if any targets
-        // point to the same block.
-        Int2IntMap instructionToSuccessor = new Int2IntOpenHashMap();
-        for (int i = 0; i < successorIndices.length; i++) {
-          int instructionIndex = payload.targets[i];
-          if (instructionToSuccessor.containsKey(instructionIndex)) {
-            successorIndices[i] = instructionToSuccessor.get(instructionIndex);
-          } else {
-            int successorIndex = successorBlocks.size();
-            successorIndices[i] = successorIndex;
-            instructionToSuccessor.put(instructionIndex, successorIndex);
-            successorBlocks.add(getBasicBlock(instructionIndex));
-          }
+      int[] successorIndices = new int[size];
+      List<BasicBlock> successorBlocks = new ArrayList<>(size);
+      // The mapping from instruction to successor is a temp mapping to track if any targets
+      // point to the same block.
+      Int2IntMap instructionToSuccessor = new Int2IntOpenHashMap(size);
+      for (int i = 0; i < targets.length; i++) {
+        int instructionIndex = targets[i];
+        if (instructionToSuccessor.containsKey(instructionIndex)) {
+          successorIndices[i] = instructionToSuccessor.get(instructionIndex);
+        } else {
+          int successorIndex = successorBlocks.size();
+          successorIndices[i] = successorIndex;
+          instructionToSuccessor.put(instructionIndex, successorIndex);
+          successorBlocks.add(getBasicBlock(instructionIndex));
         }
       }
       int fallthrough = successorBlocks.size();
-      addInstruction(new IntSwitch(getValue(value), keys, successorIndices, fallthrough));
+      addInstruction(createSwitchInstruction.apply(successorIndices, fallthrough));
       // The call to addInstruction will ensure the current block so don't amend to it before here.
       // If the block has successors then the index mappings are not valid / need to be offset.
       assert currentBlock.getSuccessors().isEmpty();
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 68b5c0e..0cd52a4 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -132,6 +132,17 @@
     }
   }
 
+  public static class StringSwitchPayload extends InstructionPayload {
+    public final int[] keys;
+    public final int[] targets;
+
+    public StringSwitchPayload(int[] keys, int[] targets) {
+      assert keys.length == targets.length;
+      this.keys = keys;
+      this.targets = targets;
+    }
+  }
+
   public static class NameComputationPayload extends InstructionPayload {
     public final NameComputationInfo<?> nameComputationInfo;
 
@@ -666,6 +677,23 @@
         Collections.singletonList(value));
   }
 
+  public LirBuilder<V, EV> addStringSwitch(
+      V value, DexString[] keys, BasicBlock[] targetsBlocks, BasicBlock fallthroughBlock) {
+    int size = keys.length;
+    assert targetsBlocks.length == size;
+    int[] keyIndices = new int[size];
+    int[] targetsIndices = new int[size];
+    for (int i = 0; i < size; i++) {
+      keyIndices[i] = getConstantIndex(keys[i]);
+      targetsIndices[i] = getBlockIndex(targetsBlocks[i]);
+    }
+    StringSwitchPayload payload = new StringSwitchPayload(keyIndices, targetsIndices);
+    return addInstructionTemplate(
+        LirOpcodes.STRINGSWITCH,
+        Collections.singletonList(payload),
+        Collections.singletonList(value));
+  }
+
   public LirBuilder<V, EV> addIf(
       IfType ifKind, ValueType valueType, V value, BasicBlock trueTarget) {
     int opcode;
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index 0fccf4e..b14c481 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
 import com.google.common.collect.ImmutableMap;
@@ -39,6 +40,7 @@
 import java.util.Map;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 public class LirCode<EV> extends Code implements Iterable<LirInstructionView> {
 
@@ -90,7 +92,7 @@
   }
 
   public static class TryCatchTable {
-    final Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers;
+    private final Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers;
 
     public TryCatchTable(Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers) {
       assert !tryCatchHandlers.isEmpty();
@@ -101,6 +103,10 @@
     public CatchHandlers<Integer> getHandlersForBlock(int blockIndex) {
       return tryCatchHandlers.get(blockIndex);
     }
+
+    public void forEachHandler(BiConsumer<Integer, CatchHandlers<Integer>> fn) {
+      tryCatchHandlers.forEach(fn);
+    }
   }
 
   public static class DebugLocalInfoTable<EV> {
@@ -451,4 +457,23 @@
       positionConsumer.accept(entry.getPosition(method));
     }
   }
+
+  public LirCode<EV> newCodeWithRewrittenConstantPool(Function<DexItem, DexItem> rewriter) {
+    DexItem[] rewrittenConstants = ArrayUtils.map(constants, rewriter, new DexItem[0]);
+    if (constants == rewrittenConstants) {
+      return this;
+    }
+    return new LirCode<>(
+        irMetadata,
+        rewrittenConstants,
+        positionTable,
+        argumentCount,
+        instructions,
+        instructionCount,
+        tryCatchTable,
+        debugLocalInfoTable,
+        strategyInfo,
+        useDexEstimationStrategy,
+        metadataMap);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index fc1ccc6..9ac6b6c 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -211,6 +211,7 @@
   int CHECKCAST_SAFE = 224;
   int CHECKCAST_IGNORE_COMPAT = 225;
   int CONSTCLASS_IGNORE_COMPAT = 226;
+  int STRINGSWITCH = 227;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -548,6 +549,8 @@
         return "CHECKCAST_IGNORE_COMPAT";
       case CONSTCLASS_IGNORE_COMPAT:
         return "CONSTCLASS_IGNORE_COMPAT";
+      case STRINGSWITCH:
+        return "STRINGSWITCH";
 
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index d979ada..e3ca61d 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
 import com.android.tools.r8.lightir.LirBuilder.NameComputationPayload;
 import com.android.tools.r8.lightir.LirBuilder.RecordFieldValuesPayload;
+import com.android.tools.r8.lightir.LirBuilder.StringSwitchPayload;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import java.util.ArrayList;
 import java.util.List;
@@ -372,6 +373,10 @@
     onInstruction();
   }
 
+  public void onStringSwitch(EV value, StringSwitchPayload payload) {
+    onInstruction();
+  }
+
   public void onFallthrough() {
     onInstruction();
   }
@@ -1009,6 +1014,14 @@
           onIntSwitch(value, payload);
           return;
         }
+      case LirOpcodes.STRINGSWITCH:
+        {
+          StringSwitchPayload payload =
+              (StringSwitchPayload) getConstantItem(view.getNextConstantOperand());
+          EV value = getNextValueOperand(view);
+          onStringSwitch(value, payload);
+          return;
+        }
       case LirOpcodes.INVOKEDIRECT:
       case LirOpcodes.INVOKEDIRECT_ITF:
         {
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index cfe1a08..c3a8406 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
+import com.android.tools.r8.lightir.LirBuilder.StringSwitchPayload;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import com.android.tools.r8.utils.StringUtils;
 import java.util.Arrays;
@@ -90,8 +91,7 @@
     if (code.getTryCatchTable() != null) {
       builder.append("try-catch-handlers:\n");
       code.getTryCatchTable()
-          .tryCatchHandlers
-          .forEach(
+          .forEachHandler(
               (index, handlers) -> {
                 builder.append(index).append(":\n");
                 for (CatchHandler<Integer> handler : handlers) {
@@ -241,6 +241,12 @@
   }
 
   @Override
+  public void onStringSwitch(EV value, StringSwitchPayload payload) {
+    appendValueArguments(value);
+    // TODO(b/225838009): Consider printing the switch payload info.
+  }
+
+  @Override
   public void onFallthrough() {
     // Nothing to append.
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java b/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
index eaa93d0..0a37c4d 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
@@ -20,8 +20,10 @@
 import com.android.tools.r8.dex.code.DexFillArrayDataPayload;
 import com.android.tools.r8.dex.code.DexFilledNewArray;
 import com.android.tools.r8.dex.code.DexGoto;
+import com.android.tools.r8.dex.code.DexIfEq;
 import com.android.tools.r8.dex.code.DexInstanceOf;
 import com.android.tools.r8.dex.code.DexInvokeCustom;
+import com.android.tools.r8.dex.code.DexInvokeVirtual;
 import com.android.tools.r8.dex.code.DexMonitorEnter;
 import com.android.tools.r8.dex.code.DexMonitorExit;
 import com.android.tools.r8.dex.code.DexMove;
@@ -36,6 +38,7 @@
 import com.android.tools.r8.dex.code.DexThrow;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
+import com.android.tools.r8.lightir.LirBuilder.StringSwitchPayload;
 
 public class LirSizeEstimation<EV> extends LirParsedInstructionCallback<EV> implements LirOpcodes {
 
@@ -66,13 +69,29 @@
     sizeEstimate += instructionSize(view.getOpcode(), view);
   }
 
+  private static int baseSwitchSize(int cases) {
+    return DexPackedSwitch.SIZE + DexPackedSwitchPayload.SIZE + ((2 + 2) * cases);
+  }
+
   @Override
   public void onIntSwitch(EV unusedValue, IntSwitchPayload payload) {
+    sizeEstimate += baseSwitchSize(payload.keys.length);
+  }
+
+  @Override
+  public void onStringSwitch(EV value, StringSwitchPayload payload) {
+    // Invoke hashcode and switch on it.
+    sizeEstimate += DexInvokeVirtual.SIZE + baseSwitchSize(payload.keys.length);
+    // Each case has an additional const-string, invoke equals, if check, cont-number and goto.
     sizeEstimate +=
-        DexPackedSwitch.SIZE
-            + DexPackedSwitchPayload.SIZE
-            + (2 * payload.keys.length)
-            + (2 * payload.targets.length);
+        payload.keys.length
+            * (DexConstString.SIZE
+                + DexInvokeVirtual.SIZE
+                + DexIfEq.SIZE
+                + DexConst4.SIZE
+                + DexGoto.SIZE);
+    // Finally the id is then the subject of another switch.
+    sizeEstimate += baseSwitchSize(payload.keys.length);
   }
 
   @Override
@@ -83,6 +102,7 @@
   private int instructionSize(int opcode, LirInstructionView view) {
     switch (opcode) {
       case TABLESWITCH:
+      case STRINGSWITCH:
       case NEWARRAYFILLEDDATA:
         // The payload instructions use the "parsed callback" to compute the payloads.
         super.onInstructionView(view);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
index 11918fa..3b43c56 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
@@ -4,16 +4,20 @@
 
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.graph.UseRegistry.MethodHandleUse;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeInstructionMetadata;
+import com.android.tools.r8.ir.code.IfType;
+import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import java.util.List;
 
@@ -38,6 +42,71 @@
   }
 
   @Override
+  public void onInstruction() {
+    // TODO(b/225838009): Consider defining an abstract base class for the parsed exception so that
+    //  missing a callback is a compile-time error.
+    boolean debug = false;
+    if (debug) {
+      throw new Unimplemented();
+    }
+  }
+
+  @Override
+  public void onFallthrough() {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onConstString(DexString string) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onThrow(EV exception) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onMoveException(DexType exceptionType) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onReturnVoid() {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onReturn(EV value) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onGoto(int blockIndex) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onPhi(DexType type, List<EV> operands) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onConstNumber(NumericType type, long value) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onCmpInstruction(int opcode, EV leftValue, EV rightValue) {
+    // Nothing to register.
+  }
+
+  @Override
+  public void onIf(IfType ifKind, int blockIndex, EV valueIndex) {
+    // Nothing to register.
+  }
+
+  @Override
   public void onCheckCast(DexType type, EV value, boolean ignoreCompatRules) {
     registry.registerCheckCast(type, ignoreCompatRules);
   }
@@ -156,4 +225,9 @@
   public void onNewUnboxedEnumInstance(DexType type, int ordinal) {
     registry.registerNewUnboxedEnumInstance(type);
   }
+
+  @Override
+  public void onRecordFieldValues(DexField[] fields, List<EV> values) {
+    registry.registerRecordFieldValues(fields);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
index 04e7817..e4fc96d 100644
--- a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
+++ b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
@@ -858,8 +858,13 @@
               Comparator.comparing(mapping -> mapping.getOutline().toString()));
       IntBox firstAvailableRange = new IntBox(lastComposedRange.minifiedRange.to + 1);
       for (OutlineCallsiteMappingInformation outlineCallSite : outlineCallSites) {
-        for (ComputedMappedRangeForOutline computedMappedRangeForOutline :
-            outlineCallsitesToPatchUp.get(outlineCallSite)) {
+        // It should be sufficient to patch any call-site because they are referencing the same
+        // outline call-site information due to using an identity hashmap.
+        List<ComputedMappedRangeForOutline> computedMappedRangeForOutlines =
+            outlineCallsitesToPatchUp.get(outlineCallSite);
+        assert verifyAllOutlineCallSitesAreEqualTo(outlineCallSite, computedMappedRangeForOutlines);
+        ComputedMappedRangeForOutline computedMappedRangeForOutline =
+            ListUtils.first(computedMappedRangeForOutlines);
           Int2IntSortedMap newPositionMap =
               new Int2IntLinkedOpenHashMap(outlineCallSite.getPositions().size());
           visitOutlineMappedPositions(
@@ -892,7 +897,19 @@
                 outlineCallSite.setPositionsInternal(newPositionMap);
               });
         }
+    }
+
+    private boolean verifyAllOutlineCallSitesAreEqualTo(
+        OutlineCallsiteMappingInformation outlineCallSite,
+        List<ComputedMappedRangeForOutline> computedMappedRangeForOutlines) {
+      for (ComputedMappedRangeForOutline computedMappedRangeForOutline :
+          computedMappedRangeForOutlines) {
+        for (OutlineCallsiteMappingInformation outlineCallsiteMappingInformation :
+            computedMappedRangeForOutline.composed.getOutlineCallsiteInformation()) {
+          assert outlineCallSite == outlineCallsiteMappingInformation;
+        }
       }
+      return true;
     }
 
     /**
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
index 05fe545..7f17e72 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
@@ -92,6 +92,7 @@
 
   public void decoupleIdentifierNameStringsInMethod(IRCode code) {
     decoupleIdentifierNameStringsInBlocks(code, null);
+    assert code.isConsistentSSA(appView);
   }
 
   public void decoupleIdentifierNameStringsInBlocks(IRCode code, Set<BasicBlock> blocks) {
diff --git a/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
new file mode 100644
index 0000000..c51b13c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
@@ -0,0 +1,225 @@
+// Copyright (c) 2016, 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;
+
+import static com.android.tools.r8.dex.Constants.ACC_PRIVATE;
+import static com.android.tools.r8.dex.Constants.ACC_PROTECTED;
+import static com.android.tools.r8.dex.Constants.ACC_PUBLIC;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
+import com.android.tools.r8.ir.optimize.MethodPoolCollection;
+import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
+import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
+import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemover;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public final class LegacyAccessModifier {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final SubtypingInfo subtypingInfo;
+  private final MethodPoolCollection methodPoolCollection;
+
+  private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
+
+  private LegacyAccessModifier(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.subtypingInfo = appView.appInfo().computeSubtypingInfo();
+    this.methodPoolCollection =
+        // We will add private instance methods when we promote them.
+        new MethodPoolCollection(
+            appView, subtypingInfo, MethodPoolCollection::excludesPrivateInstanceMethod);
+  }
+
+  /**
+   * Marks all package private and protected methods and fields as public. Makes all private static
+   * methods public. Makes private instance methods public final instance methods, if possible.
+   *
+   * <p>This will destructively update the DexApplication passed in as argument.
+   */
+  public static void run(
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions();
+    if (accessModifierOptions.isAccessModificationEnabled()
+        && accessModifierOptions.isLegacyAccessModifierEnabled()) {
+      timing.begin("Access modification");
+      new LegacyAccessModifier(appView).internalRun(executorService, timing);
+      timing.end();
+      if (appView.graphLens().isPublicizerLens()) {
+        // We can now remove redundant bridges. Note that we do not need to update the
+        // invoke-targets here, as the existing invokes will simply dispatch to the now
+        // visible super-method. MemberRebinding, if run, will then dispatch it correctly.
+        new RedundantBridgeRemover(appView.withLiveness()).run(executorService, timing);
+      }
+    }
+  }
+
+  private void internalRun(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    // Phase 1: Collect methods to check if private instance methods don't have conflicts.
+    methodPoolCollection.buildAll(executorService, timing);
+
+    // Phase 2: Visit classes and promote class/member to public if possible.
+    timing.begin("Phase 2: promoteToPublic");
+    appView.appInfo().forEachReachableInterface(clazz -> processType(clazz.getType()));
+    processType(appView.dexItemFactory().objectType);
+    timing.end();
+
+    PublicizerLens publicizerLens = lensBuilder.build(appView);
+    if (publicizerLens != null) {
+      appView.setGraphLens(publicizerLens);
+    }
+
+    appView.notifyOptimizationFinishedForTesting();
+  }
+
+  private void doPublicize(ProgramDefinition definition) {
+    definition.getAccessFlags().promoteToPublic();
+  }
+
+  private void processType(DexType type) {
+    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+    if (clazz != null) {
+      processClass(clazz);
+    }
+    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::processType);
+  }
+
+  private void processClass(DexProgramClass clazz) {
+    if (appView.appInfo().isAccessModificationAllowed(clazz)) {
+      doPublicize(clazz);
+    }
+
+    // Publicize fields.
+    clazz.forEachProgramField(this::processField);
+
+    // Publicize methods.
+    Set<DexEncodedMethod> privateInstanceMethods = new LinkedHashSet<>();
+    clazz.forEachProgramMethod(
+        method -> {
+          if (publicizeMethod(method)) {
+            privateInstanceMethods.add(method.getDefinition());
+          }
+        });
+    if (!privateInstanceMethods.isEmpty()) {
+      clazz.virtualizeMethods(privateInstanceMethods);
+    }
+
+    // Publicize inner class attribute.
+    InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass();
+    if (attr != null) {
+      int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED;
+      clazz.replaceInnerClassAttributeForThisClass(
+          new InnerClassAttribute(
+              accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName()));
+    }
+  }
+
+  private void processField(ProgramField field) {
+    if (appView.appInfo().isAccessModificationAllowed(field)) {
+      publicizeField(field);
+    }
+  }
+
+  private void publicizeField(ProgramField field) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    if (!flags.isPublic()) {
+      flags.promoteToPublic();
+    }
+  }
+
+  private boolean publicizeMethod(ProgramMethod method) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    if (accessFlags.isPublic()) {
+      return false;
+    }
+    // If this method is mentioned in keep rules, do not transform (rule applications changed).
+    DexEncodedMethod definition = method.getDefinition();
+    if (!appView.appInfo().isAccessModificationAllowed(method)) {
+      // TODO(b/131130038): Also do not publicize package-private and protected methods that are
+      //  kept.
+      if (definition.isPrivate()) {
+        return false;
+      }
+    }
+
+    if (method.getDefinition().isInstanceInitializer() || accessFlags.isProtected()) {
+      doPublicize(method);
+      return false;
+    }
+
+    if (accessFlags.isPackagePrivate()) {
+      // If we publicize a package private method we have to ensure there is no overrides of it. We
+      // could potentially publicize a method if it only has package-private overrides.
+      // TODO(b/182136236): See if we can break the hierarchy for clusters.
+      MemberPool<DexMethod> memberPool = methodPoolCollection.get(method.getHolder());
+      Wrapper<DexMethod> methodKey = MethodSignatureEquivalence.get().wrap(method.getReference());
+      if (memberPool.below(
+          methodKey,
+          false,
+          true,
+          (clazz, ignored) ->
+              !method.getContextType().getPackageName().equals(clazz.getType().getPackageName()))) {
+        return false;
+      }
+      doPublicize(method);
+      return false;
+    }
+
+    assert accessFlags.isPrivate();
+
+    if (accessFlags.isStatic()) {
+      // For private static methods we can just relax the access to public, since
+      // even though JLS prevents from declaring static method in derived class if
+      // an instance method with same signature exists in superclass, JVM actually
+      // does not take into account access of the static methods.
+      doPublicize(method);
+      return false;
+    }
+
+    // We can't publicize private instance methods in interfaces or methods that are copied from
+    // interfaces to lambda-desugared classes because this will be added as a new default method.
+    // TODO(b/111118390): It might be possible to transform it into static methods, though.
+    if (method.getHolder().isInterface() || accessFlags.isSynthetic()) {
+      return false;
+    }
+
+    boolean wasSeen = methodPoolCollection.markIfNotSeen(method.getHolder(), method.getReference());
+    if (wasSeen) {
+      // We can't do anything further because even renaming is not allowed due to the keep rule.
+      if (!appView.appInfo().isMinificationAllowed(method)) {
+        return false;
+      }
+      // TODO(b/111118390): Renaming will enable more private instance methods to be publicized.
+      return false;
+    }
+    lensBuilder.add(method.getReference());
+    accessFlags.promoteToFinal();
+    doPublicize(method);
+    // The method just became public and is therefore not a library override.
+    definition.setLibraryMethodOverride(OptionalBool.FALSE);
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
index 6304490..b388d09 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
@@ -61,7 +61,9 @@
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
     timing.begin("Access modification");
-    if (appView.options().getAccessModifierOptions().isAccessModificationEnabled()) {
+    AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions();
+    if (accessModifierOptions.isAccessModificationEnabled()
+        && !accessModifierOptions.isLegacyAccessModifierEnabled()) {
       new AccessModifier(appView)
           .processStronglyConnectedComponents(executorService)
           .installLens(executorService, timing);
@@ -165,23 +167,35 @@
       return commitMethod(method, localNamingState, namingState);
     }
 
+    if (method.getDefinition().isClassInitializer()) {
+      return commitMethod(method, localNamingState, namingState);
+    }
+
     if (method.getDefinition().isInstanceInitializer()
         || (accessFlags.isPackagePrivate()
             && !traversalState.hasIllegalOverrideOfPackagePrivateMethod(method))
         || accessFlags.isProtected()) {
-      method.getAccessFlags().promoteToPublic();
+      accessFlags.promoteToPublic();
       return commitMethod(method, localNamingState, namingState);
     }
 
     if (accessFlags.isPrivate()) {
       if (isRenamingAllowed(method)) {
-        method.getAccessFlags().promoteToPublic();
+        accessFlags
+            .promoteToPublic()
+            .applyIf(
+                !method.getHolder().isInterface() && accessFlags.belongsToVirtualPool(),
+                MethodAccessFlags::promoteToFinal);
         return commitMethod(method, localNamingState, namingState);
       }
       assert localNamingState.containsKey(method.getReference());
       assert localNamingState.get(method.getReference()) == method.getReference();
       if (namingState.isFree(method.getMethodSignature())) {
-        method.getAccessFlags().promoteToPublic();
+        accessFlags
+            .promoteToPublic()
+            .applyIf(
+                !method.getHolder().isInterface() && accessFlags.belongsToVirtualPool(),
+                MethodAccessFlags::promoteToFinal);
         namingState.addBlockedMethodSignature(method.getMethodSignature());
       }
       return commitMethod(method, method.getReference());
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
index 99a0645..81c1f03 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
@@ -5,9 +5,14 @@
 package com.android.tools.r8.optimize.accessmodification;
 
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.SystemPropertyUtils;
 
 public class AccessModifierOptions {
 
+  private boolean enableLegacyAccessModifier =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.accessmodification.legacy", false);
+
   // TODO(b/131130038): Do not allow accessmodification when kept.
   private boolean forceModifyPackagePrivateAndProtectedMethods = true;
 
@@ -22,14 +27,16 @@
   }
 
   public boolean isAccessModificationEnabled() {
-    // TODO(b/288062771): Enable access modification for L8.
-    if (!options.synthesizedClassPrefix.isEmpty()) {
+    if (isAccessModificationRulePresent()) {
+      return true;
+    }
+    if (isLegacyAccessModifierEnabled()) {
       return false;
     }
-    if (options.forceProguardCompatibility) {
-      return isAccessModificationRulePresent();
-    }
-    return true;
+    // TODO(b/288062771): Enable access modification by default for L8.
+    return options.synthesizedClassPrefix.isEmpty()
+        && !options.forceProguardCompatibility
+        && options.isOptimizing();
   }
 
   private boolean isAccessModificationRulePresent() {
@@ -46,4 +53,8 @@
     this.forceModifyPackagePrivateAndProtectedMethods =
         forceModifyPackagePrivateAndProtectedMethods;
   }
+
+  public boolean isLegacyAccessModifierEnabled() {
+    return enableLegacyAccessModifier;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java
index c76e842..91ec959 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java
@@ -7,7 +7,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.SingleValue;
 import com.android.tools.r8.ir.code.Argument;
@@ -17,12 +18,11 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Set;
 
 public class ArgumentPropagatorIROptimizer {
 
@@ -39,7 +39,7 @@
       AppView<AppInfoWithLiveness> appView,
       IRCode code,
       ConcreteCallSiteOptimizationInfo optimizationInfo) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     List<Assume> assumeInstructions = new LinkedList<>();
     List<Instruction> instructionsToAdd = new LinkedList<>();
     InstructionListIterator iterator = code.entryBlock().listIterator(code);
@@ -66,8 +66,7 @@
           Instruction replacement =
               singleValue.createMaterializingInstruction(appView, code, argument);
           replacement.setPosition(argument.getPosition());
-          affectedValues.addAll(argumentValue.affectedValues());
-          argumentValue.replaceUsers(replacement.outValue());
+          argumentValue.replaceUsers(replacement.outValue(), affectedValues);
           instructionsToAdd.add(replacement);
           continue;
         }
@@ -89,8 +88,7 @@
         if (dynamicType.getNullability().isDefinitelyNull()) {
           ConstNumber nullInstruction = code.createConstNull();
           nullInstruction.setPosition(argument.getPosition());
-          affectedValues.addAll(argumentValue.affectedValues());
-          argumentValue.replaceUsers(nullInstruction.outValue());
+          argumentValue.replaceUsers(nullInstruction.outValue(), affectedValues);
           instructionsToAdd.add(nullInstruction);
           continue;
         }
@@ -100,8 +98,13 @@
                 code.createValue(argumentValue.getType().asReferenceType().asMeetWithNotNull());
             argumentValue.replaceUsers(nonNullValue, affectedValues);
             Assume assumeNotNull =
-                Assume.createAssumeNonNullInstruction(
-                    nonNullValue, argumentValue, argument, appView);
+                Assume.create(
+                    DynamicType.definitelyNotNull(),
+                    nonNullValue,
+                    argumentValue,
+                    argument,
+                    appView,
+                    code.context());
             assumeNotNull.setPosition(argument.getPosition());
             assumeInstructions.add(assumeNotNull);
           }
@@ -109,33 +112,44 @@
         }
         DynamicTypeWithUpperBound dynamicTypeWithUpperBound =
             dynamicType.asDynamicTypeWithUpperBound();
-        Value specializedArg;
         if (dynamicTypeWithUpperBound.strictlyLessThan(argumentValue.getType(), appView)) {
-          specializedArg = code.createValue(argumentValue.getType());
-          affectedValues.addAll(argumentValue.affectedValues());
-          argumentValue.replaceUsers(specializedArg);
+          TypeElement specializedArgumentType =
+              argumentValue
+                  .getType()
+                  .asReferenceType()
+                  .getOrCreateVariant(
+                      dynamicType.getNullability().isDefinitelyNotNull()
+                          ? Nullability.definitelyNotNull()
+                          : argumentValue.getType().nullability());
+          Value specializedArg = code.createValue(specializedArgumentType);
+          argumentValue.replaceUsers(specializedArg, affectedValues);
           Assume assumeType =
-              Assume.createAssumeDynamicTypeInstruction(
-                  dynamicTypeWithUpperBound, specializedArg, argumentValue, argument, appView);
+              Assume.create(
+                  dynamicTypeWithUpperBound,
+                  specializedArg,
+                  argumentValue,
+                  argument,
+                  appView,
+                  code.context());
           assumeType.setPosition(argument.getPosition());
           assumeInstructions.add(assumeType);
-        } else {
-          specializedArg = argumentValue;
+          continue;
         }
-        assert specializedArg != null && specializedArg.getType().isReferenceType();
-        if (dynamicType.getNullability().isDefinitelyNotNull()) {
-          // If we already knew `arg` is never null, e.g., receiver, skip adding non-null.
-          if (!specializedArg.getType().isDefinitelyNotNull()) {
-            Value nonNullArg =
-                code.createValue(specializedArg.getType().asReferenceType().asMeetWithNotNull());
-            affectedValues.addAll(specializedArg.affectedValues());
-            specializedArg.replaceUsers(nonNullArg);
-            Assume assumeNotNull =
-                Assume.createAssumeNonNullInstruction(
-                    nonNullArg, specializedArg, argument, appView);
-            assumeNotNull.setPosition(argument.getPosition());
-            assumeInstructions.add(assumeNotNull);
-          }
+        if (dynamicType.getNullability().isDefinitelyNotNull()
+            && argumentValue.getType().isNullable()) {
+          Value nonNullArg =
+              code.createValue(argumentValue.getType().asReferenceType().asMeetWithNotNull());
+          argumentValue.replaceUsers(nonNullArg, affectedValues);
+          Assume assumeNotNull =
+              Assume.create(
+                  DynamicType.definitelyNotNull(),
+                  nonNullArg,
+                  argumentValue,
+                  argument,
+                  appView,
+                  code.context());
+          assumeNotNull.setPosition(argument.getPosition());
+          assumeInstructions.add(assumeNotNull);
         }
       }
     }
@@ -145,12 +159,9 @@
     iterator.previous();
     assert iterator.peekPrevious().isArgument();
     assumeInstructions.forEach(iterator::add);
-
-    // TODO(b/190154391): Can update method signature and save more on call sites.
     instructionsToAdd.forEach(iterator::add);
 
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    assert code.isConsistentSSA(appView);
   }
 }
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 ae0133f..4a737f4 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
@@ -29,6 +29,7 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.ir.optimize.info.bridge.VirtualBridgeInfo;
+import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.Timing;
@@ -326,6 +327,9 @@
 
   private Code createCodeForVirtualBridge(ProgramMethod representative, DexMethod methodToInvoke) {
     Code code = representative.getDefinition().getCode();
+    if (code.isLirCode()) {
+      return createLirCodeForVirtualBridge(code.asLirCode(), methodToInvoke);
+    }
     if (code.isCfCode()) {
       return createCfCodeForVirtualBridge(code.asCfCode(), methodToInvoke);
     }
@@ -335,6 +339,18 @@
     throw new Unreachable("Unexpected code object of type " + code.getClass().getTypeName());
   }
 
+  private LirCode<Integer> createLirCodeForVirtualBridge(
+      LirCode<Integer> code, DexMethod methodToInvoke) {
+    return code.newCodeWithRewrittenConstantPool(
+        item -> {
+          if (item instanceof DexMethod) {
+           assert methodToInvoke.match((DexMethod) item);
+           return methodToInvoke;
+          }
+          return item;
+        });
+  }
+
   private CfCode createCfCodeForVirtualBridge(CfCode code, DexMethod methodToInvoke) {
     List<CfInstruction> newInstructions = new ArrayList<>();
     boolean modified = false;
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 5b459c3..8f6b03f 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -497,18 +497,21 @@
   @Override
   public void notifyHorizontalClassMergerFinished(
       HorizontalClassMerger.Mode horizontalClassMergerMode) {
-    if (horizontalClassMergerMode.isInitial()) {
-      methodAccessInfoCollection.destroy();
+    if (horizontalClassMergerMode.isInitial()
+        && !options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
+      getMethodAccessInfoCollection().destroy();
     }
   }
 
   public void notifyMemberRebindingFinished(AppView<AppInfoWithLiveness> appView) {
     getFieldAccessInfoCollection().restrictToProgram(appView);
-    getMethodAccessInfoCollection().destroyNonDirectNonSuperInvokes();
+    if (!options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
+      getMethodAccessInfoCollection().destroyNonDirectNonSuperInvokes();
+    }
   }
 
   public void notifyRedundantBridgeRemoverFinished(boolean initial) {
-    if (initial) {
+    if (initial && !options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
       getMethodAccessInfoCollection().destroySuperInvokes();
     }
   }
@@ -516,7 +519,9 @@
   @Override
   public void notifyMinifierFinished() {
     liveMethods = ThrowingSet.get();
-    methodAccessInfoCollection.destroy();
+    if (!options().getAccessModifierOptions().isLegacyAccessModifierEnabled()) {
+      getMethodAccessInfoCollection().destroy();
+    }
   }
 
   public void notifyTreePrunerFinished(Enqueuer.Mode mode) {
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
index adbf8d0..22d9468 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
@@ -22,8 +22,6 @@
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRFinalizer;
-import com.android.tools.r8.ir.conversion.IRToCfFinalizer;
-import com.android.tools.r8.ir.conversion.IRToDexFinalizer;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.passes.ThrowCatchOptimizer;
@@ -266,7 +264,7 @@
     MutableMethodConversionOptions conversionOptions =
         mode.isInitialTreeShaking()
             ? MethodConversionOptions.forPreLirPhase(appView)
-            : MethodConversionOptions.forPostLirPhase(appView);
+            : MethodConversionOptions.forLirPhase(appView);
     conversionOptions.disableStringSwitchConversion();
 
     IRCode ir = method.buildIR(appView, conversionOptions);
@@ -278,11 +276,9 @@
     new ThrowCatchOptimizer(appView).run(ir, Timing.empty());
     rewriter.getDeadCodeRemover().run(ir, Timing.empty());
 
-    // Finalize to class files or dex.
+    // Finalize out of IR.
     IRFinalizer<?> finalizer =
-        conversionOptions.isGeneratingClassFiles()
-            ? new IRToCfFinalizer(appView, rewriter.getDeadCodeRemover())
-            : new IRToDexFinalizer(appView, rewriter.getDeadCodeRemover());
+        conversionOptions.getFinalizer(rewriter.getDeadCodeRemover(), appView);
     Code newCode = finalizer.finalizeCode(ir, BytecodeMetadataProvider.empty(), Timing.empty());
     method.setCode(newCode, appView);
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
index 7d25797..b0b905a 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingRewriter.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
@@ -31,10 +30,10 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.Sets;
 import java.util.Map;
 import java.util.Set;
 
@@ -67,7 +66,7 @@
     ProgramMethod context = code.context();
 
     // Rewrite field instructions that reference a pruned field.
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    AffectedValues affectedValues = new AffectedValues();
     BasicBlockIterator blockIterator = code.listIterator();
     boolean hasChanged = false;
     while (blockIterator.hasNext()) {
@@ -115,9 +114,7 @@
         }
       }
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     if (hasChanged) {
       code.removeRedundantBlocks();
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 4ad6ef2..380efdb 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -1683,6 +1683,8 @@
         clazz.getMethodCollection().replaceMethods(this::fixupMethod);
         clazz.setStaticFields(fixupFields(clazz.staticFields()));
         clazz.setInstanceFields(fixupFields(clazz.instanceFields()));
+        clazz.setPermittedSubclassAttributes(
+            fixupPermittedSubclassAttribute(clazz.getPermittedSubclassAttributes()));
       }
       for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
         synthesizedBridge.updateMethodSignatures(this::fixupMethodReference);
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 d49b405..41c295f 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -57,6 +57,7 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
@@ -65,6 +66,7 @@
 import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.desugar.TypeRewriter;
 import com.android.tools.r8.ir.desugar.TypeRewriter.MachineTypeRewriter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
@@ -73,6 +75,9 @@
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
+import com.android.tools.r8.lightir.IR2LirConverter;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirStrategy;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.MapVersion;
@@ -119,6 +124,8 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
@@ -2104,6 +2111,7 @@
 
     public boolean roundtripThroughLir = false;
     private boolean useLir = System.getProperty("com.android.tools.r8.nolir") == null;
+    private boolean convertLir = System.getProperty("com.android.tools.r8.convertlir") == null;
 
     public void enableLir() {
       useLir = true;
@@ -2127,9 +2135,42 @@
 
     private LirPhase currentPhase = LirPhase.PRE;
 
-    public void enterLirSupportedPhase() {
+    public void enterLirSupportedPhase(AppView<?> appView, ExecutorService executorService)
+        throws ExecutionException {
       assert isPreLirPhase();
       currentPhase = LirPhase.SUPPORTED;
+      if (!canUseLir(appView) || !convertLir) {
+        return;
+      }
+      // Convert code objects to LIR.
+      ThreadUtils.processItems(
+          appView.appInfo().classes(),
+          clazz -> {
+            // TODO(b/225838009): Also convert instance initializers to LIR, by adding support for
+            //  computing the inlining constraint for LIR and using that in the class mergers, and
+            //  class initializers, by updating the concatenation of clinits in horizontal class
+            //  merging.
+            clazz.forEachProgramMethodMatching(
+                method ->
+                    method.hasCode()
+                        && !method.isInitializer()
+                        && !appView.isCfByteCodePassThrough(method),
+                method -> {
+                  IRCode code =
+                      method.buildIR(appView, MethodConversionOptions.forLirPhase(appView));
+                  LirCode<Integer> lirCode =
+                      IR2LirConverter.translate(
+                          code,
+                          BytecodeMetadataProvider.empty(),
+                          LirStrategy.getDefaultStrategy().getEncodingStrategy(),
+                          appView.options());
+                  method.setCode(lirCode, appView);
+                });
+          },
+          executorService);
+      // Conversion to LIR via IR will allocate type elements.
+      // They are not needed after construction so remove them again.
+      appView.dexItemFactory().clearTypeElementsCache();
     }
 
     public void exitLirSupportedPhase() {
@@ -2137,6 +2178,11 @@
       currentPhase = LirPhase.POST;
     }
 
+    public void skipLirPhasesForTestingFinalOutput() {
+      assert currentPhase == LirPhase.PRE;
+      currentPhase = LirPhase.POST;
+    }
+
     public boolean isPreLirPhase() {
       return currentPhase == LirPhase.PRE;
     }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 447399f..8ffd594 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -118,6 +118,7 @@
   public static final String GENERATED_TEST_BUILD_DIR = BUILD_DIR + "generated/test/";
   public static final String LIBS_DIR = BUILD_DIR + "libs/";
   public static final String THIRD_PARTY_DIR = getProjectRoot() + "third_party/";
+  public static final String DEPENDENCIES = THIRD_PARTY_DIR + "dependencies/";
   public static final String TOOLS_DIR = getProjectRoot() + "tools/";
   public static final String TESTS_DIR = getProjectRoot() + "src/test/";
   public static final String TESTS_SOURCE_DIR = TESTS_DIR + "java/";
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java
index a27783e..f694cd7 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/ExceptionTablesTest.java
@@ -14,8 +14,10 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.android.tools.r8.utils.codeinspector.TypeSubject;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
-import java.util.stream.IntStream;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -42,6 +44,7 @@
         .addKeepMainRule(TestClass.class)
         .addVerticallyMergedClassesInspector(this::inspectVerticallyMergedClasses)
         .setMinApi(parameters)
+        .addOptionsModification(o -> o.testing.enableLir())
         .compile()
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
@@ -59,15 +62,15 @@
     assertThat(inspector.clazz(ExceptionA.class), not(isPresent()));
     assertThat(inspector.clazz(Exception1.class), not(isPresent()));
 
-    // Check that the second catch handler has been removed.
+    // Check that only two exception guard types remain.
     MethodSubject mainMethodSubject = inspector.clazz(TestClass.class).mainMethod();
     assertThat(mainMethodSubject, isPresent());
-    assertEquals(
-        2,
+    Set<TypeSubject> guards =
         mainMethodSubject
             .streamTryCatches()
-            .flatMapToInt(x -> IntStream.of(x.getNumberOfHandlers()))
-            .sum());
+            .flatMap(tryCatch -> tryCatch.guards().stream())
+            .collect(Collectors.toSet());
+    assertEquals(2, guards.size());
   }
 
   public static class TestClass {
@@ -89,11 +92,15 @@
     }
 
     private static void doSomethingThatMightThrowExceptionB() throws ExceptionB {
-      throw new ExceptionB("Ouch!");
+      if (System.nanoTime() > 0) {
+        throw new ExceptionB("Ouch!");
+      }
     }
 
     private static void doSomethingThatMightThrowException2() throws Exception2 {
-      throw new Exception2("Ouch!");
+      if (System.nanoTime() > 0) {
+        throw new Exception2("Ouch!");
+      }
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11AtomicTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11AtomicTests.java
index ddeddc0..3ac00b5 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11AtomicTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11AtomicTests.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getPathsFiles;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getTestNGMainRunner;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGSupportProgramFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8SHRINK;
@@ -27,7 +28,6 @@
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Collections;
 import java.util.List;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -79,8 +79,7 @@
   public static void compileAtomicClasses() throws Exception {
     ATOMIC_COMPILED_TESTS_FOLDER = getStaticTemp().newFolder("atomic").toPath();
     javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
-        .addClasspathFiles(
-            Collections.singletonList(Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar")))
+        .addClasspathFiles(testNGPath())
         .addSourceFiles(ATOMIC_TESTS_FILES)
         .setOutputPath(ATOMIC_COMPILED_TESTS_FOLDER)
         .compile();
@@ -93,6 +92,7 @@
         .addProgramFiles(
             ATOMIC_COMPILED_TESTS_FOLDER.resolve(ATOMIC_REFERENCE_TEST + CLASS_EXTENSION))
         .addProgramFiles(testNGSupportProgramFiles())
+        .addProgramClassFileData(getTestNGMainRunner())
         .applyIf(
             !libraryDesugaringSpecification.hasNioFileDesugaring(parameters),
             b -> b.addProgramFiles(getPathsFiles()))
@@ -110,6 +110,7 @@
         .addProgramFiles(
             getAllFilesWithSuffixInDirectory(ATOMIC_COMPILED_TESTS_FOLDER, CLASS_EXTENSION))
         .addProgramFiles(testNGSupportProgramFiles())
+        .addProgramClassFileData(getTestNGMainRunner())
         .applyIf(
             !libraryDesugaringSpecification.hasNioFileDesugaring(parameters),
             b -> b.addProgramFiles(getPathsFiles()))
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java
index d8bc956..87478e0 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentLinkedQueueTests.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getTestNGMainRunner;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGSupportProgramFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8SHRINK;
@@ -40,7 +41,6 @@
 import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Collections;
 import java.util.List;
 import org.junit.BeforeClass;
 import org.junit.Ignore;
@@ -92,8 +92,7 @@
     Path jdk11ConcurrentLinkedQueueTestsDir =
         getStaticTemp().newFolder("ConcurrentLinkedQueue").toPath();
     javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
-        .addClasspathFiles(
-            Collections.singletonList(Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar")))
+        .addClasspathFiles(testNGPath())
         .addSourceFiles(JDK_11_CONCURRENT_LINKED_QUEUE_JAVA_FILES)
         .setOutputPath(jdk11ConcurrentLinkedQueueTestsDir)
         .compile();
@@ -182,6 +181,7 @@
                 parameters, libraryDesugaringSpecification, compilationSpecification)
             .addProgramFiles(JDK_11_CONCURRENT_LINKED_QUEUE_TEST_CLASS_FILES)
             .addProgramFiles(testNGSupportProgramFiles())
+            .addProgramClassFileData(getTestNGMainRunner())
             // The WhiteBox test is using VarHandle and MethodHandles.privateLookupIn to inspect the
             // internal state of the implementation, so desugaring is needed for the program here.
             .addOptionsModification(options -> options.enableVarHandleDesugaring = true)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentMapTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentMapTests.java
index f4b8b17..c27bb8b 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentMapTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11ConcurrentMapTests.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getPathsFiles;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getTestNGMainRunner;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGSupportProgramFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8SHRINK;
@@ -82,8 +83,7 @@
   public static void compileConcurrentClasses() throws Exception {
     Path concurrentCompiledTestsFolder = getStaticTemp().newFolder("concurrentmap").toPath();
     javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
-        .addClasspathFiles(
-            Collections.singletonList(Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar")))
+        .addClasspathFiles(testNGPath())
         .addSourceFiles(getAllFilesWithSuffixInDirectory(CONCURRENT_TESTS_FOLDER, JAVA_EXTENSION))
         .setOutputPath(concurrentCompiledTestsFolder)
         .compile();
@@ -100,8 +100,7 @@
     Path concurrentHashCompiledTestsFolder =
         getStaticTemp().newFolder("concurrenthashmap").toPath();
     javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
-        .addClasspathFiles(
-            Collections.singletonList(Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar")))
+        .addClasspathFiles(testNGPath())
         .addSourceFiles(classesToCompile)
         .setOutputPath(concurrentHashCompiledTestsFolder)
         .compile();
@@ -118,6 +117,7 @@
     testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
         .addProgramFiles(CONCURRENT_COMPILED_TESTS_FILES)
         .addProgramFiles(testNGSupportProgramFiles())
+        .addProgramClassFileData(getTestNGMainRunner())
         .applyIf(
             !libraryDesugaringSpecification.hasNioFileDesugaring(parameters),
             b -> b.addProgramFiles(getPathsFiles()))
@@ -168,6 +168,7 @@
                 parameters, libraryDesugaringSpecification, compilationSpecification)
             .addProgramFiles(concurrentHashTestToCompile())
             .addProgramFiles(testNGSupportProgramFiles())
+            .addProgramClassFileData(getTestNGMainRunner())
             .applyIf(
                 !libraryDesugaringSpecification.hasNioFileDesugaring(parameters),
                 b -> b.addProgramFiles(getPathsFiles()))
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
index 0f735fe..ed6b91a 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getTestNGMainRunner;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGSupportProgramFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11TestLibraryDesugaringSpecification.EXTENSION_PATH;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11TestLibraryDesugaringSpecification.JDK11_PATH_JAVA_BASE_EXT;
@@ -262,7 +263,7 @@
     Path tmpDirectory = getStaticTemp().newFolder(name).toPath();
     List<Path> classpath = new ArrayList<>();
     classpath.add(EXTENSION_PATH);
-    classpath.add(Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar"));
+    classpath.add(testNGPath());
     if (cp != null) {
       classpath.add(cp);
     }
@@ -284,6 +285,7 @@
             .addProgramFiles(TEST_UTIL_JAR)
             .addProgramClassFileData(TEST_PROGRAM_CLASS_DATA)
             .addProgramFiles(testNGSupportProgramFiles())
+            .addProgramClassFileData(getTestNGMainRunner())
             .compile()
             .withArt6Plus64BitsLib();
     int success = 0;
@@ -341,6 +343,7 @@
             .addProgramFiles(TEST_UTIL_JAR)
             .addProgramClassFileData(TEST_PROGRAM_CLASS_DATA)
             .addProgramFiles(testNGSupportProgramFiles())
+            .addProgramClassFileData(getTestNGMainRunner())
             .addLibraryFiles(libraryDesugaringSpecification.getLibraryFiles())
             .compile()
             .withArt6Plus64BitsLib();
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java
index 9ff2346..78b8776 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java
@@ -4,9 +4,10 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getPathsFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getSafeVarArgsFile;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getTestNGMainRunner;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGSupportProgramFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11TestLibraryDesugaringSpecification.EXTENSION_PATH;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11TestLibraryDesugaringSpecification.JDK11_PATH_JAVA_BASE_EXT;
@@ -256,8 +257,7 @@
             "java.base=" + EXTENSION_PATH);
     javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
         .addOptions(options)
-        .addClasspathFiles(
-            ImmutableList.of(EXTENSION_PATH, Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar")))
+        .addClasspathFiles(testNGPath())
         .addSourceFiles(getJdk11StreamTestFiles())
         .setOutputPath(JDK_11_STREAM_TEST_CLASSES_DIR)
         .compile();
@@ -297,6 +297,7 @@
             b -> b.addProgramFiles(getPathsFiles()))
         .addProgramFiles(getSafeVarArgsFile())
         .addProgramFiles(testNGSupportProgramFiles())
+        .addProgramClassFileData(getTestNGMainRunner())
         .addOptionsModification(opt -> opt.testing.trackDesugaredAPIConversions = true)
         .disableL8AnnotationRemoval()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11SupportFiles.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11SupportFiles.java
index d3507a2..6d99f62 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11SupportFiles.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11SupportFiles.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
+import static com.android.tools.r8.TestBase.descriptor;
+import static com.android.tools.r8.TestBase.transformer;
 
 import com.android.tools.r8.ToolHelper;
 import java.nio.file.Path;
@@ -16,10 +17,11 @@
 
 public class Jdk11SupportFiles {
 
+  // TODO(b/289346278): See if we can remove the xxx subfolder.
   private static final Path ANDROID_PATHS_FILES_DIR =
-      Paths.get("third_party/android_jar/lib-v26/xxx/");
+      Paths.get(ToolHelper.THIRD_PARTY_DIR + "android_jar/lib-v26/xxx/");
   private static final Path ANDROID_SAFE_VAR_ARGS_LOCATION =
-      Paths.get("third_party/android_jar/lib-v26/java/lang/SafeVarargs.class");
+      Paths.get(ToolHelper.THIRD_PARTY_DIR + "android_jar/lib-v26/java/lang/SafeVarargs.class");
   private static final Path[] ANDROID_PATHS_FILES =
       new Path[] {
         Paths.get("java/nio/file/Files.class"),
@@ -40,10 +42,79 @@
   }
 
   public static Path[] testNGSupportProgramFiles() {
-    return new Path[] {
-      Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar"),
-      Paths.get(JDK_TESTS_BUILD_DIR + "jcommander-1.48.jar"),
-      Paths.get(ToolHelper.JAVA_CLASSES_DIR + "examplesTestNGRunner/TestNGMainRunner.class")
-    };
+    return new Path[] {testNGPath(), jcommanderPath()};
+  }
+
+  public static Path testNGPath() {
+    return Paths.get(ToolHelper.DEPENDENCIES + "org/testng/testng/6.10/testng-6.10.jar");
+  }
+
+  public static Path jcommanderPath() {
+    return Paths.get(ToolHelper.DEPENDENCIES + "com/beust/jcommander/1.48/jcommander-1.48.jar");
+  }
+
+  public static byte[] getTestNGMainRunner() throws Exception {
+    return transformer(TestNGMainRunner.class)
+        .setClassDescriptor("LTestNGMainRunner;")
+        .replaceClassDescriptorInMethodInstructions(descriptor(TestNG.class), "Lorg/testng/TestNG;")
+        .replaceClassDescriptorInMethodInstructions(
+            descriptor(TextReporter.class), "Lorg/testng/reporters/TextReporter;")
+        .transform();
+  }
+
+  /** TestNGMainRunner used as the test runner in JDK11 tests. */
+  public static class TestNGMainRunner {
+
+    private static void runTestNg(Class<?> testClass, int verbose) {
+      System.out.println("Running tests in " + testClass.getName());
+      TestNG testng = new TestNG(false);
+      testng.setTestClasses(new Class<?>[] {testClass});
+      testng.setVerbose(verbose);
+      // Deprecated API used because it works on Android unlike the recommended one.
+      testng.addListener(new TextReporter(testClass.getName(), verbose));
+      try {
+        testng.run();
+        System.out.print("Tests result in " + testClass.getName() + ": ");
+        if (testng.hasFailure()) {
+          System.out.println("FAILURE");
+        } else {
+          System.out.println("SUCCESS");
+        }
+      } catch (RuntimeException | Error e) {
+        System.out.print("Tests result in " + testClass.getName() + ": ");
+        System.out.println("ERROR");
+        e.printStackTrace();
+      }
+    }
+
+    public static void main(String[] args) throws Exception {
+      // First arg is the verbosity level.
+      // Second arg is the class to run.
+      int verbose = Integer.parseInt(args[0]);
+      runTestNg(Class.forName(args[1]), verbose);
+    }
+  }
+
+  /** Stubs for the TestNGRunner */
+  public static class TextReporter {
+
+    public TextReporter(String name, int verbose) {}
+  }
+
+  public static class TestNG {
+
+    public TestNG(boolean val) {}
+
+    public void setTestClasses(Class<?>[] classes) {}
+
+    public void setVerbose(int verbose) {}
+
+    public void addListener(Object textReporter) {}
+
+    public void run() {}
+
+    public boolean hasFailure() {
+      return false;
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TestLibraryDesugaringSpecification.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TestLibraryDesugaringSpecification.java
index 7a2760a..f909be9 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TestLibraryDesugaringSpecification.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TestLibraryDesugaringSpecification.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
 import static com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase.getAllFilesWithSuffixInDirectory;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK8;
@@ -98,8 +98,7 @@
             "java.base=" + JDK_11_JAVA_BASE_EXTENSION_FILES_DIR);
     JavaCompilerTool.create(TestRuntime.getCheckedInJdk11(), folder)
         .addOptions(options)
-        .addClasspathFiles(
-            Collections.singletonList(Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar")))
+        .addClasspathFiles(testNGPath())
         .addSourceFiles(getExtensionsFiles())
         .setOutputPath(output)
         .compile();
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TimeAbstractTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TimeAbstractTests.java
index a2c95c1..f59341e 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TimeAbstractTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11TimeAbstractTests.java
@@ -4,8 +4,10 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
 
-import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getPathsFiles;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.getTestNGMainRunner;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.jcommanderPath;
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGPath;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11SupportFiles.testNGSupportProgramFiles;
 import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11TestLibraryDesugaringSpecification.EXTENSION_PATH;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
@@ -108,10 +110,7 @@
             "java.base=" + EXTENSION_PATH);
     javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
         .addOptions(options)
-        .addClasspathFiles(
-            ImmutableList.of(
-                Paths.get(JDK_TESTS_BUILD_DIR + "testng-6.10.jar"),
-                Paths.get(JDK_TESTS_BUILD_DIR + "jcommander-1.48.jar")))
+        .addClasspathFiles(testNGPath(), jcommanderPath())
         .addSourceFiles(getJdk11TimeTestFiles())
         .setOutputPath(tmpDirectory)
         .compile();
@@ -261,6 +260,7 @@
                 parameters, libraryDesugaringSpecification, compilationSpecification)
             .addProgramFiles(JDK_11_TIME_TEST_COMPILED_FILES)
             .addProgramFiles(testNGSupportProgramFiles())
+            .addProgramClassFileData(getTestNGMainRunner())
             .applyIf(
                 !libraryDesugaringSpecification.hasNioFileDesugaring(parameters),
                 b -> b.addProgramFiles(getPathsFiles()))
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
index 2cf1e98..c871859 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
@@ -38,6 +38,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -182,6 +183,10 @@
     return this;
   }
 
+  public DesugaredLibraryTestBuilder<T> addProgramClassFileData(byte[]... classes) {
+    return addProgramClassFileData(Arrays.asList(classes));
+  }
+
   public DesugaredLibraryTestBuilder<T> addProgramClassFileData(
       Collection<byte[]> programClassFileData) {
     builder.addProgramClassFileData(programClassFileData);
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumImplementsTest.java
similarity index 61%
copy from src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
copy to src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumImplementsTest.java
index 997e4fb..b462df9 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesEnumImplementsTest.java
@@ -5,12 +5,13 @@
 package com.android.tools.r8.desugar.sealed;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertTrue;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.DesugarTestConfiguration;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestBuilder;
 import com.android.tools.r8.TestParameters;
@@ -20,7 +21,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,7 +29,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class SealedClassesTest extends TestBase {
+public class SealedClassesEnumImplementsTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
@@ -37,22 +37,18 @@
   @Parameter(1)
   public boolean keepPermittedSubclassesAttribute;
 
-  @Parameter(2)
-  public boolean repackage;
+  static final String EXPECTED = StringUtils.lines("0", "1", "true");
 
-  static final String EXPECTED = StringUtils.lines("Success!");
-
-  @Parameters(name = "{0}, keepPermittedSubclasses = {1}, repackage = {2}")
+  @Parameters(name = "{0}, keepPermittedSubclasses = {1}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
-        BooleanUtils.values(),
         BooleanUtils.values());
   }
 
   private void addTestClasses(TestBuilder<?, ?> builder) throws Exception {
     builder
-        .addProgramClasses(TestClass.class, Sub1.class, Sub2.class)
+        .addProgramClasses(TestClass.class, Enum.class)
         .addProgramClassFileData(getTransformedClasses());
   }
 
@@ -82,22 +78,9 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz(Super.class);
-    assertThat(clazz, isPresentAndRenamed());
-    ClassSubject sub1 = inspector.clazz(Sub1.class);
-    ClassSubject sub2 = inspector.clazz(Sub2.class);
-    assertThat(sub1, isPresentAndRenamed());
-    assertThat(sub2, isPresentAndRenamed());
-    if (repackage) {
-      assertEquals(-1, sub1.getFinalName().indexOf('.'));
-    } else {
-      assertTrue(sub1.getFinalName().startsWith(getClass().getPackage().getName()));
-    }
-    assertEquals(
-        parameters.isCfRuntime() && keepPermittedSubclassesAttribute
-            ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
-            : ImmutableList.of(),
-        clazz.getFinalPermittedSubclassAttributes());
+    ClassSubject iface1 = inspector.clazz(Iface.class);
+    assertThat(iface1, isPresentAndRenamed());
+    assertTrue(iface1.getFinalPermittedSubclassAttributes().isEmpty());
   }
 
   @Test
@@ -109,37 +92,42 @@
         .applyIf(
             keepPermittedSubclassesAttribute,
             TestShrinkerBuilder::addKeepAttributePermittedSubclasses)
-        // Keep the sealed class to ensure the PermittedSubclasses attribute stays live.
-        .addKeepPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        // Keep the sealed interface to ensure the PermittedSubclasses attribute stays live.
+        .addKeepPermittedSubclasses(Iface.class)
         .addKeepMainRule(TestClass.class)
-        .applyIf(repackage, b -> b.addKeepRules("-repackageclasses"))
+        .addEnumUnboxingInspector(
+            inspector -> {
+              inspector.assertUnboxed(Enum.class);
+            })
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
         .compile()
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
-        .applyIf(
-            !parameters.isCfRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
-            r -> r.assertSuccessWithOutput(EXPECTED),
-            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+        // The Iface interface which still has class file version 61 is never accessed, so no
+        // UnsupportedClassVersionError is thrown.
+        .assertSuccessWithOutput(EXPECTED);
   }
 
   public byte[] getTransformedClasses() throws Exception {
-    return transformer(Super.class)
-        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
-        .transform();
+    return transformer(Iface.class).setPermittedSubclasses(Iface.class, Enum.class).transform();
   }
 
   static class TestClass {
 
     public static void main(String[] args) {
-      new Sub1();
-      new Sub2();
-      System.out.println("Success!");
+      System.out.println(Enum.A.ordinal());
+      System.out.println(Enum.B.ordinal());
+      System.out.println(Enum.A instanceof Iface);
     }
   }
 
-  public abstract static class Super /* permits Sub1, Sub2 */ {}
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface Iface /* permits Enum */ {}
 
-  public static class Sub1 extends Super {}
-
-  public static class Sub2 extends Super {}
+  enum Enum implements Iface {
+    A,
+    B
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsTest.java
similarity index 98%
rename from src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
rename to src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsTest.java
index 997e4fb..08fde0f 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsTest.java
@@ -29,7 +29,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class SealedClassesTest extends TestBase {
+public class SealedClassesExtendsTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
index fdd28aa..c833f8a 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesExtendsVerticalMergeTest.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
-import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.TestBase;
@@ -19,6 +18,7 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import com.google.common.collect.ImmutableList;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -45,16 +45,21 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz(Super.class);
-    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject superClass = inspector.clazz(Super.class);
+    assertThat(superClass, isPresentAndRenamed());
     ClassSubject sub2 = inspector.clazz(Sub2.class);
     assertThat(sub2, isPresentAndRenamed());
     ClassSubject subSub = inspector.clazz(SubSub.class);
     assertThat(subSub, isPresentAndRenamed());
-    // TODO(b/227160052): Should be both subSub.asTypeSubject() and sub2.asTypeSubject().
-    assertEquals(
-        parameters.isCfRuntime() ? ImmutableList.of(sub2.asTypeSubject()) : ImmutableList.of(),
-        clazz.getFinalPermittedSubclassAttributes());
+    ClassSubject unrelated = inspector.clazz(UnrelatedSuper.class);
+    assertThat(subSub, isPresentAndRenamed());
+    for (ClassSubject clazz : ImmutableList.of(superClass, unrelated)) {
+      assertEquals(
+          parameters.isCfRuntime()
+              ? ImmutableList.of(subSub.asTypeSubject(), sub2.asTypeSubject())
+              : ImmutableList.of(),
+          clazz.getFinalPermittedSubclassAttributes());
+    }
   }
 
   @Test
@@ -64,8 +69,7 @@
         .apply(this::addTestClasses)
         .setMinApi(parameters)
         .addKeepAttributePermittedSubclasses()
-        .addKeepPermittedSubclasses(Super.class)
-        .addKeepPermittedSubclasses(Sub2.class)
+        .addKeepPermittedSubclasses(Super.class, Sub2.class, UnrelatedSuper.class)
         .addKeepMainRule(TestClass.class)
         .addVerticallyMergedClassesInspector(
             inspector -> {
@@ -77,19 +81,19 @@
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
         .applyIf(
-            parameters.isDexRuntime(),
+            parameters.isDexRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
             r -> r.assertSuccessWithOutput(EXPECTED),
-            parameters.isCfRuntime() && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
-            r ->
-                r.assertFailureWithErrorThatMatches(
-                    containsString("cannot inherit from sealed class")),
             r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
   }
 
-  public byte[] getTransformedClasses() throws Exception {
-    return transformer(Super.class)
-        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
-        .transform();
+  public List<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        transformer(Super.class)
+            .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+            .transform(),
+        transformer(UnrelatedSuper.class)
+            .setPermittedSubclasses(UnrelatedSuper.class, Sub1.class, Sub2.class)
+            .transform());
   }
 
   static class TestClass {
@@ -107,4 +111,6 @@
   static class Sub2 extends Super {}
 
   static class SubSub extends Sub1 {}
+
+  abstract static class UnrelatedSuper /* permits Sub1, Sub2 */ {}
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsTest.java
similarity index 70%
copy from src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
copy to src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsTest.java
index 997e4fb..ac61cf8 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsTest.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertTrue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
 
@@ -20,6 +19,8 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
@@ -29,7 +30,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class SealedClassesTest extends TestBase {
+public class SealedClassesImplementsTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
@@ -37,16 +38,12 @@
   @Parameter(1)
   public boolean keepPermittedSubclassesAttribute;
 
-  @Parameter(2)
-  public boolean repackage;
-
   static final String EXPECTED = StringUtils.lines("Success!");
 
-  @Parameters(name = "{0}, keepPermittedSubclasses = {1}, repackage = {2}")
+  @Parameters(name = "{0}, keepPermittedSubclasses = {1}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
-        BooleanUtils.values(),
         BooleanUtils.values());
   }
 
@@ -82,22 +79,24 @@
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject clazz = inspector.clazz(Super.class);
-    assertThat(clazz, isPresentAndRenamed());
+    ClassSubject iface1 = inspector.clazz(Iface1.class);
+    assertThat(iface1, isPresentAndRenamed());
+    ClassSubject iface2 = inspector.clazz(Iface2.class);
+    assertThat(iface2, isPresentAndRenamed());
     ClassSubject sub1 = inspector.clazz(Sub1.class);
     ClassSubject sub2 = inspector.clazz(Sub2.class);
     assertThat(sub1, isPresentAndRenamed());
     assertThat(sub2, isPresentAndRenamed());
-    if (repackage) {
-      assertEquals(-1, sub1.getFinalName().indexOf('.'));
-    } else {
-      assertTrue(sub1.getFinalName().startsWith(getClass().getPackage().getName()));
-    }
     assertEquals(
         parameters.isCfRuntime() && keepPermittedSubclassesAttribute
             ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
             : ImmutableList.of(),
-        clazz.getFinalPermittedSubclassAttributes());
+        iface1.getFinalPermittedSubclassAttributes());
+    assertEquals(
+        parameters.isCfRuntime() && keepPermittedSubclassesAttribute
+            ? ImmutableList.of(sub1.asTypeSubject(), sub2.asTypeSubject())
+            : ImmutableList.of(),
+        iface2.getFinalPermittedSubclassAttributes());
   }
 
   @Test
@@ -109,10 +108,13 @@
         .applyIf(
             keepPermittedSubclassesAttribute,
             TestShrinkerBuilder::addKeepAttributePermittedSubclasses)
-        // Keep the sealed class to ensure the PermittedSubclasses attribute stays live.
-        .addKeepPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
+        // Keep the sealed interface to ensure the PermittedSubclasses attribute stays live.
+        .addKeepPermittedSubclasses(Iface1.class, Iface2.class, Sub1.class, Sub2.class)
         .addKeepMainRule(TestClass.class)
-        .applyIf(repackage, b -> b.addKeepRules("-repackageclasses"))
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
         .compile()
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
@@ -122,10 +124,14 @@
             r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
   }
 
-  public byte[] getTransformedClasses() throws Exception {
-    return transformer(Super.class)
-        .setPermittedSubclasses(Super.class, Sub1.class, Sub2.class)
-        .transform();
+  public List<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        transformer(Iface1.class)
+            .setPermittedSubclasses(Iface1.class, Sub1.class, Sub2.class)
+            .transform(),
+        transformer(Iface2.class)
+            .setPermittedSubclasses(Iface2.class, Sub1.class, Sub2.class)
+            .transform());
   }
 
   static class TestClass {
@@ -137,9 +143,11 @@
     }
   }
 
-  public abstract static class Super /* permits Sub1, Sub2 */ {}
+  interface Iface1 /* permits Sub1, Sub2 */ {}
 
-  public static class Sub1 extends Super {}
+  interface Iface2 /* permits Sub1, Sub2 */ {}
 
-  public static class Sub2 extends Super {}
+  static class Sub1 implements Iface1, Iface2 {}
+
+  static class Sub2 implements Iface1 {}
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
index 43f4a41..3f218f0 100644
--- a/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedClassesImplementsVerticalMergeTest.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static junit.framework.Assert.assertEquals;
-import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
@@ -52,14 +51,17 @@
     assertThat(iface1, isPresentAndRenamed());
     ClassSubject iface2 = inspector.clazz(Iface2.class);
     assertThat(iface2, isPresentAndRenamed());
+    ClassSubject ifaceUnrelated = inspector.clazz(IfaceUnrelated.class);
+    assertThat(ifaceUnrelated, isPresentAndRenamed());
     ClassSubject sub2 = inspector.clazz(Sub2.class);
     assertThat(sub2, isPresentAndRenamed());
     ClassSubject subSub = inspector.clazz(SubSub.class);
     assertThat(subSub, Matchers.isPresentAndRenamed());
-    for (ClassSubject clazz : ImmutableList.of(iface1, iface2)) {
+    for (ClassSubject clazz : ImmutableList.of(iface1, iface2, ifaceUnrelated)) {
       assertEquals(
-          // TODO(b/227160052): Should be both subSub.asTypeSubject() and sub2.asTypeSubject().
-          parameters.isCfRuntime() ? ImmutableList.of(sub2.asTypeSubject()) : ImmutableList.of(),
+          parameters.isCfRuntime()
+              ? ImmutableList.of(subSub.asTypeSubject(), sub2.asTypeSubject())
+              : ImmutableList.of(),
           clazz.getFinalPermittedSubclassAttributes());
     }
   }
@@ -71,7 +73,8 @@
         .apply(this::addTestClasses)
         .setMinApi(parameters)
         .addKeepAttributePermittedSubclasses()
-        .addKeepPermittedSubclasses(Super.class, Iface1.class, Iface2.class, Sub2.class)
+        .addKeepPermittedSubclasses(
+            Super.class, Iface1.class, Iface2.class, Sub2.class, IfaceUnrelated.class)
         .addKeepMainRule(TestClass.class)
         .addVerticallyMergedClassesInspector(
             inspector -> {
@@ -84,12 +87,8 @@
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
         .applyIf(
-            parameters.isDexRuntime(),
+            parameters.isDexRuntime() || parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
             r -> r.assertSuccessWithOutput(EXPECTED),
-            parameters.isCfRuntime() && parameters.asCfRuntime().isNewerThanOrEqual(CfVm.JDK17),
-            r ->
-                r.assertFailureWithErrorThatMatches(
-                    containsString("cannot implement sealed interface")),
             r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
   }
 
@@ -100,6 +99,9 @@
             .transform(),
         transformer(Iface2.class)
             .setPermittedSubclasses(Iface2.class, Sub1.class, Sub2.class)
+            .transform(),
+        transformer(IfaceUnrelated.class)
+            .setPermittedSubclasses(IfaceUnrelated.class, Sub1.class, Sub2.class)
             .transform());
   }
 
@@ -117,6 +119,9 @@
   @NoUnusedInterfaceRemoval
   interface Iface2 /* permits Sub1, Sub2 */ {}
 
+  @NoUnusedInterfaceRemoval
+  interface IfaceUnrelated /* permits Sub1, Sub2 */ {}
+
   abstract static class Super {}
 
   static class Sub1 extends Super implements Iface1, Iface2 {}
diff --git a/src/test/java/com/android/tools/r8/ir/InlineTest.java b/src/test/java/com/android/tools/r8/ir/InlineTest.java
index 9100c82..9d1a5e8 100644
--- a/src/test/java/com/android/tools/r8/ir/InlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/InlineTest.java
@@ -48,6 +48,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
 import org.antlr.runtime.RecognitionException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,7 +71,7 @@
       DexApplication application,
       InternalOptions options,
       MethodSubject method,
-      List<IRCode> additionalCode)
+      List<Function<AppView<?>, IRCode>> additionalCode)
       throws ExecutionException {
     // Some tests play fast and loose with IR and the SSA value numbers are not generally unique.
     if (additionalCode != null && !additionalCode.isEmpty()) {
@@ -161,10 +162,10 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     MethodSubject methodASubject = getMethodSubject(application, signatureA);
-    IRCode codeA = methodASubject.buildIR();
+    Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
 
     MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-    IRCode codeB = methodBSubject.buildIR();
+    Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
 
     return buildTestApplication(
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
@@ -243,7 +244,7 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     MethodSubject methodASubject = getMethodSubject(application, signatureA);
-    IRCode codeA = methodASubject.buildIR();
+    Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
 
     return buildTestApplication(application, options, methodSubject, ImmutableList.of(codeA));
   }
@@ -320,16 +321,16 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     // Build three copies of a and b for inlining three times.
-    List<IRCode> additionalCode = new ArrayList<>();
+    List<Function<AppView<?>, IRCode>> additionalCode = new ArrayList<>();
     for (int i = 0; i < 3; i++) {
       MethodSubject methodASubject = getMethodSubject(application, signatureA);
-      IRCode codeA = methodASubject.buildIR();
+      Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
       additionalCode.add(codeA);
     }
 
     for (int i = 0; i < 3; i++) {
       MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-      IRCode codeB = methodBSubject.buildIR();
+      Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
       additionalCode.add(codeB);
     }
 
@@ -453,10 +454,10 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     MethodSubject methodASubject = getMethodSubject(application, signatureA);
-    IRCode codeA = methodASubject.buildIR();
+    Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
 
     MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-    IRCode codeB = methodBSubject.buildIR();
+    Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
 
     return buildTestApplication(
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
@@ -579,10 +580,10 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     MethodSubject methodASubject = getMethodSubject(application, signatureA);
-    IRCode codeA = methodASubject.buildIR();
+    Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
 
     MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-    IRCode codeB = methodBSubject.buildIR();
+    Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
 
     return buildTestApplication(
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
@@ -690,10 +691,10 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     MethodSubject methodASubject = getMethodSubject(application, signatureA);
-    IRCode codeA = methodASubject.buildIR();
+    Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
 
     MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-    IRCode codeB = methodBSubject.buildIR();
+    Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
 
     return buildTestApplication(
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
@@ -803,16 +804,16 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     // Build three copies of a and b for inlining three times.
-    List<IRCode> additionalCode = new ArrayList<>();
+    List<Function<AppView<?>, IRCode>> additionalCode = new ArrayList<>();
     for (int i = 0; i < 3; i++) {
       MethodSubject methodASubject = getMethodSubject(application, signatureA);
-      IRCode codeA = methodASubject.buildIR();
+      Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
       additionalCode.add(codeA);
     }
 
     for (int i = 0; i < 3; i++) {
       MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-      IRCode codeB = methodBSubject.buildIR();
+      Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
       additionalCode.add(codeB);
     }
 
@@ -959,16 +960,16 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     // Build three copies of a and b for inlining three times.
-    List<IRCode> additionalCode = new ArrayList<>();
+    List<Function<AppView<?>, IRCode>> additionalCode = new ArrayList<>();
     for (int i = 0; i < 3; i++) {
       MethodSubject methodASubject = getMethodSubject(application, signatureA);
-      IRCode codeA = methodASubject.buildIR();
+      Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
       additionalCode.add(codeA);
     }
 
     for (int i = 0; i < 3; i++) {
       MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-      IRCode codeB = methodBSubject.buildIR();
+      Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
       additionalCode.add(codeB);
     }
 
@@ -1205,10 +1206,10 @@
     MethodSubject methodSubject = getMethodSubject(application, signature);
 
     MethodSubject methodASubject = getMethodSubject(application, signatureA);
-    IRCode codeA = methodASubject.buildIR();
+    Function<AppView<?>, IRCode> codeA = appView -> methodASubject.buildIR(appView);
 
     MethodSubject methodBSubject = getMethodSubject(application, signatureB);
-    IRCode codeB = methodBSubject.buildIR();
+    Function<AppView<?>, IRCode> codeB = appView -> methodBSubject.buildIR(appView);
 
     return buildTestApplication(
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
diff --git a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
index 3143cb0..d835003 100644
--- a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
@@ -15,17 +15,21 @@
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
 import com.android.tools.r8.smali.SmaliTestBase;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 
 public class IrInjectionTestBase extends SmaliTestBase {
 
@@ -61,16 +65,23 @@
     public final NumberGenerator valueNumberGenerator = new NumberGenerator();
 
     public TestApplication(AppView<?> appView, MethodSubject method) {
-      this(appView, method, null);
+      this(appView, method, ImmutableList.of());
     }
 
-    public TestApplication(AppView<?> appView, MethodSubject method, List<IRCode> additionalCode) {
+    public TestApplication(
+        AppView<?> appView,
+        MethodSubject method,
+        List<Function<AppView<?>, IRCode>> additionalCode) {
       this.application = appView.appInfo().app();
       this.appView = appView;
       this.method = method.getMethod();
-      this.code = method.buildIR();
+      appView.testing().skipLirPhasesForTestingFinalOutput();
+      this.code =
+          method
+              .getProgramMethod()
+              .buildIR(appView, MethodConversionOptions.forPostLirPhase(appView));
       code.removeRedundantBlocks();
-      this.additionalCode = additionalCode;
+      this.additionalCode = ListUtils.map(additionalCode, fn -> fn.apply(appView));
       this.consumers = new AndroidAppConsumers(appView.options());
       int largestValueNumber = -1;
       for (BasicBlock block : code.blocks) {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
index 1769dc9..a1d31af 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
@@ -72,7 +72,7 @@
             buildClasses(classes).addLibraryFile(getMostRecentAndroidJar()).build());
     CodeInspector codeInspector = new CodeInspector(appView.appInfo().app());
     MethodSubject fooSubject = codeInspector.clazz(mainClass.getName()).method(signature);
-    IRCode irCode = fooSubject.buildIR();
+    IRCode irCode = fooSubject.buildIR(appView);
     new AssumeInserter(appView).insertAssumeInstructions(irCode, Timing.empty());
     inspector.accept(appView, irCode);
     verifyLastInvoke(irCode, npeCaught);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
index f041526..bb65fdc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
@@ -59,7 +59,7 @@
             buildClasses(classes).addLibraryFile(getMostRecentAndroidJar()).build());
     CodeInspector codeInspector = new CodeInspector(appView.appInfo().app());
     MethodSubject fooSubject = codeInspector.clazz(testClass.getName()).method(signature);
-    IRCode code = fooSubject.buildIR();
+    IRCode code = fooSubject.buildIR(appView);
     checkCountOfNonNull(code, 0);
 
     AssumeInserter assumeInserter = new AssumeInserter(appView);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/sync/InlineStaticSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/sync/InlineStaticSynchronizedMethodTest.java
index 482ed74..211bad5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/sync/InlineStaticSynchronizedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/sync/InlineStaticSynchronizedMethodTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -58,9 +59,10 @@
     // vms. See issue b/238399429 for details.
     if (parameters.isCfRuntime()
         || parameters.getApiLevel().isLessThanOrEqualTo(AndroidApiLevel.M)) {
-      assertThat(classSubject.uniqueMethodWithOriginalName("m1"), isPresent());
-      assertThat(classSubject.uniqueMethodWithOriginalName("m2"), not(isPresent()));
-
+      int remaining = 0;
+      remaining += classSubject.uniqueMethodWithOriginalName("m1").isPresent() ? 1 : 0;
+      remaining += classSubject.uniqueMethodWithOriginalName("m2").isPresent() ? 1 : 0;
+      assertEquals("Expected only one of m1 and m2 to be inlined", 1, remaining);
     } else {
       assertThat(classSubject.uniqueMethodWithOriginalName("m1"), not(isPresent()));
       assertThat(classSubject.uniqueMethodWithOriginalName("m2"), not(isPresent()));
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 c8859fe..e322f8d 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
@@ -285,7 +285,7 @@
             .run(parameters.getRuntime(), MAIN)
             // TODO(b/154813140): Invalidly assumes that getClass on kept classes can be optimized.
             .assertSuccessWithOutputThatMatches(not(equalTo(JAVA_OUTPUT)));
-    test(result, 8);
+    test(result, 6);
   }
 
   @Test
@@ -316,6 +316,6 @@
     } else {
       result.assertSuccessWithOutputThatMatches(not(equalTo(JAVA_OUTPUT)));
     }
-    test(result, 8);
+    test(result, 6);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index 82ef602..5c3fe96 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -204,8 +204,8 @@
 
     assertEquals(
         Lists.newArrayList(
-            "STATIC: String SimpleWithLazyInit.bar$1(String)",
-            "STATIC: String SimpleWithLazyInit.foo$1()",
+            "STATIC: String SimpleWithLazyInit.bar(String)",
+            "STATIC: String SimpleWithLazyInit.foo()",
             "STATIC: String TrivialTestClass.next()"),
         references(clazz, "testSimpleWithThrowingGetter", "void"));
 
@@ -222,8 +222,8 @@
     }
     Collections.addAll(
         expectedReferencesInTestSimpleWithLazyInit,
-        "STATIC: String SimpleWithLazyInit.bar(String)",
-        "STATIC: String SimpleWithLazyInit.foo()",
+        "STATIC: String SimpleWithLazyInit.bar$1(String)",
+        "STATIC: String SimpleWithLazyInit.foo$1()",
         "STATIC: String TrivialTestClass.next()",
         "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
         "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
@@ -298,6 +298,7 @@
             .allowAccessModification()
             .addDontObfuscate()
             .addOptionsModification(this::configure)
+            .addOptionsModification(o -> o.testing.enableLir())
             .setMinApi(parameters)
             .run(parameters.getRuntime(), main)
             .assertSuccessWithOutput(javaOutput);
@@ -340,11 +341,6 @@
     assertThat(inspector.clazz(CandidateConflictMethod.class), isPresent());
 
     List<String> expectedReferencesInTestConflictField = new ArrayList<>();
-    if (!parameters.canHaveNonReboundConstructorInvoke()) {
-      Collections.addAll(
-          expectedReferencesInTestConflictField,
-          "DIRECT: void movetohost.HostConflictField.<init>()");
-    }
     Collections.addAll(
         expectedReferencesInTestConflictField,
         "STATIC: String movetohost.CandidateConflictField.bar(String)",
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialKotlinStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialKotlinStyleTest.java
index 580b42c..4896ebd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialKotlinStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialKotlinStyleTest.java
@@ -93,23 +93,17 @@
     assertEquals(0, lambdasInInput.getNumberOfJStyleLambdas());
     assertEquals(28, lambdasInInput.getNumberOfKStyleLambdas());
 
-    if (!parameters.isAccessModificationEnabled(allowAccessModification)) {
-      // Only a subset of all K-style Kotlin lambdas are merged without -allowaccessmodification.
-      Set<ClassReference> unmergedLambdas =
-          ImmutableSet.of(
-              lambdasInInput.getKStyleLambdaReferenceFromTypeName(
-                  getTestName(), "inner.InnerKt$testInnerStateless$7"));
-      inspector
-          .assertClassReferencesMerged(
-              lambdasInInput.getKStyleLambdas().stream()
-                  .filter(not(unmergedLambdas::contains))
-                  .collect(Collectors.toList()))
-          .assertClassReferencesNotMerged(unmergedLambdas);
-      return;
-    }
-
-    // All K-style Kotlin lambdas are merged with -allowaccessmodification.
-    inspector.assertClassReferencesMerged(lambdasInInput.getKStyleLambdas());
+    // Only a subset of all K-style Kotlin lambdas are merged.
+    Set<ClassReference> unmergedLambdas =
+        ImmutableSet.of(
+            lambdasInInput.getKStyleLambdaReferenceFromTypeName(
+                getTestName(), "inner.InnerKt$testInnerStateless$7"));
+    inspector
+        .assertClassReferencesMerged(
+            lambdasInInput.getKStyleLambdas().stream()
+                .filter(not(unmergedLambdas::contains))
+                .collect(Collectors.toList()))
+        .assertClassReferencesNotMerged(unmergedLambdas);
   }
 
   private String getExpectedOutput() {
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDuplicateOutlineTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDuplicateOutlineTest.java
new file mode 100644
index 0000000..5c15d3d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDuplicateOutlineTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.mappingcompose;
+
+import static com.android.tools.r8.mappingcompose.ComposeTestHelpers.doubleToSingleQuote;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.MappingComposer;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/* This is a regression test for b/289788048 */
+public class ComposeDuplicateOutlineTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "package.Class$$ExternalSyntheticOutline0 -> package.internal.X:",
+          "# {'id':'sourceFile','fileName':'R8$$SyntheticClass'}",
+          "# {'id':'com.android.tools.r8.synthesized'}",
+          "    1:2:long package.Int2IntLinkedOpenHashMap$$InternalSyntheticOutline$HASH$0"
+              + ".m(long,long,long):0:1"
+              + " -> a",
+          "      # {'id':'com.android.tools.r8.synthesized'}",
+          "      # {'id':'com.android.tools.r8.outline'}",
+          "package.Class -> package.internal.Y:",
+          "# {'id':'sourceFile','fileName':'FieldDefinition.java'}",
+          "    1:1:void foo():21:21 -> a",
+          "    # {'id':'com.android.tools.r8.outlineCallsite',"
+              + "'positions':{'1':2,'2':3},"
+              + "'outline':'Lpackage/internal/X;a(JJJ)J'}",
+          "    2:2:void foo():1337:1337 -> a",
+          "    3:3:void foo():44:44 -> a");
+  private static final String mappingBar =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "package.internal.Y -> package.new_internal.Y:",
+          // If there are multiple throwing instructions for a line number, we can get multiple
+          // mappings in D8.
+          "    1:10:void a():1:1 -> b",
+          "    11:20:void a():1:1 -> b");
+  private static final String mappingResult =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "package.Class -> package.new_internal.Y:",
+          "# {'id':'sourceFile','fileName':'FieldDefinition.java'}",
+          "    1:10:void foo():21:21 -> b",
+          "    # {'id':'com.android.tools.r8.outlineCallsite',"
+              + "'positions':{'1':21,'2':22},"
+              + "'outline':'Lpackage/internal/X;a(JJJ)J'}",
+          "    11:20:void foo():21:21 -> b",
+          "    # {'id':'com.android.tools.r8.outlineCallsite',"
+              + "'positions':{'1':21,'2':22},"
+              + "'outline':'Lpackage/internal/X;a(JJJ)J'}",
+          "    21:21:void foo():1337:1337 -> b",
+          "    22:22:void foo():44:44 -> b",
+          "package.Class$$ExternalSyntheticOutline0 -> package.internal.X:",
+          "# {'id':'sourceFile','fileName':'R8$$SyntheticClass'}",
+          "# {'id':'com.android.tools.r8.synthesized'}",
+          "    1:2:long package.Int2IntLinkedOpenHashMap"
+              + "$$InternalSyntheticOutline$HASH$0.m(long,long,long):0:1 -> a",
+          "    # {'id':'com.android.tools.r8.synthesized'}",
+          "    # {'id':'com.android.tools.r8.outline'}");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, doubleToSingleQuote(composed));
+  }
+}
diff --git a/src/test/testngrunner/TestNGMainRunner.java b/src/test/testngrunner/TestNGMainRunner.java
deleted file mode 100644
index 22d96ef..0000000
--- a/src/test/testngrunner/TestNGMainRunner.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) 2019, 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.
-
-// This class is kept in a separate package because of the org.testng dependency.
-// R8 tests do not depend on testng, this class does.
-public class TestNGMainRunner {
-
-  private static void runTestNg(Class<?> testClass, int verbose) {
-    System.out.println("Running tests in " + testClass.getName());
-    org.testng.TestNG testng = new org.testng.TestNG(false);
-    testng.setTestClasses(new Class<?>[] {testClass});
-    testng.setVerbose(verbose);
-    // Deprecated API used because it works on Android unlike the recommended one.
-    testng.addListener(new org.testng.reporters.TextReporter(testClass.getName(), verbose));
-    try {
-      testng.run();
-      System.out.print("Tests result in " + testClass.getName() + ": ");
-      if (testng.hasFailure()) {
-        System.out.println("FAILURE");
-      } else {
-        System.out.println("SUCCESS");
-      }
-    } catch (RuntimeException | Error e) {
-      System.out.print("Tests result in " + testClass.getName() + ": ");
-      System.out.println("ERROR");
-      e.printStackTrace();
-    }
-  }
-
-  public static void main(String[] args) throws Exception {
-    // First arg is the verbosity level.
-    // Second arg is the class to run.
-    int verbose = Integer.parseInt(args[0]);
-    runTestNg(Class.forName(args[1]), verbose);
-  }
-}
diff --git a/tools/internal_test.py b/tools/internal_test.py
index a726e1e..cab2d60 100755
--- a/tools/internal_test.py
+++ b/tools/internal_test.py
@@ -62,6 +62,8 @@
 
 BENCHMARK_APPS = [chrome_data, iosched_data, r8_data, youtube_data]
 
+DEPENDENT_PYTHON_FILES = [gradle, utils, run_on_app]
+
 def find_min_xmx_command(app_data):
   record = app_data.GetMemoryData(app_data.GetLatestVersion())
   assert record['find-xmx-min'] < record['find-xmx-max']
@@ -150,8 +152,8 @@
   contents = []
   with open(sys.argv[0], 'r') as us:
     contents.append(us.read())
-  for app_data in BENCHMARK_APPS:
-    with open(app_data.__file__, 'r') as us:
+  for deps in BENCHMARK_APPS + DEPENDENT_PYTHON_FILES:
+    with open(deps.__file__, 'r') as us:
       contents.append(us.read())
   return contents